@x18n/node 0.0.2 → 0.0.3
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/dist/index.d.mts +84 -0
- package/dist/index.mjs +845 -0
- package/package.json +3 -3
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Builtins, Host, JsonObject, Logger, Plugin, Preset, Project, UserConfig } from "@x18n/core";
|
|
2
|
+
//#region src/config.d.ts
|
|
3
|
+
export declare const configFileNames: readonly string[];
|
|
4
|
+
/** The config file in `dir`, if any. */
|
|
5
|
+
export declare function findConfigFile(dir: string): string | undefined;
|
|
6
|
+
/** The nearest config file at or above `cwd`, stopping at the git root. */
|
|
7
|
+
export declare function findConfigUpward(cwd: string): string | undefined;
|
|
8
|
+
/** Imports a JS or TS module (config, plugin, executed resource). */
|
|
9
|
+
export declare function importModule(file: string): Promise<unknown>;
|
|
10
|
+
export declare function loadConfigFile(file: string): Promise<UserConfig>;
|
|
11
|
+
/** Loads a YAML plugin file (`*.x18n-plugin.yml`). */
|
|
12
|
+
export declare function loadYamlPlugin(file: string): Promise<Plugin | Preset>;
|
|
13
|
+
/** Resolves a plugin specifier relative to the config directory: a YAML file, a module path, or a package name. */
|
|
14
|
+
export declare function createPluginLoader(dir: string): (specifier: string, options: JsonObject | undefined) => Promise<Plugin | Preset>;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/host.d.ts
|
|
17
|
+
interface NodeHostOptions {
|
|
18
|
+
/** Directory of the persistent cache, or false to disable it. Default `node_modules/.cache/x18n` under the root. */
|
|
19
|
+
readonly cacheDir?: string | false | undefined;
|
|
20
|
+
readonly logger?: Logger | undefined;
|
|
21
|
+
}
|
|
22
|
+
interface NodeHost extends Host {
|
|
23
|
+
/** Writes the persistent cache to disk. */
|
|
24
|
+
readonly flush: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare const stderrLogger: Logger;
|
|
27
|
+
/** The directory holding `.git`, at or above `start`. */
|
|
28
|
+
export declare function findGitRoot(start: string): string | undefined;
|
|
29
|
+
export declare function createNodeHost(root: string, options?: NodeHostOptions): NodeHost;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/load.d.ts
|
|
32
|
+
interface LoadOptions {
|
|
33
|
+
/** Where to look for a config file (upward). Default: the process cwd. */
|
|
34
|
+
readonly cwd: string;
|
|
35
|
+
/** Explicit config file. */
|
|
36
|
+
readonly configFile?: string | undefined;
|
|
37
|
+
/** Look for a config in parent directories too. Default true. */
|
|
38
|
+
readonly upward?: boolean | undefined;
|
|
39
|
+
/** Only this project of a monorepo. */
|
|
40
|
+
readonly project?: string | undefined;
|
|
41
|
+
/** Disable the persistent cache. */
|
|
42
|
+
readonly cache?: boolean | undefined;
|
|
43
|
+
readonly builtins?: Builtins | undefined;
|
|
44
|
+
readonly logger?: NodeHost["logger"];
|
|
45
|
+
/** Wraps each project's host, for example with an overlay of unsaved editor buffers. */
|
|
46
|
+
readonly wrapHost?: ((host: NodeHost) => Host) | undefined;
|
|
47
|
+
}
|
|
48
|
+
interface LoadedProject {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/** Absolute project root. */
|
|
51
|
+
readonly root: string;
|
|
52
|
+
readonly configFile: string | undefined;
|
|
53
|
+
readonly host: NodeHost;
|
|
54
|
+
readonly project: Project;
|
|
55
|
+
}
|
|
56
|
+
interface Workspace {
|
|
57
|
+
/** Absolute root of the workspace (the directory of the root config, or the cwd). */
|
|
58
|
+
readonly root: string;
|
|
59
|
+
readonly configFile: string | undefined;
|
|
60
|
+
readonly projects: readonly LoadedProject[];
|
|
61
|
+
}
|
|
62
|
+
/** Finds the config, resolves every project and waits until each is indexed. */
|
|
63
|
+
export declare function loadWorkspace(options: LoadOptions): Promise<Workspace>;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/migrate.d.ts
|
|
66
|
+
interface MigrationFile {
|
|
67
|
+
/** Path relative to the project root. */
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly content: string;
|
|
70
|
+
}
|
|
71
|
+
interface MigrationResult {
|
|
72
|
+
readonly files: readonly MigrationFile[];
|
|
73
|
+
/** What was dropped or needs attention, for the report. */
|
|
74
|
+
readonly notes: readonly string[];
|
|
75
|
+
/** i18n-ally settings that can be removed after the migration. */
|
|
76
|
+
readonly removable: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
interface MigrateOptions {
|
|
79
|
+
readonly format?: "ts" | "yml" | "json" | undefined;
|
|
80
|
+
}
|
|
81
|
+
/** Converts i18n-ally settings, custom framework and reviews into x18n files. Never deletes i18n-ally files. */
|
|
82
|
+
export declare function migrateI18nAlly(root: string, options?: MigrateOptions): Promise<MigrationResult>;
|
|
83
|
+
//#endregion
|
|
84
|
+
export type { LoadOptions, LoadedProject, MigrateOptions, MigrationFile, MigrationResult, NodeHost, NodeHostOptions, Workspace };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { compileYamlPlugin } from "@x18n/builtins/yaml-plugin";
|
|
7
|
+
import { PluginFileSchema, X18nError, createProject, hashText, isX18nError, resolveConfig, validate } from "@x18n/core";
|
|
8
|
+
import { createJiti } from "jiti";
|
|
9
|
+
import { glob } from "tinyglobby";
|
|
10
|
+
import { parse, stringify } from "yaml";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import { watch } from "chokidar";
|
|
13
|
+
import ignore from "ignore";
|
|
14
|
+
import { createBuiltins } from "@x18n/builtins";
|
|
15
|
+
import { applyEdits, modify, parse as parse$1 } from "jsonc-parser";
|
|
16
|
+
//#region src/config.ts
|
|
17
|
+
const configFileNames = [
|
|
18
|
+
"x18n.config.ts",
|
|
19
|
+
"x18n.config.mts",
|
|
20
|
+
"x18n.config.js",
|
|
21
|
+
"x18n.config.mjs",
|
|
22
|
+
"x18n.config.yml",
|
|
23
|
+
"x18n.config.yaml",
|
|
24
|
+
"x18n.config.json"
|
|
25
|
+
];
|
|
26
|
+
/** The config file in `dir`, if any. */
|
|
27
|
+
function findConfigFile(dir) {
|
|
28
|
+
for (const name of configFileNames) {
|
|
29
|
+
const file = join(dir, name);
|
|
30
|
+
if (existsSync(file)) return file;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The nearest config file at or above `cwd`, stopping at the git root. */
|
|
34
|
+
function findConfigUpward(cwd) {
|
|
35
|
+
let dir = resolve(cwd);
|
|
36
|
+
while (true) {
|
|
37
|
+
const found = findConfigFile(dir);
|
|
38
|
+
if (found) return found;
|
|
39
|
+
const parent = dirname(dir);
|
|
40
|
+
if (parent === dir || existsSync(join(dir, ".git"))) return void 0;
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const core = fileURLToPath(import.meta.resolve("@x18n/core"));
|
|
45
|
+
const presets = fileURLToPath(import.meta.resolve("@x18n/builtins/presets"));
|
|
46
|
+
const jiti = createJiti(import.meta.url, {
|
|
47
|
+
moduleCache: false,
|
|
48
|
+
interopDefault: true,
|
|
49
|
+
alias: {
|
|
50
|
+
"@x18n/core": core,
|
|
51
|
+
"@x18n/cli/presets": presets,
|
|
52
|
+
"@x18n/cli": core,
|
|
53
|
+
"@x18n/builtins/presets": presets
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
/** Imports a JS or TS module (config, plugin, executed resource). */
|
|
57
|
+
async function importModule(file) {
|
|
58
|
+
return jiti.import(pathToFileURL(file).href, { default: true });
|
|
59
|
+
}
|
|
60
|
+
function isObject(value) {
|
|
61
|
+
return typeof value === "object" && value !== null;
|
|
62
|
+
}
|
|
63
|
+
async function loadConfigFile(file) {
|
|
64
|
+
try {
|
|
65
|
+
if (/\.ya?ml$/.test(file)) return parse(await readFile(file, "utf8")) ?? {};
|
|
66
|
+
if (file.endsWith(".json")) return JSON.parse(await readFile(file, "utf8"));
|
|
67
|
+
const loaded = await importModule(file);
|
|
68
|
+
const config = typeof loaded === "function" ? await loaded() : loaded;
|
|
69
|
+
if (!isObject(config)) throw new X18nError("X18N0002", `${file} does not export a config object`);
|
|
70
|
+
return config;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (isX18nError(error)) throw error;
|
|
73
|
+
throw new X18nError("X18N0002", `${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Loads a YAML plugin file (`*.x18n-plugin.yml`). */
|
|
77
|
+
async function loadYamlPlugin(file) {
|
|
78
|
+
const data = parse(await readFile(file, "utf8"));
|
|
79
|
+
const result = validate(PluginFileSchema, data);
|
|
80
|
+
if (!result.success) throw new X18nError("X18N1004", `${file}: ${result.issues.map((i) => `${i.path || "(root)"}: ${i.message}`).join("; ")}`);
|
|
81
|
+
return compileYamlPlugin(result.output);
|
|
82
|
+
}
|
|
83
|
+
function isPluginLike(value) {
|
|
84
|
+
return isObject(value) && "name" in value && typeof value.name === "string";
|
|
85
|
+
}
|
|
86
|
+
/** Resolves a plugin specifier relative to the config directory: a YAML file, a module path, or a package name. */
|
|
87
|
+
function createPluginLoader(dir) {
|
|
88
|
+
return async (specifier, options) => {
|
|
89
|
+
try {
|
|
90
|
+
const file = specifier.startsWith(".") || isAbsolute(specifier) ? resolve(dir, specifier) : createRequire(join(dir, "package.json")).resolve(specifier);
|
|
91
|
+
if (/\.ya?ml$/.test(file)) return await loadYamlPlugin(file);
|
|
92
|
+
const loaded = await importModule(file);
|
|
93
|
+
const plugin = typeof loaded === "function" ? loaded(options) : loaded;
|
|
94
|
+
if (!isPluginLike(plugin)) throw new Error("the module does not export a plugin or a plugin factory");
|
|
95
|
+
return plugin;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (isX18nError(error)) throw error;
|
|
98
|
+
throw new X18nError("X18N1001", `${specifier}: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Workspace package directories of `root`, from `pnpm-workspace.yaml` or the `workspaces` field of `package.json`. */
|
|
103
|
+
async function workspacePackages(root) {
|
|
104
|
+
const pnpm = parse(await readFile(join(root, "pnpm-workspace.yaml"), "utf8").catch(() => "") || "{}");
|
|
105
|
+
const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8").catch(() => "{}"));
|
|
106
|
+
const field = (value, key) => typeof value === "object" && value !== null && key in value ? new Map(Object.entries(value)).get(key) : void 0;
|
|
107
|
+
const workspaces = field(manifest, "workspaces");
|
|
108
|
+
const patterns = [
|
|
109
|
+
field(pnpm, "packages"),
|
|
110
|
+
workspaces,
|
|
111
|
+
field(workspaces, "packages")
|
|
112
|
+
].flatMap((list) => Array.isArray(list) ? list.filter((item) => typeof item === "string") : []);
|
|
113
|
+
if (patterns.length === 0) return [];
|
|
114
|
+
return (await glob(patterns.filter((p) => !p.startsWith("!")).map((p) => `${p.replace(/\/$/, "")}/package.json`), {
|
|
115
|
+
cwd: root,
|
|
116
|
+
ignore: ["**/node_modules/**", ...patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1))]
|
|
117
|
+
})).map((file) => dirname(file)).sort();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Directories, relative to `root`, whose manifests count for preset detection: the parents up to the
|
|
121
|
+
* git root (`..`, `../..`), and the workspace packages of a monorepo root, where the i18n library is often listed.
|
|
122
|
+
*/
|
|
123
|
+
async function manifestRootsOf(root, gitRoot) {
|
|
124
|
+
const out = [];
|
|
125
|
+
const top = gitRoot ?? resolve(root);
|
|
126
|
+
let dir = resolve(root);
|
|
127
|
+
while (dir !== top && dir.startsWith(top)) {
|
|
128
|
+
dir = dirname(dir);
|
|
129
|
+
out.push(relative(root, dir).replace(/\\/g, "/"));
|
|
130
|
+
}
|
|
131
|
+
return [...out, ...await workspacePackages(root)];
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/host.ts
|
|
135
|
+
const toPosix = (path) => sep === "\\" ? path.replace(/\\/g, "/") : path;
|
|
136
|
+
function isNotFound(error) {
|
|
137
|
+
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "EISDIR");
|
|
138
|
+
}
|
|
139
|
+
const stderrLogger = {
|
|
140
|
+
info: (message) => process.stderr.write(`${message}\n`),
|
|
141
|
+
warn: (message) => process.stderr.write(`warning: ${message}\n`),
|
|
142
|
+
error: (message) => process.stderr.write(`error: ${message}\n`)
|
|
143
|
+
};
|
|
144
|
+
/** The directory holding `.git`, at or above `start`. */
|
|
145
|
+
function findGitRoot(start) {
|
|
146
|
+
let dir = resolve(start);
|
|
147
|
+
while (true) {
|
|
148
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
149
|
+
const parent = dirname(dir);
|
|
150
|
+
if (parent === dir) return void 0;
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Every `.gitignore` that applies under `root`: the ones above it up to the git root, and nested ones. */
|
|
155
|
+
async function loadGitignore(root) {
|
|
156
|
+
const rules = [];
|
|
157
|
+
const gitRoot = findGitRoot(root) ?? root;
|
|
158
|
+
const add = async (dir) => {
|
|
159
|
+
const text = await readFile(join(dir, ".gitignore"), "utf8").catch(() => void 0);
|
|
160
|
+
if (text !== void 0) rules.push({
|
|
161
|
+
base: dir,
|
|
162
|
+
ig: ignore().add(text)
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
let dir = root;
|
|
166
|
+
const chain = [];
|
|
167
|
+
while (dir.startsWith(gitRoot)) {
|
|
168
|
+
chain.push(dir);
|
|
169
|
+
if (dir === gitRoot) break;
|
|
170
|
+
dir = dirname(dir);
|
|
171
|
+
}
|
|
172
|
+
for (const item of chain.reverse()) await add(item);
|
|
173
|
+
const nested = await glob(["**/.gitignore"], {
|
|
174
|
+
cwd: root,
|
|
175
|
+
ignore: ["**/node_modules/**", ".gitignore"],
|
|
176
|
+
dot: true
|
|
177
|
+
});
|
|
178
|
+
for (const file of nested) await add(join(root, dirname(file)));
|
|
179
|
+
return (path) => {
|
|
180
|
+
const absolute = join(root, path);
|
|
181
|
+
return rules.some(({ base, ig }) => {
|
|
182
|
+
const rel = toPosix(relative(base, absolute));
|
|
183
|
+
return rel !== "" && !rel.startsWith("..") && ig.ignores(rel);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** A persistent cache in one JSON file. Only entries used in this session are written back, so it stays bounded. */
|
|
188
|
+
function createFileCache(dir) {
|
|
189
|
+
const file = join(dir, "cache.json");
|
|
190
|
+
let entries;
|
|
191
|
+
const used = /* @__PURE__ */ new Map();
|
|
192
|
+
let dirty = false;
|
|
193
|
+
const load = () => {
|
|
194
|
+
entries ??= readFile(file, "utf8").then((text) => {
|
|
195
|
+
const data = JSON.parse(text);
|
|
196
|
+
return new Map(typeof data === "object" && data !== null ? Object.entries(data).filter((e) => typeof e[1] === "string") : []);
|
|
197
|
+
}).catch(() => /* @__PURE__ */ new Map());
|
|
198
|
+
return entries;
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
async get(key) {
|
|
202
|
+
const value = (await load()).get(key);
|
|
203
|
+
if (value !== void 0) used.set(key, value);
|
|
204
|
+
return value;
|
|
205
|
+
},
|
|
206
|
+
async set(key, value) {
|
|
207
|
+
used.set(key, value);
|
|
208
|
+
dirty = true;
|
|
209
|
+
},
|
|
210
|
+
async flush() {
|
|
211
|
+
if (!dirty) return;
|
|
212
|
+
await mkdir(dir, { recursive: true });
|
|
213
|
+
await writeFile(file, JSON.stringify(Object.fromEntries(used)));
|
|
214
|
+
dirty = false;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createNodeHost(root, options = {}) {
|
|
219
|
+
const absoluteRoot = resolve(root);
|
|
220
|
+
let gitignore;
|
|
221
|
+
const cacheDir = options.cacheDir === false ? void 0 : options.cacheDir ?? join(absoluteRoot, "node_modules/.cache/x18n");
|
|
222
|
+
const cache = cacheDir ? createFileCache(join(cacheDir, hashText(absoluteRoot))) : void 0;
|
|
223
|
+
const full = (path) => resolve(absoluteRoot, path);
|
|
224
|
+
return {
|
|
225
|
+
root: absoluteRoot,
|
|
226
|
+
logger: options.logger,
|
|
227
|
+
cache,
|
|
228
|
+
async readFile(path) {
|
|
229
|
+
try {
|
|
230
|
+
return await readFile(full(path), "utf8");
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (isNotFound(error)) return void 0;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async writeFile(path, content) {
|
|
237
|
+
const file = full(path);
|
|
238
|
+
await mkdir(dirname(file), { recursive: true });
|
|
239
|
+
await writeFile(file, content, "utf8");
|
|
240
|
+
},
|
|
241
|
+
async deleteFile(path) {
|
|
242
|
+
await rm(full(path), { force: true });
|
|
243
|
+
},
|
|
244
|
+
async glob(patterns, globOptions = {}) {
|
|
245
|
+
const paths = (await glob([...patterns], {
|
|
246
|
+
cwd: absoluteRoot,
|
|
247
|
+
ignore: [...globOptions.ignore ?? []],
|
|
248
|
+
dot: false,
|
|
249
|
+
followSymbolicLinks: false
|
|
250
|
+
})).map(toPosix);
|
|
251
|
+
if (!globOptions.gitignore) return paths.sort();
|
|
252
|
+
gitignore ??= loadGitignore(absoluteRoot);
|
|
253
|
+
const ignored = await gitignore;
|
|
254
|
+
return paths.filter((path) => !ignored(path)).sort();
|
|
255
|
+
},
|
|
256
|
+
watch(_patterns, onChange) {
|
|
257
|
+
let pending = [];
|
|
258
|
+
let timer;
|
|
259
|
+
const push = (type) => (file) => {
|
|
260
|
+
const path = toPosix(relative(absoluteRoot, file));
|
|
261
|
+
if (path.startsWith("..") || path.includes("node_modules/") || path.startsWith(".git/")) return;
|
|
262
|
+
pending.push({
|
|
263
|
+
type,
|
|
264
|
+
path
|
|
265
|
+
});
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
timer = setTimeout(() => {
|
|
268
|
+
const events = pending;
|
|
269
|
+
pending = [];
|
|
270
|
+
onChange(events);
|
|
271
|
+
}, 50);
|
|
272
|
+
};
|
|
273
|
+
const watcher = watch(absoluteRoot, {
|
|
274
|
+
ignoreInitial: true,
|
|
275
|
+
ignored: (path) => /[/\\](?:node_modules|\.git)(?:[/\\]|$)/.test(path)
|
|
276
|
+
});
|
|
277
|
+
watcher.on("add", push("create")).on("change", push("change")).on("unlink", push("delete"));
|
|
278
|
+
return { dispose: () => {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
watcher.close();
|
|
281
|
+
} };
|
|
282
|
+
},
|
|
283
|
+
flush: async () => cache?.flush()
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/migrate.ts
|
|
288
|
+
const frameworkPresets = {
|
|
289
|
+
"vue": "vue-i18n",
|
|
290
|
+
"vue-sfc": "vue-i18n",
|
|
291
|
+
"vuex": "vue-i18n",
|
|
292
|
+
"nuxt": "nuxt-i18n",
|
|
293
|
+
"react": "react-intl",
|
|
294
|
+
"react-i18next": "react-i18next",
|
|
295
|
+
"next-i18next": "react-i18next",
|
|
296
|
+
"i18next": "i18next",
|
|
297
|
+
"i18next-shopify": "i18next-shopify",
|
|
298
|
+
"next-intl": "next-intl",
|
|
299
|
+
"next-international": "next-international",
|
|
300
|
+
"next-translate": "next-translate",
|
|
301
|
+
"svelte": "svelte-i18n",
|
|
302
|
+
"vscode": "vscode-extension",
|
|
303
|
+
"ngx-translate": "ngx-translate",
|
|
304
|
+
"transloco": "transloco",
|
|
305
|
+
"flutter": "flutter",
|
|
306
|
+
"ember": "ember-intl",
|
|
307
|
+
"i18n-tag": "i18n-tag",
|
|
308
|
+
"fluent-vue": "fluent-vue",
|
|
309
|
+
"fluent-vue-sfc": "fluent-vue",
|
|
310
|
+
"php-joomla": "php-joomla",
|
|
311
|
+
"php-gettext": "php-gettext",
|
|
312
|
+
"laravel": "laravel",
|
|
313
|
+
"chrome-ext": "chrome-extension",
|
|
314
|
+
"ruby-rails": "ruby-rails",
|
|
315
|
+
"polyglot": "polyglot",
|
|
316
|
+
"globalize": "globalize",
|
|
317
|
+
"ui5": "ui5",
|
|
318
|
+
"lingui": "lingui",
|
|
319
|
+
"jekyll": "jekyll",
|
|
320
|
+
"i18n-js": "i18n-js"
|
|
321
|
+
};
|
|
322
|
+
const languageExtensions = {
|
|
323
|
+
javascript: [
|
|
324
|
+
"js",
|
|
325
|
+
"mjs",
|
|
326
|
+
"cjs"
|
|
327
|
+
],
|
|
328
|
+
typescript: [
|
|
329
|
+
"ts",
|
|
330
|
+
"mts",
|
|
331
|
+
"cts"
|
|
332
|
+
],
|
|
333
|
+
javascriptreact: ["jsx"],
|
|
334
|
+
typescriptreact: ["tsx"],
|
|
335
|
+
vue: ["vue"],
|
|
336
|
+
html: ["html"],
|
|
337
|
+
svelte: ["svelte"],
|
|
338
|
+
php: ["php"],
|
|
339
|
+
blade: ["blade.php"],
|
|
340
|
+
ruby: ["rb"],
|
|
341
|
+
erb: ["erb"],
|
|
342
|
+
dart: ["dart"],
|
|
343
|
+
json: ["json"],
|
|
344
|
+
yaml: ["yml", "yaml"],
|
|
345
|
+
handlebars: ["hbs"]
|
|
346
|
+
};
|
|
347
|
+
const keygenStyles = {
|
|
348
|
+
"camelCase": "camel",
|
|
349
|
+
"snake_case": "snake",
|
|
350
|
+
"kebab-case": "kebab"
|
|
351
|
+
};
|
|
352
|
+
const editorSettings = {
|
|
353
|
+
annotations: "x18n.annotations",
|
|
354
|
+
annotationInPlace: "x18n.inPlace",
|
|
355
|
+
annotationMaxLength: "x18n.annotationMaxLength",
|
|
356
|
+
displayLanguage: "x18n.displayLocale"
|
|
357
|
+
};
|
|
358
|
+
const handledByDefaults = [
|
|
359
|
+
"readonly",
|
|
360
|
+
"encoding",
|
|
361
|
+
"fullReloadOnChanged",
|
|
362
|
+
"includeSubfolders",
|
|
363
|
+
"ignoreFiles",
|
|
364
|
+
"editor.preferEditor",
|
|
365
|
+
"showFlags",
|
|
366
|
+
"annotationDelimiter",
|
|
367
|
+
"languageTagSystem",
|
|
368
|
+
"targetPickingStrategy",
|
|
369
|
+
"extract.targetPickingStrategy"
|
|
370
|
+
];
|
|
371
|
+
async function readText(path) {
|
|
372
|
+
return readFile(path, "utf8").catch(() => void 0);
|
|
373
|
+
}
|
|
374
|
+
/** The first of `paths` that exists; the i18n-ally-next name comes first. */
|
|
375
|
+
async function readFirst(root, paths) {
|
|
376
|
+
for (const path of paths) {
|
|
377
|
+
const text = await readText(join(root, path));
|
|
378
|
+
if (text !== void 0) return {
|
|
379
|
+
path,
|
|
380
|
+
text
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function isRecord(value) {
|
|
385
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
386
|
+
}
|
|
387
|
+
const asStrings = (value) => (Array.isArray(value) ? value : value === void 0 ? [] : [value]).filter((v) => typeof v === "string");
|
|
388
|
+
/** Setting prefixes by priority: the i18n-ally-next fork renames `i18n-ally.*` keys, so its value is the newest. */
|
|
389
|
+
const settingPrefixes = [
|
|
390
|
+
"i18n-ally-next",
|
|
391
|
+
"i18n-ally",
|
|
392
|
+
"vue-i18n-ally"
|
|
393
|
+
];
|
|
394
|
+
/** `i18n-ally-next.*`, `i18n-ally.*` and legacy `vue-i18n-ally.*` keys from `.vscode/settings.json` and `*.code-workspace` files. */
|
|
395
|
+
async function readSettings(root) {
|
|
396
|
+
const settings = /* @__PURE__ */ new Map();
|
|
397
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
398
|
+
const keys = [];
|
|
399
|
+
const sources = [];
|
|
400
|
+
const vscode = await readText(join(root, ".vscode/settings.json"));
|
|
401
|
+
if (vscode !== void 0) sources.push(parse$1(vscode));
|
|
402
|
+
for (const file of await readdir(root).catch(() => [])) if (file.endsWith(".code-workspace")) {
|
|
403
|
+
const data = parse$1(await readText(join(root, file)) ?? "{}");
|
|
404
|
+
if (isRecord(data)) sources.push(data.settings);
|
|
405
|
+
}
|
|
406
|
+
for (const source of sources) {
|
|
407
|
+
if (!isRecord(source)) continue;
|
|
408
|
+
for (const [key, value] of Object.entries(source)) {
|
|
409
|
+
const match = /^(i18n-ally-next|i18n-ally|vue-i18n-ally)\.(.+)$/.exec(key);
|
|
410
|
+
if (!match) continue;
|
|
411
|
+
keys.push(key);
|
|
412
|
+
const rank = settingPrefixes.indexOf(match[1]);
|
|
413
|
+
if (rank < (ranks.get(match[2]) ?? Infinity)) {
|
|
414
|
+
settings.set(match[2], value);
|
|
415
|
+
ranks.set(match[2], rank);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
settings,
|
|
421
|
+
keys
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/** `localesPaths` + `pathMatcher` + `dirStructure` + `namespace` as resource patterns with the same tokens. */
|
|
425
|
+
function resourcesOf(settings, executed) {
|
|
426
|
+
const paths = asStrings(settings.get("localesPaths")).map((path) => path.replace(/^\.\//, "").replace(/\/$/, ""));
|
|
427
|
+
const matcher = settings.get("pathMatcher");
|
|
428
|
+
const structure = settings.get("dirStructure");
|
|
429
|
+
const namespace = settings.get("namespace") === true;
|
|
430
|
+
return paths.flatMap((dir) => {
|
|
431
|
+
if (typeof matcher === "string") return [`${dir}/${matcher}`];
|
|
432
|
+
if (structure === "file") return [`${dir}/{locale}.{ext}`];
|
|
433
|
+
if (structure === "dir") return [namespace ? `${dir}/{locale}/{namespaces}.{ext}` : `${dir}/{locale}/**/*.{ext}`];
|
|
434
|
+
return [`${dir}/{locale}.{ext}`, namespace ? `${dir}/{locale}/{namespaces}.{ext}` : `${dir}/{locale}/**/*.{ext}`];
|
|
435
|
+
}).map((pattern) => executed && /\.(?:[jt]s|\{ext\})$/.test(pattern) ? {
|
|
436
|
+
pattern,
|
|
437
|
+
execute: true
|
|
438
|
+
} : { pattern });
|
|
439
|
+
}
|
|
440
|
+
function templatesOf(value) {
|
|
441
|
+
return (Array.isArray(value) ? value : []).flatMap((entry) => {
|
|
442
|
+
if (typeof entry === "string") return [{ template: entry.replace(/\$1/g, "{key}") }];
|
|
443
|
+
if (!isRecord(entry)) return [];
|
|
444
|
+
const files = asStrings(entry.include);
|
|
445
|
+
return asStrings(entry.templates ?? entry.template).map((template) => ({
|
|
446
|
+
template: template.replace(/\$1/g, "{key}"),
|
|
447
|
+
...files.length ? { files } : {}
|
|
448
|
+
}));
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function flagOf(country) {
|
|
452
|
+
return String.fromCodePoint(...[...country.toUpperCase()].map((c) => 127462 + c.charCodeAt(0) - 65));
|
|
453
|
+
}
|
|
454
|
+
/** `.vscode/i18n-ally-custom-framework.yml` into YAML plugin parts. */
|
|
455
|
+
function customFramework(data, parts) {
|
|
456
|
+
if (!isRecord(data)) return;
|
|
457
|
+
const extensions = asStrings(data.languageIds).flatMap((id) => languageExtensions[id] ?? [id]);
|
|
458
|
+
const patterns = asStrings(data.usageMatchRegex);
|
|
459
|
+
if (patterns.length) parts.regex.push({
|
|
460
|
+
extensions: extensions.length ? extensions : [
|
|
461
|
+
"js",
|
|
462
|
+
"jsx",
|
|
463
|
+
"ts",
|
|
464
|
+
"tsx",
|
|
465
|
+
"vue"
|
|
466
|
+
],
|
|
467
|
+
patterns
|
|
468
|
+
});
|
|
469
|
+
const scope = asStrings(data.scopeRangeRegex)[0];
|
|
470
|
+
if (scope) parts.scopes = scope;
|
|
471
|
+
parts.templates.push(...templatesOf(data.refactorTemplates));
|
|
472
|
+
if (data.monopoly === true) parts.exclusive = true;
|
|
473
|
+
}
|
|
474
|
+
function configFile(config, format) {
|
|
475
|
+
if (format === "yml") return {
|
|
476
|
+
path: "x18n.config.yml",
|
|
477
|
+
content: `# yaml-language-server: $schema=https://x18n.antfu.dev/schema/config.json\n${stringify(config)}`
|
|
478
|
+
};
|
|
479
|
+
if (format === "json") return {
|
|
480
|
+
path: "x18n.config.json",
|
|
481
|
+
content: `${JSON.stringify({
|
|
482
|
+
$schema: "https://x18n.antfu.dev/schema/config.json",
|
|
483
|
+
...config
|
|
484
|
+
}, null, 2)}\n`
|
|
485
|
+
};
|
|
486
|
+
return {
|
|
487
|
+
path: "x18n.config.ts",
|
|
488
|
+
content: `import { defineConfig } from '@x18n/core'\n\n// Migrated from i18n-ally by \`x18n migrate i18n-ally\`.\nexport default defineConfig(${JSON.stringify(config, null, 2).replace(/"([a-z_$][\w$]*)":/gi, "$1:").replace(/'/g, "\\'").replace(/"/g, "'")})\n`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function readerOf(settings) {
|
|
492
|
+
const get = (key) => settings.get(key);
|
|
493
|
+
return {
|
|
494
|
+
get,
|
|
495
|
+
str: (key) => {
|
|
496
|
+
const value = get(key);
|
|
497
|
+
return typeof value === "string" ? value : void 0;
|
|
498
|
+
},
|
|
499
|
+
num: (key) => {
|
|
500
|
+
const value = get(key);
|
|
501
|
+
return typeof value === "number" ? value : void 0;
|
|
502
|
+
},
|
|
503
|
+
list: (key) => asStrings(get(key)),
|
|
504
|
+
pick: (key, values) => values.find((value) => value === get(key))
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function formatsOf(r) {
|
|
508
|
+
const formats = {};
|
|
509
|
+
for (const parser of r.list("enabledParsers")) formats[parser === "yaml" ? "yml" : parser] = parser;
|
|
510
|
+
const extend = r.get("parsers.extendFileExtensions");
|
|
511
|
+
if (isRecord(extend)) {
|
|
512
|
+
for (const [ext, parser] of Object.entries(extend)) if (typeof parser === "string") formats[ext.replace(/^\./, "")] = parser;
|
|
513
|
+
}
|
|
514
|
+
return formats;
|
|
515
|
+
}
|
|
516
|
+
function extractOf(r) {
|
|
517
|
+
const keygen = r.pick("extract.keygenStrategy", [
|
|
518
|
+
"slug",
|
|
519
|
+
"random",
|
|
520
|
+
"empty",
|
|
521
|
+
"source"
|
|
522
|
+
]);
|
|
523
|
+
const keyCase = keygenStyles[r.str("extract.keygenStyle") ?? ""];
|
|
524
|
+
const byFiles = r.get("extract.ignoredByFiles");
|
|
525
|
+
return {
|
|
526
|
+
...keygen ? { keygen } : {},
|
|
527
|
+
...keyCase ? { keyCase } : {},
|
|
528
|
+
...r.str("extract.keyPrefix") ? { keyPrefix: r.str("extract.keyPrefix") } : {},
|
|
529
|
+
...r.num("extract.keyMaxLength") !== void 0 ? { keyMaxLength: r.num("extract.keyMaxLength") } : {},
|
|
530
|
+
...r.list("extract.ignored").length ? { ignore: r.list("extract.ignored") } : {},
|
|
531
|
+
...isRecord(byFiles) ? { ignoreByFile: Object.fromEntries(Object.entries(byFiles).map(([glob, list]) => [glob, asStrings(list)])) } : {},
|
|
532
|
+
...r.get("extract.autoDetect") === true ? { autoDetect: true } : {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function writeOf(r) {
|
|
536
|
+
const sort = r.get("sortKeys") === true ? r.get("sortCompare") === "locale" ? "locale" : "binary" : void 0;
|
|
537
|
+
const indent = r.get("tabStyle") === "tab" ? "tab" : r.num("indent");
|
|
538
|
+
const keepFilled = r.get("keepFulfilled") === true;
|
|
539
|
+
if (!sort && indent === void 0 && !keepFilled) return void 0;
|
|
540
|
+
return {
|
|
541
|
+
...sort ? { sort } : {},
|
|
542
|
+
...indent !== void 0 ? { indent } : {},
|
|
543
|
+
...keepFilled ? { keepFilled } : {}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function presetsOf(r, notes) {
|
|
547
|
+
return r.list("enabledFrameworks").flatMap((id) => {
|
|
548
|
+
if (id === "general" || id === "custom") return [];
|
|
549
|
+
const preset = frameworkPresets[id];
|
|
550
|
+
if (!preset) notes.push(`Framework "${id}" has no built-in preset yet; it was left out.`);
|
|
551
|
+
return preset ? [preset] : [];
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function notesOf(settings, notes) {
|
|
555
|
+
if ([...settings.keys()].some((key) => key.startsWith("translate."))) notes.push("`translate.*` settings were dropped: x18n writes translate prompts instead (`x18n prompt translate --to <locale>`).");
|
|
556
|
+
if ([...settings.keys()].some((key) => key.startsWith("review."))) notes.push("Review settings move to `review.*` once reviews ship (1.x); they were not migrated yet.");
|
|
557
|
+
for (const key of handledByDefaults) if (settings.has(key)) notes.push(`\`i18n-ally.${key}\` is handled by defaults or dropped.`);
|
|
558
|
+
}
|
|
559
|
+
function mapConfig(settings, parts, notes) {
|
|
560
|
+
const r = readerOf(settings);
|
|
561
|
+
const presets = presetsOf(r, notes);
|
|
562
|
+
const formats = formatsOf(r);
|
|
563
|
+
const executed = r.get("parsers.typescript.compilerOptions") !== void 0 || r.get("parsers.typescript.tsNodePath") !== void 0;
|
|
564
|
+
if (executed) notes.push("JS/TS locale files were executed by i18n-ally; their patterns get `execute: true` and become read-only.");
|
|
565
|
+
const keyStyle = r.get("disablePathParsing") === true ? "flat" : r.pick("keystyle", ["flat", "nested"]);
|
|
566
|
+
const usage = {
|
|
567
|
+
...r.list("keysInUse").length ? { keysInUse: r.list("keysInUse") } : {},
|
|
568
|
+
...r.list("usage.derivedKeyRules").length ? { derivedKeys: r.list("usage.derivedKeyRules") } : {},
|
|
569
|
+
...r.list("usage.scanningIgnore").length ? { exclude: r.list("usage.scanningIgnore") } : {}
|
|
570
|
+
};
|
|
571
|
+
const extract = extractOf(r);
|
|
572
|
+
const write = writeOf(r);
|
|
573
|
+
parts.templates.push(...templatesOf(r.get("refactor.templates")));
|
|
574
|
+
const regexPatterns = [...r.list("regex.usageMatch"), ...r.list("regex.usageMatchAppend")];
|
|
575
|
+
if (regexPatterns.length) parts.regex.push({
|
|
576
|
+
extensions: [
|
|
577
|
+
"js",
|
|
578
|
+
"jsx",
|
|
579
|
+
"ts",
|
|
580
|
+
"tsx",
|
|
581
|
+
"vue",
|
|
582
|
+
"html",
|
|
583
|
+
"svelte"
|
|
584
|
+
],
|
|
585
|
+
patterns: regexPatterns
|
|
586
|
+
});
|
|
587
|
+
const countries = r.get("localeCountryMap");
|
|
588
|
+
if (isRecord(countries)) {
|
|
589
|
+
for (const [locale, country] of Object.entries(countries)) if (typeof country === "string" && /^[a-z]{2}$/i.test(country)) parts.localeInfo[locale] = { flag: flagOf(country) };
|
|
590
|
+
}
|
|
591
|
+
notesOf(settings, notes);
|
|
592
|
+
const resources = resourcesOf(settings, executed);
|
|
593
|
+
return {
|
|
594
|
+
...r.str("sourceLanguage") ? { sourceLocale: r.str("sourceLanguage") } : {},
|
|
595
|
+
...r.list("ignoredLocales").length ? { ignoreLocales: r.list("ignoredLocales") } : {},
|
|
596
|
+
...presets.length || parts.exclusive ? { presets: parts.exclusive ? [] : presets } : {},
|
|
597
|
+
...resources.length ? { resources } : {},
|
|
598
|
+
...Object.keys(formats).length ? { formats } : {},
|
|
599
|
+
...keyStyle ? { keyStyle } : {},
|
|
600
|
+
...r.str("defaultNamespace") ? { namespace: { default: r.str("defaultNamespace") } } : {},
|
|
601
|
+
...Object.keys(usage).length ? { usage } : {},
|
|
602
|
+
...write ? { write } : {},
|
|
603
|
+
...Object.keys(extract).length ? { extract } : {}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** The x18n config that a project's i18n-ally settings and custom framework mean, or undefined without any. */
|
|
607
|
+
async function readI18nAllyConfig(root) {
|
|
608
|
+
const { settings, keys } = await readSettings(root);
|
|
609
|
+
return configOf(root, settings, keys);
|
|
610
|
+
}
|
|
611
|
+
async function configOf(root, settings, keys) {
|
|
612
|
+
const custom = await readFirst(root, [".vscode/i18n-ally-next-custom-framework.yml", ".vscode/i18n-ally-custom-framework.yml"]);
|
|
613
|
+
const customText = custom?.text;
|
|
614
|
+
if (keys.length === 0 && customText === void 0) return void 0;
|
|
615
|
+
const notes = [];
|
|
616
|
+
const parts = {
|
|
617
|
+
regex: [],
|
|
618
|
+
scopes: void 0,
|
|
619
|
+
templates: [],
|
|
620
|
+
localeInfo: {},
|
|
621
|
+
exclusive: false
|
|
622
|
+
};
|
|
623
|
+
if (customText !== void 0) customFramework(parse(customText), parts);
|
|
624
|
+
const config = mapConfig(settings, parts, notes);
|
|
625
|
+
if (parts.exclusive) notes.push("The custom framework was exclusive (`monopoly`), so built-in presets are turned off (`presets: []`).");
|
|
626
|
+
return {
|
|
627
|
+
config,
|
|
628
|
+
plugin: parts.regex.length > 0 || parts.scopes !== void 0 || parts.templates.length > 0 || Object.keys(parts.localeInfo).length > 0 ? {
|
|
629
|
+
name: "custom",
|
|
630
|
+
...parts.regex.length ? { scanners: { regex: parts.regex } } : {},
|
|
631
|
+
...parts.scopes ? { scopes: { regex: parts.scopes } } : {},
|
|
632
|
+
...parts.templates.length ? { extract: { templates: parts.templates } } : {},
|
|
633
|
+
...Object.keys(parts.localeInfo).length ? { localeInfo: parts.localeInfo } : {}
|
|
634
|
+
} : void 0,
|
|
635
|
+
notes,
|
|
636
|
+
keys,
|
|
637
|
+
customFramework: custom?.path
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/** Converts i18n-ally settings, custom framework and reviews into x18n files. Never deletes i18n-ally files. */
|
|
641
|
+
async function migrateI18nAlly(root, options = {}) {
|
|
642
|
+
const { settings, keys } = await readSettings(root);
|
|
643
|
+
const found = await configOf(root, settings, keys);
|
|
644
|
+
if (!found) return {
|
|
645
|
+
files: [],
|
|
646
|
+
notes: ["No i18n-ally settings found."],
|
|
647
|
+
removable: []
|
|
648
|
+
};
|
|
649
|
+
const { config, plugin } = found;
|
|
650
|
+
const notes = [...found.notes];
|
|
651
|
+
const reviews = (await readFirst(root, [".vscode/i18n-ally-next-reviews.yml", ".vscode/i18n-ally-reviews.yml"]))?.text;
|
|
652
|
+
const files = [];
|
|
653
|
+
const finalConfig = plugin !== void 0 ? {
|
|
654
|
+
...config,
|
|
655
|
+
plugins: ["./x18n/custom.x18n-plugin.yml"]
|
|
656
|
+
} : config;
|
|
657
|
+
files.push(configFile(finalConfig, options.format ?? "ts"));
|
|
658
|
+
if (plugin) files.push({
|
|
659
|
+
path: "x18n/custom.x18n-plugin.yml",
|
|
660
|
+
content: `# yaml-language-server: $schema=../node_modules/@x18n/core/schema/plugin.json\n${stringify(plugin)}`
|
|
661
|
+
});
|
|
662
|
+
let vscodeText = await readText(join(root, ".vscode/settings.json"));
|
|
663
|
+
if (vscodeText !== void 0) {
|
|
664
|
+
let changed = false;
|
|
665
|
+
for (const [from, to] of Object.entries(editorSettings)) if (settings.has(from)) {
|
|
666
|
+
vscodeText = applyEdits(vscodeText, modify(vscodeText, [to], settings.get(from), { formattingOptions: {
|
|
667
|
+
insertSpaces: true,
|
|
668
|
+
tabSize: 2
|
|
669
|
+
} }));
|
|
670
|
+
changed = true;
|
|
671
|
+
}
|
|
672
|
+
if (changed) files.push({
|
|
673
|
+
path: ".vscode/settings.json",
|
|
674
|
+
content: vscodeText
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (reviews !== void 0 && !existsSync(join(root, ".x18n/reviews.yml"))) {
|
|
678
|
+
files.push({
|
|
679
|
+
path: ".x18n/reviews.yml",
|
|
680
|
+
content: reviews
|
|
681
|
+
});
|
|
682
|
+
notes.push("Reviews were copied to `.x18n/reviews.yml`; x18n reads them once reviews ship (1.x).");
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
files,
|
|
686
|
+
notes,
|
|
687
|
+
removable: [...keys, ...found.customFramework ? [found.customFramework] : []]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/load.ts
|
|
692
|
+
/** The nearest directory at or above `cwd`, up to the git root, whose `.vscode/settings.json` has i18n-ally settings. */
|
|
693
|
+
function findI18nAllyRoot(cwd, upward) {
|
|
694
|
+
let dir = resolve(cwd);
|
|
695
|
+
while (true) {
|
|
696
|
+
const settings = join(dir, ".vscode/settings.json");
|
|
697
|
+
if (existsSync(settings) && /"(?:i18n-ally-next|i18n-ally|vue-i18n-ally)\./.test(readFileSync(settings, "utf8"))) return dir;
|
|
698
|
+
const parent = dirname(dir);
|
|
699
|
+
if (!upward || parent === dir || existsSync(join(dir, ".git"))) return void 0;
|
|
700
|
+
dir = parent;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Without an x18n config, the project's i18n-ally settings act as defaults, so an i18n-ally project
|
|
705
|
+
* works as it did in the editor. A plugin applies them, so detection and the locale guess still fill gaps.
|
|
706
|
+
*/
|
|
707
|
+
async function i18nAllyFallback(root) {
|
|
708
|
+
const found = await readI18nAllyConfig(root);
|
|
709
|
+
if (!found) return void 0;
|
|
710
|
+
const { presets, plugins: _plugins, ...defaults } = found.config;
|
|
711
|
+
const settingsPlugin = {
|
|
712
|
+
name: "x18n:i18n-ally-settings",
|
|
713
|
+
config: (draft) => applyDefaults(draft, defaults)
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
...presets ? { presets } : {},
|
|
717
|
+
plugins: [settingsPlugin, ...found.plugin ? [compileYamlPlugin(found.plugin)] : []]
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function applyDefaults(draft, config) {
|
|
721
|
+
if (config.sourceLocale) draft.sourceLocale = config.sourceLocale;
|
|
722
|
+
if (config.ignoreLocales) draft.ignoreLocales = [...draft.ignoreLocales, ...config.ignoreLocales];
|
|
723
|
+
if (config.resources) draft.resources = [...draft.resources, ...config.resources];
|
|
724
|
+
if (config.formats) draft.formats = {
|
|
725
|
+
...draft.formats,
|
|
726
|
+
...config.formats
|
|
727
|
+
};
|
|
728
|
+
if (config.keyStyle) draft.keyStyle = config.keyStyle;
|
|
729
|
+
if (config.namespace?.default) draft.namespace = {
|
|
730
|
+
...draft.namespace,
|
|
731
|
+
default: config.namespace.default
|
|
732
|
+
};
|
|
733
|
+
if (config.usage) draft.usage = {
|
|
734
|
+
...draft.usage,
|
|
735
|
+
keysInUse: [...draft.usage.keysInUse, ...config.usage.keysInUse ?? []],
|
|
736
|
+
derivedKeys: [...draft.usage.derivedKeys, ...config.usage.derivedKeys ?? []],
|
|
737
|
+
exclude: [...draft.usage.exclude, ...config.usage.exclude ?? []]
|
|
738
|
+
};
|
|
739
|
+
if (config.write) draft.write = {
|
|
740
|
+
sort: config.write.sort ?? draft.write.sort,
|
|
741
|
+
indent: config.write.indent ?? draft.write.indent,
|
|
742
|
+
keepFilled: config.write.keepFilled ?? draft.write.keepFilled
|
|
743
|
+
};
|
|
744
|
+
if (config.extract) draft.extract = {
|
|
745
|
+
...draft.extract,
|
|
746
|
+
...config.extract
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function projectSpecs(rootConfigFile, cwd, upward) {
|
|
750
|
+
if (!rootConfigFile) {
|
|
751
|
+
const settingsRoot = findI18nAllyRoot(cwd, upward);
|
|
752
|
+
const fallback = settingsRoot && await i18nAllyFallback(settingsRoot);
|
|
753
|
+
if (settingsRoot && fallback) return {
|
|
754
|
+
root: settingsRoot,
|
|
755
|
+
specs: [{
|
|
756
|
+
root: settingsRoot,
|
|
757
|
+
config: fallback,
|
|
758
|
+
parent: void 0,
|
|
759
|
+
configFile: join(settingsRoot, ".vscode/settings.json")
|
|
760
|
+
}]
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const configDir = rootConfigFile ? dirname(rootConfigFile) : resolve(cwd);
|
|
764
|
+
const rootConfig = rootConfigFile ? await loadConfigFile(rootConfigFile) : {};
|
|
765
|
+
const root = resolve(configDir, rootConfig.root ?? ".");
|
|
766
|
+
if (!rootConfig.projects?.length) return {
|
|
767
|
+
root,
|
|
768
|
+
specs: [{
|
|
769
|
+
root,
|
|
770
|
+
config: rootConfig,
|
|
771
|
+
parent: void 0,
|
|
772
|
+
configFile: rootConfigFile
|
|
773
|
+
}]
|
|
774
|
+
};
|
|
775
|
+
const { projects, ...parent } = rootConfig;
|
|
776
|
+
const dirs = await glob([...projects], {
|
|
777
|
+
cwd: root,
|
|
778
|
+
onlyDirectories: true,
|
|
779
|
+
ignore: ["**/node_modules/**"]
|
|
780
|
+
});
|
|
781
|
+
const specs = [];
|
|
782
|
+
for (const dir of dirs.sort()) {
|
|
783
|
+
const projectRoot = resolve(root, dir);
|
|
784
|
+
const file = findConfigFile(projectRoot);
|
|
785
|
+
const config = file ? await loadConfigFile(file) : {};
|
|
786
|
+
specs.push({
|
|
787
|
+
root: resolve(projectRoot, config.root ?? "."),
|
|
788
|
+
config,
|
|
789
|
+
parent,
|
|
790
|
+
configFile: file
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
root,
|
|
795
|
+
specs
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/** Finds the config, resolves every project and waits until each is indexed. */
|
|
799
|
+
async function loadWorkspace(options) {
|
|
800
|
+
const configFile = options.configFile ? resolve(options.cwd, options.configFile) : options.upward === false ? findConfigFile(resolve(options.cwd)) : findConfigUpward(options.cwd);
|
|
801
|
+
const { root, specs } = await projectSpecs(configFile, options.cwd, options.upward !== false);
|
|
802
|
+
const builtins = options.builtins ?? createBuiltins();
|
|
803
|
+
const gitRoot = findGitRoot(root);
|
|
804
|
+
const loaded = [];
|
|
805
|
+
for (const spec of specs) {
|
|
806
|
+
const name = spec.config.name ?? (spec.root === root ? relative(dirname(root), root) : relative(root, spec.root)).replace(/\\/g, "/");
|
|
807
|
+
if (options.project !== void 0 && options.project !== name) continue;
|
|
808
|
+
const host = createNodeHost(spec.root, {
|
|
809
|
+
cacheDir: options.cache === false ? false : void 0,
|
|
810
|
+
logger: options.logger
|
|
811
|
+
});
|
|
812
|
+
const projectHost = options.wrapHost?.(host) ?? host;
|
|
813
|
+
const setup = await resolveConfig({
|
|
814
|
+
host: projectHost,
|
|
815
|
+
config: spec.config,
|
|
816
|
+
parent: spec.parent,
|
|
817
|
+
builtins,
|
|
818
|
+
name,
|
|
819
|
+
runtime: "node",
|
|
820
|
+
loadPlugin: createPluginLoader(spec.configFile ? dirname(spec.configFile) : spec.root),
|
|
821
|
+
manifestRoots: await manifestRootsOf(spec.root, gitRoot)
|
|
822
|
+
});
|
|
823
|
+
const project = createProject({
|
|
824
|
+
host: projectHost,
|
|
825
|
+
setup
|
|
826
|
+
});
|
|
827
|
+
await project.ready;
|
|
828
|
+
await host.flush();
|
|
829
|
+
loaded.push({
|
|
830
|
+
name,
|
|
831
|
+
root: spec.root,
|
|
832
|
+
configFile: spec.configFile,
|
|
833
|
+
host,
|
|
834
|
+
project
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
if (options.project !== void 0 && loaded.length === 0) throw new X18nError("X18N0003", options.project);
|
|
838
|
+
return {
|
|
839
|
+
root,
|
|
840
|
+
configFile,
|
|
841
|
+
projects: loaded
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
//#endregion
|
|
845
|
+
export { configFileNames, createNodeHost, createPluginLoader, findConfigFile, findConfigUpward, findGitRoot, importModule, loadConfigFile, loadWorkspace, loadYamlPlugin, migrateI18nAlly, stderrLogger };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x18n/node",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"description": "Node.js host for x18n: file system access, config and plugin loading",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"node": ">=22"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@x18n/builtins": "0.0.
|
|
29
|
-
"@x18n/core": "0.0.
|
|
28
|
+
"@x18n/builtins": "0.0.3",
|
|
29
|
+
"@x18n/core": "0.0.3",
|
|
30
30
|
"chokidar": "^5.0.0",
|
|
31
31
|
"ignore": "^7.0.12",
|
|
32
32
|
"jiti": "^2.6.1",
|