localewarden 0.1.1 → 0.2.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/README.md +54 -9
- package/dist/checks.d.ts +19 -2
- package/dist/checks.js +94 -18
- package/dist/cli.js +24 -3
- package/dist/config.d.ts +10 -0
- package/dist/config.js +13 -0
- package/dist/files.d.ts +21 -0
- package/dist/files.js +52 -1
- package/dist/placeholders.js +2 -1
- package/dist/project.d.ts +15 -0
- package/dist/project.js +84 -4
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.js +9 -2
- package/dist/review.js +25 -5
- package/dist/scope.d.ts +15 -0
- package/dist/scope.js +26 -0
- package/dist/state.d.ts +1 -1
- package/dist/translate.js +216 -169
- package/package.json +12 -1
package/dist/files.js
CHANGED
|
@@ -87,7 +87,7 @@ export function findSourceFiles(root, pattern, sourceLanguage) {
|
|
|
87
87
|
};
|
|
88
88
|
const files = [];
|
|
89
89
|
for (const rel of all.sort()) {
|
|
90
|
-
if (
|
|
90
|
+
if (!/\.(json|arb|txt)$/.test(rel))
|
|
91
91
|
continue;
|
|
92
92
|
const match = matcher.exec(rel);
|
|
93
93
|
if (!match || match.groups?.lang !== sourceLanguage)
|
|
@@ -99,6 +99,38 @@ export function findSourceFiles(root, pattern, sourceLanguage) {
|
|
|
99
99
|
}
|
|
100
100
|
return files;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Key pattern -> RegExp. "*" matches within one segment, "**" any number of segments. A
|
|
104
|
+
* pattern without a dot matches the last segment anywhere ("id" matches "steps.2.id").
|
|
105
|
+
*/
|
|
106
|
+
export function keyPattern(pattern) {
|
|
107
|
+
const body = pattern
|
|
108
|
+
.split(/(\*\*|\*)/)
|
|
109
|
+
.map(part => (part === '**' ? '.*' : part === '*' ? '[^.]*' : escapeRegExp(part)))
|
|
110
|
+
.join('');
|
|
111
|
+
return new RegExp(pattern.includes('.') ? `^${body}$` : `(?:^|\\.)${body}$`);
|
|
112
|
+
}
|
|
113
|
+
/** Path pattern -> RegExp ("*" within a folder, "**" across folders, {lang} as written). */
|
|
114
|
+
export function pathPattern(pattern) {
|
|
115
|
+
const body = pattern
|
|
116
|
+
.replace(/\\/g, '/')
|
|
117
|
+
.replace(/^\.\//, '')
|
|
118
|
+
.split(/(\*\*\/?|\*)/)
|
|
119
|
+
.map(part => (part.startsWith('**') ? '(?:.*/)?' : part === '*' ? '[^/]*' : escapeRegExp(part)))
|
|
120
|
+
.join('');
|
|
121
|
+
return new RegExp(`^${body}$`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Values that are not text and stay as they are in every language: URLs, email addresses,
|
|
125
|
+
* file paths and plain numbers or codes without spaces.
|
|
126
|
+
*/
|
|
127
|
+
export function isLiteralValue(value) {
|
|
128
|
+
const v = value.trim();
|
|
129
|
+
return (/^(?:https?:\/\/|mailto:|tel:)\S+$/i.test(v) ||
|
|
130
|
+
/^[\w.+-]+@[\w-]+(?:\.[\w-]+)+$/.test(v) ||
|
|
131
|
+
/^(?:\.{0,2}\/)?[\w@.-]+(?:\/[\w@.-]+)*\.(?:png|jpe?g|gif|svg|webp|avif|ico|mp3|mp4|webm|wav|pdf|json|css|js|html?)$/i.test(v) ||
|
|
132
|
+
/^[\d\s.,:%+\-–/×x#]+$/.test(v));
|
|
133
|
+
}
|
|
102
134
|
export const keyOf = (segments) => segments.join('.');
|
|
103
135
|
/** All string values of a parsed JSON document, in document order. */
|
|
104
136
|
export function stringLeaves(value, prefix = [], out = []) {
|
|
@@ -209,6 +241,25 @@ export function detectFormat(text) {
|
|
|
209
241
|
export function serialize(value, format) {
|
|
210
242
|
return JSON.stringify(value, null, format.indent) + (format.finalNewline ? '\n' : '');
|
|
211
243
|
}
|
|
244
|
+
const txtKey = (rel) => path.posix.basename(rel.replace(/\\/g, '/'), '.txt');
|
|
245
|
+
/**
|
|
246
|
+
* Parses a locale file. A .txt file (fastlane metadata: description.txt, keywords.txt) is one
|
|
247
|
+
* string, keyed by its file name, so maxLength patterns like "keywords" apply to it.
|
|
248
|
+
*/
|
|
249
|
+
export function parseDoc(rel, text) {
|
|
250
|
+
if (rel.endsWith('.txt'))
|
|
251
|
+
return { [txtKey(rel)]: text.replace(/\r?\n$/, '') };
|
|
252
|
+
return JSON.parse(text);
|
|
253
|
+
}
|
|
254
|
+
/** Flutter ARB metadata ("@@locale", "@title": { description, placeholders }): not text. */
|
|
255
|
+
export const isArbMetadata = (rel, key) => rel.endsWith('.arb') && key.startsWith('@');
|
|
256
|
+
/** Text of a locale file, or null when a .txt file has no value to write. */
|
|
257
|
+
export function serializeDoc(rel, doc, format) {
|
|
258
|
+
if (!rel.endsWith('.txt'))
|
|
259
|
+
return serialize(doc, format);
|
|
260
|
+
const value = doc[txtKey(rel)];
|
|
261
|
+
return typeof value === 'string' ? value + (format.finalNewline ? '\n' : '') : null;
|
|
262
|
+
}
|
|
212
263
|
export function readText(file) {
|
|
213
264
|
try {
|
|
214
265
|
return fs.readFileSync(file, 'utf8');
|
package/dist/placeholders.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
export const DEFAULT_PLACEHOLDER_PATTERNS = [
|
|
7
7
|
'\\{\\{\\s*[\\w.-]+\\s*\\}\\}', // {{name}} i18next, Handlebars, vue-i18n
|
|
8
8
|
'\\{\\s*[\\w.-]+\\s*\\}', // {name} ICU, react-intl, i18next (custom)
|
|
9
|
-
|
|
9
|
+
// Not followed by a letter: Hungarian writes suffixes after percent signs ("100%-ig").
|
|
10
|
+
'%(?:\\d+\\$)?[-+0#]*\\d*(?:\\.\\d+)?[sdifuxXeEgGc@](?![A-Za-z\\u00C0-\\u024F])', // %s %d %1$s %.2f printf, Android, iOS
|
|
10
11
|
'%\\([\\w.-]+\\)[sdif]', // %(name)s Python
|
|
11
12
|
'%\\{[\\w.-]+\\}', // %{name} Ruby, rails-i18n
|
|
12
13
|
'\\$\\{[\\w.-]+\\}', // ${name} template literals
|
package/dist/project.d.ts
CHANGED
|
@@ -9,10 +9,25 @@ export interface Finding {
|
|
|
9
9
|
text: string;
|
|
10
10
|
note?: string;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Short sibling strings (same parent key) that differ in the source but got the same
|
|
14
|
+
* translation: answer options, tabs or menu items the user can no longer tell apart.
|
|
15
|
+
*/
|
|
16
|
+
export declare function collapsedSiblings(source: Map<string, string>, target: Map<string, string>): {
|
|
17
|
+
key: string;
|
|
18
|
+
note: string;
|
|
19
|
+
}[];
|
|
12
20
|
/**
|
|
13
21
|
* Runs the quality checks over every translated string. Approved hand edits are skipped
|
|
14
22
|
* except for placeholder and script errors, which break the app either way.
|
|
15
23
|
*/
|
|
16
24
|
export declare function checkProject(config: Config, languages?: string[]): Finding[];
|
|
25
|
+
/**
|
|
26
|
+
* Repairs placeholder findings that have exactly one possible fix: the source has one
|
|
27
|
+
* placeholder and the translation one brace token with another name ("{heures}" for
|
|
28
|
+
* "{hours}"). The file text is edited in place, so its formatting stays as it is.
|
|
29
|
+
* Returns the repaired findings.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fixPlaceholders(config: Config, findings: Finding[]): Finding[];
|
|
17
32
|
/** Table of finding counts per language and check. */
|
|
18
33
|
export declare function summaryTable(findings: Finding[], languages: string[]): string;
|
package/dist/project.js
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { CHECKS, Checker, ERROR_CHECKS } from './checks.js';
|
|
3
|
-
import { findSourceFiles, flatten, readText } from './files.js';
|
|
3
|
+
import { findSourceFiles, flatten, isArbMetadata, parseDoc, readText, writeText } from './files.js';
|
|
4
4
|
import { reviewId, State } from './state.js';
|
|
5
5
|
import { hash } from './util.js';
|
|
6
|
+
/**
|
|
7
|
+
* Short sibling strings (same parent key) that differ in the source but got the same
|
|
8
|
+
* translation: answer options, tabs or menu items the user can no longer tell apart.
|
|
9
|
+
*/
|
|
10
|
+
export function collapsedSiblings(source, target) {
|
|
11
|
+
const groups = new Map();
|
|
12
|
+
for (const key of target.keys()) {
|
|
13
|
+
// Only nested keys: in a flat file every string would be a "sibling" of every other.
|
|
14
|
+
if (!key.includes('.'))
|
|
15
|
+
continue;
|
|
16
|
+
const parent = key.slice(0, key.lastIndexOf('.'));
|
|
17
|
+
// Plural forms of one string are meant to look alike.
|
|
18
|
+
if (/_(zero|one|two|few|many|other)$/.test(key))
|
|
19
|
+
continue;
|
|
20
|
+
groups.set(parent, [...(groups.get(parent) ?? []), key]);
|
|
21
|
+
}
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const keys of groups.values()) {
|
|
24
|
+
const seen = new Map();
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
const src = source.get(key)?.trim();
|
|
27
|
+
const text = target.get(key)?.trim().toLocaleLowerCase();
|
|
28
|
+
if (!src || !text || src.length > 40)
|
|
29
|
+
continue;
|
|
30
|
+
const earlier = seen.get(text);
|
|
31
|
+
if (earlier !== undefined && source.get(earlier)?.trim().toLocaleLowerCase() !== src.toLocaleLowerCase()) {
|
|
32
|
+
out.push({ key, note: `same translation as "${earlier}", although the source differs ("${source.get(earlier)}" / "${src}")` });
|
|
33
|
+
}
|
|
34
|
+
else if (earlier === undefined) {
|
|
35
|
+
seen.set(text, key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
6
41
|
function readJson(file) {
|
|
7
42
|
const text = readText(file);
|
|
8
|
-
return text === null ? null :
|
|
43
|
+
return text === null ? null : parseDoc(file, text);
|
|
9
44
|
}
|
|
10
45
|
/**
|
|
11
46
|
* Runs the quality checks over every translated string. Approved hand edits are skipped
|
|
@@ -16,6 +51,8 @@ export function checkProject(config, languages = config.targetLanguages) {
|
|
|
16
51
|
const state = new State(path.join(config.root, config.stateDir));
|
|
17
52
|
const findings = [];
|
|
18
53
|
for (const file of findSourceFiles(config.root, config.files, config.sourceLanguage)) {
|
|
54
|
+
if (checker.scope.isExcluded(file))
|
|
55
|
+
continue;
|
|
19
56
|
const sourceDoc = readJson(path.join(config.root, file.pathFor(config.sourceLanguage)));
|
|
20
57
|
if (sourceDoc === null)
|
|
21
58
|
continue;
|
|
@@ -32,10 +69,17 @@ export function checkProject(config, languages = config.targetLanguages) {
|
|
|
32
69
|
}
|
|
33
70
|
if (doc === null)
|
|
34
71
|
continue;
|
|
35
|
-
|
|
72
|
+
const translated = flatten(doc);
|
|
73
|
+
for (const { key, note } of collapsedSiblings(source, translated)) {
|
|
74
|
+
const review = state.review[reviewId(lang, file.id, key)];
|
|
75
|
+
if (review?.status === 'approved')
|
|
76
|
+
continue;
|
|
77
|
+
findings.push({ lang, check: 'partial', severity: 'warning', file: rel, key, text: translated.get(key) ?? '', note });
|
|
78
|
+
}
|
|
79
|
+
for (const [key, text] of translated) {
|
|
36
80
|
// Plural forms only the target language has are checked against the source "_other".
|
|
37
81
|
const sourceText = source.get(key) ?? (/_(zero|one|two|few|many)$/.test(key) ? source.get(key.replace(/_(zero|one|two|few|many)$/, '_other')) : undefined);
|
|
38
|
-
if (sourceText === undefined || sourceText.trim() === '')
|
|
82
|
+
if (sourceText === undefined || sourceText.trim() === '' || isArbMetadata(rel, key) || checker.scope.isLiteral(key, sourceText))
|
|
39
83
|
continue;
|
|
40
84
|
const review = state.review[reviewId(lang, file.id, key)];
|
|
41
85
|
const approved = review?.status === 'approved' && review.valueHash === hash(text);
|
|
@@ -57,6 +101,42 @@ export function checkProject(config, languages = config.targetLanguages) {
|
|
|
57
101
|
}
|
|
58
102
|
return findings;
|
|
59
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Repairs placeholder findings that have exactly one possible fix: the source has one
|
|
106
|
+
* placeholder and the translation one brace token with another name ("{heures}" for
|
|
107
|
+
* "{hours}"). The file text is edited in place, so its formatting stays as it is.
|
|
108
|
+
* Returns the repaired findings.
|
|
109
|
+
*/
|
|
110
|
+
export function fixPlaceholders(config, findings) {
|
|
111
|
+
const fixed = [];
|
|
112
|
+
const sources = new Map();
|
|
113
|
+
for (const file of findSourceFiles(config.root, config.files, config.sourceLanguage)) {
|
|
114
|
+
const doc = readJson(path.join(config.root, file.pathFor(config.sourceLanguage)));
|
|
115
|
+
if (doc)
|
|
116
|
+
for (const lang of config.targetLanguages)
|
|
117
|
+
sources.set(file.pathFor(lang), flatten(doc));
|
|
118
|
+
}
|
|
119
|
+
const checker = new Checker(config);
|
|
120
|
+
for (const finding of findings.filter(f => f.check === 'placeholder')) {
|
|
121
|
+
const source = sources.get(finding.file)?.get(finding.key);
|
|
122
|
+
if (source === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const expected = source.match(checker.placeholderRe) ?? [];
|
|
125
|
+
const tokens = finding.text.match(/\{\{?[^{}]+\}?\}/g) ?? [];
|
|
126
|
+
const [want, got] = [expected[0], tokens[0]];
|
|
127
|
+
if (new Set(expected).size !== 1 || tokens.length !== 1 || !want || !got || got === want)
|
|
128
|
+
continue;
|
|
129
|
+
const repaired = finding.text.replace(got, want);
|
|
130
|
+
const file = path.join(config.root, finding.file);
|
|
131
|
+
const text = readText(file);
|
|
132
|
+
const before = JSON.stringify(finding.text).slice(1, -1);
|
|
133
|
+
if (text === null || text.split(before).length !== 2)
|
|
134
|
+
continue; // not found exactly once
|
|
135
|
+
writeText(file, text.replace(before, JSON.stringify(repaired).slice(1, -1)));
|
|
136
|
+
fixed.push({ ...finding, text: repaired });
|
|
137
|
+
}
|
|
138
|
+
return fixed;
|
|
139
|
+
}
|
|
60
140
|
/** Table of finding counts per language and check. */
|
|
61
141
|
export function summaryTable(findings, languages) {
|
|
62
142
|
const header = ['lang', ...CHECKS];
|
package/dist/prompt.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export interface PromptItem {
|
|
|
4
4
|
source: string;
|
|
5
5
|
/** Existing translation of an older version of the source (revision mode). */
|
|
6
6
|
previous?: string;
|
|
7
|
+
/** Maximum characters of the translation (store listings, SEO titles, buttons). */
|
|
8
|
+
maxLength?: number;
|
|
9
|
+
/** Set when --fix-flagged revises a translation that lost content. */
|
|
10
|
+
completing?: boolean;
|
|
7
11
|
}
|
|
8
12
|
/** System prompt for translating a JSON array of strings. */
|
|
9
13
|
export declare function batchPrompt(config: Config, lang: string, items: PromptItem[]): string;
|
package/dist/prompt.js
CHANGED
|
@@ -46,17 +46,24 @@ export function batchPrompt(config, lang, items) {
|
|
|
46
46
|
const revision = revised.length > 0
|
|
47
47
|
? ` REVISION: the source of some elements was edited after they had been translated. Their existing translation (of the older source) by 1-based position: ${JSON.stringify(Object.fromEntries(revised))}. For these elements, ${REVISION_RULE}`
|
|
48
48
|
: '';
|
|
49
|
+
const limited = items.map((item, i) => (item.maxLength ? `element ${i + 1} at most ${item.maxLength}` : null)).filter(Boolean);
|
|
50
|
+
const lengths = limited.length > 0
|
|
51
|
+
? ` LENGTH LIMIT: these elements are cut off past a character limit (counting spaces): ${limited.join(', ')} characters. Stay under it even if that means a shorter, freer phrasing; never pad.`
|
|
52
|
+
: '';
|
|
49
53
|
const plural = items.some(item => /_(zero|one|two|few|many|other)$/.test(item.key))
|
|
50
54
|
? ` PLURALS: keys ending in _zero, _one, _two, _few, _many or _other are plural forms (Unicode CLDR categories) of ${languageName(lang)}. Write the form of that category, even when the source text given is the English plural (_zero is used when the count is 0, _two when it is 2; keep the {{count}} placeholder).`
|
|
51
55
|
: '';
|
|
52
|
-
return `${intro(config, lang)} Translate each string in the JSON array. Return ONLY a valid JSON array of strings with the same number of elements in the same order. No explanations, no code fences.${rules(config, lang, items.map(i => i.source))}${keys}${plural}${revision}`;
|
|
56
|
+
return `${intro(config, lang)} Translate each string in the JSON array. Return ONLY a valid JSON array of strings with the same number of elements in the same order. No explanations, no code fences.${rules(config, lang, items.map(i => i.source))}${keys}${lengths}${plural}${revision}`;
|
|
53
57
|
}
|
|
54
58
|
/** System prompt for translating one string as plain text. */
|
|
55
59
|
export function singlePrompt(config, lang, item, extra = '') {
|
|
56
60
|
const revision = item.previous
|
|
57
61
|
? ` REVISION: the source was edited after it had been translated. Existing translation (of the older source): ${JSON.stringify(item.previous)}. ${REVISION_RULE[0].toUpperCase()}${REVISION_RULE.slice(1)}`
|
|
58
62
|
: '';
|
|
59
|
-
|
|
63
|
+
const length = item.maxLength
|
|
64
|
+
? ` LENGTH LIMIT: at most ${item.maxLength} characters including spaces; the text is cut off past that. Prefer a shorter, freer phrasing over a literal one.`
|
|
65
|
+
: '';
|
|
66
|
+
return `${intro(config, lang)}${length} The user message is one UI string (key: ${item.key}). Output only the translation, with no explanations, quotes or commentary.${rules(config, lang, [item.source])}${revision}${extra}`;
|
|
60
67
|
}
|
|
61
68
|
/** Turns a translation request into a minimal correction of an existing translation. */
|
|
62
69
|
export function repairInstruction(existing, problems) {
|
package/dist/review.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { flatten, readText } from './files.js';
|
|
3
|
-
import { parseReviewId, State } from './state.js';
|
|
4
|
-
import { hash } from './util.js';
|
|
2
|
+
import { findSourceFiles, flatten, parseDoc, readText } from './files.js';
|
|
3
|
+
import { parseReviewId, reviewId, State } from './state.js';
|
|
4
|
+
import { hash, today } from './util.js';
|
|
5
5
|
function currentValue(config, lang, fileId, key) {
|
|
6
|
-
const
|
|
6
|
+
const rel = fileId.split('{lang}').join(lang);
|
|
7
|
+
const text = readText(path.join(config.root, rel));
|
|
7
8
|
if (text === null)
|
|
8
9
|
return undefined;
|
|
9
10
|
try {
|
|
10
|
-
return flatten(
|
|
11
|
+
return flatten(parseDoc(rel, text)).get(key);
|
|
11
12
|
}
|
|
12
13
|
catch {
|
|
13
14
|
return undefined;
|
|
@@ -58,6 +59,25 @@ export function updateReview(config, action, selectors) {
|
|
|
58
59
|
}
|
|
59
60
|
changed++;
|
|
60
61
|
}
|
|
62
|
+
// Approving a string that is not on the list marks it as checked by a person: it is then
|
|
63
|
+
// protected like a hand edit and the quality check no longer reports warnings for it.
|
|
64
|
+
if (action === 'approve') {
|
|
65
|
+
const exact = selectors.filter(sel => sel !== 'all' && !sel.endsWith(':*') && sel.includes(':'));
|
|
66
|
+
for (const selector of exact) {
|
|
67
|
+
const colon = selector.indexOf(':');
|
|
68
|
+
const [lang, key] = [selector.slice(0, colon), selector.slice(colon + 1)];
|
|
69
|
+
for (const file of findSourceFiles(config.root, config.files, config.sourceLanguage)) {
|
|
70
|
+
const id = reviewId(lang, file.id, key);
|
|
71
|
+
if (state.review[id])
|
|
72
|
+
continue;
|
|
73
|
+
const value = currentValue(config, lang, file.id, key);
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
state.review[id] = { status: 'approved', reason: 'approved-by-hand', file: file.pathFor(lang), since: today(), valueHash: hash(value) };
|
|
77
|
+
changed++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
61
81
|
if (changed > 0)
|
|
62
82
|
state.save();
|
|
63
83
|
return changed;
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
import { type LocaleFile } from './files.js';
|
|
3
|
+
/** Which files and strings localewarden translates, and their length limits. */
|
|
4
|
+
export declare class Scope {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly excluded;
|
|
7
|
+
private readonly ignored;
|
|
8
|
+
private readonly limits;
|
|
9
|
+
constructor(config: Config);
|
|
10
|
+
/** Excluded by id ("locales/{lang}/nav.json") or by the source path ("locales/en/nav.json"). */
|
|
11
|
+
isExcluded(file: LocaleFile): boolean;
|
|
12
|
+
/** Not text: an ignored key or a URL, email, file path or number. Copied, never translated. */
|
|
13
|
+
isLiteral(key: string, value: string): boolean;
|
|
14
|
+
maxLength(key: string): number | undefined;
|
|
15
|
+
}
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isLiteralValue, keyPattern, pathPattern } from './files.js';
|
|
2
|
+
/** Which files and strings localewarden translates, and their length limits. */
|
|
3
|
+
export class Scope {
|
|
4
|
+
config;
|
|
5
|
+
excluded;
|
|
6
|
+
ignored;
|
|
7
|
+
limits;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.excluded = config.exclude.map(pathPattern);
|
|
11
|
+
this.ignored = config.ignoreKeys.map(keyPattern);
|
|
12
|
+
this.limits = Object.entries(config.maxLength).map(([pattern, max]) => [keyPattern(pattern), max]);
|
|
13
|
+
}
|
|
14
|
+
/** Excluded by id ("locales/{lang}/nav.json") or by the source path ("locales/en/nav.json"). */
|
|
15
|
+
isExcluded(file) {
|
|
16
|
+
const paths = [file.id, file.pathFor(this.config.sourceLanguage)];
|
|
17
|
+
return this.excluded.some(re => paths.some(p => re.test(p)));
|
|
18
|
+
}
|
|
19
|
+
/** Not text: an ignored key or a URL, email, file path or number. Copied, never translated. */
|
|
20
|
+
isLiteral(key, value) {
|
|
21
|
+
return this.ignored.some(re => re.test(key)) || isLiteralValue(value);
|
|
22
|
+
}
|
|
23
|
+
maxLength(key) {
|
|
24
|
+
return this.limits.find(([re]) => re.test(key))?.[1];
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/state.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* repair-failures.json targeted repairs (--fix-flagged) that were rejected, so the same
|
|
10
10
|
* value is not sent to the model again.
|
|
11
11
|
*/
|
|
12
|
-
export type ReviewReason = 'manual-edit' | 'source-changed' | 'edited-after-approval';
|
|
12
|
+
export type ReviewReason = 'manual-edit' | 'source-changed' | 'edited-after-approval' | 'approved-by-hand';
|
|
13
13
|
export interface ReviewEntry {
|
|
14
14
|
status: 'pending' | 'approved';
|
|
15
15
|
reason: ReviewReason;
|