create-procode 0.0.0-stage → 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.
Files changed (68) hide show
  1. package/README.md +266 -2
  2. package/bin/create-procode.mjs +9 -0
  3. package/package.json +30 -4
  4. package/src/args.mjs +145 -0
  5. package/src/cli.mjs +514 -0
  6. package/src/deps.mjs +208 -0
  7. package/src/log.mjs +23 -0
  8. package/src/multiLanguage.mjs +409 -0
  9. package/src/prompt.mjs +60 -0
  10. package/src/scaffold.mjs +104 -0
  11. package/templates/base/README.md.tmpl +185 -0
  12. package/templates/base/_dotenv.local.example.tmpl +8 -0
  13. package/templates/base/_dotenv.tmpl +41 -0
  14. package/templates/base/_doteslintrc.cjs.tmpl +125 -0
  15. package/templates/base/_dotgitignore.tmpl +32 -0
  16. package/templates/base/_dotnvmrc.tmpl +1 -0
  17. package/templates/base/_dotvscode/extensions.json +3 -0
  18. package/templates/base/index.html.tmpl +18 -0
  19. package/templates/base/package.json.tmpl +15 -0
  20. package/templates/base/public/favicon.svg +4 -0
  21. package/templates/base/src/AdapterProvider/AdapterFactory.ts.tmpl +21 -0
  22. package/templates/base/src/AdapterProvider/SampleContacts.ts.tmpl +136 -0
  23. package/templates/base/src/CustomAction.ts.tmpl +47 -0
  24. package/templates/base/src/CustomAuthorizationProvider.ts.tmpl +77 -0
  25. package/templates/base/src/Factory/Actions/myAction.ts.tmpl +51 -0
  26. package/templates/base/src/Factory/Actions/sampleContacts.ts.tmpl +244 -0
  27. package/templates/base/src/Factory/Actions/setField.ts.tmpl +20 -0
  28. package/templates/base/src/Factory/CustomRuleEffectFactory.ts.tmpl +30 -0
  29. package/templates/base/src/Factory/CustomValidationFactory.tsx.tmpl +24 -0
  30. package/templates/base/src/Factory/CustomWidgetFactory.tsx.tmpl +43 -0
  31. package/templates/base/src/Factory/RuleEffects/readOnlyEffect.ts.tmpl +27 -0
  32. package/templates/base/src/Factory/Validations/myValidation.ts.tmpl +52 -0
  33. package/templates/base/src/Factory/Widgets/MyWidget/index.tsx.tmpl +101 -0
  34. package/templates/base/src/Factory/Widgets/Notification/index.tsx.tmpl +53 -0
  35. package/templates/base/src/Factory/Widgets/Notification/notifiFunc.ts.tmpl +9 -0
  36. package/templates/base/src/Factory/Widgets/SectionNav/index.tsx +193 -0
  37. package/templates/base/src/Factory/Widgets/SectionNav/style.scss +139 -0
  38. package/templates/base/src/Module/Sample/Contacts.json.tmpl +965 -0
  39. package/templates/base/src/Module/index.ts.tmpl +26 -0
  40. package/templates/base/src/Screens/index.tsx.tmpl +45 -0
  41. package/templates/base/src/Utils/errorMessages.ts.tmpl +35 -0
  42. package/templates/base/src/Utils/fieldErrorMapper.ts.tmpl +46 -0
  43. package/templates/base/src/Utils/logger.ts.tmpl +21 -0
  44. package/templates/base/src/Utils/responseFormatter.ts.tmpl +39 -0
  45. package/templates/base/src/Utils/storages.ts.tmpl +35 -0
  46. package/templates/base/src/assets/styles/index.scss.tmpl +149 -0
  47. package/templates/base/src/constant.ts.tmpl +53 -0
  48. package/templates/base/src/icons.ts.tmpl +17 -0
  49. package/templates/base/src/localizationResolver.ts.tmpl +48 -0
  50. package/templates/base/src/main.tsx.tmpl +136 -0
  51. package/templates/base/src/routes.ts.tmpl +20 -0
  52. package/templates/base/src/vite-env.d.ts.tmpl +20 -0
  53. package/templates/base/tsconfig.json.tmpl +30 -0
  54. package/templates/base/tsconfig.node.json.tmpl +11 -0
  55. package/templates/base/vite.config.ts.tmpl +90 -0
  56. package/templates/multi-language/locales/fr/core.json +76 -0
  57. package/templates/multi-language/locales/fr/kendo.json +181 -0
  58. package/templates/multi-language/locales/fr/template.json +169 -0
  59. package/templates/multi-language/locales/fr/widget.json +65 -0
  60. package/templates/multi-language/locales/hi/core.json +76 -0
  61. package/templates/multi-language/locales/hi/kendo.json +181 -0
  62. package/templates/multi-language/locales/hi/template.json +169 -0
  63. package/templates/multi-language/locales/hi/widget.json +65 -0
  64. package/templates/multi-language/sample/fr.json +55 -0
  65. package/templates/multi-language/sample/hi.json +55 -0
  66. package/templates/tests/src/Module/schemas.test.ts +50 -0
  67. package/templates/tests-multi-language/src/multiLanguage/enText.ts +68 -0
  68. package/templates/tests-multi-language/src/multiLanguage/translations.test.ts +92 -0
package/src/deps.mjs ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Dependency set for a generated ProCode app.
3
+ *
4
+ * By default the CLI resolves it at scaffold time with resolveDependencySet():
5
+ * the newest published version of each ProCode package, plus the peers and
6
+ * bundled packages THOSE versions declare — so an app scaffolded after a
7
+ * library release gets that release and the peers it was built against, with
8
+ * no generator update. The constants below are the fallback (--no-latest, or
9
+ * the registry is unreachable): the set verified against a running ProCode
10
+ * host app (vsbackoffice / WorksLogicERP, 2026-10-28) at core 2.5.2 /
11
+ * kendo-widget 3.0.0 / vs-template 2.14.1 / vs-theme 2.6.1 — KendoReact 16.2.0
12
+ * with kendo-theme-default 14.7.0, and react-router-dom 7.
13
+ *
14
+ * Three rules encoded here, each of which is a hard failure if broken:
15
+ *
16
+ * 1. React stays on 18. All three runtime libraries declare `react ^18.2.0`
17
+ * as a peer; React 19 installs cleanly and then fails at runtime.
18
+ * 2. `react-redux` stays on 8.x. On 9.x you get two Provider contexts, the
19
+ * core's store becomes invisible to your components, and every screen
20
+ * renders blank with nothing in the console.
21
+ * 3. `@progress/kendo-theme-default` is a MISSING peer declaration, not an
22
+ * optional extra — the widget library's SCSS imports it by name, so
23
+ * leaving it out is a hard build failure on the first `npm run dev`.
24
+ */
25
+
26
+ /** The four ProCode packages. */
27
+ export const PROCODE = {
28
+ "procode-lowcode-core": "^2.7.0",
29
+ "procode-lowcode-kendo-widget": "^3.2.0",
30
+ "procode-vs-template": "^2.15.1",
31
+ "procode-vs-theme": "^2.7.1",
32
+ };
33
+
34
+ /** Every peer the four packages declare. peerDependenciesMeta is empty on all
35
+ * four — nothing here is optional. */
36
+ export const DECLARED_PEERS = {
37
+ react: "^18.2.0",
38
+ "react-dom": "^18.2.0",
39
+ "react-router-dom": "^6.22.3 || ^7.18.0",
40
+ axios: "^1.6.7",
41
+ "@progress/kendo-data-query": "^1.7.2",
42
+ "@progress/kendo-svg-icons": "^4.9.0 || ^5.0.0",
43
+ "@progress/kendo-react-buttons": "^16.2.0",
44
+ "@progress/kendo-react-charts": "^16.2.0",
45
+ "@progress/kendo-react-common": "^16.2.0",
46
+ "@progress/kendo-react-conversational-ui": "^16.2.0",
47
+ "@progress/kendo-react-data-tools": "^16.2.0",
48
+ "@progress/kendo-react-dateinputs": "^16.2.0",
49
+ "@progress/kendo-react-dialogs": "^16.2.0",
50
+ "@progress/kendo-react-dropdowns": "^16.2.0",
51
+ "@progress/kendo-react-editor": "^16.2.0",
52
+ "@progress/kendo-react-gauges": "^16.2.0",
53
+ "@progress/kendo-react-grid": "^16.2.0",
54
+ "@progress/kendo-react-inputs": "^16.2.0",
55
+ "@progress/kendo-react-intl": "^16.2.0",
56
+ "@progress/kendo-react-layout": "^16.2.0",
57
+ "@progress/kendo-react-listview": "^16.2.0",
58
+ "@progress/kendo-react-popup": "^16.2.0",
59
+ "@progress/kendo-react-upload": "^16.2.0",
60
+ };
61
+
62
+ /** Required at runtime or build time, but NOT declared as a peer by anything. */
63
+ export const UNDECLARED_BUT_REQUIRED = {
64
+ // NOT the KendoReact major: from KendoReact 13 the theme versions apart
65
+ // (KendoReact 16.2 ships against theme 14.7, and no theme 16 exists).
66
+ "@progress/kendo-theme-default": "^14.7.0",
67
+ // A peer of kendo-react-grid 16 that nothing in ProCode declares.
68
+ "@progress/kendo-csv": "^1.0.0",
69
+ "@progress/kendo-licensing": "^1.11.3",
70
+ "@progress/kendo-react-labels": "^16.2.0",
71
+ "@progress/kendo-intl": "^3.2.1",
72
+ "react-toastify": "^11.0.5",
73
+ sass: "^1.38.0",
74
+ };
75
+
76
+ export const DEV_DEPS = {
77
+ // vite.config.ts imports node:url.
78
+ "@types/node": "^24.9.2",
79
+ "@types/react": "^18.2.43",
80
+ "@types/react-dom": "^18.2.17",
81
+ "@vitejs/plugin-react": "^4.2.1",
82
+ typescript: "^5.2.2",
83
+ vite: "^5.2.0",
84
+ };
85
+
86
+ /** `npm test`. vitest 1.x is the line that runs on Vite 5 (DEV_DEPS.vite). */
87
+ export const TEST_DEV_DEPS = {
88
+ vitest: "^1.6.0",
89
+ };
90
+
91
+ export const LINT_DEV_DEPS = {
92
+ eslint: "^8.57.0",
93
+ "@typescript-eslint/eslint-plugin": "^7.2.0",
94
+ "@typescript-eslint/parser": "^7.2.0",
95
+ "eslint-plugin-react-hooks": "^4.6.0",
96
+ "eslint-plugin-react-refresh": "^0.4.6",
97
+ };
98
+
99
+ /** Bundled INSIDE the libraries. Listed for documentation only — do not add
100
+ * them to a generated package.json, and never bump them independently. */
101
+ export const BUNDLED_DO_NOT_ADD = {
102
+ "procode-lowcode-core": {
103
+ "@reduxjs/toolkit": "^1.9.1",
104
+ "react-redux": "^8.0.5",
105
+ "redux-saga": "^1.2.3",
106
+ uuid: "^11.1.1",
107
+ },
108
+ "procode-lowcode-kendo-widget": { lodash: "^4.17.21", uuid: "^11.1.1" },
109
+ "procode-vs-template": {
110
+ lodash: "^4.17.21",
111
+ "react-beautiful-dnd": "^13.1.1",
112
+ uuid: "^11.1.1",
113
+ },
114
+ "procode-vs-theme": {},
115
+ };
116
+
117
+ const sortKeys = (obj) =>
118
+ Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
119
+
120
+ /** The tested set, used with --no-latest or when the registry is unreachable. */
121
+ export const PINNED_SET = {
122
+ procode: PROCODE,
123
+ peers: DECLARED_PEERS,
124
+ extras: UNDECLARED_BUT_REQUIRED,
125
+ bundled: BUNDLED_DO_NOT_ADD,
126
+ source: "pinned",
127
+ };
128
+
129
+ /** Lowest version a range admits, as [major, minor, patch]; [0,0,0] if none. */
130
+ const floorOf = (range) => (String(range).match(/(\d+)\.(\d+)\.(\d+)/) ?? [0, 0, 0, 0]).slice(1).map(Number);
131
+ const newer = (a, b) => {
132
+ const [x, y] = [floorOf(a), floorOf(b)];
133
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i] ? a : b;
134
+ return a;
135
+ };
136
+
137
+ /**
138
+ * Ask the registry for the newest published version of each ProCode package,
139
+ * and derive everything else from those manifests:
140
+ *
141
+ * peers every peerDependency the four declare; where two declare the same
142
+ * peer, the range with the higher floor wins (vs-template wants
143
+ * react-router-dom ^6.22.3 where core wants ^6.6.1).
144
+ * extras required but undeclared. kendo-react-labels follows the
145
+ * kendo-react range the widget library declares (KendoReact 16 wants
146
+ * exact siblings). kendo-theme-default does NOT: its major stopped
147
+ * tracking KendoReact's at 13, so it stays on its tested pin and is
148
+ * bumped by hand with the widget library. The rest stay pinned too.
149
+ * bundled each package's own dependencies, for the _procodeBundled note.
150
+ *
151
+ * Uses the registry npm/npx was invoked with (npm_config_registry), so a
152
+ * mirror or proxy registry is honoured. Throws on any failure; the caller
153
+ * falls back to PINNED_SET.
154
+ */
155
+ export async function resolveDependencySet({ registry, timeoutMs = 10000 } = {}) {
156
+ const base = String(registry || process.env.npm_config_registry || "https://registry.npmjs.org/").replace(/\/?$/, "/");
157
+
158
+ const manifests = await Promise.all(
159
+ Object.keys(PROCODE).map(async (name) => {
160
+ const res = await fetch(base + name.replace("/", "%2f") + "/latest", {
161
+ headers: { accept: "application/json" },
162
+ signal: AbortSignal.timeout(timeoutMs),
163
+ });
164
+ if (!res.ok) throw new Error(name + ": HTTP " + res.status + " from " + base);
165
+ const manifest = await res.json();
166
+ if (typeof manifest?.version !== "string") throw new Error(name + ": no version in the registry response");
167
+ return manifest;
168
+ }),
169
+ );
170
+
171
+ const procode = {};
172
+ const peers = {};
173
+ const bundled = {};
174
+ for (const m of manifests) {
175
+ procode[m.name] = "^" + m.version;
176
+ bundled[m.name] = m.dependencies ?? {};
177
+ for (const [peer, range] of Object.entries(m.peerDependencies ?? {})) {
178
+ peers[peer] = peers[peer] ? newer(peers[peer], range) : range;
179
+ }
180
+ }
181
+
182
+ const extras = { ...UNDECLARED_BUT_REQUIRED };
183
+ const kendoReact = Object.entries(peers).find(([n]) => n.startsWith("@progress/kendo-react-"))?.[1];
184
+ if (kendoReact) {
185
+ extras["@progress/kendo-react-labels"] = kendoReact;
186
+ }
187
+
188
+ return { procode, peers, extras, bundled, source: "latest" };
189
+ }
190
+
191
+ /** `set` is PINNED_SET or what resolveDependencySet() returned. */
192
+ export function buildDependencies(set = PINNED_SET) {
193
+ return sortKeys({ ...set.procode, ...set.peers, ...set.extras });
194
+ }
195
+
196
+ /**
197
+ * The React type packages follow the React major the libraries declare, so a
198
+ * future move off React 18 does not leave @types/react behind.
199
+ */
200
+ export function buildDevDependencies({ lint = true, tests = true, set = PINNED_SET } = {}) {
201
+ const devDeps = { ...DEV_DEPS, ...(lint ? LINT_DEV_DEPS : {}), ...(tests ? TEST_DEV_DEPS : {}) };
202
+ const reactMajor = floorOf(set.peers.react ?? "")[0];
203
+ if (reactMajor && reactMajor !== floorOf(DEV_DEPS["@types/react"])[0]) {
204
+ devDeps["@types/react"] = "^" + reactMajor + ".0.0";
205
+ devDeps["@types/react-dom"] = "^" + reactMajor + ".0.0";
206
+ }
207
+ return sortKeys(devDeps);
208
+ }
package/src/log.mjs ADDED
@@ -0,0 +1,23 @@
1
+ const useColor =
2
+ process.env.NO_COLOR === undefined &&
3
+ process.env.TERM !== "dumb" &&
4
+ process.stdout.isTTY !== false;
5
+
6
+ const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
7
+
8
+ export const c = {
9
+ bold: wrap("1"),
10
+ dim: wrap("2"),
11
+ red: wrap("31"),
12
+ green: wrap("32"),
13
+ yellow: wrap("33"),
14
+ blue: wrap("34"),
15
+ magenta: wrap("35"),
16
+ cyan: wrap("36"),
17
+ };
18
+
19
+ export const log = (msg = "") => console.log(msg);
20
+ export const step = (msg) => console.log(`${c.cyan("›")} ${msg}`);
21
+ export const ok = (msg) => console.log(`${c.green("✔")} ${msg}`);
22
+ export const warn = (msg) => console.log(`${c.yellow("!")} ${msg}`);
23
+ export const info = (msg) => console.log(` ${c.dim(msg)}`);
@@ -0,0 +1,409 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { writeJson } from "./scaffold.mjs";
5
+
6
+ /**
7
+ * Multi-language scaffolding (`--multi-language`, or "yes" to the prompt).
8
+ *
9
+ * ProCode multi-language (procode-lowcode-core >= 2.6.0) is switched on by
10
+ * METADATA: the root screen — the schema `VITE_HEADER_SLUG` names, loaded before
11
+ * any page — carries `"multiLanguage": { "enabled": true, … }`. Translations are
12
+ * files fetched at run time from `public/locales/<code>/<namespace>.json`, so a
13
+ * new language never needs a build. This module writes the smallest app that
14
+ * uses all of it:
15
+ *
16
+ * src/Module/Header.json the root screen: the multiLanguage block and
17
+ * a LANGUAGESELECTOR
18
+ * src/multiLanguage/index.ts the start language (custom logic, Core prop)
19
+ * public/locales/<code>/*.json one folder per language — the four library
20
+ * namespaces and the app's own `header`
21
+ * main.tsx / Module/index.ts / wired: the Core prop, the `header` slug,
22
+ * CustomWidgetFactory.tsx custom widgets wrapped with the translator
23
+ *
24
+ * The libraries ship only their English default text. Hindi and French
25
+ * translations of that text are bundled here (taken from vsbackoffice); any
26
+ * other language gets empty library files and shows English until translated.
27
+ */
28
+
29
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ const LIBRARY_LOCALES_DIR = path.resolve(__dirname, "..", "templates", "multi-language", "locales");
31
+
32
+ /** The four library namespaces — translations of the packages' own text. */
33
+ export const LIBRARY_NAMESPACES = ["core", "widget", "kendo", "template"];
34
+
35
+ /** Names the generator knows; an unknown code is still accepted. */
36
+ const KNOWN = {
37
+ en: { english: "English", native: "English", short: "EN" },
38
+ fr: { english: "French", native: "Français", short: "FR" },
39
+ hi: { english: "Hindi", native: "हिन्दी", short: "हि" },
40
+ es: { english: "Spanish", native: "Español", short: "ES" },
41
+ de: { english: "German", native: "Deutsch", short: "DE" },
42
+ si: { english: "Sinhala", native: "සිංහල", short: "සිං" },
43
+ ta: { english: "Tamil", native: "தமிழ்", short: "த" },
44
+ ar: { english: "Arabic", native: "العربية", short: "ع", dir: "rtl" },
45
+ };
46
+
47
+ /** Each language's name, written in the UI languages the generator has text for. */
48
+ const NAME_IN = {
49
+ fr: { en: "Anglais", fr: "Français", hi: "Hindi", es: "Espagnol", de: "Allemand", si: "Cingalais", ta: "Tamoul", ar: "Arabe" },
50
+ hi: { en: "अंग्रेज़ी", fr: "फ़्रेंच", hi: "हिन्दी", es: "स्पेनिश", de: "जर्मन", si: "सिंहली", ta: "तमिल", ar: "अरबी" },
51
+ };
52
+
53
+ const CODE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
54
+
55
+ /**
56
+ * "en, fr ,hi" -> ["en", "fr", "hi"]. English always comes first: the libraries'
57
+ * default text is English, so it is the `defaultLocale` (the language of the
58
+ * default text) whatever else is offered.
59
+ */
60
+ export function parseLanguages(input) {
61
+ const codes = String(input ?? "")
62
+ .split(/[\s,;]+/)
63
+ .map((c) => c.trim())
64
+ .filter(Boolean);
65
+ const bad = codes.filter((c) => !CODE_RE.test(c));
66
+ if (bad.length) {
67
+ throw new Error(
68
+ `--languages: "${bad.join(", ")}" is not a language code. Use codes like en, fr, hi or pt-BR, separated by commas.`,
69
+ );
70
+ }
71
+ const unique = [...new Set(["en", ...codes])];
72
+ return unique;
73
+ }
74
+
75
+ /** Validator for the interactive question. */
76
+ export const languagesProblem = (value) => {
77
+ try {
78
+ parseLanguages(value);
79
+ return null;
80
+ } catch (err) {
81
+ return err.message.replace(/^--languages: /, "");
82
+ }
83
+ };
84
+
85
+ const englishName = (code) => KNOWN[code]?.english ?? code;
86
+ /** `header.languageFrench` — the extractor's key convention. */
87
+ const labelKey = (code) => "language" + englishName(code).replace(/[^A-Za-z0-9]/g, "").replace(/^./, (ch) => ch.toUpperCase());
88
+
89
+ /** The root screen. */
90
+ function headerSchema(languages, withSample) {
91
+ const namespaces = [...LIBRARY_NAMESPACES, "header", ...(withSample ? ["sample"] : [])];
92
+ return {
93
+ _readme: [
94
+ "ROOT SCREEN — the schema VITE_HEADER_SLUG (.env) names. The core loads it before any page,",
95
+ "so its `multiLanguage` block applies app-wide and no screen renders in the wrong language.",
96
+ "A screen shows this header when it sets \"enableHeader\": true.",
97
+ "",
98
+ "Adding a language: add public/locales/<code>/ (copy en/ and translate) and an entry to",
99
+ "`locales` below. No build is needed. Moving a screen's text into the files:",
100
+ " npx procode-extract-text <namespace> src/Module/<Folder>/<Screen>.json",
101
+ "then add <namespace> to `resources.namespaces`. README.md, section Multi-language, has the rest.",
102
+ "Keys beginning with an underscore are comments; the core ignores them.",
103
+ ],
104
+ screenName: "",
105
+ screenId: "header",
106
+ slug: "header",
107
+ multiLanguage: {
108
+ enabled: true,
109
+ defaultLocale: "en",
110
+ locales: languages.map((code) => ({
111
+ code,
112
+ label: `@t:header.${labelKey(code)}`,
113
+ nativeLabel: KNOWN[code]?.native ?? code,
114
+ shortLabel: KNOWN[code]?.short ?? code.toUpperCase(),
115
+ ...(KNOWN[code]?.dir ? { dir: KNOWN[code].dir } : {}),
116
+ })),
117
+ resources: {
118
+ url: "{base}locales/{locale}/{namespace}.json",
119
+ namespaces,
120
+ },
121
+ },
122
+ rootScreenElement: {
123
+ uiElementType: "CONTAINER",
124
+ role: "prc-container",
125
+ widgetStyle: { className: "app-header" },
126
+ children: [
127
+ {
128
+ id: "header-language",
129
+ uiElementType: "WIDGET",
130
+ widgetType: "LANGUAGESELECTOR",
131
+ _comment:
132
+ "The language switch. Its languages are multiLanguage.locales above. segmented = one pill per language (up to 4); menu = a globe button with a panel.",
133
+ variant: languages.length <= 4 ? "segmented" : "menu",
134
+ size: "sm",
135
+ },
136
+ ],
137
+ },
138
+ };
139
+ }
140
+
141
+ /** `header.json` for one UI language: every offered language's name in it. */
142
+ function headerBundle(uiLanguage, languages) {
143
+ const bundle = {};
144
+ for (const code of languages) {
145
+ bundle[labelKey(code)] =
146
+ uiLanguage === "en" ? englishName(code) : NAME_IN[uiLanguage]?.[code] ?? KNOWN[code]?.native ?? englishName(code);
147
+ }
148
+ return Object.fromEntries(Object.entries(bundle).sort(([a], [b]) => a.localeCompare(b)));
149
+ }
150
+
151
+ const APP_MULTI_LANGUAGE_TS = `/**
152
+ * Multi-language in this app is METADATA, not code (procode-lowcode-core 2.6.0+):
153
+ *
154
+ * - src/Module/Header.json (the root screen) switches it on — "multiLanguage.enabled" —
155
+ * and lists the languages and where the translation files are;
156
+ * - the files are public/locales/<code>/<namespace>.json, fetched at run time. A
157
+ * file's keys sit under its name (header.json -> "header.*"); core.json,
158
+ * widget.json, kendo.json and template.json translate the libraries' own text,
159
+ * which they ship only in English;
160
+ * - LANGUAGESELECTOR in Header.json is the switch, and remembers the choice.
161
+ *
162
+ * In a schema, any text field may hold a reference: "title": "@t:sample.contacts".
163
+ * In code: useTranslation().t("ns.key", params) in a component, translate() or
164
+ * toMessageRef() (a reference to store and show later) elsewhere — all from
165
+ * "procode-lowcode-core". Never translate a value that is sent to an API or compared.
166
+ *
167
+ * Adding a language: add public/locales/<code>/ and the code to Header.json's
168
+ * "multiLanguage.locales". No build is needed.
169
+ * Moving a screen's text into the files:
170
+ * npx procode-extract-text <namespace> src/Module/<Folder>/<Screen>.json
171
+ */
172
+
173
+ /** Where core remembers the chosen language (its default). */
174
+ export const LOCALE_STORAGE_KEY = "procode.locale";
175
+
176
+ /**
177
+ * The language to start in when the user has not chosen one — custom logic, run
178
+ * once at boot (the Core prop \`multiLanguage.initialLocale\` in main.tsx). A choice
179
+ * made with the LANGUAGESELECTOR is remembered and always wins over this.
180
+ *
181
+ * Reads a \`?lang=fr\` link. Extend it with a company or profile setting your
182
+ * backend returns (session storage, a cookie). Answer undefined to fall through
183
+ * to the browser's languages, then English.
184
+ */
185
+ export const initialLocale = (): string | undefined =>
186
+ new URLSearchParams(window.location.search).get("lang") ?? undefined;
187
+ `;
188
+
189
+ const README_SECTION = (languages) => `
190
+ ## Multi-language
191
+
192
+ Switched on at scaffold time (\`--multi-language\`), offering **${languages.join(", ")}**.
193
+
194
+ | What | Where |
195
+ |---|---|
196
+ | Turn it on / off, the languages, the translation files' location | \`src/Module/Header.json\` — the root screen's \`multiLanguage\` block |
197
+ | The language switch | \`LANGUAGESELECTOR\` in \`src/Module/Header.json\` (a screen shows the header with \`"enableHeader": true\`) |
198
+ | Start language (a \`?lang=fr\` link; add a company/profile setting) | \`src/multiLanguage/index.ts\` → \`initialLocale\`, passed to \`<Core>\` in \`main.tsx\` |
199
+ | Translations | \`public/locales/<code>/<namespace>.json\` — fetched at run time, no build |
200
+ | The libraries' own text | \`core\`, \`widget\`, \`kendo\`, \`template\` namespaces — English is built in; Hindi and French are included |
201
+
202
+ - **In a schema**, any text field may hold a reference: \`"title": "@t:sample.contacts"\`.
203
+ - **Move a screen's text** into the files: \`npx procode-extract-text <namespace> src/Module/<Folder>/<Screen>.json\`,
204
+ then add \`<namespace>\` to \`resources.namespaces\` and translate \`public/locales/<code>/<namespace>.json\`.
205
+ Check every language has every key: \`npx procode-extract-text <namespace> --check\`.
206
+ - **In code**: \`useTranslation().t("ns.key", params)\` in a component; \`toMessageRef("ns.key", params)\`
207
+ for text you store and show later (a validation message, a toast, a value in state); \`translate()\` only
208
+ at a boundary. All from \`procode-lowcode-core\`. Never translate a value that is sent to an API or compared.
209
+ - **Add a language**: create \`public/locales/<code>/\` (copy \`en/\`, translate) and add it to
210
+ \`multiLanguage.locales\` in \`Header.json\`.
211
+ `;
212
+
213
+ /**
214
+ * Edit a generated file as LF text and write it back with the line endings it
215
+ * had (the templates may be checked out CRLF). Every edit fails loudly when its
216
+ * anchor is missing — a silent no-op would ship a half-wired app.
217
+ */
218
+ function editFile(file, edit) {
219
+ const raw = fs.readFileSync(file, "utf8");
220
+ const crlf = raw.includes("\r\n");
221
+ const next = edit(raw.replace(/\r\n/g, "\n"));
222
+ fs.writeFileSync(file, crlf ? next.replace(/\n/g, "\r\n") : next, "utf8");
223
+ }
224
+
225
+ const missing = (file, anchor) =>
226
+ new Error(`multi-language: could not find "${anchor}" in ${file} — the template changed.`);
227
+
228
+ /** Insert `addition` after the line that contains `anchor`. */
229
+ function insertAfter(file, anchor, addition) {
230
+ editFile(file, (text) => {
231
+ const at = text.indexOf(anchor);
232
+ if (at === -1) throw missing(file, anchor);
233
+ const lineEnd = text.indexOf("\n", at);
234
+ const cut = lineEnd === -1 ? text.length : lineEnd + 1;
235
+ return text.slice(0, cut) + addition + text.slice(cut);
236
+ });
237
+ }
238
+
239
+ function replaceOnce(file, from, to) {
240
+ editFile(file, (text) => {
241
+ if (!text.includes(from)) throw missing(file, from);
242
+ return text.replace(from, to);
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Write and wire everything. `root` is the scaffolded project. Returns the
248
+ * project-relative files written, and which languages got library translations.
249
+ */
250
+ export function applyMultiLanguage(root, { languages, withSample }) {
251
+ const written = [];
252
+ const rel = (p) => path.relative(root, p).split(path.sep).join("/");
253
+
254
+ // ---- the root screen ----------------------------------------------------
255
+ const headerFile = path.join(root, "src", "Module", "Header.json");
256
+ writeJson(headerFile, headerSchema(languages, withSample));
257
+ written.push(rel(headerFile));
258
+
259
+ // ---- translation files --------------------------------------------------
260
+ const translatedLibraries = [];
261
+ for (const code of languages) {
262
+ const dir = path.join(root, "public", "locales", code);
263
+ fs.mkdirSync(dir, { recursive: true });
264
+ for (const ns of LIBRARY_NAMESPACES) {
265
+ const bundled = path.join(LIBRARY_LOCALES_DIR, code, `${ns}.json`);
266
+ const target = path.join(dir, `${ns}.json`);
267
+ // English: empty — the libraries' English is built in; a key put here
268
+ // re-words it. Other languages: the bundled translation, or empty.
269
+ if (code !== "en" && fs.existsSync(bundled)) fs.copyFileSync(bundled, target);
270
+ else writeJson(target, {});
271
+ written.push(rel(target));
272
+ }
273
+ if (code !== "en" && fs.existsSync(path.join(LIBRARY_LOCALES_DIR, code))) translatedLibraries.push(code);
274
+ const header = path.join(dir, "header.json");
275
+ writeJson(header, headerBundle(code, languages));
276
+ written.push(rel(header));
277
+ }
278
+
279
+ // ---- the app's multi-language module -----------------------------------
280
+ const mlDir = path.join(root, "src", "multiLanguage");
281
+ fs.mkdirSync(mlDir, { recursive: true });
282
+ fs.writeFileSync(path.join(mlDir, "index.ts"), APP_MULTI_LANGUAGE_TS, "utf8");
283
+ written.push("src/multiLanguage/index.ts");
284
+
285
+ // ---- main.tsx: the Core prop -------------------------------------------
286
+ const main = path.join(root, "src", "main.tsx");
287
+ insertAfter(main, 'import { APPLICATION_CODE, AuthToken } from "./constant";', 'import { initialLocale } from "./multiLanguage";\n');
288
+ insertAfter(
289
+ main,
290
+ " NotificationElement: Notification,",
291
+ [
292
+ "",
293
+ " // Multi-language is switched on by src/Module/Header.json (the root screen's",
294
+ " // `multiLanguage` block). This prop holds what metadata cannot: custom logic",
295
+ " // for the start language, run once at boot (src/multiLanguage/index.ts).",
296
+ " // `resolveInitialLocale` / `onLocaleChange` belong here too, for a language",
297
+ " // kept in a user profile.",
298
+ " multiLanguage: { initialLocale },",
299
+ "",
300
+ ].join("\n"),
301
+ );
302
+
303
+ // ---- the root screen's slug --------------------------------------------
304
+ const moduleIndex = path.join(root, "src", "Module", "index.ts");
305
+ const SLUG_FN = /export const getSchemaBySlugService = \((\w+): string, (\w+): string\): any => \{\n/;
306
+ editFile(moduleIndex, (text) => {
307
+ if (!SLUG_FN.test(text)) throw missing(moduleIndex, "export const getSchemaBySlugService = (slug: string, …): any => {");
308
+ return (
309
+ 'import Header from "./Header.json";\n' +
310
+ text.replace(
311
+ SLUG_FN,
312
+ (head, slug) =>
313
+ head +
314
+ " // The root screen (VITE_HEADER_SLUG): loaded before every page, it switches\n" +
315
+ " // multi-language on and draws the language selector.\n" +
316
+ ` if (${slug} === "header") return Header;\n`,
317
+ )
318
+ );
319
+ });
320
+
321
+ // ---- custom widgets follow the language --------------------------------
322
+ const factory = path.join(root, "src", "Factory", "CustomWidgetFactory.tsx");
323
+ insertAfter(factory, 'import { WidgetTypes } from "../constant";', 'import { withWidgetMultiLanguage } from "procode-lowcode-kendo-widget";\n');
324
+ replaceOnce(
325
+ factory,
326
+ " private constructor() {\n this.createWidgetMap();\n }",
327
+ [
328
+ " private constructor() {",
329
+ " this.createWidgetMap();",
330
+ " // Each custom widget gets the translator core hands it on kendo-widget's",
331
+ " // context, plus Kendo's LocalizationProvider — so a Dropdown or DatePicker",
332
+ ' // a widget draws itself (from "procode-lowcode-kendo-widget/widgets") shows',
333
+ " // its own text and Kendo's in the active language (kendo-widget 3.2.0+).",
334
+ " this.widgetMap.forEach((widget, type) =>",
335
+ " this.widgetMap.set(type, withWidgetMultiLanguage(widget as any) as React.FC<any>),",
336
+ " );",
337
+ " }",
338
+ ].join("\n"),
339
+ );
340
+
341
+ // ---- the sample screen shows the header (and so the switch) ------------
342
+ if (withSample) {
343
+ // Its text moves to en/sample.json when procode-extract-text runs (after
344
+ // install); the other languages start with an empty file to translate.
345
+ seedSampleBundles(root, languages);
346
+ replaceOnce(path.join(root, "src", "Module", "Sample", "Contacts.json"), '"enableHeader": false,', '"enableHeader": true,');
347
+ }
348
+
349
+ // ---- header styling -----------------------------------------------------
350
+ const append = (file, addition) => editFile(file, (text) => text.replace(/\n*$/, "\n") + addition);
351
+ append(
352
+ path.join(root, "src", "assets", "styles", "index.scss"),
353
+ [
354
+ "",
355
+ "// The root screen's bar (src/Module/Header.json): the language switch at the right.",
356
+ ".app-header {",
357
+ " display: flex;",
358
+ " justify-content: flex-end;",
359
+ " align-items: center;",
360
+ " padding: 8px 16px;",
361
+ "}",
362
+ "",
363
+ ].join("\n"),
364
+ );
365
+
366
+ // ---- README -------------------------------------------------------------
367
+ append(path.join(root, "README.md"), README_SECTION(languages));
368
+
369
+ return { written, translatedLibraries };
370
+ }
371
+
372
+ const SAMPLE_TRANSLATIONS_DIR = path.resolve(__dirname, "..", "templates", "multi-language", "sample");
373
+
374
+ /**
375
+ * After procode-extract-text has written en/sample.json: give each language the
376
+ * generator has a translation of the sample screen for (fr, hi) that text,
377
+ * limited to the keys the extractor produced — a key the screen no longer has
378
+ * is dropped, and one with no translation shows its English until translated.
379
+ * Returns the languages filled.
380
+ */
381
+ export function applySampleTranslations(root, languages) {
382
+ const enFile = path.join(root, "public", "locales", "en", "sample.json");
383
+ if (!fs.existsSync(enFile)) return [];
384
+ const english = JSON.parse(fs.readFileSync(enFile, "utf8"));
385
+ const keys = Object.keys(english);
386
+ const filled = [];
387
+ for (const code of languages) {
388
+ if (code === "en") continue;
389
+ const bundled = path.join(SAMPLE_TRANSLATIONS_DIR, `${code}.json`);
390
+ // A language the generator has no sample translation for starts with the
391
+ // English text — what the screen would show anyway — so every language has
392
+ // every key (the translation test checks that) and a translator replaces it.
393
+ const text = fs.existsSync(bundled) ? JSON.parse(fs.readFileSync(bundled, "utf8")) : {};
394
+ const out = Object.fromEntries(keys.map((k) => [k, typeof text[k] === "string" ? text[k] : english[k]]));
395
+ writeJson(path.join(root, "public", "locales", code, "sample.json"), out);
396
+ if (fs.existsSync(bundled)) filled.push(code);
397
+ }
398
+ return filled;
399
+ }
400
+
401
+ /** A "sample" bundle for each non-English language, so every language folder has every namespace. */
402
+ export function seedSampleBundles(root, languages) {
403
+ for (const code of languages) {
404
+ if (code === "en") continue;
405
+ const file = path.join(root, "public", "locales", code, "sample.json");
406
+ if (!fs.existsSync(file)) writeJson(file, {});
407
+ }
408
+ }
409
+