@i18n-micro/cli 0.0.0-stage → 2.0.1

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/bin/i18n-micro.mjs +3 -0
  4. package/dist/chunks/check-CwSzATDI.mjs +175 -0
  5. package/dist/chunks/clean-Do9z6D5j.mjs +68 -0
  6. package/dist/chunks/clean-bQVjG_2M.mjs +1 -0
  7. package/dist/chunks/export-C2J-pagB.mjs +219 -0
  8. package/dist/chunks/export-usBaNIpp.mjs +1 -0
  9. package/dist/chunks/extract-CJ8UfrVJ.mjs +30 -0
  10. package/dist/chunks/format-6CtMgO-M.mjs +26 -0
  11. package/dist/chunks/http-whB7a5ye.mjs +100 -0
  12. package/dist/chunks/import-CnL8U6Za.mjs +99 -0
  13. package/dist/chunks/info-Lom_wHos.mjs +32 -0
  14. package/dist/chunks/json-BV_apCMu.mjs +439 -0
  15. package/dist/chunks/lint-DDGSQAXd.mjs +125 -0
  16. package/dist/chunks/nuxt-dKxG087V.mjs +366 -0
  17. package/dist/chunks/package-BejjFS6s.mjs +3 -0
  18. package/dist/chunks/project-DgWgr7pF.mjs +206 -0
  19. package/dist/chunks/protect-D1HIuxmJ.mjs +36 -0
  20. package/dist/chunks/pseudo-oXBaONgF.mjs +32 -0
  21. package/dist/chunks/rename-uJnBX86A.mjs +117 -0
  22. package/dist/chunks/runner-CsPPffV1.mjs +200 -0
  23. package/dist/chunks/search-Dj_Ynq_x.mjs +67 -0
  24. package/dist/chunks/source-Bx2lHSW7.mjs +744 -0
  25. package/dist/chunks/spec-DwveYr8X.mjs +507 -0
  26. package/dist/chunks/stats-BU-SmrVR.mjs +156 -0
  27. package/dist/chunks/sync-BY2EFEda.mjs +1 -0
  28. package/dist/chunks/sync-C0NVDabK.mjs +56 -0
  29. package/dist/chunks/sync-remote-y3ZATbF2.mjs +148 -0
  30. package/dist/chunks/text-to-i18n-DweavFLm.mjs +404 -0
  31. package/dist/chunks/translate-CmnmgAHX.mjs +543 -0
  32. package/dist/cli.mjs +4 -0
  33. package/dist/index.mjs +3 -0
  34. package/dist/src/cli.d.ts +1 -0
  35. package/dist/src/commands/check.d.ts +28 -0
  36. package/dist/src/commands/clean.d.ts +26 -0
  37. package/dist/src/commands/export.d.ts +10 -0
  38. package/dist/src/commands/extract.d.ts +6 -0
  39. package/dist/src/commands/format.d.ts +8 -0
  40. package/dist/src/commands/import.d.ts +8 -0
  41. package/dist/src/commands/info.d.ts +5 -0
  42. package/dist/src/commands/lint.d.ts +16 -0
  43. package/dist/src/commands/pseudo.d.ts +9 -0
  44. package/dist/src/commands/rename.d.ts +9 -0
  45. package/dist/src/commands/search.d.ts +20 -0
  46. package/dist/src/commands/stats.d.ts +20 -0
  47. package/dist/src/commands/sync-remote.d.ts +30 -0
  48. package/dist/src/commands/sync.d.ts +20 -0
  49. package/dist/src/commands/text-to-i18n.d.ts +10 -0
  50. package/dist/src/commands/translate.d.ts +14 -0
  51. package/dist/src/csv.d.ts +3 -0
  52. package/dist/src/errors.d.ts +22 -0
  53. package/dist/src/fs.d.ts +45 -0
  54. package/dist/src/glossary.d.ts +12 -0
  55. package/dist/src/help.d.ts +3 -0
  56. package/dist/src/http.d.ts +10 -0
  57. package/dist/src/index.d.ts +3 -0
  58. package/dist/src/json.d.ts +34 -0
  59. package/dist/src/lint-rules.d.ts +9 -0
  60. package/dist/src/log.d.ts +88 -0
  61. package/dist/src/nuxt.d.ts +66 -0
  62. package/dist/src/po.d.ts +14 -0
  63. package/dist/src/project.d.ts +64 -0
  64. package/dist/src/runner.d.ts +6 -0
  65. package/dist/src/source.d.ts +109 -0
  66. package/dist/src/spec.d.ts +57 -0
  67. package/dist/src/text-to-i18n.d.ts +46 -0
  68. package/dist/src/translate/ai.d.ts +2 -0
  69. package/dist/src/translate/drivers.d.ts +26 -0
  70. package/dist/src/translate/engine.d.ts +33 -0
  71. package/dist/src/translate/protect.d.ts +11 -0
  72. package/package.json +71 -3
@@ -0,0 +1,206 @@
1
+ import { S as validationError, a as flattenStrings, d as ownValue, f as pathConflict, i as flatten, l as isObject, m as sortKeys, o as getPath, p as setPath, t as deletePath, v as notFoundError, x as usageError } from "./json-BV_apCMu.mjs";
2
+ import { a as readJsonIfExists, f as stringifyJson, g as writeFilesAtomic, i as matchLineEnding, m as walkFiles, p as stringifyLike, t as loadProjectConfig, u as relativePath } from "./nuxt-dKxG087V.mjs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { dirname, join, resolve, sep } from "node:path";
5
+ import { deepMergeTranslationsRecursive } from "@i18n-micro/utils/deep-merge";
6
+ var merge = (target, source) => deepMergeTranslationsRecursive(target, source);
7
+ /** Scopes arrive from imported files and folder names, and become directories and object keys. */
8
+ function assertSafeScope(scope) {
9
+ const parts = scope.split("\\").join("/").split("/");
10
+ if (scope.startsWith("/") || parts.includes("..") || parts.includes("__proto__")) throw validationError(`Unsafe page scope: ${scope}`);
11
+ }
12
+ function readLayer(dir, code, withPages) {
13
+ const pagesDir = join(dir, "pages");
14
+ const fileName = `${code}.json`;
15
+ const pages = {};
16
+ const files = withPages ? walkFiles(pagesDir, (path) => path.endsWith(`${sep}${fileName}`)) : [];
17
+ for (const file of files) {
18
+ const scope = relativePath(pagesDir, dirname(file));
19
+ assertSafeScope(scope);
20
+ pages[scope] = readJsonIfExists(file);
21
+ }
22
+ return {
23
+ global: readJsonIfExists(join(dir, fileName)),
24
+ pages
25
+ };
26
+ }
27
+ function mergeLayers(layers) {
28
+ const out = {
29
+ global: {},
30
+ pages: {}
31
+ };
32
+ for (const layer of layers) {
33
+ out.global = merge(out.global, layer.global);
34
+ for (const [scope, tree] of Object.entries(layer.pages)) out.pages[scope] = merge(ownValue(out.pages, scope) ?? {}, tree);
35
+ }
36
+ return out;
37
+ }
38
+ /** The file's own order first: a save must not shuffle a file because a layer lists keys differently. */
39
+ var orderedLike = (tree, own) => [.../* @__PURE__ */ new Set([...Object.keys(own), ...Object.keys(tree)])].filter((key) => Object.hasOwn(tree, key));
40
+ /**
41
+ * What the project file has to contain so the merged view stays the same.
42
+ *
43
+ * Commands see every layer merged, the way the app does, but a layer's key does not belong in
44
+ * the app's file: copied there it would silently shadow the layer forever.
45
+ */
46
+ function ownPart(tree, inherited, own) {
47
+ const out = {};
48
+ for (const key of orderedLike(tree, own)) {
49
+ const value = tree[key];
50
+ const base = Object.hasOwn(inherited, key) ? inherited[key] : void 0;
51
+ const mine = Object.hasOwn(own, key) ? own[key] : void 0;
52
+ if (isObject(value) && isObject(base)) {
53
+ const nested = ownPart(value, base, isObject(mine) ? mine : {});
54
+ if (Object.keys(nested).length > 0 || mine !== void 0) out[key] = nested;
55
+ continue;
56
+ }
57
+ if (mine !== void 0 || JSON.stringify(value) !== JSON.stringify(base)) out[key] = value;
58
+ }
59
+ return out;
60
+ }
61
+ /** `de.json` or `pages/about/de.json`: the one place a scope turns into a path. */
62
+ var relativeFile = (code, scope, extension = "json") => scope === "" ? `${code}.${extension}` : `pages/${scope}/${code}.${extension}`;
63
+ var LocaleSet = class {
64
+ inherited;
65
+ own;
66
+ global;
67
+ pages;
68
+ modified = false;
69
+ constructor(merged, inherited, own) {
70
+ this.inherited = inherited;
71
+ this.own = own;
72
+ this.global = structuredClone(merged.global);
73
+ this.pages = structuredClone(merged.pages);
74
+ }
75
+ get pageScopes() {
76
+ return Object.keys(this.pages);
77
+ }
78
+ /** The global scope first, then every page. */
79
+ get scopes() {
80
+ return ["", ...this.pageScopes];
81
+ }
82
+ tree(scope) {
83
+ return scope === "" ? this.global : ownValue(this.pages, scope) ?? {};
84
+ }
85
+ /** The project's file as it was read. */
86
+ ownTree(scope) {
87
+ return scope === "" ? this.own.global : ownValue(this.own.pages, scope) ?? {};
88
+ }
89
+ inheritedTree(scope) {
90
+ return scope === "" ? this.inherited.global : ownValue(this.inherited.pages, scope) ?? {};
91
+ }
92
+ /** The project's file as it stands now: edits in, layers out. */
93
+ fileTree(scope) {
94
+ return ownPart(this.tree(scope), this.inheritedTree(scope), this.ownTree(scope));
95
+ }
96
+ replaceTree(scope, tree) {
97
+ if (scope === "") this.global = tree;
98
+ else this.pages[scope] = tree;
99
+ this.modified = true;
100
+ }
101
+ strings(scope = "") {
102
+ return flattenStrings(this.tree(scope));
103
+ }
104
+ leaves(scope = "") {
105
+ return flatten(this.tree(scope));
106
+ }
107
+ get(key, scope = "") {
108
+ return getPath(this.tree(scope), key);
109
+ }
110
+ /** What the app reads on `scope`: the page's value, then the global one. */
111
+ resolve(key, scope) {
112
+ return this.get(key, scope) ?? this.get(key);
113
+ }
114
+ /** Writes `value`, or returns the key that blocks it: a write never destroys another value. */
115
+ set(key, value, scope = "") {
116
+ assertSafeScope(scope);
117
+ if (key.split(".").includes("__proto__")) return key;
118
+ const conflict = pathConflict(this.tree(scope), key);
119
+ if (conflict) return conflict;
120
+ if (scope !== "" && !Object.hasOwn(this.pages, scope)) this.pages[scope] = {};
121
+ setPath(this.tree(scope), key, value);
122
+ this.modified = true;
123
+ return null;
124
+ }
125
+ delete(key, scope = "") {
126
+ if (deletePath(this.tree(scope), key)) this.modified = true;
127
+ }
128
+ };
129
+ var Project = class Project {
130
+ config;
131
+ locales;
132
+ constructor(config, locales) {
133
+ this.config = config;
134
+ this.locales = locales;
135
+ }
136
+ static async load(options) {
137
+ const config = await loadProjectConfig(resolve(options.cwd ?? "."));
138
+ const requested = options.translationDir ? resolve(config.rootDir, options.translationDir) : config.translationDir;
139
+ const own = requested === config.translationDir;
140
+ const resolved = {
141
+ ...config,
142
+ translationDir: requested,
143
+ translationDirs: own ? config.translationDirs : [requested],
144
+ additionalDirs: own ? config.additionalDirs : []
145
+ };
146
+ const locales = /* @__PURE__ */ new Map();
147
+ for (const { code } of resolved.locales) {
148
+ const layers = resolved.translationDirs.map((dir) => readLayer(dir, code, !resolved.disablePageLocales && !resolved.additionalDirs.includes(dir)));
149
+ locales.set(code, new LocaleSet(mergeLayers(layers), mergeLayers(layers.slice(0, -1)), layers.at(-1)));
150
+ }
151
+ return new Project(resolved, locales);
152
+ }
153
+ get codes() {
154
+ return this.config.locales.map((locale) => locale.code);
155
+ }
156
+ /** Locales to fill in from the default one. */
157
+ get targetCodes() {
158
+ return this.codes.filter((code) => code !== this.config.defaultLocale);
159
+ }
160
+ /** The comma list of a `--locale` flag, or every target locale. */
161
+ selectLocales(list) {
162
+ if (list.length === 0) return this.targetCodes;
163
+ const unknown = list.filter((code) => !this.targetCodes.includes(code));
164
+ if (unknown.length > 0) throw usageError(`Not a target locale: ${unknown.join(", ")}`, [`Target locales: ${this.targetCodes.join(", ")}`]);
165
+ return list;
166
+ }
167
+ locale(code) {
168
+ const set = this.locales.get(code);
169
+ if (!set) throw notFoundError(`Locale ${code} not found`, ["Check configured locales in nuxt.config i18n.locales."], { Locale: code });
170
+ return set;
171
+ }
172
+ get reference() {
173
+ return this.locale(this.config.defaultLocale);
174
+ }
175
+ filesOf(code, set, options) {
176
+ const scopes = [.../* @__PURE__ */ new Set([...set.scopes, ...Object.keys(set.own.pages)])];
177
+ const files = [];
178
+ for (const scope of scopes) {
179
+ const path = join(this.config.translationDir, ...relativeFile(code, scope).split("/"));
180
+ const own = set.ownTree(scope);
181
+ const part = set.fileTree(scope);
182
+ const content = options.sort ? sortKeys(part) : part;
183
+ const previous = existsSync(path) ? readFileSync(path, "utf8") : void 0;
184
+ if (previous === void 0 && Object.keys(content).length === 0) continue;
185
+ if (!options.rewrite && previous !== void 0 && JSON.stringify(content) === JSON.stringify(own)) continue;
186
+ const text = options.indent ? matchLineEnding(previous, stringifyJson(content, options.indent)) : stringifyLike(previous, content);
187
+ if (text !== previous) files.push([path, text]);
188
+ }
189
+ return files;
190
+ }
191
+ /** The writes a save would make, for a caller that has other files to change in the same step. */
192
+ pendingFiles(options = {}) {
193
+ return this.codes.flatMap((code) => {
194
+ const set = this.locale(code);
195
+ return set.modified ? this.filesOf(code, set, options) : [];
196
+ });
197
+ }
198
+ /** Write modified locales in one step, see `writeFilesAtomic`. Returns the paths written. */
199
+ save(options = {}) {
200
+ const files = this.pendingFiles(options);
201
+ writeFilesAtomic(files);
202
+ for (const code of this.codes) this.locale(code).modified = false;
203
+ return files.map(([path]) => path);
204
+ }
205
+ };
206
+ export { relativeFile as n, Project as t };
@@ -0,0 +1,36 @@
1
+ // src/translate/protect.ts
2
+ var PLACEHOLDER = /%?\{[^{}]+\}|\{\{[^{}]+\}\}/g;
3
+ /** Replaces placeholders with XML tags, the one thing every service with markup support leaves alone. */
4
+ function protect(text) {
5
+ const tokens = [];
6
+ return {
7
+ masked: text.replace(PLACEHOLDER, (match) => {
8
+ tokens.push(match);
9
+ return `<v${tokens.length - 1}/>`;
10
+ }),
11
+ tokens
12
+ };
13
+ }
14
+ function restore(masked, tokens) {
15
+ let out = masked;
16
+ for (const [index, token] of tokens.entries()) {
17
+ const selfClosed = new RegExp(`<\\s*v${index}\\s*/\\s*>`, "gi");
18
+ const paired = new RegExp(`<\\s*v${index}\\s*>\\s*</\\s*v${index}\\s*>`, "gi");
19
+ out = out.replace(selfClosed, () => token).replace(paired, () => token);
20
+ }
21
+ return out;
22
+ }
23
+ var bag = (items) => [...items].sort().join("\0");
24
+ /** A placeholder lost, repeated, added or still masked after translation: such a value must not reach a locale file. */
25
+ var isIntact = (translated, tokens) => !/<\s*\/?\s*v\d+\s*\/?\s*>/i.test(translated) && bag(translated.match(PLACEHOLDER) ?? []) === bag(tokens);
26
+ var ENTITIES = {
27
+ "&amp;": "&",
28
+ "&lt;": "<",
29
+ "&gt;": ">",
30
+ "&quot;": "\"",
31
+ "&#39;": `'`,
32
+ "&#x27;": `'`
33
+ };
34
+ /** Services in HTML mode return `&#39;` for an apostrophe. */
35
+ var decodeEntities = (text) => text.replace(/&(?:amp|lt|gt|quot|#39|#x27);/g, (entity) => ENTITIES[entity] ?? entity);
36
+ export { restore as i, isIntact as n, protect as r, decodeEntities as t };
@@ -0,0 +1,32 @@
1
+ import { A as log, S as validationError, v as notFoundError } from "./json-BV_apCMu.mjs";
2
+ import { t as Project } from "./project-DgWgr7pF.mjs";
3
+ import { i as restore, r as protect } from "./protect-D1HIuxmJ.mjs";
4
+ // src/commands/pseudo.ts
5
+ /** Tags, masked placeholders among them, and entities are markup: split on them, the odd chunks are theirs. */
6
+ var elongate = (text) => text.split(/(<\/?[a-z][^<>]*>|&#?\w+;)/i).map((chunk, index) => index % 2 === 1 ? chunk : chunk.replace(/[a-z]/gi, "$&$&")).join("");
7
+ function pseudoLocalize(text) {
8
+ const { masked, tokens } = protect(text);
9
+ return restore(`[${elongate(masked)}]`, tokens);
10
+ }
11
+ async function run(args) {
12
+ const project = await Project.load(args);
13
+ const sourceCode = args.sourceLocale ?? project.config.defaultLocale;
14
+ if (sourceCode === args.targetLocale) throw validationError("Source and target locale must be different");
15
+ if (!project.codes.includes(args.targetLocale)) throw notFoundError(`Locale ${args.targetLocale} not found`, ["Add it to i18n.locales in nuxt.config first."]);
16
+ const source = project.locale(sourceCode);
17
+ const target = project.locale(args.targetLocale);
18
+ let updated = 0;
19
+ let skipped = 0;
20
+ for (const scope of source.scopes) for (const [key, value] of Object.entries(source.strings(scope))) {
21
+ const current = target.get(key, scope);
22
+ const kept = !args.replace && current !== void 0 && (typeof current !== "string" || current.trim() !== "");
23
+ const conflict = kept ? null : target.set(key, pseudoLocalize(value), scope);
24
+ if (conflict) log.warn(`Skipping ${key}: ${conflict} already holds a value.`);
25
+ if (kept || conflict) skipped += 1;
26
+ else updated += 1;
27
+ }
28
+ project.save();
29
+ log.success(`Pseudo locale generated: ${sourceCode} -> ${args.targetLocale}`);
30
+ log.note(`Updated keys: ${updated}, skipped keys: ${skipped}`);
31
+ }
32
+ export { run as default, pseudoLocalize };
@@ -0,0 +1,117 @@
1
+ import { A as log, S as validationError, f as pathConflict, i as flatten, o as getPath, p as setPath, s as groupBy, t as deletePath, u as isWritableKey, v as notFoundError } from "./json-BV_apCMu.mjs";
2
+ import { d as splitList, g as writeFilesAtomic } from "./nuxt-dKxG087V.mjs";
3
+ import { t as Project } from "./project-DgWgr7pF.mjs";
4
+ import { i as isOwnSource, n as dataTokensOf, u as scanProject } from "./source-Bx2lHSW7.mjs";
5
+ import { readFileSync } from "node:fs";
6
+ import { MagicString } from "@vue/compiler-sfc";
7
+ // src/commands/rename.ts
8
+ function renameFor(from, to) {
9
+ return {
10
+ from,
11
+ to,
12
+ renamed: (key) => key === from ? to : key.startsWith(`${from}.`) ? `${to}${key.slice(from.length)}` : null
13
+ };
14
+ }
15
+ /** One path at a time, on a copy: a clash halfway leaves the tree as it was, and every other key as written. */
16
+ function moved(tree, rename) {
17
+ const moves = Object.keys(flatten(tree)).flatMap((key) => {
18
+ const target = rename.renamed(key);
19
+ return target ? [{
20
+ key,
21
+ target,
22
+ value: getPath(tree, key),
23
+ flat: Object.hasOwn(tree, key)
24
+ }] : [];
25
+ });
26
+ if (moves.length === 0) return null;
27
+ const next = structuredClone(tree);
28
+ for (const { key, value } of moves) {
29
+ deletePath(next, key);
30
+ const second = getPath(next, key);
31
+ if (second !== void 0 && JSON.stringify(second) !== JSON.stringify(value)) throw validationError(`${key} is written twice in one file, with different values.`, ["Keep one of them, then rename."], { Key: key });
32
+ deletePath(next, key);
33
+ }
34
+ for (const { target, value, flat } of moves) {
35
+ const clash = getPath(next, target) === void 0 ? pathConflict(next, target) : target;
36
+ if (clash) throw validationError(`Target key already exists: ${clash}`, [], { Key: clash });
37
+ if (flat) next[target] = value;
38
+ else setPath(next, target, value);
39
+ }
40
+ return next;
41
+ }
42
+ function applyToLocales(project, rename) {
43
+ let changed = 0;
44
+ for (const code of project.codes) {
45
+ const locale = project.locale(code);
46
+ if (locale.scopes.filter((scope) => {
47
+ const next = moved(locale.tree(scope), rename);
48
+ if (next) locale.replaceTree(scope, next);
49
+ if (next && getPath(locale.inheritedTree(scope), rename.from) !== void 0) log.warn(`${rename.from} is also defined in a layer of ${code}: it stays there.`);
50
+ return Boolean(next);
51
+ }).length > 0) changed += 1;
52
+ }
53
+ return changed;
54
+ }
55
+ /** Only what the parser placed. A position that does not hold the key means the expression was decoded: leave it. */
56
+ function rewritable(ref, content) {
57
+ const written = ref.keyPrefix ? ref.key.slice(ref.keyPrefix.length) : ref.key;
58
+ return ref.start >= 0 && content.slice(ref.start, ref.end) === written;
59
+ }
60
+ function rewriteFile(file, refs, rename, skipped, config) {
61
+ if (!isOwnSource(config, file)) {
62
+ skipped.push(...refs);
63
+ return null;
64
+ }
65
+ const content = readFileSync(file, "utf8");
66
+ const magic = new MagicString(content);
67
+ let count = 0;
68
+ for (const ref of refs) {
69
+ const target = rename.renamed(ref.key);
70
+ const fits = !ref.keyPrefix || target.startsWith(ref.keyPrefix);
71
+ if (!rewritable(ref, content) || !fits) {
72
+ skipped.push(ref);
73
+ continue;
74
+ }
75
+ magic.overwrite(ref.start, ref.end, target.slice(ref.keyPrefix?.length ?? 0));
76
+ count += 1;
77
+ }
78
+ return count > 0 ? [file, magic.toString()] : null;
79
+ }
80
+ function rewriteSources(scan, rename, skipped, config) {
81
+ return [...groupBy(scan.refs.filter((ref) => rename.renamed(ref.key)), (ref) => ref.file)].flatMap(([file, refs]) => {
82
+ const change = rewriteFile(file, refs, rename, skipped, config);
83
+ return change ? [change] : [];
84
+ });
85
+ }
86
+ /** What a rename cannot do by itself. Silence here is how `$t(name)` ends up pointing at nothing. */
87
+ function warnLeftovers(scan, rename, skipped) {
88
+ for (const ref of skipped) log.warn(`Not rewritten, change by hand: ${ref.file}:${ref.line}`);
89
+ for (const entry of scan.dynamic) if (entry.prefix && (rename.renamed(entry.prefix.endsWith(".") ? entry.prefix.slice(0, -1) : entry.prefix) || rename.from.startsWith(entry.prefix))) log.warn(`Computed key may need the new name: ${entry.file}:${entry.line}: ${entry.expression}`);
90
+ for (const [text, locations] of scan.literals) {
91
+ if (!rename.renamed(text)) continue;
92
+ for (const location of locations) log.warn(`The key also appears as a plain string: ${location}`);
93
+ }
94
+ for (const file of scan.dataFiles) if (dataTokensOf(file).some((token) => rename.renamed(token))) log.warn(`A data file mentions the key, check it by hand: ${file}`);
95
+ }
96
+ async function run(args) {
97
+ const { from, to, dryRun } = args;
98
+ if (!from || !to) throw validationError("--from and --to cannot be empty");
99
+ if (from === to) throw validationError("Source and target keys must be different");
100
+ if (!isWritableKey(to)) throw validationError(`--to cannot be a key: ${to}`, ["No quotes, backslashes, empty segments or __proto__."]);
101
+ const project = await Project.load(args);
102
+ const rename = renameFor(from, to);
103
+ const localesUpdated = applyToLocales(project, rename);
104
+ const scan = scanProject(project.config, { ignore: splitList(args.ignore) });
105
+ const skipped = [];
106
+ const sourceChanges = rewriteSources(scan, rename, skipped, project.config);
107
+ const replaced = scan.refs.filter((ref) => rename.renamed(ref.key)).length - skipped.length;
108
+ if (localesUpdated === 0 && sourceChanges.length === 0) throw notFoundError(`Key not found: ${from}`, ["Search for the key first: i18n-micro search \"<fragment>\""], { Key: from });
109
+ warnLeftovers(scan, rename, skipped);
110
+ if (dryRun) log.note("Dry run: no files were modified.");
111
+ else {
112
+ writeFilesAtomic([...project.pendingFiles(), ...sourceChanges]);
113
+ log.success(`Key renamed: ${from} -> ${to}`);
114
+ }
115
+ log.note(`Locales: ${localesUpdated}, source files: ${sourceChanges.length}, source replacements: ${replaced}`);
116
+ }
117
+ export { run as default };
@@ -0,0 +1,200 @@
1
+ import { t as version$1 } from "./package-BejjFS6s.mjs";
2
+ import { A as log, D as closePrompts, N as spinner, O as formatDuration, P as style, S as validationError, b as reportError, d as ownValue, k as isInteractive, x as usageError } from "./json-BV_apCMu.mjs";
3
+ import { a as commands, i as REMOVED_COMMANDS, n as COMMAND_GROUPS } from "./spec-DwveYr8X.mjs";
4
+ import { defineCommand, runCommand } from "citty";
5
+ // src/help.ts
6
+ /** The layout of citty's `renderUsage`, as nuxi prints it, with commands grouped and examples added. */
7
+ var BIN = "i18n-micro";
8
+ var title = (text) => style(["underline", "bold"], text);
9
+ /** Two columns, the first padded to the widest: `pad` first, colors after, or the escapes count as width. */
10
+ function table(rows) {
11
+ const width = Math.max(0, ...rows.map(([left]) => left.length));
12
+ return rows.map(([left, right]) => ` ${style("cyan", left.padEnd(width))} ${right}`);
13
+ }
14
+ /** Flags every command has: listed after the ones of the command. */
15
+ var COMMON = /* @__PURE__ */ new Set([
16
+ "cwd",
17
+ "logLevel",
18
+ "debug"
19
+ ]);
20
+ function flag(name, arg) {
21
+ const names = [...arg.alias ? [`-${arg.alias}`] : [], `--${name}`].join(", ");
22
+ if (arg.type === "boolean") return names;
23
+ return `${names} <${arg.values ? arg.values.join("|") : arg.numeric ? "n" : "value"}>`;
24
+ }
25
+ function describe(arg) {
26
+ const fallback = arg.defaultHint?.replaceAll("`", "") ?? (arg.default === void 0 || arg.default === false ? void 0 : String(arg.default));
27
+ const notes = [arg.required ? "(Required)" : "", fallback === void 0 ? "" : `(Default: ${fallback})`].filter(Boolean).join(" ");
28
+ return notes ? `${arg.description} ${style("gray", notes)}` : arg.description;
29
+ }
30
+ function rootUsage(version) {
31
+ const width = Math.max(...Object.keys(commands).map((name) => name.length));
32
+ const groups = COMMAND_GROUPS.flatMap((group) => {
33
+ const rows = Object.entries(commands).filter(([, spec]) => spec.group === group);
34
+ return ["", ` ${style("bold", group)}`].concat(rows.map(([name, spec]) => ` ${style("cyan", name.padEnd(width))} ${spec.description}`));
35
+ });
36
+ return [
37
+ style("gray", `Command-line tools for nuxt-i18n-micro (${BIN} v${version})`),
38
+ "",
39
+ `${title("USAGE")} ${style("cyan", `${BIN} <command> [OPTIONS]`)}`,
40
+ "",
41
+ title("COMMANDS"),
42
+ ...groups,
43
+ "",
44
+ `Use ${style("cyan", `${BIN} <command> --help`)} for more information about a command.`
45
+ ].join("\n");
46
+ }
47
+ function commandUsage(name, spec, version) {
48
+ const entries = Object.entries(spec.args);
49
+ const positionals = entries.filter(([, arg]) => arg.type === "positional");
50
+ const options = entries.filter(([, arg]) => arg.type !== "positional").sort(([a], [b]) => Number(COMMON.has(a)) - Number(COMMON.has(b)));
51
+ const usage = [`${BIN} ${name}`, options.length > 0 ? "[OPTIONS]" : ""];
52
+ for (const [arg, { required }] of positionals) usage.push(required ? `<${arg.toUpperCase()}>` : `[${arg.toUpperCase()}]`);
53
+ for (const [arg, option] of options) if (option.required) usage.push(flag(arg, option));
54
+ const optionRows = options.flatMap(([arg, option]) => {
55
+ const row = [flag(arg, option), describe(option)];
56
+ return option.type === "boolean" && option.default === true ? [row, [`--no-${arg}`, `Turn --${arg} off`]] : [row];
57
+ });
58
+ const section = (heading, lines) => lines.length > 0 ? [
59
+ title(heading),
60
+ "",
61
+ ...lines,
62
+ ""
63
+ ] : [];
64
+ return [
65
+ style("gray", `${spec.description} (${BIN} ${name} v${version})`),
66
+ "",
67
+ `${title("USAGE")} ${style("cyan", usage.filter(Boolean).join(" "))}`,
68
+ "",
69
+ ...section("ARGUMENTS", table(positionals.map(([arg, option]) => [arg.toUpperCase(), describe(option)]))),
70
+ ...section("OPTIONS", table(optionRows)),
71
+ ...section("EXAMPLES", (spec.examples ?? []).map((example) => ` ${style("gray", "$")} ${example}`))
72
+ ].join("\n").trimEnd();
73
+ }
74
+ // src/runner.ts
75
+ var version = version$1;
76
+ function citty(spec) {
77
+ const { type, description, default: fallback, required, alias } = spec;
78
+ return {
79
+ type,
80
+ description,
81
+ default: fallback,
82
+ required,
83
+ alias
84
+ };
85
+ }
86
+ function convert(name, spec, value) {
87
+ if (value === void 0 || value === "" && !spec.numeric) return value;
88
+ if (spec.values && !spec.values.includes(String(value))) throw usageError(`Invalid value "${String(value)}" for --${name}`, [`Accepted: ${spec.values.join(", ")}`]);
89
+ if (!spec.numeric) return value;
90
+ const number = Number(value);
91
+ if (!(Number.isFinite(number) && number > 0 && (spec.numeric === "number" || Number.isInteger(number)))) throw validationError(`--${name} must be a positive ${spec.numeric === "int" ? "integer" : "number"}`, [`Example: --${name} 10`]);
92
+ return number;
93
+ }
94
+ var toCamel = (name) => name.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
95
+ var flagName = (raw) => raw.split("=")[0];
96
+ function assertKnownFlag(name, spec, raw) {
97
+ const flag = toCamel(flagName(raw).slice(2));
98
+ const positive = flag.replace(/^no([A-Z])/, (_match, letter) => letter.toLowerCase());
99
+ if (flag !== "help" && !spec.args[flag] && !spec.args[positive]) throw usageError(`Unknown flag ${flagName(raw)} for ${name}`, [`See i18n-micro ${name} --help`]);
100
+ return {
101
+ arg: spec.args[flag] ?? spec.args[positive],
102
+ negated: !spec.args[flag]
103
+ };
104
+ }
105
+ /** The parser reads anything but `false` as true, so `--force=no` would mean the opposite. */
106
+ function assertBooleanValue(raw) {
107
+ const separator = raw.indexOf("=");
108
+ if (separator !== -1 && !["true", "false"].includes(raw.slice(separator + 1))) throw usageError(`${flagName(raw)} takes true or false`, [`Or write ${flagName(raw)} alone.`]);
109
+ }
110
+ /** The parser hands the next word to a string flag, dashes or not: `--exclude --dryRun` would lose the `--dryRun`. */
111
+ function assertHasValue(raw, next) {
112
+ if (next === void 0 || next.startsWith("-") && next !== "-") throw usageError(`${raw} needs a value`, [`For a value that starts with a dash: ${raw}=<value>`]);
113
+ }
114
+ /**
115
+ * A mistyped `--dryrun`, `-dryRun` or `dryRun` on `clean` must not run the real thing: whatever
116
+ * the parser would drop or misread is an error here. Messages name the flag, never a value:
117
+ * a mistyped `--token` would otherwise end up in a CI log.
118
+ */
119
+ function assertKnownArgs(name, spec, rawArgs) {
120
+ let positionals = 0;
121
+ const end = rawArgs.includes("--") ? rawArgs.indexOf("--") : rawArgs.length;
122
+ for (let index = 0; index < end; index += 1) {
123
+ const raw = rawArgs[index];
124
+ if (raw.startsWith("--")) {
125
+ const { arg, negated } = assertKnownFlag(name, spec, raw);
126
+ if (arg?.type === "boolean" || negated) assertBooleanValue(raw);
127
+ else if (arg?.type === "string" && !raw.includes("=")) {
128
+ assertHasValue(raw, index + 1 < end ? rawArgs[index + 1] : void 0);
129
+ index += 1;
130
+ }
131
+ } else if (raw.startsWith("-") && raw !== "-") throw usageError(`Not a flag of ${name}`, ["Long flags take two dashes.", `See i18n-micro ${name} --help`]);
132
+ else positionals += 1;
133
+ }
134
+ positionals += Math.max(0, rawArgs.length - end - 1);
135
+ if (positionals > Object.values(spec.args).filter((arg) => arg.type === "positional").length) throw usageError(`Too many arguments for ${name}`, [`See i18n-micro ${name} --help`]);
136
+ }
137
+ function validated(spec, args) {
138
+ const out = { ...args };
139
+ for (const [name, arg] of Object.entries(spec.args)) out[name] = convert(name, arg, args[name]);
140
+ return out;
141
+ }
142
+ function toCommand(name, spec) {
143
+ const args = Object.fromEntries(Object.entries(spec.args).map(([key, arg]) => [key, citty(arg)]));
144
+ return defineCommand({
145
+ meta: {
146
+ name,
147
+ description: spec.description
148
+ },
149
+ args,
150
+ async run({ args: parsed, rawArgs }) {
151
+ assertKnownArgs(name, spec, rawArgs);
152
+ log.setSilent(parsed.logLevel === "silent");
153
+ if (isInteractive() && !parsed.json) log.info(`${style("green", `i18n-micro ${style("bold", `v${version}`)}`)} ${style("gray", `· ${name}`)}`);
154
+ spinner.start(`Running ${name}`);
155
+ await (await spec.load()).default(validated(spec, parsed));
156
+ }
157
+ });
158
+ }
159
+ var main = defineCommand({
160
+ meta: {
161
+ name: "i18n-micro",
162
+ version,
163
+ description: "Command-line tools for nuxt-i18n-micro"
164
+ },
165
+ subCommands: Object.fromEntries(Object.entries(commands).map(([name, spec]) => [name, toCommand(name, spec)]))
166
+ });
167
+ /** How long the command took, as nuxi ends a build: on a terminal only, where it cannot unsettle a diff of the output. */
168
+ function footer(started, rawArgs) {
169
+ if (isInteractive() && !rawArgs.some((arg) => arg === "--json" || arg.startsWith("--json="))) log.info(style("gray", `\nDone in ${formatDuration(performance.now() - started)}`));
170
+ }
171
+ /** Run the CLI. Failures set `process.exitCode`; nothing here throws. */
172
+ async function runCli(rawArgs) {
173
+ process.exitCode = 0;
174
+ const first = rawArgs[0] ?? "";
175
+ try {
176
+ if (rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0])) {
177
+ console.log(version);
178
+ return;
179
+ }
180
+ const replacement = ownValue(REMOVED_COMMANDS, first);
181
+ if (replacement) throw usageError(`The ${first} command was removed. Use ${replacement}.`);
182
+ if (rawArgs.length === 0 || rawArgs.includes("--help") || rawArgs.includes("-h")) {
183
+ const spec = ownValue(commands, first);
184
+ log.info(spec ? commandUsage(first, spec, version) : rootUsage(version));
185
+ return;
186
+ }
187
+ if (first.startsWith("-")) throw usageError("Flags go after the command.");
188
+ if (!ownValue(commands, first)) throw usageError(`Unknown command ${first}`, ["See i18n-micro --help"]);
189
+ const started = performance.now();
190
+ await runCommand(main, { rawArgs });
191
+ spinner.stop();
192
+ footer(started, rawArgs);
193
+ } catch (error) {
194
+ spinner.stop();
195
+ reportError(error, rawArgs.includes("--debug"));
196
+ } finally {
197
+ closePrompts();
198
+ }
199
+ }
200
+ export { runCli as t };
@@ -0,0 +1,67 @@
1
+ import { A as log, P as style, S as validationError, a as flattenStrings, o as getPath } from "./json-BV_apCMu.mjs";
2
+ import { d as splitList, u as relativePath } from "./nuxt-dKxG087V.mjs";
3
+ import { t as Project } from "./project-DgWgr7pF.mjs";
4
+ import { f as usageDoubts, p as usageIndex, u as scanProject } from "./source-Bx2lHSW7.mjs";
5
+ // src/commands/search.ts
6
+ function scopesOf(project, wanted) {
7
+ const all = [...new Set(project.codes.flatMap((code) => project.locale(code).scopes))];
8
+ if (wanted === "global") return [""];
9
+ return wanted === "pages" ? all.filter((scope) => scope !== "") : all;
10
+ }
11
+ function matchesOf(project, scan, args) {
12
+ const isUsed = usageIndex(scan);
13
+ const fold = (text) => args.caseSensitive ? text : text.toLowerCase();
14
+ const query = fold((args.query ?? "").trim());
15
+ const usagesOf = (key) => scan.refs.filter((ref) => ref.key === key).map((ref) => `${relativePath(project.config.rootDir, ref.file)}:${ref.line}`);
16
+ return scopesOf(project, args.scope).flatMap((scope) => {
17
+ const byLocale = project.codes.map((code) => [code, flattenStrings(project.locale(code).tree(scope))]);
18
+ return [...new Set(byLocale.flatMap(([, strings]) => Object.keys(strings)))].flatMap((key) => {
19
+ const values = byLocale.map(([, strings]) => strings[key] ?? "");
20
+ const used = isUsed(key);
21
+ const removable = project.codes.some((code) => {
22
+ const locale = project.locale(code);
23
+ return locale.get(key, scope) !== void 0 && getPath(locale.inheritedTree(scope), key) === void 0;
24
+ });
25
+ if (args.onlyUnused && (used || !removable)) return [];
26
+ if (query && ![key, ...values].some((text) => fold(text).includes(query))) return [];
27
+ return [{
28
+ scope: scope === "" ? "global" : `pages/${scope}`,
29
+ key,
30
+ translations: Object.fromEntries(byLocale.map(([code, strings]) => [code, strings[key] ?? null])),
31
+ missingLocales: byLocale.filter(([, strings]) => strings[key] === void 0).map(([code]) => code),
32
+ isUsed: used,
33
+ usages: usagesOf(key)
34
+ }];
35
+ });
36
+ });
37
+ }
38
+ function printMatch(match) {
39
+ log.section(`${match.scope}: ${match.key}`);
40
+ log.rows(Object.entries(match.translations).map(([code, value]) => [code, value ?? "[missing]"]));
41
+ log.info(style("gray", ` ${match.isUsed ? `Used in: ${match.usages.join(", ") || "a string or a computed key"}` : "Not used"}`));
42
+ }
43
+ async function run(args) {
44
+ const query = (args.query ?? "").trim();
45
+ if (!query && !args.onlyUnused) throw validationError("Search query cannot be empty unless --onlyUnused is set", ["Example: i18n-micro search \"welcome\""]);
46
+ const project = await Project.load(args);
47
+ const scan = scanProject(project.config, { ignore: splitList(args.ignore) });
48
+ const found = matchesOf(project, scan, args);
49
+ const matches = args.limit ? found.slice(0, args.limit) : found;
50
+ const doubts = args.onlyUnused ? usageDoubts(scan) : [];
51
+ for (const doubt of doubts) log.warn(doubt);
52
+ if (args.json) {
53
+ log.json({
54
+ query,
55
+ totalMatches: found.length,
56
+ matches,
57
+ ...args.onlyUnused ? { doubts } : {}
58
+ });
59
+ return;
60
+ }
61
+ if (matches.length === 0) {
62
+ log.note(query ? `No matches found for "${query}"` : "No matches found");
63
+ return;
64
+ }
65
+ for (const match of matches) printMatch(match);
66
+ }
67
+ export { run as default };