@x18n/builtins 0.0.0 → 0.0.3

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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { shouldExtract } from "./extractors.mjs";
2
- import { localeSpellingKey } from "@x18n/core";
2
+ import { createPlaceholderComparer, localeSpellingKey } from "@x18n/core";
3
3
  //#region src/checks/index.ts
4
4
  function list(items) {
5
5
  return items.join(", ");
@@ -159,25 +159,20 @@ const unusedKey = {
159
159
  };
160
160
  }
161
161
  };
162
- function placeholderNames(ctx, value) {
163
- return ctx.placeholders(value).map((p) => p.name).sort();
164
- }
165
162
  const placeholderMismatch = {
166
163
  id: "placeholder-mismatch",
167
164
  defaultSeverity: "error",
168
165
  description: "Target value placeholders differ from the source value.",
169
166
  *run(ctx) {
167
+ const compare = createPlaceholderComparer(ctx.placeholderSyntaxes, ctx.placeholders);
170
168
  for (const key of ctx.keys()) {
171
169
  const source = ctx.message(key, ctx.sourceLocale);
172
170
  if (typeof source?.value !== "string") continue;
173
- const expected = placeholderNames(ctx, source.value);
174
171
  for (const locale of ctx.locales) {
175
172
  if (locale === ctx.sourceLocale) continue;
176
173
  const target = ctx.message(key, locale);
177
174
  if (typeof target?.value !== "string" || target.value === "") continue;
178
- const actual = placeholderNames(ctx, target.value);
179
- const missing = expected.filter((name) => !actual.includes(name));
180
- const extra = actual.filter((name) => !expected.includes(name));
175
+ const { missing, extra } = compare(source.value, target.value);
181
176
  if (missing.length || extra.length) {
182
177
  const parts = [missing.length ? `missing ${list(missing)}` : "", extra.length ? `unexpected ${list(extra)}` : ""].filter(Boolean);
183
178
  yield {
@@ -279,16 +274,20 @@ const hardString = {
279
274
  };
280
275
  }
281
276
  };
277
+ /** A term as a whole word, so "Home" does not match "Homepage". */
278
+ function wordPattern(term) {
279
+ return new RegExp(`(?<![\\p{L}\\p{N}])${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\p{L}\\p{N}])`, "iu");
280
+ }
282
281
  function* glossaryDiagnostics(ctx, entry) {
283
- const term = entry.term.toLowerCase();
282
+ const term = wordPattern(entry.term);
284
283
  for (const key of ctx.keys()) {
285
284
  const source = ctx.message(key, ctx.sourceLocale)?.value;
286
- if (typeof source !== "string" || !source.toLowerCase().includes(term)) continue;
285
+ if (typeof source !== "string" || !term.test(source)) continue;
287
286
  for (const locale of ctx.locales) {
288
287
  const value = ctx.message(key, locale)?.value;
289
288
  const expected = entry.doNotTranslate ? entry.term : entry.translations?.[locale];
290
289
  if (locale === ctx.sourceLocale || typeof value !== "string" || value === "" || !expected) continue;
291
- if (!value.toLowerCase().includes(expected.toLowerCase())) yield {
290
+ if (!wordPattern(expected).test(value)) yield {
292
291
  key,
293
292
  locale,
294
293
  message: `"${key}" in ${locale} should use "${expected}" for "${entry.term}".`
@@ -48,6 +48,10 @@ const rules = [
48
48
  id: "single-word",
49
49
  test: (text, kind) => !textKinds.has(kind) && !kind.endsWith("attribute") && !/\s/.test(text.trim()) ? "should-exclude" : "none"
50
50
  },
51
+ {
52
+ id: "attribute-token",
53
+ test: (text, kind) => kind.endsWith("attribute") && /^[a-z][\w-]*$/.test(text.trim()) ? "must-exclude" : "none"
54
+ },
51
55
  {
52
56
  id: "identifier",
53
57
  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"
@@ -187,6 +191,20 @@ function rawKeyFor(project, key) {
187
191
  const ns = project.config.namespace.default;
188
192
  return project.namespaced && ns && key.startsWith(`${ns}:`) ? key.slice(ns.length + 1) : key;
189
193
  }
194
+ /**
195
+ * Escapes a key for the quotes around `{key}` in a template, since `keygen: 'source'` keys hold any text:
196
+ * backslashes inside a JS string, entities inside a double-quoted attribute (`:title="$t('{key}')"`).
197
+ */
198
+ function quoteKey(before, key) {
199
+ const quote = before.at(-1);
200
+ const inAttribute = /="[^"]*$/.test(before);
201
+ let out = key;
202
+ if (quote === "'" || quote === "`" || quote === "\"" && !inAttribute) out = out.replace(quote === "`" ? /[\\`]|\$\{/g : new RegExp(`[\\\\${quote}]`, "g"), (c) => `\\${c}`);
203
+ return inAttribute ? out.replace(/&/g, "&amp;").replace(/"/g, "&quot;") : out;
204
+ }
205
+ function fillTemplate(template, values) {
206
+ return template.replace(/\{(key|attr|args)\}/g, (_, name, at) => name === "key" ? quoteKey(template.slice(0, at), values.key) : values[name]);
207
+ }
190
208
  /** Hard strings worth extracting, with proposed keys, values and replacements. */
191
209
  function findCandidates(project, options = {}) {
192
210
  const inFiles = options.files?.length ? pathMatcher(options.files) : void 0;
@@ -214,7 +232,11 @@ function findCandidates(project, options = {}) {
214
232
  key,
215
233
  reused,
216
234
  value,
217
- replacement: chosen.replace("{key}", rawKeyFor(project, key)).replace("{attr}", hard.attribute ?? "").replace("{args}", args),
235
+ replacement: fillTemplate(chosen, {
236
+ key: rawKeyFor(project, key),
237
+ attr: hard.attribute ?? "",
238
+ args
239
+ }),
218
240
  imports: template?.imports ?? [],
219
241
  alternatives: templates.slice(1)
220
242
  });
@@ -83,7 +83,7 @@ function compilePattern(pattern, extensions) {
83
83
  const extension = groups.ext ?? path.slice(path.lastIndexOf(".") + 1);
84
84
  return {
85
85
  locale: groups.locale,
86
- namespace: groups.namespace?.replace(/\//g, "."),
86
+ namespace: groups.namespace,
87
87
  ext: extension
88
88
  };
89
89
  }
@@ -135,7 +135,13 @@ async function guessResources(ctx) {
135
135
  }
136
136
  return specs;
137
137
  }
138
- async function findBySpecs(ctx, specs, guessed) {
138
+ /** A namespace from a path in the style of `namespace.fromPath`. Transloco camelCases scope folders, as its `toCamelCase`. */
139
+ function namespaceFromPath(path, style) {
140
+ if (style === "slash") return path;
141
+ if (style === "camel") return path.replace(/[-_/\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toLowerCase());
142
+ return path.replace(/\//g, ".");
143
+ }
144
+ async function findBySpecs(ctx, specs, strictLocales) {
139
145
  const out = [];
140
146
  for (const spec of specs) {
141
147
  const compiled = compilePattern(spec.pattern, ctx.extensions);
@@ -148,11 +154,12 @@ async function findBySpecs(ctx, specs, guessed) {
148
154
  const matched = compiled.match(path);
149
155
  if (!matched) continue;
150
156
  const locale = fixed ?? matched.locale ?? (spec.pattern.includes("{locale?}") ? ctx.config.sourceLocale : void 0);
151
- if (!locale || guessed && !isLocaleCode(locale)) continue;
157
+ if (!locale || strictLocales && !isLocaleCode(locale)) continue;
158
+ const namespace = spec.namespace ?? (matched.namespace === void 0 ? void 0 : namespaceFromPath(matched.namespace, ctx.config.namespace.fromPath));
152
159
  out.push({
153
160
  path,
154
161
  locale,
155
- namespace: spec.namespace ?? matched.namespace,
162
+ namespace,
156
163
  format: spec.format,
157
164
  readonly: spec.execute === true
158
165
  });
@@ -163,16 +170,18 @@ async function findBySpecs(ctx, specs, guessed) {
163
170
  const patternFinder = {
164
171
  id: "pattern",
165
172
  resolveNew(target, ctx) {
173
+ const style = ctx.config.namespace.fromPath;
166
174
  for (const spec of ctx.config.resources) {
167
- if (/\{namespaces?\}/.test(spec.pattern) !== (target.namespace !== void 0) || spec.execute || !spec.pattern.includes("{locale")) continue;
168
- const path = spec.pattern.replace(/^\.\//, "").replace(/\{locale\??\}/g, target.locale).replace("{namespaces}", (target.namespace ?? "").replace(/\./g, "/")).replace("{namespace}", target.namespace ?? "").replace("{ext}", spec.format === "yaml" ? "yml" : spec.format ?? "json");
175
+ const wantsNamespace = /\{namespaces?\}/.test(spec.pattern);
176
+ if (wantsNamespace !== (target.namespace !== void 0) || wantsNamespace && style === "camel" || spec.execute || !spec.pattern.includes("{locale")) continue;
177
+ const path = spec.pattern.replace(/^\.\//, "").replace(/\{locale\??\}/g, target.locale).replace("{namespaces}", style === "slash" ? target.namespace ?? "" : (target.namespace ?? "").replace(/\./g, "/")).replace("{namespace}", target.namespace ?? "").replace("{ext}", spec.format === "yaml" ? "yml" : spec.format ?? "json");
169
178
  if (!/[*?{}[\]]/.test(path)) return path;
170
179
  }
171
180
  },
172
181
  async find(ctx) {
173
182
  const configured = ctx.config.resources;
174
- const found = configured.length > 0 ? await findBySpecs(ctx, configured, false) : [];
175
183
  const fromUser = ctx.config.origins.resources === "user" || ctx.config.origins.resources === "parent";
184
+ const found = configured.length > 0 ? await findBySpecs(ctx, configured, !fromUser) : [];
176
185
  if (found.length > 0 || fromUser) return found;
177
186
  return findBySpecs(ctx, await guessResources(ctx), true);
178
187
  }
@@ -185,4 +194,4 @@ function pattern() {
185
194
  };
186
195
  }
187
196
  //#endregion
188
- export { patternFinder as i, guessResources as n, pattern as r, compilePattern as t };
197
+ export { patternFinder as a, pattern as i, guessResources as n, namespaceFromPath as r, compilePattern as t };
@@ -1,4 +1,4 @@
1
- import { FinderContext, LocaleFinder, Plugin, ResourceSpec } from "@x18n/core";
1
+ import { FinderContext, LocaleFinder, NamespaceFromPath, Plugin, ResourceSpec } from "@x18n/core";
2
2
  //#region src/finders/pattern.d.ts
3
3
  interface CompiledPattern {
4
4
  /** Glob for the host. */
@@ -16,6 +16,8 @@ interface CompiledPattern {
16
16
  export declare function compilePattern(pattern: string, extensions: readonly string[]): CompiledPattern;
17
17
  /** Guesses resource patterns from directories named like `locales`, as i18n-ally did without `localesPaths`. */
18
18
  export declare function guessResources(ctx: FinderContext): Promise<ResourceSpec[]>;
19
+ /** A namespace from a path in the style of `namespace.fromPath`. Transloco camelCases scope folders, as its `toCamelCase`. */
20
+ export declare function namespaceFromPath(path: string, style: NamespaceFromPath): string;
19
21
  export declare const patternFinder: LocaleFinder;
20
22
  /** The `pattern` locale finder: files that match `resources[].pattern`, or guessed locale directories. */
21
23
  export declare function pattern(): Plugin;
package/dist/finders.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { i as patternFinder, n as guessResources, r as pattern, t as compilePattern } from "./finders-Cf-IEaDm.mjs";
2
- import { n as sfcFluentBlocks, t as sfcBlocks } from "./sfc-blocks-CQhgEeT6.mjs";
3
- export { compilePattern, guessResources, pattern, patternFinder, sfcBlocks, sfcFluentBlocks };
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-BwJwCUka.mjs";
3
+ export { compilePattern, guessResources, namespaceFromPath, pattern, patternFinder, sfcBlocks, sfcFluentBlocks };
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { recommended } from "./checks.mjs";
2
- import { r as pattern } from "./finders-Cf-IEaDm.mjs";
2
+ import { i as pattern } from "./finders-CqTGh4OJ.mjs";
3
3
  import { a as json5, c as ini, i as php, l as ftl, n as properties, o as json, r as po, s as jsonc, t as yaml, u as ecmascript } from "./formats-BAogSrvw.mjs";
4
- import { i as js, n as vue } from "./vue-6FYzIrQl.mjs";
4
+ import { i as js, n as vue } from "./vue-5GFR5hft.mjs";
5
5
  import { manifests } from "./manifests.mjs";
6
- import { n as regex } from "./regex-350J_An-.mjs";
7
- import { t as presets } from "./presets-CxJ8CsTD.mjs";
6
+ import { n as regex } from "./regex-B9TBGgXC.mjs";
7
+ import { t as presets } from "./presets-DNnE_2MQ.mjs";
8
8
  import { reporters } from "./reporters.mjs";
9
- import { t as svelte } from "./scanners--prozYaE.mjs";
9
+ import { t as svelte } from "./scanners-B27IEH4_.mjs";
10
10
  //#region src/index.ts
11
11
  /** Plugins that are always on, and the presets that `'auto'` detects. */
12
12
  function createBuiltins() {
@@ -1,7 +1,7 @@
1
1
  import { defaultTemplates } from "./extractors.mjs";
2
- import { n as sfcFluentBlocks, t as sfcBlocks } from "./sfc-blocks-CQhgEeT6.mjs";
2
+ import { n as sfcFluentBlocks, t as sfcBlocks } from "./sfc-blocks-BwJwCUka.mjs";
3
3
  import { braces, colon, dollars, doubleBraces, fluent, icu, linked, nesting, percentBraces, placeholders, printf } from "./placeholders.mjs";
4
- import { r as regexScopes } from "./regex-350J_An-.mjs";
4
+ import { r as regexScopes } from "./regex-B9TBGgXC.mjs";
5
5
  import { isMessageLeaf } from "@x18n/core";
6
6
  //#region src/presets/shared.ts
7
7
  /** One plugin holding a library's call specs, regexes, namespace rules, defaults and extraction templates. */
@@ -10,8 +10,9 @@ function library(options) {
10
10
  name: `x18n:${options.name}`,
11
11
  async config(draft, ctx) {
12
12
  if (options.namespace) draft.namespace = {
13
- separator: options.namespace.separator,
14
- default: draft.namespace.default ?? options.namespace.default
13
+ separator: options.namespace.separator ?? draft.namespace.separator,
14
+ default: draft.namespace.default ?? options.namespace.default,
15
+ fromPath: options.namespace.fromPath ?? draft.namespace.fromPath
15
16
  };
16
17
  if (options.resources) draft.resources = [...draft.resources, ...options.resources.filter((spec) => !draft.resources.some((r) => r.pattern === spec.pattern))];
17
18
  if (options.keyStyle && draft.keyStyle === "auto" && !ctx.user.keyStyle) draft.keyStyle = options.keyStyle;
@@ -21,7 +22,7 @@ function library(options) {
21
22
  regex: [...draft.usage.regex, ...options.regex ?? []],
22
23
  derivedKeys: [...draft.usage.derivedKeys, ...options.derivedKeys ?? []]
23
24
  };
24
- await options.extra?.(draft);
25
+ await options.extra?.(draft, ctx);
25
26
  },
26
27
  localeFinders: options.finders,
27
28
  normalizeKey: options.normalizeKey,
@@ -96,6 +97,7 @@ function transloco() {
96
97
  plugins: [
97
98
  library({
98
99
  name: "transloco",
100
+ namespace: { fromPath: "camel" },
99
101
  calls: [{ callee: [
100
102
  "translate",
101
103
  "translocoService.translate",
@@ -213,7 +215,10 @@ function vueI18n() {
213
215
  }
214
216
  ]
215
217
  }),
216
- placeholders(braces, linked),
218
+ placeholders({
219
+ ...braces,
220
+ implicit: ["n", "count"]
221
+ }, linked),
217
222
  sfcBlocks()
218
223
  ]
219
224
  };
@@ -237,11 +242,30 @@ function nuxtI18n() {
237
242
  }]
238
243
  };
239
244
  }
245
+ /** `defaultNS` from the i18next init code (`i18n.ts`, `next-i18next.config.js`), as keys without a namespace use it. */
246
+ async function findDefaultNamespace(host) {
247
+ const files = await host.glob(["**/*i18n*.{js,mjs,cjs,ts,mts,cts}", "**/i18n/index.{js,ts}"], {
248
+ ignore: ["**/node_modules/**", "**/dist/**"],
249
+ gitignore: true
250
+ });
251
+ for (const file of files.slice(0, 20)) {
252
+ const found = /\bdefaultNS\s*:\s*['"`]([^'"`]+)['"`]/.exec(await host.readFile(file) ?? "");
253
+ if (found) return found[1];
254
+ }
255
+ }
240
256
  const i18nextPlugin = library({
241
257
  name: "i18next",
242
258
  namespace: {
243
259
  separator: ":",
244
- default: "translation"
260
+ default: "translation",
261
+ fromPath: "slash"
262
+ },
263
+ extra: async (draft, ctx) => {
264
+ const found = ctx.user.namespace?.default ? void 0 : await findDefaultNamespace(ctx.host);
265
+ if (found) draft.namespace = {
266
+ ...draft.namespace,
267
+ default: found
268
+ };
245
269
  },
246
270
  calls: [
247
271
  {
@@ -338,6 +362,10 @@ function reactI18next() {
338
362
  kinds: ["jsx-attribute"],
339
363
  imports: ["import i18next from 'i18next'"]
340
364
  }]
365
+ }),
366
+ regexScopes("\\bwithTranslation\\(\\s*\\[?\\s*['\"`]([^'\"`]+)['\"`]", "react-i18next-hoc", {
367
+ wholeFile: true,
368
+ extensions: scriptExtensions
341
369
  })
342
370
  ]
343
371
  };
package/dist/presets.mjs CHANGED
@@ -1 +1 @@
1
- export { n as chromeExtension, r as emberIntl, i as fluentVue, b as flutter, x as general, a as globalize, o as i18nJs, s as i18nTag, S as i18next, c as i18nextShopify, m as jekyll, h as laravel, l as lingui, u as nextInternational, C as nextIntl, d as nextTranslate, A as ngxTranslate, w as nuxtI18n, g as phpGettext, _ as phpJoomla, f as polyglot, t as presets, v as railsLazyKey, T as reactI18next, E as reactIntl, y as rubyRails, D as svelteI18n, j as transloco, p as ui5, O as vscodeExtension, k as vueI18n } from "./presets-CxJ8CsTD.mjs";
1
+ export { n as chromeExtension, r as emberIntl, i as fluentVue, b as flutter, x as general, a as globalize, o as i18nJs, s as i18nTag, S as i18next, c as i18nextShopify, m as jekyll, h as laravel, l as lingui, u as nextInternational, C as nextIntl, d as nextTranslate, A as ngxTranslate, w as nuxtI18n, g as phpGettext, _ as phpJoomla, f as polyglot, t as presets, v as railsLazyKey, T as reactI18next, E as reactIntl, y as rubyRails, D as svelteI18n, j as transloco, p as ui5, O as vscodeExtension, k as vueI18n } from "./presets-DNnE_2MQ.mjs";
@@ -47,7 +47,11 @@ function regex(specs = [], id = "regex") {
47
47
  },
48
48
  scan(file, ctx) {
49
49
  const all = [...specs, ...ctx.config.usage.regex];
50
- return { usages: scanWithRegex(file.text, regexesFor(file.extension, all)) };
50
+ const usages = scanWithRegex(file.text, regexesFor(file.extension, all));
51
+ return {
52
+ usages,
53
+ keySlots: usages.map((usage) => usage.range)
54
+ };
51
55
  }
52
56
  };
53
57
  return {
@@ -60,7 +64,7 @@ function regex(specs = [], id = "regex") {
60
64
  }
61
65
  /**
62
66
  * Regions after each match of `pattern` (group 1 = namespace, or key prefix with `as: 'keyPrefix'`),
63
- * up to the next match: i18n-ally `scopeRangeRegex`.
67
+ * up to the next match: i18n-ally `scopeRangeRegex`. With `wholeFile`, the first match scopes the whole file.
64
68
  */
65
69
  function regexScopes(pattern, id = "scopes", options = {}) {
66
70
  const re = new RegExp(pattern, "g");
@@ -71,10 +75,14 @@ function regexScopes(pattern, id = "scopes", options = {}) {
71
75
  extensions: options.extensions ?? ["*"],
72
76
  scan(file) {
73
77
  const matches = [...file.text.matchAll(re)];
78
+ if (options.wholeFile) matches.splice(1);
74
79
  return {
75
80
  usages: [],
76
81
  scopes: matches.map((match, i) => ({
77
- range: {
82
+ range: options.wholeFile ? {
83
+ start: 0,
84
+ end: file.text.length
85
+ } : {
78
86
  start: match.index + match[0].length,
79
87
  end: matches[i + 1]?.index ?? file.text.length
80
88
  },
@@ -1,4 +1,4 @@
1
- import { a as loadParser, o as scanSource } from "./vue-6FYzIrQl.mjs";
1
+ import { a as loadParser, o as scanSource } from "./vue-5GFR5hft.mjs";
2
2
  //#region src/scanners/svelte.ts
3
3
  let compiler;
4
4
  function isSvelteNode(value) {
@@ -30,11 +30,12 @@ export declare function scanWithRegex(text: string, regexes: readonly RegExp[]):
30
30
  export declare function regex(specs?: readonly RegexScannerSpec[], id?: string): Plugin;
31
31
  /**
32
32
  * Regions after each match of `pattern` (group 1 = namespace, or key prefix with `as: 'keyPrefix'`),
33
- * up to the next match: i18n-ally `scopeRangeRegex`.
33
+ * up to the next match: i18n-ally `scopeRangeRegex`. With `wholeFile`, the first match scopes the whole file.
34
34
  */
35
35
  export declare function regexScopes(pattern: string, id?: string, options?: {
36
36
  readonly as?: "namespace" | "keyPrefix";
37
37
  readonly extensions?: readonly string[];
38
+ readonly wholeFile?: boolean;
38
39
  }): Plugin;
39
40
  //#endregion
40
41
  //#region src/scanners/svelte.d.ts
package/dist/scanners.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as loadParser, i as js, n as vue, o as scanSource, r as calleeName } from "./vue-6FYzIrQl.mjs";
2
- import { i as scanWithRegex, n as regex, r as regexScopes, t as compileUsagePattern } from "./regex-350J_An-.mjs";
3
- import { t as svelte } from "./scanners--prozYaE.mjs";
1
+ import { a as loadParser, i as js, n as vue, o as scanSource, r as calleeName } from "./vue-5GFR5hft.mjs";
2
+ import { i as scanWithRegex, n as regex, r as regexScopes, t as compileUsagePattern } from "./regex-B9TBGgXC.mjs";
3
+ import { t as svelte } from "./scanners-B27IEH4_.mjs";
4
4
  export { calleeName, compileUsagePattern, js, loadParser, regex, regexScopes, scanSource, scanWithRegex, svelte, vue };
@@ -1,5 +1,5 @@
1
1
  import { a as json5, o as json, t as yaml } from "./formats-BAogSrvw.mjs";
2
- import { t as loadVueCompiler } from "./vue-6FYzIrQl.mjs";
2
+ import { t as loadVueCompiler } from "./vue-5GFR5hft.mjs";
3
3
  import { isMessageLeaf, pathId } from "@x18n/core";
4
4
  //#region src/finders/sfc-blocks.ts
5
5
  const formatOfLang = {
@@ -549,12 +549,10 @@ const TEXT = 2;
549
549
  const SIMPLE_EXPRESSION = 4;
550
550
  const INTERPOLATION = 5;
551
551
  const ATTRIBUTE = 6;
552
- const keyPathComponents = /* @__PURE__ */ new Map([
553
- ["i18n-t", "keypath"],
554
- ["I18nT", "keypath"],
555
- ["i18n", "path"],
556
- ["i18n-d", "path"]
557
- ]);
552
+ /** `keypath` is the vue-i18n prop for a key, on `<i18n-t>` and on the app's own wrappers of it; `path` on the legacy components. */
553
+ function isKeyAttribute(tag, name) {
554
+ return name === "keypath" || name === "path" && (tag === "i18n" || tag === "i18n-d");
555
+ }
558
556
  const ignoredAttributes = /* @__PURE__ */ new Set([
559
557
  "class",
560
558
  "style",
@@ -603,6 +601,18 @@ var TemplateScan = class {
603
601
  }));
604
602
  } catch {}
605
603
  }
604
+ /** `:keypath="cond ? 'a' : 'b'"`: the expression is read as the key argument of a translate call. */
605
+ keyExpression(content, offset) {
606
+ const callee = "__x18nKey(";
607
+ try {
608
+ merge(this.out, scanSource(this.parse, `${callee}${content})`, {
609
+ calls: [{ callee: "__x18nKey" }],
610
+ offset: offset - 10,
611
+ filename: "template.ts",
612
+ hardStrings: false
613
+ }));
614
+ } catch {}
615
+ }
606
616
  /** `v-t="'key'"` and `v-t="{ path: 'key' }"`. */
607
617
  vt(dir) {
608
618
  const exp = dir.exp;
@@ -631,7 +641,7 @@ var TemplateScan = class {
631
641
  start: value.loc.start.offset + 1,
632
642
  end: value.loc.end.offset - 1
633
643
  };
634
- if (keyPathComponents.get(element.tag) === prop.name) {
644
+ if (isKeyAttribute(element.tag, prop.name)) {
635
645
  this.out.usages.push({
636
646
  raw: value.content,
637
647
  range,
@@ -656,21 +666,8 @@ var TemplateScan = class {
656
666
  }
657
667
  const exp = prop.exp;
658
668
  if (exp && "content" in exp) {
659
- if (prop.name === "bind" && prop.arg && "content" in prop.arg && keyPathComponents.get(element.tag) === prop.arg.content && /^\s*(['"`]).*\1\s*$/.test(exp.content)) {
660
- const start = exp.loc.start.offset + exp.content.search(/['"`]/) + 1;
661
- const raw = exp.content.trim().slice(1, -1);
662
- this.out.usages.push({
663
- raw,
664
- range: {
665
- start,
666
- end: start + raw.length
667
- },
668
- kind: "call",
669
- dynamic: false
670
- });
671
- return;
672
- }
673
- this.expression(exp.content, exp.loc.start.offset);
669
+ if (prop.name === "bind" && prop.arg && "content" in prop.arg && isKeyAttribute(element.tag, prop.arg.content)) this.keyExpression(exp.content, exp.loc.start.offset);
670
+ else this.expression(exp.content, exp.loc.start.offset);
674
671
  }
675
672
  }
676
673
  node(node) {
@@ -1,5 +1,5 @@
1
1
  import { placeholderFromShape } from "./placeholders.mjs";
2
- import { r as regexScopes } from "./regex-350J_An-.mjs";
2
+ import { r as regexScopes } from "./regex-B9TBGgXC.mjs";
3
3
  //#region src/yaml-plugin/index.ts
4
4
  /**
5
5
  * Compiles a validated YAML plugin into plugin objects. Each section maps to the
@@ -21,7 +21,8 @@ function compileYamlPlugin(data) {
21
21
  if (defaults?.keyStyle) draft.keyStyle = defaults.keyStyle;
22
22
  if (defaults?.namespace) draft.namespace = {
23
23
  separator: defaults.namespace.separator ?? draft.namespace.separator,
24
- default: defaults.namespace.default ?? draft.namespace.default
24
+ default: defaults.namespace.default ?? draft.namespace.default,
25
+ fromPath: defaults.namespace.fromPath ?? draft.namespace.fromPath
25
26
  };
26
27
  draft.usage = {
27
28
  ...draft.usage,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/builtins",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.0.3",
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",
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@vue/compiler-sfc": "^3.5.44",
36
- "@x18n/core": "0.0.0",
36
+ "@x18n/core": "0.0.3",
37
37
  "json5": "^2.2.3",
38
38
  "jsonc-parser": "^3.3.1",
39
39
  "oxc-parser": "^0.154.0",