1 |
|
2 | 'use strict'
|
3 | const parse = require('format-message-parse')
|
4 | const interpret = require('format-message-interpret')
|
5 | const plurals = require('format-message-interpret/plurals')
|
6 | const lookupClosestLocale = require('lookup-closest-locale')
|
7 | const origFormats = require('format-message-formats')
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | function assign/*:: <T: Object> */ (target/*: T */, source/*: Object */) {
|
70 | Object.keys(source).forEach(function (key) { target[key] = source[key] })
|
71 | return target
|
72 | }
|
73 |
|
74 | function namespace ()/*: FormatMessage */ {
|
75 | const formats = assign({}, origFormats)
|
76 | let currentLocales = 'en'
|
77 | let translations = {}
|
78 | let generateId = function (pattern) { return pattern }
|
79 | let missingReplacement = null
|
80 | let missingTranslation = 'warning'
|
81 | let 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 |
|
131 | if (typeof console !== 'undefined') console.warn(message)
|
132 | } else if (missingTranslation !== 'ignore') {
|
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') {
|
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 |
|
225 | module.exports = exports = namespace()
|