@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
@@ -0,0 +1,202 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { FormatError } from './error.js';
3
+ /**
4
+ * gettext .po / .pot.
5
+ *
6
+ * Two things make this format different from the others: the msgid *is* the
7
+ * source text, so a .pot with empty translations is a usable source locale; and
8
+ * the format already has a notion of an outdated translation — the fuzzy flag —
9
+ * which maps onto the same rule the translation memory feeds.
10
+ */
11
+ export class PoParseError extends FormatError {
12
+ constructor(file, line, message) {
13
+ super(file, message, line);
14
+ }
15
+ }
16
+ const SIMPLE_ESCAPES = {
17
+ n: '\n',
18
+ t: '\t',
19
+ r: '\r',
20
+ a: '\x07',
21
+ b: '\b',
22
+ f: '\f',
23
+ v: '\v',
24
+ '"': '"',
25
+ '\\': '\\',
26
+ };
27
+ function unescape(raw) {
28
+ let out = '';
29
+ for (let i = 0; i < raw.length; i++) {
30
+ const ch = raw[i];
31
+ if (ch !== '\\') {
32
+ out += ch;
33
+ continue;
34
+ }
35
+ const next = raw[i + 1];
36
+ if (next === undefined) {
37
+ out += ch;
38
+ continue;
39
+ }
40
+ if (next in SIMPLE_ESCAPES) {
41
+ out += SIMPLE_ESCAPES[next];
42
+ i++;
43
+ continue;
44
+ }
45
+ if (next === 'x') {
46
+ const hex = /^[0-9A-Fa-f]{1,2}/.exec(raw.slice(i + 2));
47
+ if (hex) {
48
+ out += String.fromCharCode(Number.parseInt(hex[0], 16));
49
+ i += 1 + hex[0].length;
50
+ continue;
51
+ }
52
+ }
53
+ const octal = /^[0-7]{1,3}/.exec(raw.slice(i + 1));
54
+ if (octal) {
55
+ out += String.fromCharCode(Number.parseInt(octal[0], 8) & 0xff);
56
+ i += octal[0].length;
57
+ continue;
58
+ }
59
+ out += next;
60
+ i++;
61
+ }
62
+ return out;
63
+ }
64
+ /** A quoted chunk, as it appears after a keyword or on a continuation line. */
65
+ function readQuoted(file, line, text) {
66
+ const trimmed = text.trim();
67
+ if (!trimmed.startsWith('"') || !trimmed.endsWith('"') || trimmed.length < 2) {
68
+ throw new PoParseError(file, line, 'Expected a quoted string');
69
+ }
70
+ return unescape(trimmed.slice(1, -1));
71
+ }
72
+ const KEYWORD = /^(msgctxt|msgid_plural|msgid|msgstr)(\[(\d+)\])?\s*(.*)$/;
73
+ export function parsePo(text, file) {
74
+ const entries = [];
75
+ const lines = text.split(/\r?\n/);
76
+ // Assignments stay in the loop body rather than in closures, so the compiler
77
+ // can follow whether an entry is open.
78
+ let current = null;
79
+ let field = null;
80
+ let msgstrIndex = null;
81
+ let fuzzy = false;
82
+ let seenKeyword = false;
83
+ for (const [index, rawLine] of lines.entries()) {
84
+ const lineNo = index + 1;
85
+ const line = rawLine.trim();
86
+ // A blank line ends an entry; #~ marks one already removed from the
87
+ // catalogue, so its block is dropped rather than parsed.
88
+ if (line === '' || line.startsWith('#~')) {
89
+ if (current && seenKeyword)
90
+ entries.push(current);
91
+ current = null;
92
+ field = null;
93
+ msgstrIndex = null;
94
+ fuzzy = false;
95
+ seenKeyword = false;
96
+ continue;
97
+ }
98
+ if (line.startsWith('#')) {
99
+ if (line.startsWith('#,')) {
100
+ const flags = line
101
+ .slice(2)
102
+ .split(',')
103
+ .map((f) => f.trim());
104
+ if (flags.includes('fuzzy')) {
105
+ fuzzy = true;
106
+ if (current)
107
+ current.fuzzy = true;
108
+ }
109
+ }
110
+ continue;
111
+ }
112
+ const match = KEYWORD.exec(line);
113
+ if (match) {
114
+ if (!current) {
115
+ current = { context: null, msgid: '', msgidPlural: null, msgstr: [], fuzzy, line: lineNo };
116
+ }
117
+ const entry = current;
118
+ const keyword = match[1];
119
+ seenKeyword = true;
120
+ const value = readQuoted(file, lineNo, match[4] || '""');
121
+ if (keyword === 'msgstr') {
122
+ const at = match[3] === undefined ? 0 : Number.parseInt(match[3], 10);
123
+ entry.msgstr[at] = value;
124
+ msgstrIndex = at;
125
+ field = null;
126
+ }
127
+ else if (keyword === 'msgctxt') {
128
+ entry.context = value;
129
+ field = 'msgctxt';
130
+ msgstrIndex = null;
131
+ }
132
+ else if (keyword === 'msgid') {
133
+ entry.msgid = value;
134
+ field = 'msgid';
135
+ msgstrIndex = null;
136
+ }
137
+ else {
138
+ entry.msgidPlural = value;
139
+ field = 'msgid_plural';
140
+ msgstrIndex = null;
141
+ }
142
+ continue;
143
+ }
144
+ if (line.startsWith('"')) {
145
+ if (!current)
146
+ throw new PoParseError(file, lineNo, 'Continuation without a preceding keyword');
147
+ const entry = current;
148
+ const value = readQuoted(file, lineNo, line);
149
+ if (msgstrIndex !== null)
150
+ entry.msgstr[msgstrIndex] = (entry.msgstr[msgstrIndex] ?? '') + value;
151
+ else if (field === 'msgctxt')
152
+ entry.context = (entry.context ?? '') + value;
153
+ else if (field === 'msgid')
154
+ entry.msgid += value;
155
+ else if (field === 'msgid_plural')
156
+ entry.msgidPlural = (entry.msgidPlural ?? '') + value;
157
+ else
158
+ throw new PoParseError(file, lineNo, 'Continuation without a preceding keyword');
159
+ continue;
160
+ }
161
+ throw new PoParseError(file, lineNo, `Unexpected line: ${line.slice(0, 40)}`);
162
+ }
163
+ if (current && seenKeyword)
164
+ entries.push(current);
165
+ let nplurals = null;
166
+ const header = entries.find((e) => e.msgid === '' && e.context === null);
167
+ if (header) {
168
+ const declared = /nplurals\s*=\s*(\d+)/.exec(header.msgstr[0] ?? '');
169
+ if (declared)
170
+ nplurals = Number.parseInt(declared[1], 10);
171
+ }
172
+ return { entries: entries.filter((e) => e.msgid !== '' || e.context !== null), nplurals };
173
+ }
174
+ /** msgctxt disambiguates an otherwise identical msgid. */
175
+ export function poKey(entry) {
176
+ return entry.context === null ? entry.msgid : `${entry.context}|${entry.msgid}`;
177
+ }
178
+ export function readPoLocale(file, namespace, _locale, isSource, target) {
179
+ const parsed = parsePo(readFileSync(file, 'utf8'), file);
180
+ if (parsed.nplurals !== null)
181
+ target.nplurals = parsed.nplurals;
182
+ for (const entry of parsed.entries) {
183
+ const base = namespace ? `${namespace}.${poKey(entry)}` : poKey(entry);
184
+ if (entry.msgidPlural === null) {
185
+ // The msgid is the source text by definition, whatever this catalogue
186
+ // has translated it to.
187
+ const value = isSource ? entry.msgid : (entry.msgstr[0] ?? '');
188
+ target.leaves.set(base, { value, kind: 'string', file, fuzzy: entry.fuzzy });
189
+ continue;
190
+ }
191
+ const sourceForms = [entry.msgid, entry.msgidPlural];
192
+ const count = Math.max(entry.msgstr.length, isSource ? sourceForms.length : 1);
193
+ target.plurals.set(base, entry.msgstr.length);
194
+ target.containers.add(base);
195
+ for (let i = 0; i < count; i++) {
196
+ const translated = entry.msgstr[i] ?? '';
197
+ const value = isSource ? sourceForms[Math.min(i, 1)] : translated;
198
+ target.leaves.set(`${base}.${i}`, { value, kind: 'string', file, fuzzy: entry.fuzzy });
199
+ }
200
+ }
201
+ }
202
+ //# sourceMappingURL=po.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"po.js","sourceRoot":"","sources":["../../src/formats/po.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Writing back into a locale file.
3
+ *
4
+ * Reading throws away everything that is not a key or a value: comments,
5
+ * quote styles, blank lines, anchors, translator notes. Writing must not, so
6
+ * none of these writers re-serialise a parsed tree. They edit the text in
7
+ * place — replacing the exact span of one value, or inserting one entry —
8
+ * and leave every byte they did not have a reason to touch.
9
+ */
10
+ export interface Edit {
11
+ /** Key relative to this file, with any file-level namespace already removed. */
12
+ key: string;
13
+ value: string;
14
+ }
15
+ export interface WriteOutcome {
16
+ content: string;
17
+ /** Edits the format could not take, and why. */
18
+ skipped: Array<{
19
+ key: string;
20
+ reason: string;
21
+ }>;
22
+ }
23
+ export type LocaleWriter = (content: string, file: string, locale: string, edits: Edit[]) => WriteOutcome;
24
+ /** Applies text edits from the end backwards, so earlier offsets stay valid. */
25
+ export declare function spliceAll(content: string, edits: Array<{
26
+ start: number;
27
+ end: number;
28
+ text: string;
29
+ }>): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Writing back into a locale file.
3
+ *
4
+ * Reading throws away everything that is not a key or a value: comments,
5
+ * quote styles, blank lines, anchors, translator notes. Writing must not, so
6
+ * none of these writers re-serialise a parsed tree. They edit the text in
7
+ * place — replacing the exact span of one value, or inserting one entry —
8
+ * and leave every byte they did not have a reason to touch.
9
+ */
10
+ /** Applies text edits from the end backwards, so earlier offsets stay valid. */
11
+ export function spliceAll(content, edits) {
12
+ const ordered = [...edits].sort((a, b) => b.start - a.start);
13
+ let out = content;
14
+ for (const edit of ordered) {
15
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
16
+ }
17
+ return out;
18
+ }
19
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/formats/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqBH,gFAAgF;AAChF,MAAM,UAAU,SAAS,CACvB,OAAe,EACf,KAA0D;IAE1D,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAC7D,IAAI,GAAG,GAAG,OAAO,CAAC;IAClB,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Edit, WriteOutcome } from './write.js';
2
+ /**
3
+ * YAML is the one format edited through a document model rather than by
4
+ * splicing text, because the library keeps comments, anchors and node styles
5
+ * across a round trip and a hand-rolled splicer would not.
6
+ *
7
+ * Two things are still done by hand. An existing scalar is mutated in place
8
+ * instead of replaced, so a block scalar stays a block scalar and keeps its
9
+ * comments. And an alias is refused outright: writing through `*shared` would
10
+ * silently break every other key that shares the anchor.
11
+ */
12
+ export declare function writeYamlLocale(content: string, file: string, locale: string, edits: Edit[]): WriteOutcome;
@@ -0,0 +1,42 @@
1
+ import { isAlias, isScalar, parseDocument } from 'yaml';
2
+ import { FormatError } from './error.js';
3
+ import { localeRootKey } from './yaml.js';
4
+ /**
5
+ * YAML is the one format edited through a document model rather than by
6
+ * splicing text, because the library keeps comments, anchors and node styles
7
+ * across a round trip and a hand-rolled splicer would not.
8
+ *
9
+ * Two things are still done by hand. An existing scalar is mutated in place
10
+ * instead of replaced, so a block scalar stays a block scalar and keeps its
11
+ * comments. And an alias is refused outright: writing through `*shared` would
12
+ * silently break every other key that shares the anchor.
13
+ */
14
+ export function writeYamlLocale(content, file, locale, edits) {
15
+ const doc = parseDocument(content);
16
+ if (doc.errors.length > 0) {
17
+ throw new FormatError(file, doc.errors[0].message);
18
+ }
19
+ const root = localeRootKey(doc.toJS(), locale);
20
+ const skipped = [];
21
+ for (const edit of edits) {
22
+ const path = (root ? [root] : []).concat(edit.key.split('.'));
23
+ const existing = doc.getIn(path, true);
24
+ if (isAlias(existing)) {
25
+ skipped.push({ key: edit.key, reason: 'the value is a YAML alias, shared with other keys' });
26
+ continue;
27
+ }
28
+ if (isScalar(existing)) {
29
+ // Keeps the node's style — block scalars stay block scalars.
30
+ existing.value = edit.value;
31
+ continue;
32
+ }
33
+ if (existing !== undefined && existing !== null) {
34
+ skipped.push({ key: edit.key, reason: 'the key holds a collection, not a string' });
35
+ continue;
36
+ }
37
+ doc.setIn(path, edit.value);
38
+ }
39
+ // lineWidth 0 disables re-wrapping, so untouched long lines stay as written.
40
+ return { content: doc.toString({ lineWidth: 0 }), skipped };
41
+ }
42
+ //# sourceMappingURL=yaml-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yaml-write.js","sourceRoot":"","sources":["../../src/formats/yaml-write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,IAAY,EACZ,MAAc,EACd,KAAa;IAEb,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;IAC/C,MAAM,OAAO,GAA2C,EAAE,CAAC;IAE3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9D,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAEvC,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,mDAAmD,EAAE,CAAC,CAAC;YAC7F,SAAS;QACX,CAAC;QACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACvB,6DAA6D;YAC7D,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YAC5B,SAAS;QACX,CAAC;QACD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YAChD,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,0CAA0C,EAAE,CAAC,CAAC;YACpF,SAAS;QACX,CAAC;QAED,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,6EAA6E;IAC7E,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ReadTarget } from '../types.js';
2
+ import { FormatError } from './error.js';
3
+ /**
4
+ * Rails and Symfony locale files.
5
+ *
6
+ * Unlike the PHP parser, this one delegates: reading YAML is not a security
7
+ * question, and the spec is deep enough — anchors, block scalars, implicit
8
+ * typing — that a hand-rolled subset would quietly misread real files.
9
+ */
10
+ export declare class YamlParseError extends FormatError {
11
+ }
12
+ /**
13
+ * Rails nests the whole file under its locale code. Left in place, every key in
14
+ * en.yml would differ from every key in fr.yml.
15
+ */
16
+ export declare function localeRootKey(parsed: unknown, locale: string): string | null;
17
+ export declare function stripLocaleRoot(parsed: unknown, locale: string): unknown;
18
+ export declare function readYamlLocale(file: string, namespace: string, locale: string, _isSource: boolean, target: ReadTarget): void;
@@ -0,0 +1,51 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { parse } from 'yaml';
3
+ import { FormatError } from './error.js';
4
+ import { flattenValue } from './flatten.js';
5
+ /**
6
+ * Rails and Symfony locale files.
7
+ *
8
+ * Unlike the PHP parser, this one delegates: reading YAML is not a security
9
+ * question, and the spec is deep enough — anchors, block scalars, implicit
10
+ * typing — that a hand-rolled subset would quietly misread real files.
11
+ */
12
+ export class YamlParseError extends FormatError {
13
+ }
14
+ function normaliseLocale(value) {
15
+ return value.toLowerCase().replace(/_/g, '-');
16
+ }
17
+ /**
18
+ * Rails nests the whole file under its locale code. Left in place, every key in
19
+ * en.yml would differ from every key in fr.yml.
20
+ */
21
+ export function localeRootKey(parsed, locale) {
22
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
23
+ return null;
24
+ const keys = Object.keys(parsed);
25
+ if (keys.length !== 1)
26
+ return null;
27
+ const root = keys[0];
28
+ return normaliseLocale(root) === normaliseLocale(locale) ? root : null;
29
+ }
30
+ export function stripLocaleRoot(parsed, locale) {
31
+ const root = localeRootKey(parsed, locale);
32
+ return root === null ? parsed : parsed[root];
33
+ }
34
+ export function readYamlLocale(file, namespace, locale, _isSource, target) {
35
+ let parsed;
36
+ try {
37
+ parsed = parse(readFileSync(file, 'utf8'));
38
+ }
39
+ catch (err) {
40
+ throw new YamlParseError(file, err instanceof Error ? err.message : String(err));
41
+ }
42
+ if (parsed === null || parsed === undefined)
43
+ return; // an empty document
44
+ const inner = stripLocaleRoot(parsed, locale);
45
+ if (Array.isArray(inner)) {
46
+ target.skipped.push({ file, reason: 'the file holds a list, not keyed translations' });
47
+ return;
48
+ }
49
+ flattenValue(inner, namespace, file, target.leaves, target.containers);
50
+ }
51
+ //# sourceMappingURL=yaml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yaml.js","sourceRoot":"","sources":["../../src/formats/yaml.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;GAMG;AAEH,MAAM,OAAO,cAAe,SAAQ,WAAW;CAAG;AAElD,SAAS,eAAe,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAe,EAAE,MAAc;IAC3D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAExF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAiC,CAAC,CAAC;IAC5D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;IACtB,OAAO,eAAe,CAAC,IAAI,CAAC,KAAK,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAe,EAAE,MAAc;IAC7D,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,MAAkC,CAAC,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,SAAiB,EACjB,MAAc,EACd,SAAkB,EAClB,MAAkB;IAElB,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CAAC,IAAI,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACnF,CAAC;IAED,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,CAAC,oBAAoB;IAEzE,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,+CAA+C,EAAE,CAAC,CAAC;QACvF,OAAO;IACT,CAAC;IACD,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;AACzE,CAAC"}
@@ -0,0 +1,38 @@
1
+ export declare const GLOSSARY_FILE = ".i18n/glossary.json";
2
+ export type MatchMode = 'prefix' | 'exact' | 'substring';
3
+ export interface GlossaryTerm {
4
+ /** The word as it appears in the source locale. */
5
+ source: string;
6
+ /** Accepted renderings per locale. Locales left out are not checked. */
7
+ targets: Record<string, string[]>;
8
+ /** Default `prefix`, which tolerates suffix inflection: panier → paniers. */
9
+ match: MatchMode;
10
+ caseSensitive: boolean;
11
+ }
12
+ export interface Glossary {
13
+ version: 1;
14
+ /** Tokens that must survive verbatim: brand and product names, protocols. */
15
+ doNotTranslate: string[];
16
+ terms: GlossaryTerm[];
17
+ }
18
+ export declare class GlossaryError extends Error {
19
+ }
20
+ export declare function glossaryPath(root: string, explicit?: string): string;
21
+ export declare function loadGlossary(file: string): Glossary | null;
22
+ export declare function usesWordBoundaries(term: string): boolean;
23
+ /**
24
+ * Whether `term` occurs in `text`.
25
+ *
26
+ * `prefix` is the default because suffix inflection is the common case: a
27
+ * glossary saying "panier" should accept "paniers", and one saying "корзин"
28
+ * should accept every case ending.
29
+ */
30
+ export declare function containsTerm(text: string, term: string, match?: MatchMode, caseSensitive?: boolean): boolean;
31
+ export interface TermExpectation {
32
+ term: GlossaryTerm;
33
+ accepted: string[];
34
+ }
35
+ /** Terms that apply to this source string for this locale, with their forms. */
36
+ export declare function expectationsFor(glossary: Glossary, locale: string, sourceValue: string): TermExpectation[];
37
+ /** Do-not-translate tokens present in the source but missing from the target. */
38
+ export declare function missingVerbatim(glossary: Glossary, sourceValue: string, targetValue: string): string[];
@@ -0,0 +1,140 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ export const GLOSSARY_FILE = '.i18n/glossary.json';
4
+ export class GlossaryError extends Error {
5
+ }
6
+ export function glossaryPath(root, explicit) {
7
+ return resolve(root, explicit ?? GLOSSARY_FILE);
8
+ }
9
+ function fail(file, message) {
10
+ throw new GlossaryError(`Invalid glossary ${file}\n ${message}`);
11
+ }
12
+ function asStringArray(file, value, where) {
13
+ if (!Array.isArray(value) || value.some((v) => typeof v !== 'string')) {
14
+ fail(file, `${where} must be an array of strings`);
15
+ }
16
+ return value;
17
+ }
18
+ export function loadGlossary(file) {
19
+ if (!existsSync(file))
20
+ return null;
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
24
+ }
25
+ catch (err) {
26
+ fail(file, err instanceof Error ? err.message : String(err));
27
+ }
28
+ if (typeof parsed !== 'object' || parsed === null)
29
+ fail(file, 'expected an object');
30
+ const raw = parsed;
31
+ if (raw['version'] !== 1)
32
+ fail(file, 'expected "version": 1');
33
+ const doNotTranslate = raw['doNotTranslate'] === undefined
34
+ ? []
35
+ : asStringArray(file, raw['doNotTranslate'], 'doNotTranslate');
36
+ const termsRaw = raw['terms'] === undefined ? [] : raw['terms'];
37
+ if (!Array.isArray(termsRaw))
38
+ fail(file, 'terms must be an array');
39
+ const terms = termsRaw.map((entry, index) => {
40
+ const where = `terms[${index}]`;
41
+ if (typeof entry !== 'object' || entry === null)
42
+ fail(file, `${where} must be an object`);
43
+ const term = entry;
44
+ const source = term['source'];
45
+ if (typeof source !== 'string' || source === '') {
46
+ fail(file, `${where}.source must be a non-empty string`);
47
+ }
48
+ const targetsRaw = term['targets'];
49
+ if (typeof targetsRaw !== 'object' || targetsRaw === null || Array.isArray(targetsRaw)) {
50
+ fail(file, `${where}.targets must be an object keyed by locale`);
51
+ }
52
+ const targets = {};
53
+ for (const [locale, value] of Object.entries(targetsRaw)) {
54
+ const forms = asStringArray(file, value, `${where}.targets.${locale}`);
55
+ if (forms.length === 0)
56
+ fail(file, `${where}.targets.${locale} must list at least one form`);
57
+ targets[locale] = forms;
58
+ }
59
+ const match = term['match'] ?? 'prefix';
60
+ if (match !== 'prefix' && match !== 'exact' && match !== 'substring') {
61
+ fail(file, `${where}.match must be "prefix", "exact" or "substring"`);
62
+ }
63
+ const caseSensitive = term['caseSensitive'] ?? false;
64
+ if (typeof caseSensitive !== 'boolean')
65
+ fail(file, `${where}.caseSensitive must be a boolean`);
66
+ return { source, targets, match, caseSensitive };
67
+ });
68
+ return { version: 1, doNotTranslate, terms };
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // Matching
72
+ // ---------------------------------------------------------------------------
73
+ const WORDLIKE = /[\p{L}\p{N}]/u;
74
+ /**
75
+ * Scripts written without spaces, where a term is normally surrounded by other
76
+ * letters and boundary checks would never match.
77
+ */
78
+ const NO_WORD_BREAKS = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
79
+ export function usesWordBoundaries(term) {
80
+ return !NO_WORD_BREAKS.test(term);
81
+ }
82
+ /**
83
+ * Whether `term` occurs in `text`.
84
+ *
85
+ * `prefix` is the default because suffix inflection is the common case: a
86
+ * glossary saying "panier" should accept "paniers", and one saying "корзин"
87
+ * should accept every case ending.
88
+ */
89
+ export function containsTerm(text, term, match = 'prefix', caseSensitive = false) {
90
+ if (term === '')
91
+ return false;
92
+ const haystack = caseSensitive ? text : text.toLowerCase();
93
+ const needle = caseSensitive ? term : term.toLowerCase();
94
+ if (match === 'substring' || !usesWordBoundaries(term)) {
95
+ return haystack.includes(needle);
96
+ }
97
+ let from = 0;
98
+ for (;;) {
99
+ const at = haystack.indexOf(needle, from);
100
+ if (at === -1)
101
+ return false;
102
+ const before = at === 0 ? '' : haystack[at - 1];
103
+ const startsWord = before === '' || !WORDLIKE.test(before);
104
+ if (startsWord) {
105
+ if (match === 'prefix')
106
+ return true;
107
+ const afterIndex = at + needle.length;
108
+ const after = afterIndex >= haystack.length ? '' : haystack[afterIndex];
109
+ if (after === '' || !WORDLIKE.test(after))
110
+ return true;
111
+ }
112
+ from = at + 1;
113
+ }
114
+ }
115
+ /** Terms that apply to this source string for this locale, with their forms. */
116
+ export function expectationsFor(glossary, locale, sourceValue) {
117
+ const out = [];
118
+ for (const term of glossary.terms) {
119
+ const accepted = term.targets[locale];
120
+ if (!accepted)
121
+ continue;
122
+ if (!containsTerm(sourceValue, term.source, term.match, term.caseSensitive))
123
+ continue;
124
+ out.push({ term, accepted });
125
+ }
126
+ return out;
127
+ }
128
+ /** Do-not-translate tokens present in the source but missing from the target. */
129
+ export function missingVerbatim(glossary, sourceValue, targetValue) {
130
+ const missing = [];
131
+ for (const token of glossary.doNotTranslate) {
132
+ // Brand names carry their capitalisation, so this comparison is exact.
133
+ if (!containsTerm(sourceValue, token, 'exact', true))
134
+ continue;
135
+ if (!containsTerm(targetValue, token, 'exact', true))
136
+ missing.push(token);
137
+ }
138
+ return missing;
139
+ }
140
+ //# sourceMappingURL=glossary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glossary.js","sourceRoot":"","sources":["../src/glossary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ export declare const LIMITS_FILE = ".i18n/limits.json";
2
+ export interface LimitPattern {
3
+ match: string;
4
+ max: number;
5
+ }
6
+ export interface Limits {
7
+ version: 1;
8
+ /** Applied to every measurable key when set. */
9
+ default: number | null;
10
+ keys: Record<string, number>;
11
+ patterns: LimitPattern[];
12
+ }
13
+ export declare class LimitsError extends Error {
14
+ }
15
+ export declare function limitsPath(root: string, explicit?: string): string;
16
+ export declare function loadLimits(file: string): Limits | null;
17
+ /** Exact keys win over patterns, and the first matching pattern wins over the default. */
18
+ export declare function limitFor(limits: Limits, key: string): number | null;
19
+ /**
20
+ * Width in display columns rather than UTF-16 units: a CJK character occupies
21
+ * two columns, combining marks none. Counting `.length` would report Japanese
22
+ * as half its real size and any emoji as double.
23
+ */
24
+ export declare function displayWidth(text: string): number;
25
+ /**
26
+ * An ICU plural carries every branch in one string but shows one at a time, and
27
+ * its selectors are not displayed at all, so measuring it would be meaningless.
28
+ */
29
+ export declare function measurable(text: string): boolean;
30
+ /** The widest thing the user can actually end up seeing. */
31
+ export declare function renderedWidth(text: string, laravel: boolean): number;
32
+ /**
33
+ * Conventional translation-expansion allowances: short strings grow much more
34
+ * in relative terms than long ones, so a single ratio would either miss real
35
+ * overflow or flag every button. These are rules of thumb, not a standard.
36
+ */
37
+ export declare function allowanceFor(sourceWidth: number): number;