@multiplatform.one/i18n 6.0.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.
@@ -0,0 +1,294 @@
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
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "vitest";
2
+ import { getPersistedLanguage, persistLanguage, parseCookieValue } from "../src/localePersistence";
3
+
4
+ describe("localePersistence", () => {
5
+ // Test 1: Cookie set/get roundtrip with preferred_language
6
+ describe("cookie roundtrip", () => {
7
+ let originalDocument: typeof globalThis.document;
8
+
9
+ beforeEach(() => {
10
+ // Mock document.cookie for Node.js test environment
11
+ originalDocument = globalThis.document;
12
+ let cookieStore = "";
13
+ Object.defineProperty(globalThis, "document", {
14
+ value: {
15
+ get cookie() {
16
+ return cookieStore;
17
+ },
18
+ set cookie(val: string) {
19
+ // Simple cookie store: accumulate cookies separated by '; '
20
+ const name = val.split("=")[0];
21
+ const parts = cookieStore.split("; ").filter((c) => !c.startsWith(`${name}=`));
22
+ parts.push(val.split(";")[0]);
23
+ cookieStore = parts.filter(Boolean).join("; ");
24
+ },
25
+ },
26
+ writable: true,
27
+ configurable: true,
28
+ });
29
+ });
30
+
31
+ afterEach(() => {
32
+ Object.defineProperty(globalThis, "document", {
33
+ value: originalDocument,
34
+ writable: true,
35
+ configurable: true,
36
+ });
37
+ });
38
+
39
+ it("persists and reads back a language cookie", () => {
40
+ expect(getPersistedLanguage()).toBeNull();
41
+ persistLanguage("es");
42
+ expect(getPersistedLanguage()).toBe("es");
43
+ persistLanguage("te");
44
+ expect(getPersistedLanguage()).toBe("te");
45
+ });
46
+ });
47
+
48
+ describe("parseCookieValue", () => {
49
+ it("extracts a named cookie from a header string", () => {
50
+ const header = "session_id=abc123; preferred_language=fr; theme=dark";
51
+ expect(parseCookieValue(header, "preferred_language")).toBe("fr");
52
+ expect(parseCookieValue(header, "session_id")).toBe("abc123");
53
+ expect(parseCookieValue(header, "theme")).toBe("dark");
54
+ });
55
+
56
+ it("returns null for missing cookie", () => {
57
+ expect(parseCookieValue("session_id=abc", "preferred_language")).toBeNull();
58
+ expect(parseCookieValue("", "preferred_language")).toBeNull();
59
+ });
60
+
61
+ it("decodes URI-encoded values", () => {
62
+ expect(parseCookieValue("preferred_language=zh-CN", "preferred_language")).toBe("zh-CN");
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { execSync } from "node:child_process";
3
+ import path from "node:path";
4
+
5
+ const rootdir = path.resolve(__dirname, "../../..");
6
+
7
+ describe("make.mk FRAPPE_APP_EXISTS flag", () => {
8
+ // Test 1: Verify the Makefile variable detects apps/frappe directory presence
9
+ it("sets VITE_FRAPPE_ENABLED=true when apps/frappe exists", () => {
10
+ // Use --eval to print the evaluated variable value
11
+ const result = execSync(
12
+ `make -f make.mk rootdir=${rootdir} --eval='print-flag: ; @echo $(VITE_FRAPPE_ENABLED)' print-flag 2>/dev/null`,
13
+ { cwd: rootdir, encoding: "utf-8", env: { ...process.env, VITE_FRAPPE_ENABLED: undefined } },
14
+ ).trim();
15
+ expect(result).toBe("true");
16
+ });
17
+
18
+ it("FRAPPE_APP_EXISTS reflects directory presence", () => {
19
+ const result = execSync(
20
+ `make -f make.mk rootdir=${rootdir} --eval='print-flag: ; @echo $(FRAPPE_APP_EXISTS)' print-flag 2>/dev/null`,
21
+ { cwd: rootdir, encoding: "utf-8" },
22
+ ).trim();
23
+ expect(result).toBe("true");
24
+ });
25
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "vitest";
2
+
3
+ // Test 4: useLanguage persists to cookie when language changes
4
+ // We test the integration by verifying that persistLanguage is called
5
+ // when the changeLanguage callback is invoked.
6
+ describe("useLanguage persistence", () => {
7
+ let cookieStore: string;
8
+
9
+ beforeEach(() => {
10
+ cookieStore = "";
11
+ Object.defineProperty(globalThis, "document", {
12
+ value: {
13
+ get cookie() {
14
+ return cookieStore;
15
+ },
16
+ set cookie(val: string) {
17
+ const name = val.split("=")[0];
18
+ const parts = cookieStore.split("; ").filter((c) => c && !c.startsWith(`${name}=`));
19
+ parts.push(val.split(";")[0]);
20
+ cookieStore = parts.filter(Boolean).join("; ");
21
+ },
22
+ },
23
+ writable: true,
24
+ configurable: true,
25
+ });
26
+ });
27
+
28
+ afterEach(() => {
29
+ Object.defineProperty(globalThis, "document", {
30
+ value: undefined,
31
+ writable: true,
32
+ configurable: true,
33
+ });
34
+ });
35
+
36
+ it("persistLanguage sets the preferred_language cookie", async () => {
37
+ const { persistLanguage, getPersistedLanguage } = await import("../src/locale_persistence");
38
+
39
+ // Before: no cookie
40
+ expect(getPersistedLanguage()).toBeNull();
41
+
42
+ // Simulate what useLanguage does when setLanguage is called
43
+ persistLanguage("es");
44
+ expect(getPersistedLanguage()).toBe("es");
45
+
46
+ // Change again
47
+ persistLanguage("de");
48
+ expect(getPersistedLanguage()).toBe("de");
49
+ });
50
+
51
+ it("cookie string contains correct attributes", async () => {
52
+ const { persistLanguage } = await import("../src/locale_persistence");
53
+ // Capture the raw cookie string set on document
54
+ let lastCookie = "";
55
+ Object.defineProperty(globalThis, "document", {
56
+ value: {
57
+ get cookie() {
58
+ return cookieStore;
59
+ },
60
+ set cookie(val: string) {
61
+ lastCookie = val;
62
+ },
63
+ },
64
+ writable: true,
65
+ configurable: true,
66
+ });
67
+
68
+ persistLanguage("fr");
69
+ expect(lastCookie).toContain("preferred_language=fr");
70
+ expect(lastCookie).toContain("path=/");
71
+ expect(lastCookie).toContain("max-age=");
72
+ expect(lastCookie).toContain("samesite=lax");
73
+ });
74
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "../../tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "composite": false,
6
+ "noEmit": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules"],
10
+ "references": []
11
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ // Ensure VITE_FRAPPE_ENABLED is "false" in test env by default
4
+ process.env.VITE_FRAPPE_ENABLED = "false";
5
+
6
+ export default defineConfig({
7
+ test: {
8
+ environment: "jsdom",
9
+ testTimeout: 10000,
10
+ include: ["tests/**/*.test.{ts,tsx}", "src/**/*.spec.{ts,tsx}"],
11
+ exclude: ["**/node_modules/**", "**/dist/**"],
12
+ },
13
+ });