@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.
- package/LICENSE +21 -0
- package/README.md +666 -0
- package/dist/apply.d.ts +66 -0
- package/dist/apply.js +254 -0
- package/dist/apply.js.map +1 -0
- package/dist/check.d.ts +5 -0
- package/dist/check.js +326 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +525 -0
- package/dist/cli.js.map +1 -0
- package/dist/formats/error.d.ts +12 -0
- package/dist/formats/error.js +19 -0
- package/dist/formats/error.js.map +1 -0
- package/dist/formats/flatten.d.ts +6 -0
- package/dist/formats/flatten.js +26 -0
- package/dist/formats/flatten.js.map +1 -0
- package/dist/formats/json-write.d.ts +7 -0
- package/dist/formats/json-write.js +55 -0
- package/dist/formats/json-write.js.map +1 -0
- package/dist/formats/json.d.ts +6 -0
- package/dist/formats/json.js +21 -0
- package/dist/formats/json.js.map +1 -0
- package/dist/formats/php-write.d.ts +16 -0
- package/dist/formats/php-write.js +130 -0
- package/dist/formats/php-write.js.map +1 -0
- package/dist/formats/php.d.ts +44 -0
- package/dist/formats/php.js +356 -0
- package/dist/formats/php.js.map +1 -0
- package/dist/formats/po-write.d.ts +3 -0
- package/dist/formats/po-write.js +200 -0
- package/dist/formats/po-write.js.map +1 -0
- package/dist/formats/po.d.ts +30 -0
- package/dist/formats/po.js +202 -0
- package/dist/formats/po.js.map +1 -0
- package/dist/formats/write.d.ts +29 -0
- package/dist/formats/write.js +19 -0
- package/dist/formats/write.js.map +1 -0
- package/dist/formats/yaml-write.d.ts +12 -0
- package/dist/formats/yaml-write.js +42 -0
- package/dist/formats/yaml-write.js.map +1 -0
- package/dist/formats/yaml.d.ts +18 -0
- package/dist/formats/yaml.js +51 -0
- package/dist/formats/yaml.js.map +1 -0
- package/dist/glossary.d.ts +38 -0
- package/dist/glossary.js +140 -0
- package/dist/glossary.js.map +1 -0
- package/dist/lengths.d.ts +37 -0
- package/dist/lengths.js +195 -0
- package/dist/lengths.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +374 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +72 -0
- package/dist/memory.js +162 -0
- package/dist/memory.js.map +1 -0
- package/dist/placeholders.d.ts +15 -0
- package/dist/placeholders.js +103 -0
- package/dist/placeholders.js.map +1 -0
- package/dist/plurals.d.ts +59 -0
- package/dist/plurals.js +280 -0
- package/dist/plurals.js.map +1 -0
- package/dist/report.d.ts +2 -0
- package/dist/report.js +126 -0
- package/dist/report.js.map +1 -0
- package/dist/scan.d.ts +23 -0
- package/dist/scan.js +250 -0
- package/dist/scan.js.map +1 -0
- package/dist/translate.d.ts +93 -0
- package/dist/translate.js +369 -0
- package/dist/translate.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +28 -0
- package/dist/version.js.map +1 -0
- package/package.json +77 -0
- package/src/apply.ts +334 -0
- package/src/check.ts +468 -0
- package/src/cli.ts +637 -0
- package/src/formats/error.ts +19 -0
- package/src/formats/flatten.ts +38 -0
- package/src/formats/json-write.ts +66 -0
- package/src/formats/json.ts +27 -0
- package/src/formats/php-write.ts +146 -0
- package/src/formats/php.ts +414 -0
- package/src/formats/po-write.ts +230 -0
- package/src/formats/po.ts +233 -0
- package/src/formats/write.ts +41 -0
- package/src/formats/yaml-write.ts +53 -0
- package/src/formats/yaml.ts +62 -0
- package/src/glossary.ts +191 -0
- package/src/lengths.ts +214 -0
- package/src/mcp.ts +451 -0
- package/src/memory.ts +227 -0
- package/src/placeholders.ts +123 -0
- package/src/plurals.ts +320 -0
- package/src/report.ts +162 -0
- package/src/scan.ts +280 -0
- package/src/translate.ts +486 -0
- package/src/types.ts +136 -0
- package/src/version.ts +27 -0
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Config } from './types.js';
|
|
2
|
+
export declare const MEMORY_FILE = ".i18n/memory.json";
|
|
3
|
+
export interface MemoryEntry {
|
|
4
|
+
/** Hash of the source string at the moment this translation was recorded. */
|
|
5
|
+
sourceHash: string;
|
|
6
|
+
value: string;
|
|
7
|
+
origin: 'human' | 'machine';
|
|
8
|
+
reviewed: boolean;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
/** When a person signed off on it, for entries that have been reviewed. */
|
|
11
|
+
reviewedAt?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Unreviewed {
|
|
14
|
+
locale: string;
|
|
15
|
+
key: string;
|
|
16
|
+
entry: MemoryEntry;
|
|
17
|
+
}
|
|
18
|
+
/** Everything still waiting for a person to look at it, in a stable order. */
|
|
19
|
+
export declare function unreviewed(memory: Memory, locales?: string[], keys?: string[]): Unreviewed[];
|
|
20
|
+
/**
|
|
21
|
+
* Signs off on translations a person has actually looked at.
|
|
22
|
+
*
|
|
23
|
+
* Nothing else in the tool can set this: a machine translation is recorded
|
|
24
|
+
* unreviewed and stays that way until someone says otherwise, which is the
|
|
25
|
+
* whole point of recording it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function markReviewed(entries: Unreviewed[], now: string): number;
|
|
28
|
+
export interface Memory {
|
|
29
|
+
version: 1;
|
|
30
|
+
sourceLocale: string;
|
|
31
|
+
/** locale -> key -> entry. Nested rather than a flat list, for readable diffs. */
|
|
32
|
+
entries: Record<string, Record<string, MemoryEntry>>;
|
|
33
|
+
}
|
|
34
|
+
export declare class MemoryError extends Error {
|
|
35
|
+
}
|
|
36
|
+
/** 48 bits is plenty to notice an edited string, and keeps the file readable. */
|
|
37
|
+
export declare function hashValue(value: string): string;
|
|
38
|
+
export declare function memoryPath(root: string, explicit?: string): string;
|
|
39
|
+
export declare function emptyMemory(sourceLocale: string): Memory;
|
|
40
|
+
export declare function loadMemory(file: string): Memory | null;
|
|
41
|
+
/** Writes with sorted keys so the file diffs cleanly in review. */
|
|
42
|
+
export declare function saveMemory(file: string, memory: Memory): void;
|
|
43
|
+
export interface StaleVerdict {
|
|
44
|
+
stale: boolean;
|
|
45
|
+
tracked: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A translation is stale when the source moved and the translation demonstrably
|
|
49
|
+
* did not. If the target no longer matches what was recorded, someone edited it
|
|
50
|
+
* by hand — we cannot claim it is outdated, so we stay quiet.
|
|
51
|
+
*/
|
|
52
|
+
export declare function judge(entry: MemoryEntry | undefined, sourceValue: string, targetValue: string): StaleVerdict;
|
|
53
|
+
export interface SyncOptions {
|
|
54
|
+
origin: 'human' | 'machine';
|
|
55
|
+
reviewed: boolean;
|
|
56
|
+
locales?: string[];
|
|
57
|
+
/** Accept the current state wholesale, clearing stale flags. */
|
|
58
|
+
force: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface SyncResult {
|
|
61
|
+
created: number;
|
|
62
|
+
updated: number;
|
|
63
|
+
keptStale: number;
|
|
64
|
+
unchanged: number;
|
|
65
|
+
removed: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Records the current translations. Without --force this never clears a stale
|
|
69
|
+
* flag: an entry whose target is unchanged keeps its old source hash, because
|
|
70
|
+
* nothing about the translation has actually been redone.
|
|
71
|
+
*/
|
|
72
|
+
export declare function syncMemory(config: Config, memory: Memory, opts: SyncOptions): SyncResult;
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { loadBundle } from './scan.js';
|
|
5
|
+
export const MEMORY_FILE = '.i18n/memory.json';
|
|
6
|
+
/** Everything still waiting for a person to look at it, in a stable order. */
|
|
7
|
+
export function unreviewed(memory, locales, keys) {
|
|
8
|
+
const wantedLocale = locales && locales.length > 0 ? new Set(locales) : null;
|
|
9
|
+
const wantedKey = keys && keys.length > 0 ? new Set(keys) : null;
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const locale of Object.keys(memory.entries).sort()) {
|
|
12
|
+
if (wantedLocale && !wantedLocale.has(locale))
|
|
13
|
+
continue;
|
|
14
|
+
const byKey = memory.entries[locale] ?? {};
|
|
15
|
+
for (const key of Object.keys(byKey).sort()) {
|
|
16
|
+
if (wantedKey && !wantedKey.has(key))
|
|
17
|
+
continue;
|
|
18
|
+
const entry = byKey[key];
|
|
19
|
+
if (!entry.reviewed)
|
|
20
|
+
out.push({ locale, key, entry });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Signs off on translations a person has actually looked at.
|
|
27
|
+
*
|
|
28
|
+
* Nothing else in the tool can set this: a machine translation is recorded
|
|
29
|
+
* unreviewed and stays that way until someone says otherwise, which is the
|
|
30
|
+
* whole point of recording it.
|
|
31
|
+
*/
|
|
32
|
+
export function markReviewed(entries, now) {
|
|
33
|
+
for (const { entry } of entries) {
|
|
34
|
+
entry.reviewed = true;
|
|
35
|
+
entry.reviewedAt = now;
|
|
36
|
+
}
|
|
37
|
+
return entries.length;
|
|
38
|
+
}
|
|
39
|
+
export class MemoryError extends Error {
|
|
40
|
+
}
|
|
41
|
+
/** 48 bits is plenty to notice an edited string, and keeps the file readable. */
|
|
42
|
+
export function hashValue(value) {
|
|
43
|
+
return createHash('sha256').update(value, 'utf8').digest('hex').slice(0, 12);
|
|
44
|
+
}
|
|
45
|
+
export function memoryPath(root, explicit) {
|
|
46
|
+
return resolve(root, explicit ?? MEMORY_FILE);
|
|
47
|
+
}
|
|
48
|
+
export function emptyMemory(sourceLocale) {
|
|
49
|
+
return { version: 1, sourceLocale, entries: {} };
|
|
50
|
+
}
|
|
51
|
+
export function loadMemory(file) {
|
|
52
|
+
if (!existsSync(file))
|
|
53
|
+
return null;
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
throw new MemoryError(`Invalid memory file ${file}\n ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
const memory = parsed;
|
|
62
|
+
if (memory?.version !== 1 || typeof memory.entries !== 'object' || memory.entries === null) {
|
|
63
|
+
throw new MemoryError(`Unrecognised memory file format in ${file} (expected version 1)`);
|
|
64
|
+
}
|
|
65
|
+
return { version: 1, sourceLocale: memory.sourceLocale ?? '', entries: memory.entries };
|
|
66
|
+
}
|
|
67
|
+
/** Writes with sorted keys so the file diffs cleanly in review. */
|
|
68
|
+
export function saveMemory(file, memory) {
|
|
69
|
+
const entries = {};
|
|
70
|
+
for (const locale of Object.keys(memory.entries).sort()) {
|
|
71
|
+
const byKey = memory.entries[locale] ?? {};
|
|
72
|
+
const sorted = {};
|
|
73
|
+
for (const key of Object.keys(byKey).sort())
|
|
74
|
+
sorted[key] = byKey[key];
|
|
75
|
+
entries[locale] = sorted;
|
|
76
|
+
}
|
|
77
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
78
|
+
writeFileSync(file, `${JSON.stringify({ version: 1, sourceLocale: memory.sourceLocale, entries }, null, 2)}\n`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A translation is stale when the source moved and the translation demonstrably
|
|
82
|
+
* did not. If the target no longer matches what was recorded, someone edited it
|
|
83
|
+
* by hand — we cannot claim it is outdated, so we stay quiet.
|
|
84
|
+
*/
|
|
85
|
+
export function judge(entry, sourceValue, targetValue) {
|
|
86
|
+
if (!entry)
|
|
87
|
+
return { stale: false, tracked: false };
|
|
88
|
+
const moved = entry.sourceHash !== hashValue(sourceValue);
|
|
89
|
+
return { stale: moved && entry.value === targetValue, tracked: true };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Records the current translations. Without --force this never clears a stale
|
|
93
|
+
* flag: an entry whose target is unchanged keeps its old source hash, because
|
|
94
|
+
* nothing about the translation has actually been redone.
|
|
95
|
+
*/
|
|
96
|
+
export function syncMemory(config, memory, opts) {
|
|
97
|
+
const source = loadBundle(config, config.sourceLocale);
|
|
98
|
+
const result = { created: 0, updated: 0, keptStale: 0, unchanged: 0, removed: 0 };
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
const wanted = opts.locales && opts.locales.length > 0 ? new Set(opts.locales) : null;
|
|
101
|
+
memory.sourceLocale = config.sourceLocale;
|
|
102
|
+
for (const locale of config.locales) {
|
|
103
|
+
if (locale === config.sourceLocale)
|
|
104
|
+
continue;
|
|
105
|
+
if (wanted && !wanted.has(locale))
|
|
106
|
+
continue;
|
|
107
|
+
const target = loadBundle(config, locale);
|
|
108
|
+
const byKey = (memory.entries[locale] ??= {});
|
|
109
|
+
for (const [key, sourceLeaf] of source.leaves) {
|
|
110
|
+
const targetLeaf = target.leaves.get(key);
|
|
111
|
+
if (!targetLeaf || targetLeaf.value.trim() === '')
|
|
112
|
+
continue;
|
|
113
|
+
const entry = byKey[key];
|
|
114
|
+
const sourceHash = hashValue(sourceLeaf.value);
|
|
115
|
+
if (!entry) {
|
|
116
|
+
byKey[key] = {
|
|
117
|
+
sourceHash,
|
|
118
|
+
value: targetLeaf.value,
|
|
119
|
+
origin: opts.origin,
|
|
120
|
+
reviewed: opts.reviewed,
|
|
121
|
+
updatedAt: now,
|
|
122
|
+
};
|
|
123
|
+
result.created++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const translationChanged = entry.value !== targetLeaf.value;
|
|
127
|
+
if (translationChanged || opts.force) {
|
|
128
|
+
if (entry.sourceHash === sourceHash && !translationChanged) {
|
|
129
|
+
result.unchanged++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
byKey[key] = {
|
|
133
|
+
sourceHash,
|
|
134
|
+
value: targetLeaf.value,
|
|
135
|
+
origin: translationChanged ? opts.origin : entry.origin,
|
|
136
|
+
reviewed: translationChanged ? opts.reviewed : entry.reviewed,
|
|
137
|
+
updatedAt: now,
|
|
138
|
+
// A sign-off survives a source edit; only a rewritten translation
|
|
139
|
+
// invalidates it.
|
|
140
|
+
...(translationChanged || entry.reviewedAt === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { reviewedAt: entry.reviewedAt }),
|
|
143
|
+
};
|
|
144
|
+
result.updated++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (entry.sourceHash !== sourceHash)
|
|
148
|
+
result.keptStale++;
|
|
149
|
+
else
|
|
150
|
+
result.unchanged++;
|
|
151
|
+
}
|
|
152
|
+
// Drop bookkeeping for keys that no longer exist in the source.
|
|
153
|
+
for (const key of Object.keys(byKey)) {
|
|
154
|
+
if (!source.leaves.has(key)) {
|
|
155
|
+
delete byKey[key];
|
|
156
|
+
result.removed++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder extraction across the interpolation syntaxes that actually collide
|
|
3
|
+
* in the wild. Patterns are applied in a fixed order and every match is masked
|
|
4
|
+
* out of the working copy, so `{{name}}` is never also counted as `{name}`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_SYNTAXES: string[];
|
|
7
|
+
export declare const ALL_SYNTAXES: string[];
|
|
8
|
+
/** Returns a multiset of placeholder labels: `{name}` twice is not the same as once. */
|
|
9
|
+
export declare function extractPlaceholders(text: string, syntaxes: string[]): Map<string, number>;
|
|
10
|
+
export interface PlaceholderDiff {
|
|
11
|
+
missing: string[];
|
|
12
|
+
extra: string[];
|
|
13
|
+
}
|
|
14
|
+
/** What the target lost from the source, and what it invented. */
|
|
15
|
+
export declare function diffPlaceholders(source: Map<string, number>, target: Map<string, number>): PlaceholderDiff;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder extraction across the interpolation syntaxes that actually collide
|
|
3
|
+
* in the wild. Patterns are applied in a fixed order and every match is masked
|
|
4
|
+
* out of the working copy, so `{{name}}` is never also counted as `{name}`.
|
|
5
|
+
*/
|
|
6
|
+
const PATTERNS = {
|
|
7
|
+
// {{ name }} — mustache / i18next default
|
|
8
|
+
mustache: {
|
|
9
|
+
re: /\{\{\s*[#/^&]?\s*([\w.$-]+)\s*\}\}/g,
|
|
10
|
+
label: (m) => `{{${m[1]}}}`,
|
|
11
|
+
},
|
|
12
|
+
// %{name} — ruby i18n
|
|
13
|
+
ruby: {
|
|
14
|
+
re: /%\{\s*([\w.$-]+)\s*\}/g,
|
|
15
|
+
label: (m) => `%{${m[1]}}`,
|
|
16
|
+
},
|
|
17
|
+
// {count, plural, ...} — ICU complex argument; matched by its head so that
|
|
18
|
+
// nested braces in the sub-messages do not hide the argument name.
|
|
19
|
+
icu_complex: {
|
|
20
|
+
re: /\{\s*([\w.$-]+)\s*,\s*(plural|select|selectordinal|number|date|time)\b/g,
|
|
21
|
+
label: (m) => `{${m[1]},${m[2]}}`,
|
|
22
|
+
},
|
|
23
|
+
// {name} — ICU simple argument
|
|
24
|
+
icu: {
|
|
25
|
+
re: /\{\s*([\w.$-]+)\s*\}/g,
|
|
26
|
+
label: (m) => `{${m[1]}}`,
|
|
27
|
+
},
|
|
28
|
+
// %s, %d, %1$s — printf
|
|
29
|
+
printf: {
|
|
30
|
+
re: /%(?:(\d+)\$)?([sdifux])/g,
|
|
31
|
+
label: (m) => (m[1] ? `%${m[1]}$${m[2]}` : `%${m[2]}`),
|
|
32
|
+
},
|
|
33
|
+
// <0>...</0> — react-i18next <Trans> indices
|
|
34
|
+
tag: {
|
|
35
|
+
re: /<\/?(\d+)>/g,
|
|
36
|
+
label: (m) => m[0],
|
|
37
|
+
},
|
|
38
|
+
// :name — Laravel. Off by default: it false-positives on prose like
|
|
39
|
+
// "Warning:Important". Enabled automatically for PHP locale files.
|
|
40
|
+
// Laravel treats :name, :Name and :NAME as one placeholder rendered with
|
|
41
|
+
// different capitalisation, so they normalise to one label here.
|
|
42
|
+
laravel: {
|
|
43
|
+
re: /(?<![\w:]):([a-zA-Z][\w]*)/g,
|
|
44
|
+
label: (m) => `:${m[1].toLowerCase()}`,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** Longest / most specific syntaxes first, so masking works. */
|
|
48
|
+
const ORDER = ['mustache', 'ruby', 'icu_complex', 'icu', 'printf', 'tag', 'laravel'];
|
|
49
|
+
export const DEFAULT_SYNTAXES = ['mustache', 'ruby', 'icu_complex', 'icu', 'printf', 'tag'];
|
|
50
|
+
export const ALL_SYNTAXES = ORDER;
|
|
51
|
+
const MASK = '\u0000';
|
|
52
|
+
function maskSpans(text, spans) {
|
|
53
|
+
const chars = [...text];
|
|
54
|
+
for (const [start, end] of spans) {
|
|
55
|
+
for (let i = start; i < end && i < chars.length; i++)
|
|
56
|
+
chars[i] = MASK;
|
|
57
|
+
}
|
|
58
|
+
return chars.join('');
|
|
59
|
+
}
|
|
60
|
+
/** Returns a multiset of placeholder labels: `{name}` twice is not the same as once. */
|
|
61
|
+
export function extractPlaceholders(text, syntaxes) {
|
|
62
|
+
const counts = new Map();
|
|
63
|
+
let masked = text;
|
|
64
|
+
for (const id of ORDER) {
|
|
65
|
+
if (!syntaxes.includes(id))
|
|
66
|
+
continue;
|
|
67
|
+
const spec = PATTERNS[id];
|
|
68
|
+
if (!spec)
|
|
69
|
+
continue;
|
|
70
|
+
const re = new RegExp(spec.re.source, spec.re.flags);
|
|
71
|
+
const spans = [];
|
|
72
|
+
let m;
|
|
73
|
+
while ((m = re.exec(masked)) !== null) {
|
|
74
|
+
if (m[0].length === 0) {
|
|
75
|
+
re.lastIndex++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const label = spec.label(m);
|
|
79
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
80
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
81
|
+
}
|
|
82
|
+
if (spans.length > 0)
|
|
83
|
+
masked = maskSpans(masked, spans);
|
|
84
|
+
}
|
|
85
|
+
return counts;
|
|
86
|
+
}
|
|
87
|
+
/** What the target lost from the source, and what it invented. */
|
|
88
|
+
export function diffPlaceholders(source, target) {
|
|
89
|
+
const missing = [];
|
|
90
|
+
const extra = [];
|
|
91
|
+
for (const [label, count] of source) {
|
|
92
|
+
const got = target.get(label) ?? 0;
|
|
93
|
+
if (got < count)
|
|
94
|
+
missing.push(count - got > 1 ? `${label} x${count - got}` : label);
|
|
95
|
+
}
|
|
96
|
+
for (const [label, count] of target) {
|
|
97
|
+
const had = source.get(label) ?? 0;
|
|
98
|
+
if (count > had)
|
|
99
|
+
extra.push(count - had > 1 ? `${label} x${count - had}` : label);
|
|
100
|
+
}
|
|
101
|
+
return { missing, extra };
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=placeholders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholders.js","sourceRoot":"","sources":["../src/placeholders.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,MAAM,QAAQ,GAAgC;IAC5C,0CAA0C;IAC1C,QAAQ,EAAE;QACR,EAAE,EAAE,qCAAqC;QACzC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;KAC5B;IACD,sBAAsB;IACtB,IAAI,EAAE;QACJ,EAAE,EAAE,wBAAwB;QAC5B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG;KAC3B;IACD,2EAA2E;IAC3E,mEAAmE;IACnE,WAAW,EAAE;QACX,EAAE,EAAE,yEAAyE;QAC7E,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG;KAClC;IACD,+BAA+B;IAC/B,GAAG,EAAE;QACH,EAAE,EAAE,uBAAuB;QAC3B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG;KAC1B;IACD,wBAAwB;IACxB,MAAM,EAAE;QACN,EAAE,EAAE,0BAA0B;QAC9B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACvD;IACD,6CAA6C;IAC7C,GAAG,EAAE;QACH,EAAE,EAAE,aAAa;QACjB,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;KACnB;IACD,oEAAoE;IACpE,mEAAmE;IACnE,yEAAyE;IACzE,iEAAiE;IACjE,OAAO,EAAE;QACP,EAAE,EAAE,6BAA6B;QACjC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,EAAE;KACxC;CACF,CAAC;AAEF,gEAAgE;AAChE,MAAM,KAAK,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;AAErF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC5F,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,CAAC;AAElC,MAAM,IAAI,GAAG,QAAQ,CAAC;AAEtB,SAAS,SAAS,CAAC,IAAY,EAAE,KAA8B;IAC7D,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACxB,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;QACjC,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE;YAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,QAAkB;IAClE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,IAAI,MAAM,GAAG,IAAI,CAAC;IAElB,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,SAAS;QACrC,MAAM,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QACrD,MAAM,KAAK,GAA4B,EAAE,CAAC;QAC1C,IAAI,CAAyB,CAAC;QAE9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACtC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtB,EAAE,CAAC,SAAS,EAAE,CAAC;gBACf,SAAS;YACX,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAChD,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAOD,kEAAkE;AAClE,MAAM,UAAU,gBAAgB,CAC9B,MAA2B,EAC3B,MAA2B;IAE3B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,GAAG,GAAG,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,KAAK,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtF,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK,GAAG,GAAG;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,KAAK,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACpF,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLDR plural categories, taken from the ICU data already in the runtime rather
|
|
3
|
+
* than a hand-maintained table: the table would go stale, and this is the same
|
|
4
|
+
* data the application itself will use at runtime.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CATEGORY_NAMES: readonly ["zero", "one", "two", "few", "many", "other"];
|
|
7
|
+
export type Category = (typeof CATEGORY_NAMES)[number];
|
|
8
|
+
export declare function isCategory(name: string): name is Category;
|
|
9
|
+
/**
|
|
10
|
+
* Categories CLDR defines for a locale, or null when we cannot tell.
|
|
11
|
+
*
|
|
12
|
+
* Intl.PluralRules silently falls back to the system locale for tags it does
|
|
13
|
+
* not know — asking for "zz" on a Russian machine reports four categories — so
|
|
14
|
+
* a resolved language subtag that does not match the request means "unknown"
|
|
15
|
+
* rather than a wrong answer.
|
|
16
|
+
*/
|
|
17
|
+
export declare function categoriesFor(locale: string): Set<string> | null;
|
|
18
|
+
export interface PluralBlock {
|
|
19
|
+
arg: string;
|
|
20
|
+
type: 'plural' | 'selectordinal';
|
|
21
|
+
/** Keyword selectors: one, few, other … */
|
|
22
|
+
categories: Set<string>;
|
|
23
|
+
/** Exact selectors: =0, =1 … which are not CLDR categories. */
|
|
24
|
+
exact: Set<string>;
|
|
25
|
+
}
|
|
26
|
+
export interface IcuScan {
|
|
27
|
+
blocks: PluralBlock[];
|
|
28
|
+
error: string | null;
|
|
29
|
+
}
|
|
30
|
+
/** Whether the string is trying to be an ICU plural at all. */
|
|
31
|
+
export declare function looksLikeIcuPlural(text: string): boolean;
|
|
32
|
+
export declare function scanIcu(text: string): IcuScan;
|
|
33
|
+
export interface SuffixKey {
|
|
34
|
+
base: string;
|
|
35
|
+
category: Category;
|
|
36
|
+
separator: '_' | '.';
|
|
37
|
+
}
|
|
38
|
+
export interface PluralGroup {
|
|
39
|
+
categories: Set<Category>;
|
|
40
|
+
/** Kept so a finding can name the keys as the project actually writes them. */
|
|
41
|
+
separator: '_' | '.';
|
|
42
|
+
}
|
|
43
|
+
/** i18next writes item_one; Rails and Symfony nest it as item.one. */
|
|
44
|
+
export declare function splitPluralSuffix(key: string): SuffixKey | null;
|
|
45
|
+
/**
|
|
46
|
+
* Groups keys like item_one / item_other into base -> categories.
|
|
47
|
+
*
|
|
48
|
+
* A group must carry `other`, which every CLDR plural set has. Without that
|
|
49
|
+
* test, Rails' `restrict_dependent_destroy.has_one` / `has_many` — ActiveRecord
|
|
50
|
+
* association names — read as a plural group and were asked to grow the forms
|
|
51
|
+
* their language requires.
|
|
52
|
+
*/
|
|
53
|
+
export declare function pluralGroups(keys: Iterable<string>): Map<string, PluralGroup>;
|
|
54
|
+
/**
|
|
55
|
+
* Laravel picks a segment by position or by explicit {0} / [1,*] ranges, which
|
|
56
|
+
* is its own mechanism and not CLDR. All we can honestly compare is the segment
|
|
57
|
+
* count, so that is all this reports.
|
|
58
|
+
*/
|
|
59
|
+
export declare function pipeSegments(text: string): number;
|