-
Notifications
You must be signed in to change notification settings - Fork 2
/
core.ts
343 lines (303 loc) · 10.5 KB
/
core.ts
1
2
3
4
5
6
7
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import {
vsprintf
} from 'sprintf-js';
import {
I18nLocale,
I18nPluralLocale
} from './types';
import Config from './config';
const SEPARATOR = '.';
const DEFAULT_VALUE_SEPARATOR = ':';
const NOOP = () => null;
/**
* Check data type.
* @param maybeStrings - Maybe array of strings.
* @returns Given data is array of strings.
*/
export function isStringsArray(maybeStrings: any): maybeStrings is TemplateStringsArray | string[] {
return Array.isArray(maybeStrings);
}
/**
* Handling of template literals.
* @param text - Text or text parts.
* @returns Processed text.
*/
export function preProcess<T>(text: T | TemplateStringsArray | string[]): T | string {
if (isStringsArray(text)) {
return text.join(
text.hasOwnProperty('raw')
? '%s'
: ''
);
}
return text;
}
/**
* Handling of templates.
* @param text - Text to process.
* @param namedValues - Named values for mustache.
* @param values - List of values for vsprintf.
* @param count - Plural param.
* @returns Processed text.
*/
export function postProcess(config: Config, text: string, namedValues: any, values: any[], count?: number) {
let processedText = config.processors.reduce(
(text, processor) => processor(text, namedValues, values, count),
text
);
// replace the counter
if (typeof count === 'number') {
processedText = vsprintf(processedText, [count]);
}
// if we have extra arguments with values to get replaced,
// an additional substition injects those strings afterwards
if (/%/.test(processedText) && values.length) {
processedText = vsprintf(processedText, values);
}
return String(processedText);
}
/**
* Get singular from plurals object.
* @param plurals - Plurals object to get sigular form.
* @returns Singular.
*/
export function getSingularFromPlurals(plurals: I18nPluralLocale) {
if (typeof plurals.one !== 'undefined') {
return plurals.one;
}
// in case there is no 'one' but an 'other' rule
if (typeof plurals.other !== 'undefined') {
return plurals.other;
}
return null;
}
/**
* Parse float from string, if `num` is `string`.
* @param num - Number or string to handle.
* @returns Maybe number and number is or not.
*/
export function tryParseFloat(num: string | number): [number, boolean] {
if (typeof num === 'number') {
return [
num,
true
];
}
const maybeNumber = parseFloat(num);
const isNumberLike = num === String(maybeNumber);
return [
maybeNumber,
isNumberLike
];
}
/**
* Core translate function.
* @param config - Config object.
* @param locale - Target locale.
* @param singular - Singular form.
* @param plural - Plural form.
* @returns Translation.
*/
export function translate(config: Config, locale: string, singular: string, plural?: string) {
const {
objectNotation
} = config;
const targetLocale = config.getLocale(true, locale);
let targetSingular = singular;
let targetPlural = plural;
let defaultSingular = targetSingular;
let defaultPlural = targetPlural;
if (objectNotation) {
let indexOfColon = targetSingular.indexOf(DEFAULT_VALUE_SEPARATOR);
// We compare against 0 instead of -1 because we don't really expect the string to start with ':'.
if (indexOfColon > 0) {
defaultSingular = targetSingular.substring(indexOfColon + 1);
targetSingular = targetSingular.substring(0, indexOfColon);
}
if (targetPlural && typeof targetPlural !== 'number') {
indexOfColon = targetPlural.indexOf(DEFAULT_VALUE_SEPARATOR);
if (indexOfColon > 0) {
defaultPlural = targetPlural.substring(indexOfColon + 1);
targetPlural = targetPlural.substring(0, indexOfColon);
}
}
}
const accessor = localeAccessor(config, targetLocale, targetSingular, true);
const mutator = localeMutator(config, targetLocale, targetSingular, false);
const accessFirstTry = accessor();
if (targetPlural && (
!accessFirstTry || typeof accessFirstTry !== 'object'
)) {
mutator({
one: defaultSingular || targetSingular,
other: defaultPlural || targetPlural
});
}
if (!accessor()) {
mutator(defaultSingular || targetSingular);
}
return accessor();
}
/**
* Get singluar or plurls object.
* @param result - Singluar or plurls object.
* @returns Valid result.
*/
function getValidResult(result: I18nLocale) {
return typeof result === 'string'
|| result !== null && typeof result === 'object'
&& (result.hasOwnProperty('one') || result.hasOwnProperty('other'))
? result
: null;
}
/**
* Allows delayed access to translations nested inside objects.
* @param config - Config object.
* @param locale - The locale to use.
* @param singular - The singular term to look up.
* @param allowDelayedTraversal - Is delayed traversal of the tree allowed?
* This parameter is used internally. It allows to signal the accessor that
* a translation was not found in the initial lookup and that an invocation
* of the accessor may trigger another traversal of the tree.
* @returns A function that, when invoked, returns the current value stored
* in the object at the requested location.
*/
function localeAccessor(
config: Config,
locale: string,
singular: string,
allowDelayedTraversal = true
): () => I18nLocale {
const {
locales,
objectNotation
} = config;
// Bail out on non-existent locales to defend against internal errors.
if (typeof locales[locale] !== 'object') {
return NOOP;
}
// Handle object lookup notation
const indexOfSeparator = objectNotation && singular.lastIndexOf(SEPARATOR);
if (objectNotation && (indexOfSeparator > 0 && indexOfSeparator < singular.length - 1)) {
// The accessor we're trying to find and which we want to return.
let accessor;
// Do we need to re-traverse the tree upon invocation of the accessor?
let reTraverse = false;
// Split the provided term and run the callback for each subterm.
singular.split(SEPARATOR).reduce((object, index) => {
// Make the accessor return null.
accessor = NOOP;
// If our current target object (in the locale tree) doesn't exist or
// it doesn't have the next subterm as a member...
if (object === null || !object.hasOwnProperty(index)) {
// ...remember that we need retraversal (because we didn't find our target).
reTraverse = allowDelayedTraversal;
// Return null to avoid deeper iterations.
return null;
}
// We can traverse deeper, so we generate an accessor for this current level.
accessor = () => getValidResult(object[index]);
// Return a reference to the next deeper level in the locale tree.
return object[index];
}, locales[locale]);
// Return the requested accessor.
return () => (
// If we need to re-traverse (because we didn't find our target term)
// traverse again and return the new result (but don't allow further iterations)
// or return the previously found accessor if it was already valid.
reTraverse ? localeAccessor(config, locale, singular, false)() : accessor()
);
}
// No object notation, just return an accessor that performs array lookup.
return () => getValidResult(locales[locale][singular]);
}
/**
* Allows delayed mutation of a translation nested inside objects.
* @description Construction of the mutator will attempt to locate the requested term
* inside the object, but if part of the branch does not exist yet, it will not be
* created until the mutator is actually invoked. At that point, re-traversal of the
* tree is performed and missing parts along the branch will be created.
* @param config - Config object.
* @param locale - The locale to use.
* @param singular - The singular term to look up.
* @param allowBranching - Is the mutator allowed to create previously
* non-existent branches along the requested locale path?
* @returns A function that takes one argument. When the function is
* invoked, the targeted translation term will be set to the given value inside the locale table.
*/
function localeMutator(
config: Config,
locale: string,
singular: string,
allowBranching = false
): (input: I18nLocale) => I18nLocale {
const {
locales,
objectNotation,
unknownPhraseListener
} = config;
const withUnknownPhraseListener = typeof unknownPhraseListener === 'function';
// Bail out on non-existent locales to defend against internal errors.
if (typeof locales[locale] !== 'object') {
return NOOP;
}
// Handle object lookup notation
const indexOfSeparator = objectNotation && singular.lastIndexOf(SEPARATOR);
if (objectNotation && (indexOfSeparator > 0 && indexOfSeparator < singular.length - 1)) {
// This will become the function we want to return.
let accessor;
// Fix object path.
let fixObject = () => ({});
// Are we going to need to re-traverse the tree when the mutator is invoked?
let reTraverse = false;
// Split the provided term and run the callback for each subterm.
singular.split(SEPARATOR).reduce((prevObject, index) => {
let object = prevObject;
// Make the mutator do nothing.
accessor = NOOP;
// If our current target object (in the locale tree) doesn't exist or
// it doesn't have the next subterm as a member...
if (object === null || !object.hasOwnProperty(index)) {
// ...check if we're allowed to create new branches.
if (allowBranching) {
// Fix `object` if `object` is not Object.
if (object === null || typeof object !== 'object') {
object = fixObject();
}
// If we are allowed to, create a new object along the path.
object[index] = {};
} else {
// If we aren't allowed, remember that we need to re-traverse later on and...
reTraverse = true;
// ...return null to make the next iteration bail our early on.
return null;
}
}
// Generate a mutator for the current level.
accessor = withUnknownPhraseListener
? (value) => {
unknownPhraseListener(locale, singular, value);
return object[index] = value;
}
: value => (object[index] = value);
// Generate a fixer for the current level.
fixObject = () => (object[index] = {});
// Return a reference to the next deeper level in the locale tree.
return object[index];
}, locales[locale]);
// Return the final mutator.
return value => (
// If we need to re-traverse the tree
// invoke the search again, but allow branching this time (because here the mutator is being invoked)
// otherwise, just change the value directly.
reTraverse ? localeMutator(config, locale, singular, true)(value) : accessor(value)
);
}
// No object notation, just return a mutator that performs array lookup and changes the value.
return withUnknownPhraseListener
? (value) => {
unknownPhraseListener(locale, singular, value);
return locales[locale][singular] = value;
}
: value => (locales[locale][singular] = value);
}