@prosopo/locale 3.6.2 → 3.8.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.
Files changed (106) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +3 -6
  2. package/.turbo/turbo-build$colon$tsc.log +6 -9
  3. package/.turbo/turbo-build.log +5 -11
  4. package/CHANGELOG.md +43 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/locales/en/translation.cjs +24 -2
  7. package/dist/locales/ar/translation.json +24 -2
  8. package/dist/locales/az/translation.json +24 -2
  9. package/dist/locales/cs/translation.json +24 -2
  10. package/dist/locales/de/translation.json +24 -2
  11. package/dist/locales/el/translation.json +24 -2
  12. package/dist/locales/en/translation.js +24 -2
  13. package/dist/locales/en/translation.json +24 -2
  14. package/dist/locales/es/translation.json +24 -2
  15. package/dist/locales/fi/translation.json +24 -2
  16. package/dist/locales/fr/translation.json +24 -2
  17. package/dist/locales/hi/translation.json +24 -2
  18. package/dist/locales/hu/translation.json +24 -2
  19. package/dist/locales/id/translation.json +24 -2
  20. package/dist/locales/it/translation.json +24 -2
  21. package/dist/locales/ja/translation.json +24 -2
  22. package/dist/locales/jv/translation.json +24 -2
  23. package/dist/locales/ko/translation.json +24 -2
  24. package/dist/locales/ml/translation.json +24 -2
  25. package/dist/locales/ms/translation.json +24 -2
  26. package/dist/locales/nl/translation.json +24 -2
  27. package/dist/locales/no/translation.json +24 -2
  28. package/dist/locales/pl/translation.json +24 -2
  29. package/dist/locales/pt/translation.json +24 -2
  30. package/dist/locales/pt-BR/translation.json +24 -2
  31. package/dist/locales/ro/translation.json +24 -2
  32. package/dist/locales/ru/translation.json +24 -2
  33. package/dist/locales/sr/translation.json +24 -2
  34. package/dist/locales/sv/translation.json +24 -2
  35. package/dist/locales/th/translation.json +24 -2
  36. package/dist/locales/tr/translation.json +24 -2
  37. package/dist/locales/uk/translation.json +24 -2
  38. package/dist/locales/vi/translation.json +24 -2
  39. package/dist/locales/zh-CN/translation.json +24 -2
  40. package/package.json +65 -66
  41. package/src/direction.ts +0 -36
  42. package/src/errorMessage.ts +0 -37
  43. package/src/i18SharedOptions.ts +0 -29
  44. package/src/i18nBackend.ts +0 -69
  45. package/src/i18nFrontend.ts +0 -267
  46. package/src/i18nMiddleware.ts +0 -35
  47. package/src/index.ts +0 -33
  48. package/src/loadI18next.ts +0 -136
  49. package/src/locales/ar/translation.json +0 -493
  50. package/src/locales/az/translation.json +0 -493
  51. package/src/locales/cs/translation.json +0 -493
  52. package/src/locales/de/translation.json +0 -493
  53. package/src/locales/el/translation.json +0 -493
  54. package/src/locales/en/translation.json +0 -493
  55. package/src/locales/es/translation.json +0 -493
  56. package/src/locales/fi/translation.json +0 -493
  57. package/src/locales/fr/translation.json +0 -493
  58. package/src/locales/hi/translation.json +0 -493
  59. package/src/locales/hu/translation.json +0 -493
  60. package/src/locales/id/translation.json +0 -493
  61. package/src/locales/it/translation.json +0 -493
  62. package/src/locales/ja/translation.json +0 -493
  63. package/src/locales/jv/translation.json +0 -493
  64. package/src/locales/ko/translation.json +0 -493
  65. package/src/locales/ml/translation.json +0 -493
  66. package/src/locales/ms/translation.json +0 -493
  67. package/src/locales/nl/translation.json +0 -493
  68. package/src/locales/no/translation.json +0 -493
  69. package/src/locales/pl/translation.json +0 -493
  70. package/src/locales/pt/translation.json +0 -493
  71. package/src/locales/pt-BR/translation.json +0 -493
  72. package/src/locales/ro/translation.json +0 -493
  73. package/src/locales/ru/translation.json +0 -493
  74. package/src/locales/sr/translation.json +0 -493
  75. package/src/locales/sv/translation.json +0 -493
  76. package/src/locales/th/translation.json +0 -493
  77. package/src/locales/tr/translation.json +0 -493
  78. package/src/locales/uk/translation.json +0 -493
  79. package/src/locales/vi/translation.json +0 -493
  80. package/src/locales/zh-CN/translation.json +0 -493
  81. package/src/process.ts +0 -29
  82. package/src/tests/direction.unit.test.ts +0 -52
  83. package/src/tests/errorMessage.unit.test.ts +0 -84
  84. package/src/tests/i18SharedOptions.unit.test.ts +0 -139
  85. package/src/tests/i18nBackend.unit.test.ts +0 -241
  86. package/src/tests/i18nFrontend.unit.test.ts +0 -404
  87. package/src/tests/i18nMiddleware.unit.test.ts +0 -159
  88. package/src/tests/loadI18next.unit.test.ts +0 -443
  89. package/src/tests/locale.test-d.ts +0 -184
  90. package/src/tests/localePlaceholders.unit.test.ts +0 -81
  91. package/src/tests/locales.unit.test.ts +0 -79
  92. package/src/tests/process.unit.test.ts +0 -33
  93. package/src/tests/thrownErrorKeys.unit.test.ts +0 -91
  94. package/src/tests/translationKey.unit.test.ts +0 -155
  95. package/src/tests/translations.unit.test.ts +0 -97
  96. package/src/tests/translator.unit.test.ts +0 -213
  97. package/src/tests/util.unit.test.ts +0 -102
  98. package/src/translationKey.ts +0 -73
  99. package/src/translations.ts +0 -55
  100. package/src/translator.ts +0 -69
  101. package/src/types.ts +0 -47
  102. package/src/util.ts +0 -25
  103. package/tsconfig.cjs.json +0 -21
  104. package/tsconfig.json +0 -22
  105. package/tsconfig.tsbuildinfo +0 -1
  106. package/tsconfig.types.json +0 -9
@@ -1,139 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
16
- import { Languages } from "../translations.js";
17
-
18
- // The module reads process.env at import time, so every expectation about the
19
- // debug flag has to re-import it under a fresh module registry.
20
- type SharedOptions =
21
- typeof import("../i18SharedOptions.js")["i18nSharedOptions"];
22
-
23
- const loadOptions = async (): Promise<SharedOptions> => {
24
- vi.resetModules();
25
- const { i18nSharedOptions } = await import("../i18SharedOptions.js");
26
- return i18nSharedOptions;
27
- };
28
-
29
- const originalLogLevel = process.env.PROSOPO_LOG_LEVEL;
30
-
31
- beforeEach(() => {
32
- Reflect.deleteProperty(process.env, "PROSOPO_LOG_LEVEL");
33
- });
34
-
35
- afterEach(() => {
36
- vi.doUnmock("../process.js");
37
- vi.resetModules();
38
- if (originalLogLevel === undefined) {
39
- Reflect.deleteProperty(process.env, "PROSOPO_LOG_LEVEL");
40
- } else {
41
- process.env.PROSOPO_LOG_LEVEL = originalLogLevel;
42
- }
43
- });
44
-
45
- describe("i18nSharedOptions", () => {
46
- test("falls back to English", async () => {
47
- const options = await loadOptions();
48
- expect(options.fallbackLng).toBe("en");
49
- });
50
-
51
- test("uses the single translation namespace", async () => {
52
- const options = await loadOptions();
53
- expect(options.namespace).toBe("translation");
54
- });
55
-
56
- test("supports exactly the declared language codes", async () => {
57
- const options = await loadOptions();
58
- expect(new Set(options.supportedLngs)).toEqual(
59
- new Set(Object.values(Languages)),
60
- );
61
- });
62
-
63
- // With this false, i18next will not silently serve `en` resources for a
64
- // request for `en-GB`. Flipping it would quietly change which file a
65
- // regional locale resolves to.
66
- test("does not accept non-explicit regional variants", async () => {
67
- const options = await loadOptions();
68
- expect(options.nonExplicitSupportedLngs).toBe(false);
69
- });
70
-
71
- describe("debug flag", () => {
72
- test("is off when PROSOPO_LOG_LEVEL is unset", async () => {
73
- const options = await loadOptions();
74
- expect(options.debug).toBe(false);
75
- });
76
-
77
- test("is on only for the exact value 'debug'", async () => {
78
- process.env.PROSOPO_LOG_LEVEL = "debug";
79
- expect((await loadOptions()).debug).toBe(true);
80
- });
81
-
82
- test("is off for any other log level", async () => {
83
- for (const level of ["info", "warn", "error", "trace", ""]) {
84
- process.env.PROSOPO_LOG_LEVEL = level;
85
- expect((await loadOptions()).debug, level).toBe(false);
86
- }
87
- });
88
-
89
- test("is case sensitive — 'DEBUG' does not enable it", async () => {
90
- process.env.PROSOPO_LOG_LEVEL = "DEBUG";
91
- expect((await loadOptions()).debug).toBe(false);
92
- });
93
- });
94
-
95
- // This module is reached transitively from browser bundles, where `process`
96
- // is absent or partial, so both degraded shapes have to load cleanly.
97
- //
98
- // The `process` read goes through getProcess() precisely so it can be mocked
99
- // here. Stubbing the global instead is not viable: vitest itself calls
100
- // process.memoryUsage() between suites, so removing the real global fails
101
- // the run rather than the module under test.
102
- describe("browser runtime, where process may be absent or partial", () => {
103
- const mockProcess = (value: NodeJS.Process | undefined): void => {
104
- vi.resetModules();
105
- vi.doMock("../process.js", () => ({
106
- getProcess: (): NodeJS.Process | undefined => value,
107
- }));
108
- };
109
-
110
- test("loads when there is no process at all", async () => {
111
- mockProcess(undefined);
112
- const { i18nSharedOptions } = await import("../i18SharedOptions.js");
113
- expect(i18nSharedOptions.debug).toBe(false);
114
- expect(i18nSharedOptions.fallbackLng).toBe("en");
115
- });
116
-
117
- test("loads when process exists but exposes no env", async () => {
118
- // Prototype-chained off the real process so the shape stays honest;
119
- // only `env` is shadowed away.
120
- const partial: NodeJS.Process = Object.create(process);
121
- Reflect.defineProperty(partial, "env", {
122
- value: undefined,
123
- configurable: true,
124
- });
125
- mockProcess(partial);
126
-
127
- const { i18nSharedOptions } = await import("../i18SharedOptions.js");
128
- expect(i18nSharedOptions.debug).toBe(false);
129
- expect(i18nSharedOptions.fallbackLng).toBe("en");
130
- });
131
-
132
- test("still reads the log level when process is present", async () => {
133
- process.env.PROSOPO_LOG_LEVEL = "debug";
134
- mockProcess(process);
135
- const { i18nSharedOptions } = await import("../i18SharedOptions.js");
136
- expect(i18nSharedOptions.debug).toBe(true);
137
- });
138
- });
139
- });
@@ -1,241 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { InitOptions, i18n } from "i18next";
16
- import { afterEach, describe, expect, test, vi } from "vitest";
17
-
18
- // i18next is a mutable singleton and the module under test configures it as a
19
- // side effect of the first call. Both are mocked so each test starts from a
20
- // known `isInitialized` state without a real filesystem backend.
21
-
22
- type Listener = (...args: unknown[]) => void;
23
-
24
- interface FakeI18n {
25
- isInitialized: boolean;
26
- use: (plugin: unknown) => FakeI18n;
27
- init: (options: InitOptions) => FakeI18n;
28
- on: (event: string, listener: Listener) => void;
29
- }
30
-
31
- interface Harness {
32
- fake: FakeI18n;
33
- plugins: unknown[];
34
- initOptions: InitOptions[];
35
- listeners: Map<string, Listener[]>;
36
- emit: (event: string) => void;
37
- }
38
-
39
- const makeHarness = (isInitialized: boolean): Harness => {
40
- const plugins: unknown[] = [];
41
- const initOptions: InitOptions[] = [];
42
- const listeners = new Map<string, Listener[]>();
43
-
44
- const fake: FakeI18n = {
45
- isInitialized,
46
- use: (plugin: unknown): FakeI18n => {
47
- plugins.push(plugin);
48
- return fake;
49
- },
50
- init: (options: InitOptions): FakeI18n => {
51
- initOptions.push(options);
52
- return fake;
53
- },
54
- on: (event: string, listener: Listener): void => {
55
- listeners.set(event, [...(listeners.get(event) ?? []), listener]);
56
- },
57
- };
58
-
59
- return {
60
- fake,
61
- plugins,
62
- initOptions,
63
- listeners,
64
- emit: (event: string): void => {
65
- for (const listener of listeners.get(event) ?? []) {
66
- listener();
67
- }
68
- },
69
- };
70
- };
71
-
72
- /**
73
- * Load the module under a fresh registry with i18next, the filesystem backend
74
- * and the side detection all mocked.
75
- */
76
- const load = async (options: {
77
- isInitialized?: boolean;
78
- serverSide?: boolean;
79
- }): Promise<{
80
- initializeI18n: (callback?: (value: i18n) => void) => i18n;
81
- harness: Harness;
82
- }> => {
83
- vi.resetModules();
84
- const harness = makeHarness(options.isInitialized ?? false);
85
-
86
- vi.doMock("i18next", () => ({ default: harness.fake }));
87
- vi.doMock("i18next-fs-backend/cjs", () => ({ default: "FSBackend" }));
88
- vi.doMock("i18next-http-middleware", () => ({
89
- LanguageDetector: class {
90
- options: unknown;
91
- constructor(_services: unknown, opts: unknown) {
92
- this.options = opts;
93
- }
94
- },
95
- }));
96
- vi.doMock("../util.js", () => ({
97
- isServerSide: (): boolean => options.serverSide ?? true,
98
- isClientSide: (): boolean => !(options.serverSide ?? true),
99
- }));
100
-
101
- const imported = await import("../i18nBackend.js");
102
- return { initializeI18n: imported.default, harness };
103
- };
104
-
105
- afterEach(() => {
106
- vi.resetModules();
107
- vi.doUnmock("i18next");
108
- vi.doUnmock("i18next-fs-backend/cjs");
109
- vi.doUnmock("i18next-http-middleware");
110
- vi.doUnmock("../util.js");
111
- });
112
-
113
- describe("i18nBackend on a server, not yet initialised", () => {
114
- test("initialises i18next exactly once", async () => {
115
- const { initializeI18n, harness } = await load({});
116
- initializeI18n();
117
- expect(harness.initOptions).toHaveLength(1);
118
- });
119
-
120
- test("registers the filesystem backend and the header language detector", async () => {
121
- // The detector is what makes Accept-Language work; losing it would
122
- // silently serve every request in the fallback language.
123
- const { initializeI18n, harness } = await load({});
124
- initializeI18n();
125
- expect(harness.plugins).toHaveLength(2);
126
- expect(harness.plugins[0]).toBe("FSBackend");
127
- });
128
-
129
- test("carries the shared options through to init", async () => {
130
- const { initializeI18n, harness } = await load({});
131
- initializeI18n();
132
- const options = harness.initOptions[0];
133
- expect(options?.fallbackLng).toBe("en");
134
- expect(options?.supportedLngs).toContain("en");
135
- expect(options?.nonExplicitSupportedLngs).toBe(false);
136
- });
137
-
138
- test("declares the translation namespace", async () => {
139
- const { initializeI18n, harness } = await load({});
140
- initializeI18n();
141
- expect(harness.initOptions[0]?.ns).toEqual(["translation"]);
142
- });
143
-
144
- test("points the backend at a filesystem path, not a file:// URL", async () => {
145
- // i18next-fs-backend reads with node:fs, which cannot open a file://
146
- // URL — the module strips the scheme for exactly this reason.
147
- const { initializeI18n, harness } = await load({});
148
- initializeI18n();
149
- const backend = harness.initOptions[0]?.backend as
150
- | { loadPath?: string }
151
- | undefined;
152
- expect(backend?.loadPath).toBeDefined();
153
- expect(backend?.loadPath).not.toContain("file://");
154
- expect(backend?.loadPath).toContain("/locales/{{lng}}/{{ns}}.json");
155
- });
156
-
157
- test("defers the callback until resources have loaded", async () => {
158
- const callback = vi.fn();
159
- const { initializeI18n, harness } = await load({});
160
-
161
- initializeI18n(callback);
162
- expect(callback).not.toHaveBeenCalled();
163
-
164
- harness.emit("loaded");
165
- expect(callback).toHaveBeenCalledTimes(1);
166
- });
167
-
168
- test("fires the callback once, however often `loaded` is emitted", async () => {
169
- // i18next emits `loaded` per namespace and again after changeLanguage,
170
- // so an unguarded listener would repeat whatever side effect the caller
171
- // attached to it.
172
- const callback = vi.fn();
173
- const { initializeI18n, harness } = await load({});
174
-
175
- initializeI18n(callback);
176
- harness.emit("loaded");
177
- harness.emit("loaded");
178
- harness.emit("loaded");
179
- expect(callback).toHaveBeenCalledTimes(1);
180
- });
181
-
182
- test("hands the i18next instance to the callback", async () => {
183
- const callback = vi.fn();
184
- const { initializeI18n, harness } = await load({});
185
- initializeI18n(callback);
186
- harness.emit("loaded");
187
- expect(callback).toHaveBeenCalledWith(harness.fake);
188
- });
189
-
190
- test("returns the i18next instance", async () => {
191
- const { initializeI18n, harness } = await load({});
192
- expect(initializeI18n()).toBe(harness.fake);
193
- });
194
-
195
- test("tolerates being called with no callback at all", async () => {
196
- const { initializeI18n, harness } = await load({});
197
- initializeI18n();
198
- expect(() => harness.emit("loaded")).not.toThrow();
199
- });
200
- });
201
-
202
- // Both of these skip initialisation. The `loaded` listener is only registered
203
- // on the init path, so without an explicit callback here the caller
204
- // (loadI18next) would wait forever for an event that is never coming.
205
- describe("i18nBackend when there is nothing to initialise", () => {
206
- test("fires the callback immediately when i18next is already up", async () => {
207
- const callback = vi.fn();
208
- const { initializeI18n, harness } = await load({ isInitialized: true });
209
-
210
- const returned = initializeI18n(callback);
211
-
212
- expect(harness.initOptions).toHaveLength(0);
213
- expect(callback).toHaveBeenCalledTimes(1);
214
- expect(callback).toHaveBeenCalledWith(harness.fake);
215
- expect(returned).toBe(harness.fake);
216
- });
217
-
218
- test("fires the callback immediately when running client side", async () => {
219
- // The filesystem backend cannot work in a browser, so initialisation is
220
- // correctly skipped — but the caller still has to be released.
221
- const callback = vi.fn();
222
- const { initializeI18n, harness } = await load({ serverSide: false });
223
-
224
- const returned = initializeI18n(callback);
225
-
226
- expect(harness.initOptions).toHaveLength(0);
227
- expect(callback).toHaveBeenCalledTimes(1);
228
- expect(returned).toBe(harness.fake);
229
- });
230
-
231
- test("registers no plugins when initialisation is skipped", async () => {
232
- const { initializeI18n, harness } = await load({ isInitialized: true });
233
- initializeI18n();
234
- expect(harness.plugins).toHaveLength(0);
235
- });
236
-
237
- test("tolerates no callback when initialisation is skipped", async () => {
238
- const { initializeI18n } = await load({ isInitialized: true });
239
- expect(() => initializeI18n()).not.toThrow();
240
- });
241
- });