@katerynakhar/i18n-keeper 0.16.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +666 -0
  3. package/dist/apply.d.ts +66 -0
  4. package/dist/apply.js +254 -0
  5. package/dist/apply.js.map +1 -0
  6. package/dist/check.d.ts +5 -0
  7. package/dist/check.js +326 -0
  8. package/dist/check.js.map +1 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +525 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/formats/error.d.ts +12 -0
  13. package/dist/formats/error.js +19 -0
  14. package/dist/formats/error.js.map +1 -0
  15. package/dist/formats/flatten.d.ts +6 -0
  16. package/dist/formats/flatten.js +26 -0
  17. package/dist/formats/flatten.js.map +1 -0
  18. package/dist/formats/json-write.d.ts +7 -0
  19. package/dist/formats/json-write.js +55 -0
  20. package/dist/formats/json-write.js.map +1 -0
  21. package/dist/formats/json.d.ts +6 -0
  22. package/dist/formats/json.js +21 -0
  23. package/dist/formats/json.js.map +1 -0
  24. package/dist/formats/php-write.d.ts +16 -0
  25. package/dist/formats/php-write.js +130 -0
  26. package/dist/formats/php-write.js.map +1 -0
  27. package/dist/formats/php.d.ts +44 -0
  28. package/dist/formats/php.js +356 -0
  29. package/dist/formats/php.js.map +1 -0
  30. package/dist/formats/po-write.d.ts +3 -0
  31. package/dist/formats/po-write.js +200 -0
  32. package/dist/formats/po-write.js.map +1 -0
  33. package/dist/formats/po.d.ts +30 -0
  34. package/dist/formats/po.js +202 -0
  35. package/dist/formats/po.js.map +1 -0
  36. package/dist/formats/write.d.ts +29 -0
  37. package/dist/formats/write.js +19 -0
  38. package/dist/formats/write.js.map +1 -0
  39. package/dist/formats/yaml-write.d.ts +12 -0
  40. package/dist/formats/yaml-write.js +42 -0
  41. package/dist/formats/yaml-write.js.map +1 -0
  42. package/dist/formats/yaml.d.ts +18 -0
  43. package/dist/formats/yaml.js +51 -0
  44. package/dist/formats/yaml.js.map +1 -0
  45. package/dist/glossary.d.ts +38 -0
  46. package/dist/glossary.js +140 -0
  47. package/dist/glossary.js.map +1 -0
  48. package/dist/lengths.d.ts +37 -0
  49. package/dist/lengths.js +195 -0
  50. package/dist/lengths.js.map +1 -0
  51. package/dist/mcp.d.ts +2 -0
  52. package/dist/mcp.js +374 -0
  53. package/dist/mcp.js.map +1 -0
  54. package/dist/memory.d.ts +72 -0
  55. package/dist/memory.js +162 -0
  56. package/dist/memory.js.map +1 -0
  57. package/dist/placeholders.d.ts +15 -0
  58. package/dist/placeholders.js +103 -0
  59. package/dist/placeholders.js.map +1 -0
  60. package/dist/plurals.d.ts +59 -0
  61. package/dist/plurals.js +280 -0
  62. package/dist/plurals.js.map +1 -0
  63. package/dist/report.d.ts +2 -0
  64. package/dist/report.js +126 -0
  65. package/dist/report.js.map +1 -0
  66. package/dist/scan.d.ts +23 -0
  67. package/dist/scan.js +250 -0
  68. package/dist/scan.js.map +1 -0
  69. package/dist/translate.d.ts +93 -0
  70. package/dist/translate.js +369 -0
  71. package/dist/translate.js.map +1 -0
  72. package/dist/types.d.ts +88 -0
  73. package/dist/types.js +50 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/version.d.ts +1 -0
  76. package/dist/version.js +28 -0
  77. package/dist/version.js.map +1 -0
  78. package/package.json +77 -0
  79. package/src/apply.ts +334 -0
  80. package/src/check.ts +468 -0
  81. package/src/cli.ts +637 -0
  82. package/src/formats/error.ts +19 -0
  83. package/src/formats/flatten.ts +38 -0
  84. package/src/formats/json-write.ts +66 -0
  85. package/src/formats/json.ts +27 -0
  86. package/src/formats/php-write.ts +146 -0
  87. package/src/formats/php.ts +414 -0
  88. package/src/formats/po-write.ts +230 -0
  89. package/src/formats/po.ts +233 -0
  90. package/src/formats/write.ts +41 -0
  91. package/src/formats/yaml-write.ts +53 -0
  92. package/src/formats/yaml.ts +62 -0
  93. package/src/glossary.ts +191 -0
  94. package/src/lengths.ts +214 -0
  95. package/src/mcp.ts +451 -0
  96. package/src/memory.ts +227 -0
  97. package/src/placeholders.ts +123 -0
  98. package/src/plurals.ts +320 -0
  99. package/src/report.ts +162 -0
  100. package/src/scan.ts +280 -0
  101. package/src/translate.ts +486 -0
  102. package/src/types.ts +136 -0
  103. package/src/version.ts +27 -0
package/dist/scan.js ADDED
@@ -0,0 +1,250 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { basename, join, relative, resolve, sep } from 'node:path';
3
+ import { FormatError } from './formats/error.js';
4
+ import { readJsonLocale } from './formats/json.js';
5
+ import { readPhpLocale } from './formats/php.js';
6
+ import { readPoLocale } from './formats/po.js';
7
+ import { readYamlLocale } from './formats/yaml.js';
8
+ import { DEFAULT_RULES, } from './types.js';
9
+ import { DEFAULT_SYNTAXES } from './placeholders.js';
10
+ const CANDIDATE_DIRS = [
11
+ 'locales',
12
+ 'src/locales',
13
+ 'public/locales',
14
+ 'app/locales',
15
+ 'i18n',
16
+ 'src/i18n',
17
+ 'lang',
18
+ 'resources/lang',
19
+ 'translations',
20
+ 'src/translations',
21
+ 'config/locales',
22
+ 'locale',
23
+ 'po',
24
+ ];
25
+ /** Formats we can read, longest extension first so stripping is unambiguous. */
26
+ const EXTENSIONS = ['.json', '.yaml', '.php', '.yml', '.pot', '.po'];
27
+ function readerFor(file) {
28
+ if (file.endsWith('.php'))
29
+ return readPhpLocale;
30
+ if (file.endsWith('.po') || file.endsWith('.pot'))
31
+ return readPoLocale;
32
+ if (file.endsWith('.yml') || file.endsWith('.yaml'))
33
+ return readYamlLocale;
34
+ return readJsonLocale;
35
+ }
36
+ function stripExtension(name) {
37
+ for (const ext of EXTENSIONS) {
38
+ if (name.endsWith(ext))
39
+ return name.slice(0, -ext.length);
40
+ }
41
+ return null;
42
+ }
43
+ const LOCALE_CODE = /^[a-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})*$/;
44
+ export class ScanError extends Error {
45
+ }
46
+ function isDir(p) {
47
+ try {
48
+ return statSync(p).isDirectory();
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /** Finds the directory holding locale files, or throws with the paths it tried. */
55
+ export function findLocalesDir(root, explicit) {
56
+ if (explicit) {
57
+ const dir = resolve(root, explicit);
58
+ if (!isDir(dir))
59
+ throw new ScanError(`Locales directory not found: ${dir}`);
60
+ return dir;
61
+ }
62
+ for (const candidate of CANDIDATE_DIRS) {
63
+ const dir = resolve(root, candidate);
64
+ if (isDir(dir) && listLocales(dir).locales.length > 0)
65
+ return dir;
66
+ }
67
+ throw new ScanError(`No locales directory found under ${root}.\nTried: ${CANDIDATE_DIRS.join(', ')}\nPass one explicitly with --locales <path>.`);
68
+ }
69
+ /** `locales/en.json` is flat; `locales/en/common.json` is nested. */
70
+ export function listLocales(dir) {
71
+ const entries = readdirSync(dir, { withFileTypes: true });
72
+ const nested = entries
73
+ .filter((e) => e.isDirectory() && LOCALE_CODE.test(e.name))
74
+ .map((e) => e.name);
75
+ if (nested.length > 0)
76
+ return { layout: 'nested', locales: nested.sort() };
77
+ const flat = entries
78
+ .filter((e) => e.isFile())
79
+ .map((e) => stripExtension(e.name))
80
+ .filter((name) => name !== null && LOCALE_CODE.test(name));
81
+ return { layout: 'flat', locales: [...new Set(flat)].sort() };
82
+ }
83
+ function collectLocaleFiles(dir, acc = []) {
84
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
85
+ const full = join(dir, entry.name);
86
+ if (entry.isDirectory())
87
+ collectLocaleFiles(full, acc);
88
+ else if (entry.isFile() && stripExtension(entry.name) !== null)
89
+ acc.push(full);
90
+ }
91
+ return acc;
92
+ }
93
+ /**
94
+ * Whether a bundle's source text comes from msgid rather than from the locale
95
+ * itself. In gettext every catalogue carries the untranslated string, so the
96
+ * locale named as the source only supplies the keys.
97
+ */
98
+ export function sourceIsMsgid(bundle) {
99
+ return bundle.files.length > 0 && bundle.files.every((f) => f.endsWith('.po') || f.endsWith('.pot'));
100
+ }
101
+ export function loadBundle(config, locale) {
102
+ const target = {
103
+ skipped: [],
104
+ leaves: new Map(),
105
+ containers: new Set(),
106
+ plurals: new Map(),
107
+ nplurals: null,
108
+ };
109
+ const files = [];
110
+ const unreadable = [];
111
+ const isSource = locale === config.sourceLocale;
112
+ /**
113
+ * A malformed file is reported, not thrown. A project with a hundred
114
+ * locales and one bad file should still be checked; refusing to read any
115
+ * of it is how a linter becomes something people stop running.
116
+ */
117
+ const read = (file, namespace) => {
118
+ try {
119
+ readerFor(file)(file, namespace, locale, isSource, target);
120
+ }
121
+ catch (err) {
122
+ if (!(err instanceof FormatError))
123
+ throw err;
124
+ unreadable.push({ file, message: err.message });
125
+ }
126
+ };
127
+ if (config.layout === 'flat') {
128
+ // A locale may be en.json or en.php; both are read when both exist.
129
+ for (const ext of EXTENSIONS) {
130
+ const file = join(config.localesDir, `${locale}${ext}`);
131
+ if (!existsSync(file))
132
+ continue;
133
+ files.push(file);
134
+ read(file, '');
135
+ }
136
+ if (files.length === 0) {
137
+ throw new ScanError(`No locale file for "${locale}" in ${config.localesDir}`);
138
+ }
139
+ }
140
+ else {
141
+ const localeRoot = join(config.localesDir, locale);
142
+ for (const file of collectLocaleFiles(localeRoot).sort()) {
143
+ // lang/en/shop/pricing.php -> "shop.pricing";
144
+ // locale/fr/LC_MESSAGES/messages.po -> "messages", since the gettext
145
+ // directory is layout, not namespace.
146
+ const relativePath = relative(localeRoot, file);
147
+ const namespace = (stripExtension(relativePath) ?? relativePath)
148
+ .split(sep)
149
+ .filter((part) => part !== 'LC_MESSAGES')
150
+ .join('.');
151
+ files.push(file);
152
+ read(file, namespace);
153
+ }
154
+ }
155
+ return {
156
+ locale,
157
+ files,
158
+ skipped: target.skipped,
159
+ unreadable,
160
+ leaves: target.leaves,
161
+ containers: target.containers,
162
+ plurals: target.plurals,
163
+ nplurals: target.nplurals,
164
+ };
165
+ }
166
+ export function detectProject(root, opts = {}) {
167
+ const resolvedRoot = resolve(root);
168
+ if (!existsSync(resolvedRoot))
169
+ throw new ScanError(`Path not found: ${resolvedRoot}`);
170
+ const localesDir = findLocalesDir(resolvedRoot, opts.localesDir);
171
+ const { layout, locales } = listLocales(localesDir);
172
+ if (locales.length === 0)
173
+ throw new ScanError(`No locale files in ${localesDir}`);
174
+ let sourceLocale = opts.sourceLocale ?? '';
175
+ if (sourceLocale && !locales.includes(sourceLocale)) {
176
+ throw new ScanError(`Source locale "${sourceLocale}" not among: ${locales.join(', ')}`);
177
+ }
178
+ // Laravel's :name is off by default because it false-positives on prose,
179
+ // but in a PHP project it is the interpolation syntax actually in use — and
180
+ // Laravel keeps string-keyed translations in JSON, so the file extension
181
+ // alone is not the signal. A composer.json or a lang/ directory is.
182
+ const syntaxes = [...DEFAULT_SYNTAXES];
183
+ if (usesLaravelPlaceholders(resolvedRoot, localesDir, layout, locales))
184
+ syntaxes.push('laravel');
185
+ const config = {
186
+ root: resolvedRoot,
187
+ localesDir,
188
+ sourceLocale: sourceLocale || locales[0],
189
+ locales,
190
+ layout,
191
+ placeholderSyntaxes: syntaxes,
192
+ ignoreIdentical: [],
193
+ rules: { ...DEFAULT_RULES },
194
+ };
195
+ if (!sourceLocale) {
196
+ // Plain `en` first; then a single regional English, which is the source in
197
+ // a project that spells it en-US. Several of them means none is: Sphinx
198
+ // carries en_DE and en_GB purely for date formats, so there the most
199
+ // complete catalogue is the better guess than whichever sorts first.
200
+ const english = locales.filter((l) => /^en([-_]|$)/.test(l));
201
+ config.sourceLocale =
202
+ locales.find((l) => l === 'en') ??
203
+ (english.length === 1 ? english[0] : mostComplete(config, locales));
204
+ }
205
+ return config;
206
+ }
207
+ /** The locale with the most keys, which in practice is the one others follow. */
208
+ function mostComplete(config, locales) {
209
+ let best = locales[0];
210
+ let bestCount = -1;
211
+ for (const locale of locales) {
212
+ let count = 0;
213
+ try {
214
+ count = loadBundle({ ...config, sourceLocale: locale }, locale).leaves.size;
215
+ }
216
+ catch {
217
+ continue; // an unreadable locale simply cannot be the source
218
+ }
219
+ if (count > bestCount) {
220
+ best = locale;
221
+ bestCount = count;
222
+ }
223
+ }
224
+ return best;
225
+ }
226
+ function hasPhpLocales(localesDir, layout, locales) {
227
+ if (layout === 'flat') {
228
+ return locales.some((locale) => existsSync(join(localesDir, `${locale}.php`)));
229
+ }
230
+ return locales.some((locale) => {
231
+ const dir = join(localesDir, locale);
232
+ return isDir(dir) && collectLocaleFiles(dir).some((file) => file.endsWith('.php'));
233
+ });
234
+ }
235
+ /**
236
+ * Whether this project interpolates the PHP way.
237
+ *
238
+ * Tying it to the .php extension missed every Laravel app that keeps its
239
+ * string-keyed translations in lang/xx.json — which is Laravel's own
240
+ * convention, so the placeholders went unchecked in exactly the projects the
241
+ * syntax exists for.
242
+ */
243
+ function usesLaravelPlaceholders(root, localesDir, layout, locales) {
244
+ if (hasPhpLocales(localesDir, layout, locales))
245
+ return true;
246
+ if (existsSync(join(root, 'composer.json')))
247
+ return true;
248
+ return basename(localesDir) === 'lang';
249
+ }
250
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,aAAa,GAKd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,cAAc,GAAG;IACrB,SAAS;IACT,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,MAAM;IACN,UAAU;IACV,MAAM;IACN,gBAAgB;IAChB,cAAc;IACd,kBAAkB;IAClB,gBAAgB;IAChB,QAAQ;IACR,IAAI;CACL,CAAC;AAEF,gFAAgF;AAChF,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AAErE,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,aAAa,CAAC;IAChD,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IACvE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,cAAc,CAAC;IAC3E,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,SAAS,cAAc,CAAC,IAAY;IAClC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,WAAW,GAAG,uCAAuC,CAAC;AAE5D,MAAM,OAAO,SAAU,SAAQ,KAAK;CAAG;AAEvC,SAAS,KAAK,CAAC,CAAS;IACtB,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,QAAiB;IAC5D,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAC;QAC5E,OAAO,GAAG,CAAC;IACb,CAAC;IACD,KAAK,MAAM,SAAS,IAAI,cAAc,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACrC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,GAAG,CAAC;IACpE,CAAC;IACD,MAAM,IAAI,SAAS,CACjB,oCAAoC,IAAI,aAAa,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,8CAA8C,CAC7H,CAAC;AACJ,CAAC;AAOD,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1D,MAAM,MAAM,GAAG,OAAO;SACnB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAC1D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;IAE3E,MAAM,IAAI,GAAG,OAAO;SACjB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAClC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;AAChE,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAW,EAAE,MAAgB,EAAE;IACzD,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,WAAW,EAAE;YAAE,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;aAClD,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAoB;IAChD,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AACvG,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,MAAc;IACvD,MAAM,MAAM,GAAe;QACzB,OAAO,EAAE,EAAE;QACX,MAAM,EAAE,IAAI,GAAG,EAAgB;QAC/B,UAAU,EAAE,IAAI,GAAG,EAAU;QAC7B,OAAO,EAAE,IAAI,GAAG,EAAkB;QAClC,QAAQ,EAAE,IAAI;KACf,CAAC;IACF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,UAAU,GAA6C,EAAE,CAAC;IAChE,MAAM,QAAQ,GAAG,MAAM,KAAK,MAAM,CAAC,YAAY,CAAC;IAEhD;;;;OAIG;IACH,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,SAAiB,EAAQ,EAAE;QACrD,IAAI,CAAC;YACH,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,CAAC,GAAG,YAAY,WAAW,CAAC;gBAAE,MAAM,GAAG,CAAC;YAC7C,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAClD,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC7B,oEAAoE;QACpE,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC;YACxD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAChC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACjB,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,uBAAuB,MAAM,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACnD,KAAK,MAAM,IAAI,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACzD,8CAA8C;YAC9C,qEAAqE;YACrE,sCAAsC;YACtC,MAAM,YAAY,GAAG,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAChD,MAAM,SAAS,GAAG,CAAC,cAAc,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC;iBAC7D,KAAK,CAAC,GAAG,CAAC;iBACV,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,aAAa,CAAC;iBACxC,IAAI,CAAC,GAAG,CAAC,CAAC;YACb,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM;QACN,KAAK;QACL,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU;QACV,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC;AACJ,CAAC;AAOD,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,OAAsB,EAAE;IAClE,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,mBAAmB,YAAY,EAAE,CAAC,CAAC;IAEtF,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACjE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IAEpD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,sBAAsB,UAAU,EAAE,CAAC,CAAC;IAElF,IAAI,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;IAC3C,IAAI,YAAY,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,SAAS,CAAC,kBAAkB,YAAY,gBAAgB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,oEAAoE;IACpE,MAAM,QAAQ,GAAG,CAAC,GAAG,gBAAgB,CAAC,CAAC;IACvC,IAAI,uBAAuB,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC;QAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAEjG,MAAM,MAAM,GAAW;QACrB,IAAI,EAAE,YAAY;QAClB,UAAU;QACV,YAAY,EAAE,YAAY,IAAI,OAAO,CAAC,CAAC,CAAE;QACzC,OAAO;QACP,MAAM;QACN,mBAAmB,EAAE,QAAQ;QAC7B,eAAe,EAAE,EAAE;QACnB,KAAK,EAAE,EAAE,GAAG,aAAa,EAAE;KAC5B,CAAC;IAEF,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,2EAA2E;QAC3E,wEAAwE;QACxE,qEAAqE;QACrE,qEAAqE;QACrE,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,MAAM,CAAC,YAAY;YACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;gBAC/B,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,iFAAiF;AACjF,SAAS,YAAY,CAAC,MAAc,EAAE,OAAiB;IACrD,IAAI,IAAI,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;IACvB,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;IAEnB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,CAAC;YACH,KAAK,GAAG,UAAU,CAAC,EAAE,GAAG,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;QAC9E,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,mDAAmD;QAC/D,CAAC;QACD,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,IAAI,GAAG,MAAM,CAAC;YACd,SAAS,GAAG,KAAK,CAAC;QACpB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,UAAkB,EAAE,MAAyB,EAAE,OAAiB;IACrF,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACrC,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,uBAAuB,CAC9B,IAAY,EACZ,UAAkB,EAClB,MAAyB,EACzB,OAAiB;IAEjB,IAAI,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACzD,OAAO,QAAQ,CAAC,UAAU,CAAC,KAAK,MAAM,CAAC;AACzC,CAAC"}
@@ -0,0 +1,93 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { type Glossary } from './glossary.js';
3
+ import { type Limits } from './lengths.js';
4
+ import type { Config, Finding, LocaleBundle } from './types.js';
5
+ /**
6
+ * The one part of this tool that needs a network and cannot be verified by
7
+ * reading the output. So it is not trusted: every proposal is put back through
8
+ * the same deterministic checks the linter already applies, and anything that
9
+ * fails is rejected rather than written.
10
+ */
11
+ export declare const DEFAULT_MODEL = "claude-opus-5";
12
+ export declare const DEFAULT_BATCH = 20;
13
+ export declare const DEFAULT_CAP = 50;
14
+ export type JobKind = 'fill' | 'repair' | 'refresh';
15
+ export declare const JOB_KINDS: readonly JobKind[];
16
+ /**
17
+ * Defects worth handing back to the model.
18
+ *
19
+ * A rule belongs here only if `validate` can confirm the repair afterwards.
20
+ * That is the whole selection principle: a fix the gate cannot check is a fix
21
+ * nobody can trust, so those findings are left for a human.
22
+ *
23
+ * It excludes `identical_to_source` (often correct — "Email" is "Email" in
24
+ * French, and forcing a change would make it worse), `plural_extra_category`
25
+ * and `plural_selector_lost` (the single-string validator does not check for
26
+ * either, so a repair could not be verified).
27
+ */
28
+ export declare const REPAIRABLE_RULES: Set<string>;
29
+ export interface Job {
30
+ locale: string;
31
+ key: string;
32
+ source: string;
33
+ kind: JobKind;
34
+ /** The translation being replaced: broken for a repair, outdated for a refresh. */
35
+ previous: string | null;
36
+ /** For a repair, exactly what the linter found wrong. */
37
+ problems: string[];
38
+ placeholders: string[];
39
+ glossary: Array<{
40
+ term: string;
41
+ accepted: string[];
42
+ }>;
43
+ doNotTranslate: string[];
44
+ maxWidth: number | null;
45
+ isIcuPlural: boolean;
46
+ }
47
+ export interface Proposal {
48
+ locale: string;
49
+ key: string;
50
+ kind: JobKind;
51
+ source: string;
52
+ value: string;
53
+ accepted: boolean;
54
+ /** Deterministic checks the proposal failed, if any. */
55
+ rejections: string[];
56
+ attempts: number;
57
+ }
58
+ /** Turns a lint report into a work list. */
59
+ export declare function collectJobs(config: Config, findings: Finding[], source: LocaleBundle, glossary: Glossary | null, limits: Limits | null, locales?: string[], kinds?: JobKind[]): Job[];
60
+ /** What `validate` needs to judge a string; a full Job satisfies it. */
61
+ export type Constraints = Pick<Job, 'source' | 'locale' | 'maxWidth'>;
62
+ /** The same rules the linter enforces, applied to one proposed string. */
63
+ export declare function validate(config: Config, job: Constraints, proposed: string, glossary: Glossary | null): string[];
64
+ export interface TranslateOptions {
65
+ model: string;
66
+ effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
67
+ client?: Anthropic;
68
+ }
69
+ /** The model declined one batch on content grounds; other batches may still run. */
70
+ export declare class TranslationRefused extends Error {
71
+ }
72
+ /** One request for one locale's batch. Returns text keyed by job index. */
73
+ export declare function requestBatch(jobs: Job[], locale: string, options: TranslateOptions, feedback?: Map<number, string[]>): Promise<Map<number, string>>;
74
+ export interface TranslationRun {
75
+ proposals: Proposal[];
76
+ /**
77
+ * Why the run stopped early, if it did. Anything that is not a content
78
+ * refusal — no credentials, a rate limit, a network failure — will hit every
79
+ * remaining batch the same way, so the run stops instead of reporting the
80
+ * same infrastructure error once per string.
81
+ */
82
+ aborted: string | null;
83
+ }
84
+ export interface RunOptions extends TranslateOptions {
85
+ batchSize: number;
86
+ /** Called before each request so the caller can report progress. */
87
+ onBatch?: (locale: string, size: number, attempt: number) => void;
88
+ }
89
+ /**
90
+ * Translates a work list, validating every proposal and retrying once with the
91
+ * specific failures handed back to the model.
92
+ */
93
+ export declare function runTranslation(config: Config, jobs: Job[], glossary: Glossary | null, options: RunOptions): Promise<TranslationRun>;