create-procode 1.0.0 → 1.1.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 +28 -2
- package/package.json +1 -1
- package/src/args.mjs +6 -0
- package/src/cli.mjs +123 -1
- package/src/deps.mjs +208 -195
- package/src/multiLanguage.mjs +409 -0
- package/templates/base/README.md.tmpl +5 -3
- package/templates/base/src/assets/styles/index.scss.tmpl +149 -146
- package/templates/base/vite.config.ts.tmpl +83 -54
- package/templates/multi-language/locales/fr/core.json +76 -0
- package/templates/multi-language/locales/fr/kendo.json +181 -0
- package/templates/multi-language/locales/fr/template.json +169 -0
- package/templates/multi-language/locales/fr/widget.json +65 -0
- package/templates/multi-language/locales/hi/core.json +76 -0
- package/templates/multi-language/locales/hi/kendo.json +181 -0
- package/templates/multi-language/locales/hi/template.json +169 -0
- package/templates/multi-language/locales/hi/widget.json +65 -0
- package/templates/multi-language/sample/fr.json +55 -0
- package/templates/multi-language/sample/hi.json +55 -0
- package/templates/tests/src/Module/schemas.test.ts +50 -0
- package/templates/tests-multi-language/src/multiLanguage/enText.ts +68 -0
- package/templates/tests-multi-language/src/multiLanguage/translations.test.ts +92 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
candidateKeys,
|
|
3
|
+
flattenMessages,
|
|
4
|
+
interpolate,
|
|
5
|
+
parseMessageRef,
|
|
6
|
+
} from "procode-lowcode-core";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test helper: the English text of an "@t:" reference, read straight from this
|
|
10
|
+
* app's public/locales/en/*.json (no Core, no async loading) and formatted by
|
|
11
|
+
* core's own engine — the same plural suffixes (`_one` / `_other`) and `{name}`
|
|
12
|
+
* placeholders `translate` applies. Once a screen's text is moved into the
|
|
13
|
+
* translation files its schema holds references, not English; a test that
|
|
14
|
+
* asserts what a user sees reads it back through this:
|
|
15
|
+
*
|
|
16
|
+
* expect(enText(result.message)).toBe("Please enter a name");
|
|
17
|
+
*
|
|
18
|
+
* Plain text is returned unchanged. Library keys (core.*, widget.*, kendo.*,
|
|
19
|
+
* template.*) are not in these files; a key with no English THROWS, so a typo
|
|
20
|
+
* fails its test loudly instead of passing on a raw key.
|
|
21
|
+
*/
|
|
22
|
+
const bundles = import.meta.glob<Record<string, unknown>>("/public/locales/en/*.json", {
|
|
23
|
+
eager: true,
|
|
24
|
+
import: "default",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// One flat table, `<namespace>.<key>` → text, built the way core builds it from
|
|
28
|
+
// the same files (each file's content sits under its namespace).
|
|
29
|
+
const english: Record<string, string> = {};
|
|
30
|
+
Object.entries(bundles).forEach(([file, messages]) => {
|
|
31
|
+
const namespace = file.replace(/^.*\//, "").replace(/\.json$/, "");
|
|
32
|
+
flattenMessages({ [namespace]: messages as Record<string, string> }, "", english);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Is this key (or a plural form of it) in the English files? */
|
|
36
|
+
export const hasEnglish = (key: string, params?: Record<string, unknown>): boolean =>
|
|
37
|
+
candidateKeys(key, "en", params as never).some((k) => k in english);
|
|
38
|
+
|
|
39
|
+
export const enText = (value: unknown): unknown => {
|
|
40
|
+
if (typeof value !== "string") return value;
|
|
41
|
+
const ref = parseMessageRef(value);
|
|
42
|
+
if (!ref) return value;
|
|
43
|
+
const key = candidateKeys(ref.key, "en", ref.params).find((k) => k in english);
|
|
44
|
+
if (key === undefined) throw new Error(`enText: no English for "${ref.key}"`);
|
|
45
|
+
return interpolate(english[key], ref.params, "en");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* For a schema test that asserts the English copy: every "@t:" reference in the
|
|
50
|
+
* given schema object is replaced, in place, by its English. Vitest gives each
|
|
51
|
+
* test file its own module graph, so the change stays inside that file.
|
|
52
|
+
*/
|
|
53
|
+
export const withEnglishText = <T>(value: T): T => {
|
|
54
|
+
const seen = new WeakSet<object>();
|
|
55
|
+
const walk = (node: unknown): void => {
|
|
56
|
+
if (!node || typeof node !== "object" || seen.has(node as object)) return;
|
|
57
|
+
seen.add(node as object);
|
|
58
|
+
Object.entries(node as Record<string, unknown>).forEach(([key, item]) => {
|
|
59
|
+
if (typeof item === "string" && item.startsWith("@t:")) {
|
|
60
|
+
(node as Record<string, unknown>)[key] = enText(item);
|
|
61
|
+
} else {
|
|
62
|
+
walk(item);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
walk(value);
|
|
67
|
+
return value;
|
|
68
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseMessageRef } from "procode-lowcode-core";
|
|
3
|
+
import Header from "../Module/Header.json";
|
|
4
|
+
import { hasEnglish } from "./enText";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Nothing missing, in any language. A gap is silent at runtime — the screen shows
|
|
8
|
+
* the English (or, for a typo'd key, the raw key) with one console warning — so
|
|
9
|
+
* these turn it into a failing test:
|
|
10
|
+
*
|
|
11
|
+
* 1. every "@t:" reference in a screen JSON has English text;
|
|
12
|
+
* 2. every language offered in Header.json has every key English has, in
|
|
13
|
+
* every namespace the app owns;
|
|
14
|
+
* 3. a translation uses only {placeholders} its English has — a misspelt
|
|
15
|
+
* one is printed literally. (Dropping one is allowed: English's plural
|
|
16
|
+
* "contact{s}" needs no {s} in Hindi.)
|
|
17
|
+
*
|
|
18
|
+
* The library namespaces (core, widget, kendo, template) are not checked: the
|
|
19
|
+
* libraries carry their own English, and an untranslated library key falls back
|
|
20
|
+
* to it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const LIBRARY = new Set(["core", "widget", "kendo", "template"]);
|
|
24
|
+
const settings = (Header as any).multiLanguage;
|
|
25
|
+
const codes: string[] = settings.locales.map((l: { code: string }) => l.code);
|
|
26
|
+
const appNamespaces: string[] = settings.resources.namespaces.filter((ns: string) => !LIBRARY.has(ns));
|
|
27
|
+
|
|
28
|
+
const files = import.meta.glob<Record<string, string>>("/public/locales/*/*.json", {
|
|
29
|
+
eager: true,
|
|
30
|
+
import: "default",
|
|
31
|
+
});
|
|
32
|
+
const bundle = (code: string, ns: string): Record<string, string> | undefined =>
|
|
33
|
+
files[`/public/locales/${code}/${ns}.json`];
|
|
34
|
+
|
|
35
|
+
const screens = Object.entries(
|
|
36
|
+
import.meta.glob<Record<string, any>>("../Module/**/*.json", { eager: true, import: "default" }),
|
|
37
|
+
).filter(([, schema]) => schema && typeof schema === "object" && "rootScreenElement" in schema);
|
|
38
|
+
|
|
39
|
+
/** Every "@t:" string anywhere in a value. */
|
|
40
|
+
const references = (value: unknown, out: string[] = []): string[] => {
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
if (value.startsWith("@t:")) out.push(value);
|
|
43
|
+
} else if (value && typeof value === "object") {
|
|
44
|
+
Object.values(value).forEach((item) => references(item, out));
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const placeholders = (text: string) => (text.match(/\{\s*[A-Za-z0-9_.-]+/g) ?? []).map((p) => p.replace(/[{\s]/g, "")).sort();
|
|
50
|
+
|
|
51
|
+
describe("translations", () => {
|
|
52
|
+
it("every reference in a screen has English text", () => {
|
|
53
|
+
const missing = screens.flatMap(([file, schema]) =>
|
|
54
|
+
references(schema)
|
|
55
|
+
.map((ref) => parseMessageRef(ref))
|
|
56
|
+
.filter((ref) => ref && !LIBRARY.has(ref.key.split(".")[0]) && !hasEnglish(ref.key, ref.params))
|
|
57
|
+
.map((ref) => `${file}: ${ref!.key}`),
|
|
58
|
+
);
|
|
59
|
+
expect(missing).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("every language has every key English has", () => {
|
|
63
|
+
const gaps = codes
|
|
64
|
+
.filter((code) => code !== "en")
|
|
65
|
+
.flatMap((code) =>
|
|
66
|
+
appNamespaces.flatMap((ns) => {
|
|
67
|
+
const english = bundle("en", ns) ?? {};
|
|
68
|
+
const other = bundle(code, ns) ?? {};
|
|
69
|
+
return Object.keys(english)
|
|
70
|
+
.filter((key) => !(key in other))
|
|
71
|
+
.map((key) => `${code}/${ns}.json: ${key}`);
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
expect(gaps).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("a translation uses no {placeholder} its English lacks", () => {
|
|
78
|
+
const changed = codes
|
|
79
|
+
.filter((code) => code !== "en")
|
|
80
|
+
.flatMap((code) =>
|
|
81
|
+
appNamespaces.flatMap((ns) => {
|
|
82
|
+
const english = bundle("en", ns) ?? {};
|
|
83
|
+
const other = bundle(code, ns) ?? {};
|
|
84
|
+
return Object.keys(english)
|
|
85
|
+
.filter((key) => key in other)
|
|
86
|
+
.filter((key) => placeholders(other[key]).some((p) => !placeholders(english[key]).includes(p)))
|
|
87
|
+
.map((key) => `${code}/${ns}.json: ${key}`);
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
expect(changed).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|