@x18n/builtins 0.0.12 → 0.0.14
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/angular-templates-DqIOGfzT.mjs +8 -0
- package/dist/checks.mjs +3 -2
- package/dist/extractors.d.mts +2 -2
- package/dist/extractors.mjs +18 -6
- package/dist/finders.d.mts +4 -1
- package/dist/finders.mjs +1 -1
- package/dist/formats/ecmascript.d.mts +5 -0
- package/dist/formats/ecmascript.mjs +211 -0
- package/dist/formats/ftl.d.mts +5 -0
- package/dist/formats/ftl.mjs +234 -0
- package/dist/formats/ini.d.mts +5 -0
- package/dist/formats/ini.mjs +158 -0
- package/dist/formats/json.d.mts +6 -0
- package/dist/formats/json.mjs +10 -0
- package/dist/formats/json5.d.mts +5 -0
- package/dist/formats/json5.mjs +30 -0
- package/dist/formats/jsonc.d.mts +6 -0
- package/dist/formats/jsonc.mjs +10 -0
- package/dist/formats/php.d.mts +5 -0
- package/dist/formats/php.mjs +200 -0
- package/dist/formats/po.d.mts +5 -0
- package/dist/formats/po.mjs +177 -0
- package/dist/formats/properties.d.mts +5 -0
- package/dist/formats/properties.mjs +123 -0
- package/dist/formats/resx.d.mts +6 -0
- package/dist/formats/resx.mjs +202 -0
- package/dist/formats/yaml.d.mts +5 -0
- package/dist/formats/yaml.mjs +113 -0
- package/dist/formats.d.mts +10 -34
- package/dist/formats.mjs +61 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +28 -22
- package/dist/{vue-5GFR5hft.mjs → js-CV-ECNJG.mjs} +133 -273
- package/dist/json-CyjI8bjp.d.mts +8 -0
- package/dist/json-Db-8qSbE.mjs +73 -0
- package/dist/jsx-templates-B1J0no3i.mjs +17 -0
- package/dist/locale-root-C_rch3IQ.mjs +15 -0
- package/dist/manifests.d.mts +8 -0
- package/dist/manifests.mjs +66 -2
- package/dist/placeholders.d.mts +11 -0
- package/dist/placeholders.mjs +28 -1
- package/dist/plugin-Cb1b0axD.mjs +83 -0
- package/dist/plural-forms-B7_QX4nG.mjs +12 -0
- package/dist/presets/cargo-i18n.d.mts +6 -0
- package/dist/presets/cargo-i18n.mjs +28 -0
- package/dist/presets/chrome-extension.d.mts +6 -0
- package/dist/presets/chrome-extension.mjs +52 -0
- package/dist/presets/dotnet-resx.d.mts +6 -0
- package/dist/presets/dotnet-resx.mjs +64 -0
- package/dist/presets/easy-localization.d.mts +6 -0
- package/dist/presets/easy-localization.mjs +36 -0
- package/dist/presets/ember-intl.d.mts +5 -0
- package/dist/presets/ember-intl.mjs +31 -0
- package/dist/presets/fluent-react.d.mts +5 -0
- package/dist/presets/fluent-react.mjs +46 -0
- package/dist/presets/fluent-vue.d.mts +5 -0
- package/dist/presets/fluent-vue.mjs +51 -0
- package/dist/presets/flutter-translate.d.mts +6 -0
- package/dist/presets/flutter-translate.mjs +30 -0
- package/dist/presets/flutter.d.mts +6 -0
- package/dist/presets/flutter.mjs +21 -0
- package/dist/presets/general.d.mts +6 -0
- package/dist/presets/general.mjs +17 -0
- package/dist/presets/globalize.d.mts +5 -0
- package/dist/presets/globalize.mjs +25 -0
- package/dist/presets/gravity-ui-i18n.d.mts +6 -0
- package/dist/presets/gravity-ui-i18n.mjs +41 -0
- package/dist/presets/i18n-js.d.mts +5 -0
- package/dist/presets/i18n-js.mjs +19 -0
- package/dist/presets/i18n-tag.d.mts +5 -0
- package/dist/presets/i18n-tag.mjs +21 -0
- package/dist/presets/i18next-shopify.d.mts +6 -0
- package/dist/presets/i18next-shopify.mjs +23 -0
- package/dist/presets/i18next.d.mts +5 -0
- package/dist/presets/i18next.mjs +10 -0
- package/dist/presets/i18nice.d.mts +6 -0
- package/dist/presets/i18nice.mjs +29 -0
- package/dist/presets/jekyll.d.mts +5 -0
- package/dist/presets/jekyll.mjs +23 -0
- package/dist/presets/laravel-vue-i18n.d.mts +6 -0
- package/dist/presets/laravel-vue-i18n.mjs +54 -0
- package/dist/presets/laravel.d.mts +6 -0
- package/dist/presets/laravel.mjs +23 -0
- package/dist/presets/lingui.d.mts +5 -0
- package/dist/presets/lingui.mjs +61 -0
- package/dist/presets/next-international.d.mts +5 -0
- package/dist/presets/next-international.mjs +27 -0
- package/dist/presets/next-intl.d.mts +5 -0
- package/dist/presets/next-intl.mjs +55 -0
- package/dist/presets/next-translate.d.mts +5 -0
- package/dist/presets/next-translate.mjs +53 -0
- package/dist/presets/ngx-translate.d.mts +5 -0
- package/dist/presets/ngx-translate.mjs +41 -0
- package/dist/presets/nuxt-i18n-micro.d.mts +5 -0
- package/dist/presets/nuxt-i18n-micro.mjs +81 -0
- package/dist/presets/nuxt-i18n.d.mts +5 -0
- package/dist/presets/nuxt-i18n.mjs +23 -0
- package/dist/presets/paraglide.d.mts +5 -0
- package/dist/presets/paraglide.mjs +38 -0
- package/dist/presets/php-gettext.d.mts +6 -0
- package/dist/presets/php-gettext.mjs +27 -0
- package/dist/presets/php-joomla.d.mts +6 -0
- package/dist/presets/php-joomla.mjs +22 -0
- package/dist/presets/polyglot.d.mts +5 -0
- package/dist/presets/polyglot.mjs +23 -0
- package/dist/presets/python-gettext.d.mts +6 -0
- package/dist/presets/python-gettext.mjs +44 -0
- package/dist/presets/react-i18next.d.mts +5 -0
- package/dist/presets/react-i18next.mjs +67 -0
- package/dist/presets/react-intl.d.mts +5 -0
- package/dist/presets/react-intl.mjs +47 -0
- package/dist/presets/ruby-rails.d.mts +7 -0
- package/dist/presets/ruby-rails.mjs +44 -0
- package/dist/presets/solid-primitives-i18n.d.mts +5 -0
- package/dist/presets/solid-primitives-i18n.mjs +29 -0
- package/dist/presets/svelte-i18n.d.mts +5 -0
- package/dist/presets/svelte-i18n.mjs +33 -0
- package/dist/presets/svelte-i18next.d.mts +5 -0
- package/dist/presets/svelte-i18next.mjs +27 -0
- package/dist/presets/transloco.d.mts +5 -0
- package/dist/presets/transloco.mjs +44 -0
- package/dist/presets/typesafe-i18n.d.mts +6 -0
- package/dist/presets/typesafe-i18n.mjs +61 -0
- package/dist/presets/ui5.d.mts +5 -0
- package/dist/presets/ui5.mjs +35 -0
- package/dist/presets/vscode-extension.d.mts +5 -0
- package/dist/presets/vscode-extension.mjs +53 -0
- package/dist/presets/vue-i18n.d.mts +5 -0
- package/dist/presets/vue-i18n.mjs +59 -0
- package/dist/presets.d.mts +7 -54
- package/dist/presets.mjs +346 -1
- package/dist/reporters/github.d.mts +6 -0
- package/dist/reporters/github.mjs +27 -0
- package/dist/reporters/json.d.mts +6 -0
- package/dist/reporters/json.mjs +24 -0
- package/dist/reporters/markdown.d.mts +6 -0
- package/dist/reporters/markdown.mjs +37 -0
- package/dist/reporters/pretty.d.mts +6 -0
- package/dist/reporters/pretty.mjs +64 -0
- package/dist/reporters/sarif.d.mts +6 -0
- package/dist/reporters/sarif.mjs +48 -0
- package/dist/reporters.d.mts +5 -12
- package/dist/reporters.mjs +8 -218
- package/dist/scanners/astro.d.mts +6 -0
- package/dist/scanners/astro.mjs +256 -0
- package/dist/scanners/js.d.mts +6 -0
- package/dist/scanners/js.mjs +33 -0
- package/dist/scanners/regex-scopes.d.mts +14 -0
- package/dist/scanners/regex-scopes.mjs +41 -0
- package/dist/scanners/regex.d.mts +9 -0
- package/dist/{regex-B9TBGgXC.mjs → scanners/regex.mjs} +2 -33
- package/dist/scanners/svelte.d.mts +6 -0
- package/dist/{scanners-B27IEH4_.mjs → scanners/svelte.mjs} +8 -23
- package/dist/scanners/vue.d.mts +6 -0
- package/dist/scanners/vue.mjs +172 -0
- package/dist/scanners.d.mts +9 -47
- package/dist/scanners.mjs +36 -4
- package/dist/sfc-blocks-DaGKDZsb.mjs +213 -0
- package/dist/shared-BWaXUmwA.mjs +70 -0
- package/dist/shared-D-tbsYmc.mjs +30 -0
- package/dist/shared-D4NJBKEP.mjs +17 -0
- package/dist/shared-Tq06G0gp.mjs +6 -0
- package/dist/shared-meYeVckh.mjs +11 -0
- package/dist/vue-compiler-0sfLIiiL.mjs +8 -0
- package/package.json +67 -3
- package/dist/formats-CFkM5co0.mjs +0 -1370
- package/dist/presets-DJrGl3HR.mjs +0 -1184
- package/dist/sfc-blocks-BIPqKcvd.mjs +0 -161
- package/dist/yaml-plugin.d.mts +0 -8
- package/dist/yaml-plugin.mjs +0 -68
package/dist/checks.mjs
CHANGED
|
@@ -32,7 +32,7 @@ const missingKey = {
|
|
|
32
32
|
const missingTranslation = {
|
|
33
33
|
id: "missing-translation",
|
|
34
34
|
defaultSeverity: "warning",
|
|
35
|
-
description: "A source-locale key that target locales lack. One diagnostic per key, at the source entry.",
|
|
35
|
+
description: "A source-locale key that target locales lack, or a plural form their language needs. One diagnostic per key, at the source entry.",
|
|
36
36
|
*run(ctx) {
|
|
37
37
|
const missing = /* @__PURE__ */ new Map();
|
|
38
38
|
for (const coverage of ctx.coverage()) {
|
|
@@ -44,7 +44,8 @@ const missingTranslation = {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
for (const [key, locales] of missing) {
|
|
47
|
-
const
|
|
47
|
+
const other = ctx.formOf(key)?.keyOf("other");
|
|
48
|
+
const source = ctx.message(key, ctx.sourceLocale) ?? (other === void 0 ? void 0 : ctx.message(other, ctx.sourceLocale));
|
|
48
49
|
yield {
|
|
49
50
|
key,
|
|
50
51
|
locale: ctx.sourceLocale,
|
package/dist/extractors.d.mts
CHANGED
|
@@ -3,12 +3,12 @@ import { ExtractItem, ExtractOptions, ExtractTemplate, HardString, KeyString, Pr
|
|
|
3
3
|
export type Score = "must-include" | "should-include" | "none" | "should-exclude" | "must-exclude";
|
|
4
4
|
export interface ExtractionRule {
|
|
5
5
|
readonly id: string;
|
|
6
|
-
readonly test: (text: string, kind: string) => Score;
|
|
6
|
+
readonly test: (text: string, kind: string, attribute: string | undefined) => Score;
|
|
7
7
|
}
|
|
8
8
|
/** Built-in rules, applied in order. A `must-*` wins at once; otherwise the last `should-*` decides. */
|
|
9
9
|
export declare const rules: readonly ExtractionRule[];
|
|
10
10
|
/** Folds rule scores the i18n-ally way (`shouldExtract`). */
|
|
11
|
-
export declare function shouldExtract(text: string, kind: string, list?: readonly ExtractionRule[]): boolean;
|
|
11
|
+
export declare function shouldExtract(text: string, kind: string, list?: readonly ExtractionRule[], attribute?: string): boolean;
|
|
12
12
|
export type KeyCase = "camel" | "snake" | "kebab" | "dot";
|
|
13
13
|
/** "Forgot password?" becomes `forgotPassword` (camel), `forgot_password`, `forgot-password` or `forgot.password`. */
|
|
14
14
|
export declare function slugify(text: string, keyCase?: KeyCase, maxLength?: number): string;
|
package/dist/extractors.mjs
CHANGED
|
@@ -4,6 +4,7 @@ const textKinds = /* @__PURE__ */ new Set([
|
|
|
4
4
|
"jsx-text",
|
|
5
5
|
"vue-template-text",
|
|
6
6
|
"svelte-text",
|
|
7
|
+
"astro-text",
|
|
7
8
|
"html-text"
|
|
8
9
|
]);
|
|
9
10
|
/** Built-in rules, applied in order. A `must-*` wins at once; otherwise the last `should-*` decides. */
|
|
@@ -43,6 +44,14 @@ const rules = [
|
|
|
43
44
|
return tokens.every((t) => /^!?[a-z][\w:/[\].#%!-]*$/.test(t)) && tokens.filter((t) => /[-:!/[\d]/.test(t)).length * 2 > tokens.length ? "must-exclude" : "none";
|
|
44
45
|
}
|
|
45
46
|
},
|
|
47
|
+
{
|
|
48
|
+
id: "attributify",
|
|
49
|
+
test: (text, kind, attribute) => kind.endsWith("attribute") && (attribute === "text" || attribute?.includes(":") === true) && text.trim().split(/\s+/).every((t) => /^!?-?[a-z\d$[(][\w:/[\]().,#%!$+~-]*$/.test(t)) ? "must-exclude" : "none"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "css-value",
|
|
53
|
+
test: (text) => /^-?[a-z][\w-]*\(.*\)$/s.test(text.trim()) || /\bvar\(--/.test(text) ? "must-exclude" : "none"
|
|
54
|
+
},
|
|
46
55
|
{
|
|
47
56
|
id: "non-ascii",
|
|
48
57
|
test: (text) => /\P{ASCII}/u.test(text) && /\p{L}/u.test(text) ? "must-include" : "none"
|
|
@@ -80,10 +89,10 @@ function isExtractableFile(path) {
|
|
|
80
89
|
return !nonUiFiles.some((pattern) => pattern.test(path));
|
|
81
90
|
}
|
|
82
91
|
/** Folds rule scores the i18n-ally way (`shouldExtract`). */
|
|
83
|
-
function shouldExtract(text, kind, list = rules) {
|
|
92
|
+
function shouldExtract(text, kind, list = rules, attribute) {
|
|
84
93
|
let result = "none";
|
|
85
94
|
for (const rule of list) {
|
|
86
|
-
const score = rule.test(text, kind);
|
|
95
|
+
const score = rule.test(text, kind, attribute);
|
|
87
96
|
if (score === "must-include") return true;
|
|
88
97
|
if (score === "must-exclude") return false;
|
|
89
98
|
if (score !== "none") result = score;
|
|
@@ -115,11 +124,11 @@ function slugify(text, keyCase = "camel", maxLength = 40) {
|
|
|
115
124
|
const defaultTemplates = [
|
|
116
125
|
{
|
|
117
126
|
template: "{t('{key}'{args})}",
|
|
118
|
-
kinds: ["jsx-text"]
|
|
127
|
+
kinds: ["jsx-text", "astro-text"]
|
|
119
128
|
},
|
|
120
129
|
{
|
|
121
130
|
template: "{attr}={t('{key}'{args})}",
|
|
122
|
-
kinds: ["jsx-attribute"]
|
|
131
|
+
kinds: ["jsx-attribute", "astro-attribute"]
|
|
123
132
|
},
|
|
124
133
|
{
|
|
125
134
|
template: "{{ $t('{key}'{args}) }}",
|
|
@@ -216,7 +225,7 @@ function commentIgnores(text) {
|
|
|
216
225
|
function* extractableHardStrings(options, hardStrings, textOf) {
|
|
217
226
|
const ignores = /* @__PURE__ */ new Map();
|
|
218
227
|
for (const hard of hardStrings) {
|
|
219
|
-
if (!isExtractableFile(hard.path) || !shouldExtract(hard.text, hard.kind) || ignoredByConfig(options, hard)) continue;
|
|
228
|
+
if (!isExtractableFile(hard.path) || !shouldExtract(hard.text, hard.kind, rules, hard.attribute) || ignoredByConfig(options, hard)) continue;
|
|
220
229
|
let ignored = ignores.get(hard.path);
|
|
221
230
|
if (!ignored) {
|
|
222
231
|
const text = textOf(hard.path);
|
|
@@ -250,7 +259,10 @@ function quoteKey(before, key) {
|
|
|
250
259
|
return inAttribute ? out.replace(/&/g, "&").replace(/"/g, """) : out;
|
|
251
260
|
}
|
|
252
261
|
function fillTemplate(template, values) {
|
|
253
|
-
return template.replace(/\{(key|attr|args)\}/g, (_, name, at) =>
|
|
262
|
+
return template.replace(/\{(key|attr|args)\}/g, (_, name, at) => {
|
|
263
|
+
if (name === "key") return quoteKey(template.slice(0, at), values.key);
|
|
264
|
+
return name === "args" && template[at - 1] === "(" ? values.args.replace(/^, /, "") : values[name];
|
|
265
|
+
});
|
|
254
266
|
}
|
|
255
267
|
/** Hard strings worth extracting, with proposed keys, values and replacements. */
|
|
256
268
|
function findCandidates(project, options = {}) {
|
package/dist/finders.d.mts
CHANGED
|
@@ -23,7 +23,10 @@ export declare const patternFinder: LocaleFinder;
|
|
|
23
23
|
export declare function pattern(): Plugin;
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/finders/sfc-blocks.d.ts
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Vue `<i18n>` blocks: one resource per block and locale (`locale="en"`, or every top-level locale key).
|
|
28
|
+
* A block with `src` is a resource in the referenced file, so edits go there.
|
|
29
|
+
*/
|
|
27
30
|
export declare function sfcBlocks(): Plugin;
|
|
28
31
|
/** fluent-vue `<fluent locale="en">` blocks in Vue SFCs, read with the `ftl` format. */
|
|
29
32
|
export declare function sfcFluentBlocks(): Plugin;
|
package/dist/finders.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { a as patternFinder, i as pattern, n as guessResources, r as namespaceFromPath, t as compilePattern } from "./finders-CqTGh4OJ.mjs";
|
|
2
|
-
import { n as sfcFluentBlocks, t as sfcBlocks } from "./sfc-blocks-
|
|
2
|
+
import { n as sfcFluentBlocks, t as sfcBlocks } from "./sfc-blocks-DaGKDZsb.mjs";
|
|
3
3
|
export { compilePattern, guessResources, namespaceFromPath, pattern, patternFinder, sfcBlocks, sfcFluentBlocks };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { a as splice } from "../shared-BWaXUmwA.mjs";
|
|
2
|
+
import { applyMessageOps, diffText, errors, isMessageLeaf, pathId } from "@x18n/core";
|
|
3
|
+
import { parseSync } from "oxc-parser";
|
|
4
|
+
//#region src/formats/ecmascript/index.ts
|
|
5
|
+
/** Type-only wrappers, `defineX({ ... })` helpers that return their argument, and loaders that only return an object (`defineI18nLocale(() => ({ ... }))`). */
|
|
6
|
+
function unwrap(node) {
|
|
7
|
+
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "ParenthesizedExpression") return unwrap(node.expression);
|
|
8
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") return unwrap(node.body);
|
|
9
|
+
if ((node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") && node.body?.type === "BlockStatement") {
|
|
10
|
+
const [only, ...rest] = node.body.body;
|
|
11
|
+
if (only?.type === "ReturnStatement" && only.argument && rest.length === 0) return unwrap(only.argument);
|
|
12
|
+
}
|
|
13
|
+
const argument = node.type === "CallExpression" && node.arguments.length === 1 ? node.arguments[0] : void 0;
|
|
14
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && /^define[A-Z]/.test(node.callee.name) && argument && argument.type !== "SpreadElement") return unwrap(argument);
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
function findRoot(program, fail) {
|
|
18
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const statement of program.body) {
|
|
20
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
21
|
+
for (const declarator of statement.declarations) if (declarator.id.type === "Identifier" && declarator.init) bindings.set(declarator.id.name, declarator.init);
|
|
22
|
+
}
|
|
23
|
+
let exported;
|
|
24
|
+
for (const statement of program.body) if (statement.type === "ExportDefaultDeclaration" && statement.declaration.type !== "FunctionDeclaration" && statement.declaration.type !== "ClassDeclaration" && statement.declaration.type !== "TSInterfaceDeclaration") exported = statement.declaration;
|
|
25
|
+
else if (statement.type === "ExpressionStatement" && statement.expression.type === "AssignmentExpression" && isModuleExports(statement.expression.left)) exported = statement.expression.right;
|
|
26
|
+
else if (statement.type === "ExportDefaultDeclaration") fail("X18N2004", statement, "the default export is not an object literal");
|
|
27
|
+
if (!exported) return program.body.length === 0 ? void 0 : fail("X18N2004", program, "no `export default { ... }` or `module.exports = { ... }`");
|
|
28
|
+
let root = unwrap(exported);
|
|
29
|
+
if (root.type === "Identifier" && bindings.has(root.name)) root = unwrap(bindings.get(root.name));
|
|
30
|
+
if (root.type === "ObjectExpression") return root;
|
|
31
|
+
return fail(root.type === "Literal" || root.type === "ArrayExpression" ? "X18N2003" : "X18N2004", root, "the default export is not an object literal");
|
|
32
|
+
}
|
|
33
|
+
function isModuleExports(node) {
|
|
34
|
+
return node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && node.object.name === "module" && node.property.type === "Identifier" && node.property.name === "exports";
|
|
35
|
+
}
|
|
36
|
+
function readProperty(property, fail) {
|
|
37
|
+
if (property.type === "SpreadElement" || property.kind !== "init" || property.method || property.shorthand) return fail("X18N2004", property, "only `key: value` properties are static");
|
|
38
|
+
const { key, value } = property;
|
|
39
|
+
if (key.type === "Identifier" && !property.computed) return {
|
|
40
|
+
key: key.name,
|
|
41
|
+
range: {
|
|
42
|
+
start: key.start,
|
|
43
|
+
end: key.end
|
|
44
|
+
},
|
|
45
|
+
value: unwrap(value)
|
|
46
|
+
};
|
|
47
|
+
if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) return {
|
|
48
|
+
key: String(key.value),
|
|
49
|
+
range: typeof key.value === "string" ? {
|
|
50
|
+
start: key.start + 1,
|
|
51
|
+
end: key.end - 1
|
|
52
|
+
} : {
|
|
53
|
+
start: key.start,
|
|
54
|
+
end: key.end
|
|
55
|
+
},
|
|
56
|
+
value: unwrap(value)
|
|
57
|
+
};
|
|
58
|
+
return fail("X18N2004", key, "computed keys are not static");
|
|
59
|
+
}
|
|
60
|
+
function readLeaf(node, text) {
|
|
61
|
+
if (node.type === "Literal" && !("regex" in node) && typeof node.value !== "bigint") return {
|
|
62
|
+
value: node.value,
|
|
63
|
+
quote: typeof node.value === "string" ? text[node.start] : void 0
|
|
64
|
+
};
|
|
65
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0 && node.quasis[0]?.value.cooked != null) return {
|
|
66
|
+
value: node.quasis[0].value.cooked,
|
|
67
|
+
quote: "`"
|
|
68
|
+
};
|
|
69
|
+
if (node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal" && typeof node.argument.value === "number") return {
|
|
70
|
+
value: -node.argument.value,
|
|
71
|
+
quote: void 0
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function readObject(node, path, module, text, fail) {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const property of node.properties) {
|
|
77
|
+
const { key, range, value } = readProperty(property, fail);
|
|
78
|
+
const childPath = [...path, key];
|
|
79
|
+
const id = pathId(childPath);
|
|
80
|
+
if (value.type === "ObjectExpression") {
|
|
81
|
+
module.ranges.set(id, {
|
|
82
|
+
key: range,
|
|
83
|
+
value: {
|
|
84
|
+
start: value.start,
|
|
85
|
+
end: value.end
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
out[key] = readObject(value, childPath, module, text, fail);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const leaf = readLeaf(value, text);
|
|
92
|
+
if (!leaf) return fail("X18N2004", value, `${value.type} is not a static value`);
|
|
93
|
+
const quoted = leaf.quote !== void 0;
|
|
94
|
+
module.ranges.set(id, {
|
|
95
|
+
key: range,
|
|
96
|
+
value: quoted ? {
|
|
97
|
+
start: value.start + 1,
|
|
98
|
+
end: value.end - 1
|
|
99
|
+
} : {
|
|
100
|
+
start: value.start,
|
|
101
|
+
end: value.end
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
module.scalars.set(id, {
|
|
105
|
+
range: {
|
|
106
|
+
start: value.start,
|
|
107
|
+
end: value.end
|
|
108
|
+
},
|
|
109
|
+
quote: leaf.quote
|
|
110
|
+
});
|
|
111
|
+
out[key] = leaf.value;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function readModule(text, path) {
|
|
116
|
+
const fail = (code, node, reason) => {
|
|
117
|
+
throw errors[code]({
|
|
118
|
+
path,
|
|
119
|
+
line: text.slice(0, node.start).split("\n").length,
|
|
120
|
+
reason
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const result = parseSync(path, text, {
|
|
124
|
+
lang: /\.[mc]?ts$/.test(path) ? "ts" : "js",
|
|
125
|
+
sourceType: "module"
|
|
126
|
+
});
|
|
127
|
+
if (result.errors.length > 0) throw errors.X18N2002({
|
|
128
|
+
path,
|
|
129
|
+
reason: result.errors[0].message
|
|
130
|
+
});
|
|
131
|
+
const root = findRoot(result.program, fail);
|
|
132
|
+
const found = {
|
|
133
|
+
ranges: /* @__PURE__ */ new Map(),
|
|
134
|
+
scalars: /* @__PURE__ */ new Map()
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
root,
|
|
138
|
+
tree: root ? readObject(root, [], found, text, fail) : {},
|
|
139
|
+
...found
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const singleEncodings = {
|
|
143
|
+
"\\": "\\\\",
|
|
144
|
+
"'": "\\'",
|
|
145
|
+
"\n": "\\n",
|
|
146
|
+
"\r": "\\r",
|
|
147
|
+
"\u2028": "\\u2028",
|
|
148
|
+
"\u2029": "\\u2029"
|
|
149
|
+
};
|
|
150
|
+
function literal(value, quote) {
|
|
151
|
+
if (typeof value !== "string") return String(value);
|
|
152
|
+
if (quote === "\"") return JSON.stringify(value);
|
|
153
|
+
if (quote === "`") return `\`${value.replace(/\\|`|\$\{/g, (match) => `\\${match}`)}\``;
|
|
154
|
+
return `'${value.replace(/[\\'\n\r\u2028\u2029]/g, (char) => singleEncodings[char])}'`;
|
|
155
|
+
}
|
|
156
|
+
function objectText(tree, unit, depth) {
|
|
157
|
+
const entries = Object.entries(tree);
|
|
158
|
+
if (entries.length === 0) return "{}";
|
|
159
|
+
const pad = unit.repeat(depth + 1);
|
|
160
|
+
return `{\n${entries.map(([key, child]) => {
|
|
161
|
+
const name = /^[a-z_$][\w$]*$/i.test(key) ? key : literal(key, "'");
|
|
162
|
+
return `${pad}${name}: ${isMessageLeaf(child) ? literal(child, "'") : objectText(child, unit, depth + 1)},\n`;
|
|
163
|
+
}).join("")}${unit.repeat(depth)}}`;
|
|
164
|
+
}
|
|
165
|
+
/** Indent unit: configured, else the file's (first indented line), else 2 spaces. */
|
|
166
|
+
function unitOf(text, indent) {
|
|
167
|
+
if (indent !== void 0) return indent === "tab" ? " " : " ".repeat(indent);
|
|
168
|
+
return /^([ \t]+)\S/m.exec(text)?.[1] ?? " ";
|
|
169
|
+
}
|
|
170
|
+
function moduleText(tree, path, unit) {
|
|
171
|
+
return `${path.endsWith(".cjs") ? "module.exports =" : "export default"} ${objectText(tree, unit, 0)}\n`;
|
|
172
|
+
}
|
|
173
|
+
function applyOp(text, op, path, unit) {
|
|
174
|
+
const module = readModule(text, path);
|
|
175
|
+
const scalar = module.scalars.get(pathId(op.path));
|
|
176
|
+
if (op.type === "set" && scalar) return splice(text, scalar.range, literal(op.value, scalar.quote));
|
|
177
|
+
const tree = applyMessageOps(module.tree, [op]);
|
|
178
|
+
return module.root ? splice(text, module.root, objectText(tree, unit, 0)) : moduleText(tree, path, unit);
|
|
179
|
+
}
|
|
180
|
+
function ecmascript() {
|
|
181
|
+
return {
|
|
182
|
+
name: "x18n:format-js",
|
|
183
|
+
formats: [{
|
|
184
|
+
id: "js",
|
|
185
|
+
extensions: [
|
|
186
|
+
"js",
|
|
187
|
+
"mjs",
|
|
188
|
+
"cjs",
|
|
189
|
+
"ts",
|
|
190
|
+
"mts",
|
|
191
|
+
"cts"
|
|
192
|
+
],
|
|
193
|
+
parse(text, ctx) {
|
|
194
|
+
const { tree, ranges } = readModule(text, ctx.path);
|
|
195
|
+
return {
|
|
196
|
+
tree,
|
|
197
|
+
ranges
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
edit(text, ops, ctx) {
|
|
201
|
+
const unit = unitOf(text, ctx.indent);
|
|
202
|
+
let next = text;
|
|
203
|
+
for (const op of ops) next = applyOp(next, op, ctx.path, unit);
|
|
204
|
+
return diffText(text, next);
|
|
205
|
+
},
|
|
206
|
+
serialize: (tree, ctx) => moduleText(tree, ctx.path, unitOf("", ctx.indent))
|
|
207
|
+
}]
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { ecmascript as default, ecmascript };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { a as splice, c as withBlankLines, i as leafText, n as detectEol, o as splitLines, t as appendLines } from "../shared-BWaXUmwA.mjs";
|
|
2
|
+
import { diffText, errors, pathId, walkLeaves } from "@x18n/core";
|
|
3
|
+
//#region src/formats/ftl/index.ts
|
|
4
|
+
const trimmedEnd = (text, line) => line.start + text.slice(line.start, line.end).trimEnd().length;
|
|
5
|
+
const indentOf = (text, line) => /^[ \t]*/.exec(text.slice(line.start, line.end))[0].length;
|
|
6
|
+
function finishPattern(text, draft) {
|
|
7
|
+
const lines = [...draft.lines];
|
|
8
|
+
while (lines.length > 0 && text.slice(lines.at(-1).start, lines.at(-1).end).trim() === "") lines.pop();
|
|
9
|
+
const indent = Math.min(...lines.filter((l) => text.slice(l.start, l.end).trim() !== "").map((l) => indentOf(text, l)));
|
|
10
|
+
const inline = text.slice(draft.inline.start, draft.inline.end).trimEnd();
|
|
11
|
+
const parts = [...inline ? [inline] : [], ...lines.map((l) => text.slice(Math.min(l.start + indent, l.end), trimmedEnd(text, l)))];
|
|
12
|
+
const first = lines[0];
|
|
13
|
+
if (!inline && !first) return {
|
|
14
|
+
value: void 0,
|
|
15
|
+
range: {
|
|
16
|
+
start: draft.after,
|
|
17
|
+
end: draft.after
|
|
18
|
+
},
|
|
19
|
+
source: {
|
|
20
|
+
start: draft.after,
|
|
21
|
+
end: draft.inline.end
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const start = inline ? draft.inline.start : first.start + indent;
|
|
25
|
+
const end = lines.length > 0 ? trimmedEnd(text, lines.at(-1)) : trimmedEnd(text, draft.inline);
|
|
26
|
+
return {
|
|
27
|
+
value: parts.join("\n"),
|
|
28
|
+
range: {
|
|
29
|
+
start,
|
|
30
|
+
end
|
|
31
|
+
},
|
|
32
|
+
source: {
|
|
33
|
+
start: draft.after,
|
|
34
|
+
end
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function read(text, path) {
|
|
39
|
+
const messages = [];
|
|
40
|
+
let current;
|
|
41
|
+
let commentStart;
|
|
42
|
+
let blanks = [];
|
|
43
|
+
let lineNumber = 0;
|
|
44
|
+
for (const line of splitLines(text)) {
|
|
45
|
+
lineNumber++;
|
|
46
|
+
const content = text.slice(line.start, line.end);
|
|
47
|
+
if (content.trim() === "") {
|
|
48
|
+
blanks.push(line);
|
|
49
|
+
commentStart = void 0;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (/^[ \t]/.test(content)) {
|
|
53
|
+
if (!current) throw errors.X18N2002({
|
|
54
|
+
path,
|
|
55
|
+
line: lineNumber,
|
|
56
|
+
reason: "indented line outside a message"
|
|
57
|
+
});
|
|
58
|
+
const attribute = /^[ \t]+\.([a-z][\w-]*)[ \t]*=[ \t]*/i.exec(content);
|
|
59
|
+
if (attribute) {
|
|
60
|
+
const nameStart = line.start + attribute[0].indexOf(".") + 1;
|
|
61
|
+
const after = line.start + attribute[0].trimEnd().length;
|
|
62
|
+
current.attributes.push({
|
|
63
|
+
name: attribute[1],
|
|
64
|
+
nameRange: {
|
|
65
|
+
start: nameStart,
|
|
66
|
+
end: nameStart + attribute[1].length
|
|
67
|
+
},
|
|
68
|
+
pattern: {
|
|
69
|
+
after,
|
|
70
|
+
inline: {
|
|
71
|
+
start: line.start + attribute[0].length,
|
|
72
|
+
end: line.end
|
|
73
|
+
},
|
|
74
|
+
lines: []
|
|
75
|
+
},
|
|
76
|
+
lines: {
|
|
77
|
+
start: line.start,
|
|
78
|
+
end: line.next
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
const last = current.attributes.at(-1);
|
|
83
|
+
(last ?? current).pattern.lines.push(...blanks, line);
|
|
84
|
+
if (last) last.lines.end = line.next;
|
|
85
|
+
}
|
|
86
|
+
current.block.end = line.next;
|
|
87
|
+
blanks = [];
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
blanks = [];
|
|
91
|
+
current = void 0;
|
|
92
|
+
if (content.startsWith("#")) {
|
|
93
|
+
commentStart = /^#(?: |$)/.test(content) ? commentStart ?? line.start : void 0;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const message = /^(-?[a-z][\w-]*)[ \t]*=[ \t]*/i.exec(content);
|
|
97
|
+
if (!message) throw errors.X18N2002({
|
|
98
|
+
path,
|
|
99
|
+
line: lineNumber,
|
|
100
|
+
reason: "expected a message, a term or a comment"
|
|
101
|
+
});
|
|
102
|
+
current = {
|
|
103
|
+
id: message[1],
|
|
104
|
+
idRange: {
|
|
105
|
+
start: line.start,
|
|
106
|
+
end: line.start + message[1].length
|
|
107
|
+
},
|
|
108
|
+
block: {
|
|
109
|
+
start: commentStart ?? line.start,
|
|
110
|
+
end: line.next
|
|
111
|
+
},
|
|
112
|
+
pattern: {
|
|
113
|
+
after: line.start + message[0].trimEnd().length,
|
|
114
|
+
inline: {
|
|
115
|
+
start: line.start + message[0].length,
|
|
116
|
+
end: line.end
|
|
117
|
+
},
|
|
118
|
+
lines: []
|
|
119
|
+
},
|
|
120
|
+
attributes: []
|
|
121
|
+
};
|
|
122
|
+
commentStart = void 0;
|
|
123
|
+
messages.push(current);
|
|
124
|
+
}
|
|
125
|
+
return messages;
|
|
126
|
+
}
|
|
127
|
+
function encode(value, indent, eol) {
|
|
128
|
+
if (value === "") return " { \"\" }";
|
|
129
|
+
const lines = value.split("\n");
|
|
130
|
+
if (lines.length === 1) return ` ${value}`;
|
|
131
|
+
return lines.map((line) => line === "" ? eol : `${eol}${" ".repeat(indent)}${line}`).join("");
|
|
132
|
+
}
|
|
133
|
+
function splitKey(key) {
|
|
134
|
+
const dot = key.indexOf(".");
|
|
135
|
+
return dot === -1 ? {
|
|
136
|
+
id: key,
|
|
137
|
+
attribute: void 0
|
|
138
|
+
} : {
|
|
139
|
+
id: key.slice(0, dot),
|
|
140
|
+
attribute: key.slice(dot + 1)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function setValue(text, key, value, messages) {
|
|
144
|
+
const eol = detectEol(text);
|
|
145
|
+
const { id, attribute } = splitKey(key);
|
|
146
|
+
const message = messages.findLast((m) => m.id === id);
|
|
147
|
+
if (!message) return appendLines(text, attribute ? `${id} =\n .${attribute} =${encode(value, 8, "\n")}\n` : `${id} =${encode(value, 4, "\n")}\n`);
|
|
148
|
+
if (!attribute) return splice(text, finishPattern(text, message.pattern).source, encode(value, 4, eol));
|
|
149
|
+
const existing = message.attributes.findLast((a) => a.name === attribute);
|
|
150
|
+
if (existing) return splice(text, finishPattern(text, existing.pattern).source, encode(value, indentOf(text, existing.lines) + 4, eol));
|
|
151
|
+
const end = message.block.end;
|
|
152
|
+
const lead = end === text.length && !text.endsWith("\n") ? eol : "";
|
|
153
|
+
return splice(text, {
|
|
154
|
+
start: end,
|
|
155
|
+
end
|
|
156
|
+
}, `${lead} .${attribute} =${encode(value, 8, eol)}${eol}`);
|
|
157
|
+
}
|
|
158
|
+
function remove(text, key, messages) {
|
|
159
|
+
const { id, attribute } = splitKey(key);
|
|
160
|
+
const message = messages.findLast((m) => m.id === id);
|
|
161
|
+
if (!message) return text;
|
|
162
|
+
const pattern = finishPattern(text, message.pattern);
|
|
163
|
+
if (!attribute) {
|
|
164
|
+
if (pattern.value === void 0) return text;
|
|
165
|
+
return message.attributes.length > 0 ? splice(text, pattern.source, "") : splice(text, withBlankLines(text, message.block), "");
|
|
166
|
+
}
|
|
167
|
+
const existing = message.attributes.findLast((a) => a.name === attribute);
|
|
168
|
+
if (!existing) return text;
|
|
169
|
+
if (pattern.value === void 0 && message.attributes.length === 1) return splice(text, withBlankLines(text, message.block), "");
|
|
170
|
+
return splice(text, existing.lines, "");
|
|
171
|
+
}
|
|
172
|
+
function applyOp(text, op, path) {
|
|
173
|
+
const key = op.path.join(".");
|
|
174
|
+
const messages = read(text, path);
|
|
175
|
+
return op.type === "set" ? setValue(text, key, leafText(op.value), messages) : remove(text, key, messages);
|
|
176
|
+
}
|
|
177
|
+
function serialize(tree) {
|
|
178
|
+
const messages = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const leaf of walkLeaves(tree)) {
|
|
180
|
+
const { id, attribute } = splitKey(leaf.path.join("."));
|
|
181
|
+
const message = messages.get(id) ?? {
|
|
182
|
+
value: void 0,
|
|
183
|
+
attributes: []
|
|
184
|
+
};
|
|
185
|
+
messages.set(id, message);
|
|
186
|
+
if (attribute) message.attributes.push(` .${attribute} =${encode(leafText(leaf.value), 8, "\n")}\n`);
|
|
187
|
+
else message.value = leafText(leaf.value);
|
|
188
|
+
}
|
|
189
|
+
return [...messages].map(([id, m]) => `${id} =${m.value === void 0 ? "" : encode(m.value, 4, "\n")}\n${m.attributes.join("")}`).join("");
|
|
190
|
+
}
|
|
191
|
+
function ftl() {
|
|
192
|
+
return {
|
|
193
|
+
name: "x18n:format-ftl",
|
|
194
|
+
formats: [{
|
|
195
|
+
id: "ftl",
|
|
196
|
+
extensions: ["ftl"],
|
|
197
|
+
parse(text, ctx) {
|
|
198
|
+
const tree = {};
|
|
199
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const message of read(text, ctx.path)) {
|
|
201
|
+
const pattern = finishPattern(text, message.pattern);
|
|
202
|
+
if (pattern.value !== void 0) {
|
|
203
|
+
tree[message.id] = pattern.value;
|
|
204
|
+
ranges.set(pathId([message.id]), {
|
|
205
|
+
key: message.idRange,
|
|
206
|
+
value: pattern.range
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
for (const attribute of message.attributes) {
|
|
210
|
+
const key = `${message.id}.${attribute.name}`;
|
|
211
|
+
const value = finishPattern(text, attribute.pattern);
|
|
212
|
+
tree[key] = value.value ?? "";
|
|
213
|
+
ranges.set(pathId([key]), {
|
|
214
|
+
key: attribute.nameRange,
|
|
215
|
+
value: value.range
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
tree,
|
|
221
|
+
ranges
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
edit(text, ops, ctx) {
|
|
225
|
+
let next = text;
|
|
226
|
+
for (const op of ops) next = applyOp(next, op, ctx.path);
|
|
227
|
+
return diffText(text, next);
|
|
228
|
+
},
|
|
229
|
+
serialize
|
|
230
|
+
}]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
export { ftl as default, ftl };
|