@x18n/builtins 0.0.3 → 0.0.6
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/dist/checks.d.mts +1 -0
- package/dist/checks.mjs +28 -5
- package/dist/extractors.d.mts +7 -1
- package/dist/extractors.mjs +57 -10
- package/dist/reporters.mjs +2 -1
- package/package.json +6 -6
package/dist/checks.d.mts
CHANGED
|
@@ -2,6 +2,7 @@ import { Check, Plugin } from "@x18n/core";
|
|
|
2
2
|
//#region src/checks/index.d.ts
|
|
3
3
|
export declare const missingKey: Check;
|
|
4
4
|
export declare const missingTranslation: Check;
|
|
5
|
+
export declare const staleTranslation: Check;
|
|
5
6
|
export declare const emptyTranslation: Check;
|
|
6
7
|
export declare const keyConflict: Check;
|
|
7
8
|
export declare const localeCodeConflict: Check;
|
package/dist/checks.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractableHardStrings } from "./extractors.mjs";
|
|
2
2
|
import { createPlaceholderComparer, localeSpellingKey } from "@x18n/core";
|
|
3
3
|
//#region src/checks/index.ts
|
|
4
4
|
function list(items) {
|
|
@@ -70,6 +70,23 @@ const missingTranslation = {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
+
function changed(change) {
|
|
74
|
+
if (!Number.isFinite(change.updatedAt)) return "changed locally";
|
|
75
|
+
const revision = change.revision ? ` in ${change.revision.slice(0, 7)}` : "";
|
|
76
|
+
return `changed ${new Date(change.updatedAt).toISOString().slice(0, 10)}${revision}`;
|
|
77
|
+
}
|
|
78
|
+
const staleTranslation = {
|
|
79
|
+
id: "stale-translation",
|
|
80
|
+
defaultSeverity: "warning",
|
|
81
|
+
description: "A translation that last changed before its source message, by the history providers (git).",
|
|
82
|
+
*run(ctx) {
|
|
83
|
+
for (const coverage of ctx.coverage()) for (const key of coverage.outdated) yield {
|
|
84
|
+
key,
|
|
85
|
+
locale: coverage.locale,
|
|
86
|
+
message: `"${key}" in ${coverage.locale} is older than ${ctx.sourceLocale} (source ${changed(ctx.history(key, ctx.sourceLocale))}).`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
73
90
|
const emptyTranslation = {
|
|
74
91
|
id: "empty-translation",
|
|
75
92
|
defaultSeverity: "warning",
|
|
@@ -265,7 +282,7 @@ const hardString = {
|
|
|
265
282
|
description: "A hard-coded string that should probably be a message. On with `extract.autoDetect`.",
|
|
266
283
|
*run(ctx) {
|
|
267
284
|
if (!ctx.config.extract.autoDetect && ctx.config.checks["hard-string"] === void 0) return;
|
|
268
|
-
for (const hard of ctx.hardStrings()
|
|
285
|
+
for (const hard of extractableHardStrings(ctx.config.extract, ctx.hardStrings(), ctx.text)) yield {
|
|
269
286
|
location: {
|
|
270
287
|
path: hard.path,
|
|
271
288
|
range: hard.range
|
|
@@ -274,9 +291,14 @@ const hardString = {
|
|
|
274
291
|
};
|
|
275
292
|
}
|
|
276
293
|
};
|
|
277
|
-
|
|
294
|
+
const unspaced = "\\p{scx=Han}\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Hangul}\\p{scx=Thai}\\p{scx=Lao}\\p{scx=Khmer}\\p{scx=Myanmar}";
|
|
295
|
+
const startsUnspaced = new RegExp(`^[${unspaced}]`, "u");
|
|
296
|
+
const endsUnspaced = new RegExp(`[${unspaced}]$`, "u");
|
|
297
|
+
/** A term as a whole word, so "Home" does not match "Homepage"; a plain substring at edges in unspaced scripts. */
|
|
278
298
|
function wordPattern(term) {
|
|
279
|
-
|
|
299
|
+
const before = startsUnspaced.test(term) ? "" : "(?<![\\p{L}\\p{N}])";
|
|
300
|
+
const after = endsUnspaced.test(term) ? "" : "(?![\\p{L}\\p{N}])";
|
|
301
|
+
return new RegExp(`${before}${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}${after}`, "iu");
|
|
280
302
|
}
|
|
281
303
|
function* glossaryDiagnostics(ctx, entry) {
|
|
282
304
|
const term = wordPattern(entry.term);
|
|
@@ -309,6 +331,7 @@ const allChecks = [
|
|
|
309
331
|
placeholderMismatch,
|
|
310
332
|
keyConflict,
|
|
311
333
|
missingTranslation,
|
|
334
|
+
staleTranslation,
|
|
312
335
|
emptyTranslation,
|
|
313
336
|
unusedKey,
|
|
314
337
|
localeCodeConflict,
|
|
@@ -326,4 +349,4 @@ function recommended() {
|
|
|
326
349
|
};
|
|
327
350
|
}
|
|
328
351
|
//#endregion
|
|
329
|
-
export { allChecks, dynamicKey, emptyTranslation, glossary, hardString, identicalToSource, invalidSyntax, keyConflict, keyStyle, localeCodeConflict, missingKey, missingTranslation, placeholderMismatch, recommended, unusedKey };
|
|
352
|
+
export { allChecks, dynamicKey, emptyTranslation, glossary, hardString, identicalToSource, invalidSyntax, keyConflict, keyStyle, localeCodeConflict, missingKey, missingTranslation, placeholderMismatch, recommended, staleTranslation, unusedKey };
|
package/dist/extractors.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExtractItem, ExtractTemplate, KeyString, Project, TextRange } from "@x18n/core";
|
|
1
|
+
import { ExtractItem, ExtractOptions, ExtractTemplate, HardString, KeyString, Project, TextRange } from "@x18n/core";
|
|
2
2
|
//#region src/extractors/index.d.ts
|
|
3
3
|
export type Score = "must-include" | "should-include" | "none" | "should-exclude" | "must-exclude";
|
|
4
4
|
export interface ExtractionRule {
|
|
@@ -38,6 +38,12 @@ export interface FindOptions {
|
|
|
38
38
|
/** Only these files (globs). */
|
|
39
39
|
readonly files?: readonly string[] | undefined;
|
|
40
40
|
}
|
|
41
|
+
/** Hard strings worth extracting: they pass the rules and no non-UI file, config ignore or control comment skips them. */
|
|
42
|
+
export declare function extractableHardStrings(options: ExtractOptions, hardStrings: Iterable<HardString & {
|
|
43
|
+
readonly path: string;
|
|
44
|
+
}>, textOf: (path: string) => string | undefined): Generator<HardString & {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
}>;
|
|
41
47
|
/** Hard strings worth extracting, with proposed keys, values and replacements. */
|
|
42
48
|
export declare function findCandidates(project: Project, options?: FindOptions): ExtractCandidate[];
|
|
43
49
|
export interface ExtractPlanFile {
|
package/dist/extractors.mjs
CHANGED
|
@@ -30,7 +30,18 @@ const rules = [
|
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
id: "markup",
|
|
33
|
-
test: (text) =>
|
|
33
|
+
test: (text) => /^<.*>$/s.test(text.trim()) && !/\p{L}/u.test(text.replace(/<[^>]*>|\{\d+\}/g, "")) ? "must-exclude" : "none"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "svg-path",
|
|
37
|
+
test: (text) => /^\s*M\s*-?[\d.]/i.test(text) && /^[MLHVCSQTAZ\d\s.,-]+$/i.test(text) ? "must-exclude" : "none"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "class-list",
|
|
41
|
+
test: (text) => {
|
|
42
|
+
const tokens = text.trim().split(/\s+/);
|
|
43
|
+
return tokens.every((t) => /^!?[a-z][\w:/[\].#%!-]*$/.test(t)) && tokens.filter((t) => /[-:!/[\d]/.test(t)).length * 2 > tokens.length ? "must-exclude" : "none";
|
|
44
|
+
}
|
|
34
45
|
},
|
|
35
46
|
{
|
|
36
47
|
id: "non-ascii",
|
|
@@ -57,6 +68,17 @@ const rules = [
|
|
|
57
68
|
test: (text) => /^[a-z]+(?:[A-Z][a-z\d]*)+$/.test(text) || /^[A-Z\d]+(?:_[A-Z\d]+)+$/.test(text) || /^[a-z\d]+(?:[-_.:][a-z\d]+)+$/.test(text) ? "should-exclude" : "none"
|
|
58
69
|
}
|
|
59
70
|
];
|
|
71
|
+
/** Files whose strings users never see, by the naming conventions of common languages and test runners. */
|
|
72
|
+
const nonUiFiles = [
|
|
73
|
+
/(?:^|\/)(?:tests?|specs?|e2e|cypress|fixtures?|mocks?|testdata|__(?:tests|mocks|snapshots|fixtures)__)\//i,
|
|
74
|
+
/[._-](?:tests?|specs?|e2e|cy|stories|story|config|conf)\.\w+$/i,
|
|
75
|
+
/(?:^|\/)(?:test_[^/]+|(?:tests?|specs?|conftest|config)\.\w+)$/i,
|
|
76
|
+
/[a-z\d](?:Tests?|Spec|IT)\.(?:java|kt|kts|scala|groovy|cs|fs|vb|php|swift|m|mm)$/,
|
|
77
|
+
/\.d\.[cm]?ts$/
|
|
78
|
+
];
|
|
79
|
+
function isExtractableFile(path) {
|
|
80
|
+
return !nonUiFiles.some((pattern) => pattern.test(path));
|
|
81
|
+
}
|
|
60
82
|
/** Folds rule scores the i18n-ally way (`shouldExtract`). */
|
|
61
83
|
function shouldExtract(text, kind, list = rules) {
|
|
62
84
|
let result = "none";
|
|
@@ -166,18 +188,43 @@ var KeyAllocator = class {
|
|
|
166
188
|
};
|
|
167
189
|
}
|
|
168
190
|
};
|
|
169
|
-
function
|
|
170
|
-
const options = project.config.extract;
|
|
191
|
+
function ignoredByConfig(options, hard) {
|
|
171
192
|
const matches = (rule) => {
|
|
172
193
|
const regex = /^\/(.+)\/([a-z]*)$/.exec(rule);
|
|
173
194
|
return regex ? new RegExp(regex[1], regex[2]).test(hard.text) : rule === hard.text;
|
|
174
195
|
};
|
|
175
196
|
if (options.ignore?.some(matches)) return true;
|
|
176
197
|
for (const [glob, list] of Object.entries(options.ignoreByFile ?? {})) if (pathMatcher([glob])(hard.path) && list.some(matches)) return true;
|
|
177
|
-
|
|
198
|
+
return hard.attribute !== void 0 && options.ignoreAttributes?.includes(hard.attribute) === true;
|
|
199
|
+
}
|
|
200
|
+
const ignoreLine = /x18n-ignore(?![-\w])/;
|
|
201
|
+
/**
|
|
202
|
+
* Offsets silenced by control comments: `x18n-ignore-file` anywhere, inside `x18n-ignore-start` ... `x18n-ignore-end`
|
|
203
|
+
* (to the end of the file when unclosed), or on the line of, or the line after, `x18n-ignore`.
|
|
204
|
+
*/
|
|
205
|
+
function commentIgnores(text) {
|
|
206
|
+
if (text.includes("x18n-ignore-file")) return () => true;
|
|
207
|
+
const regions = [...text.matchAll(/x18n-ignore-start[\s\S]*?(?:x18n-ignore-end|$)/g)].map((m) => [m.index, m.index + m[0].length]);
|
|
178
208
|
const index = createLineIndex(text);
|
|
179
|
-
|
|
180
|
-
|
|
209
|
+
return (offset) => {
|
|
210
|
+
if (regions.some(([start, end]) => offset >= start && offset < end)) return true;
|
|
211
|
+
const line = index.position(offset).line;
|
|
212
|
+
return ignoreLine.test(index.line(line)) || line > 0 && ignoreLine.test(index.line(line - 1));
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Hard strings worth extracting: they pass the rules and no non-UI file, config ignore or control comment skips them. */
|
|
216
|
+
function* extractableHardStrings(options, hardStrings, textOf) {
|
|
217
|
+
const ignores = /* @__PURE__ */ new Map();
|
|
218
|
+
for (const hard of hardStrings) {
|
|
219
|
+
if (!isExtractableFile(hard.path) || !shouldExtract(hard.text, hard.kind) || ignoredByConfig(options, hard)) continue;
|
|
220
|
+
let ignored = ignores.get(hard.path);
|
|
221
|
+
if (!ignored) {
|
|
222
|
+
const text = textOf(hard.path);
|
|
223
|
+
ignored = text === void 0 ? () => true : commentIgnores(text);
|
|
224
|
+
ignores.set(hard.path, ignored);
|
|
225
|
+
}
|
|
226
|
+
if (!ignored(hard.range.start)) yield hard;
|
|
227
|
+
}
|
|
181
228
|
}
|
|
182
229
|
/** Placeholder shape of the project's first placeholder syntax, `{name}` by default. */
|
|
183
230
|
function placeholderShape(project) {
|
|
@@ -211,10 +258,10 @@ function findCandidates(project, options = {}) {
|
|
|
211
258
|
const keys = new KeyAllocator(project);
|
|
212
259
|
const shape = placeholderShape(project);
|
|
213
260
|
const out = [];
|
|
214
|
-
|
|
215
|
-
|
|
261
|
+
const inScope = [...project.hardStrings()].filter((hard) => !inFiles || inFiles(hard.path));
|
|
262
|
+
for (const hard of extractableHardStrings(project.config.extract, inScope, (path) => project.text(path))) {
|
|
216
263
|
const text = project.text(hard.path);
|
|
217
|
-
if (text === void 0
|
|
264
|
+
if (text === void 0) continue;
|
|
218
265
|
const value = hard.kind === "js-template" ? hard.text.replace(/\{(\d+)\}/g, (_, i) => shape(hard.args?.[Number(i)]?.name ?? `arg${i}`)) : hard.text;
|
|
219
266
|
const { key, reused } = keys.allocate(value, hard.path, hard.text.replace(/\{\d+\}/g, " "));
|
|
220
267
|
const templates = templatesFor(project, hard.kind, hard.path);
|
|
@@ -266,4 +313,4 @@ function extractItems(project, items) {
|
|
|
266
313
|
});
|
|
267
314
|
}
|
|
268
315
|
//#endregion
|
|
269
|
-
export { defaultTemplates, extractItems, findCandidates, rules, shouldExtract, slugify, toPlanFile };
|
|
316
|
+
export { defaultTemplates, extractItems, extractableHardStrings, findCandidates, rules, shouldExtract, slugify, toPlanFile };
|
package/dist/reporters.mjs
CHANGED
|
@@ -82,7 +82,8 @@ const jsonReporter = {
|
|
|
82
82
|
total: c.total,
|
|
83
83
|
translated: c.translated,
|
|
84
84
|
missing: c.missing.length,
|
|
85
|
-
empty: c.empty.length
|
|
85
|
+
empty: c.empty.length,
|
|
86
|
+
outdated: c.outdated.length
|
|
86
87
|
}))
|
|
87
88
|
})) };
|
|
88
89
|
return `${JSON.stringify(envelope("check", data, diagnostics), null, 2)}\n`;
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x18n/builtins",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.6",
|
|
5
5
|
"description": "Built-in x18n plugins and presets: formats, locale finders, scanners, checks and reporters",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://github.com/
|
|
8
|
+
"homepage": "https://github.com/x18n-dev/x18n#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
11
|
+
"url": "git+https://github.com/x18n-dev/x18n.git",
|
|
12
12
|
"directory": "packages/builtins"
|
|
13
13
|
},
|
|
14
|
-
"bugs": "https://github.com/
|
|
14
|
+
"bugs": "https://github.com/x18n-dev/x18n/issues",
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./dist/index.mjs",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@vue/compiler-sfc": "^3.5.44",
|
|
36
|
-
"@x18n/core": "0.0.
|
|
36
|
+
"@x18n/core": "0.0.6",
|
|
37
37
|
"json5": "^2.2.3",
|
|
38
38
|
"jsonc-parser": "^3.3.1",
|
|
39
|
-
"oxc-parser": "^0.
|
|
39
|
+
"oxc-parser": "^0.155.0",
|
|
40
40
|
"svelte": "^5.57.2",
|
|
41
41
|
"yaml": "^2.9.1"
|
|
42
42
|
},
|