UNPKG

8.42 kBJavaScriptView Raw
1// @flow
2'use strict'
3const parse = require('format-message-parse')
4const interpret = require('format-message-interpret')
5const plurals = require('format-message-interpret/plurals')
6const lookupClosestLocale = require('lookup-closest-locale')
7const origFormats = require('format-message-formats')
8
9/*::
10import type { Types } from 'format-message-interpret'
11type Locale = string
12type Locales = Locale | Locale[]
13type Message = string | {|
14 id?: string,
15 default: string,
16 description?: string
17|}
18type Translations = { [string]: ?{ [string]: string | Translation } }
19type Translation = {
20 message: string,
21 format?: (args?: Object) => string,
22 toParts?: (args?: Object) => any[],
23}
24type Replacement = ?string | (string, string, locales?: Locales) => ?string
25type GenerateId = (string) => string
26type MissingTranslation = 'ignore' | 'warning' | 'error'
27type FormatObject = { [string]: * }
28type Options = {
29 locale?: Locales,
30 translations?: ?Translations,
31 generateId?: GenerateId,
32 missingReplacement?: Replacement,
33 missingTranslation?: MissingTranslation,
34 formats?: {
35 number?: FormatObject,
36 date?: FormatObject,
37 time?: FormatObject
38 },
39 types?: Types
40}
41type Setup = {|
42 locale: Locales,
43 translations: Translations,
44 generateId: GenerateId,
45 missingReplacement: Replacement,
46 missingTranslation: MissingTranslation,
47 formats: {
48 number: FormatObject,
49 date: FormatObject,
50 time: FormatObject
51 },
52 types: Types
53|}
54type FormatMessage = {
55 (msg: Message, args?: Object, locales?: Locales): string,
56 rich (msg: Message, args?: Object, locales?: Locales): any[],
57 setup (opt?: Options): Setup,
58 number (value: number, style?: string, locales?: Locales): string,
59 date (value: number | Date, style?: string, locales?: Locales): string,
60 time (value: number | Date, style?: string, locales?: Locales): string,
61 select (value: any, options: Object): any,
62 custom (placeholder: any[], locales: Locales, value: any, args: Object): any,
63 plural (value: number, offset: any, options: any, locale: any): any,
64 selectordinal (value: number, offset: any, options: any, locale: any): any,
65 namespace (): FormatMessage
66}
67*/
68
69function assign/*:: <T: Object> */ (target/*: T */, source/*: Object */) {
70 Object.keys(source).forEach(function (key) { target[key] = source[key] })
71 return target
72}
73
74function namespace ()/*: FormatMessage */ {
75 const formats = assign({}, origFormats)
76 let currentLocales/*: Locales */ = 'en'
77 let translations/*: Translations */ = {}
78 let generateId/*: GenerateId */ = function (pattern) { return pattern }
79 let missingReplacement/*: Replacement */ = null
80 let missingTranslation/*: MissingTranslation */ = 'warning'
81 let types/*: Types */ = {}
82
83 function formatMessage (msg/*: Message */, args/*:: ?: Object */, locales/*:: ?: Locales */) {
84 const pattern = typeof msg === 'string' ? msg : msg.default
85 const id = (typeof msg === 'object' && msg.id) || generateId(pattern)
86 const translated = translate(pattern, id, locales || currentLocales)
87 const format = translated.format || (
88 translated.format = interpret(parse(translated.message), locales || currentLocales, types)
89 )
90 return format(args)
91 }
92
93 formatMessage.rich = function rich (msg/*: Message */, args/*:: ?: Object */, locales/*:: ?: Locales */) {
94 const pattern = typeof msg === 'string' ? msg : msg.default
95 const id = (typeof msg === 'object' && msg.id) || generateId(pattern)
96 const translated = translate(pattern, id, locales || currentLocales)
97 const format = translated.toParts || (
98 translated.toParts = interpret.toParts(parse(pattern, { tagsType: tagsType }), locales || currentLocales, types)
99 )
100 return format(args)
101 }
102
103 const tagsType = '<>'
104 function richType (node/*: any[] */, locales/*: Locales */) {
105 const style = node[2]
106 return function (fn, args) {
107 const props = typeof style === 'object' ? mapObject(style, args) : style
108 return typeof fn === 'function' ? fn(props) : fn
109 }
110 }
111 types[tagsType] = richType
112
113 function mapObject (object/* { [string]: (args?: Object) => any } */, args/*: ?Object */) {
114 return Object.keys(object).reduce(function (mapped, key) {
115 mapped[key] = object[key](args)
116 return mapped
117 }, {})
118 }
119
120 function translate (pattern/*: string */, id/*: string */, locales/*: Locales */)/*: Translation */ {
121 const locale = lookupClosestLocale(locales, translations) || 'en'
122 const messages = translations[locale] || (translations[locale] = {})
123 let translated = messages[id]
124 if (typeof translated === 'string') {
125 translated = messages[id] = { message: translated }
126 }
127 if (!translated) {
128 const message = 'Translation for "' + id + '" in "' + locale + '" is missing'
129 if (missingTranslation === 'warning') {
130 /* istanbul ignore else */
131 if (typeof console !== 'undefined') console.warn(message)
132 } else if (missingTranslation !== 'ignore') { // 'error'
133 throw new Error(message)
134 }
135 const replacement = typeof missingReplacement === 'function'
136 ? missingReplacement(pattern, id, locale) || pattern
137 : missingReplacement || pattern
138 translated = messages[id] = { message: replacement }
139 }
140 return translated
141 }
142
143 formatMessage.setup = function setup (opt/*:: ?: Options */) {
144 opt = opt || {}
145 if (opt.locale) currentLocales = opt.locale
146 if ('translations' in opt) translations = opt.translations || {}
147 if (opt.generateId) generateId = opt.generateId
148 if ('missingReplacement' in opt) missingReplacement = opt.missingReplacement
149 if (opt.missingTranslation) missingTranslation = opt.missingTranslation
150 if (opt.formats) {
151 if (opt.formats.number) assign(formats.number, opt.formats.number)
152 if (opt.formats.date) assign(formats.date, opt.formats.date)
153 if (opt.formats.time) assign(formats.time, opt.formats.time)
154 }
155 if (opt.types) {
156 types = opt.types
157 types[tagsType] = richType
158 }
159 return {
160 locale: currentLocales,
161 translations: translations,
162 generateId: generateId,
163 missingReplacement: missingReplacement,
164 missingTranslation: missingTranslation,
165 formats: formats,
166 types: types
167 }
168 }
169
170 formatMessage.number = function (value/*: number */, style/*:: ?: string */, locales/*:: ?: Locales */) {
171 const options = (style && formats.number[style]) ||
172 formats.parseNumberPattern(style) ||
173 formats.number.default
174 return value.toLocaleString(locales || currentLocales, options)
175 }
176
177 formatMessage.date = function (value/*: number | Date */, style/*:: ?: string */, locales/*:: ?: Locales */) {
178 const options = (style && formats.date[style]) ||
179 formats.parseDatePattern(style) ||
180 formats.date.default
181 return new Date(value).toLocaleDateString(locales || currentLocales, options)
182 }
183
184 formatMessage.time = function (value/*: number | Date */, style/*:: ?: string */, locales/*:: ?: Locales */) {
185 const options = (style && formats.time[style]) ||
186 formats.parseDatePattern(style) ||
187 formats.time.default
188 return new Date(value).toLocaleTimeString(locales || currentLocales, options)
189 }
190
191 formatMessage.select = function (value/*: any */, options/*: Object */) {
192 return options[value] || options.other
193 }
194
195 formatMessage.custom = function (placeholder/*: any[] */, locales/*: Locales */, value/*: any */, args/*: Object */) {
196 if (!(placeholder[1] in types)) return value
197 return types[placeholder[1]](placeholder, locales)(value, args)
198 }
199
200 formatMessage.plural = plural.bind(null, 'cardinal')
201 formatMessage.selectordinal = plural.bind(null, 'ordinal')
202 function plural (
203 pluralType/*: 'cardinal' | 'ordinal' */,
204 value/*: number */,
205 offset/*: any */,
206 options/*: any */,
207 locale/*: any */
208 ) {
209 if (typeof offset === 'object' && typeof options !== 'object') { // offset is optional
210 locale = options
211 options = offset
212 offset = 0
213 }
214 const closest = lookupClosestLocale(locale || currentLocales, plurals)
215 const plural = (closest && plurals[closest][pluralType]) || returnOther
216 return options['=' + +value] || options[plural(value - offset)] || options.other
217 }
218 function returnOther (/*:: n:number */) { return 'other' }
219
220 formatMessage.namespace = namespace
221
222 return formatMessage
223}
224
225module.exports = exports = namespace()