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.
- package/README.md +266 -2
- package/bin/create-procode.mjs +9 -0
- package/package.json +30 -4
- package/src/args.mjs +145 -0
- package/src/cli.mjs +514 -0
- package/src/deps.mjs +208 -0
- package/src/log.mjs +23 -0
- package/src/multiLanguage.mjs +409 -0
- package/src/prompt.mjs +60 -0
- package/src/scaffold.mjs +104 -0
- package/templates/base/README.md.tmpl +185 -0
- package/templates/base/_dotenv.local.example.tmpl +8 -0
- package/templates/base/_dotenv.tmpl +41 -0
- package/templates/base/_doteslintrc.cjs.tmpl +125 -0
- package/templates/base/_dotgitignore.tmpl +32 -0
- package/templates/base/_dotnvmrc.tmpl +1 -0
- package/templates/base/_dotvscode/extensions.json +3 -0
- package/templates/base/index.html.tmpl +18 -0
- package/templates/base/package.json.tmpl +15 -0
- package/templates/base/public/favicon.svg +4 -0
- package/templates/base/src/AdapterProvider/AdapterFactory.ts.tmpl +21 -0
- package/templates/base/src/AdapterProvider/SampleContacts.ts.tmpl +136 -0
- package/templates/base/src/CustomAction.ts.tmpl +47 -0
- package/templates/base/src/CustomAuthorizationProvider.ts.tmpl +77 -0
- package/templates/base/src/Factory/Actions/myAction.ts.tmpl +51 -0
- package/templates/base/src/Factory/Actions/sampleContacts.ts.tmpl +244 -0
- package/templates/base/src/Factory/Actions/setField.ts.tmpl +20 -0
- package/templates/base/src/Factory/CustomRuleEffectFactory.ts.tmpl +30 -0
- package/templates/base/src/Factory/CustomValidationFactory.tsx.tmpl +24 -0
- package/templates/base/src/Factory/CustomWidgetFactory.tsx.tmpl +43 -0
- package/templates/base/src/Factory/RuleEffects/readOnlyEffect.ts.tmpl +27 -0
- package/templates/base/src/Factory/Validations/myValidation.ts.tmpl +52 -0
- package/templates/base/src/Factory/Widgets/MyWidget/index.tsx.tmpl +101 -0
- package/templates/base/src/Factory/Widgets/Notification/index.tsx.tmpl +53 -0
- package/templates/base/src/Factory/Widgets/Notification/notifiFunc.ts.tmpl +9 -0
- package/templates/base/src/Factory/Widgets/SectionNav/index.tsx +193 -0
- package/templates/base/src/Factory/Widgets/SectionNav/style.scss +139 -0
- package/templates/base/src/Module/Sample/Contacts.json.tmpl +965 -0
- package/templates/base/src/Module/index.ts.tmpl +26 -0
- package/templates/base/src/Screens/index.tsx.tmpl +45 -0
- package/templates/base/src/Utils/errorMessages.ts.tmpl +35 -0
- package/templates/base/src/Utils/fieldErrorMapper.ts.tmpl +46 -0
- package/templates/base/src/Utils/logger.ts.tmpl +21 -0
- package/templates/base/src/Utils/responseFormatter.ts.tmpl +39 -0
- package/templates/base/src/Utils/storages.ts.tmpl +35 -0
- package/templates/base/src/assets/styles/index.scss.tmpl +149 -0
- package/templates/base/src/constant.ts.tmpl +53 -0
- package/templates/base/src/icons.ts.tmpl +17 -0
- package/templates/base/src/localizationResolver.ts.tmpl +48 -0
- package/templates/base/src/main.tsx.tmpl +136 -0
- package/templates/base/src/routes.ts.tmpl +20 -0
- package/templates/base/src/vite-env.d.ts.tmpl +20 -0
- package/templates/base/tsconfig.json.tmpl +30 -0
- package/templates/base/tsconfig.node.json.tmpl +11 -0
- package/templates/base/vite.config.ts.tmpl +90 -0
- 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,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activate": "Activer",
|
|
3
|
+
"active": "Actif",
|
|
4
|
+
"addContact": "Ajouter un contact",
|
|
5
|
+
"anythingTheTeamShouldKnow": "Tout ce que l'équipe doit savoir",
|
|
6
|
+
"asiaPacific": "Asie-Pacifique",
|
|
7
|
+
"cancel": "Annuler",
|
|
8
|
+
"changeTheStatusOfTheSelectedContacts": "Modifier le statut des contacts sélectionnés ?",
|
|
9
|
+
"city": "Ville",
|
|
10
|
+
"confirmation": "Confirmation",
|
|
11
|
+
"contactDetails": "Coordonnées du contact",
|
|
12
|
+
"contacts": "Contacts",
|
|
13
|
+
"contactSaved": "Contact enregistré",
|
|
14
|
+
"countContactSWillBeUpdated": "{count} contact{s} vont être mis à jour.",
|
|
15
|
+
"deactivate": "Désactiver",
|
|
16
|
+
"delete": "Supprimer",
|
|
17
|
+
"deleteTheSelectedContacts": "Supprimer les contacts sélectionnés ?",
|
|
18
|
+
"department": "Service",
|
|
19
|
+
"departments": "Services",
|
|
20
|
+
"eGCtA1b2": "ex. CT_A1B2",
|
|
21
|
+
"email": "E-mail",
|
|
22
|
+
"engineering": "Ingénierie",
|
|
23
|
+
"europe": "Europe",
|
|
24
|
+
"finance": "Finance",
|
|
25
|
+
"fullName": "Nom complet",
|
|
26
|
+
"joined": "Arrivée",
|
|
27
|
+
"keepThem": "Les conserver",
|
|
28
|
+
"keepTheNameUnder40Characters": "Le nom doit faire moins de 40 caractères",
|
|
29
|
+
"name": "Nom",
|
|
30
|
+
"nameCompanyCom": "nom@entreprise.com",
|
|
31
|
+
"no": "Non",
|
|
32
|
+
"northAmerica": "Amérique du Nord",
|
|
33
|
+
"notes": "Notes",
|
|
34
|
+
"notesAreLimitedTo200Characters": "Les notes sont limitées à 200 caractères",
|
|
35
|
+
"onlyAskedForTheSalesDepartment": "Demandé uniquement pour le service Ventes",
|
|
36
|
+
"pleaseChooseADepartment": "Veuillez choisir un service",
|
|
37
|
+
"pleaseEnterAName": "Veuillez saisir un nom",
|
|
38
|
+
"pleaseEnterAnEmailAddress": "Veuillez saisir une adresse e-mail",
|
|
39
|
+
"referenceCode": "Code de référence",
|
|
40
|
+
"sales": "Ventes",
|
|
41
|
+
"salesRegion": "Région commerciale",
|
|
42
|
+
"save": "Enregistrer",
|
|
43
|
+
"saved": "Enregistré",
|
|
44
|
+
"selectADepartment": "Sélectionnez un service",
|
|
45
|
+
"selectARegion": "Sélectionnez une région",
|
|
46
|
+
"selectRowsForBulkActionsClickA": "Sélectionnez des lignes pour les actions groupées, cliquez sur un nom pour le modifier, ou ajoutez un contact. Tout le comportement de cette page est déclaré dans src/Module/Sample/Contacts.json.",
|
|
47
|
+
"status": "Statut",
|
|
48
|
+
"support": "Support",
|
|
49
|
+
"thatDoesNotLookLikeAnEmail": "Cette adresse e-mail ne semble pas valide",
|
|
50
|
+
"totalContacts": "Total des contacts",
|
|
51
|
+
"useCapitalsDigitsAndUnderscoreOnly": "Utilisez uniquement des majuscules, des chiffres et le trait de soulignement",
|
|
52
|
+
"yesChangeIt": "Oui, modifier",
|
|
53
|
+
"youAreAboutToDeleteCountContact": "Vous allez supprimer {count} contact{s}. Cette action est irréversible.",
|
|
54
|
+
"yyyyMmDd": "aaaa-mm-jj"
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activate": "सक्रिय करें",
|
|
3
|
+
"active": "सक्रिय",
|
|
4
|
+
"addContact": "संपर्क जोड़ें",
|
|
5
|
+
"anythingTheTeamShouldKnow": "टीम को जो कुछ भी जानना चाहिए",
|
|
6
|
+
"asiaPacific": "एशिया-प्रशांत",
|
|
7
|
+
"cancel": "रद्द करें",
|
|
8
|
+
"changeTheStatusOfTheSelectedContacts": "चुने गए संपर्कों की स्थिति बदलें?",
|
|
9
|
+
"city": "शहर",
|
|
10
|
+
"confirmation": "पुष्टि",
|
|
11
|
+
"contactDetails": "संपर्क विवरण",
|
|
12
|
+
"contacts": "संपर्क",
|
|
13
|
+
"contactSaved": "संपर्क सहेजा गया",
|
|
14
|
+
"countContactSWillBeUpdated": "{count} संपर्क अपडेट किए जाएँगे।",
|
|
15
|
+
"deactivate": "निष्क्रिय करें",
|
|
16
|
+
"delete": "हटाएँ",
|
|
17
|
+
"deleteTheSelectedContacts": "चुने गए संपर्क हटाएँ?",
|
|
18
|
+
"department": "विभाग",
|
|
19
|
+
"departments": "विभाग",
|
|
20
|
+
"eGCtA1b2": "उदा. CT_A1B2",
|
|
21
|
+
"email": "ईमेल",
|
|
22
|
+
"engineering": "इंजीनियरिंग",
|
|
23
|
+
"europe": "यूरोप",
|
|
24
|
+
"finance": "वित्त",
|
|
25
|
+
"fullName": "पूरा नाम",
|
|
26
|
+
"joined": "शामिल होने की तिथि",
|
|
27
|
+
"keepThem": "रहने दें",
|
|
28
|
+
"keepTheNameUnder40Characters": "नाम 40 अक्षरों से कम रखें",
|
|
29
|
+
"name": "नाम",
|
|
30
|
+
"nameCompanyCom": "name@company.com",
|
|
31
|
+
"no": "नहीं",
|
|
32
|
+
"northAmerica": "उत्तरी अमेरिका",
|
|
33
|
+
"notes": "टिप्पणियाँ",
|
|
34
|
+
"notesAreLimitedTo200Characters": "टिप्पणियाँ 200 अक्षरों तक सीमित हैं",
|
|
35
|
+
"onlyAskedForTheSalesDepartment": "केवल बिक्री विभाग के लिए पूछा जाता है",
|
|
36
|
+
"pleaseChooseADepartment": "कृपया एक विभाग चुनें",
|
|
37
|
+
"pleaseEnterAName": "कृपया एक नाम दर्ज करें",
|
|
38
|
+
"pleaseEnterAnEmailAddress": "कृपया एक ईमेल पता दर्ज करें",
|
|
39
|
+
"referenceCode": "संदर्भ कोड",
|
|
40
|
+
"sales": "बिक्री",
|
|
41
|
+
"salesRegion": "बिक्री क्षेत्र",
|
|
42
|
+
"save": "सहेजें",
|
|
43
|
+
"saved": "सहेजा गया",
|
|
44
|
+
"selectADepartment": "एक विभाग चुनें",
|
|
45
|
+
"selectARegion": "एक क्षेत्र चुनें",
|
|
46
|
+
"selectRowsForBulkActionsClickA": "सामूहिक कार्रवाई के लिए पंक्तियाँ चुनें, संपादित करने के लिए किसी नाम पर क्लिक करें, या नया संपर्क जोड़ें। इस पृष्ठ का हर व्यवहार src/Module/Sample/Contacts.json में घोषित है।",
|
|
47
|
+
"status": "स्थिति",
|
|
48
|
+
"support": "सहायता",
|
|
49
|
+
"thatDoesNotLookLikeAnEmail": "यह ईमेल पता सही नहीं लगता",
|
|
50
|
+
"totalContacts": "कुल संपर्क",
|
|
51
|
+
"useCapitalsDigitsAndUnderscoreOnly": "केवल बड़े अक्षर, अंक और अंडरस्कोर का उपयोग करें",
|
|
52
|
+
"yesChangeIt": "हाँ, बदलें",
|
|
53
|
+
"youAreAboutToDeleteCountContact": "आप {count} संपर्क हटाने वाले हैं। यह पूर्ववत नहीं किया जा सकता।",
|
|
54
|
+
"yyyyMmDd": "yyyy-mm-dd"
|
|
55
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wiring guards for every screen under src/Module.
|
|
5
|
+
*
|
|
6
|
+
* Each failure this file catches is SILENT at runtime: a slug with no branch in
|
|
7
|
+
* getSchemaBySlugService is "page not found" with nothing in the console, and two
|
|
8
|
+
* screens sharing a screenId share one adapter. Add a screen JSON and these run
|
|
9
|
+
* against it with no edit here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// constant.ts reads the auth cookie when it loads, and the registries reach it
|
|
13
|
+
// through their imports. Stub the two browser globals before anything loads —
|
|
14
|
+
// hence the dynamic import in beforeAll rather than a static one (that would be
|
|
15
|
+
// hoisted above these lines; top-level await is unavailable at the es2020 target
|
|
16
|
+
// vite.config.ts keeps for Kendo).
|
|
17
|
+
(globalThis as any).document ??= { cookie: "" };
|
|
18
|
+
(globalThis as any).window ??= { location: { search: "" } };
|
|
19
|
+
|
|
20
|
+
let getSchemaBySlugService: (slug: string, appCode: string) => unknown;
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
({ getSchemaBySlugService } = await import("./index"));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Every JSON under src/Module that is a screen (has a rootScreenElement). */
|
|
26
|
+
const screens = Object.entries(
|
|
27
|
+
import.meta.glob<Record<string, any>>("./**/*.json", { eager: true, import: "default" }),
|
|
28
|
+
).filter(([, schema]) => schema && typeof schema === "object" && "rootScreenElement" in schema);
|
|
29
|
+
|
|
30
|
+
describe("screens under src/Module", () => {
|
|
31
|
+
it("each declares a slug and a screenId", () => {
|
|
32
|
+
const missing = screens
|
|
33
|
+
.filter(([, schema]) => !schema.slug || !schema.screenId)
|
|
34
|
+
.map(([file]) => file);
|
|
35
|
+
expect(missing).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("each slug is registered in getSchemaBySlugService (else the URL is page-not-found)", () => {
|
|
39
|
+
const unregistered = screens
|
|
40
|
+
.filter(([, schema]) => schema.slug && getSchemaBySlugService(schema.slug, "") !== schema)
|
|
41
|
+
.map(([file, schema]) => `${file} (slug "${schema.slug}")`);
|
|
42
|
+
expect(unregistered).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("no two screens share a screenId (they would share one adapter)", () => {
|
|
46
|
+
const ids = screens.map(([, schema]) => schema.screenId).filter(Boolean);
|
|
47
|
+
const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
48
|
+
expect(duplicates).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -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
|
+
});
|