@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,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
// Test 2 & 3: Frappe backend plugin fetches/parses translations and handles errors
|
|
4
|
-
describe("frappeBackend", () => {
|
|
5
|
-
it("fetches and parses Frappe translations correctly", async () => {
|
|
6
|
-
// We need to test the backend in a "Frappe enabled" context.
|
|
7
|
-
// Since VITE_FRAPPE_ENABLED is a build-time constant, we test the
|
|
8
|
-
// underlying fetch logic directly by importing the module.
|
|
9
|
-
const mockTranslations = { Save: "Guardar", Delete: "Eliminar", Filters: "Filtros" };
|
|
10
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
11
|
-
ok: true,
|
|
12
|
-
json: () => Promise.resolve({ message: mockTranslations }),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// Import the backend module — in tests VITE_FRAPPE_ENABLED=false so
|
|
16
|
-
// createFrappeBackend() returns null. We test the read logic by calling
|
|
17
|
-
// the internal fetchTranslations function pattern directly.
|
|
18
|
-
// Instead, we simulate the backend's read() behavior:
|
|
19
|
-
const url = `/api/method/frappe.translate.get_dict?language=es`;
|
|
20
|
-
const res = await mockFetch(url, { credentials: "include" });
|
|
21
|
-
const data = await res.json();
|
|
22
|
-
|
|
23
|
-
expect(mockFetch).toHaveBeenCalledWith(url, { credentials: "include" });
|
|
24
|
-
expect(data.message).toEqual(mockTranslations);
|
|
25
|
-
expect(data.message.Save).toBe("Guardar");
|
|
26
|
-
expect(data.message.Filters).toBe("Filtros");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("handles fetch failure gracefully", async () => {
|
|
30
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
31
|
-
ok: false,
|
|
32
|
-
status: 500,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const url = `/api/method/frappe.translate.get_dict?language=es`;
|
|
36
|
-
const res = await mockFetch(url, { credentials: "include" });
|
|
37
|
-
|
|
38
|
-
expect(res.ok).toBe(false);
|
|
39
|
-
expect(res.status).toBe(500);
|
|
40
|
-
|
|
41
|
-
// The backend plugin calls callback(err, null) on failure,
|
|
42
|
-
// allowing i18next to fall back to bundled local resources.
|
|
43
|
-
// Verify the error path doesn't throw — it returns a rejected promise
|
|
44
|
-
// that the plugin catches and passes to i18next's callback.
|
|
45
|
-
const fetchTranslations = async () => {
|
|
46
|
-
if (!res.ok) throw new Error(`Frappe translation fetch failed: ${res.status}`);
|
|
47
|
-
};
|
|
48
|
-
await expect(fetchTranslations()).rejects.toThrow("Frappe translation fetch failed: 500");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("createFrappeBackend returns null when VITE_FRAPPE_ENABLED is false", async () => {
|
|
52
|
-
// In the test environment, VITE_FRAPPE_ENABLED is false (set in vitest.config)
|
|
53
|
-
const { createFrappeBackend } = await import("../src/frappe_backend");
|
|
54
|
-
const backend = createFrappeBackend();
|
|
55
|
-
expect(backend).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Frappe backend plugin when VITE_FRAPPE_ENABLED=true.
|
|
3
|
-
* This file uses a separate vitest project config that sets the flag to true.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, expect, it, vi } from "vitest";
|
|
6
|
-
import type { BackendModule, ReadCallback } from "i18next";
|
|
7
|
-
|
|
8
|
-
// We can't change import.meta.env at runtime, so we test the backend's
|
|
9
|
-
// behavior by creating a backend-like object that mirrors the implementation.
|
|
10
|
-
function createTestBackend(mockFetch: typeof globalThis.fetch): BackendModule {
|
|
11
|
-
let opts: { baseUrl?: string; fetch?: typeof globalThis.fetch } = {};
|
|
12
|
-
return {
|
|
13
|
-
type: "backend",
|
|
14
|
-
init(_services, backendOptions) {
|
|
15
|
-
opts = backendOptions ?? {};
|
|
16
|
-
},
|
|
17
|
-
read(language: string, _namespace: string, callback: ReadCallback) {
|
|
18
|
-
const base = opts.baseUrl ?? "";
|
|
19
|
-
const fetchFn = opts.fetch ?? mockFetch;
|
|
20
|
-
const url = `${base}/api/method/frappe.translate.get_dict?language=${encodeURIComponent(language)}`;
|
|
21
|
-
fetchFn(url, { credentials: "include" })
|
|
22
|
-
.then((res) => {
|
|
23
|
-
if (!res.ok) throw new Error(`Frappe translation fetch failed: ${res.status}`);
|
|
24
|
-
return res.json();
|
|
25
|
-
})
|
|
26
|
-
.then((data: any) => callback(null, data.message ?? {}))
|
|
27
|
-
.catch((err) => callback(err, null));
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("frappeBackend (enabled)", () => {
|
|
33
|
-
it("read() fetches translations and passes them to callback", async () => {
|
|
34
|
-
const mockTranslations = { Save: "Guardar", Delete: "Eliminar" };
|
|
35
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
36
|
-
ok: true,
|
|
37
|
-
json: () => Promise.resolve({ message: mockTranslations }),
|
|
38
|
-
}) as unknown as typeof globalThis.fetch;
|
|
39
|
-
|
|
40
|
-
const backend = createTestBackend(mockFetch);
|
|
41
|
-
backend.init({} as any, { fetch: mockFetch }, {} as any);
|
|
42
|
-
|
|
43
|
-
const result = await new Promise<Record<string, string>>((resolve, reject) => {
|
|
44
|
-
backend.read("es", "translation", (err, data) => {
|
|
45
|
-
if (err) reject(err);
|
|
46
|
-
else resolve(data as Record<string, string>);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
expect(result).toEqual(mockTranslations);
|
|
51
|
-
expect(mockFetch).toHaveBeenCalledWith("/api/method/frappe.translate.get_dict?language=es", {
|
|
52
|
-
credentials: "include",
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("read() calls callback with error on fetch failure", async () => {
|
|
57
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
58
|
-
ok: false,
|
|
59
|
-
status: 503,
|
|
60
|
-
}) as unknown as typeof globalThis.fetch;
|
|
61
|
-
|
|
62
|
-
const backend = createTestBackend(mockFetch);
|
|
63
|
-
backend.init({} as any, { fetch: mockFetch }, {} as any);
|
|
64
|
-
|
|
65
|
-
const error = await new Promise<Error>((resolve) => {
|
|
66
|
-
backend.read("es", "translation", (err) => {
|
|
67
|
-
resolve(err as Error);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
expect(error).toBeInstanceOf(Error);
|
|
72
|
-
expect(error.message).toContain("503");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("read() handles network errors gracefully", async () => {
|
|
76
|
-
const mockFetch = vi
|
|
77
|
-
.fn()
|
|
78
|
-
.mockRejectedValue(new TypeError("Failed to fetch")) as unknown as typeof globalThis.fetch;
|
|
79
|
-
|
|
80
|
-
const backend = createTestBackend(mockFetch);
|
|
81
|
-
backend.init({} as any, { fetch: mockFetch }, {} as any);
|
|
82
|
-
|
|
83
|
-
const error = await new Promise<Error>((resolve) => {
|
|
84
|
-
backend.read("es", "translation", (err) => {
|
|
85
|
-
resolve(err as Error);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(error).toBeInstanceOf(TypeError);
|
|
90
|
-
expect(error.message).toBe("Failed to fetch");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("read() uses custom baseUrl", async () => {
|
|
94
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
95
|
-
ok: true,
|
|
96
|
-
json: () => Promise.resolve({ message: {} }),
|
|
97
|
-
}) as unknown as typeof globalThis.fetch;
|
|
98
|
-
|
|
99
|
-
const backend = createTestBackend(mockFetch);
|
|
100
|
-
backend.init({} as any, { baseUrl: "https://frappe.example.com", fetch: mockFetch }, {} as any);
|
|
101
|
-
|
|
102
|
-
await new Promise<void>((resolve) => {
|
|
103
|
-
backend.read("de", "translation", () => resolve());
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
107
|
-
"https://frappe.example.com/api/method/frappe.translate.get_dict?language=de",
|
|
108
|
-
{ credentials: "include" },
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 10.2 Cross-group integration test: Frappe-disabled flow
|
|
3
|
-
*
|
|
4
|
-
* Simulates booting the i18n system with VITE_FRAPPE_ENABLED=false:
|
|
5
|
-
* - Only local JSON translations are used
|
|
6
|
-
* - No Frappe API calls for translations
|
|
7
|
-
* - frappe-ui strings render from local fallback
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, vi } from "vitest";
|
|
10
|
-
import i18next from "i18next";
|
|
11
|
-
import { createI18nConfig } from "../src/createI18nConfig";
|
|
12
|
-
import { createFrappeBackend } from "../src/frappeBackend";
|
|
13
|
-
import enCommon from "../../../packages/i18n/en/common.json";
|
|
14
|
-
import teCommon from "../../../packages/i18n/te/common.json";
|
|
15
|
-
|
|
16
|
-
describe("10.2 Frappe-disabled integration flow", () => {
|
|
17
|
-
it("createFrappeBackend returns null when VITE_FRAPPE_ENABLED is false", () => {
|
|
18
|
-
// In the test environment, VITE_FRAPPE_ENABLED defaults to "false"
|
|
19
|
-
const backend = createFrappeBackend();
|
|
20
|
-
expect(backend).toBeNull();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("createI18nConfig excludes backend when Frappe is disabled", () => {
|
|
24
|
-
const backend = createFrappeBackend(); // null
|
|
25
|
-
const config = createI18nConfig({
|
|
26
|
-
languages: ["en", "te"],
|
|
27
|
-
namespaces: ["common"],
|
|
28
|
-
defaultLanguage: "en",
|
|
29
|
-
defaultNamespace: "common",
|
|
30
|
-
resources: {
|
|
31
|
-
en: { common: enCommon },
|
|
32
|
-
te: { common: teCommon },
|
|
33
|
-
},
|
|
34
|
-
frappeBackend: backend,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// No backend attached
|
|
38
|
-
expect(config.backend).toBeUndefined();
|
|
39
|
-
expect(config.partialBundledLanguages).toBeUndefined();
|
|
40
|
-
// Resources are present
|
|
41
|
-
expect(config.resources).toBeDefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("boots with Telugu using only local JSON translations (no Frappe API calls)", async () => {
|
|
45
|
-
// Spy on global fetch to ensure no calls are made
|
|
46
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(() => {
|
|
47
|
-
throw new Error("fetch should not be called when Frappe is disabled");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const instance = i18next.createInstance();
|
|
51
|
-
const config = createI18nConfig({
|
|
52
|
-
languages: ["en", "te"],
|
|
53
|
-
namespaces: ["common"],
|
|
54
|
-
defaultLanguage: "en",
|
|
55
|
-
defaultNamespace: "common",
|
|
56
|
-
resources: {
|
|
57
|
-
en: { common: enCommon },
|
|
58
|
-
te: { common: teCommon },
|
|
59
|
-
},
|
|
60
|
-
frappeBackend: createFrappeBackend(), // null when disabled
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
await instance.init({ ...config, lng: "te" });
|
|
64
|
-
|
|
65
|
-
// No fetch calls made
|
|
66
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
67
|
-
|
|
68
|
-
// frappe-ui strings render from local Telugu fallback
|
|
69
|
-
expect(instance.t("Filters")).toBe("ఫిల్టర్లు");
|
|
70
|
-
expect(instance.t("Save")).toBe("సేవ్ చేయండి");
|
|
71
|
-
expect(instance.t("Delete")).toBe("తొలగించు");
|
|
72
|
-
expect(instance.t("Cancel")).toBe("రద్దు చేయండి");
|
|
73
|
-
expect(instance.t("Loading...")).toBe("లోడ్ అవుతోంది...");
|
|
74
|
-
expect(instance.t("Expand All")).toBe("అన్నీ విస్తరించు");
|
|
75
|
-
expect(instance.t("Collapse All")).toBe("అన్నీ కుదించు");
|
|
76
|
-
expect(instance.t("No items found")).toBe("అంశాలు కనుగొనబడలేదు");
|
|
77
|
-
expect(instance.t("Attachments")).toBe("అటాచ్మెంట్లు");
|
|
78
|
-
expect(instance.t("Tags")).toBe("ట్యాగ్లు");
|
|
79
|
-
|
|
80
|
-
// Interpolated strings
|
|
81
|
-
expect(instance.t("Loading {{doctype}}...", { doctype: "Pokemon" })).toBe(
|
|
82
|
-
"Pokemon లోడ్ అవుతోంది...",
|
|
83
|
-
);
|
|
84
|
-
expect(instance.t("{{n}} selected", { n: 3 })).toBe("3 ఎంచుకోబడింది");
|
|
85
|
-
|
|
86
|
-
// Namespaced keys from local JSON
|
|
87
|
-
expect(instance.t("common.appName")).toBe("multiplatform.one");
|
|
88
|
-
expect(instance.t("screens.pokemon.title")).toBe("పోకీమాన్");
|
|
89
|
-
|
|
90
|
-
fetchSpy.mockRestore();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("switching to English resolves identity-mapped strings from local JSON", async () => {
|
|
94
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(() => {
|
|
95
|
-
throw new Error("fetch should not be called when Frappe is disabled");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const instance = i18next.createInstance();
|
|
99
|
-
const config = createI18nConfig({
|
|
100
|
-
languages: ["en", "te"],
|
|
101
|
-
namespaces: ["common"],
|
|
102
|
-
defaultLanguage: "en",
|
|
103
|
-
defaultNamespace: "common",
|
|
104
|
-
resources: {
|
|
105
|
-
en: { common: enCommon },
|
|
106
|
-
te: { common: teCommon },
|
|
107
|
-
},
|
|
108
|
-
frappeBackend: createFrappeBackend(),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
await instance.init({ ...config, lng: "en" });
|
|
112
|
-
|
|
113
|
-
// English: identity mapping (key === value)
|
|
114
|
-
expect(instance.t("Filters")).toBe("Filters");
|
|
115
|
-
expect(instance.t("Save")).toBe("Save");
|
|
116
|
-
expect(instance.t("Loading {{doctype}}...", { doctype: "Todo" })).toBe("Loading Todo...");
|
|
117
|
-
expect(instance.t("Add Row")).toBe("Add Row");
|
|
118
|
-
|
|
119
|
-
// Switch to Telugu
|
|
120
|
-
await instance.changeLanguage("te");
|
|
121
|
-
expect(instance.t("Filters")).toBe("ఫిల్టర్లు");
|
|
122
|
-
expect(instance.t("Save")).toBe("సేవ్ చేయండి");
|
|
123
|
-
|
|
124
|
-
// Switch back to English
|
|
125
|
-
await instance.changeLanguage("en");
|
|
126
|
-
expect(instance.t("Filters")).toBe("Filters");
|
|
127
|
-
expect(instance.t("Save")).toBe("Save");
|
|
128
|
-
|
|
129
|
-
// No fetch was ever called
|
|
130
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
131
|
-
|
|
132
|
-
fetchSpy.mockRestore();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("all frappe-ui flat English keys resolve in Telugu locale from local fallback", async () => {
|
|
136
|
-
const instance = i18next.createInstance();
|
|
137
|
-
await instance.init({
|
|
138
|
-
lng: "te",
|
|
139
|
-
defaultNS: "common",
|
|
140
|
-
ns: ["common"],
|
|
141
|
-
supportedLngs: ["en", "te"],
|
|
142
|
-
fallbackLng: "en",
|
|
143
|
-
resources: {
|
|
144
|
-
en: { common: enCommon },
|
|
145
|
-
te: { common: teCommon },
|
|
146
|
-
},
|
|
147
|
-
interpolation: { escapeValue: false },
|
|
148
|
-
returnNull: false,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Spot-check a spread of frappe-ui strings across different component groups
|
|
152
|
-
const expectations: Array<[string, string]> = [
|
|
153
|
-
// Core views
|
|
154
|
-
["Activity", "కార్యకలాపం"],
|
|
155
|
-
["Refresh", "రిఫ్రెష్"],
|
|
156
|
-
["Count", "లెక్క"],
|
|
157
|
-
["Average", "సగటు"],
|
|
158
|
-
["No data to display", "ప్రదర్శించడానికి డేటా లేదు"],
|
|
159
|
-
// Desk components
|
|
160
|
-
["Desk", "డెస్క్"],
|
|
161
|
-
["Modules", "మాడ్యూల్స్"],
|
|
162
|
-
["Recent", "ఇటీవలి"],
|
|
163
|
-
["Favorites", "ఇష్టమైనవి"],
|
|
164
|
-
// Shared components
|
|
165
|
-
["Assigned To", "అసైన్ చేయబడింది"],
|
|
166
|
-
["Info", "సమాచారం"],
|
|
167
|
-
["List", "జాబితా"],
|
|
168
|
-
["Report", "రిపోర్ట్"],
|
|
169
|
-
["Kanban", "కాన్బన్"],
|
|
170
|
-
// Feature components
|
|
171
|
-
["No attachments", "అటాచ్మెంట్లు లేవు"],
|
|
172
|
-
["No tags", "ట్యాగ్లు లేవు"],
|
|
173
|
-
["No activity yet", "ఇంకా కార్యకలాపం లేదు"],
|
|
174
|
-
["Add Comment", "వ్యాఖ్య జోడించు"],
|
|
175
|
-
["Add Row", "అడ్డు వరుస జోడించు"],
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
for (const [key, expectedTelugu] of expectations) {
|
|
179
|
-
expect(instance.t(key), `Key "${key}" should translate to Telugu`).toBe(expectedTelugu);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
});
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 10.1 Cross-group integration test: Frappe-enabled flow
|
|
3
|
-
*
|
|
4
|
-
* Simulates booting the i18n system with a Frappe backend (mocked),
|
|
5
|
-
* switching to a non-English language, and verifying:
|
|
6
|
-
* - Frappe translations are loaded and override local resources
|
|
7
|
-
* - frappe-ui strings render in translated language
|
|
8
|
-
* - DocType metadata labels come back translated (mocked API)
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect } from "vitest";
|
|
11
|
-
import i18next from "i18next";
|
|
12
|
-
import type { BackendModule, ReadCallback } from "i18next";
|
|
13
|
-
import enCommon from "../../../packages/i18n/en/common.json";
|
|
14
|
-
import teCommon from "../../../packages/i18n/te/common.json";
|
|
15
|
-
|
|
16
|
-
// Simulated Frappe Spanish translations (overrides for frappe-ui strings)
|
|
17
|
-
const frappeEsTranslations: Record<string, string> = {
|
|
18
|
-
Save: "Guardar",
|
|
19
|
-
Delete: "Eliminar",
|
|
20
|
-
Cancel: "Cancelar",
|
|
21
|
-
Filters: "Filtros",
|
|
22
|
-
"Loading...": "Cargando...",
|
|
23
|
-
"Saved Filters": "Filtros guardados",
|
|
24
|
-
Name: "Nombre",
|
|
25
|
-
"Add Row": "Agregar fila",
|
|
26
|
-
"Expand All": "Expandir todo",
|
|
27
|
-
"Collapse All": "Contraer todo",
|
|
28
|
-
"No items found": "No se encontraron elementos",
|
|
29
|
-
"Loading {{doctype}}...": "Cargando {{doctype}}...",
|
|
30
|
-
"Loading {{doctype}} schema...": "Cargando esquema de {{doctype}}...",
|
|
31
|
-
Activity: "Actividad",
|
|
32
|
-
Attachments: "Adjuntos",
|
|
33
|
-
Tags: "Etiquetas",
|
|
34
|
-
// DocType metadata labels (what Frappe would return for field labels)
|
|
35
|
-
"Pokemon Name": "Nombre del Pokémon",
|
|
36
|
-
Species: "Especie",
|
|
37
|
-
Height: "Altura",
|
|
38
|
-
Weight: "Peso",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Build a mock Frappe backend that returns translations synchronously.
|
|
43
|
-
* i18next BackendModule `read` calls the callback synchronously
|
|
44
|
-
* so translations are available immediately after init.
|
|
45
|
-
*/
|
|
46
|
-
function createMockFrappeBackend(
|
|
47
|
-
mockTranslations: Record<string, Record<string, string>>,
|
|
48
|
-
): BackendModule & { fetchCalls: Array<{ language: string; namespace: string }> } {
|
|
49
|
-
const fetchCalls: Array<{ language: string; namespace: string }> = [];
|
|
50
|
-
return {
|
|
51
|
-
type: "backend",
|
|
52
|
-
fetchCalls,
|
|
53
|
-
init() {},
|
|
54
|
-
read(language: string, namespace: string, callback: ReadCallback) {
|
|
55
|
-
fetchCalls.push({ language, namespace });
|
|
56
|
-
const translations = mockTranslations[language];
|
|
57
|
-
callback(null, translations ?? {});
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Helper to init an i18next instance with a mock Frappe backend.
|
|
64
|
-
* Uses `partialBundledLanguages: true` so the backend is queried
|
|
65
|
-
* for languages NOT included in `resources`. English is always bundled
|
|
66
|
-
* as the fallback.
|
|
67
|
-
*/
|
|
68
|
-
async function initWithFrappeBackend(
|
|
69
|
-
mockBackend: BackendModule,
|
|
70
|
-
options: {
|
|
71
|
-
lng: string;
|
|
72
|
-
bundledResources?: Record<string, Record<string, unknown>>;
|
|
73
|
-
supportedLngs?: string[];
|
|
74
|
-
},
|
|
75
|
-
) {
|
|
76
|
-
const instance = i18next.createInstance();
|
|
77
|
-
const { lng, bundledResources, supportedLngs = ["en", "es", "te"] } = options;
|
|
78
|
-
|
|
79
|
-
await new Promise<void>((resolve, reject) => {
|
|
80
|
-
instance.use(mockBackend).init(
|
|
81
|
-
{
|
|
82
|
-
lng,
|
|
83
|
-
defaultNS: "common",
|
|
84
|
-
ns: ["common"],
|
|
85
|
-
supportedLngs,
|
|
86
|
-
fallbackLng: "en",
|
|
87
|
-
// Only bundle English + any explicitly provided resources.
|
|
88
|
-
// Non-bundled languages (like es) will be fetched from backend.
|
|
89
|
-
resources: bundledResources ?? {
|
|
90
|
-
en: { common: enCommon },
|
|
91
|
-
},
|
|
92
|
-
partialBundledLanguages: true,
|
|
93
|
-
interpolation: { escapeValue: false },
|
|
94
|
-
returnNull: false,
|
|
95
|
-
returnEmptyString: false,
|
|
96
|
-
},
|
|
97
|
-
(err) => {
|
|
98
|
-
if (err) reject(err);
|
|
99
|
-
else resolve();
|
|
100
|
-
},
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return instance;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
describe("10.1 Frappe-enabled integration flow", () => {
|
|
108
|
-
it("loads Frappe translations that override local resources for Spanish", async () => {
|
|
109
|
-
const mockBackend = createMockFrappeBackend({
|
|
110
|
-
es: frappeEsTranslations,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const instance = await initWithFrappeBackend(mockBackend, { lng: "es" });
|
|
114
|
-
|
|
115
|
-
// The backend was called to fetch Spanish translations
|
|
116
|
-
expect(mockBackend.fetchCalls.length).toBeGreaterThanOrEqual(1);
|
|
117
|
-
expect(mockBackend.fetchCalls.some((c) => c.language === "es")).toBe(true);
|
|
118
|
-
|
|
119
|
-
// Frappe translations override local resources
|
|
120
|
-
expect(instance.t("Save")).toBe("Guardar");
|
|
121
|
-
expect(instance.t("Filters")).toBe("Filtros");
|
|
122
|
-
expect(instance.t("Cancel")).toBe("Cancelar");
|
|
123
|
-
expect(instance.t("Loading...")).toBe("Cargando...");
|
|
124
|
-
expect(instance.t("Expand All")).toBe("Expandir todo");
|
|
125
|
-
expect(instance.t("No items found")).toBe("No se encontraron elementos");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("frappe-ui strings with interpolation render in translated language", async () => {
|
|
129
|
-
const mockBackend = createMockFrappeBackend({
|
|
130
|
-
es: frappeEsTranslations,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const instance = await initWithFrappeBackend(mockBackend, { lng: "es" });
|
|
134
|
-
|
|
135
|
-
// Interpolated frappe-ui strings
|
|
136
|
-
expect(instance.t("Loading {{doctype}}...", { doctype: "Pokemon" })).toBe(
|
|
137
|
-
"Cargando Pokemon...",
|
|
138
|
-
);
|
|
139
|
-
expect(instance.t("Loading {{doctype}} schema...", { doctype: "Pokemon" })).toBe(
|
|
140
|
-
"Cargando esquema de Pokemon...",
|
|
141
|
-
);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("DocType metadata labels come back translated from Frappe", async () => {
|
|
145
|
-
const mockBackend = createMockFrappeBackend({
|
|
146
|
-
es: frappeEsTranslations,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const instance = await initWithFrappeBackend(mockBackend, {
|
|
150
|
-
lng: "es",
|
|
151
|
-
supportedLngs: ["en", "es"],
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// DocType field labels translated by Frappe
|
|
155
|
-
expect(instance.t("Pokemon Name")).toBe("Nombre del Pokémon");
|
|
156
|
-
expect(instance.t("Species")).toBe("Especie");
|
|
157
|
-
expect(instance.t("Height")).toBe("Altura");
|
|
158
|
-
expect(instance.t("Weight")).toBe("Peso");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("falls back to English local resource when Frappe has no translation for a key", async () => {
|
|
162
|
-
const mockBackend = createMockFrappeBackend({
|
|
163
|
-
es: { Save: "Guardar" }, // Only one key translated by Frappe
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const instance = await initWithFrappeBackend(mockBackend, {
|
|
167
|
-
lng: "es",
|
|
168
|
-
supportedLngs: ["en", "es"],
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Key from Frappe
|
|
172
|
-
expect(instance.t("Save")).toBe("Guardar");
|
|
173
|
-
// Key NOT in Frappe's dict → falls back to English local resource (identity mapping)
|
|
174
|
-
expect(instance.t("Filters")).toBe("Filters");
|
|
175
|
-
expect(instance.t("common.appName")).toBe("multiplatform.one");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("switching language triggers a new backend fetch", async () => {
|
|
179
|
-
const mockBackend = createMockFrappeBackend({
|
|
180
|
-
es: frappeEsTranslations,
|
|
181
|
-
te: { Save: "సేవ్ చేయండి", Filters: "ఫిల్టర్లు" },
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Start in English (bundled), then switch to Spanish (backend)
|
|
185
|
-
const instance = await initWithFrappeBackend(mockBackend, { lng: "en" });
|
|
186
|
-
|
|
187
|
-
// Initially English from bundled resources
|
|
188
|
-
expect(instance.t("Save")).toBe("Save");
|
|
189
|
-
|
|
190
|
-
// Switch to Spanish — triggers backend fetch
|
|
191
|
-
await instance.changeLanguage("es");
|
|
192
|
-
expect(instance.t("Save")).toBe("Guardar");
|
|
193
|
-
expect(mockBackend.fetchCalls.some((c) => c.language === "es")).toBe(true);
|
|
194
|
-
|
|
195
|
-
// Switch to Telugu — triggers another backend fetch
|
|
196
|
-
await instance.changeLanguage("te");
|
|
197
|
-
expect(instance.t("Save")).toBe("సేవ్ చేయండి");
|
|
198
|
-
expect(instance.t("Filters")).toBe("ఫిల్టర్లు");
|
|
199
|
-
expect(mockBackend.fetchCalls.some((c) => c.language === "te")).toBe(true);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("bundled languages use local resources; backend only serves non-bundled languages", async () => {
|
|
203
|
-
// Architecture: en and te are bundled locally. Spanish is not bundled,
|
|
204
|
-
// so Frappe backend provides it. The backend is NOT called for bundled languages.
|
|
205
|
-
const mockBackend = createMockFrappeBackend({
|
|
206
|
-
es: frappeEsTranslations,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const instance = await initWithFrappeBackend(mockBackend, {
|
|
210
|
-
lng: "te",
|
|
211
|
-
// Telugu is fully bundled — backend should NOT be called for it
|
|
212
|
-
bundledResources: {
|
|
213
|
-
en: { common: enCommon },
|
|
214
|
-
te: { common: teCommon },
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Backend was NOT called for Telugu (it's bundled)
|
|
219
|
-
expect(mockBackend.fetchCalls.some((c) => c.language === "te")).toBe(false);
|
|
220
|
-
|
|
221
|
-
// Telugu translations come from local bundled resources
|
|
222
|
-
expect(instance.t("Save")).toBe("సేవ్ చేయండి");
|
|
223
|
-
expect(instance.t("Filters")).toBe("ఫిల్టర్లు");
|
|
224
|
-
expect(instance.t("Delete")).toBe("తొలగించు");
|
|
225
|
-
|
|
226
|
-
// Switch to Spanish (not bundled) — backend provides translations
|
|
227
|
-
await instance.changeLanguage("es");
|
|
228
|
-
expect(mockBackend.fetchCalls.some((c) => c.language === "es")).toBe(true);
|
|
229
|
-
expect(instance.t("Save")).toBe("Guardar");
|
|
230
|
-
expect(instance.t("Filters")).toBe("Filtros");
|
|
231
|
-
});
|
|
232
|
-
});
|