@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,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,91 +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
- import fs from "node:fs";
15
- import path from "node:path";
16
- import { describe, expect, test } from "vitest";
17
- import translationEn from "../locales/en/translation.json" with {
18
- type: "json",
19
- };
20
-
21
- // Error classes and ResultReason carry a catalogue key. A key with no entry
22
- // reaches users and logs untranslated, so every one used in the workspace's
23
- // source must exist in the English catalogue (the parity test in
24
- // locales.unit.test.ts carries it to every other locale).
25
-
26
- const PACKAGES_DIR = path.resolve("..");
27
- const THROWN_KEY = /Error\(\s*["']([A-Z][A-Z_]*\.[A-Z0-9_]+)["']/g;
28
- const REASON_KEY = /=\s*"([A-Z][A-Z_]*\.[A-Z0-9_]+)"/g;
29
-
30
- const isCatalogueKey = (key: string): boolean => {
31
- const [section, name] = key.split(".");
32
- const entries: Record<string, unknown> | undefined =
33
- section && section in translationEn
34
- ? translationEn[section as keyof typeof translationEn]
35
- : undefined;
36
- return name !== undefined && entries !== undefined && name in entries;
37
- };
38
-
39
- const sourceFiles = (dir: string): string[] =>
40
- fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry: fs.Dirent) => {
41
- const full = path.join(dir, entry.name);
42
- if (entry.isDirectory()) {
43
- return entry.name === "tests" || entry.name === "node_modules"
44
- ? []
45
- : sourceFiles(full);
46
- }
47
- return entry.name.endsWith(".ts") && !entry.name.includes(".test.")
48
- ? [full]
49
- : [];
50
- });
51
-
52
- const workspaceSources = (): string[] =>
53
- fs
54
- .readdirSync(PACKAGES_DIR)
55
- .map((name: string) => path.join(PACKAGES_DIR, name, "src"))
56
- .filter((dir: string) => fs.existsSync(dir))
57
- .flatMap(sourceFiles);
58
-
59
- const uncataloguedKeys = (files: string[], pattern: RegExp): string[] => {
60
- const missing = new Set<string>();
61
- for (const file of files) {
62
- for (const match of fs.readFileSync(file, "utf8").matchAll(pattern)) {
63
- const key = match[1];
64
- if (key && !isCatalogueKey(key)) {
65
- missing.add(`${key} (${path.relative(PACKAGES_DIR, file)})`);
66
- }
67
- }
68
- }
69
- return [...missing].sort();
70
- };
71
-
72
- describe("error keys", () => {
73
- test("every key an error class is constructed with is in the catalogue", () => {
74
- expect(uncataloguedKeys(workspaceSources(), THROWN_KEY)).toEqual([]);
75
- });
76
-
77
- test("every ResultReason is in the catalogue", () => {
78
- const reasons = path.join(PACKAGES_DIR, "types/src/provider/reasons.ts");
79
- expect(uncataloguedKeys([reasons], REASON_KEY)).toEqual([]);
80
- });
81
-
82
- test("the scan finds the keys it is looking for", () => {
83
- const found = workspaceSources().flatMap((file: string) =>
84
- [...fs.readFileSync(file, "utf8").matchAll(THROWN_KEY)].map(
85
- (match: RegExpMatchArray) => match[1],
86
- ),
87
- );
88
- expect(found).toContain("DATABASE.DATABASE_IMPORT_FAILED");
89
- expect(found.length).toBeGreaterThan(100);
90
- });
91
- });
@@ -1,155 +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
- isTranslationKey,
23
- translationKeys,
24
- } from "../translationKey.js";
25
-
26
- describe("getLeafFieldPath", () => {
27
- test("returns no paths for a bare string, which is a leaf and not a container", () => {
28
- expect(getLeafFieldPath("a string")).toEqual([]);
29
- });
30
-
31
- test("returns no paths for the empty string", () => {
32
- expect(getLeafFieldPath("")).toEqual([]);
33
- });
34
-
35
- // The string is the leaf, so the path ends at its key. This used to yield
36
- // [] — the recursion returned nothing for a string and the parent mapped
37
- // over that empty list — which made the whole function return [] for every
38
- // input and left `translationKeys` empty.
39
- test("emits a top-level string value under its own key", () => {
40
- expect(getLeafFieldPath({ topLevel: "value" })).toEqual(["topLevel"]);
41
- });
42
-
43
- test("emits section.key for a one-level-deep section", () => {
44
- expect(getLeafFieldPath({ section: { key: "value" } })).toEqual([
45
- "section.key",
46
- ]);
47
- });
48
-
49
- test("emits every key of a section, in insertion order", () => {
50
- expect(
51
- getLeafFieldPath({ section: { first: "1", second: "2", third: "3" } }),
52
- ).toEqual(["section.first", "section.second", "section.third"]);
53
- });
54
-
55
- test("recurses to arbitrary depth, joining each level with a dot", () => {
56
- expect(getLeafFieldPath({ a: { b: { c: { d: "leaf" } } } })).toEqual([
57
- "a.b.c.d",
58
- ]);
59
- });
60
-
61
- test("handles sibling branches of differing depth", () => {
62
- expect(
63
- getLeafFieldPath({
64
- shallow: { key: "v" },
65
- deep: { nested: { key: "v" } },
66
- }),
67
- ).toEqual(["shallow.key", "deep.nested.key"]);
68
- });
69
-
70
- test("returns no paths for an empty object", () => {
71
- expect(getLeafFieldPath({})).toEqual([]);
72
- });
73
-
74
- // Same silent-drop hazard as the top-level string: an empty section
75
- // contributes nothing rather than erroring, so a translation file that
76
- // loses all of a section's contents still parses.
77
- test("drops an empty nested section rather than erroring", () => {
78
- expect(getLeafFieldPath({ section: {} })).toEqual([]);
79
- });
80
-
81
- test("keeps populated siblings when one section is empty", () => {
82
- expect(getLeafFieldPath({ empty: {}, full: { key: "v" } })).toEqual([
83
- "full.key",
84
- ]);
85
- });
86
-
87
- // Object.keys never yields a key whose value is undefined by omission, so
88
- // this guard is only reachable via an explicitly-assigned undefined. It
89
- // still has to hold: a JSON file cannot express it, but a caller passing a
90
- // hand-built object can, and a silently-skipped key would corrupt the
91
- // schema.
92
- test("throws on an explicitly undefined value, naming the offending key", () => {
93
- const node = {
94
- section: { broken: undefined },
95
- } as unknown as TranslationNode;
96
- expect(() => getLeafFieldPath(node)).toThrow(
97
- "Undefined value for key broken",
98
- );
99
- });
100
-
101
- test("throws before emitting any of the sibling keys of the broken one", () => {
102
- const node = {
103
- section: { ok: "v", broken: undefined },
104
- } as unknown as TranslationNode;
105
- expect(() => getLeafFieldPath(node)).toThrow(/Undefined value/);
106
- });
107
-
108
- test("does not mutate the input", () => {
109
- const node: TranslationNode = { section: { key: "value" } };
110
- const snapshot = JSON.stringify(node);
111
- getLeafFieldPath(node);
112
- expect(JSON.stringify(node)).toBe(snapshot);
113
- });
114
- });
115
-
116
- describe("translationKeys", () => {
117
- const keys = getLeafFieldPath(translationEn as TranslationNode);
118
-
119
- test("is non-empty — mongoose validates against an empty enum too", () => {
120
- expect(keys.length).toBeGreaterThan(0);
121
- });
122
-
123
- test("is exactly the set of keys derived from the English translation", () => {
124
- expect(new Set(translationKeys)).toEqual(new Set(keys));
125
- });
126
-
127
- test("holds leaves, not section names", () => {
128
- const [firstKey] = keys;
129
- expect(firstKey).toBeDefined();
130
- const section = String(firstKey).split(".")[0];
131
- expect(translationKeys).not.toContain(section);
132
- });
133
-
134
- test("does not contain a key absent from the English translation", () => {
135
- expect(translationKeys).not.toContain("no.such.key");
136
- });
137
-
138
- test("does not contain the empty string", () => {
139
- expect(translationKeys).not.toContain("");
140
- });
141
- });
142
-
143
- describe("isTranslationKey", () => {
144
- test("accepts a leaf of the English catalogue", () => {
145
- expect(isTranslationKey("WIDGET.I_AM_HUMAN")).toBe(true);
146
- expect(isTranslationKey("API.INVALID_SITE_KEY")).toBe(true);
147
- });
148
-
149
- test("rejects a section, a mistyped key, free text and the empty string", () => {
150
- expect(isTranslationKey("WIDGET")).toBe(false);
151
- expect(isTranslationKey("WIDGET.I_AM_HUMANN")).toBe(false);
152
- expect(isTranslationKey("Invalid site key")).toBe(false);
153
- expect(isTranslationKey("")).toBe(false);
154
- });
155
- });
@@ -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
- });