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,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
+
@@ -35,7 +35,9 @@ the `KENDO_UI_LICENSE` environment variable in CI. Then run `npm install`, or
35
35
  1. Point `VITE_CENTRAL_SERVICE_BASE_URL` in `.env` at your environment.
36
36
  2. Copy `.env.local.example` to `.env.local` and put a dev token in
37
37
  `VITE_HARDCODED_TOKEN` — or sign in so the `AuthToken` cookie is set.
38
- **`.env.local` is gitignored. Never commit a token to `.env`.**
38
+ **`.env.local` is gitignored. Never commit a token to `.env`.** `npm run build`
39
+ refuses to run while `VITE_HARDCODED_TOKEN` is set, so a personal token cannot
40
+ be compiled into `dist/` (a built app authenticates from the cookie).
39
41
  3. Follow the `_readme` at the top of `src/Module/Sample/Contacts.json`: add an
40
42
  `apiConfig`, bind `LOAD_SCREEN_DATA`, and swap the `CUS_SAMPLE_*` stand-ins for
41
43
  `DELETE_ASYNC` / `UPDATE_ASYNC` / `FORMSUBMIT_CREATE_ASYNC`.
@@ -45,9 +47,9 @@ the `KENDO_UI_LICENSE` environment variable in CI. Then run `npm install`, or
45
47
  | Script | What it does |
46
48
  |---|---|
47
49
  | `npm run dev` | Vite dev server on port 5173 |
48
- | `npm run build` | `tsc` then `vite build` |
50
+ | `npm run build` | `tsc` then `vite build` — refused while `VITE_HARDCODED_TOKEN` is set |
49
51
  | `npm run preview` | Serve the built bundle |
50
- | `npm run lint` | ESLint over `src/` |
52
+ | `npm run lint` | ESLint over `src/` |__TEST_SCRIPT_ROW__
51
53
 
52
54
  ## Layout
53
55