@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.
Files changed (2) hide show
  1. package/dist/index.mjs +209 -14
  2. 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 { execFile, spawn } from "node:child_process";
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
- const toPosix = (path) => sep === "\\" ? path.replace(/\\/g, "/") : path;
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
- const asStrings = (value) => (Array.isArray(value) ? value : value === void 0 ? [] : [value]).filter((v) => typeof v === "string");
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 ([...settings.keys()].some((key) => key.startsWith("review."))) notes.push("Review settings move to `review.*` once reviews ship (1.x); they were not migrated yet.");
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 = (await readFirst(root, [".vscode/i18n-ally-next-reviews.yml", ".vscode/i18n-ally-reviews.yml"]))?.text;
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: ["./x18n/custom.x18n-plugin.yml"]
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 copied to `.x18n/reviews.yml`; x18n reads them once reviews ship (1.x).");
1239
+ notes.push("Reviews were converted to `.x18n/reviews.yml`.");
1058
1240
  }
1059
1241
  return {
1060
1242
  files,
1061
1243
  notes,
1062
- removable: [...keys, ...found.customFramework ? [found.customFramework] : []]
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.13",
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.13",
29
- "@x18n/core": "0.0.13",
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",