@prosopo/locale 3.6.1 → 3.7.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 (144) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +8 -9
  2. package/.turbo/turbo-build$colon$tsc.log +6 -9
  3. package/.turbo/turbo-build.log +12 -16
  4. package/CHANGELOG.md +40 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/direction.cjs +19 -0
  7. package/dist/cjs/errorMessage.cjs +15 -0
  8. package/dist/cjs/index.cjs +5 -0
  9. package/dist/cjs/locales/en/translation.cjs +23 -5
  10. package/dist/cjs/translationKey.cjs +4 -0
  11. package/dist/direction.d.ts +3 -0
  12. package/dist/direction.d.ts.map +1 -0
  13. package/dist/direction.js +19 -0
  14. package/dist/direction.js.map +1 -0
  15. package/dist/errorMessage.d.ts +7 -0
  16. package/dist/errorMessage.d.ts.map +1 -0
  17. package/dist/errorMessage.js +15 -0
  18. package/dist/errorMessage.js.map +1 -0
  19. package/dist/index.d.ts +5 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/locales/ar/translation.json +23 -5
  24. package/dist/locales/az/translation.json +23 -5
  25. package/dist/locales/cs/translation.json +23 -5
  26. package/dist/locales/de/translation.json +23 -5
  27. package/dist/locales/el/translation.json +23 -5
  28. package/dist/locales/en/translation.js +23 -5
  29. package/dist/locales/en/translation.json +23 -5
  30. package/dist/locales/es/translation.json +23 -5
  31. package/dist/locales/fi/translation.json +23 -5
  32. package/dist/locales/fr/translation.json +23 -5
  33. package/dist/locales/hi/translation.json +23 -5
  34. package/dist/locales/hu/translation.json +23 -5
  35. package/dist/locales/id/translation.json +23 -5
  36. package/dist/locales/it/translation.json +23 -5
  37. package/dist/locales/ja/translation.json +23 -5
  38. package/dist/locales/jv/translation.json +23 -5
  39. package/dist/locales/ko/translation.json +23 -5
  40. package/dist/locales/ml/translation.json +23 -5
  41. package/dist/locales/ms/translation.json +23 -5
  42. package/dist/locales/nl/translation.json +23 -5
  43. package/dist/locales/no/translation.json +23 -5
  44. package/dist/locales/pl/translation.json +23 -5
  45. package/dist/locales/pt/translation.json +23 -5
  46. package/dist/locales/pt-BR/translation.json +23 -5
  47. package/dist/locales/ro/translation.json +23 -5
  48. package/dist/locales/ru/translation.json +23 -5
  49. package/dist/locales/sr/translation.json +23 -5
  50. package/dist/locales/sv/translation.json +23 -5
  51. package/dist/locales/th/translation.json +23 -5
  52. package/dist/locales/tr/translation.json +23 -5
  53. package/dist/locales/uk/translation.json +23 -5
  54. package/dist/locales/vi/translation.json +23 -5
  55. package/dist/locales/zh-CN/translation.json +23 -5
  56. package/dist/tests/direction.unit.test.d.ts +2 -0
  57. package/dist/tests/direction.unit.test.d.ts.map +1 -0
  58. package/dist/tests/direction.unit.test.js +31 -0
  59. package/dist/tests/direction.unit.test.js.map +1 -0
  60. package/dist/tests/errorMessage.unit.test.d.ts +2 -0
  61. package/dist/tests/errorMessage.unit.test.d.ts.map +1 -0
  62. package/dist/tests/errorMessage.unit.test.js +49 -0
  63. package/dist/tests/errorMessage.unit.test.js.map +1 -0
  64. package/dist/tests/i18nFrontend.unit.test.js +22 -11
  65. package/dist/tests/i18nFrontend.unit.test.js.map +1 -1
  66. package/dist/tests/locale.test-d.js +18 -3
  67. package/dist/tests/locale.test-d.js.map +1 -1
  68. package/dist/tests/thrownErrorKeys.unit.test.d.ts +2 -0
  69. package/dist/tests/thrownErrorKeys.unit.test.d.ts.map +1 -0
  70. package/dist/tests/thrownErrorKeys.unit.test.js +59 -0
  71. package/dist/tests/thrownErrorKeys.unit.test.js.map +1 -0
  72. package/dist/tests/translationKey.unit.test.js +13 -1
  73. package/dist/tests/translationKey.unit.test.js.map +1 -1
  74. package/dist/translationKey.d.ts +7 -1
  75. package/dist/translationKey.d.ts.map +1 -1
  76. package/dist/translationKey.js +4 -1
  77. package/dist/translationKey.js.map +1 -1
  78. package/dist/translator.d.ts +2 -1
  79. package/dist/translator.d.ts.map +1 -1
  80. package/dist/translator.js.map +1 -1
  81. package/dist/types.d.ts +2 -1
  82. package/dist/types.d.ts.map +1 -1
  83. package/package.json +65 -66
  84. package/src/i18SharedOptions.ts +0 -29
  85. package/src/i18nBackend.ts +0 -69
  86. package/src/i18nFrontend.ts +0 -267
  87. package/src/i18nMiddleware.ts +0 -35
  88. package/src/index.ts +0 -29
  89. package/src/loadI18next.ts +0 -136
  90. package/src/locales/ar/translation.json +0 -476
  91. package/src/locales/az/translation.json +0 -476
  92. package/src/locales/cs/translation.json +0 -476
  93. package/src/locales/de/translation.json +0 -476
  94. package/src/locales/el/translation.json +0 -476
  95. package/src/locales/en/translation.json +0 -476
  96. package/src/locales/es/translation.json +0 -476
  97. package/src/locales/fi/translation.json +0 -476
  98. package/src/locales/fr/translation.json +0 -476
  99. package/src/locales/hi/translation.json +0 -476
  100. package/src/locales/hu/translation.json +0 -476
  101. package/src/locales/id/translation.json +0 -476
  102. package/src/locales/it/translation.json +0 -476
  103. package/src/locales/ja/translation.json +0 -476
  104. package/src/locales/jv/translation.json +0 -476
  105. package/src/locales/ko/translation.json +0 -476
  106. package/src/locales/ml/translation.json +0 -476
  107. package/src/locales/ms/translation.json +0 -476
  108. package/src/locales/nl/translation.json +0 -476
  109. package/src/locales/no/translation.json +0 -476
  110. package/src/locales/pl/translation.json +0 -476
  111. package/src/locales/pt/translation.json +0 -476
  112. package/src/locales/pt-BR/translation.json +0 -476
  113. package/src/locales/ro/translation.json +0 -476
  114. package/src/locales/ru/translation.json +0 -476
  115. package/src/locales/sr/translation.json +0 -476
  116. package/src/locales/sv/translation.json +0 -476
  117. package/src/locales/th/translation.json +0 -476
  118. package/src/locales/tr/translation.json +0 -476
  119. package/src/locales/uk/translation.json +0 -476
  120. package/src/locales/vi/translation.json +0 -476
  121. package/src/locales/zh-CN/translation.json +0 -476
  122. package/src/process.ts +0 -29
  123. package/src/tests/i18SharedOptions.unit.test.ts +0 -139
  124. package/src/tests/i18nBackend.unit.test.ts +0 -241
  125. package/src/tests/i18nFrontend.unit.test.ts +0 -393
  126. package/src/tests/i18nMiddleware.unit.test.ts +0 -159
  127. package/src/tests/loadI18next.unit.test.ts +0 -443
  128. package/src/tests/locale.test-d.ts +0 -160
  129. package/src/tests/localePlaceholders.unit.test.ts +0 -81
  130. package/src/tests/locales.unit.test.ts +0 -79
  131. package/src/tests/process.unit.test.ts +0 -33
  132. package/src/tests/translationKey.unit.test.ts +0 -140
  133. package/src/tests/translations.unit.test.ts +0 -97
  134. package/src/tests/translator.unit.test.ts +0 -213
  135. package/src/tests/util.unit.test.ts +0 -102
  136. package/src/translationKey.ts +0 -64
  137. package/src/translations.ts +0 -55
  138. package/src/translator.ts +0 -68
  139. package/src/types.ts +0 -42
  140. package/src/util.ts +0 -25
  141. package/tsconfig.cjs.json +0 -21
  142. package/tsconfig.json +0 -22
  143. package/tsconfig.tsbuildinfo +0 -1
  144. package/tsconfig.types.json +0 -9
@@ -1,160 +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
- // Asserted against the package entrypoint, which is what consumers import — a
16
- // barrel that stops re-exporting something fails here rather than at a
17
- // downstream build.
18
-
19
- import { assertType, describe, expectTypeOf, it } from "vitest";
20
- import {
21
- type Language,
22
- LanguageCodes,
23
- Languages,
24
- type Ti18n,
25
- type TranslationKey,
26
- createTranslator,
27
- i18nMiddleware,
28
- isClientSide,
29
- isLanguage,
30
- loadI18next,
31
- translationKeys,
32
- } from "../index.js";
33
-
34
- describe("Languages", () => {
35
- it("is a frozen const map of name to code, not a widened record", () => {
36
- // `as const` is what lets `Language` be the union of the codes.
37
- // Widening this to Record<string, string> would silently reduce it to
38
- // `string`.
39
- expectTypeOf(Languages.english).toEqualTypeOf<"en">();
40
- expectTypeOf(Languages.chinese).toEqualTypeOf<"zh-CN">();
41
- });
42
-
43
- it("rejects assignment to a code", () => {
44
- // @ts-expect-error Languages is const
45
- Languages.english = "fr";
46
- });
47
-
48
- it("has no member for a language that is not shipped", () => {
49
- // @ts-expect-error klingon is not a supported language
50
- Languages.klingon;
51
- });
52
- });
53
-
54
- describe("isLanguage", () => {
55
- it("narrows an arbitrary string to a declared code", () => {
56
- expectTypeOf(isLanguage).guards.toEqualTypeOf<Language>();
57
- });
58
-
59
- it("accepts any string at the boundary — input is untrusted markup", () => {
60
- expectTypeOf(isLanguage).toBeCallableWith("anything");
61
- });
62
-
63
- it("is the union of the declared codes, not a widened string", () => {
64
- expectTypeOf<Language>().toExtend<string>();
65
- assertType<Language>("en");
66
- });
67
- });
68
-
69
- describe("TranslationKey", () => {
70
- it("is a plain string — keys are read from JSON, never narrowed", () => {
71
- expectTypeOf<TranslationKey>().toEqualTypeOf<string>();
72
- });
73
-
74
- it("accepts a key literal, so error call sites need no cast", () => {
75
- // packages/common's ProsopoError takes TranslationKey and is called
76
- // with literals throughout.
77
- assertType<TranslationKey>("API.UNKNOWN");
78
- });
79
-
80
- it("exposes the derived keys as an array", () => {
81
- // What the mongoose schemas in types-database put in their `enum` fields.
82
- expectTypeOf(translationKeys).toExtend<readonly string[]>();
83
- });
84
-
85
- it("exposes the declared language codes as an array", () => {
86
- expectTypeOf(LanguageCodes).toExtend<readonly string[]>();
87
- });
88
- });
89
-
90
- describe("isClientSide", () => {
91
- it("takes no arguments and returns a boolean", () => {
92
- expectTypeOf(isClientSide).parameters.toEqualTypeOf<[]>();
93
- expectTypeOf(isClientSide).returns.toEqualTypeOf<boolean>();
94
- });
95
- });
96
-
97
- describe("loadI18next", () => {
98
- it("requires the backend flag", () => {
99
- expectTypeOf(loadI18next).toBeCallableWith(true);
100
- expectTypeOf(loadI18next).toBeCallableWith(false, "fr");
101
- });
102
-
103
- it("rejects a call with no arguments", () => {
104
- // @ts-expect-error the backend flag is required
105
- loadI18next();
106
- });
107
-
108
- it("rejects a non-boolean first argument", () => {
109
- // Catches the easy mistake of passing the language positionally.
110
- // @ts-expect-error the first parameter is the backend flag
111
- loadI18next("fr");
112
- });
113
-
114
- it("resolves to the i18next instance type the package re-exports", () => {
115
- expectTypeOf(loadI18next).returns.toEqualTypeOf<Promise<Ti18n>>();
116
- });
117
- });
118
-
119
- describe("Ti18n", () => {
120
- it("carries the i18next surface consumers need", () => {
121
- expectTypeOf<Ti18n>().toHaveProperty("t");
122
- expectTypeOf<Ti18n>().toHaveProperty("language");
123
- expectTypeOf<Ti18n>().toHaveProperty("changeLanguage");
124
- });
125
- });
126
-
127
- describe("i18nMiddleware", () => {
128
- it("takes handler options and resolves to an express handler", () => {
129
- expectTypeOf(i18nMiddleware).toBeCallableWith({});
130
- expectTypeOf(i18nMiddleware).returns.toExtend<Promise<unknown>>();
131
- });
132
-
133
- it("rejects an unknown option", () => {
134
- // @ts-expect-error notAnOption is not part of HandleOptions
135
- i18nMiddleware({ notAnOption: true });
136
- });
137
-
138
- it("rejects being called with no options", () => {
139
- // @ts-expect-error options are required
140
- i18nMiddleware();
141
- });
142
- });
143
-
144
- describe("createTranslator", () => {
145
- it("is callable with no instance, so a standalone widget can boot one", () => {
146
- expectTypeOf(createTranslator).toBeCallableWith();
147
- });
148
-
149
- it("returns the translator surface the widgets render against", () => {
150
- expectTypeOf(createTranslator).returns.toHaveProperty("t");
151
- expectTypeOf(createTranslator).returns.toHaveProperty("isReady");
152
- expectTypeOf(createTranslator).returns.toHaveProperty("subscribe");
153
- expectTypeOf(createTranslator).returns.toHaveProperty("i18n");
154
- });
155
-
156
- it("rejects anything that is not an i18next instance", () => {
157
- // @ts-expect-error the only parameter is an existing i18next instance
158
- createTranslator({ notAnInstance: 1 });
159
- });
160
- });
@@ -1,81 +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 fs from "node:fs";
16
- import path from "node:path";
17
- import { describe, expect, it } from "vitest";
18
- import { Languages } from "../index.js";
19
-
20
- type TranslationTree = { [key: string]: string | TranslationTree };
21
-
22
- const readLocale = (locale: string): TranslationTree =>
23
- JSON.parse(
24
- fs.readFileSync(
25
- path.resolve(`./src/locales/${locale}/translation.json`),
26
- "utf8",
27
- ),
28
- );
29
-
30
- const flatten = (tree: TranslationTree, prefix = ""): Array<[string, string]> =>
31
- Object.entries(tree).flatMap(
32
- ([key, value]): Array<[string, string]> =>
33
- typeof value === "string"
34
- ? [[`${prefix}${key}`, value]]
35
- : flatten(value, `${prefix}${key}.`),
36
- );
37
-
38
- const placeholders = (text: string): string[] =>
39
- (text.match(/\{\{[^}]*\}\}/g) ?? [])
40
- .map((token: string) => token.replace(/\s+/g, ""))
41
- .sort();
42
-
43
- const en = new Map<string, string>(flatten(readLocale("en")));
44
- const locales: string[] = Object.values(Languages).filter(
45
- (locale: string) => locale !== "en",
46
- );
47
-
48
- describe("locale interpolation placeholders", () => {
49
- it.each(locales)(
50
- "%s keeps every {{placeholder}} of the en source",
51
- (locale: string) => {
52
- const mismatches: string[] = flatten(readLocale(locale))
53
- .filter(
54
- ([key, value]: [string, string]) =>
55
- en.has(key) &&
56
- placeholders(value).join() !==
57
- placeholders(en.get(key) ?? "").join(),
58
- )
59
- .map(([key, value]: [string, string]) => `${key}: ${value}`);
60
- expect(mismatches).toEqual([]);
61
- },
62
- );
63
- });
64
-
65
- // A tool once stripped every Devanagari vowel sign and virama from the Hindi
66
- // file ("मैं मनुष्य हूँ" became "म मनषय ह"), which the key and placeholder
67
- // checks cannot see.
68
- describe("hi translations", () => {
69
- it("keep their Devanagari vowel signs", () => {
70
- const devanagariLetter = /[ऄ-हक़-ॡ]/gu;
71
- const sign = /[ऀ-ःऺ-ॏॕ-ॗॢॣ]/u;
72
- const stripped: string[] = flatten(readLocale("hi"))
73
- .filter(
74
- ([, value]: [string, string]) =>
75
- (value.match(devanagariLetter) ?? []).length >= 4 &&
76
- !sign.test(value),
77
- )
78
- .map(([key, value]: [string, string]) => `${key}: ${value}`);
79
- expect(stripped).toEqual([]);
80
- });
81
- });
@@ -1,79 +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 fs from "node:fs";
16
- import path from "node:path";
17
- import { describe, expect, test } from "vitest";
18
- import { Languages } from "../index.js";
19
-
20
- describe("logging", () => {
21
- test("Make sure the number of files present in the locales folder is the same size as the Languages array", () => {
22
- const locales = fs.readdirSync(path.resolve("./src/locales"));
23
- expect(locales.length).to.equal(Object.keys(Languages).length);
24
- });
25
-
26
- test("Get all locale JSON files and ensure the keys are identical in each one", () => {
27
- const locales = Object.values(Languages);
28
- const sectionKeys = new Set<string>();
29
- console.log("Local test running at ", path.resolve("."));
30
- const sectionKeysObj: { [key: string]: Set<string> } = {};
31
- const innerKeysObj: { [key: string]: { [key: string]: Set<string> } } = {};
32
- for (const locale of locales) {
33
- const localeData = fs.readFileSync(
34
- path.resolve(`./src/locales/${locale}/translation.json`),
35
- "utf8",
36
- );
37
- const localeKeys = Object.keys(JSON.parse(localeData));
38
- for (const sectionKey of localeKeys) {
39
- sectionKeys.add(sectionKey);
40
- }
41
- sectionKeysObj[locale] = new Set(localeKeys);
42
- for (const sectionKey of localeKeys) {
43
- const innerKeys = JSON.parse(localeData)[sectionKey];
44
- if (typeof innerKeys !== "object") {
45
- continue;
46
- }
47
-
48
- if (!innerKeysObj[locale]) {
49
- innerKeysObj[locale] = {};
50
- }
51
-
52
- // @ts-ignore
53
- if (!innerKeysObj[locale][sectionKey]) {
54
- // @ts-ignore
55
- innerKeysObj[locale][sectionKey] = new Set();
56
- }
57
-
58
- for (const innerKey of Object.keys(innerKeys)) {
59
- // @ts-ignore
60
- innerKeysObj[locale][sectionKey].add(innerKey);
61
- }
62
- }
63
- }
64
-
65
- for (const locale of Array.from(locales)) {
66
- console.log("Checking", locale);
67
- expect(sectionKeysObj[locale]).to.deep.equal(sectionKeysObj.en);
68
- // @ts-ignore
69
- for (const sectionKey of Object.keys(innerKeysObj[locale])) {
70
- console.log(`Checking ${locale} ${sectionKey}`);
71
- // @ts-ignore
72
- expect(innerKeysObj[locale][sectionKey]).to.deep.equal(
73
- // @ts-ignore
74
- innerKeysObj.en[sectionKey],
75
- );
76
- }
77
- }
78
- });
79
- });
@@ -1,33 +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 { describe, expect, test } from "vitest";
16
- import { getProcess } from "../process.js";
17
-
18
- describe("getProcess", () => {
19
- // Under vitest there is always a real `process`, so only this arm is
20
- // directly observable here. The undefined arm is what consumers mock, and
21
- // i18SharedOptions.unit.test.ts exercises their behaviour against it.
22
- test("returns the process global when one exists", () => {
23
- expect(getProcess()).toBe(process);
24
- });
25
-
26
- test("exposes env, which is the only member consumers use", () => {
27
- expect(getProcess()?.env).toBe(process.env);
28
- });
29
-
30
- test("is stable across calls rather than returning a copy", () => {
31
- expect(getProcess()).toBe(getProcess());
32
- });
33
- });
@@ -1,140 +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 { describe, expect, test } from "vitest";
16
- import translationEn from "../locales/en/translation.json" with {
17
- type: "json",
18
- };
19
- import {
20
- type TranslationNode,
21
- getLeafFieldPath,
22
- translationKeys,
23
- } from "../translationKey.js";
24
-
25
- describe("getLeafFieldPath", () => {
26
- test("returns no paths for a bare string, which is a leaf and not a container", () => {
27
- expect(getLeafFieldPath("a string")).toEqual([]);
28
- });
29
-
30
- test("returns no paths for the empty string", () => {
31
- expect(getLeafFieldPath("")).toEqual([]);
32
- });
33
-
34
- // The string is the leaf, so the path ends at its key. This used to yield
35
- // [] — the recursion returned nothing for a string and the parent mapped
36
- // over that empty list — which made the whole function return [] for every
37
- // input and left `translationKeys` empty.
38
- test("emits a top-level string value under its own key", () => {
39
- expect(getLeafFieldPath({ topLevel: "value" })).toEqual(["topLevel"]);
40
- });
41
-
42
- test("emits section.key for a one-level-deep section", () => {
43
- expect(getLeafFieldPath({ section: { key: "value" } })).toEqual([
44
- "section.key",
45
- ]);
46
- });
47
-
48
- test("emits every key of a section, in insertion order", () => {
49
- expect(
50
- getLeafFieldPath({ section: { first: "1", second: "2", third: "3" } }),
51
- ).toEqual(["section.first", "section.second", "section.third"]);
52
- });
53
-
54
- test("recurses to arbitrary depth, joining each level with a dot", () => {
55
- expect(getLeafFieldPath({ a: { b: { c: { d: "leaf" } } } })).toEqual([
56
- "a.b.c.d",
57
- ]);
58
- });
59
-
60
- test("handles sibling branches of differing depth", () => {
61
- expect(
62
- getLeafFieldPath({
63
- shallow: { key: "v" },
64
- deep: { nested: { key: "v" } },
65
- }),
66
- ).toEqual(["shallow.key", "deep.nested.key"]);
67
- });
68
-
69
- test("returns no paths for an empty object", () => {
70
- expect(getLeafFieldPath({})).toEqual([]);
71
- });
72
-
73
- // Same silent-drop hazard as the top-level string: an empty section
74
- // contributes nothing rather than erroring, so a translation file that
75
- // loses all of a section's contents still parses.
76
- test("drops an empty nested section rather than erroring", () => {
77
- expect(getLeafFieldPath({ section: {} })).toEqual([]);
78
- });
79
-
80
- test("keeps populated siblings when one section is empty", () => {
81
- expect(getLeafFieldPath({ empty: {}, full: { key: "v" } })).toEqual([
82
- "full.key",
83
- ]);
84
- });
85
-
86
- // Object.keys never yields a key whose value is undefined by omission, so
87
- // this guard is only reachable via an explicitly-assigned undefined. It
88
- // still has to hold: a JSON file cannot express it, but a caller passing a
89
- // hand-built object can, and a silently-skipped key would corrupt the
90
- // schema.
91
- test("throws on an explicitly undefined value, naming the offending key", () => {
92
- const node = {
93
- section: { broken: undefined },
94
- } as unknown as TranslationNode;
95
- expect(() => getLeafFieldPath(node)).toThrow(
96
- "Undefined value for key broken",
97
- );
98
- });
99
-
100
- test("throws before emitting any of the sibling keys of the broken one", () => {
101
- const node = {
102
- section: { ok: "v", broken: undefined },
103
- } as unknown as TranslationNode;
104
- expect(() => getLeafFieldPath(node)).toThrow(/Undefined value/);
105
- });
106
-
107
- test("does not mutate the input", () => {
108
- const node: TranslationNode = { section: { key: "value" } };
109
- const snapshot = JSON.stringify(node);
110
- getLeafFieldPath(node);
111
- expect(JSON.stringify(node)).toBe(snapshot);
112
- });
113
- });
114
-
115
- describe("translationKeys", () => {
116
- const keys = getLeafFieldPath(translationEn as TranslationNode);
117
-
118
- test("is non-empty — mongoose validates against an empty enum too", () => {
119
- expect(keys.length).toBeGreaterThan(0);
120
- });
121
-
122
- test("is exactly the set of keys derived from the English translation", () => {
123
- expect(new Set(translationKeys)).toEqual(new Set(keys));
124
- });
125
-
126
- test("holds leaves, not section names", () => {
127
- const [firstKey] = keys;
128
- expect(firstKey).toBeDefined();
129
- const section = String(firstKey).split(".")[0];
130
- expect(translationKeys).not.toContain(section);
131
- });
132
-
133
- test("does not contain a key absent from the English translation", () => {
134
- expect(translationKeys).not.toContain("no.such.key");
135
- });
136
-
137
- test("does not contain the empty string", () => {
138
- expect(translationKeys).not.toContain("");
139
- });
140
- });
@@ -1,97 +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 fs from "node:fs";
16
- import path from "node:path";
17
- import { describe, expect, test } from "vitest";
18
- import { LanguageCodes, Languages, isLanguage } from "../translations.js";
19
-
20
- const codes: string[] = Object.values(Languages);
21
-
22
- describe("Languages", () => {
23
- test("is non-empty", () => {
24
- expect(codes.length).toBeGreaterThan(0);
25
- });
26
-
27
- test("maps English language names to BCP 47 codes", () => {
28
- expect(Languages.english).toBe("en");
29
- expect(Languages.chinese).toBe("zh-CN");
30
- expect(Languages.portugeseBrazil).toBe("pt-BR");
31
- });
32
-
33
- test("includes the fallback language", () => {
34
- expect(codes).toContain("en");
35
- });
36
-
37
- // A duplicated code would silently make one of the language names
38
- // unreachable, while the locales-folder count test would still pass.
39
- test("has no duplicate codes", () => {
40
- expect(new Set(codes).size).toBe(codes.length);
41
- });
42
-
43
- test("every code is a well-formed, lowercase-primary language tag", () => {
44
- for (const code of codes) {
45
- expect(code).toMatch(/^[a-z]{2}(-[A-Z]{2})?$/);
46
- }
47
- });
48
-
49
- test("every code has a locales directory containing translation.json", () => {
50
- for (const code of codes) {
51
- const file = path.resolve(`./src/locales/${code}/translation.json`);
52
- expect(fs.existsSync(file), `missing ${code}`).toBe(true);
53
- }
54
- });
55
-
56
- test("every locales directory is claimed by a code — no orphans", () => {
57
- const dirs = fs.readdirSync(path.resolve("./src/locales"));
58
- for (const dir of dirs) {
59
- expect(codes, `orphan locale directory ${dir}`).toContain(dir);
60
- }
61
- });
62
- });
63
-
64
- describe("isLanguage", () => {
65
- test("accepts every declared code", () => {
66
- for (const code of codes) {
67
- expect(isLanguage(code)).toBe(true);
68
- }
69
- });
70
-
71
- test("every declared code is in LanguageCodes", () => {
72
- expect(new Set(LanguageCodes)).toEqual(new Set(codes));
73
- });
74
-
75
- test("exposes each code as a constant for use in code", () => {
76
- expect(Languages.english).toBe("en");
77
- });
78
-
79
- test("rejects an undeclared language code", () => {
80
- expect(isLanguage("xx")).toBe(false);
81
- });
82
-
83
- test("rejects the empty string", () => {
84
- expect(isLanguage("")).toBe(false);
85
- });
86
-
87
- // i18next treated `en-GB` as a region of `en`, and the browser i18n resolves
88
- // a regional tag to its base language before asking — so a regional variant
89
- // is deliberately not itself a declared code.
90
- test("rejects a regional variant of a supported language", () => {
91
- expect(isLanguage("en-GB")).toBe(false);
92
- });
93
-
94
- test("is case sensitive — `EN` is not `en`", () => {
95
- expect(isLanguage("EN")).toBe(false);
96
- });
97
- });