@dialpad/i18n 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/.eslintrc.cjs +12 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +34 -0
- package/README.md +533 -0
- package/base-tsconfig.json +19 -0
- package/dist/i18n.cjs +119 -0
- package/dist/i18n.cjs.map +1 -0
- package/dist/i18n.js +113 -0
- package/dist/i18n.js.map +1 -0
- package/eslint-tsconfig.json +5 -0
- package/index.html +11 -0
- package/index.ts +18 -0
- package/package.json +49 -0
- package/src/__test__/locale-manager.formatters.test.ts +139 -0
- package/src/__test__/locale-manager.test.ts +511 -0
- package/src/locale-manager.ts +139 -0
- package/tsconfig.json +10 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
// compilation checks or maybe just be all .js files?
|
|
3
|
+
import type { BundleSource } from '@dialpad/i18n-services/bundle-source';
|
|
4
|
+
import { MemoryStorageWrapper } from '@dialpad/i18n-services/storage';
|
|
5
|
+
import type { LocaleManagerParams } from '@dialpad/i18n-services/locale-manager';
|
|
6
|
+
|
|
7
|
+
import { expect, describe, it, beforeEach, vi } from 'vitest';
|
|
8
|
+
import { INJECTION_KEY_PREFIX, LocaleManager } from '../locale-manager';
|
|
9
|
+
import type { App } from 'vue';
|
|
10
|
+
|
|
11
|
+
const EN_US = 'en-US'; // for brevity...
|
|
12
|
+
|
|
13
|
+
// could certainly go harder on this mock but I don't think it's worth atm.
|
|
14
|
+
function mockBundleSource(): BundleSource {
|
|
15
|
+
return {
|
|
16
|
+
getBundles: (lc: string[], ns: string[]): any => {
|
|
17
|
+
return Promise.resolve(lc.map((l) => ({ locales: [l] })));
|
|
18
|
+
},
|
|
19
|
+
addSource: () => {
|
|
20
|
+
throw new Error("Don't set me up like that!");
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mockVueApp(): any {
|
|
26
|
+
return { use: () => {}, provide: () => {} };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// TODO - would go well in a testing util lib. not quite sync, but immediate
|
|
30
|
+
// and reliable for stuff that you don't expect to resovle for "a hot second"
|
|
31
|
+
async function isResolved(check: Promise<unknown>): Promise<boolean> {
|
|
32
|
+
const dummy = {};
|
|
33
|
+
return (await Promise.race([check, Promise.resolve(dummy)])) !== dummy;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const testManager = (props?: Partial<LocaleManagerParams>) =>
|
|
37
|
+
new LocaleManager({
|
|
38
|
+
bundleSource: mockBundleSource(),
|
|
39
|
+
warmUp: false,
|
|
40
|
+
fallbackLocale: 'en-US',
|
|
41
|
+
...props,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('LocaleManager', () => {
|
|
45
|
+
let manager: LocaleManager = testManager();
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
manager = testManager();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('initializes with fallback', () => {
|
|
51
|
+
expect(manager['preferredLocale']).toEqual(EN_US);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('can never have zero allowedLocales', () => {
|
|
55
|
+
manager['setAllowedLocales']([]);
|
|
56
|
+
expect(manager['allowedLocales'].length).toEqual(1);
|
|
57
|
+
// EVEN IF YOU VIOLATE IT TERRIBLY
|
|
58
|
+
manager['allowedLocales'] = []; // evil access which a just god would never allow
|
|
59
|
+
expect(manager['preferredLocale']).toEqual(EN_US);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('wont do anything useful without some namespaces', async () => {
|
|
63
|
+
expect(() => manager['getCurrentBundles']()).toThrowError(/no namespaces/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('requires warming up before offering bundles', async () => {
|
|
67
|
+
expect(() => manager['currentBundles']).toThrowError(
|
|
68
|
+
/No bundles! you must await/,
|
|
69
|
+
);
|
|
70
|
+
expect(manager['_cachedBundles']).toEqual(null);
|
|
71
|
+
expect(manager['_cachedHash']).toEqual(null);
|
|
72
|
+
const spy = vi.spyOn(manager['bundleSource'], 'getBundles');
|
|
73
|
+
await manager.change({ namespaces: ['x'] });
|
|
74
|
+
await manager.ready;
|
|
75
|
+
expect(spy).toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('uses its cache by default', async () => {
|
|
79
|
+
const spy = vi.spyOn(manager['bundleSource'], 'getBundles');
|
|
80
|
+
await manager.change({ namespaces: ['x'] });
|
|
81
|
+
const buns = manager['currentBundles'];
|
|
82
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(manager['_cachedBundles']).toBe(buns);
|
|
84
|
+
|
|
85
|
+
// options were not changed, so the bundles we get must be the same object.
|
|
86
|
+
await manager.change({});
|
|
87
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(manager['_cachedBundles']).toBe(buns);
|
|
89
|
+
|
|
90
|
+
await manager.change({ useCache: true });
|
|
91
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(manager['_cachedBundles']).toBe(buns);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('can ignore its own cache', async () => {
|
|
96
|
+
const spy = vi.spyOn(manager['bundleSource'], 'getBundles');
|
|
97
|
+
|
|
98
|
+
await manager.change({ namespaces: ['x'] });
|
|
99
|
+
const buns = manager['currentBundles'];
|
|
100
|
+
const hash = manager['_cachedHash'];
|
|
101
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(manager['_cachedBundles']).toBe(buns);
|
|
103
|
+
|
|
104
|
+
// called even though hash was the same.
|
|
105
|
+
await manager.change({ useCache: false });
|
|
106
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
107
|
+
expect(manager['_cachedBundles']).not.toBe(buns);
|
|
108
|
+
expect(manager['_cachedBundles']).toEqual(buns);
|
|
109
|
+
expect(manager['_cachedHash']).toEqual(hash);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('always ignores cache when parameters change.', async () => {
|
|
113
|
+
const spy = vi.spyOn(manager['bundleSource'], 'getBundles');
|
|
114
|
+
|
|
115
|
+
await manager.change({ namespaces: ['x'] });
|
|
116
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
117
|
+
|
|
118
|
+
await manager.change({ namespaces: ['y'], useCache: true });
|
|
119
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
120
|
+
|
|
121
|
+
await manager.change({ allowedLocales: ['aa-AA', 'b'], useCache: true });
|
|
122
|
+
expect(spy).toHaveBeenCalledTimes(3);
|
|
123
|
+
|
|
124
|
+
await manager.change({ allowedLocales: ['b', 'a'], useCache: true });
|
|
125
|
+
expect(spy).toHaveBeenCalledTimes(4);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// For the forseeable future, we will only care about two locales at most: the
|
|
129
|
+
// user's setting and the fallback (en-US). LocaleManager will seemlessly
|
|
130
|
+
// support as many as we care to provide, but until we've settled on *how* we
|
|
131
|
+
// might like to manage that, we should only provide those two bundles.
|
|
132
|
+
it('only cares about the preferred and default locales.', async () => {
|
|
133
|
+
const spy = vi.spyOn(manager['bundleSource'], 'getBundles');
|
|
134
|
+
await manager.change({
|
|
135
|
+
namespaces: ['x'],
|
|
136
|
+
allowedLocales: ['aa-BB', 'xx-YY', 'pp-QQ'],
|
|
137
|
+
});
|
|
138
|
+
expect(spy).toHaveBeenCalledWith(['aa-BB', EN_US], ['x']);
|
|
139
|
+
expect(manager['activeLocales']).toEqual(['aa-BB', EN_US]);
|
|
140
|
+
|
|
141
|
+
await manager.change({ preferredLocale: 'pp-QQ' });
|
|
142
|
+
expect(spy).toHaveBeenCalledWith(['aa-BB', EN_US], ['x']);
|
|
143
|
+
expect(manager['activeLocales']).toEqual(['pp-QQ', EN_US]);
|
|
144
|
+
|
|
145
|
+
// that's only one thing if they're the same, regardless of how many total
|
|
146
|
+
// allowedLocales have been provided.
|
|
147
|
+
await manager.change({ preferredLocale: 'en-US' });
|
|
148
|
+
expect(spy).toHaveBeenCalledWith([EN_US], ['x']);
|
|
149
|
+
expect(manager['activeLocales']).toEqual([EN_US]);
|
|
150
|
+
expect(manager['allowedLocales'].length).toEqual(4);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('warms up bundleSource at init time by default', async () => {
|
|
154
|
+
const bundleSource = mockBundleSource();
|
|
155
|
+
const spy = vi.spyOn(bundleSource, 'getBundles');
|
|
156
|
+
manager = testManager({
|
|
157
|
+
bundleSource,
|
|
158
|
+
warmUp: undefined,
|
|
159
|
+
namespaces: ['foo'],
|
|
160
|
+
preferredLocale: 'aa-BB',
|
|
161
|
+
});
|
|
162
|
+
await manager.ready;
|
|
163
|
+
expect(manager['activeLocales']).toEqual(['aa-BB', EN_US]);
|
|
164
|
+
expect(spy).toHaveBeenCalledWith(['aa-BB', EN_US], ['foo']);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('can disable warmup', async () => {
|
|
168
|
+
const bundleSource = mockBundleSource();
|
|
169
|
+
const bundleSpy = vi.spyOn(bundleSource, 'getBundles');
|
|
170
|
+
// @ts-expect-error - warmUp is private so this will fail
|
|
171
|
+
const warmUpSpy = vi.spyOn(LocaleManager.prototype, 'warmUp');
|
|
172
|
+
manager = testManager({
|
|
173
|
+
bundleSource,
|
|
174
|
+
warmUp: false,
|
|
175
|
+
namespaces: ['foo', 'bar'],
|
|
176
|
+
preferredLocale: 'aa-BB',
|
|
177
|
+
});
|
|
178
|
+
await expect(isResolved(manager.ready)).resolves.toEqual(false);
|
|
179
|
+
// options are set nonetheless
|
|
180
|
+
expect(manager['activeLocales']).toEqual(['aa-BB', EN_US]);
|
|
181
|
+
expect(manager['currentNamespaces']).toEqual(new Set(['foo', 'bar']));
|
|
182
|
+
expect(bundleSpy).not.toHaveBeenCalled();
|
|
183
|
+
expect(warmUpSpy).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('vue plugin', () => {
|
|
187
|
+
let useSpy, proSpy, app;
|
|
188
|
+
|
|
189
|
+
beforeEach(async () => {
|
|
190
|
+
app = mockVueApp();
|
|
191
|
+
useSpy = vi.spyOn(app, 'use');
|
|
192
|
+
proSpy = vi.spyOn(app, 'provide');
|
|
193
|
+
manager = testManager({ warmUp: true, namespaces: ['foo'] });
|
|
194
|
+
await manager.ready;
|
|
195
|
+
|
|
196
|
+
expect(manager['fluent']).toEqual(null);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('install()', () => {
|
|
200
|
+
it('should call this.addToVue with provided app and namespace', () => {
|
|
201
|
+
const addToVueSpy = vi.spyOn(manager as any, 'addToVue');
|
|
202
|
+
manager.install(app, 'my-namespace');
|
|
203
|
+
expect(addToVueSpy).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(addToVueSpy).toHaveBeenCalledWith(app, 'my-namespace');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should initialize this.fluent when it is null', () => {
|
|
208
|
+
(manager as any).fluent = null;
|
|
209
|
+
manager.install(app);
|
|
210
|
+
expect((manager as any).fluent).not.toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should call this.addToVue with provided app and namespace', () => {
|
|
214
|
+
const namespace = 'my-namespace';
|
|
215
|
+
manager.install(app, namespace);
|
|
216
|
+
const addToVueSpy = vi.spyOn(manager as any, 'addToVue');
|
|
217
|
+
|
|
218
|
+
manager.install(app, namespace);
|
|
219
|
+
|
|
220
|
+
// Expect the addToVue method to have been called with the provided app and namespace
|
|
221
|
+
expect(addToVueSpy).toHaveBeenCalledTimes(1);
|
|
222
|
+
expect(addToVueSpy).toHaveBeenCalledWith(app, namespace);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should call this.addToVue with default namespace when not provided', () => {
|
|
226
|
+
const addToVueSpy = vi.spyOn(manager as any, 'addToVue');
|
|
227
|
+
manager.install(app);
|
|
228
|
+
expect(addToVueSpy).toHaveBeenCalledTimes(1);
|
|
229
|
+
expect(addToVueSpy).toHaveBeenCalledWith(app, 'default');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('cannot be installed prior to warmup', async () => {
|
|
234
|
+
const manager = testManager({ warmUp: false, namespaces: ['foo'] });
|
|
235
|
+
const app = mockVueApp();
|
|
236
|
+
expect(() => {
|
|
237
|
+
manager.install(app);
|
|
238
|
+
}).toThrowError(/No bundles!/);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('can be installed under the correct circumstances', async () => {
|
|
242
|
+
manager.install(app);
|
|
243
|
+
|
|
244
|
+
expect(useSpy).toHaveBeenCalledWith(manager['fluent']);
|
|
245
|
+
// checking 'app.inject' would be much more of a pain!
|
|
246
|
+
expect(proSpy).toHaveBeenCalledWith(
|
|
247
|
+
`${INJECTION_KEY_PREFIX}.default`,
|
|
248
|
+
manager,
|
|
249
|
+
);
|
|
250
|
+
const mf = manager['fluent'];
|
|
251
|
+
expect(mf?.bundles).toEqual(manager['currentBundles']);
|
|
252
|
+
|
|
253
|
+
// won't make another:
|
|
254
|
+
manager.install(app);
|
|
255
|
+
expect(manager['fluent']).toBe(mf);
|
|
256
|
+
|
|
257
|
+
// changing options sets the fluent instance's new bundles:
|
|
258
|
+
if (mf) {
|
|
259
|
+
mf.bundles = [];
|
|
260
|
+
}
|
|
261
|
+
await manager.change();
|
|
262
|
+
expect(mf?.bundles).toEqual(manager['currentBundles']);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('determineLocale', () => {
|
|
267
|
+
let storageWrapper;
|
|
268
|
+
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
storageWrapper = new MemoryStorageWrapper();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('returns the provided locale if given', () => {
|
|
274
|
+
const localeManager = testManager({
|
|
275
|
+
fallbackLocale: 'es-ES',
|
|
276
|
+
warmUp: false,
|
|
277
|
+
namespaces: ['foo'],
|
|
278
|
+
});
|
|
279
|
+
const result = localeManager['determineLocale']({
|
|
280
|
+
preferredLocale: 'fr-FR',
|
|
281
|
+
});
|
|
282
|
+
expect(result).toBe('fr-FR');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('returns the first provided locale if an array is given', () => {
|
|
286
|
+
const localeManager = testManager({
|
|
287
|
+
fallbackLocale: 'es-ES',
|
|
288
|
+
warmUp: false,
|
|
289
|
+
namespaces: ['foo'],
|
|
290
|
+
});
|
|
291
|
+
const result = localeManager['determineLocale']({
|
|
292
|
+
allowedLocales: ['de-DE', 'fr-FR'],
|
|
293
|
+
});
|
|
294
|
+
expect(result).toBe('de-DE');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('returns stored locale if available', () => {
|
|
298
|
+
const localeManager = new LocaleManager({
|
|
299
|
+
allowedLocales: ['en-US'],
|
|
300
|
+
fallbackLocale: 'es-ES',
|
|
301
|
+
warmUp: false,
|
|
302
|
+
bundleSource: mockBundleSource(),
|
|
303
|
+
storageWrapper,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
storageWrapper.setItem('user-locale', 'de-DE');
|
|
307
|
+
const result = localeManager['determineLocale']({});
|
|
308
|
+
expect(result).toBe('de-DE');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('returns browser locale if no stored locale', () => {
|
|
312
|
+
const localeManager = testManager({
|
|
313
|
+
fallbackLocale: 'es-ES',
|
|
314
|
+
warmUp: false,
|
|
315
|
+
namespaces: ['foo'],
|
|
316
|
+
storageWrapper,
|
|
317
|
+
});
|
|
318
|
+
storageWrapper.removeItem('user-locale');
|
|
319
|
+
vi.spyOn(navigator, 'languages', 'get').mockReturnValue(['it-IT']);
|
|
320
|
+
const result = localeManager['determineLocale']({});
|
|
321
|
+
expect(result).toBe('it-IT');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns fallback locale if no other locale is found', () => {
|
|
325
|
+
const localeManager = testManager({
|
|
326
|
+
fallbackLocale: 'es-ES',
|
|
327
|
+
warmUp: false,
|
|
328
|
+
namespaces: ['foo'],
|
|
329
|
+
storageWrapper,
|
|
330
|
+
});
|
|
331
|
+
storageWrapper.removeItem('user-locale');
|
|
332
|
+
vi.spyOn(navigator, 'languages', 'get').mockReturnValue([]);
|
|
333
|
+
const result = localeManager['determineLocale']({});
|
|
334
|
+
expect(result).toBe('es-ES');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('change locales', () => {
|
|
339
|
+
it('changes locale if the new locale is available', async () => {
|
|
340
|
+
const localeManager = testManager({ warmUp: false, namespaces: ['foo'] });
|
|
341
|
+
// @ts-expect-error - setPreferredLocale is private so this will fail
|
|
342
|
+
vi.spyOn(localeManager, 'setPreferredLocale');
|
|
343
|
+
|
|
344
|
+
// is not part of bundles but you can set it anyways (we think that's by design)
|
|
345
|
+
await localeManager.change({ preferredLocale: 'fr-FR' });
|
|
346
|
+
expect(localeManager['setPreferredLocale']).toHaveBeenCalledWith('fr-FR');
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('setOptions', () => {
|
|
351
|
+
it('sets allowed locales and preferred locale when passing an array', () => {
|
|
352
|
+
const localeManager = testManager({
|
|
353
|
+
fallbackLocale: 'es-ES',
|
|
354
|
+
warmUp: false,
|
|
355
|
+
namespaces: ['foo'],
|
|
356
|
+
});
|
|
357
|
+
// @ts-expect-error - setAllowedLocales is private so this will fail
|
|
358
|
+
vi.spyOn(localeManager, 'setAllowedLocales');
|
|
359
|
+
// @ts-expect-error - setPreferredLocale is private so this will fail
|
|
360
|
+
vi.spyOn(localeManager, 'setPreferredLocale');
|
|
361
|
+
localeManager['setOptions']({ allowedLocales: ['fr-FR', 'de-DE'] });
|
|
362
|
+
expect(localeManager['setAllowedLocales']).toHaveBeenCalledWith([
|
|
363
|
+
'fr-FR',
|
|
364
|
+
'de-DE',
|
|
365
|
+
]);
|
|
366
|
+
expect(localeManager['allowedLocales']).toEqual([
|
|
367
|
+
'fr-FR',
|
|
368
|
+
'de-DE',
|
|
369
|
+
'es-ES',
|
|
370
|
+
]);
|
|
371
|
+
expect(localeManager['setPreferredLocale']).toHaveBeenCalled();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('sets preferred locale when passing a string', () => {
|
|
375
|
+
const localeManager = testManager({
|
|
376
|
+
fallbackLocale: 'es-ES',
|
|
377
|
+
warmUp: false,
|
|
378
|
+
namespaces: ['foo'],
|
|
379
|
+
});
|
|
380
|
+
// @ts-expect-error - setPreferredLocale is private so this will fail
|
|
381
|
+
vi.spyOn(localeManager, 'setPreferredLocale');
|
|
382
|
+
localeManager['setOptions']({ preferredLocale: 'fr-FR' });
|
|
383
|
+
expect(localeManager['setPreferredLocale']).toHaveBeenCalledWith('fr-FR');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('adds namespaces correctly when passing an array', () => {
|
|
387
|
+
const localeManager = testManager({
|
|
388
|
+
fallbackLocale: 'es-ES',
|
|
389
|
+
warmUp: false,
|
|
390
|
+
namespaces: ['foo'],
|
|
391
|
+
});
|
|
392
|
+
// @ts-expect-error - addNamespaces is private so this will fail
|
|
393
|
+
vi.spyOn(localeManager, 'addNamespaces');
|
|
394
|
+
localeManager['setOptions']({ namespaces: ['namespace1', 'namespace2'] });
|
|
395
|
+
expect(localeManager['addNamespaces']).toHaveBeenCalledWith([
|
|
396
|
+
'namespace1',
|
|
397
|
+
'namespace2',
|
|
398
|
+
]);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe('setPreferredLocale', () => {
|
|
403
|
+
it('moves existing locale to the front of the allowedLocales list', () => {
|
|
404
|
+
const localeManager = testManager({
|
|
405
|
+
preferredLocale: 'en-US',
|
|
406
|
+
fallbackLocale: 'es-ES',
|
|
407
|
+
warmUp: false,
|
|
408
|
+
namespaces: ['foo'],
|
|
409
|
+
});
|
|
410
|
+
localeManager['setPreferredLocale']('fr-FR');
|
|
411
|
+
expect(localeManager['allowedLocales'][0]).toBe('fr-FR');
|
|
412
|
+
expect(localeManager['allowedLocales']).toEqual(['fr-FR', 'en-US']);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('adds new locale to the front if not already present', () => {
|
|
416
|
+
const localeManager = testManager({
|
|
417
|
+
preferredLocale: 'en-US',
|
|
418
|
+
fallbackLocale: 'es-ES',
|
|
419
|
+
warmUp: false,
|
|
420
|
+
namespaces: ['foo'],
|
|
421
|
+
});
|
|
422
|
+
localeManager['setPreferredLocale']('fr-FR');
|
|
423
|
+
expect(localeManager['allowedLocales'][0]).toBe('fr-FR');
|
|
424
|
+
expect(localeManager['allowedLocales']).toEqual(['fr-FR', 'en-US']);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('changeLocale', () => {
|
|
429
|
+
it('throws an error when namespace is provided but no matching LocaleManager is found', () => {
|
|
430
|
+
manager.app = { _context: { provides: {} } } as App;
|
|
431
|
+
expect(() => {
|
|
432
|
+
manager.changeLocale({}, 'non-existent-namespace');
|
|
433
|
+
}).toThrowError('LocaleManager not found!');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('throws an error when no LocaleManager instances are set up yet', () => {
|
|
437
|
+
expect(() => {
|
|
438
|
+
manager.changeLocale();
|
|
439
|
+
}).toThrowError('No locale managers are set up yet!');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('updates the locale of a single LocaleManager instance when namespace is provided', () => {
|
|
443
|
+
const localeManager = testManager({
|
|
444
|
+
namespaces: ['your-app'],
|
|
445
|
+
preferredLocale: 'en-US',
|
|
446
|
+
});
|
|
447
|
+
localeManager.app = {
|
|
448
|
+
_context: {
|
|
449
|
+
provides: { [`${INJECTION_KEY_PREFIX}.your-app`]: localeManager },
|
|
450
|
+
},
|
|
451
|
+
} as unknown as App;
|
|
452
|
+
const updateLocaleSettingsSpy = vi.spyOn(
|
|
453
|
+
localeManager,
|
|
454
|
+
'updateLocaleSettings',
|
|
455
|
+
);
|
|
456
|
+
localeManager.changeLocale({}, 'your-app');
|
|
457
|
+
expect(updateLocaleSettingsSpy).toHaveBeenCalledTimes(1);
|
|
458
|
+
expect(localeManager['preferredLocale']).toBe('en-US');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('updates the locale of all LocaleManager instances when no namespace is provided', () => {
|
|
462
|
+
const localeManager1 = testManager({
|
|
463
|
+
namespaces: ['foo'],
|
|
464
|
+
preferredLocale: 'en-US',
|
|
465
|
+
});
|
|
466
|
+
const localeManager2 = testManager({
|
|
467
|
+
namespaces: ['foo'],
|
|
468
|
+
preferredLocale: 'en-US',
|
|
469
|
+
});
|
|
470
|
+
localeManager1.app = {
|
|
471
|
+
_context: {
|
|
472
|
+
provides: {
|
|
473
|
+
[`${INJECTION_KEY_PREFIX}.namespace-1`]: localeManager1,
|
|
474
|
+
[`${INJECTION_KEY_PREFIX}.namespace-2`]: localeManager2,
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
} as unknown as App;
|
|
478
|
+
const updateLocaleSettingsSpy1 = vi.spyOn(
|
|
479
|
+
localeManager1,
|
|
480
|
+
'updateLocaleSettings',
|
|
481
|
+
);
|
|
482
|
+
const updateLocaleSettingsSpy2 = vi.spyOn(
|
|
483
|
+
localeManager2,
|
|
484
|
+
'updateLocaleSettings',
|
|
485
|
+
);
|
|
486
|
+
localeManager1.changeLocale();
|
|
487
|
+
expect(updateLocaleSettingsSpy1).toHaveBeenCalledTimes(1);
|
|
488
|
+
expect(updateLocaleSettingsSpy2).toHaveBeenCalledTimes(1);
|
|
489
|
+
expect(localeManager1['preferredLocale']).toBe('en-US');
|
|
490
|
+
expect(localeManager2['preferredLocale']).toBe('en-US');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('passes args to the updateLocaleSettings method of each LocaleManager instance', () => {
|
|
494
|
+
const localeManager = testManager({ namespaces: ['foo'] });
|
|
495
|
+
localeManager.app = {
|
|
496
|
+
_context: {
|
|
497
|
+
provides: { [`${INJECTION_KEY_PREFIX}.namespace-1`]: localeManager },
|
|
498
|
+
},
|
|
499
|
+
} as unknown as App;
|
|
500
|
+
const updateLocaleSettingsSpy = vi.spyOn(
|
|
501
|
+
localeManager,
|
|
502
|
+
'updateLocaleSettings',
|
|
503
|
+
);
|
|
504
|
+
const args = { preferredLocale: 'es-LA' };
|
|
505
|
+
localeManager.changeLocale(args, 'namespace-1');
|
|
506
|
+
expect(updateLocaleSettingsSpy).toHaveBeenCalledTimes(1);
|
|
507
|
+
expect(updateLocaleSettingsSpy).toHaveBeenCalledWith(args);
|
|
508
|
+
expect(localeManager['preferredLocale']).toBe('es-LA');
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { App, Ref } from 'vue';
|
|
2
|
+
import type {
|
|
3
|
+
FluentFormat,
|
|
4
|
+
FluentFormatAttrs,
|
|
5
|
+
LocaleManagerParams,
|
|
6
|
+
SetLocaleParams,
|
|
7
|
+
} from '@dialpad/i18n-services/locale-manager';
|
|
8
|
+
|
|
9
|
+
import { computed, inject, ref } from 'vue';
|
|
10
|
+
import { BaseLocaleManager } from '@dialpad/i18n-services/locale-manager';
|
|
11
|
+
|
|
12
|
+
export const INJECTION_KEY_PREFIX = 'GLOBAL_LOCALE_MANAGER';
|
|
13
|
+
|
|
14
|
+
export interface UseI18N {
|
|
15
|
+
currentLocale: Ref<string | null>;
|
|
16
|
+
setI18N: (args?: Partial<SetLocaleParams>, namespace?: string) => void;
|
|
17
|
+
$t: FluentFormat;
|
|
18
|
+
$ta: FluentFormatAttrs;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class LocaleManager extends BaseLocaleManager {
|
|
22
|
+
currentLocaleProp: Ref<string | null> = ref<string | null>(null);
|
|
23
|
+
app: App | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(params: LocaleManagerParams) {
|
|
26
|
+
super(params);
|
|
27
|
+
this.setCurrentLocaleProp(
|
|
28
|
+
this.determineLocale({
|
|
29
|
+
preferredLocale: params.preferredLocale,
|
|
30
|
+
allowedLocales: params.allowedLocales,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected setCurrentLocaleProp(locale: string): void {
|
|
36
|
+
this.currentLocaleProp = ref(locale);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Registers this LocaleManager with the Vue instance so that it can
|
|
41
|
+
* be used by the {@link useI18N} hook.
|
|
42
|
+
*
|
|
43
|
+
* @param app - The Vue app to register with.
|
|
44
|
+
* @param namespace - The namespace to install the LocaleManager under.
|
|
45
|
+
* Defaults to 'default'
|
|
46
|
+
*/
|
|
47
|
+
install(app: App, namespace = 'default'): void {
|
|
48
|
+
if (this.fluent === null) {
|
|
49
|
+
this.fluent = this.initFluent();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.addToVue(app, namespace);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private addToVue(app: App, namespace: string): void {
|
|
56
|
+
if (!this.fluent) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'fluent not ready, you probably want to call install(...) first',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
this.app = app;
|
|
62
|
+
app.use(this.fluent);
|
|
63
|
+
// TODO - allow custom injection key i 'spose
|
|
64
|
+
app.provide(parseInjectionName(namespace), this);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Changes the locale of the {@link LocaleManager} that is currently active in
|
|
69
|
+
* the app, or the one specified by the namespace.
|
|
70
|
+
*
|
|
71
|
+
* @param args - Optional parameters to pass to the underlying
|
|
72
|
+
* {@link BaseLocaleManager#updateLocaleSettings} method.
|
|
73
|
+
* @param namespace - Optional namespace to change the locale of. If not
|
|
74
|
+
* provided, all LocaleManagers will be changed.
|
|
75
|
+
*/
|
|
76
|
+
changeLocale(args?: Partial<SetLocaleParams>, namespace?: string): void {
|
|
77
|
+
const localeManagers: LocaleManager[] = [];
|
|
78
|
+
const providedValues = this.app?._context.provides;
|
|
79
|
+
if (providedValues) {
|
|
80
|
+
if (namespace) {
|
|
81
|
+
// We cannot use inject here because is likely to not either inside setup() or functional components. Also see Line #93
|
|
82
|
+
// btw, Object.entries() returns [key, value]
|
|
83
|
+
const keyValue =
|
|
84
|
+
Object.entries(providedValues).find(
|
|
85
|
+
([key, value]) =>
|
|
86
|
+
typeof key === 'string' &&
|
|
87
|
+
key === parseInjectionName(namespace) &&
|
|
88
|
+
value instanceof LocaleManager,
|
|
89
|
+
) ?? [];
|
|
90
|
+
const localeManager = keyValue[1];
|
|
91
|
+
if (!localeManager) {
|
|
92
|
+
throw new Error('LocaleManager not found!');
|
|
93
|
+
}
|
|
94
|
+
localeManagers.push(localeManager);
|
|
95
|
+
} else {
|
|
96
|
+
// This is very ugly but it's the only way I found to get all the localeManagers without
|
|
97
|
+
// having to create a singleton, if you have a better idea, let me know!! :-]
|
|
98
|
+
// Get string-keyed properties
|
|
99
|
+
Object.entries(providedValues).forEach(([key, value]) => {
|
|
100
|
+
if (
|
|
101
|
+
typeof key === 'string' &&
|
|
102
|
+
key.startsWith(INJECTION_KEY_PREFIX.toString()) &&
|
|
103
|
+
value instanceof LocaleManager
|
|
104
|
+
) {
|
|
105
|
+
localeManagers.push(value);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error('No locale managers are set up yet!');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const localeManager of localeManagers) {
|
|
114
|
+
localeManager.updateLocaleSettings(args);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// TODO - allow custom injection key or whatever???
|
|
120
|
+
// TODO - also maybe just use getCurrentApp + the app's global config? idk.
|
|
121
|
+
export function useI18N(namespace = 'default'): UseI18N {
|
|
122
|
+
const localeManager: LocaleManager | undefined = inject(
|
|
123
|
+
parseInjectionName(namespace),
|
|
124
|
+
);
|
|
125
|
+
if (!localeManager)
|
|
126
|
+
throw new Error(`locale manager doesn't exist using ${namespace}`);
|
|
127
|
+
return {
|
|
128
|
+
currentLocale: computed(() => localeManager.currentLocaleProp.value),
|
|
129
|
+
setI18N: (args?: Partial<SetLocaleParams>, namespace?: string) => {
|
|
130
|
+
localeManager.changeLocale(args, namespace);
|
|
131
|
+
},
|
|
132
|
+
$t: localeManager.fluentFormat,
|
|
133
|
+
$ta: localeManager.fluentFormatAttrs,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseInjectionName(namespace: string): string {
|
|
138
|
+
return `${INJECTION_KEY_PREFIX}.${namespace}`;
|
|
139
|
+
}
|
package/tsconfig.json
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import vue from '@vitejs/plugin-vue';
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
build: {
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
minify: false,
|
|
9
|
+
rollupOptions: {
|
|
10
|
+
external: ['vue', /^@dialpad/],
|
|
11
|
+
output: {
|
|
12
|
+
preserveModules: false,
|
|
13
|
+
minifyInternalExports: false,
|
|
14
|
+
exports: 'named',
|
|
15
|
+
},
|
|
16
|
+
treeshake: 'smallest',
|
|
17
|
+
},
|
|
18
|
+
lib: {
|
|
19
|
+
entry: './index.ts',
|
|
20
|
+
formats: ['es', 'cjs'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
test: {
|
|
24
|
+
// Add test runner globals like test/it/describe/afterEach etc.
|
|
25
|
+
// This is required for VTL to automically call cleanup() in afterEach().
|
|
26
|
+
// See: https://vitest.dev/guide/migration.html
|
|
27
|
+
globals: true,
|
|
28
|
+
environment: 'jsdom',
|
|
29
|
+
coverage: {
|
|
30
|
+
reporter: ['html', 'json'],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
plugins: [vue()],
|
|
34
|
+
resolve: {
|
|
35
|
+
alias: {
|
|
36
|
+
'@': fileURLToPath(new URL('../src', import.meta.url)),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|