@the-i18n-kit/cli 4.9.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 +368 -0
- package/dist/_shared-Dv1EW2gy.js +248 -0
- package/dist/_shared-Dv1EW2gy.js.map +1 -0
- package/dist/add-BvXrJkqZ.js +40 -0
- package/dist/add-BvXrJkqZ.js.map +1 -0
- package/dist/bin-Cs-dvpEC.d.ts +1 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/check-9dcNfHtC.js +49 -0
- package/dist/check-9dcNfHtC.js.map +1 -0
- package/dist/cli-CiX4llY1.js +82 -0
- package/dist/cli-CiX4llY1.js.map +1 -0
- package/dist/config/framework/stubs/next-intl-routing-DEwC7Bbk.d.ts +19 -0
- package/dist/config/framework/stubs/next-intl-routing-DEwC7Bbk.d.ts.map +1 -0
- package/dist/config/framework/stubs/next-intl-routing.js +22 -0
- package/dist/config/framework/stubs/next-intl-routing.js.map +1 -0
- package/dist/config/framework/stubs/unplugin-vue-i18n-D9yaDzRk.d.ts +25 -0
- package/dist/config/framework/stubs/unplugin-vue-i18n-D9yaDzRk.d.ts.map +1 -0
- package/dist/config/framework/stubs/unplugin-vue-i18n.js +26 -0
- package/dist/config/framework/stubs/unplugin-vue-i18n.js.map +1 -0
- package/dist/define-config-4ubtfvUy.js +14 -0
- package/dist/define-config-4ubtfvUy.js.map +1 -0
- package/dist/define-config-Dhg13592.d.ts +142 -0
- package/dist/define-config-Dhg13592.d.ts.map +1 -0
- package/dist/define-config-lQrIpPSK.d.ts +2 -0
- package/dist/define-config.d.ts +1 -0
- package/dist/define-config.js +2 -0
- package/dist/detect-Dr2skxeL.js +17 -0
- package/dist/detect-Dr2skxeL.js.map +1 -0
- package/dist/empty-p0wct8Hk.js +36 -0
- package/dist/empty-p0wct8Hk.js.map +1 -0
- package/dist/errors-coI1dhw1.js +33 -0
- package/dist/errors-coI1dhw1.js.map +1 -0
- package/dist/find-duplicates-DuI8niF7.js +31 -0
- package/dist/find-duplicates-DuI8niF7.js.map +1 -0
- package/dist/get-BskDC2wT.js +41 -0
- package/dist/get-BskDC2wT.js.map +1 -0
- package/dist/index-BtSA6-iT.d.ts +1130 -0
- package/dist/index-BtSA6-iT.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/init-D7r6iX3q.js +33 -0
- package/dist/init-D7r6iX3q.js.map +1 -0
- package/dist/list-dirs-BHk38JXZ.js +17 -0
- package/dist/list-dirs-BHk38JXZ.js.map +1 -0
- package/dist/missing-0gSZKTgt.js +51 -0
- package/dist/missing-0gSZKTgt.js.map +1 -0
- package/dist/operations-BCJd7in3.js +6464 -0
- package/dist/operations-BCJd7in3.js.map +1 -0
- package/dist/php-reader-DcgOAhTw.js +2 -0
- package/dist/php-reader-DuZ0Hyl_.js +32 -0
- package/dist/php-reader-DuZ0Hyl_.js.map +1 -0
- package/dist/providers-BAwPjeCy.js +482 -0
- package/dist/providers-BAwPjeCy.js.map +1 -0
- package/dist/remove-DR_mRiMA.js +41 -0
- package/dist/remove-DR_mRiMA.js.map +1 -0
- package/dist/remove-orphans-CcA3XQHJ.js +57 -0
- package/dist/remove-orphans-CcA3XQHJ.js.map +1 -0
- package/dist/rename-CWLfThC1.js +45 -0
- package/dist/rename-CWLfThC1.js.map +1 -0
- package/dist/scaffold-BahXKbo9.js +39 -0
- package/dist/scaffold-BahXKbo9.js.map +1 -0
- package/dist/scan-CTxs3kl7.js +31 -0
- package/dist/scan-CTxs3kl7.js.map +1 -0
- package/dist/search-BonEgmSt.js +49 -0
- package/dist/search-BonEgmSt.js.map +1 -0
- package/dist/status-C_5Khw14.js +45 -0
- package/dist/status-C_5Khw14.js.map +1 -0
- package/dist/stdout-guard-sJHFWVem.js +20 -0
- package/dist/stdout-guard-sJHFWVem.js.map +1 -0
- package/dist/translate-CnXvBi5O.js +92 -0
- package/dist/translate-CnXvBi5O.js.map +1 -0
- package/dist/translate-key-DwO9XZMc.js +73 -0
- package/dist/translate-key-DwO9XZMc.js.map +1 -0
- package/dist/update-HR8MvPlI.js +40 -0
- package/dist/update-HR8MvPlI.js.map +1 -0
- package/dist/write-CAD9xQDy.js +47 -0
- package/dist/write-CAD9xQDy.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { i as toErrorMessage, n as FileIOError } from "./errors-coI1dhw1.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { fromString } from "php-array-reader";
|
|
5
|
+
//#region src/io/php-reader.ts
|
|
6
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
7
|
+
function clearPhpFileCacheEntry(filePath) {
|
|
8
|
+
fileCache.delete(filePath);
|
|
9
|
+
}
|
|
10
|
+
async function readPhpLocaleFile(filePath) {
|
|
11
|
+
if (!existsSync(filePath)) throw new FileIOError(`File not found: ${filePath}`, filePath, "FILE_NOT_FOUND");
|
|
12
|
+
try {
|
|
13
|
+
const mtime = (await stat(filePath)).mtimeMs;
|
|
14
|
+
const cached = fileCache.get(filePath);
|
|
15
|
+
if (cached && cached.mtime === mtime) return structuredClone(cached.data);
|
|
16
|
+
const parsed = fromString(await readFile(filePath, "utf-8"));
|
|
17
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new FileIOError(`PHP locale file did not return an associative array: ${filePath}`, filePath);
|
|
18
|
+
const data = parsed;
|
|
19
|
+
fileCache.set(filePath, {
|
|
20
|
+
data: structuredClone(data),
|
|
21
|
+
mtime
|
|
22
|
+
});
|
|
23
|
+
return data;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error instanceof FileIOError) throw error;
|
|
26
|
+
throw new FileIOError(`Failed to read PHP locale file: ${filePath}: ${toErrorMessage(error)}`, filePath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { readPhpLocaleFile as n, clearPhpFileCacheEntry as t };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=php-reader-DuZ0Hyl_.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-reader-DuZ0Hyl_.js","names":[],"sources":["../src/io/php-reader.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { fromString } from 'php-array-reader'\nimport { FileIOError, toErrorMessage } from '../utils/errors'\n\nconst fileCache = new Map<string, { data: Record<string, unknown>; mtime: number }>()\n\nexport function clearPhpFileCache(): void {\n fileCache.clear()\n}\n\nexport function clearPhpFileCacheEntry(filePath: string): void {\n fileCache.delete(filePath)\n}\n\nexport async function readPhpLocaleFile(filePath: string): Promise<Record<string, unknown>> {\n if (!existsSync(filePath)) {\n throw new FileIOError(`File not found: ${filePath}`, filePath, 'FILE_NOT_FOUND')\n }\n\n try {\n const fileStat = await stat(filePath)\n const mtime = fileStat.mtimeMs\n\n const cached = fileCache.get(filePath)\n if (cached && cached.mtime === mtime) {\n return structuredClone(cached.data)\n }\n\n const content = await readFile(filePath, 'utf-8')\n const parsed = fromString(content)\n\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new FileIOError(\n `PHP locale file did not return an associative array: ${filePath}`,\n filePath,\n )\n }\n\n const data = parsed as Record<string, unknown>\n fileCache.set(filePath, { data: structuredClone(data), mtime })\n return data\n }\n catch (error) {\n if (error instanceof FileIOError) throw error\n throw new FileIOError(\n `Failed to read PHP locale file: ${filePath}: ${toErrorMessage(error)}`,\n filePath,\n )\n }\n}\n"],"mappings":";;;;;AAKA,MAAM,4BAAY,IAAI,KAA+D;AAMrF,SAAgB,uBAAuB,UAAwB;AAC7D,WAAU,OAAO,SAAS;;AAG5B,eAAsB,kBAAkB,UAAoD;AAC1F,KAAI,CAAC,WAAW,SAAS,CACvB,OAAM,IAAI,YAAY,mBAAmB,YAAY,UAAU,iBAAiB;AAGlF,KAAI;EAEF,MAAM,SADW,MAAM,KAAK,SAAS,EACd;EAEvB,MAAM,SAAS,UAAU,IAAI,SAAS;AACtC,MAAI,UAAU,OAAO,UAAU,MAC7B,QAAO,gBAAgB,OAAO,KAAK;EAIrC,MAAM,SAAS,WADC,MAAM,SAAS,UAAU,QAAQ,CACf;AAElC,MAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,YACR,wDAAwD,YACxD,SACD;EAGH,MAAM,OAAO;AACb,YAAU,IAAI,UAAU;GAAE,MAAM,gBAAgB,KAAK;GAAE;GAAO,CAAC;AAC/D,SAAO;UAEF,OAAO;AACZ,MAAI,iBAAiB,YAAa,OAAM;AACxC,QAAM,IAAI,YACR,mCAAmC,SAAS,IAAI,eAAe,MAAM,IACrE,SACD"}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { i as toErrorMessage, t as ConfigError } from "./errors-coI1dhw1.js";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { createConsola } from "consola";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region src/utils/logger.ts
|
|
9
|
+
const log = createConsola({
|
|
10
|
+
stdout: process.stderr,
|
|
11
|
+
stderr: process.stderr
|
|
12
|
+
}).withTag("the-i18n-cli");
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/config/schema.ts
|
|
15
|
+
/**
|
|
16
|
+
* The one definition of the declared half of the kit's configuration —
|
|
17
|
+
* everything a human tells the tool, as opposed to what it can see for itself.
|
|
18
|
+
*
|
|
19
|
+
* `.i18n-mcp.json`, `i18n-kit.config.ts` and the `i18nKit` block in
|
|
20
|
+
* `nuxt.config.ts` all accept this same shape, and all validate against this
|
|
21
|
+
* same schema. The `ProjectConfig` interface in `./types.ts` documents it;
|
|
22
|
+
* the guard at the bottom of this file fails the build if the two drift.
|
|
23
|
+
*/
|
|
24
|
+
const nonEmptyString = z.string().min(1).refine((s) => s.trim().length > 0, "Must not be empty or whitespace-only");
|
|
25
|
+
const layerRuleSchema = z.object({
|
|
26
|
+
layer: z.string(),
|
|
27
|
+
description: z.string(),
|
|
28
|
+
when: z.string()
|
|
29
|
+
});
|
|
30
|
+
const localeDirEntrySchema = z.union([nonEmptyString, z.object({
|
|
31
|
+
path: nonEmptyString,
|
|
32
|
+
layer: nonEmptyString
|
|
33
|
+
})]);
|
|
34
|
+
const projectConfigSchema = z.object({
|
|
35
|
+
$schema: z.string().optional(),
|
|
36
|
+
framework: z.string().optional(),
|
|
37
|
+
context: z.string().optional(),
|
|
38
|
+
layerRules: z.array(layerRuleSchema).optional(),
|
|
39
|
+
glossary: z.record(z.string(), z.string()).optional(),
|
|
40
|
+
translationPrompt: z.string().optional(),
|
|
41
|
+
localeNotes: z.record(z.string(), z.string()).optional(),
|
|
42
|
+
examples: z.array(z.record(z.string(), z.string())).optional(),
|
|
43
|
+
orphanScan: z.record(z.string(), z.object({ ignorePatterns: z.array(z.string()).optional() }).passthrough()).optional(),
|
|
44
|
+
localeDirs: z.array(localeDirEntrySchema).optional(),
|
|
45
|
+
defaultLocale: nonEmptyString.optional(),
|
|
46
|
+
locales: z.array(nonEmptyString).optional(),
|
|
47
|
+
protectedLocales: z.array(nonEmptyString).optional(),
|
|
48
|
+
reportOutput: z.union([z.literal(true), nonEmptyString]).optional(),
|
|
49
|
+
localeFileFormat: z.enum(["json", "php-array"]).optional(),
|
|
50
|
+
providerBaseUrl: nonEmptyString.optional(),
|
|
51
|
+
samplingPreferences: z.unknown().optional()
|
|
52
|
+
}).strict();
|
|
53
|
+
/**
|
|
54
|
+
* Validate a candidate project config against the published schema, returning
|
|
55
|
+
* the formatted issue list rather than throwing. `init` uses this to refuse to
|
|
56
|
+
* emit a config the tool would then reject; the loaders turn the same failure
|
|
57
|
+
* into a ConfigError naming the file it came from.
|
|
58
|
+
*/
|
|
59
|
+
function validateProjectConfig(value) {
|
|
60
|
+
const result = projectConfigSchema.safeParse(value);
|
|
61
|
+
if (result.success) return {
|
|
62
|
+
ok: true,
|
|
63
|
+
data: result.data
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: result.error.issues.map((issue) => {
|
|
68
|
+
return ` ${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`;
|
|
69
|
+
}).join("\n")
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Strip the deprecated keys the schema still accepts, warning once, so that a
|
|
74
|
+
* config keeps validating without the value going on to mean anything. Applied
|
|
75
|
+
* by every loader — a key ignored in `.i18n-mcp.json` but silently honoured in
|
|
76
|
+
* `i18n-kit.config.ts` would be exactly the divergence this file exists to
|
|
77
|
+
* prevent.
|
|
78
|
+
*/
|
|
79
|
+
function dropDeprecatedKeys(config, source) {
|
|
80
|
+
const data = config;
|
|
81
|
+
if ("samplingPreferences" in data) {
|
|
82
|
+
log.warn(`${source}: "samplingPreferences" is deprecated and ignored — MCP sampling was removed. Configure a provider (e.g. I18N_PROVIDER/I18N_MODEL) instead.`);
|
|
83
|
+
delete data.samplingPreferences;
|
|
84
|
+
}
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/config/typed-config.ts
|
|
89
|
+
/**
|
|
90
|
+
* Loading `i18n-kit.config.ts` — the same configuration `.i18n-mcp.json`
|
|
91
|
+
* carries, written in TypeScript so the editor checks it.
|
|
92
|
+
*
|
|
93
|
+
* Read with no build step, which is the whole point: a `protectedLocales` entry
|
|
94
|
+
* that only takes effect once something has been built is a locale that quietly
|
|
95
|
+
* goes unprotected in a fresh checkout. Everything here runs off the source
|
|
96
|
+
* file as it sits on disk.
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* Candidate names, in the order a directory is searched. `.ts` first because
|
|
100
|
+
* it is the one worth having; the rest exist so a JavaScript project is not
|
|
101
|
+
* told to adopt TypeScript to get a typed config.
|
|
102
|
+
*/
|
|
103
|
+
const TYPED_CONFIG_FILENAMES = [
|
|
104
|
+
"i18n-kit.config.ts",
|
|
105
|
+
"i18n-kit.config.mts",
|
|
106
|
+
"i18n-kit.config.js",
|
|
107
|
+
"i18n-kit.config.mjs",
|
|
108
|
+
"i18n-kit.config.cjs",
|
|
109
|
+
"i18n-kit.config.cts"
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* Walk up from `startDir` for the nearest `i18n-kit.config.*`, the same way
|
|
113
|
+
* `findConfigFile` looks for `.i18n-mcp.json`. Both stop at the first
|
|
114
|
+
* directory that has one, so a config in an app directory shadows one in the
|
|
115
|
+
* repository root rather than merging with it.
|
|
116
|
+
*/
|
|
117
|
+
function findTypedConfigFile(startDir) {
|
|
118
|
+
let dir = resolve(startDir);
|
|
119
|
+
while (true) {
|
|
120
|
+
const found = TYPED_CONFIG_FILENAMES.map((name) => join(dir, name)).filter((path) => existsSync(path));
|
|
121
|
+
if (found[0]) {
|
|
122
|
+
if (found.length > 1) log.warn(`Several i18n-kit configs in ${dir} (${found.map((p) => p.slice(dir.length + 1)).join(", ")}). Using ${found[0].slice(dir.length + 1)} — delete the others, they are being ignored.`);
|
|
123
|
+
return found[0];
|
|
124
|
+
}
|
|
125
|
+
const parent = dirname(dir);
|
|
126
|
+
if (parent === dir) break;
|
|
127
|
+
dir = parent;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Load and validate the nearest typed config, or null when there isn't one.
|
|
133
|
+
*
|
|
134
|
+
* Throws, unlike the framework-config readers this issue also adds. Those read
|
|
135
|
+
* a file someone wrote for another tool, so failing to read it means falling
|
|
136
|
+
* back to a guess. This file exists only for the kit: if it throws, or declares
|
|
137
|
+
* a key that does not exist, carrying on without it is precisely the silent
|
|
138
|
+
* non-application the typed config was added to prevent.
|
|
139
|
+
*/
|
|
140
|
+
async function loadTypedConfig(projectDir) {
|
|
141
|
+
const path = findTypedConfigFile(projectDir);
|
|
142
|
+
if (!path) {
|
|
143
|
+
log.debug(`No i18n-kit.config.* found in ${projectDir} or any parent directory — skipping`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
log.info(`Found typed project config: ${path}`);
|
|
147
|
+
let module;
|
|
148
|
+
try {
|
|
149
|
+
const { createJiti } = await import("jiti");
|
|
150
|
+
module = await createJiti(import.meta.url, {
|
|
151
|
+
interopDefault: false,
|
|
152
|
+
alias: selfAlias()
|
|
153
|
+
}).import(path);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new ConfigError(`Failed to load ${path}: ${toErrorMessage(error)}`);
|
|
156
|
+
}
|
|
157
|
+
if (module === null || typeof module !== "object") throw new ConfigError(`${path} must export its config as the default export — write \`export default defineI18nKitConfig({ ... })\`.`);
|
|
158
|
+
const commonJs = path.endsWith(".cts") || path.endsWith(".cjs");
|
|
159
|
+
if (!("default" in module) && !commonJs) throw new ConfigError(`${path} must export its config as the default export — write \`export default defineI18nKitConfig({ ... })\`.`);
|
|
160
|
+
const exported = "default" in module ? module.default : module;
|
|
161
|
+
if (exported === null || typeof exported !== "object" || Array.isArray(exported)) throw new ConfigError(`${path} must export a config object as its default export — got ${describe(exported)}. Wrap it in defineI18nKitConfig() from "the-i18n-cli/config".`);
|
|
162
|
+
const result = validateProjectConfig(exported);
|
|
163
|
+
if (!result.ok) throw new ConfigError(`Invalid ${path}:\n${result.error}`);
|
|
164
|
+
return {
|
|
165
|
+
path,
|
|
166
|
+
config: dropDeprecatedKeys(result.data, path)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Point `the-i18n-cli/config` at the copy of `defineI18nKitConfig` that is
|
|
171
|
+
* already running.
|
|
172
|
+
*
|
|
173
|
+
* A config file imports it for the types, and types only need the package
|
|
174
|
+
* installed at author time — but the CLI is often run through `npx` in a
|
|
175
|
+
* project that never added it as a dependency, where a bare import would be
|
|
176
|
+
* unresolvable and take the whole config down with it. Resolving it to
|
|
177
|
+
* ourselves costs nothing and is the same function either way: identity.
|
|
178
|
+
*
|
|
179
|
+
* Returns no alias when the entry cannot be located, which leaves normal
|
|
180
|
+
* resolution to find the installed package.
|
|
181
|
+
*/
|
|
182
|
+
function selfAlias() {
|
|
183
|
+
for (const candidate of [
|
|
184
|
+
"./define-config.js",
|
|
185
|
+
"../define-config.js",
|
|
186
|
+
"../define-config.ts"
|
|
187
|
+
]) {
|
|
188
|
+
const path = fileURLToPath(new URL(candidate, import.meta.url));
|
|
189
|
+
if (existsSync(path)) return { "the-i18n-cli/config": path };
|
|
190
|
+
}
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
function describe(value) {
|
|
194
|
+
if (value === null) return "null";
|
|
195
|
+
if (Array.isArray(value)) return "an array";
|
|
196
|
+
if (typeof value === "function") return "a function (call defineI18nKitConfig, do not pass it)";
|
|
197
|
+
return typeof value;
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/config/project-config.ts
|
|
201
|
+
const CONFIG_FILENAME = ".i18n-mcp.json";
|
|
202
|
+
/**
|
|
203
|
+
* Walk up the directory tree from `startDir` to find the nearest config file.
|
|
204
|
+
* Returns the full path if found, null otherwise.
|
|
205
|
+
*/
|
|
206
|
+
function findConfigFile(startDir) {
|
|
207
|
+
let dir = startDir;
|
|
208
|
+
while (true) {
|
|
209
|
+
const candidate = join(dir, CONFIG_FILENAME);
|
|
210
|
+
if (existsSync(candidate)) return candidate;
|
|
211
|
+
const parent = dirname(dir);
|
|
212
|
+
if (parent === dir) break;
|
|
213
|
+
dir = parent;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Load the project's declared configuration, from either place it can be
|
|
219
|
+
* declared: `i18n-kit.config.ts` and `.i18n-mcp.json`. Both are searched from
|
|
220
|
+
* projectDir upwards, the way ESLint, Prettier and tsconfig resolve configs.
|
|
221
|
+
*
|
|
222
|
+
* Returns null when neither exists — the case that must keep behaving exactly
|
|
223
|
+
* as it did before there were two. Throws ConfigError when either file is
|
|
224
|
+
* present but unusable, or when the two disagree.
|
|
225
|
+
*
|
|
226
|
+
* Every adapter funnels through here, which is what makes the typed config
|
|
227
|
+
* work for all of them rather than for whichever one was taught about it.
|
|
228
|
+
*/
|
|
229
|
+
async function loadProjectConfig(projectDir) {
|
|
230
|
+
const [json, typed] = await Promise.all([loadJsonConfig(projectDir), loadTypedConfig(projectDir)]);
|
|
231
|
+
if (!typed) return json?.config ?? null;
|
|
232
|
+
if (!json) return typed.config;
|
|
233
|
+
const conflicts = conflictingKeys(typed.config, json.config);
|
|
234
|
+
if (conflicts.length > 0) throw new ConfigError(`${conflicts.join(", ")} ${conflicts.length === 1 ? "is" : "are"} declared in both\n ${typed.path}\n ${json.path}\nNeither wins — a reader cannot tell which one took effect. Delete the duplicate from one of them.`);
|
|
235
|
+
return {
|
|
236
|
+
...json.config,
|
|
237
|
+
...typed.config
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/** Keys present in both files, ignoring the JSON-only `$schema`. */
|
|
241
|
+
function conflictingKeys(typed, json) {
|
|
242
|
+
const a = typed;
|
|
243
|
+
const b = json;
|
|
244
|
+
return Object.keys(a).filter((key) => key !== "$schema" && a[key] !== void 0 && b[key] !== void 0);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Load the optional `.i18n-mcp.json`, with the path it was found at.
|
|
248
|
+
* Returns null if no config file is found in any ancestor.
|
|
249
|
+
* Throws ConfigError if a file is found but is invalid.
|
|
250
|
+
*/
|
|
251
|
+
async function loadJsonConfig(projectDir) {
|
|
252
|
+
log.debug(`Searching for ${CONFIG_FILENAME} starting from: ${projectDir}`);
|
|
253
|
+
const configPath = findConfigFile(projectDir);
|
|
254
|
+
if (!configPath) {
|
|
255
|
+
log.info(`No ${CONFIG_FILENAME} found in ${projectDir} or any parent directory — skipping`);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
log.info(`Found project config: ${configPath}`);
|
|
259
|
+
let raw;
|
|
260
|
+
try {
|
|
261
|
+
raw = await readFile(configPath, "utf-8");
|
|
262
|
+
} catch (error) {
|
|
263
|
+
throw new ConfigError(`Failed to read ${CONFIG_FILENAME}: ${toErrorMessage(error)}`);
|
|
264
|
+
}
|
|
265
|
+
let parsed;
|
|
266
|
+
try {
|
|
267
|
+
parsed = JSON.parse(raw);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new ConfigError(`Invalid JSON in ${CONFIG_FILENAME}: ${toErrorMessage(error)}`);
|
|
270
|
+
}
|
|
271
|
+
const result = validateProjectConfig(parsed);
|
|
272
|
+
if (!result.ok) throw new ConfigError(`Invalid ${CONFIG_FILENAME}:\n${result.error}`);
|
|
273
|
+
log.debug(`Project config loaded successfully from ${configPath}`);
|
|
274
|
+
return {
|
|
275
|
+
path: configPath,
|
|
276
|
+
config: dropDeprecatedKeys(result.data, CONFIG_FILENAME)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/llm/providers.ts
|
|
281
|
+
/**
|
|
282
|
+
* A classified provider failure. `auth` errors are not retryable (the caller
|
|
283
|
+
* should abort the run), `rate-limit` errors should be retried with backoff,
|
|
284
|
+
* and `provider` covers everything else (server errors, network, …).
|
|
285
|
+
* `config` marks an unusable provider setup and is raised while building the
|
|
286
|
+
* TranslateFn, before any request exists — so it surfaces from the command
|
|
287
|
+
* and never reaches the retry loop.
|
|
288
|
+
*/
|
|
289
|
+
var TranslateProviderError = class extends Error {
|
|
290
|
+
kind;
|
|
291
|
+
status;
|
|
292
|
+
constructor(message, kind, status) {
|
|
293
|
+
super(message);
|
|
294
|
+
this.name = "TranslateProviderError";
|
|
295
|
+
this.kind = kind;
|
|
296
|
+
this.status = status;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
/** Defensively extract an HTTP status code from an unknown SDK error shape. */
|
|
300
|
+
function extractStatus(error) {
|
|
301
|
+
if (error === null || typeof error !== "object") return void 0;
|
|
302
|
+
const e = error;
|
|
303
|
+
if (typeof e.status === "number") return e.status;
|
|
304
|
+
if (e.response !== null && typeof e.response === "object") {
|
|
305
|
+
const status = e.response.status;
|
|
306
|
+
if (typeof status === "number") return status;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Classify an SDK error into a TranslateProviderError:
|
|
311
|
+
* 401/403 → auth, 429 → rate-limit, anything else → provider.
|
|
312
|
+
* Already-classified errors pass through unchanged.
|
|
313
|
+
*/
|
|
314
|
+
function classifyProviderError(error) {
|
|
315
|
+
if (error instanceof TranslateProviderError) return error;
|
|
316
|
+
const status = extractStatus(error);
|
|
317
|
+
return new TranslateProviderError(error instanceof Error ? error.message : String(error), status === 401 || status === 403 ? "auth" : status === 429 ? "rate-limit" : "provider", status);
|
|
318
|
+
}
|
|
319
|
+
/** Environment variable carrying the provider base URL override. */
|
|
320
|
+
const BASE_URL_ENV = "I18N_BASE_URL";
|
|
321
|
+
/**
|
|
322
|
+
* Resolve the provider base URL from its three sources, highest precedence
|
|
323
|
+
* first: an explicit flag, the I18N_BASE_URL env var, then the project
|
|
324
|
+
* config's `providerBaseUrl`.
|
|
325
|
+
*
|
|
326
|
+
* Blank values count as unset. Shells produce them routinely — `--baseUrl
|
|
327
|
+
* "$UNSET"` or an exported-but-empty variable — and a blank must not shadow a
|
|
328
|
+
* real endpoint configured further down the chain. A blank in the config file
|
|
329
|
+
* is a different case: it cannot arise by accident, so the strict schema
|
|
330
|
+
* rejects it at load time rather than letting it reach this function.
|
|
331
|
+
*/
|
|
332
|
+
function resolveProviderBaseUrl(sources) {
|
|
333
|
+
for (const value of [
|
|
334
|
+
sources.flag,
|
|
335
|
+
sources.env,
|
|
336
|
+
sources.config
|
|
337
|
+
]) if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
338
|
+
}
|
|
339
|
+
const ENV_KEY_MAP = {
|
|
340
|
+
openai: "OPENAI_API_KEY",
|
|
341
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
342
|
+
google: "GEMINI_API_KEY"
|
|
343
|
+
};
|
|
344
|
+
function resolveApiKey(provider, configKey) {
|
|
345
|
+
if (configKey) return configKey;
|
|
346
|
+
const envKey = ENV_KEY_MAP[provider];
|
|
347
|
+
const key = process.env[envKey];
|
|
348
|
+
if (!key) throw new Error(`No API key found for provider "${provider}". Set ${envKey} environment variable or pass apiKey in config.`);
|
|
349
|
+
return key;
|
|
350
|
+
}
|
|
351
|
+
async function createOpenAiTranslateFn(config) {
|
|
352
|
+
let OpenAI;
|
|
353
|
+
try {
|
|
354
|
+
OpenAI = (await import("openai")).default;
|
|
355
|
+
} catch {
|
|
356
|
+
throw new Error("Provider \"openai\" requires the \"openai\" package. Install it with:\n npm install openai\n pnpm add openai\n yarn add openai");
|
|
357
|
+
}
|
|
358
|
+
const apiKey = resolveApiKey("openai", config.apiKey);
|
|
359
|
+
const client = new OpenAI({
|
|
360
|
+
apiKey,
|
|
361
|
+
baseURL: config.baseUrl
|
|
362
|
+
});
|
|
363
|
+
return async (opts) => {
|
|
364
|
+
let response;
|
|
365
|
+
try {
|
|
366
|
+
response = await client.chat.completions.create({
|
|
367
|
+
model: config.model,
|
|
368
|
+
messages: [{
|
|
369
|
+
role: "system",
|
|
370
|
+
content: opts.systemPrompt
|
|
371
|
+
}, {
|
|
372
|
+
role: "user",
|
|
373
|
+
content: opts.userMessage
|
|
374
|
+
}],
|
|
375
|
+
max_tokens: opts.maxTokens,
|
|
376
|
+
temperature: 0,
|
|
377
|
+
response_format: { type: "json_object" }
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
throw classifyProviderError(error);
|
|
381
|
+
}
|
|
382
|
+
const choice = response.choices?.[0];
|
|
383
|
+
const text = choice?.message?.content ?? "";
|
|
384
|
+
const truncated = choice?.finish_reason === "length";
|
|
385
|
+
return {
|
|
386
|
+
text,
|
|
387
|
+
model: response.model || config.model,
|
|
388
|
+
...truncated ? { truncated: true } : {}
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function createGoogleTranslateFn(config) {
|
|
393
|
+
let GoogleGenAI;
|
|
394
|
+
try {
|
|
395
|
+
GoogleGenAI = (await import("@google/genai")).GoogleGenAI;
|
|
396
|
+
} catch {
|
|
397
|
+
throw new Error("Provider \"google\" requires the \"@google/genai\" package. Install it with:\n npm install @google/genai\n pnpm add @google/genai\n yarn add @google/genai");
|
|
398
|
+
}
|
|
399
|
+
const apiKey = resolveApiKey("google", config.apiKey);
|
|
400
|
+
const client = new GoogleGenAI({ apiKey });
|
|
401
|
+
return async (opts) => {
|
|
402
|
+
let response;
|
|
403
|
+
try {
|
|
404
|
+
response = await client.models.generateContent({
|
|
405
|
+
model: config.model,
|
|
406
|
+
contents: opts.userMessage,
|
|
407
|
+
config: {
|
|
408
|
+
systemInstruction: opts.systemPrompt,
|
|
409
|
+
maxOutputTokens: opts.maxTokens,
|
|
410
|
+
temperature: 0,
|
|
411
|
+
responseMimeType: "application/json"
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
throw classifyProviderError(error);
|
|
416
|
+
}
|
|
417
|
+
const text = response.text ?? "";
|
|
418
|
+
const truncated = response.candidates?.[0]?.finishReason === "MAX_TOKENS";
|
|
419
|
+
return {
|
|
420
|
+
text,
|
|
421
|
+
model: response.modelVersion || config.model,
|
|
422
|
+
...truncated ? { truncated: true } : {}
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async function createAnthropicTranslateFn(config) {
|
|
427
|
+
let Anthropic;
|
|
428
|
+
try {
|
|
429
|
+
Anthropic = (await import("@anthropic-ai/sdk")).Anthropic;
|
|
430
|
+
} catch {
|
|
431
|
+
throw new Error("Provider \"anthropic\" requires the \"@anthropic-ai/sdk\" package. Install it with:\n npm install @anthropic-ai/sdk\n pnpm add @anthropic-ai/sdk\n yarn add @anthropic-ai/sdk");
|
|
432
|
+
}
|
|
433
|
+
const apiKey = resolveApiKey("anthropic", config.apiKey);
|
|
434
|
+
const client = new Anthropic({
|
|
435
|
+
apiKey,
|
|
436
|
+
baseURL: config.baseUrl
|
|
437
|
+
});
|
|
438
|
+
return async (opts) => {
|
|
439
|
+
let response;
|
|
440
|
+
try {
|
|
441
|
+
response = await client.messages.create({
|
|
442
|
+
model: config.model,
|
|
443
|
+
system: opts.systemPrompt,
|
|
444
|
+
messages: [{
|
|
445
|
+
role: "user",
|
|
446
|
+
content: opts.userMessage
|
|
447
|
+
}],
|
|
448
|
+
max_tokens: opts.maxTokens,
|
|
449
|
+
temperature: 0
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
throw classifyProviderError(error);
|
|
453
|
+
}
|
|
454
|
+
const text = (response.content?.find((block) => block.type === "text"))?.text ?? "";
|
|
455
|
+
const truncated = response.stop_reason === "max_tokens";
|
|
456
|
+
return {
|
|
457
|
+
text,
|
|
458
|
+
model: response.model || config.model,
|
|
459
|
+
...truncated ? { truncated: true } : {}
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Create a TranslateFn from an LLM provider config.
|
|
465
|
+
* Throws if the provider SDK is not installed or API key is missing.
|
|
466
|
+
*/
|
|
467
|
+
async function createTranslateFn(config) {
|
|
468
|
+
if (config.provider === "google" && config.baseUrl) throw new TranslateProviderError(`Provider "google" does not support a custom base URL. Remove --baseUrl / ${BASE_URL_ENV} / providerBaseUrl, or use an OpenAI-compatible provider.`, "config");
|
|
469
|
+
switch (config.provider) {
|
|
470
|
+
case "openai": return createOpenAiTranslateFn(config);
|
|
471
|
+
case "anthropic": return createAnthropicTranslateFn(config);
|
|
472
|
+
case "google": return createGoogleTranslateFn(config);
|
|
473
|
+
default: {
|
|
474
|
+
const _exhaustive = config.provider;
|
|
475
|
+
throw new Error(`Unknown provider: ${_exhaustive}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
export { resolveProviderBaseUrl as a, validateProjectConfig as c, createTranslateFn as i, log as l, TranslateProviderError as n, CONFIG_FILENAME as o, classifyProviderError as r, loadProjectConfig as s, BASE_URL_ENV as t };
|
|
481
|
+
|
|
482
|
+
//# sourceMappingURL=providers-BAwPjeCy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers-BAwPjeCy.js","names":[],"sources":["../src/utils/logger.ts","../src/config/schema.ts","../src/config/typed-config.ts","../src/config/project-config.ts","../src/llm/providers.ts"],"sourcesContent":["import { createConsola } from 'consola'\n\n// All diagnostics go to stderr — stdout is reserved for command results so\n// that `the-i18n-cli … | jq` and CI `$(…)` captures always receive pure JSON.\nexport const log = createConsola({\n stdout: process.stderr,\n stderr: process.stderr,\n}).withTag('the-i18n-cli')\n","/**\n * The one definition of the declared half of the kit's configuration —\n * everything a human tells the tool, as opposed to what it can see for itself.\n *\n * `.i18n-mcp.json`, `i18n-kit.config.ts` and the `i18nKit` block in\n * `nuxt.config.ts` all accept this same shape, and all validate against this\n * same schema. The `ProjectConfig` interface in `./types.ts` documents it;\n * the guard at the bottom of this file fails the build if the two drift.\n */\nimport { z } from 'zod'\nimport type { ProjectConfig } from './types.js'\nimport { log } from '../utils/logger.js'\n\nconst nonEmptyString = z.string().min(1).refine(s => s.trim().length > 0, 'Must not be empty or whitespace-only')\n\nconst layerRuleSchema = z.object({\n layer: z.string(),\n description: z.string(),\n when: z.string(),\n})\n\nconst localeDirEntrySchema = z.union([\n nonEmptyString,\n z.object({\n path: nonEmptyString,\n layer: nonEmptyString,\n }),\n])\n\nconst projectConfigSchema = z.object({\n $schema: z.string().optional(),\n framework: z.string().optional(),\n context: z.string().optional(),\n layerRules: z.array(layerRuleSchema).optional(),\n glossary: z.record(z.string(), z.string()).optional(),\n translationPrompt: z.string().optional(),\n localeNotes: z.record(z.string(), z.string()).optional(),\n examples: z.array(z.record(z.string(), z.string())).optional(),\n orphanScan: z.record(z.string(), z.object({\n ignorePatterns: z.array(z.string()).optional(),\n }).passthrough()).optional(), // passthrough for backwards-compat with deprecated keys like includeParentLayer\n localeDirs: z.array(localeDirEntrySchema).optional(),\n defaultLocale: nonEmptyString.optional(),\n locales: z.array(nonEmptyString).optional(),\n protectedLocales: z.array(nonEmptyString).optional(),\n reportOutput: z.union([z.literal(true), nonEmptyString]).optional(),\n localeFileFormat: z.enum(['json', 'php-array']).optional(),\n providerBaseUrl: nonEmptyString.optional(),\n // Deprecated with the removal of MCP sampling: accepted so existing config\n // files keep validating (the schema is strict), warned about, and ignored.\n samplingPreferences: z.unknown().optional(),\n}).strict()\n\n/**\n * Validate a candidate project config against the published schema, returning\n * the formatted issue list rather than throwing. `init` uses this to refuse to\n * emit a config the tool would then reject; the loaders turn the same failure\n * into a ConfigError naming the file it came from.\n */\nexport function validateProjectConfig(\n value: unknown,\n): { ok: true, data: ProjectConfig } | { ok: false, error: string } {\n const result = projectConfigSchema.safeParse(value)\n if (result.success) return { ok: true, data: result.data as ProjectConfig }\n\n const error = result.error.issues\n .map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join('.') : '(root)'\n return ` ${path}: ${issue.message}`\n })\n .join('\\n')\n return { ok: false, error }\n}\n\n/**\n * Strip the deprecated keys the schema still accepts, warning once, so that a\n * config keeps validating without the value going on to mean anything. Applied\n * by every loader — a key ignored in `.i18n-mcp.json` but silently honoured in\n * `i18n-kit.config.ts` would be exactly the divergence this file exists to\n * prevent.\n */\nexport function dropDeprecatedKeys(config: ProjectConfig, source: string): ProjectConfig {\n const data = config as ProjectConfig & { samplingPreferences?: unknown }\n if ('samplingPreferences' in data) {\n log.warn(\n `${source}: \"samplingPreferences\" is deprecated and ignored — `\n + 'MCP sampling was removed. Configure a provider (e.g. I18N_PROVIDER/I18N_MODEL) instead.',\n )\n delete data.samplingPreferences\n }\n return data\n}\n\n// ─── Drift guard ────────────────────────────────────────────────\n//\n// A key added to the interface but not the schema is rejected at runtime as an\n// unknown key (the schema is strict) — a typed config that the editor accepts\n// and the CLI refuses. A key added to the schema but not the interface is\n// accepted at runtime and invisible in an editor. Both are the drift this\n// module exists to prevent, so both are a type error here.\n//\n// `$schema` is JSON-only, and `samplingPreferences` is accepted purely so old\n// files keep validating; neither belongs in the documented type.\n\ntype SchemaKey = Exclude<keyof z.infer<typeof projectConfigSchema>, '$schema' | 'samplingPreferences'>\n\n/** Fails to compile unless `T` is `never`, naming the offending key when not. */\ntype NoDrift<T extends never> = T\n\ntype _SchemaCoversType = NoDrift<Exclude<keyof ProjectConfig, SchemaKey>>\ntype _TypeCoversSchema = NoDrift<Exclude<SchemaKey, keyof ProjectConfig>>\n","/**\n * Loading `i18n-kit.config.ts` — the same configuration `.i18n-mcp.json`\n * carries, written in TypeScript so the editor checks it.\n *\n * Read with no build step, which is the whole point: a `protectedLocales` entry\n * that only takes effect once something has been built is a locale that quietly\n * goes unprotected in a fresh checkout. Everything here runs off the source\n * file as it sits on disk.\n */\nimport { existsSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { ProjectConfig } from './types.js'\nimport { dropDeprecatedKeys, validateProjectConfig } from './schema.js'\nimport { ConfigError, toErrorMessage } from '../utils/errors.js'\nimport { log } from '../utils/logger.js'\n\n/**\n * Candidate names, in the order a directory is searched. `.ts` first because\n * it is the one worth having; the rest exist so a JavaScript project is not\n * told to adopt TypeScript to get a typed config.\n */\nconst TYPED_CONFIG_FILENAMES = [\n 'i18n-kit.config.ts',\n 'i18n-kit.config.mts',\n 'i18n-kit.config.js',\n 'i18n-kit.config.mjs',\n 'i18n-kit.config.cjs',\n 'i18n-kit.config.cts',\n] as const\n\n/**\n * Walk up from `startDir` for the nearest `i18n-kit.config.*`, the same way\n * `findConfigFile` looks for `.i18n-mcp.json`. Both stop at the first\n * directory that has one, so a config in an app directory shadows one in the\n * repository root rather than merging with it.\n */\nexport function findTypedConfigFile(startDir: string): string | null {\n // Absolute from the outset: a relative `--projectDir` would otherwise yield\n // a relative config path, and jiti reads a relative path as a bare module\n // specifier — resolved against node_modules, where it is never found.\n let dir = resolve(startDir)\n while (true) {\n const found = TYPED_CONFIG_FILENAMES\n .map(name => join(dir, name))\n .filter(path => existsSync(path))\n\n if (found[0]) {\n if (found.length > 1) {\n log.warn(\n `Several i18n-kit configs in ${dir} (${found.map(p => p.slice(dir.length + 1)).join(', ')}). `\n + `Using ${found[0].slice(dir.length + 1)} — delete the others, they are being ignored.`,\n )\n }\n return found[0]\n }\n\n const parent = dirname(dir)\n if (parent === dir) break // filesystem root\n dir = parent\n }\n return null\n}\n\n/**\n * Load and validate the nearest typed config, or null when there isn't one.\n *\n * Throws, unlike the framework-config readers this issue also adds. Those read\n * a file someone wrote for another tool, so failing to read it means falling\n * back to a guess. This file exists only for the kit: if it throws, or declares\n * a key that does not exist, carrying on without it is precisely the silent\n * non-application the typed config was added to prevent.\n */\nexport async function loadTypedConfig(\n projectDir: string,\n): Promise<{ path: string, config: ProjectConfig } | null> {\n const path = findTypedConfigFile(projectDir)\n if (!path) {\n log.debug(`No i18n-kit.config.* found in ${projectDir} or any parent directory — skipping`)\n return null\n }\n\n log.info(`Found typed project config: ${path}`)\n\n let module: unknown\n try {\n const { createJiti } = await import('jiti')\n // interopDefault stays off so that a file with only named exports is\n // distinguishable from one with a default: with the interop on, the whole\n // namespace comes back and the schema rejects it as a config full of\n // unknown keys, which explains nothing.\n const jiti = createJiti(import.meta.url, {\n interopDefault: false,\n alias: selfAlias(),\n })\n module = await jiti.import(path)\n }\n catch (error) {\n throw new ConfigError(\n `Failed to load ${path}: ${toErrorMessage(error)}`,\n )\n }\n\n if (module === null || typeof module !== 'object') {\n throw new ConfigError(\n `${path} must export its config as the default export — `\n + 'write `export default defineI18nKitConfig({ ... })`.',\n )\n }\n\n // In a `.cts` file `module.exports` comes back as the module itself, with no\n // `default` to unwrap — so for the CommonJS spellings, that object *is* the\n // config. Everywhere else a missing `default` means a file of named exports,\n // and saying so beats letting the schema report a config full of unknown keys.\n const commonJs = path.endsWith('.cts') || path.endsWith('.cjs')\n if (!('default' in module) && !commonJs) {\n throw new ConfigError(\n `${path} must export its config as the default export — `\n + 'write `export default defineI18nKitConfig({ ... })`.',\n )\n }\n\n const exported = 'default' in module ? (module as { default: unknown }).default : module\n\n if (exported === null || typeof exported !== 'object' || Array.isArray(exported)) {\n throw new ConfigError(\n `${path} must export a config object as its default export — got ${describe(exported)}. `\n + 'Wrap it in defineI18nKitConfig() from \"the-i18n-cli/config\".',\n )\n }\n\n const result = validateProjectConfig(exported)\n if (!result.ok) {\n throw new ConfigError(`Invalid ${path}:\\n${result.error}`)\n }\n\n return { path, config: dropDeprecatedKeys(result.data, path) }\n}\n\n/**\n * Point `the-i18n-cli/config` at the copy of `defineI18nKitConfig` that is\n * already running.\n *\n * A config file imports it for the types, and types only need the package\n * installed at author time — but the CLI is often run through `npx` in a\n * project that never added it as a dependency, where a bare import would be\n * unresolvable and take the whole config down with it. Resolving it to\n * ourselves costs nothing and is the same function either way: identity.\n *\n * Returns no alias when the entry cannot be located, which leaves normal\n * resolution to find the installed package.\n */\nfunction selfAlias(): Record<string, string> {\n // `./` when running from the bundle, where define-config.js sits beside the\n // chunk this code ends up in; `../` when running from source, as tests do.\n for (const candidate of ['./define-config.js', '../define-config.js', '../define-config.ts']) {\n const path = fileURLToPath(new URL(candidate, import.meta.url))\n if (existsSync(path)) return { 'the-i18n-cli/config': path }\n }\n return {}\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'an array'\n if (typeof value === 'function') return 'a function (call defineI18nKitConfig, do not pass it)'\n return typeof value\n}\n","import { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join, dirname } from 'node:path'\nimport type { ProjectConfig } from './types.js'\nimport { dropDeprecatedKeys, validateProjectConfig } from './schema.js'\nimport { loadTypedConfig } from './typed-config.js'\nimport { ConfigError, toErrorMessage } from '../utils/errors.js'\nimport { log } from '../utils/logger.js'\n\nexport const CONFIG_FILENAME = '.i18n-mcp.json'\n\n// The schema itself lives in ./schema.ts, shared with the typed-config loader.\n// Re-exported here because this has been its import path since before there\n// was a second config format.\nexport { validateProjectConfig } from './schema.js'\n\n// ─── Config file discovery ──────────────────────────────────────\n\n/**\n * Walk up the directory tree from `startDir` to find the nearest config file.\n * Returns the full path if found, null otherwise.\n */\nexport function findConfigFile(startDir: string): string | null {\n let dir = startDir\n while (true) {\n const candidate = join(dir, CONFIG_FILENAME)\n if (existsSync(candidate)) {\n return candidate\n }\n const parent = dirname(dir)\n if (parent === dir) break // filesystem root\n dir = parent\n }\n return null\n}\n\n/**\n * Load the project's declared configuration, from either place it can be\n * declared: `i18n-kit.config.ts` and `.i18n-mcp.json`. Both are searched from\n * projectDir upwards, the way ESLint, Prettier and tsconfig resolve configs.\n *\n * Returns null when neither exists — the case that must keep behaving exactly\n * as it did before there were two. Throws ConfigError when either file is\n * present but unusable, or when the two disagree.\n *\n * Every adapter funnels through here, which is what makes the typed config\n * work for all of them rather than for whichever one was taught about it.\n */\nexport async function loadProjectConfig(projectDir: string): Promise<ProjectConfig | null> {\n const [json, typed] = await Promise.all([\n loadJsonConfig(projectDir),\n loadTypedConfig(projectDir),\n ])\n\n if (!typed) return json?.config ?? null\n if (!json) return typed.config\n\n const conflicts = conflictingKeys(typed.config, json.config)\n if (conflicts.length > 0) {\n throw new ConfigError(\n `${conflicts.join(', ')} ${conflicts.length === 1 ? 'is' : 'are'} declared in both\\n`\n + ` ${typed.path}\\n`\n + ` ${json.path}\\n`\n + 'Neither wins — a reader cannot tell which one took effect. '\n + 'Delete the duplicate from one of them.',\n )\n }\n\n // Disjoint by the check above, so the spread order only decides where\n // untouched keys come from, not who wins.\n return { ...json.config, ...typed.config }\n}\n\n/** Keys present in both files, ignoring the JSON-only `$schema`. */\nfunction conflictingKeys(typed: ProjectConfig, json: ProjectConfig): string[] {\n const a = typed as Record<string, unknown>\n const b = json as Record<string, unknown>\n return Object.keys(a).filter(key => key !== '$schema' && a[key] !== undefined && b[key] !== undefined)\n}\n\n/**\n * Load the optional `.i18n-mcp.json`, with the path it was found at.\n * Returns null if no config file is found in any ancestor.\n * Throws ConfigError if a file is found but is invalid.\n */\nasync function loadJsonConfig(\n projectDir: string,\n): Promise<{ path: string, config: ProjectConfig } | null> {\n log.debug(`Searching for ${CONFIG_FILENAME} starting from: ${projectDir}`)\n\n const configPath = findConfigFile(projectDir)\n\n if (!configPath) {\n log.info(`No ${CONFIG_FILENAME} found in ${projectDir} or any parent directory — skipping`)\n return null\n }\n\n log.info(`Found project config: ${configPath}`)\n\n let raw: string\n try {\n raw = await readFile(configPath, 'utf-8')\n } catch (error) {\n throw new ConfigError(\n `Failed to read ${CONFIG_FILENAME}: ${toErrorMessage(error)}`,\n )\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (error) {\n throw new ConfigError(\n `Invalid JSON in ${CONFIG_FILENAME}: ${toErrorMessage(error)}`,\n )\n }\n\n const result = validateProjectConfig(parsed)\n\n if (!result.ok) {\n throw new ConfigError(`Invalid ${CONFIG_FILENAME}:\\n${result.error}`)\n }\n\n log.debug(`Project config loaded successfully from ${configPath}`)\n\n return { path: configPath, config: dropDeprecatedKeys(result.data, CONFIG_FILENAME) }\n}\n","import type { TranslateFn, TranslateRequest, TranslateResponse } from '../core/types.js'\n\nexport type LlmProvider = 'openai' | 'anthropic' | 'google'\n\n// ─── Error classification ───────────────────────────────────────\n\n/** How a provider failure should be handled by the caller. */\nexport type TranslateProviderErrorKind = 'auth' | 'rate-limit' | 'provider' | 'config'\n\n/**\n * A classified provider failure. `auth` errors are not retryable (the caller\n * should abort the run), `rate-limit` errors should be retried with backoff,\n * and `provider` covers everything else (server errors, network, …).\n * `config` marks an unusable provider setup and is raised while building the\n * TranslateFn, before any request exists — so it surfaces from the command\n * and never reaches the retry loop.\n */\nexport class TranslateProviderError extends Error {\n public readonly kind: TranslateProviderErrorKind\n public readonly status?: number\n\n constructor(message: string, kind: TranslateProviderErrorKind, status?: number) {\n super(message)\n this.name = 'TranslateProviderError'\n this.kind = kind\n this.status = status\n }\n}\n\n/** Defensively extract an HTTP status code from an unknown SDK error shape. */\nfunction extractStatus(error: unknown): number | undefined {\n if (error === null || typeof error !== 'object') return undefined\n const e = error as { status?: unknown, response?: unknown }\n if (typeof e.status === 'number') return e.status\n if (e.response !== null && typeof e.response === 'object') {\n const status = (e.response as { status?: unknown }).status\n if (typeof status === 'number') return status\n }\n return undefined\n}\n\n/**\n * Classify an SDK error into a TranslateProviderError:\n * 401/403 → auth, 429 → rate-limit, anything else → provider.\n * Already-classified errors pass through unchanged.\n */\nexport function classifyProviderError(error: unknown): TranslateProviderError {\n if (error instanceof TranslateProviderError) return error\n const status = extractStatus(error)\n const message = error instanceof Error ? error.message : String(error)\n const kind: TranslateProviderErrorKind\n = status === 401 || status === 403 ? 'auth'\n : status === 429 ? 'rate-limit'\n : 'provider'\n return new TranslateProviderError(message, kind, status)\n}\n\nexport interface LlmProviderConfig {\n provider: LlmProvider\n model: string\n /** Override API key. Falls back to env vars */\n apiKey?: string\n /** Base URL override for proxies / compatible APIs */\n baseUrl?: string\n}\n\n/** Environment variable carrying the provider base URL override. */\nexport const BASE_URL_ENV = 'I18N_BASE_URL'\n\n/**\n * Resolve the provider base URL from its three sources, highest precedence\n * first: an explicit flag, the I18N_BASE_URL env var, then the project\n * config's `providerBaseUrl`.\n *\n * Blank values count as unset. Shells produce them routinely — `--baseUrl\n * \"$UNSET\"` or an exported-but-empty variable — and a blank must not shadow a\n * real endpoint configured further down the chain. A blank in the config file\n * is a different case: it cannot arise by accident, so the strict schema\n * rejects it at load time rather than letting it reach this function.\n */\nexport function resolveProviderBaseUrl(sources: {\n flag?: string\n env?: string\n config?: string\n}): string | undefined {\n for (const value of [sources.flag, sources.env, sources.config]) {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n }\n return undefined\n}\n\nconst ENV_KEY_MAP: Record<LlmProvider, string> = {\n openai: 'OPENAI_API_KEY',\n anthropic: 'ANTHROPIC_API_KEY',\n google: 'GEMINI_API_KEY',\n}\n\nfunction resolveApiKey(provider: LlmProvider, configKey?: string): string {\n if (configKey) return configKey\n const envKey = ENV_KEY_MAP[provider]\n const key = process.env[envKey]\n if (!key) {\n throw new Error(\n `No API key found for provider \"${provider}\". Set ${envKey} environment variable or pass apiKey in config.`,\n )\n }\n return key\n}\n\nasync function createOpenAiTranslateFn(config: LlmProviderConfig): Promise<TranslateFn> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import of optional peer dep\n let OpenAI: any\n try {\n OpenAI = (await import('openai')).default as any // eslint-disable-line @typescript-eslint/no-explicit-any\n } catch {\n throw new Error(\n 'Provider \"openai\" requires the \"openai\" package. Install it with:\\n npm install openai\\n pnpm add openai\\n yarn add openai',\n )\n }\n\n const apiKey = resolveApiKey('openai', config.apiKey)\n const client = new OpenAI({ apiKey, baseURL: config.baseUrl })\n\n return async (opts: TranslateRequest): Promise<TranslateResponse> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK types not available\n let response: any\n try {\n response = await client.chat.completions.create({\n model: config.model,\n messages: [\n { role: 'system', content: opts.systemPrompt },\n { role: 'user', content: opts.userMessage },\n ],\n max_tokens: opts.maxTokens,\n temperature: 0,\n response_format: { type: 'json_object' },\n })\n } catch (error) {\n throw classifyProviderError(error)\n }\n\n const choice = response.choices?.[0]\n const text = choice?.message?.content ?? ''\n const truncated = choice?.finish_reason === 'length'\n return { text, model: response.model || config.model, ...(truncated ? { truncated: true } : {}) }\n }\n}\n\nasync function createGoogleTranslateFn(config: LlmProviderConfig): Promise<TranslateFn> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import of optional peer dep\n let GoogleGenAI: any\n try {\n GoogleGenAI = (await import('@google/genai')).GoogleGenAI as any // eslint-disable-line @typescript-eslint/no-explicit-any\n } catch {\n throw new Error(\n 'Provider \"google\" requires the \"@google/genai\" package. Install it with:\\n npm install @google/genai\\n pnpm add @google/genai\\n yarn add @google/genai',\n )\n }\n\n const apiKey = resolveApiKey('google', config.apiKey)\n const client = new GoogleGenAI({ apiKey })\n\n return async (opts: TranslateRequest): Promise<TranslateResponse> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK types not available\n let response: any\n try {\n response = await client.models.generateContent({\n model: config.model,\n contents: opts.userMessage,\n config: {\n systemInstruction: opts.systemPrompt,\n maxOutputTokens: opts.maxTokens,\n temperature: 0,\n responseMimeType: 'application/json',\n },\n })\n } catch (error) {\n throw classifyProviderError(error)\n }\n\n const text = response.text ?? ''\n const truncated = response.candidates?.[0]?.finishReason === 'MAX_TOKENS'\n return { text, model: response.modelVersion || config.model, ...(truncated ? { truncated: true } : {}) }\n }\n}\n\nasync function createAnthropicTranslateFn(config: LlmProviderConfig): Promise<TranslateFn> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import of optional peer dep\n let Anthropic: any\n try {\n Anthropic = (await import('@anthropic-ai/sdk')).Anthropic as any // eslint-disable-line @typescript-eslint/no-explicit-any\n } catch {\n throw new Error(\n 'Provider \"anthropic\" requires the \"@anthropic-ai/sdk\" package. Install it with:\\n npm install @anthropic-ai/sdk\\n pnpm add @anthropic-ai/sdk\\n yarn add @anthropic-ai/sdk',\n )\n }\n\n const apiKey = resolveApiKey('anthropic', config.apiKey)\n const client = new Anthropic({ apiKey, baseURL: config.baseUrl })\n\n return async (opts: TranslateRequest): Promise<TranslateResponse> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK types not available\n let response: any\n try {\n response = await client.messages.create({\n model: config.model,\n system: opts.systemPrompt,\n messages: [{ role: 'user', content: opts.userMessage }],\n max_tokens: opts.maxTokens,\n temperature: 0,\n })\n } catch (error) {\n throw classifyProviderError(error)\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK types not available\n const textBlock = response.content?.find((block: any) => block.type === 'text')\n const text = textBlock?.text ?? ''\n const truncated = response.stop_reason === 'max_tokens'\n return { text, model: response.model || config.model, ...(truncated ? { truncated: true } : {}) }\n }\n}\n\n/**\n * Create a TranslateFn from an LLM provider config.\n * Throws if the provider SDK is not installed or API key is missing.\n */\nexport async function createTranslateFn(config: LlmProviderConfig): Promise<TranslateFn> {\n // @google/genai exposes no endpoint override, so a base URL here would be\n // silently ignored and translate against the wrong endpoint. Refuse loudly.\n if (config.provider === 'google' && config.baseUrl) {\n throw new TranslateProviderError(\n 'Provider \"google\" does not support a custom base URL. '\n + `Remove --baseUrl / ${BASE_URL_ENV} / providerBaseUrl, or use an OpenAI-compatible provider.`,\n 'config',\n )\n }\n\n switch (config.provider) {\n case 'openai':\n return createOpenAiTranslateFn(config)\n case 'anthropic':\n return createAnthropicTranslateFn(config)\n case 'google':\n return createGoogleTranslateFn(config)\n default: {\n const _exhaustive: never = config.provider\n throw new Error(`Unknown provider: ${_exhaustive}`)\n }\n }\n}\n"],"mappings":";;;;;;;;AAIA,MAAa,MAAM,cAAc;CAC/B,QAAQ,QAAQ;CAChB,QAAQ,QAAQ;CACjB,CAAC,CAAC,QAAQ,eAAe;;;;;;;;;;;;ACM1B,MAAM,iBAAiB,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAO,MAAK,EAAE,MAAM,CAAC,SAAS,GAAG,uCAAuC;AAEjH,MAAM,kBAAkB,EAAE,OAAO;CAC/B,OAAO,EAAE,QAAQ;CACjB,aAAa,EAAE,QAAQ;CACvB,MAAM,EAAE,QAAQ;CACjB,CAAC;AAEF,MAAM,uBAAuB,EAAE,MAAM,CACnC,gBACA,EAAE,OAAO;CACP,MAAM;CACN,OAAO;CACR,CAAC,CACH,CAAC;AAEF,MAAM,sBAAsB,EAAE,OAAO;CACnC,SAAS,EAAE,QAAQ,CAAC,UAAU;CAC9B,WAAW,EAAE,QAAQ,CAAC,UAAU;CAChC,SAAS,EAAE,QAAQ,CAAC,UAAU;CAC9B,YAAY,EAAE,MAAM,gBAAgB,CAAC,UAAU;CAC/C,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,UAAU;CACrD,mBAAmB,EAAE,QAAQ,CAAC,UAAU;CACxC,aAAa,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,UAAU;CACxD,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU;CAC9D,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,OAAO,EACxC,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,EAC/C,CAAC,CAAC,aAAa,CAAC,CAAC,UAAU;CAC5B,YAAY,EAAE,MAAM,qBAAqB,CAAC,UAAU;CACpD,eAAe,eAAe,UAAU;CACxC,SAAS,EAAE,MAAM,eAAe,CAAC,UAAU;CAC3C,kBAAkB,EAAE,MAAM,eAAe,CAAC,UAAU;CACpD,cAAc,EAAE,MAAM,CAAC,EAAE,QAAQ,KAAK,EAAE,eAAe,CAAC,CAAC,UAAU;CACnE,kBAAkB,EAAE,KAAK,CAAC,QAAQ,YAAY,CAAC,CAAC,UAAU;CAC1D,iBAAiB,eAAe,UAAU;CAG1C,qBAAqB,EAAE,SAAS,CAAC,UAAU;CAC5C,CAAC,CAAC,QAAQ;;;;;;;AAQX,SAAgB,sBACd,OACkE;CAClE,MAAM,SAAS,oBAAoB,UAAU,MAAM;AACnD,KAAI,OAAO,QAAS,QAAO;EAAE,IAAI;EAAM,MAAM,OAAO;EAAuB;AAQ3E,QAAO;EAAE,IAAI;EAAO,OANN,OAAO,MAAM,OACxB,KAAK,UAAU;AAEd,UAAO,KADM,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,IAAI,GAAG,SAC3C,IAAI,MAAM;IAC3B,CACD,KAAK,KAAK;EACc;;;;;;;;;AAU7B,SAAgB,mBAAmB,QAAuB,QAA+B;CACvF,MAAM,OAAO;AACb,KAAI,yBAAyB,MAAM;AACjC,MAAI,KACF,GAAG,OAAO,6IAEX;AACD,SAAO,KAAK;;AAEd,QAAO;;;;;;;;;;;;;;;;;;ACpET,MAAM,yBAAyB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;AAQD,SAAgB,oBAAoB,UAAiC;CAInE,IAAI,MAAM,QAAQ,SAAS;AAC3B,QAAO,MAAM;EACX,MAAM,QAAQ,uBACX,KAAI,SAAQ,KAAK,KAAK,KAAK,CAAC,CAC5B,QAAO,SAAQ,WAAW,KAAK,CAAC;AAEnC,MAAI,MAAM,IAAI;AACZ,OAAI,MAAM,SAAS,EACjB,KAAI,KACF,+BAA+B,IAAI,IAAI,MAAM,KAAI,MAAK,EAAE,MAAM,IAAI,SAAS,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,WAC/E,MAAM,GAAG,MAAM,IAAI,SAAS,EAAE,CAAC,+CAC3C;AAEH,UAAO,MAAM;;EAGf,MAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,WAAW,IAAK;AACpB,QAAM;;AAER,QAAO;;;;;;;;;;;AAYT,eAAsB,gBACpB,YACyD;CACzD,MAAM,OAAO,oBAAoB,WAAW;AAC5C,KAAI,CAAC,MAAM;AACT,MAAI,MAAM,iCAAiC,WAAW,qCAAqC;AAC3F,SAAO;;AAGT,KAAI,KAAK,+BAA+B,OAAO;CAE/C,IAAI;AACJ,KAAI;EACF,MAAM,EAAE,eAAe,MAAM,OAAO;AASpC,WAAS,MAJI,WAAW,OAAO,KAAK,KAAK;GACvC,gBAAgB;GAChB,OAAO,WAAW;GACnB,CAAC,CACkB,OAAO,KAAK;UAE3B,OAAO;AACZ,QAAM,IAAI,YACR,kBAAkB,KAAK,IAAI,eAAe,MAAM,GACjD;;AAGH,KAAI,WAAW,QAAQ,OAAO,WAAW,SACvC,OAAM,IAAI,YACR,GAAG,KAAK,wGAET;CAOH,MAAM,WAAW,KAAK,SAAS,OAAO,IAAI,KAAK,SAAS,OAAO;AAC/D,KAAI,EAAE,aAAa,WAAW,CAAC,SAC7B,OAAM,IAAI,YACR,GAAG,KAAK,wGAET;CAGH,MAAM,WAAW,aAAa,SAAU,OAAgC,UAAU;AAElF,KAAI,aAAa,QAAQ,OAAO,aAAa,YAAY,MAAM,QAAQ,SAAS,CAC9E,OAAM,IAAI,YACR,GAAG,KAAK,2DAA2D,SAAS,SAAS,CAAC,gEAEvF;CAGH,MAAM,SAAS,sBAAsB,SAAS;AAC9C,KAAI,CAAC,OAAO,GACV,OAAM,IAAI,YAAY,WAAW,KAAK,KAAK,OAAO,QAAQ;AAG5D,QAAO;EAAE;EAAM,QAAQ,mBAAmB,OAAO,MAAM,KAAK;EAAE;;;;;;;;;;;;;;;AAgBhE,SAAS,YAAoC;AAG3C,MAAK,MAAM,aAAa;EAAC;EAAsB;EAAuB;EAAsB,EAAE;EAC5F,MAAM,OAAO,cAAc,IAAI,IAAI,WAAW,OAAO,KAAK,IAAI,CAAC;AAC/D,MAAI,WAAW,KAAK,CAAE,QAAO,EAAE,uBAAuB,MAAM;;AAE9D,QAAO,EAAE;;AAGX,SAAS,SAAS,OAAwB;AACxC,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO;AACjC,KAAI,OAAO,UAAU,WAAY,QAAO;AACxC,QAAO,OAAO;;;;AC7JhB,MAAa,kBAAkB;;;;;AAa/B,SAAgB,eAAe,UAAiC;CAC9D,IAAI,MAAM;AACV,QAAO,MAAM;EACX,MAAM,YAAY,KAAK,KAAK,gBAAgB;AAC5C,MAAI,WAAW,UAAU,CACvB,QAAO;EAET,MAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,WAAW,IAAK;AACpB,QAAM;;AAER,QAAO;;;;;;;;;;;;;;AAeT,eAAsB,kBAAkB,YAAmD;CACzF,MAAM,CAAC,MAAM,SAAS,MAAM,QAAQ,IAAI,CACtC,eAAe,WAAW,EAC1B,gBAAgB,WAAW,CAC5B,CAAC;AAEF,KAAI,CAAC,MAAO,QAAO,MAAM,UAAU;AACnC,KAAI,CAAC,KAAM,QAAO,MAAM;CAExB,MAAM,YAAY,gBAAgB,MAAM,QAAQ,KAAK,OAAO;AAC5D,KAAI,UAAU,SAAS,EACrB,OAAM,IAAI,YACR,GAAG,UAAU,KAAK,KAAK,CAAC,GAAG,UAAU,WAAW,IAAI,OAAO,MAAM,uBAC1D,MAAM,KAAK,MACX,KAAK,KAAK,qGAGlB;AAKH,QAAO;EAAE,GAAG,KAAK;EAAQ,GAAG,MAAM;EAAQ;;;AAI5C,SAAS,gBAAgB,OAAsB,MAA+B;CAC5E,MAAM,IAAI;CACV,MAAM,IAAI;AACV,QAAO,OAAO,KAAK,EAAE,CAAC,QAAO,QAAO,QAAQ,aAAa,EAAE,SAAS,KAAA,KAAa,EAAE,SAAS,KAAA,EAAU;;;;;;;AAQxG,eAAe,eACb,YACyD;AACzD,KAAI,MAAM,iBAAiB,gBAAgB,kBAAkB,aAAa;CAE1E,MAAM,aAAa,eAAe,WAAW;AAE7C,KAAI,CAAC,YAAY;AACf,MAAI,KAAK,MAAM,gBAAgB,YAAY,WAAW,qCAAqC;AAC3F,SAAO;;AAGT,KAAI,KAAK,yBAAyB,aAAa;CAE/C,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,SAAS,YAAY,QAAQ;UAClC,OAAO;AACd,QAAM,IAAI,YACR,kBAAkB,gBAAgB,IAAI,eAAe,MAAM,GAC5D;;CAGH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,OAAO;AACd,QAAM,IAAI,YACR,mBAAmB,gBAAgB,IAAI,eAAe,MAAM,GAC7D;;CAGH,MAAM,SAAS,sBAAsB,OAAO;AAE5C,KAAI,CAAC,OAAO,GACV,OAAM,IAAI,YAAY,WAAW,gBAAgB,KAAK,OAAO,QAAQ;AAGvE,KAAI,MAAM,2CAA2C,aAAa;AAElE,QAAO;EAAE,MAAM;EAAY,QAAQ,mBAAmB,OAAO,MAAM,gBAAgB;EAAE;;;;;;;;;;;;AC5GvF,IAAa,yBAAb,cAA4C,MAAM;CAChD;CACA;CAEA,YAAY,SAAiB,MAAkC,QAAiB;AAC9E,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,OAAO;AACZ,OAAK,SAAS;;;;AAKlB,SAAS,cAAc,OAAoC;AACzD,KAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO,KAAA;CACxD,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,WAAW,SAAU,QAAO,EAAE;AAC3C,KAAI,EAAE,aAAa,QAAQ,OAAO,EAAE,aAAa,UAAU;EACzD,MAAM,SAAU,EAAE,SAAkC;AACpD,MAAI,OAAO,WAAW,SAAU,QAAO;;;;;;;;AAU3C,SAAgB,sBAAsB,OAAwC;AAC5E,KAAI,iBAAiB,uBAAwB,QAAO;CACpD,MAAM,SAAS,cAAc,MAAM;AAMnC,QAAO,IAAI,uBALK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EAElE,WAAW,OAAO,WAAW,MAAM,SACjC,WAAW,MAAM,eACf,YACyC,OAAO;;;AAa1D,MAAa,eAAe;;;;;;;;;;;;AAa5B,SAAgB,uBAAuB,SAIhB;AACrB,MAAK,MAAM,SAAS;EAAC,QAAQ;EAAM,QAAQ;EAAK,QAAQ;EAAO,CAC7D,KAAI,OAAO,UAAU,YAAY,MAAM,MAAM,CAAC,SAAS,EAAG,QAAO,MAAM,MAAM;;AAKjF,MAAM,cAA2C;CAC/C,QAAQ;CACR,WAAW;CACX,QAAQ;CACT;AAED,SAAS,cAAc,UAAuB,WAA4B;AACxE,KAAI,UAAW,QAAO;CACtB,MAAM,SAAS,YAAY;CAC3B,MAAM,MAAM,QAAQ,IAAI;AACxB,KAAI,CAAC,IACH,OAAM,IAAI,MACR,kCAAkC,SAAS,SAAS,OAAO,iDAC5D;AAEH,QAAO;;AAGT,eAAe,wBAAwB,QAAiD;CAEtF,IAAI;AACJ,KAAI;AACF,YAAU,MAAM,OAAO,WAAW;SAC5B;AACN,QAAM,IAAI,MACR,oIACD;;CAGH,MAAM,SAAS,cAAc,UAAU,OAAO,OAAO;CACrD,MAAM,SAAS,IAAI,OAAO;EAAE;EAAQ,SAAS,OAAO;EAAS,CAAC;AAE9D,QAAO,OAAO,SAAuD;EAEnE,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,OAAO,KAAK,YAAY,OAAO;IAC9C,OAAO,OAAO;IACd,UAAU,CACR;KAAE,MAAM;KAAU,SAAS,KAAK;KAAc,EAC9C;KAAE,MAAM;KAAQ,SAAS,KAAK;KAAa,CAC5C;IACD,YAAY,KAAK;IACjB,aAAa;IACb,iBAAiB,EAAE,MAAM,eAAe;IACzC,CAAC;WACK,OAAO;AACd,SAAM,sBAAsB,MAAM;;EAGpC,MAAM,SAAS,SAAS,UAAU;EAClC,MAAM,OAAO,QAAQ,SAAS,WAAW;EACzC,MAAM,YAAY,QAAQ,kBAAkB;AAC5C,SAAO;GAAE;GAAM,OAAO,SAAS,SAAS,OAAO;GAAO,GAAI,YAAY,EAAE,WAAW,MAAM,GAAG,EAAE;GAAG;;;AAIrG,eAAe,wBAAwB,QAAiD;CAEtF,IAAI;AACJ,KAAI;AACF,iBAAe,MAAM,OAAO,kBAAkB;SACxC;AACN,QAAM,IAAI,MACR,gKACD;;CAGH,MAAM,SAAS,cAAc,UAAU,OAAO,OAAO;CACrD,MAAM,SAAS,IAAI,YAAY,EAAE,QAAQ,CAAC;AAE1C,QAAO,OAAO,SAAuD;EAEnE,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,OAAO,OAAO,gBAAgB;IAC7C,OAAO,OAAO;IACd,UAAU,KAAK;IACf,QAAQ;KACN,mBAAmB,KAAK;KACxB,iBAAiB,KAAK;KACtB,aAAa;KACb,kBAAkB;KACnB;IACF,CAAC;WACK,OAAO;AACd,SAAM,sBAAsB,MAAM;;EAGpC,MAAM,OAAO,SAAS,QAAQ;EAC9B,MAAM,YAAY,SAAS,aAAa,IAAI,iBAAiB;AAC7D,SAAO;GAAE;GAAM,OAAO,SAAS,gBAAgB,OAAO;GAAO,GAAI,YAAY,EAAE,WAAW,MAAM,GAAG,EAAE;GAAG;;;AAI5G,eAAe,2BAA2B,QAAiD;CAEzF,IAAI;AACJ,KAAI;AACF,eAAa,MAAM,OAAO,sBAAsB;SAC1C;AACN,QAAM,IAAI,MACR,mLACD;;CAGH,MAAM,SAAS,cAAc,aAAa,OAAO,OAAO;CACxD,MAAM,SAAS,IAAI,UAAU;EAAE;EAAQ,SAAS,OAAO;EAAS,CAAC;AAEjE,QAAO,OAAO,SAAuD;EAEnE,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,OAAO,SAAS,OAAO;IACtC,OAAO,OAAO;IACd,QAAQ,KAAK;IACb,UAAU,CAAC;KAAE,MAAM;KAAQ,SAAS,KAAK;KAAa,CAAC;IACvD,YAAY,KAAK;IACjB,aAAa;IACd,CAAC;WACK,OAAO;AACd,SAAM,sBAAsB,MAAM;;EAKpC,MAAM,QADY,SAAS,SAAS,MAAM,UAAe,MAAM,SAAS,OAAO,GACvD,QAAQ;EAChC,MAAM,YAAY,SAAS,gBAAgB;AAC3C,SAAO;GAAE;GAAM,OAAO,SAAS,SAAS,OAAO;GAAO,GAAI,YAAY,EAAE,WAAW,MAAM,GAAG,EAAE;GAAG;;;;;;;AAQrG,eAAsB,kBAAkB,QAAiD;AAGvF,KAAI,OAAO,aAAa,YAAY,OAAO,QACzC,OAAM,IAAI,uBACR,4EACwB,aAAa,4DACrC,SACD;AAGH,SAAQ,OAAO,UAAf;EACE,KAAK,SACH,QAAO,wBAAwB,OAAO;EACxC,KAAK,YACH,QAAO,2BAA2B,OAAO;EAC3C,KAAK,SACH,QAAO,wBAAwB,OAAO;EACxC,SAAS;GACP,MAAM,cAAqB,OAAO;AAClC,SAAM,IAAI,MAAM,qBAAqB,cAAc"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "./stdout-guard-sJHFWVem.js";
|
|
2
|
+
import { l as removeTranslations } from "./operations-BCJd7in3.js";
|
|
3
|
+
import "./providers-BAwPjeCy.js";
|
|
4
|
+
import "./php-reader-DuZ0Hyl_.js";
|
|
5
|
+
import { o as splitList, t as createCommand } from "./_shared-Dv1EW2gy.js";
|
|
6
|
+
//#region src/commands/remove.ts
|
|
7
|
+
var remove_default = createCommand({
|
|
8
|
+
name: "remove",
|
|
9
|
+
description: "Remove translation keys from all locale files in a layer",
|
|
10
|
+
args: {
|
|
11
|
+
layer: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Layer name",
|
|
14
|
+
required: true
|
|
15
|
+
},
|
|
16
|
+
keys: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Comma-separated key paths to remove",
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
dryRun: {
|
|
22
|
+
type: "boolean",
|
|
23
|
+
description: "Preview changes without writing",
|
|
24
|
+
default: false
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async run(args) {
|
|
28
|
+
const keys = splitList(args.keys) ?? [];
|
|
29
|
+
if (keys.length === 0) throw new Error("No keys provided. Pass comma-separated key paths via --keys");
|
|
30
|
+
return removeTranslations({
|
|
31
|
+
layer: args.layer,
|
|
32
|
+
keys,
|
|
33
|
+
dryRun: args.dryRun,
|
|
34
|
+
projectDir: args.projectDir
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
export { remove_default as default };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=remove-DR_mRiMA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove-DR_mRiMA.js","names":[],"sources":["../src/commands/remove.ts"],"sourcesContent":["import { createCommand, splitList } from './_shared.js'\nimport { removeTranslations } from '../core/operations.js'\n\nexport default createCommand({\n name: 'remove',\n description: 'Remove translation keys from all locale files in a layer',\n args: {\n layer: { type: 'string', description: 'Layer name', required: true },\n keys: { type: 'string', description: 'Comma-separated key paths to remove', required: true },\n dryRun: { type: 'boolean', description: 'Preview changes without writing', default: false },\n },\n async run(args) {\n const keys = splitList(args.keys) ?? []\n if (keys.length === 0) {\n throw new Error('No keys provided. Pass comma-separated key paths via --keys')\n }\n return removeTranslations({ layer: args.layer, keys, dryRun: args.dryRun, projectDir: args.projectDir })\n },\n})\n"],"mappings":";;;;;;AAGA,IAAA,iBAAe,cAAc;CAC3B,MAAM;CACN,aAAa;CACb,MAAM;EACJ,OAAO;GAAE,MAAM;GAAU,aAAa;GAAc,UAAU;GAAM;EACpE,MAAM;GAAE,MAAM;GAAU,aAAa;GAAuC,UAAU;GAAM;EAC5F,QAAQ;GAAE,MAAM;GAAW,aAAa;GAAmC,SAAS;GAAO;EAC5F;CACD,MAAM,IAAI,MAAM;EACd,MAAM,OAAO,UAAU,KAAK,KAAK,IAAI,EAAE;AACvC,MAAI,KAAK,WAAW,EAClB,OAAM,IAAI,MAAM,8DAA8D;AAEhF,SAAO,mBAAmB;GAAE,OAAO,KAAK;GAAO;GAAM,QAAQ,KAAK;GAAQ,YAAY,KAAK;GAAY,CAAC;;CAE3G,CAAC"}
|