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,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
|
|