@mmstack/translate-tools 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Miha J. Mulec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # @mmstack/translate-tools
2
+
3
+ Build-time tooling for [`@mmstack/translate`](https://www.npmjs.com/package/@mmstack/translate): export your TypeScript-authored
4
+ namespaces to JSON for translators / TMS platforms, then import the translations back as generated
5
+ `createTranslation` modules.
6
+
7
+ Authoring stays TypeScript-first, you keep writing `createNamespace({ ... })`. This tool just round-trips the **translations** through plain JSON files and writes the per-locale TypeScript for you, wiring each new locale into its `registerNamespace` registry. Allowing for easier co-working with translation teams or sharing between projects.
8
+
9
+ It's a standalone CLI (+ a programmatic API), works in an Nx monorepo or a plain `ng new` app.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -D @mmstack/translate-tools
15
+ # or: pnpm add -D @mmstack/translate-tools
16
+ ```
17
+
18
+ ## Compatibility
19
+
20
+ This package is **versioned independently** (starting at `1.x`) rather than tracking Angular like the
21
+ other `@mmstack/*` libraries. it's a Node tool that knows mmstack's _authoring patterns_, not
22
+ Angular. It **never imports `@mmstack/translate`**; it statically reads your `createNamespace` /
23
+ `registerNamespace` / `createTranslation` source, so a single version works across all current
24
+ `@mmstack/translate` releases (v19+). A version-compatibility table will be added here only if the
25
+ authoring API ever diverges.
26
+
27
+ ## The workflow
28
+
29
+ ```bash
30
+ # 1. Hand off source strings to translators / your TMS
31
+ npx mmtranslate export --src "src/**/*.ts" --out i18n
32
+
33
+ # 2. ...they translate i18n/<namespace>.<locale>.json...
34
+
35
+ # 3. Bring the translations back as generated TypeScript
36
+ npx mmtranslate import --src "src/**/*.ts" --in i18n
37
+ ```
38
+
39
+ ## Commands
40
+
41
+ ### `export`
42
+
43
+ Writes one **nested** JSON file per namespace per locale `<namespace>.<locale>.json` for the
44
+ source locale and every registered target locale. Keys mirror your `createNamespace` tree; ICU
45
+ messages are preserved verbatim.
46
+
47
+ ```bash
48
+ npx mmtranslate export --src "src/**/*.ts" --out i18n --source-locale en
49
+ ```
50
+
51
+ ```jsonc
52
+ // i18n/quote.en.json
53
+ {
54
+ "title": "Quotes",
55
+ "detail": { "authorLabel": "Author" },
56
+ }
57
+ ```
58
+
59
+ It also writes a small hidden `.mmtranslate-meta.json` recording the source locale, so a later `import` knows which locale is the source even without `--source-locale`. It's not a translation file translators/TMS platforms can ignore it.
60
+
61
+ ### `import`
62
+
63
+ For each `<namespace>.<locale>.json` that isn't the source locale, it:
64
+
65
+ - **validates** every leaf is valid ICU, uses the **same placeholders** as the source (a dropped or
66
+ renamed `{name}` is reported), **covers every source key** (a missing key is reported), and
67
+ **contains no unknown keys** (an extra key would generate TypeScript that doesn't compile —
68
+ `createTranslation` is typed to the source shape). A file with any issue is rejected so nothing
69
+ "malformed" is written — always **per file**, so one bad file never blocks the rest of the run;
70
+ - for a **new** locale, generates `<namespace>.<locale>.ts` (a `createTranslation` call) next to the
71
+ source namespace and inserts its loader into the matching `registerNamespace(...)` call. If a file
72
+ already exists at that path the locale is **rejected** unless you pass `--force`;
73
+ - for an **existing** locale, updates that module's translation in place.
74
+
75
+ `.json` files the run doesn't recognize — a typo'd namespace, a stray-dot name — are reported as
76
+ **skipped** with a reason, so a mis-named file can't silently vanish from a run.
77
+
78
+ `import` reads the source locale from the sidecar `export` wrote, so `--source-locale` only needs to
79
+ be repeated if you're importing files that weren't produced by this tool.
80
+
81
+ The source `createNamespace` is never regenerated.
82
+
83
+ ```bash
84
+ npx mmtranslate import --src "src/**/*.ts" --in i18n --source-locale en
85
+ ```
86
+
87
+ ### `generate-manifest`
88
+
89
+ Writes a config listing the discovered namespaces, their registry files, and locales. It's a starting point you can hand-edit if you'd rather pin discovery than rely on the automatic glob, or if that's not grabbing things correctly for some reason.
90
+
91
+ ```bash
92
+ npx mmtranslate generate-manifest --src "src/**/*.ts" --out mmtranslate.config.ts
93
+ ```
94
+
95
+ ### Options
96
+
97
+ | Flag | Applies to | Default | Meaning |
98
+ | -------------------------- | -------------------------- | ------------------------------------------- | ---------------------------------------------------------------------- |
99
+ | `--src <glob>` | all (repeatable) | `src/**/*.ts` | Source files to scan. |
100
+ | `--out <dir\|file>` | export / generate-manifest | `translations` / `mmtranslate.config.ts` | Output dir (export) or manifest file. |
101
+ | `--in <dir>` | import | `translations` | Directory of translated JSON. |
102
+ | `--source-locale <locale>` | all | `en` (import: the value recorded at export) | Label for the default/source translation (your app's `defaultLocale`). |
103
+ | `--force` | import | off | Overwrite an existing file when adding a new locale. |
104
+
105
+ ## How discovery works
106
+
107
+ The tool statically reads your `registerNamespace(default, { locale: loader })` calls, these are
108
+ the per-namespace registries and resolves each loader to the `createNamespace`,
109
+ `createMergedNamespace`, or `createTranslation` call it points at, lifting the translation object
110
+ directly from source. No code runs.
111
+
112
+ - **`withParams(...)`** is handled transparently: it's exported as its full ICU string, and target
113
+ locales are emitted as plain strings (they never need to repeat the wrapper).
114
+ - **Merged namespaces** (`common.createMergedNamespace('quote', ...)`) export only their own keys —
115
+ the shared/common namespace is its own file, with no duplication.
116
+ - **`registerRemoteNamespace`** or any imperative additions to translations are skipped.
117
+
118
+ ## Programmatic API
119
+
120
+ The engine is exported too, if you'd rather script it:
121
+
122
+ ```ts
123
+ import { Project } from 'ts-morph';
124
+ import {
125
+ discoverFromProject,
126
+ planExport,
127
+ runImport,
128
+ } from '@mmstack/translate-tools';
129
+ ```
130
+
131
+ `runExport` / `runImport` / `runGenerateManifest` (fs-performing), plus the building blocks:
132
+ `discoverFromProject`, `planExport`, `validateImport`, `applyImport`, and the lower-level
133
+ `lift` / `codegen` / `registry` / `nested` / `icu` helpers.
134
+
135
+ ## Supported source shapes
136
+
137
+ For a namespace to be discovered and round-tripped:
138
+
139
+ - Loaders use a static dynamic `import('./path')`. Every form `@mmstack/translate` accepts at
140
+ runtime is supported: `.then((m) => m.quote.translation)`, `.then((m) => m.default)`, and the
141
+ `() => import('./path')` shorthand (which auto-resolves the module's `default` or `translation`
142
+ export). Async/`await` or computed-access loaders are skipped with a warning.
143
+ - Translation values are string literals, template literals (no substitutions),
144
+ `withParams('literal')`, or nested objects. Dynamic values (a variable, a concatenation) are
145
+ rejected with a clear error — inline a literal so the tool can round-trip it.
146
+ - A namespace name contains no `.` (it's the first segment of the export file name).
package/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/cli.mjs ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { a as runGenerateManifest, i as runExport, o as runImport } from "./commands-yoWN-Tne.mjs";
3
+ import * as path from "node:path";
4
+ //#region src/lib/args.ts
5
+ /** Minimal argv option readers shared by the CLI. Both guard against a missing value so a typo
6
+ * (`--out --in i18n`) throws instead of silently swallowing the next flag as the option's value. */
7
+ /** Read a `--name value` option; throws if the value is missing or is itself a flag. Returns
8
+ * undefined when the option is absent. */
9
+ function flag(args, name) {
10
+ const i = args.indexOf(`--${name}`);
11
+ if (i < 0) return void 0;
12
+ const value = args[i + 1];
13
+ if (value === void 0 || value.startsWith("--")) throw new Error(`Missing value for --${name}`);
14
+ return value;
15
+ }
16
+ /** Read a repeatable `--name value` option into an array; throws on any missing value. */
17
+ function multiFlag(args, name) {
18
+ const out = [];
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i] !== `--${name}`) continue;
21
+ const value = args[i + 1];
22
+ if (value === void 0 || value.startsWith("--")) throw new Error(`Missing value for --${name}`);
23
+ out.push(value);
24
+ i += 1;
25
+ }
26
+ return out;
27
+ }
28
+ //#endregion
29
+ //#region src/cli.ts
30
+ const USAGE = `Usage: mmtranslate <command> [options]
31
+
32
+ Commands:
33
+ export Write nested JSON per namespace per locale (source + targets).
34
+ import Read translated JSON back into TypeScript createTranslation files,
35
+ registering any new locales.
36
+ generate-manifest Write a config listing the discovered namespaces/registries.
37
+
38
+ Options:
39
+ --src <glob> Source glob to scan (repeatable). Default: src/**/*.ts
40
+ --out <dir|file> Output dir (export) or manifest file (generate-manifest).
41
+ --in <dir> Input dir of translated JSON (import).
42
+ --source-locale <l> Locale label for the default/source translation. Default: en
43
+ (import falls back to the value recorded at export time).
44
+ --force import: overwrite an existing file when adding a new locale.
45
+ `;
46
+ function main() {
47
+ const [, , command, ...rest] = process.argv;
48
+ const cwd = process.cwd();
49
+ const srcGlobsArg = multiFlag(rest, "src");
50
+ const srcGlobs = srcGlobsArg.length ? srcGlobsArg : ["src/**/*.ts"];
51
+ const sourceLocale = flag(rest, "source-locale");
52
+ if (command === "export") {
53
+ const outDir = path.resolve(cwd, flag(rest, "out") ?? "translations");
54
+ const files = runExport({
55
+ cwd,
56
+ srcGlobs,
57
+ outDir,
58
+ sourceLocale
59
+ });
60
+ console.log(`Exported ${files.length} file(s) to ${outDir}`);
61
+ return;
62
+ }
63
+ if (command === "import") {
64
+ const report = runImport({
65
+ cwd,
66
+ srcGlobs,
67
+ inDir: path.resolve(cwd, flag(rest, "in") ?? "translations"),
68
+ sourceLocale,
69
+ force: rest.includes("--force")
70
+ });
71
+ for (const { file, reason } of report.skipped) console.warn(`⚠ skipped ${file}: ${reason}`);
72
+ for (const { file, issues } of report.rejected) {
73
+ console.error(`✗ ${file}`);
74
+ for (const issue of issues) console.error(` ${issue.key}: ${issue.message}`);
75
+ }
76
+ console.log(`Imported ${report.applied} locale file(s).`);
77
+ if (report.rejected.length) process.exitCode = 1;
78
+ return;
79
+ }
80
+ if (command === "generate-manifest") {
81
+ const outFile = path.resolve(cwd, flag(rest, "out") ?? "mmtranslate.config.ts");
82
+ runGenerateManifest({
83
+ cwd,
84
+ srcGlobs,
85
+ outFile,
86
+ sourceLocale
87
+ });
88
+ console.log(`Wrote manifest to ${outFile}`);
89
+ return;
90
+ }
91
+ console.error(USAGE);
92
+ process.exitCode = command ? 1 : 0;
93
+ }
94
+ try {
95
+ main();
96
+ } catch (e) {
97
+ console.error(e instanceof Error ? e.message : String(e));
98
+ process.exitCode = 1;
99
+ }
100
+ //#endregion
101
+ export {};
102
+
103
+ //# sourceMappingURL=cli.mjs.map
package/cli.mjs.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../../../packages/translate-tools/src/lib/args.ts","../../../packages/translate-tools/src/cli.ts"],"sourcesContent":["/** Minimal argv option readers shared by the CLI. Both guard against a missing value so a typo\n * (`--out --in i18n`) throws instead of silently swallowing the next flag as the option's value. */\n\n/** Read a `--name value` option; throws if the value is missing or is itself a flag. Returns\n * undefined when the option is absent. */\nexport function flag(args: string[], name: string): string | undefined {\n const i = args.indexOf(`--${name}`);\n if (i < 0) return undefined;\n const value = args[i + 1];\n if (value === undefined || value.startsWith('--'))\n throw new Error(`Missing value for --${name}`);\n return value;\n}\n\n/** Read a repeatable `--name value` option into an array; throws on any missing value. */\nexport function multiFlag(args: string[], name: string): string[] {\n const out: string[] = [];\n for (let i = 0; i < args.length; i++) {\n if (args[i] !== `--${name}`) continue;\n const value = args[i + 1];\n if (value === undefined || value.startsWith('--'))\n throw new Error(`Missing value for --${name}`);\n out.push(value);\n i += 1;\n }\n return out;\n}\n","#!/usr/bin/env node\nimport * as path from 'node:path';\nimport { flag, multiFlag } from './lib/args';\nimport {\n runExport,\n runGenerateManifest,\n runImport,\n} from './lib/commands';\n\nconst USAGE = `Usage: mmtranslate <command> [options]\n\nCommands:\n export Write nested JSON per namespace per locale (source + targets).\n import Read translated JSON back into TypeScript createTranslation files,\n registering any new locales.\n generate-manifest Write a config listing the discovered namespaces/registries.\n\nOptions:\n --src <glob> Source glob to scan (repeatable). Default: src/**/*.ts\n --out <dir|file> Output dir (export) or manifest file (generate-manifest).\n --in <dir> Input dir of translated JSON (import).\n --source-locale <l> Locale label for the default/source translation. Default: en\n (import falls back to the value recorded at export time).\n --force import: overwrite an existing file when adding a new locale.\n`;\n\nfunction main(): void {\n const [, , command, ...rest] = process.argv;\n const cwd = process.cwd();\n const srcGlobsArg = multiFlag(rest, 'src');\n const srcGlobs = srcGlobsArg.length ? srcGlobsArg : ['src/**/*.ts'];\n const sourceLocale = flag(rest, 'source-locale');\n\n if (command === 'export') {\n const outDir = path.resolve(cwd, flag(rest, 'out') ?? 'translations');\n const files = runExport({ cwd, srcGlobs, outDir, sourceLocale });\n console.log(`Exported ${files.length} file(s) to ${outDir}`);\n return;\n }\n\n if (command === 'import') {\n const inDir = path.resolve(cwd, flag(rest, 'in') ?? 'translations');\n const force = rest.includes('--force');\n const report = runImport({ cwd, srcGlobs, inDir, sourceLocale, force });\n for (const { file, reason } of report.skipped)\n console.warn(`⚠ skipped ${file}: ${reason}`);\n for (const { file, issues } of report.rejected) {\n console.error(`✗ ${file}`);\n for (const issue of issues) console.error(` ${issue.key}: ${issue.message}`);\n }\n console.log(`Imported ${report.applied} locale file(s).`);\n if (report.rejected.length) process.exitCode = 1;\n return;\n }\n\n if (command === 'generate-manifest') {\n const outFile = path.resolve(cwd, flag(rest, 'out') ?? 'mmtranslate.config.ts');\n runGenerateManifest({ cwd, srcGlobs, outFile, sourceLocale });\n console.log(`Wrote manifest to ${outFile}`);\n return;\n }\n\n console.error(USAGE);\n process.exitCode = command ? 1 : 0;\n}\n\ntry {\n main();\n} catch (e) {\n console.error(e instanceof Error ? e.message : String(e));\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;;;AAKA,SAAgB,KAAK,MAAgB,MAAkC;CACrE,MAAM,IAAI,KAAK,QAAQ,KAAK,MAAM;CAClC,IAAI,IAAI,GAAG,OAAO,KAAA;CAClB,MAAM,QAAQ,KAAK,IAAI;CACvB,IAAI,UAAU,KAAA,KAAa,MAAM,WAAW,IAAI,GAC9C,MAAM,IAAI,MAAM,uBAAuB,MAAM;CAC/C,OAAO;AACT;;AAGA,SAAgB,UAAU,MAAgB,MAAwB;CAChE,MAAM,MAAgB,CAAC;CACvB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,IAAI,KAAK,OAAO,KAAK,QAAQ;EAC7B,MAAM,QAAQ,KAAK,IAAI;EACvB,IAAI,UAAU,KAAA,KAAa,MAAM,WAAW,IAAI,GAC9C,MAAM,IAAI,MAAM,uBAAuB,MAAM;EAC/C,IAAI,KAAK,KAAK;EACd,KAAK;CACP;CACA,OAAO;AACT;;;ACjBA,MAAM,QAAQ;;;;;;;;;;;;;;;;AAiBd,SAAS,OAAa;CACpB,MAAM,KAAK,SAAS,GAAG,QAAQ,QAAQ;CACvC,MAAM,MAAM,QAAQ,IAAI;CACxB,MAAM,cAAc,UAAU,MAAM,KAAK;CACzC,MAAM,WAAW,YAAY,SAAS,cAAc,CAAC,aAAa;CAClE,MAAM,eAAe,KAAK,MAAM,eAAe;CAE/C,IAAI,YAAY,UAAU;EACxB,MAAM,SAAS,KAAK,QAAQ,KAAK,KAAK,MAAM,KAAK,KAAK,cAAc;EACpE,MAAM,QAAQ,UAAU;GAAE;GAAK;GAAU;GAAQ;EAAa,CAAC;EAC/D,QAAQ,IAAI,YAAY,MAAM,OAAO,cAAc,QAAQ;EAC3D;CACF;CAEA,IAAI,YAAY,UAAU;EAGxB,MAAM,SAAS,UAAU;GAAE;GAAK;GAAU,OAF5B,KAAK,QAAQ,KAAK,KAAK,MAAM,IAAI,KAAK,cAEN;GAAG;GAAc,OADjD,KAAK,SAAS,SACuC;EAAE,CAAC;EACtE,KAAK,MAAM,EAAE,MAAM,YAAY,OAAO,SACpC,QAAQ,KAAK,aAAa,KAAK,IAAI,QAAQ;EAC7C,KAAK,MAAM,EAAE,MAAM,YAAY,OAAO,UAAU;GAC9C,QAAQ,MAAM,KAAK,MAAM;GACzB,KAAK,MAAM,SAAS,QAAQ,QAAQ,MAAM,MAAM,MAAM,IAAI,IAAI,MAAM,SAAS;EAC/E;EACA,QAAQ,IAAI,YAAY,OAAO,QAAQ,iBAAiB;EACxD,IAAI,OAAO,SAAS,QAAQ,QAAQ,WAAW;EAC/C;CACF;CAEA,IAAI,YAAY,qBAAqB;EACnC,MAAM,UAAU,KAAK,QAAQ,KAAK,KAAK,MAAM,KAAK,KAAK,uBAAuB;EAC9E,oBAAoB;GAAE;GAAK;GAAU;GAAS;EAAa,CAAC;EAC5D,QAAQ,IAAI,qBAAqB,SAAS;EAC1C;CACF;CAEA,QAAQ,MAAM,KAAK;CACnB,QAAQ,WAAW,UAAU,IAAI;AACnC;AAEA,IAAI;CACF,KAAK;AACP,SAAS,GAAG;CACV,QAAQ,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;CACxD,QAAQ,WAAW;AACrB"}