@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.
- package/.turbo/turbo-build$colon$cjs.log +8 -9
- package/.turbo/turbo-build$colon$tsc.log +6 -9
- package/.turbo/turbo-build.log +12 -16
- package/CHANGELOG.md +40 -0
- package/LICENSE +201 -0
- package/dist/cjs/direction.cjs +19 -0
- package/dist/cjs/errorMessage.cjs +15 -0
- package/dist/cjs/index.cjs +5 -0
- package/dist/cjs/locales/en/translation.cjs +23 -5
- package/dist/cjs/translationKey.cjs +4 -0
- package/dist/direction.d.ts +3 -0
- package/dist/direction.d.ts.map +1 -0
- package/dist/direction.js +19 -0
- package/dist/direction.js.map +1 -0
- package/dist/errorMessage.d.ts +7 -0
- package/dist/errorMessage.d.ts.map +1 -0
- package/dist/errorMessage.js +15 -0
- package/dist/errorMessage.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/locales/ar/translation.json +23 -5
- package/dist/locales/az/translation.json +23 -5
- package/dist/locales/cs/translation.json +23 -5
- package/dist/locales/de/translation.json +23 -5
- package/dist/locales/el/translation.json +23 -5
- package/dist/locales/en/translation.js +23 -5
- package/dist/locales/en/translation.json +23 -5
- package/dist/locales/es/translation.json +23 -5
- package/dist/locales/fi/translation.json +23 -5
- package/dist/locales/fr/translation.json +23 -5
- package/dist/locales/hi/translation.json +23 -5
- package/dist/locales/hu/translation.json +23 -5
- package/dist/locales/id/translation.json +23 -5
- package/dist/locales/it/translation.json +23 -5
- package/dist/locales/ja/translation.json +23 -5
- package/dist/locales/jv/translation.json +23 -5
- package/dist/locales/ko/translation.json +23 -5
- package/dist/locales/ml/translation.json +23 -5
- package/dist/locales/ms/translation.json +23 -5
- package/dist/locales/nl/translation.json +23 -5
- package/dist/locales/no/translation.json +23 -5
- package/dist/locales/pl/translation.json +23 -5
- package/dist/locales/pt/translation.json +23 -5
- package/dist/locales/pt-BR/translation.json +23 -5
- package/dist/locales/ro/translation.json +23 -5
- package/dist/locales/ru/translation.json +23 -5
- package/dist/locales/sr/translation.json +23 -5
- package/dist/locales/sv/translation.json +23 -5
- package/dist/locales/th/translation.json +23 -5
- package/dist/locales/tr/translation.json +23 -5
- package/dist/locales/uk/translation.json +23 -5
- package/dist/locales/vi/translation.json +23 -5
- package/dist/locales/zh-CN/translation.json +23 -5
- package/dist/tests/direction.unit.test.d.ts +2 -0
- package/dist/tests/direction.unit.test.d.ts.map +1 -0
- package/dist/tests/direction.unit.test.js +31 -0
- package/dist/tests/direction.unit.test.js.map +1 -0
- package/dist/tests/errorMessage.unit.test.d.ts +2 -0
- package/dist/tests/errorMessage.unit.test.d.ts.map +1 -0
- package/dist/tests/errorMessage.unit.test.js +49 -0
- package/dist/tests/errorMessage.unit.test.js.map +1 -0
- package/dist/tests/i18nFrontend.unit.test.js +22 -11
- package/dist/tests/i18nFrontend.unit.test.js.map +1 -1
- package/dist/tests/locale.test-d.js +18 -3
- package/dist/tests/locale.test-d.js.map +1 -1
- package/dist/tests/thrownErrorKeys.unit.test.d.ts +2 -0
- package/dist/tests/thrownErrorKeys.unit.test.d.ts.map +1 -0
- package/dist/tests/thrownErrorKeys.unit.test.js +59 -0
- package/dist/tests/thrownErrorKeys.unit.test.js.map +1 -0
- package/dist/tests/translationKey.unit.test.js +13 -1
- package/dist/tests/translationKey.unit.test.js.map +1 -1
- package/dist/translationKey.d.ts +7 -1
- package/dist/translationKey.d.ts.map +1 -1
- package/dist/translationKey.js +4 -1
- package/dist/translationKey.js.map +1 -1
- package/dist/translator.d.ts +2 -1
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +65 -66
- package/src/i18SharedOptions.ts +0 -29
- package/src/i18nBackend.ts +0 -69
- package/src/i18nFrontend.ts +0 -267
- package/src/i18nMiddleware.ts +0 -35
- package/src/index.ts +0 -29
- package/src/loadI18next.ts +0 -136
- package/src/locales/ar/translation.json +0 -476
- package/src/locales/az/translation.json +0 -476
- package/src/locales/cs/translation.json +0 -476
- package/src/locales/de/translation.json +0 -476
- package/src/locales/el/translation.json +0 -476
- package/src/locales/en/translation.json +0 -476
- package/src/locales/es/translation.json +0 -476
- package/src/locales/fi/translation.json +0 -476
- package/src/locales/fr/translation.json +0 -476
- package/src/locales/hi/translation.json +0 -476
- package/src/locales/hu/translation.json +0 -476
- package/src/locales/id/translation.json +0 -476
- package/src/locales/it/translation.json +0 -476
- package/src/locales/ja/translation.json +0 -476
- package/src/locales/jv/translation.json +0 -476
- package/src/locales/ko/translation.json +0 -476
- package/src/locales/ml/translation.json +0 -476
- package/src/locales/ms/translation.json +0 -476
- package/src/locales/nl/translation.json +0 -476
- package/src/locales/no/translation.json +0 -476
- package/src/locales/pl/translation.json +0 -476
- package/src/locales/pt/translation.json +0 -476
- package/src/locales/pt-BR/translation.json +0 -476
- package/src/locales/ro/translation.json +0 -476
- package/src/locales/ru/translation.json +0 -476
- package/src/locales/sr/translation.json +0 -476
- package/src/locales/sv/translation.json +0 -476
- package/src/locales/th/translation.json +0 -476
- package/src/locales/tr/translation.json +0 -476
- package/src/locales/uk/translation.json +0 -476
- package/src/locales/vi/translation.json +0 -476
- package/src/locales/zh-CN/translation.json +0 -476
- package/src/process.ts +0 -29
- package/src/tests/i18SharedOptions.unit.test.ts +0 -139
- package/src/tests/i18nBackend.unit.test.ts +0 -241
- package/src/tests/i18nFrontend.unit.test.ts +0 -393
- package/src/tests/i18nMiddleware.unit.test.ts +0 -159
- package/src/tests/loadI18next.unit.test.ts +0 -443
- package/src/tests/locale.test-d.ts +0 -160
- package/src/tests/localePlaceholders.unit.test.ts +0 -81
- package/src/tests/locales.unit.test.ts +0 -79
- package/src/tests/process.unit.test.ts +0 -33
- package/src/tests/translationKey.unit.test.ts +0 -140
- package/src/tests/translations.unit.test.ts +0 -97
- package/src/tests/translator.unit.test.ts +0 -213
- package/src/tests/util.unit.test.ts +0 -102
- package/src/translationKey.ts +0 -64
- package/src/translations.ts +0 -55
- package/src/translator.ts +0 -68
- package/src/types.ts +0 -42
- package/src/util.ts +0 -25
- package/tsconfig.cjs.json +0 -21
- package/tsconfig.json +0 -22
- package/tsconfig.tsbuildinfo +0 -1
- 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
|
-
});
|