@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.
Files changed (170) hide show
  1. package/dist/angular-templates-DqIOGfzT.mjs +8 -0
  2. package/dist/checks.mjs +3 -2
  3. package/dist/extractors.d.mts +2 -2
  4. package/dist/extractors.mjs +18 -6
  5. package/dist/finders.d.mts +4 -1
  6. package/dist/finders.mjs +1 -1
  7. package/dist/formats/ecmascript.d.mts +5 -0
  8. package/dist/formats/ecmascript.mjs +211 -0
  9. package/dist/formats/ftl.d.mts +5 -0
  10. package/dist/formats/ftl.mjs +234 -0
  11. package/dist/formats/ini.d.mts +5 -0
  12. package/dist/formats/ini.mjs +158 -0
  13. package/dist/formats/json.d.mts +6 -0
  14. package/dist/formats/json.mjs +10 -0
  15. package/dist/formats/json5.d.mts +5 -0
  16. package/dist/formats/json5.mjs +30 -0
  17. package/dist/formats/jsonc.d.mts +6 -0
  18. package/dist/formats/jsonc.mjs +10 -0
  19. package/dist/formats/php.d.mts +5 -0
  20. package/dist/formats/php.mjs +200 -0
  21. package/dist/formats/po.d.mts +5 -0
  22. package/dist/formats/po.mjs +177 -0
  23. package/dist/formats/properties.d.mts +5 -0
  24. package/dist/formats/properties.mjs +123 -0
  25. package/dist/formats/resx.d.mts +6 -0
  26. package/dist/formats/resx.mjs +202 -0
  27. package/dist/formats/yaml.d.mts +5 -0
  28. package/dist/formats/yaml.mjs +113 -0
  29. package/dist/formats.d.mts +10 -34
  30. package/dist/formats.mjs +61 -1
  31. package/dist/index.d.mts +2 -2
  32. package/dist/index.mjs +28 -22
  33. package/dist/{vue-5GFR5hft.mjs → js-CV-ECNJG.mjs} +133 -273
  34. package/dist/json-CyjI8bjp.d.mts +8 -0
  35. package/dist/json-Db-8qSbE.mjs +73 -0
  36. package/dist/jsx-templates-B1J0no3i.mjs +17 -0
  37. package/dist/locale-root-C_rch3IQ.mjs +15 -0
  38. package/dist/manifests.d.mts +8 -0
  39. package/dist/manifests.mjs +66 -2
  40. package/dist/placeholders.d.mts +11 -0
  41. package/dist/placeholders.mjs +28 -1
  42. package/dist/plugin-Cb1b0axD.mjs +83 -0
  43. package/dist/plural-forms-B7_QX4nG.mjs +12 -0
  44. package/dist/presets/cargo-i18n.d.mts +6 -0
  45. package/dist/presets/cargo-i18n.mjs +28 -0
  46. package/dist/presets/chrome-extension.d.mts +6 -0
  47. package/dist/presets/chrome-extension.mjs +52 -0
  48. package/dist/presets/dotnet-resx.d.mts +6 -0
  49. package/dist/presets/dotnet-resx.mjs +64 -0
  50. package/dist/presets/easy-localization.d.mts +6 -0
  51. package/dist/presets/easy-localization.mjs +36 -0
  52. package/dist/presets/ember-intl.d.mts +5 -0
  53. package/dist/presets/ember-intl.mjs +31 -0
  54. package/dist/presets/fluent-react.d.mts +5 -0
  55. package/dist/presets/fluent-react.mjs +46 -0
  56. package/dist/presets/fluent-vue.d.mts +5 -0
  57. package/dist/presets/fluent-vue.mjs +51 -0
  58. package/dist/presets/flutter-translate.d.mts +6 -0
  59. package/dist/presets/flutter-translate.mjs +30 -0
  60. package/dist/presets/flutter.d.mts +6 -0
  61. package/dist/presets/flutter.mjs +21 -0
  62. package/dist/presets/general.d.mts +6 -0
  63. package/dist/presets/general.mjs +17 -0
  64. package/dist/presets/globalize.d.mts +5 -0
  65. package/dist/presets/globalize.mjs +25 -0
  66. package/dist/presets/gravity-ui-i18n.d.mts +6 -0
  67. package/dist/presets/gravity-ui-i18n.mjs +41 -0
  68. package/dist/presets/i18n-js.d.mts +5 -0
  69. package/dist/presets/i18n-js.mjs +19 -0
  70. package/dist/presets/i18n-tag.d.mts +5 -0
  71. package/dist/presets/i18n-tag.mjs +21 -0
  72. package/dist/presets/i18next-shopify.d.mts +6 -0
  73. package/dist/presets/i18next-shopify.mjs +23 -0
  74. package/dist/presets/i18next.d.mts +5 -0
  75. package/dist/presets/i18next.mjs +10 -0
  76. package/dist/presets/i18nice.d.mts +6 -0
  77. package/dist/presets/i18nice.mjs +29 -0
  78. package/dist/presets/jekyll.d.mts +5 -0
  79. package/dist/presets/jekyll.mjs +23 -0
  80. package/dist/presets/laravel-vue-i18n.d.mts +6 -0
  81. package/dist/presets/laravel-vue-i18n.mjs +54 -0
  82. package/dist/presets/laravel.d.mts +6 -0
  83. package/dist/presets/laravel.mjs +23 -0
  84. package/dist/presets/lingui.d.mts +5 -0
  85. package/dist/presets/lingui.mjs +61 -0
  86. package/dist/presets/next-international.d.mts +5 -0
  87. package/dist/presets/next-international.mjs +27 -0
  88. package/dist/presets/next-intl.d.mts +5 -0
  89. package/dist/presets/next-intl.mjs +55 -0
  90. package/dist/presets/next-translate.d.mts +5 -0
  91. package/dist/presets/next-translate.mjs +53 -0
  92. package/dist/presets/ngx-translate.d.mts +5 -0
  93. package/dist/presets/ngx-translate.mjs +41 -0
  94. package/dist/presets/nuxt-i18n-micro.d.mts +5 -0
  95. package/dist/presets/nuxt-i18n-micro.mjs +81 -0
  96. package/dist/presets/nuxt-i18n.d.mts +5 -0
  97. package/dist/presets/nuxt-i18n.mjs +23 -0
  98. package/dist/presets/paraglide.d.mts +5 -0
  99. package/dist/presets/paraglide.mjs +38 -0
  100. package/dist/presets/php-gettext.d.mts +6 -0
  101. package/dist/presets/php-gettext.mjs +27 -0
  102. package/dist/presets/php-joomla.d.mts +6 -0
  103. package/dist/presets/php-joomla.mjs +22 -0
  104. package/dist/presets/polyglot.d.mts +5 -0
  105. package/dist/presets/polyglot.mjs +23 -0
  106. package/dist/presets/python-gettext.d.mts +6 -0
  107. package/dist/presets/python-gettext.mjs +44 -0
  108. package/dist/presets/react-i18next.d.mts +5 -0
  109. package/dist/presets/react-i18next.mjs +67 -0
  110. package/dist/presets/react-intl.d.mts +5 -0
  111. package/dist/presets/react-intl.mjs +47 -0
  112. package/dist/presets/ruby-rails.d.mts +7 -0
  113. package/dist/presets/ruby-rails.mjs +44 -0
  114. package/dist/presets/solid-primitives-i18n.d.mts +5 -0
  115. package/dist/presets/solid-primitives-i18n.mjs +29 -0
  116. package/dist/presets/svelte-i18n.d.mts +5 -0
  117. package/dist/presets/svelte-i18n.mjs +33 -0
  118. package/dist/presets/svelte-i18next.d.mts +5 -0
  119. package/dist/presets/svelte-i18next.mjs +27 -0
  120. package/dist/presets/transloco.d.mts +5 -0
  121. package/dist/presets/transloco.mjs +44 -0
  122. package/dist/presets/typesafe-i18n.d.mts +6 -0
  123. package/dist/presets/typesafe-i18n.mjs +61 -0
  124. package/dist/presets/ui5.d.mts +5 -0
  125. package/dist/presets/ui5.mjs +35 -0
  126. package/dist/presets/vscode-extension.d.mts +5 -0
  127. package/dist/presets/vscode-extension.mjs +53 -0
  128. package/dist/presets/vue-i18n.d.mts +5 -0
  129. package/dist/presets/vue-i18n.mjs +59 -0
  130. package/dist/presets.d.mts +7 -54
  131. package/dist/presets.mjs +346 -1
  132. package/dist/reporters/github.d.mts +6 -0
  133. package/dist/reporters/github.mjs +27 -0
  134. package/dist/reporters/json.d.mts +6 -0
  135. package/dist/reporters/json.mjs +24 -0
  136. package/dist/reporters/markdown.d.mts +6 -0
  137. package/dist/reporters/markdown.mjs +37 -0
  138. package/dist/reporters/pretty.d.mts +6 -0
  139. package/dist/reporters/pretty.mjs +64 -0
  140. package/dist/reporters/sarif.d.mts +6 -0
  141. package/dist/reporters/sarif.mjs +48 -0
  142. package/dist/reporters.d.mts +5 -12
  143. package/dist/reporters.mjs +8 -218
  144. package/dist/scanners/astro.d.mts +6 -0
  145. package/dist/scanners/astro.mjs +256 -0
  146. package/dist/scanners/js.d.mts +6 -0
  147. package/dist/scanners/js.mjs +33 -0
  148. package/dist/scanners/regex-scopes.d.mts +14 -0
  149. package/dist/scanners/regex-scopes.mjs +41 -0
  150. package/dist/scanners/regex.d.mts +9 -0
  151. package/dist/{regex-B9TBGgXC.mjs → scanners/regex.mjs} +2 -33
  152. package/dist/scanners/svelte.d.mts +6 -0
  153. package/dist/{scanners-B27IEH4_.mjs → scanners/svelte.mjs} +8 -23
  154. package/dist/scanners/vue.d.mts +6 -0
  155. package/dist/scanners/vue.mjs +172 -0
  156. package/dist/scanners.d.mts +9 -47
  157. package/dist/scanners.mjs +36 -4
  158. package/dist/sfc-blocks-DaGKDZsb.mjs +213 -0
  159. package/dist/shared-BWaXUmwA.mjs +70 -0
  160. package/dist/shared-D-tbsYmc.mjs +30 -0
  161. package/dist/shared-D4NJBKEP.mjs +17 -0
  162. package/dist/shared-Tq06G0gp.mjs +6 -0
  163. package/dist/shared-meYeVckh.mjs +11 -0
  164. package/dist/vue-compiler-0sfLIiiL.mjs +8 -0
  165. package/package.json +67 -3
  166. package/dist/formats-CFkM5co0.mjs +0 -1370
  167. package/dist/presets-DJrGl3HR.mjs +0 -1184
  168. package/dist/sfc-blocks-BIPqKcvd.mjs +0 -161
  169. package/dist/yaml-plugin.d.mts +0 -8
  170. package/dist/yaml-plugin.mjs +0 -68
@@ -0,0 +1,8 @@
1
+ //#region src/presets/shared/angular-templates.ts
2
+ const angularTemplates = [
3
+ "html",
4
+ "ts",
5
+ "js"
6
+ ];
7
+ //#endregion
8
+ export { angularTemplates as t };
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 source = ctx.message(key, ctx.sourceLocale);
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,
@@ -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;
@@ -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) => name === "key" ? quoteKey(template.slice(0, at), values.key) : values[name]);
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 = {}) {
@@ -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
- /** Vue `<i18n>` blocks: one resource per block and locale (`locale="en"`, or every top-level locale key). */
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-BIPqKcvd.mjs";
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,5 @@
1
+ import { Plugin } from "@x18n/core";
2
+ //#region src/formats/ecmascript/index.d.ts
3
+ export declare function ecmascript(): Plugin;
4
+ //#endregion
5
+ export { ecmascript as default };
@@ -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,5 @@
1
+ import { Plugin } from "@x18n/core";
2
+ //#region src/formats/ftl/index.d.ts
3
+ export declare function ftl(): Plugin;
4
+ //#endregion
5
+ export { ftl as default };
@@ -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 };
@@ -0,0 +1,5 @@
1
+ import { Plugin } from "@x18n/core";
2
+ //#region src/formats/ini/index.d.ts
3
+ export declare function ini(): Plugin;
4
+ //#endregion
5
+ export { ini as default };