UNPKG

4.96 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: function (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 = Object.assign({}, prevLocale, {
43 strings: Object.assign({}, prevLocale.strings, locale.strings)
44 })
45 this.locale.pluralize = locale.pluralize || prevLocale.pluralize
46 }
47
48 /**
49 * Takes a string with placeholder variables like `%{smart_count} file selected`
50 * and replaces it with values from options `{smart_count: 5}`
51 *
52 * @license https://github.com/airbnb/polyglot.js/blob/master/LICENSE
53 * taken from https://github.com/airbnb/polyglot.js/blob/master/lib/polyglot.js#L299
54 *
55 * @param {string} phrase that needs interpolation, with placeholders
56 * @param {object} options with values that will be used to replace placeholders
57 * @returns {string} interpolated
58 */
59 interpolate (phrase, options) {
60 const { split, replace } = String.prototype
61 const dollarRegex = /\$/g
62 const dollarBillsYall = '$$$$'
63 let interpolated = [phrase]
64
65 for (const arg in options) {
66 if (arg !== '_' && has(options, arg)) {
67 // Ensure replacement value is escaped to prevent special $-prefixed
68 // regex replace tokens. the "$$$$" is needed because each "$" needs to
69 // be escaped with "$" itself, and we need two in the resulting output.
70 var replacement = options[arg]
71 if (typeof replacement === 'string') {
72 replacement = replace.call(options[arg], dollarRegex, dollarBillsYall)
73 }
74 // We create a new `RegExp` each time instead of using a more-efficient
75 // string replace so that the same argument can be replaced multiple times
76 // in the same phrase.
77 interpolated = insertReplacement(interpolated, new RegExp('%\\{' + arg + '\\}', 'g'), replacement)
78 }
79 }
80
81 return interpolated
82
83 function insertReplacement (source, rx, replacement) {
84 const newParts = []
85 source.forEach((chunk) => {
86 // When the source contains multiple placeholders for interpolation,
87 // we should ignore chunks that are not strings, because those
88 // can be JSX objects and will be otherwise incorrectly turned into strings.
89 // Without this condition we’d get this: [object Object] hello [object Object] my <button>
90 if (typeof chunk !== 'string') {
91 return newParts.push(chunk)
92 }
93
94 split.call(chunk, rx).forEach((raw, i, list) => {
95 if (raw !== '') {
96 newParts.push(raw)
97 }
98
99 // Interlace with the `replacement` value
100 if (i < list.length - 1) {
101 newParts.push(replacement)
102 }
103 })
104 })
105 return newParts
106 }
107 }
108
109 /**
110 * Public translate method
111 *
112 * @param {string} key
113 * @param {object} options with values that will be used later to replace placeholders in string
114 * @returns {string} translated (and interpolated)
115 */
116 translate (key, options) {
117 return this.translateArray(key, options).join('')
118 }
119
120 /**
121 * Get a translation and return the translated and interpolated parts as an array.
122 *
123 * @param {string} key
124 * @param {object} options with values that will be used to replace placeholders
125 * @returns {Array} The translated and interpolated parts, in order.
126 */
127 translateArray (key, options) {
128 if (!has(this.locale.strings, key)) {
129 throw new Error(`missing string: ${key}`)
130 }
131
132 const string = this.locale.strings[key]
133 const hasPluralForms = typeof string === 'object'
134
135 if (hasPluralForms) {
136 if (options && typeof options.smart_count !== 'undefined') {
137 const plural = this.locale.pluralize(options.smart_count)
138 return this.interpolate(string[plural], options)
139 } else {
140 throw new Error('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
141 }
142 }
143
144 return this.interpolate(string, options)
145 }
146}