1 | const 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 | */
|
14 | module.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 | }
|