@multiplatform.one/i18n 6.1.0 → 6.4.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/README.md +55 -0
- package/package.json +16 -5
- package/src/frappeBackend.spec.ts +108 -0
- package/src/localePersistence.spec.ts +62 -0
- package/types/createI18nConfig.d.ts +38 -0
- package/types/createI18nConfig.d.ts.map +1 -0
- package/types/detectLanguage.d.ts +10 -0
- package/types/detectLanguage.d.ts.map +1 -0
- package/types/formatters.d.ts +23 -0
- package/types/formatters.d.ts.map +1 -0
- package/types/frappeBackend.d.ts +14 -0
- package/types/frappeBackend.d.ts.map +1 -0
- package/types/index.d.ts +9 -0
- package/types/index.d.ts.map +1 -0
- package/types/localePersistence.d.ts +7 -0
- package/types/localePersistence.d.ts.map +1 -0
- package/types/useLanguage.d.ts +2 -0
- package/types/useLanguage.d.ts.map +1 -0
- package/CHANGELOG.md +0 -47
- package/tests/createI18nConfig.test.ts +0 -58
- package/tests/detectLanguage.test.ts +0 -66
- package/tests/formatters.test.ts +0 -50
- package/tests/frappeBackend.test.ts +0 -57
- package/tests/frappeBackendEnabled.test.ts +0 -111
- package/tests/integrationFrappeDisabled.test.ts +0 -182
- package/tests/integrationFrappeEnabled.test.ts +0 -232
- package/tests/integrationLanguagePersistence.test.ts +0 -241
- package/tests/integrationTreeshaking.test.ts +0 -126
- package/tests/localFallbackDictionaries.test.ts +0 -294
- package/tests/localePersistence.test.ts +0 -65
- package/tests/makeMkFlag.test.ts +0 -25
- package/tests/useLanguagePersistence.test.ts +0 -74
- package/tsconfig.json +0 -11
- package/vitest.config.mjs +0 -13
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 10.3 Cross-group integration test: Language persistence roundtrip
|
|
3
|
-
*
|
|
4
|
-
* Verifies end-to-end language persistence:
|
|
5
|
-
* - Change language → cookie set → simulate reload → same language restored
|
|
6
|
-
* - Change language → Frappe API receives new language context
|
|
7
|
-
* - SSR detection → correct language from Accept-Language
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
10
|
-
import i18next from "i18next";
|
|
11
|
-
import { persistLanguage, getPersistedLanguage, parseCookieValue } from "../src/localePersistence";
|
|
12
|
-
import { detectLanguage } from "../src/detectLanguage";
|
|
13
|
-
import enCommon from "../../../packages/i18n/en/common.json";
|
|
14
|
-
import teCommon from "../../../packages/i18n/te/common.json";
|
|
15
|
-
|
|
16
|
-
describe("10.3 Language persistence roundtrip", () => {
|
|
17
|
-
let cookieStore: string;
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
cookieStore = "";
|
|
21
|
-
Object.defineProperty(globalThis, "document", {
|
|
22
|
-
value: {
|
|
23
|
-
get cookie() {
|
|
24
|
-
return cookieStore;
|
|
25
|
-
},
|
|
26
|
-
set cookie(val: string) {
|
|
27
|
-
const name = val.split("=")[0];
|
|
28
|
-
const parts = cookieStore.split("; ").filter((c) => c && !c.startsWith(`${name}=`));
|
|
29
|
-
parts.push(val.split(";")[0]);
|
|
30
|
-
cookieStore = parts.filter(Boolean).join("; ");
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
writable: true,
|
|
34
|
-
configurable: true,
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
afterEach(() => {
|
|
39
|
-
Object.defineProperty(globalThis, "document", {
|
|
40
|
-
value: undefined,
|
|
41
|
-
writable: true,
|
|
42
|
-
configurable: true,
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("change language → cookie set → simulate reload → same language restored", async () => {
|
|
47
|
-
// Step 1: Initialize i18next with English
|
|
48
|
-
const instance1 = i18next.createInstance();
|
|
49
|
-
await instance1.init({
|
|
50
|
-
lng: "en",
|
|
51
|
-
defaultNS: "common",
|
|
52
|
-
ns: ["common"],
|
|
53
|
-
supportedLngs: ["en", "te"],
|
|
54
|
-
fallbackLng: "en",
|
|
55
|
-
resources: {
|
|
56
|
-
en: { common: enCommon },
|
|
57
|
-
te: { common: teCommon },
|
|
58
|
-
},
|
|
59
|
-
interpolation: { escapeValue: false },
|
|
60
|
-
returnNull: false,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
expect(instance1.language).toBe("en");
|
|
64
|
-
expect(instance1.t("Save")).toBe("Save");
|
|
65
|
-
|
|
66
|
-
// Step 2: Change language to Telugu and persist (simulates useLanguage.changeLanguage)
|
|
67
|
-
persistLanguage("te");
|
|
68
|
-
await instance1.changeLanguage("te");
|
|
69
|
-
|
|
70
|
-
expect(instance1.language).toBe("te");
|
|
71
|
-
expect(instance1.t("Save")).toBe("సేవ్ చేయండి");
|
|
72
|
-
|
|
73
|
-
// Verify cookie is set
|
|
74
|
-
expect(getPersistedLanguage()).toBe("te");
|
|
75
|
-
|
|
76
|
-
// Step 3: Simulate "reload" — create a new i18next instance and read persisted language
|
|
77
|
-
const persistedLang = getPersistedLanguage();
|
|
78
|
-
expect(persistedLang).toBe("te");
|
|
79
|
-
|
|
80
|
-
const instance2 = i18next.createInstance();
|
|
81
|
-
await instance2.init({
|
|
82
|
-
lng: persistedLang!, // Use the persisted language
|
|
83
|
-
defaultNS: "common",
|
|
84
|
-
ns: ["common"],
|
|
85
|
-
supportedLngs: ["en", "te"],
|
|
86
|
-
fallbackLng: "en",
|
|
87
|
-
resources: {
|
|
88
|
-
en: { common: enCommon },
|
|
89
|
-
te: { common: teCommon },
|
|
90
|
-
},
|
|
91
|
-
interpolation: { escapeValue: false },
|
|
92
|
-
returnNull: false,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Language is restored from cookie
|
|
96
|
-
expect(instance2.language).toBe("te");
|
|
97
|
-
expect(instance2.t("Save")).toBe("సేవ్ చేయండి");
|
|
98
|
-
expect(instance2.t("Filters")).toBe("ఫిల్టర్లు");
|
|
99
|
-
expect(instance2.t("Delete")).toBe("తొలగించు");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("language change updates cookie that Frappe API can read from request", async () => {
|
|
103
|
-
// Simulate language change
|
|
104
|
-
persistLanguage("es");
|
|
105
|
-
expect(getPersistedLanguage()).toBe("es");
|
|
106
|
-
|
|
107
|
-
// The preferred_language cookie is included in HTTP requests (via credentials: "include")
|
|
108
|
-
// Frappe reads this cookie to determine the user's language preference.
|
|
109
|
-
// Verify the cookie value is parseable from a simulated request cookie header.
|
|
110
|
-
const simulatedCookieHeader = `session_id=abc123; ${cookieStore}`;
|
|
111
|
-
const parsedLang = parseCookieValue(simulatedCookieHeader, "preferred_language");
|
|
112
|
-
expect(parsedLang).toBe("es");
|
|
113
|
-
|
|
114
|
-
// Change language again
|
|
115
|
-
persistLanguage("te");
|
|
116
|
-
const updatedCookieHeader = `session_id=abc123; ${cookieStore}`;
|
|
117
|
-
const updatedLang = parseCookieValue(updatedCookieHeader, "preferred_language");
|
|
118
|
-
expect(updatedLang).toBe("te");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("SSR detects language from cookie in Request headers", () => {
|
|
122
|
-
const supported = ["en", "es", "te"];
|
|
123
|
-
const opts = { supportedLanguages: supported, defaultLanguage: "en" };
|
|
124
|
-
|
|
125
|
-
// User previously set Telugu
|
|
126
|
-
const req = new Request("http://localhost:8000/", {
|
|
127
|
-
headers: {
|
|
128
|
-
cookie: "preferred_language=te; session_id=xyz",
|
|
129
|
-
"accept-language": "en-US,en;q=0.9",
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const detected = detectLanguage(req, opts);
|
|
134
|
-
// Cookie takes precedence over Accept-Language
|
|
135
|
-
expect(detected).toBe("te");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("SSR falls back to Accept-Language when no cookie is set", () => {
|
|
139
|
-
const supported = ["en", "es", "te"];
|
|
140
|
-
const opts = { supportedLanguages: supported, defaultLanguage: "en" };
|
|
141
|
-
|
|
142
|
-
const req = new Request("http://localhost:8000/", {
|
|
143
|
-
headers: {
|
|
144
|
-
"accept-language": "es-MX;q=1.0, en;q=0.5",
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const detected = detectLanguage(req, opts);
|
|
149
|
-
expect(detected).toBe("es");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("full roundtrip: persist → SSR detect → initialize with correct language", async () => {
|
|
153
|
-
// Step 1: User changes language on the client
|
|
154
|
-
persistLanguage("te");
|
|
155
|
-
|
|
156
|
-
// Step 2: On next request, SSR reads cookie from the request
|
|
157
|
-
const simulatedRequest = new Request("http://localhost:8000/", {
|
|
158
|
-
headers: {
|
|
159
|
-
cookie: cookieStore,
|
|
160
|
-
"accept-language": "en-US,en;q=0.9",
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const detected = detectLanguage(simulatedRequest, {
|
|
165
|
-
supportedLanguages: ["en", "te", "es"],
|
|
166
|
-
defaultLanguage: "en",
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
expect(detected).toBe("te");
|
|
170
|
-
|
|
171
|
-
// Step 3: SSR initializes i18next with detected language
|
|
172
|
-
const instance = i18next.createInstance();
|
|
173
|
-
await instance.init({
|
|
174
|
-
lng: detected,
|
|
175
|
-
defaultNS: "common",
|
|
176
|
-
ns: ["common"],
|
|
177
|
-
supportedLngs: ["en", "te"],
|
|
178
|
-
fallbackLng: "en",
|
|
179
|
-
resources: {
|
|
180
|
-
en: { common: enCommon },
|
|
181
|
-
te: { common: teCommon },
|
|
182
|
-
},
|
|
183
|
-
interpolation: { escapeValue: false },
|
|
184
|
-
returnNull: false,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// SSR renders in the user's chosen language
|
|
188
|
-
expect(instance.language).toBe("te");
|
|
189
|
-
expect(instance.t("Save")).toBe("సేవ్ చేయండి");
|
|
190
|
-
expect(instance.t("Filters")).toBe("ఫిల్టర్లు");
|
|
191
|
-
expect(instance.t("common.appName")).toBe("multiplatform.one");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("language change triggers Frappe API to receive new language context via cookie", async () => {
|
|
195
|
-
// Simulate the sequence:
|
|
196
|
-
// 1. User is in English
|
|
197
|
-
// 2. User switches to Telugu
|
|
198
|
-
// 3. Next HTTP request includes preferred_language=te cookie
|
|
199
|
-
persistLanguage("en");
|
|
200
|
-
expect(getPersistedLanguage()).toBe("en");
|
|
201
|
-
|
|
202
|
-
// Simulate: user clicks "Telugu" in language switcher
|
|
203
|
-
persistLanguage("te");
|
|
204
|
-
expect(getPersistedLanguage()).toBe("te");
|
|
205
|
-
|
|
206
|
-
// Simulate: next API call includes cookies
|
|
207
|
-
// The HttpClient sends requests with credentials: "include",
|
|
208
|
-
// so the preferred_language cookie is automatically included.
|
|
209
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
210
|
-
ok: true,
|
|
211
|
-
json: () =>
|
|
212
|
-
Promise.resolve({
|
|
213
|
-
data: [
|
|
214
|
-
{ name: "Pikachu", species: "పికాచు" }, // Telugu metadata from Frappe
|
|
215
|
-
],
|
|
216
|
-
}),
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Simulate an API call that would be made after language change
|
|
220
|
-
await mockFetch("/api/resource/Pokemon", {
|
|
221
|
-
credentials: "include",
|
|
222
|
-
headers: {
|
|
223
|
-
"Accept-Language": "te",
|
|
224
|
-
Cookie: cookieStore,
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Verify the API call was made with language context
|
|
229
|
-
expect(mockFetch).toHaveBeenCalledWith("/api/resource/Pokemon", {
|
|
230
|
-
credentials: "include",
|
|
231
|
-
headers: expect.objectContaining({
|
|
232
|
-
"Accept-Language": "te",
|
|
233
|
-
}),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// The cookie header contains preferred_language=te
|
|
237
|
-
const callArgs = mockFetch.mock.calls[0][1];
|
|
238
|
-
const cookieVal = parseCookieValue(callArgs.headers.Cookie, "preferred_language");
|
|
239
|
-
expect(cookieVal).toBe("te");
|
|
240
|
-
});
|
|
241
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 10.5 Bundle size / tree-shaking verification
|
|
3
|
-
*
|
|
4
|
-
* Verifies that the Frappe backend plugin code is structured to tree-shake
|
|
5
|
-
* out of non-Frappe builds:
|
|
6
|
-
* - createFrappeBackend is guarded by process.env.VITE_FRAPPE_ENABLED
|
|
7
|
-
* - When VITE_FRAPPE_ENABLED=false, createFrappeBackend returns null
|
|
8
|
-
* - createI18nConfig does not attach backend when VITE_FRAPPE_ENABLED=false
|
|
9
|
-
* - The Frappe API URL telltale string is inside the guarded code path
|
|
10
|
-
* - The module source structure enables dead-code elimination
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect } from "vitest";
|
|
13
|
-
import { readFileSync } from "node:fs";
|
|
14
|
-
import { resolve } from "node:path";
|
|
15
|
-
import { createFrappeBackend } from "../src/frappeBackend";
|
|
16
|
-
import { createI18nConfig } from "../src/createI18nConfig";
|
|
17
|
-
import type { BackendModule } from "i18next";
|
|
18
|
-
|
|
19
|
-
describe("10.5 Tree-shaking & bundle verification", () => {
|
|
20
|
-
describe("runtime guards", () => {
|
|
21
|
-
it("createFrappeBackend returns null when VITE_FRAPPE_ENABLED is false", () => {
|
|
22
|
-
// In the test environment, VITE_FRAPPE_ENABLED defaults to "false"
|
|
23
|
-
const backend = createFrappeBackend();
|
|
24
|
-
expect(backend).toBeNull();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("createI18nConfig does not attach backend when disabled", () => {
|
|
28
|
-
const config = createI18nConfig({
|
|
29
|
-
languages: ["en"],
|
|
30
|
-
namespaces: ["common"],
|
|
31
|
-
defaultLanguage: "en",
|
|
32
|
-
defaultNamespace: "common",
|
|
33
|
-
resources: { en: { common: {} } },
|
|
34
|
-
frappeBackend: null,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
expect(config.backend).toBeUndefined();
|
|
38
|
-
expect(config.partialBundledLanguages).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("createI18nConfig omits backend even when a non-null backend is passed but flag is false", () => {
|
|
42
|
-
const mockBackend: BackendModule = {
|
|
43
|
-
type: "backend",
|
|
44
|
-
init() {},
|
|
45
|
-
read() {},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const config = createI18nConfig({
|
|
49
|
-
languages: ["en"],
|
|
50
|
-
namespaces: ["common"],
|
|
51
|
-
defaultLanguage: "en",
|
|
52
|
-
defaultNamespace: "common",
|
|
53
|
-
resources: { en: { common: {} } },
|
|
54
|
-
frappeBackend: mockBackend,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Even though a backend was provided, it's not attached because VITE_FRAPPE_ENABLED=false
|
|
58
|
-
expect(config.backend).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("source code structure enables tree-shaking", () => {
|
|
63
|
-
it("frappeBackend.ts guards the entire backend creation with VITE_FRAPPE_ENABLED check", () => {
|
|
64
|
-
const source = readFileSync(resolve(__dirname, "../src/frappeBackend.ts"), "utf-8");
|
|
65
|
-
|
|
66
|
-
// The guard must be present — this is what allows bundlers to eliminate the code
|
|
67
|
-
expect(source).toContain("process.env.VITE_FRAPPE_ENABLED");
|
|
68
|
-
expect(source).toContain("return null");
|
|
69
|
-
|
|
70
|
-
// The Frappe API URL (telltale string) is INSIDE the guarded code path
|
|
71
|
-
expect(source).toContain("frappe.translate.get_dict");
|
|
72
|
-
|
|
73
|
-
// The guard pattern: returns null early when flag is not "true"
|
|
74
|
-
// This means when the bundler replaces VITE_FRAPPE_ENABLED with "false",
|
|
75
|
-
// the entire backend object literal becomes dead code.
|
|
76
|
-
expect(source).toMatch(/VITE_FRAPPE_ENABLED.*!==.*"true".*return null/s);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("createI18nConfig.ts guards backend attachment with VITE_FRAPPE_ENABLED check", () => {
|
|
80
|
-
const source = readFileSync(resolve(__dirname, "../src/createI18nConfig.ts"), "utf-8");
|
|
81
|
-
|
|
82
|
-
// The config conditionally includes backend only when enabled
|
|
83
|
-
expect(source).toContain("process.env.VITE_FRAPPE_ENABLED");
|
|
84
|
-
expect(source).toContain("partialBundledLanguages");
|
|
85
|
-
|
|
86
|
-
// The guard pattern: if block only executes when VITE_FRAPPE_ENABLED === "true"
|
|
87
|
-
expect(source).toMatch(/VITE_FRAPPE_ENABLED.*===.*"true".*frappeBackend/s);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("frappeBackend.ts does not have side effects at module level", () => {
|
|
91
|
-
const source = readFileSync(resolve(__dirname, "../src/frappeBackend.ts"), "utf-8");
|
|
92
|
-
|
|
93
|
-
// The fetch function is defined but not called at module level.
|
|
94
|
-
// Only createFrappeBackend() is exported, and it returns null when disabled.
|
|
95
|
-
// This ensures Vite/Rollup can tree-shake the entire module.
|
|
96
|
-
expect(source).not.toMatch(/^fetch\(/m);
|
|
97
|
-
expect(source).not.toMatch(/^await /m);
|
|
98
|
-
// The package.json has "sideEffects": false, confirming tree-shaking
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("package.json has sideEffects: false for tree-shaking", () => {
|
|
102
|
-
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf-8"));
|
|
103
|
-
expect(pkg.sideEffects).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("export structure", () => {
|
|
108
|
-
it("index.ts exports createFrappeBackend (tree-shakeable named export)", () => {
|
|
109
|
-
const source = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
110
|
-
|
|
111
|
-
// Named exports allow tree-shaking — consumers that don't import
|
|
112
|
-
// createFrappeBackend won't include it in their bundle
|
|
113
|
-
expect(source).toContain("createFrappeBackend");
|
|
114
|
-
|
|
115
|
-
// It's a named re-export, not a default export or namespace import
|
|
116
|
-
expect(source).toMatch(/export\s*\{.*createFrappeBackend.*\}/s);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("all i18n exports are named (not default) for optimal tree-shaking", () => {
|
|
120
|
-
const source = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
121
|
-
|
|
122
|
-
// No default export — all named exports are individually tree-shakeable
|
|
123
|
-
expect(source).not.toContain("export default");
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
});
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import i18next from "i18next";
|
|
3
|
-
import enCommon from "../../../packages/i18n/en/common.json";
|
|
4
|
-
import teCommon from "../../../packages/i18n/te/common.json";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Complete inventory of flat English string keys used in frappe-ui components
|
|
8
|
-
* (Groups 5-7). Each string is used as its own translation key following
|
|
9
|
-
* Frappe's __() convention.
|
|
10
|
-
*/
|
|
11
|
-
const frappeUiStrings = [
|
|
12
|
-
// FrappeForm
|
|
13
|
-
"Validation error occurred",
|
|
14
|
-
"Failed to save document",
|
|
15
|
-
"Delete this {{doctype}}?",
|
|
16
|
-
"Failed to delete",
|
|
17
|
-
"Loading {{doctype}}...",
|
|
18
|
-
"Loading form...",
|
|
19
|
-
"Not Saved",
|
|
20
|
-
"Delete",
|
|
21
|
-
"Cancel",
|
|
22
|
-
"Saving...",
|
|
23
|
-
"Save",
|
|
24
|
-
"Activity",
|
|
25
|
-
|
|
26
|
-
// FrappeListView
|
|
27
|
-
"Name",
|
|
28
|
-
"Loading {{doctype}} schema...",
|
|
29
|
-
"List view",
|
|
30
|
-
"Table view",
|
|
31
|
-
"Refresh",
|
|
32
|
-
"Add {{doctype}}",
|
|
33
|
-
"Filters",
|
|
34
|
-
"Saved Filters",
|
|
35
|
-
|
|
36
|
-
// FrappeReportBuilder
|
|
37
|
-
"Count",
|
|
38
|
-
"Sum",
|
|
39
|
-
"Average",
|
|
40
|
-
"Min",
|
|
41
|
-
"Max",
|
|
42
|
-
"Bar chart",
|
|
43
|
-
"Toggle Columns",
|
|
44
|
-
"Group By:",
|
|
45
|
-
"None",
|
|
46
|
-
"Clear group by",
|
|
47
|
-
"Load",
|
|
48
|
-
"Saved Reports",
|
|
49
|
-
"Delete {{name}}",
|
|
50
|
-
"Columns",
|
|
51
|
-
"Chart",
|
|
52
|
-
"Export",
|
|
53
|
-
"Save Report",
|
|
54
|
-
"Report name",
|
|
55
|
-
"Loading data...",
|
|
56
|
-
"No data to display",
|
|
57
|
-
"(empty)",
|
|
58
|
-
"record",
|
|
59
|
-
"records",
|
|
60
|
-
"Yes",
|
|
61
|
-
"No",
|
|
62
|
-
|
|
63
|
-
// FrappeTreeView
|
|
64
|
-
"This DocType does not support tree view",
|
|
65
|
-
"Loading {{doctype}} tree...",
|
|
66
|
-
"Updating...",
|
|
67
|
-
"Expand All",
|
|
68
|
-
"Collapse All",
|
|
69
|
-
"No items found",
|
|
70
|
-
|
|
71
|
-
// FrappeImageView
|
|
72
|
-
"No image field configured for {{doctype}}",
|
|
73
|
-
|
|
74
|
-
// FrappeCalendar
|
|
75
|
-
"Today",
|
|
76
|
-
"Month",
|
|
77
|
-
"Week",
|
|
78
|
-
"Day",
|
|
79
|
-
"Events",
|
|
80
|
-
"+{{n}} more",
|
|
81
|
-
|
|
82
|
-
// FrappeKanban
|
|
83
|
-
"Drop here",
|
|
84
|
-
"No items",
|
|
85
|
-
|
|
86
|
-
// FrappeDashboard
|
|
87
|
-
"vs {{previousValue}}",
|
|
88
|
-
"Loading dashboard...",
|
|
89
|
-
"Last updated: {{time}}",
|
|
90
|
-
|
|
91
|
-
// FrappeList
|
|
92
|
-
"No {{doctype}} records found",
|
|
93
|
-
"{{n}} selected",
|
|
94
|
-
"Select all",
|
|
95
|
-
"Showing {{n}} items with virtual scrolling",
|
|
96
|
-
|
|
97
|
-
// FrappeTable
|
|
98
|
-
"Error loading {{doctype}}",
|
|
99
|
-
|
|
100
|
-
// ViewSwitcher
|
|
101
|
-
"List",
|
|
102
|
-
"Table",
|
|
103
|
-
"Report",
|
|
104
|
-
"Image",
|
|
105
|
-
"Tree",
|
|
106
|
-
"Kanban",
|
|
107
|
-
"Calendar",
|
|
108
|
-
|
|
109
|
-
// FormSidebar
|
|
110
|
-
"Write",
|
|
111
|
-
"Read",
|
|
112
|
-
"Submit",
|
|
113
|
-
"Share",
|
|
114
|
-
"Assigned To",
|
|
115
|
-
"Add assignment",
|
|
116
|
-
"No assignments",
|
|
117
|
-
"Attachments",
|
|
118
|
-
"Tags",
|
|
119
|
-
"Shared With",
|
|
120
|
-
"Add share",
|
|
121
|
-
"Not shared",
|
|
122
|
-
"Info",
|
|
123
|
-
"Created by {{owner}} on {{date}}",
|
|
124
|
-
"Last edited by {{modified_by}} on {{date}}",
|
|
125
|
-
|
|
126
|
-
// DeskShell / DeskFormPage / DeskSidebar / DeskListPage
|
|
127
|
-
"Desk",
|
|
128
|
-
"No DocType specified",
|
|
129
|
-
"New",
|
|
130
|
-
"Modules",
|
|
131
|
-
"Recent",
|
|
132
|
-
"Favorites",
|
|
133
|
-
|
|
134
|
-
// Awesomebar
|
|
135
|
-
"Search or type a command (Ctrl+K)",
|
|
136
|
-
"Result:",
|
|
137
|
-
"Commands",
|
|
138
|
-
"Navigation",
|
|
139
|
-
"Documents",
|
|
140
|
-
"Searching documents...",
|
|
141
|
-
|
|
142
|
-
// FilterManager
|
|
143
|
-
"Filter name is required",
|
|
144
|
-
"Failed to save filter",
|
|
145
|
-
"Save Filter",
|
|
146
|
-
"Save the current filter configuration for quick access later.",
|
|
147
|
-
"Filter Name",
|
|
148
|
-
"e.g., Big Pokemon",
|
|
149
|
-
"Make this filter global (visible to all users)",
|
|
150
|
-
"Current filters: {{n}} active",
|
|
151
|
-
|
|
152
|
-
// SavedFilterDropdown
|
|
153
|
-
"(Global)",
|
|
154
|
-
|
|
155
|
-
// Attachments
|
|
156
|
-
"B",
|
|
157
|
-
"KB",
|
|
158
|
-
"MB",
|
|
159
|
-
"GB",
|
|
160
|
-
"File size exceeds {{limit}} limit",
|
|
161
|
-
"Failed to read file",
|
|
162
|
-
"Upload failed",
|
|
163
|
-
"Delete failed",
|
|
164
|
-
"No attachments",
|
|
165
|
-
"Uploading...",
|
|
166
|
-
"Upload",
|
|
167
|
-
"Download",
|
|
168
|
-
"Close",
|
|
169
|
-
|
|
170
|
-
// Tags
|
|
171
|
-
"Add tag...",
|
|
172
|
-
'Tag "{{name}}" is already applied',
|
|
173
|
-
"Failed to add tag",
|
|
174
|
-
"Failed to remove tag",
|
|
175
|
-
"No tags",
|
|
176
|
-
'Create "{{value}}"',
|
|
177
|
-
"Suggestions",
|
|
178
|
-
"No available tags",
|
|
179
|
-
"Add Tag",
|
|
180
|
-
"Click + to add tags",
|
|
181
|
-
|
|
182
|
-
// Timeline
|
|
183
|
-
"just now",
|
|
184
|
-
"{{count}} minute ago",
|
|
185
|
-
"{{count}} minutes ago",
|
|
186
|
-
"{{count}} hour ago",
|
|
187
|
-
"{{count}} hours ago",
|
|
188
|
-
"{{count}} day ago",
|
|
189
|
-
"{{count}} days ago",
|
|
190
|
-
"{{count}} week ago",
|
|
191
|
-
"{{count}} weeks ago",
|
|
192
|
-
"{{count}} month ago",
|
|
193
|
-
"{{count}} months ago",
|
|
194
|
-
"No activity yet",
|
|
195
|
-
"+{{count}} more changes",
|
|
196
|
-
"made changes",
|
|
197
|
-
"added an attachment",
|
|
198
|
-
"assigned",
|
|
199
|
-
"Add a comment...",
|
|
200
|
-
"Add Comment",
|
|
201
|
-
"Sending...",
|
|
202
|
-
"Comment",
|
|
203
|
-
"Failed to add comment",
|
|
204
|
-
"{{count}} entry",
|
|
205
|
-
"{{count}} entries",
|
|
206
|
-
|
|
207
|
-
// ChildTable
|
|
208
|
-
"#",
|
|
209
|
-
"Add Row",
|
|
210
|
-
|
|
211
|
-
// SchemaForm
|
|
212
|
-
"Unsupported field type: {{fieldtype}}",
|
|
213
|
-
|
|
214
|
-
// Toolbar
|
|
215
|
-
"Loading...",
|
|
216
|
-
] as const;
|
|
217
|
-
|
|
218
|
-
describe("Group 8: Local Fallback Dictionaries", () => {
|
|
219
|
-
it("Test 1: All frappe-ui strings exist as keys in en/common.json", () => {
|
|
220
|
-
const enKeys = Object.keys(enCommon);
|
|
221
|
-
const missing: string[] = [];
|
|
222
|
-
|
|
223
|
-
for (const str of frappeUiStrings) {
|
|
224
|
-
if (!enKeys.includes(str)) {
|
|
225
|
-
missing.push(str);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
expect(missing, `Missing keys in en/common.json:\n ${missing.join("\n ")}`).toEqual([]);
|
|
230
|
-
|
|
231
|
-
// Also verify identity mapping: for English, key === value
|
|
232
|
-
for (const str of frappeUiStrings) {
|
|
233
|
-
expect((enCommon as Record<string, unknown>)[str]).toBe(str);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("Test 2: te/common.json has the same key set as en/common.json", () => {
|
|
238
|
-
const enKeys = Object.keys(enCommon).sort();
|
|
239
|
-
const teKeys = Object.keys(teCommon).sort();
|
|
240
|
-
|
|
241
|
-
// Every key in en must also appear in te
|
|
242
|
-
const missingInTe = enKeys.filter((k) => !teKeys.includes(k));
|
|
243
|
-
expect(
|
|
244
|
-
missingInTe,
|
|
245
|
-
`Keys present in en/common.json but missing in te/common.json:\n ${missingInTe.join("\n ")}`,
|
|
246
|
-
).toEqual([]);
|
|
247
|
-
|
|
248
|
-
// Every key in te must also appear in en (no stale keys)
|
|
249
|
-
const extraInTe = teKeys.filter((k) => !enKeys.includes(k));
|
|
250
|
-
expect(
|
|
251
|
-
extraInTe,
|
|
252
|
-
`Keys present in te/common.json but missing in en/common.json:\n ${extraInTe.join("\n ")}`,
|
|
253
|
-
).toEqual([]);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('Test 3: t("Filters") resolves correctly in English locale', async () => {
|
|
257
|
-
const instance = i18next.createInstance();
|
|
258
|
-
await instance.init({
|
|
259
|
-
lng: "en",
|
|
260
|
-
defaultNS: "common",
|
|
261
|
-
ns: ["common"],
|
|
262
|
-
resources: {
|
|
263
|
-
en: { common: enCommon },
|
|
264
|
-
te: { common: teCommon },
|
|
265
|
-
},
|
|
266
|
-
interpolation: { escapeValue: false },
|
|
267
|
-
returnNull: false,
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// Flat English string keys resolve via identity mapping
|
|
271
|
-
expect(instance.t("Filters")).toBe("Filters");
|
|
272
|
-
expect(instance.t("Save")).toBe("Save");
|
|
273
|
-
expect(instance.t("Loading...")).toBe("Loading...");
|
|
274
|
-
|
|
275
|
-
// Interpolated keys resolve with variable substitution
|
|
276
|
-
expect(instance.t("Loading {{doctype}}...", { doctype: "Pokemon" })).toBe("Loading Pokemon...");
|
|
277
|
-
expect(instance.t("{{n}} selected", { n: 5 })).toBe("5 selected");
|
|
278
|
-
|
|
279
|
-
// Namespaced keys still work
|
|
280
|
-
expect(instance.t("common.appName")).toBe("multiplatform.one");
|
|
281
|
-
expect(instance.t("screens.pokemon.title")).toBe("Pokemon");
|
|
282
|
-
|
|
283
|
-
// Switch to Telugu and verify translations differ from English
|
|
284
|
-
await instance.changeLanguage("te");
|
|
285
|
-
const teFilters = (teCommon as Record<string, unknown>)["Filters"];
|
|
286
|
-
const teSave = (teCommon as Record<string, unknown>)["Save"];
|
|
287
|
-
expect(instance.t("Filters")).toBe(teFilters);
|
|
288
|
-
expect(instance.t("Save")).toBe(teSave);
|
|
289
|
-
expect(instance.t("Filters")).not.toBe("Filters");
|
|
290
|
-
expect(instance.t("Save")).not.toBe("Save");
|
|
291
|
-
// appName stays the same across locales
|
|
292
|
-
expect(instance.t("common.appName")).toBe("multiplatform.one");
|
|
293
|
-
});
|
|
294
|
-
});
|