@x18n/node 0.0.13 → 0.0.15
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.mjs +209 -14
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -3,18 +3,90 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { compileYamlPlugin } from "@x18n/builtins/yaml-plugin";
|
|
7
6
|
import { PluginFileSchema, createProject, errors, hashText, isX18nError, resolveConfig, validate } from "@x18n/core";
|
|
8
7
|
import { createJiti } from "jiti";
|
|
9
8
|
import { glob } from "tinyglobby";
|
|
10
9
|
import { parse, stringify } from "yaml";
|
|
10
|
+
import { placeholderFromShape } from "@x18n/builtins/placeholders";
|
|
11
|
+
import regexScopes from "@x18n/builtins/scanners/regex-scopes";
|
|
12
|
+
import { execFile, spawn } from "node:child_process";
|
|
11
13
|
import process from "node:process";
|
|
12
14
|
import { watch } from "chokidar";
|
|
13
15
|
import ignore from "ignore";
|
|
14
16
|
import { createBuiltins } from "@x18n/builtins";
|
|
15
17
|
import { Buffer } from "node:buffer";
|
|
16
|
-
import {
|
|
18
|
+
import { yamlReviewStore } from "@x18n/builtins/reviews";
|
|
17
19
|
import { applyEdits, modify, parse as parse$1 } from "jsonc-parser";
|
|
20
|
+
//#region src/yaml-plugin.ts
|
|
21
|
+
/**
|
|
22
|
+
* Compiles a validated YAML plugin into plugin objects. Each section maps to the
|
|
23
|
+
* options of a built-in factory, so moving to a TS plugin is a small change.
|
|
24
|
+
*/
|
|
25
|
+
function compileYamlPlugin(data) {
|
|
26
|
+
const replacements = (data.normalizeKey ?? []).map((rule) => ({
|
|
27
|
+
from: new RegExp(rule.replace.pattern, "g"),
|
|
28
|
+
to: rule.replace.with
|
|
29
|
+
}));
|
|
30
|
+
const localeInfo = data.localeInfo;
|
|
31
|
+
const plugin = {
|
|
32
|
+
name: data.name,
|
|
33
|
+
config(draft) {
|
|
34
|
+
const defaults = data.config;
|
|
35
|
+
if (defaults?.sourceLocale) draft.sourceLocale = defaults.sourceLocale;
|
|
36
|
+
if (defaults?.locales) draft.locales = defaults.locales;
|
|
37
|
+
if (defaults?.resources && draft.resources.length === 0) draft.resources = defaults.resources;
|
|
38
|
+
if (defaults?.keyStyle) draft.keyStyle = defaults.keyStyle;
|
|
39
|
+
if (defaults?.namespace) draft.namespace = {
|
|
40
|
+
separator: defaults.namespace.separator ?? draft.namespace.separator,
|
|
41
|
+
default: defaults.namespace.default ?? draft.namespace.default,
|
|
42
|
+
fallback: defaults.namespace.fallback ?? draft.namespace.fallback,
|
|
43
|
+
byFile: {
|
|
44
|
+
...draft.namespace.byFile,
|
|
45
|
+
...defaults.namespace.byFile
|
|
46
|
+
},
|
|
47
|
+
fromPath: defaults.namespace.fromPath ?? draft.namespace.fromPath
|
|
48
|
+
};
|
|
49
|
+
draft.usage = {
|
|
50
|
+
...draft.usage,
|
|
51
|
+
calls: [...draft.usage.calls, ...data.scanners?.calls ?? []],
|
|
52
|
+
regex: [...draft.usage.regex, ...data.scanners?.regex ?? []]
|
|
53
|
+
};
|
|
54
|
+
if (data.extract?.templates) draft.extract = {
|
|
55
|
+
...draft.extract,
|
|
56
|
+
templates: [...draft.extract.templates ?? [], ...data.extract.templates]
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
derivedKeys: data.derivedKeys,
|
|
60
|
+
placeholders: data.placeholders?.map(placeholderFromShape),
|
|
61
|
+
normalizeKey: replacements.length > 0 ? (raw) => replacements.reduce((text, rule) => text.replace(rule.from, rule.to), raw) : void 0,
|
|
62
|
+
localeInfo: localeInfo ? (code) => {
|
|
63
|
+
const info = localeInfo[code];
|
|
64
|
+
return info ? {
|
|
65
|
+
...info.flag ? { flag: info.flag } : {},
|
|
66
|
+
...info.displayName ? { displayName: info.displayName } : {}
|
|
67
|
+
} : void 0;
|
|
68
|
+
} : void 0
|
|
69
|
+
};
|
|
70
|
+
const plugins = [plugin, ...data.scopes ? [regexScopes(data.scopes.regex, `${data.name}-scopes`)] : []];
|
|
71
|
+
const detect = data.detect;
|
|
72
|
+
if (!detect) return plugins.length === 1 ? plugin : {
|
|
73
|
+
name: data.name,
|
|
74
|
+
plugins
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
name: data.name,
|
|
78
|
+
exclusive: data.exclusive,
|
|
79
|
+
plugins,
|
|
80
|
+
async detect(ctx) {
|
|
81
|
+
const deps = detect.dependencies;
|
|
82
|
+
if (deps?.any && !ctx.hasAnyDependency(deps.any)) return false;
|
|
83
|
+
if (deps?.all && !deps.all.every(ctx.hasDependency)) return false;
|
|
84
|
+
for (const pattern of detect.files ?? []) if (!await ctx.fileExists(pattern)) return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
18
90
|
//#region src/config.ts
|
|
19
91
|
const configFileNames = [
|
|
20
92
|
"x18n.config.ts",
|
|
@@ -43,6 +115,15 @@ function findConfigUpward(cwd) {
|
|
|
43
115
|
dir = parent;
|
|
44
116
|
}
|
|
45
117
|
}
|
|
118
|
+
/** One config file per directory below `dir`, shallowest first. */
|
|
119
|
+
async function findConfigsBelow(dir) {
|
|
120
|
+
const files = await glob(configFileNames.map((name) => `**/${name}`), {
|
|
121
|
+
cwd: dir,
|
|
122
|
+
absolute: true,
|
|
123
|
+
ignore: ["**/node_modules/**"]
|
|
124
|
+
});
|
|
125
|
+
return [...new Set(files.map((file) => dirname(file)))].sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)).flatMap((found) => findConfigFile(found) ?? []);
|
|
126
|
+
}
|
|
46
127
|
function resolveOwn(specifier) {
|
|
47
128
|
try {
|
|
48
129
|
return fileURLToPath(import.meta.resolve(specifier));
|
|
@@ -153,7 +234,9 @@ async function manifestRootsOf(root, gitRoot) {
|
|
|
153
234
|
}
|
|
154
235
|
//#endregion
|
|
155
236
|
//#region src/host.ts
|
|
156
|
-
|
|
237
|
+
function toPosix(path) {
|
|
238
|
+
return sep === "\\" ? path.replace(/\\/g, "/") : path;
|
|
239
|
+
}
|
|
157
240
|
function isNotFound(error) {
|
|
158
241
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "EISDIR");
|
|
159
242
|
}
|
|
@@ -205,6 +288,24 @@ async function loadGitignore(root) {
|
|
|
205
288
|
});
|
|
206
289
|
};
|
|
207
290
|
}
|
|
291
|
+
function gitConfig(cwd, key) {
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
execFile("git", ["config", key], {
|
|
294
|
+
cwd,
|
|
295
|
+
encoding: "utf8"
|
|
296
|
+
}, (error, stdout) => {
|
|
297
|
+
resolve(error ? void 0 : stdout.trim() || void 0);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/** The git `user.name` and `user.email`, or undefined without git or a configured user. */
|
|
302
|
+
async function gitIdentity(cwd) {
|
|
303
|
+
const [name, email] = await Promise.all([gitConfig(cwd, "user.name"), gitConfig(cwd, "user.email")]);
|
|
304
|
+
return name === void 0 && email === void 0 ? void 0 : {
|
|
305
|
+
...name !== void 0 && { name },
|
|
306
|
+
...email !== void 0 && { email }
|
|
307
|
+
};
|
|
308
|
+
}
|
|
208
309
|
/** A persistent cache in one JSON file. Only entries used in this session are written back, so it stays bounded. */
|
|
209
310
|
function createFileCache(dir) {
|
|
210
311
|
const file = join(dir, "cache.json");
|
|
@@ -242,6 +343,7 @@ function createNodeHost(root, options = {}) {
|
|
|
242
343
|
const cacheDir = options.cacheDir === false ? void 0 : options.cacheDir ?? join(absoluteRoot, "node_modules/.cache/x18n");
|
|
243
344
|
const cache = cacheDir ? createFileCache(join(cacheDir, hashText(absoluteRoot))) : void 0;
|
|
244
345
|
const full = (path) => resolve(absoluteRoot, path);
|
|
346
|
+
let identity;
|
|
245
347
|
return {
|
|
246
348
|
root: absoluteRoot,
|
|
247
349
|
logger: options.logger,
|
|
@@ -301,6 +403,7 @@ function createNodeHost(root, options = {}) {
|
|
|
301
403
|
watcher.close();
|
|
302
404
|
} };
|
|
303
405
|
},
|
|
406
|
+
identity: () => identity ??= gitIdentity(absoluteRoot),
|
|
304
407
|
flush: async () => cache?.flush()
|
|
305
408
|
};
|
|
306
409
|
}
|
|
@@ -686,7 +789,7 @@ const frameworkPresets = {
|
|
|
686
789
|
"ui5": "ui5",
|
|
687
790
|
"lingui": "lingui",
|
|
688
791
|
"jekyll": "jekyll",
|
|
689
|
-
"i18n-js": "i18n-js"
|
|
792
|
+
"i18n-js": "ruby-i18n-js"
|
|
690
793
|
};
|
|
691
794
|
const languageExtensions = {
|
|
692
795
|
javascript: [
|
|
@@ -759,7 +862,9 @@ async function readFirst(root, paths) {
|
|
|
759
862
|
function isRecord(value) {
|
|
760
863
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
761
864
|
}
|
|
762
|
-
|
|
865
|
+
function asStrings(value) {
|
|
866
|
+
return (Array.isArray(value) ? value : value === void 0 ? [] : [value]).filter((v) => typeof v === "string");
|
|
867
|
+
}
|
|
763
868
|
/** Setting prefixes by priority: the i18n-ally-next fork renames `i18n-ally.*` keys, so its value is the newest. */
|
|
764
869
|
const settingPrefixes = [
|
|
765
870
|
"i18n-ally-next",
|
|
@@ -928,7 +1033,7 @@ function presetsOf(r, notes) {
|
|
|
928
1033
|
}
|
|
929
1034
|
function notesOf(settings, notes) {
|
|
930
1035
|
if ([...settings.keys()].some((key) => key.startsWith("translate."))) notes.push("`translate.*` settings were dropped: x18n writes translate prompts instead (`x18n prompt translate --to <locale>`).");
|
|
931
|
-
if (
|
|
1036
|
+
if (settings.has("review.removeCommentOnResolved")) notes.push("`i18n-ally.review.removeCommentOnResolved` was dropped: x18n keeps resolved reviews and hides them with `x18n review list --open`.");
|
|
932
1037
|
for (const key of handledByDefaults) if (settings.has(key)) notes.push(`\`i18n-ally.${key}\` is handled by defaults or dropped.`);
|
|
933
1038
|
}
|
|
934
1039
|
function mapConfig(settings, parts, notes) {
|
|
@@ -975,8 +1080,85 @@ function mapConfig(settings, parts, notes) {
|
|
|
975
1080
|
...r.str("defaultNamespace") ? { namespace: { default: r.str("defaultNamespace") } } : {},
|
|
976
1081
|
...Object.keys(usage).length ? { usage } : {},
|
|
977
1082
|
...write ? { write } : {},
|
|
978
|
-
...Object.keys(extract).length ? { extract } : {}
|
|
1083
|
+
...Object.keys(extract).length ? { extract } : {},
|
|
1084
|
+
...reviewOf(r)
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
function reviewOf(r) {
|
|
1088
|
+
const enabled = r.get("review.enabled");
|
|
1089
|
+
const author = {
|
|
1090
|
+
...r.str("review.user.name") && { name: r.str("review.user.name") },
|
|
1091
|
+
...r.str("review.user.email") && { email: r.str("review.user.email") }
|
|
1092
|
+
};
|
|
1093
|
+
const review = {
|
|
1094
|
+
...typeof enabled === "boolean" && { enabled },
|
|
1095
|
+
...Object.keys(author).length > 0 && { author }
|
|
979
1096
|
};
|
|
1097
|
+
return Object.keys(review).length > 0 ? { review } : {};
|
|
1098
|
+
}
|
|
1099
|
+
const reviewTypes = {
|
|
1100
|
+
comment: "comment",
|
|
1101
|
+
approve: "approve",
|
|
1102
|
+
request_change: "request-change"
|
|
1103
|
+
};
|
|
1104
|
+
function str(value) {
|
|
1105
|
+
return typeof value === "string" ? value : void 0;
|
|
1106
|
+
}
|
|
1107
|
+
function recordOf(value) {
|
|
1108
|
+
return isRecord(value) ? value : {};
|
|
1109
|
+
}
|
|
1110
|
+
/** One i18n-ally comment; one with a `suggestion` becomes a suggestion. */
|
|
1111
|
+
function convertComment(comment) {
|
|
1112
|
+
const user = recordOf(comment.user);
|
|
1113
|
+
const name = str(user.name);
|
|
1114
|
+
const email = str(user.email);
|
|
1115
|
+
const text = str(comment.comment);
|
|
1116
|
+
const suggestion = str(comment.suggestion);
|
|
1117
|
+
return {
|
|
1118
|
+
id: str(comment.id) ?? hashText(JSON.stringify(comment)).slice(0, 6),
|
|
1119
|
+
type: suggestion === void 0 ? reviewTypes[str(comment.type) ?? "comment"] ?? "comment" : "suggestion",
|
|
1120
|
+
...(name !== void 0 || email !== void 0) && { author: {
|
|
1121
|
+
...name !== void 0 && { name },
|
|
1122
|
+
...email !== void 0 && { email }
|
|
1123
|
+
} },
|
|
1124
|
+
time: str(comment.time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1125
|
+
...text !== void 0 && { text },
|
|
1126
|
+
...suggestion !== void 0 && { suggestion },
|
|
1127
|
+
...comment.resolved === true && { resolved: true }
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
/** The comments of one key and locale, with the translation candidate as a last suggestion. */
|
|
1131
|
+
function convertLocale(key, locale, item) {
|
|
1132
|
+
const comments = (Array.isArray(item.comments) ? item.comments : []).filter(isRecord).map(convertComment);
|
|
1133
|
+
const candidate = recordOf(item.translation_candidate);
|
|
1134
|
+
const text = str(candidate.text);
|
|
1135
|
+
if (text !== void 0) comments.push({
|
|
1136
|
+
id: hashText(`${key}:${locale}:${text}`).slice(0, 6),
|
|
1137
|
+
type: "suggestion",
|
|
1138
|
+
time: str(candidate.time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1139
|
+
text: "Translation candidate from i18n-ally",
|
|
1140
|
+
suggestion: text
|
|
1141
|
+
});
|
|
1142
|
+
return comments;
|
|
1143
|
+
}
|
|
1144
|
+
/** i18n-ally's `reviews.<key>.locales.<locale>.comments` as x18n threads; translation candidates become suggestions. */
|
|
1145
|
+
function convertI18nAllyReviews(text, notes) {
|
|
1146
|
+
const reviews = recordOf(recordOf(parse(text)).reviews);
|
|
1147
|
+
const threads = {};
|
|
1148
|
+
let descriptions = 0;
|
|
1149
|
+
for (const [key, entry] of Object.entries(reviews)) {
|
|
1150
|
+
const { description, locales } = recordOf(entry);
|
|
1151
|
+
if (description !== void 0) descriptions++;
|
|
1152
|
+
for (const [locale, item] of Object.entries(recordOf(locales))) {
|
|
1153
|
+
const comments = convertLocale(key, locale, recordOf(item));
|
|
1154
|
+
if (comments.length > 0) (threads[key] ??= {})[locale] = comments;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (descriptions > 0) notes.push(`${descriptions} key description${descriptions === 1 ? " was" : "s were"} dropped: x18n reviews have no key descriptions.`);
|
|
1158
|
+
return yamlReviewStore.serialize({
|
|
1159
|
+
threads,
|
|
1160
|
+
sources: {}
|
|
1161
|
+
});
|
|
980
1162
|
}
|
|
981
1163
|
/** The x18n config that a project's i18n-ally settings and custom framework mean, or undefined without any. */
|
|
982
1164
|
async function readI18nAllyConfig(root) {
|
|
@@ -1023,15 +1205,15 @@ async function migrateI18nAlly(root, options = {}) {
|
|
|
1023
1205
|
};
|
|
1024
1206
|
const { config, plugin } = found;
|
|
1025
1207
|
const notes = [...found.notes];
|
|
1026
|
-
const reviews =
|
|
1208
|
+
const reviews = await readFirst(root, [".vscode/i18n-ally-next-reviews.yml", ".vscode/i18n-ally-reviews.yml"]);
|
|
1027
1209
|
const files = [];
|
|
1028
1210
|
const finalConfig = plugin !== void 0 ? {
|
|
1029
1211
|
...config,
|
|
1030
|
-
plugins: ["
|
|
1212
|
+
plugins: ["./.x18n/custom.x18n-plugin.yml"]
|
|
1031
1213
|
} : config;
|
|
1032
1214
|
files.push(configFile(finalConfig, options.format ?? "ts"));
|
|
1033
1215
|
if (plugin) files.push({
|
|
1034
|
-
path: "x18n/custom.x18n-plugin.yml",
|
|
1216
|
+
path: ".x18n/custom.x18n-plugin.yml",
|
|
1035
1217
|
content: `# yaml-language-server: $schema=../node_modules/@x18n/core/schema/plugin.json\n${stringify(plugin)}`
|
|
1036
1218
|
});
|
|
1037
1219
|
let vscodeText = await readText(join(root, ".vscode/settings.json"));
|
|
@@ -1052,14 +1234,18 @@ async function migrateI18nAlly(root, options = {}) {
|
|
|
1052
1234
|
if (reviews !== void 0 && !existsSync(join(root, ".x18n/reviews.yml"))) {
|
|
1053
1235
|
files.push({
|
|
1054
1236
|
path: ".x18n/reviews.yml",
|
|
1055
|
-
content: reviews
|
|
1237
|
+
content: convertI18nAllyReviews(reviews.text, notes)
|
|
1056
1238
|
});
|
|
1057
|
-
notes.push("Reviews were
|
|
1239
|
+
notes.push("Reviews were converted to `.x18n/reviews.yml`.");
|
|
1058
1240
|
}
|
|
1059
1241
|
return {
|
|
1060
1242
|
files,
|
|
1061
1243
|
notes,
|
|
1062
|
-
removable: [
|
|
1244
|
+
removable: [
|
|
1245
|
+
...keys,
|
|
1246
|
+
...found.customFramework ? [found.customFramework] : [],
|
|
1247
|
+
...reviews ? [reviews.path] : []
|
|
1248
|
+
]
|
|
1063
1249
|
};
|
|
1064
1250
|
}
|
|
1065
1251
|
//#endregion
|
|
@@ -1127,6 +1313,15 @@ function applyDefaults(draft, config) {
|
|
|
1127
1313
|
}
|
|
1128
1314
|
async function projectSpecs(rootConfigFile, cwd, upward) {
|
|
1129
1315
|
if (!rootConfigFile) {
|
|
1316
|
+
const nested = await findConfigsBelow(cwd);
|
|
1317
|
+
if (nested.length) {
|
|
1318
|
+
const specs = [];
|
|
1319
|
+
for (const file of nested) if (!specs.some((spec) => spec.configFile === file)) specs.push(...(await projectSpecs(file, cwd, false)).specs);
|
|
1320
|
+
return {
|
|
1321
|
+
root: resolve(cwd),
|
|
1322
|
+
specs
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1130
1325
|
const settingsRoot = findI18nAllyRoot(cwd, upward);
|
|
1131
1326
|
const fallback = settingsRoot && await i18nAllyFallback(settingsRoot);
|
|
1132
1327
|
if (settingsRoot && fallback) return {
|
|
@@ -1179,7 +1374,7 @@ async function loadWorkspace(options) {
|
|
|
1179
1374
|
const configFile = options.configFile ? resolve(options.cwd, options.configFile) : options.upward === false ? findConfigFile(resolve(options.cwd)) : findConfigUpward(options.cwd);
|
|
1180
1375
|
const { root, specs } = await projectSpecs(configFile, options.cwd, options.upward !== false);
|
|
1181
1376
|
const gitRoot = findGitRoot(root);
|
|
1182
|
-
const defaults = options.builtins ?? createBuiltins();
|
|
1377
|
+
const defaults = options.builtins ?? await createBuiltins();
|
|
1183
1378
|
const builtins = gitRoot ? {
|
|
1184
1379
|
...defaults,
|
|
1185
1380
|
plugins: [...defaults.plugins, gitHistory()]
|
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.15",
|
|
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.15",
|
|
29
|
+
"@x18n/core": "0.0.15",
|
|
30
30
|
"chokidar": "^6.0.0",
|
|
31
31
|
"ignore": "^7.0.13",
|
|
32
32
|
"jiti": "^2.7.0",
|