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.
@@ -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
+ });