UNPKG

4.9 kBJavaScriptView Raw
1const has = require('./hasProperty')
2
3/**
4 * Translates strings with interpolation & pluralization support.
5 * Extensible with custom dictionaries and pluralization functions.
6 *
7 * Borrows heavily from and inspired by Polyglot https://github.com/airbnb/polyglot.js,
8 * basically a stripped-down version of it. Differences: pluralization functions are not hardcoded
9 * and can be easily added among with dictionaries, nested objects are used for pluralization
10 * as opposed to `||||` delimeter
11 *
12 * Usage example: `translator.translate('files_chosen', {smart_count: 3})`
13 */
14module.exports = class Translator {
15 /**
16 * @param {object|Array<object>} locales - locale or list of locales.
17 */
18 constructor (locales) {
19 this.locale = {
20 strings: {},
21 pluralize (n) {
22 if (n === 1) {
23 return 0
24 }
25 return 1
26 },
27 }
28
29 if (Array.isArray(locales)) {
30 locales.forEach((locale) => this._apply(locale))
31 } else {
32 this._apply(locales)
33 }
34 }
35
36 _apply (locale) {
37 if (!locale || !locale.strings) {
38 return
39 }
40
41 const prevLocale = this.locale
42 this.locale = { ...prevLocale, strings: { ...prevLocale.strings, ...locale.strings } }
43 this.locale.pluralize = locale.pluralize || prevLocale.pluralize
44 }
45
46 /**
47 * Takes a string with placeholder variables like `%{smart_count} file selected`
48 * and replaces it with values from options `{smart_count: 5}`
49 *
50 * @license https://github.com/airbnb/polyglot.js/blob/master/LICENSE
51 * taken from https://github.com/airbnb/polyglot.js/blob/master/lib/polyglot.js#L299
52 *
53 * @param {string} phrase that needs interpolation, with placeholders
54 * @param {object} options with values that will be used to replace placeholders
55 * @returns {string} interpolated
56 */
57 interpolate (phrase, options) {
58 const { split, replace } = String.prototype
59 const dollarRegex = /\$/g
60 const dollarBillsYall = '$$$$'
61 let interpolated = [phrase]
62
63 for (const arg in options) {
64 if (arg !== '_' && has(options, arg)) {
65 // Ensure replacement value is escaped to prevent special $-prefixed
66 // regex replace tokens. the "$$$$" is needed because each "$" needs to
67 // be escaped with "$" itself, and we need two in the resulting output.
68 var replacement = options[arg]
69 if (typeof replacement === 'string') {
70 replacement = replace.call(options[arg], dollarRegex, dollarBillsYall)
71 }
72 // We create a new `RegExp` each time instead of using a more-efficient
73 // string replace so that the same argument can be replaced multiple times
74 // in the same phrase.
75 interpolated = insertReplacement(interpolated, new RegExp(`%\\{${arg}\\}`, 'g'), replacement)
76 }
77 }
78
79 return interpolated
80
81 function insertReplacement (source, rx, replacement) {
82 const newParts = []
83 source.forEach((chunk) => {
84 // When the source contains multiple placeholders for interpolation,
85 // we should ignore chunks that are not strings, because those
86 // can be JSX objects and will be otherwise incorrectly turned into strings.
87 // Without this condition we’d get this: [object Object] hello [object Object] my <button>
88 if (typeof chunk !== 'string') {
89 return newParts.push(chunk)
90 }
91
92 split.call(chunk, rx).forEach((raw, i, list) => {
93 if (raw !== '') {
94 newParts.push(raw)
95 }
96
97 // Interlace with the `replacement` value
98 if (i < list.length - 1) {
99 newParts.push(replacement)
100 }
101 })
102 })
103 return newParts
104 }
105 }
106
107 /**
108 * Public translate method
109 *
110 * @param {string} key
111 * @param {object} options with values that will be used later to replace placeholders in string
112 * @returns {string} translated (and interpolated)
113 */
114 translate (key, options) {
115 return this.translateArray(key, options).join('')
116 }
117
118 /**
119 * Get a translation and return the translated and interpolated parts as an array.
120 *
121 * @param {string} key
122 * @param {object} options with values that will be used to replace placeholders
123 * @returns {Array} The translated and interpolated parts, in order.
124 */
125 translateArray (key, options) {
126 if (!has(this.locale.strings, key)) {
127 throw new Error(`missing string: ${key}`)
128 }
129
130 const string = this.locale.strings[key]
131 const hasPluralForms = typeof string === 'object'
132
133 if (hasPluralForms) {
134 if (options && typeof options.smart_count !== 'undefined') {
135 const plural = this.locale.pluralize(options.smart_count)
136 return this.interpolate(string[plural], options)
137 }
138 throw new Error('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
139 }
140
141 return this.interpolate(string, options)
142 }
143}