@x18n/builtins 0.0.2 → 0.0.5

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.
@@ -0,0 +1,316 @@
1
+ import { X18nError, createLineIndex, pathMatcher } from "@x18n/core";
2
+ //#region src/extractors/index.ts
3
+ const textKinds = /* @__PURE__ */ new Set([
4
+ "jsx-text",
5
+ "vue-template-text",
6
+ "svelte-text",
7
+ "html-text"
8
+ ]);
9
+ /** Built-in rules, applied in order. A `must-*` wins at once; otherwise the last `should-*` decides. */
10
+ const rules = [
11
+ {
12
+ id: "letters",
13
+ test: (text) => /\p{L}/u.test(text) ? "none" : "must-exclude"
14
+ },
15
+ {
16
+ id: "digits",
17
+ test: (text) => /^[\d\s.,:%+-]+$/.test(text) ? "must-exclude" : "none"
18
+ },
19
+ {
20
+ id: "url",
21
+ test: (text) => /^(?:[a-z][\w+.-]*:\/\/|mailto:|#|\/\/)/i.test(text.trim()) ? "must-exclude" : "none"
22
+ },
23
+ {
24
+ id: "path",
25
+ test: (text) => /^(?:\.{0,2}\/|~\/|@\/)[\w./@-]*$/.test(text.trim()) || /^[\w-]+\.[a-z]{2,4}$/i.test(text.trim()) ? "must-exclude" : "none"
26
+ },
27
+ {
28
+ id: "translate-call",
29
+ test: (text) => /\$?t\(['"`]/.test(text) || text.includes("{{") ? "must-exclude" : "none"
30
+ },
31
+ {
32
+ id: "markup",
33
+ test: (text) => /^<.*>$/s.test(text.trim()) && !/\p{L}/u.test(text.replace(/<[^>]*>|\{\d+\}/g, "")) ? "must-exclude" : "none"
34
+ },
35
+ {
36
+ id: "svg-path",
37
+ test: (text) => /^\s*M\s*-?[\d.]/i.test(text) && /^[MLHVCSQTAZ\d\s.,-]+$/i.test(text) ? "must-exclude" : "none"
38
+ },
39
+ {
40
+ id: "class-list",
41
+ test: (text) => {
42
+ const tokens = text.trim().split(/\s+/);
43
+ return tokens.every((t) => /^!?[a-z][\w:/[\].#%!-]*$/.test(t)) && tokens.filter((t) => /[-:!/[\d]/.test(t)).length * 2 > tokens.length ? "must-exclude" : "none";
44
+ }
45
+ },
46
+ {
47
+ id: "non-ascii",
48
+ test: (text) => /\P{ASCII}/u.test(text) && /\p{L}/u.test(text) ? "must-include" : "none"
49
+ },
50
+ {
51
+ id: "sentence",
52
+ test: (text) => /\p{L}\s+\p{L}/u.test(text) ? "should-include" : "none"
53
+ },
54
+ {
55
+ id: "text-node",
56
+ test: (text, kind) => textKinds.has(kind) || kind.endsWith("attribute") ? "should-include" : "none"
57
+ },
58
+ {
59
+ id: "single-word",
60
+ test: (text, kind) => !textKinds.has(kind) && !kind.endsWith("attribute") && !/\s/.test(text.trim()) ? "should-exclude" : "none"
61
+ },
62
+ {
63
+ id: "attribute-token",
64
+ test: (text, kind) => kind.endsWith("attribute") && /^[a-z][\w-]*$/.test(text.trim()) ? "must-exclude" : "none"
65
+ },
66
+ {
67
+ id: "identifier",
68
+ test: (text) => /^[a-z]+(?:[A-Z][a-z\d]*)+$/.test(text) || /^[A-Z\d]+(?:_[A-Z\d]+)+$/.test(text) || /^[a-z\d]+(?:[-_.:][a-z\d]+)+$/.test(text) ? "should-exclude" : "none"
69
+ }
70
+ ];
71
+ /** Files whose strings users never see, by the naming conventions of common languages and test runners. */
72
+ const nonUiFiles = [
73
+ /(?:^|\/)(?:tests?|specs?|e2e|cypress|fixtures?|mocks?|testdata|__(?:tests|mocks|snapshots|fixtures)__)\//i,
74
+ /[._-](?:tests?|specs?|e2e|cy|stories|story|config|conf)\.\w+$/i,
75
+ /(?:^|\/)(?:test_[^/]+|(?:tests?|specs?|conftest|config)\.\w+)$/i,
76
+ /[a-z\d](?:Tests?|Spec|IT)\.(?:java|kt|kts|scala|groovy|cs|fs|vb|php|swift|m|mm)$/,
77
+ /\.d\.[cm]?ts$/
78
+ ];
79
+ function isExtractableFile(path) {
80
+ return !nonUiFiles.some((pattern) => pattern.test(path));
81
+ }
82
+ /** Folds rule scores the i18n-ally way (`shouldExtract`). */
83
+ function shouldExtract(text, kind, list = rules) {
84
+ let result = "none";
85
+ for (const rule of list) {
86
+ const score = rule.test(text, kind);
87
+ if (score === "must-include") return true;
88
+ if (score === "must-exclude") return false;
89
+ if (score !== "none") result = score;
90
+ }
91
+ return result === "should-include";
92
+ }
93
+ /** Words of a text for slugs: letters and digits only. */
94
+ function words(text) {
95
+ return text.normalize("NFKD").replace(/[\u0300-\u036F]/g, "").split(/[^\p{L}\d]+/u).filter(Boolean);
96
+ }
97
+ /** "Forgot password?" becomes `forgotPassword` (camel), `forgot_password`, `forgot-password` or `forgot.password`. */
98
+ function slugify(text, keyCase = "camel", maxLength = 40) {
99
+ const parts = [];
100
+ let length = 0;
101
+ for (const word of words(text).map((w) => w.toLowerCase())) {
102
+ if (length + word.length > maxLength && parts.length > 0) break;
103
+ parts.push(word);
104
+ length += word.length + 1;
105
+ }
106
+ if (parts.length === 0) return "text";
107
+ switch (keyCase) {
108
+ case "snake": return parts.join("_");
109
+ case "kebab": return parts.join("-");
110
+ case "dot": return parts.join(".");
111
+ default: return parts.map((word, i) => i === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
112
+ }
113
+ }
114
+ /** Fallback templates per kind, used after config and preset templates. */
115
+ const defaultTemplates = [
116
+ {
117
+ template: "{t('{key}'{args})}",
118
+ kinds: ["jsx-text"]
119
+ },
120
+ {
121
+ template: "{attr}={t('{key}'{args})}",
122
+ kinds: ["jsx-attribute"]
123
+ },
124
+ {
125
+ template: "{{ $t('{key}'{args}) }}",
126
+ kinds: ["vue-template-text", "html-text"]
127
+ },
128
+ {
129
+ template: ":{attr}=\"$t('{key}'{args})\"",
130
+ kinds: ["vue-attribute"]
131
+ },
132
+ {
133
+ template: "{$t('{key}'{args})}",
134
+ kinds: ["svelte-text"]
135
+ },
136
+ {
137
+ template: "{attr}={$t('{key}'{args})}",
138
+ kinds: ["svelte-attribute"]
139
+ },
140
+ { template: "t('{key}'{args})" }
141
+ ];
142
+ function prefixFor(template, path) {
143
+ if (!template) return "";
144
+ const base = path.slice(path.lastIndexOf("/") + 1).replace(/\.[^.]+$/, "");
145
+ const dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/")).split("/").at(-1) : "";
146
+ return template.replace("{fileName}", slugify(base)).replace("{dirName}", slugify(dir)).replace("{component}", base.replace(/(?:^|[-_])(\w)/g, (_, c) => c.toUpperCase()));
147
+ }
148
+ function randomKey() {
149
+ return Math.random().toString(36).slice(2, 10);
150
+ }
151
+ var KeyAllocator = class {
152
+ project;
153
+ byValue = /* @__PURE__ */ new Map();
154
+ taken = /* @__PURE__ */ new Map();
155
+ constructor(project) {
156
+ this.project = project;
157
+ const options = project.config.extract;
158
+ for (const key of project.keys()) {
159
+ const value = project.message(key, project.sourceLocale)?.value;
160
+ if (typeof value === "string") {
161
+ this.taken.set(key, value);
162
+ if (options.reuseExisting !== false && !this.byValue.has(value)) this.byValue.set(value, key);
163
+ }
164
+ }
165
+ }
166
+ allocate(value, path, slugText) {
167
+ const options = this.project.config.extract;
168
+ const reused = this.byValue.get(value);
169
+ if (reused) return {
170
+ key: reused,
171
+ reused: this.taken.has(reused)
172
+ };
173
+ const strategy = options.keygen ?? "slug";
174
+ const prefix = prefixFor(options.keyPrefix, path);
175
+ let base;
176
+ if (strategy === "source") base = value;
177
+ else if (strategy === "random") base = randomKey();
178
+ else if (strategy === "empty") base = "";
179
+ else base = slugify(slugText, options.keyCase ?? "camel", options.keyMaxLength ?? 40);
180
+ const raw = prefix ? `${prefix}.${base}` : base;
181
+ let key = this.project.parseKey(raw);
182
+ for (let i = 2; this.taken.has(key) && this.taken.get(key) !== value; i++) key = this.project.parseKey(`${raw}${i}`);
183
+ this.taken.set(key, value);
184
+ this.byValue.set(value, key);
185
+ return {
186
+ key,
187
+ reused: false
188
+ };
189
+ }
190
+ };
191
+ function ignoredByConfig(options, hard) {
192
+ const matches = (rule) => {
193
+ const regex = /^\/(.+)\/([a-z]*)$/.exec(rule);
194
+ return regex ? new RegExp(regex[1], regex[2]).test(hard.text) : rule === hard.text;
195
+ };
196
+ if (options.ignore?.some(matches)) return true;
197
+ for (const [glob, list] of Object.entries(options.ignoreByFile ?? {})) if (pathMatcher([glob])(hard.path) && list.some(matches)) return true;
198
+ return hard.attribute !== void 0 && options.ignoreAttributes?.includes(hard.attribute) === true;
199
+ }
200
+ const ignoreLine = /x18n-ignore(?![-\w])/;
201
+ /**
202
+ * Offsets silenced by control comments: `x18n-ignore-file` anywhere, inside `x18n-ignore-start` ... `x18n-ignore-end`
203
+ * (to the end of the file when unclosed), or on the line of, or the line after, `x18n-ignore`.
204
+ */
205
+ function commentIgnores(text) {
206
+ if (text.includes("x18n-ignore-file")) return () => true;
207
+ const regions = [...text.matchAll(/x18n-ignore-start[\s\S]*?(?:x18n-ignore-end|$)/g)].map((m) => [m.index, m.index + m[0].length]);
208
+ const index = createLineIndex(text);
209
+ return (offset) => {
210
+ if (regions.some(([start, end]) => offset >= start && offset < end)) return true;
211
+ const line = index.position(offset).line;
212
+ return ignoreLine.test(index.line(line)) || line > 0 && ignoreLine.test(index.line(line - 1));
213
+ };
214
+ }
215
+ /** Hard strings worth extracting: they pass the rules and no non-UI file, config ignore or control comment skips them. */
216
+ function* extractableHardStrings(options, hardStrings, textOf) {
217
+ const ignores = /* @__PURE__ */ new Map();
218
+ for (const hard of hardStrings) {
219
+ if (!isExtractableFile(hard.path) || !shouldExtract(hard.text, hard.kind) || ignoredByConfig(options, hard)) continue;
220
+ let ignored = ignores.get(hard.path);
221
+ if (!ignored) {
222
+ const text = textOf(hard.path);
223
+ ignored = text === void 0 ? () => true : commentIgnores(text);
224
+ ignores.set(hard.path, ignored);
225
+ }
226
+ if (!ignored(hard.range.start)) yield hard;
227
+ }
228
+ }
229
+ /** Placeholder shape of the project's first placeholder syntax, `{name}` by default. */
230
+ function placeholderShape(project) {
231
+ const example = project.placeholderSyntaxes.find((s) => s.example?.includes("name"))?.example ?? "{name}";
232
+ return (name) => example.replace("name", name);
233
+ }
234
+ function templatesFor(project, kind, path) {
235
+ return [...project.extractTemplates, ...defaultTemplates].filter((t) => (!t.kinds || t.kinds.includes(kind)) && (!t.files || pathMatcher(t.files)(path))).map((t) => t.template);
236
+ }
237
+ function rawKeyFor(project, key) {
238
+ const ns = project.config.namespace.default;
239
+ return project.namespaced && ns && key.startsWith(`${ns}:`) ? key.slice(ns.length + 1) : key;
240
+ }
241
+ /**
242
+ * Escapes a key for the quotes around `{key}` in a template, since `keygen: 'source'` keys hold any text:
243
+ * backslashes inside a JS string, entities inside a double-quoted attribute (`:title="$t('{key}')"`).
244
+ */
245
+ function quoteKey(before, key) {
246
+ const quote = before.at(-1);
247
+ const inAttribute = /="[^"]*$/.test(before);
248
+ let out = key;
249
+ if (quote === "'" || quote === "`" || quote === "\"" && !inAttribute) out = out.replace(quote === "`" ? /[\\`]|\$\{/g : new RegExp(`[\\\\${quote}]`, "g"), (c) => `\\${c}`);
250
+ return inAttribute ? out.replace(/&/g, "&amp;").replace(/"/g, "&quot;") : out;
251
+ }
252
+ 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]);
254
+ }
255
+ /** Hard strings worth extracting, with proposed keys, values and replacements. */
256
+ function findCandidates(project, options = {}) {
257
+ const inFiles = options.files?.length ? pathMatcher(options.files) : void 0;
258
+ const keys = new KeyAllocator(project);
259
+ const shape = placeholderShape(project);
260
+ const out = [];
261
+ const inScope = [...project.hardStrings()].filter((hard) => !inFiles || inFiles(hard.path));
262
+ for (const hard of extractableHardStrings(project.config.extract, inScope, (path) => project.text(path))) {
263
+ const text = project.text(hard.path);
264
+ if (text === void 0) continue;
265
+ const value = hard.kind === "js-template" ? hard.text.replace(/\{(\d+)\}/g, (_, i) => shape(hard.args?.[Number(i)]?.name ?? `arg${i}`)) : hard.text;
266
+ const { key, reused } = keys.allocate(value, hard.path, hard.text.replace(/\{\d+\}/g, " "));
267
+ const templates = templatesFor(project, hard.kind, hard.path);
268
+ const chosen = templates[0];
269
+ const args = hard.args?.length ? `, { ${hard.args.map((a) => a.name === a.expression ? a.name : `${a.name}: ${a.expression}`).join(", ")} }` : "";
270
+ const template = project.extractTemplates.find((t) => t.template === chosen);
271
+ out.push({
272
+ path: hard.path,
273
+ line: createLineIndex(text).position(hard.range.start).line + 1,
274
+ kind: hard.kind,
275
+ text: hard.text,
276
+ original: text.slice(hard.range.start, hard.range.end),
277
+ range: hard.range,
278
+ attribute: hard.attribute,
279
+ key,
280
+ reused,
281
+ value,
282
+ replacement: fillTemplate(chosen, {
283
+ key: rawKeyFor(project, key),
284
+ attr: hard.attribute ?? "",
285
+ args
286
+ }),
287
+ imports: template?.imports ?? [],
288
+ alternatives: templates.slice(1)
289
+ });
290
+ }
291
+ return out;
292
+ }
293
+ function toPlanFile(project, candidates) {
294
+ return {
295
+ x18n: 1,
296
+ project: project.name,
297
+ items: candidates
298
+ };
299
+ }
300
+ /** Turns plan items (maybe edited by a human or agent) into edit items, after checking the files did not change. */
301
+ function extractItems(project, items) {
302
+ return items.map((item) => {
303
+ const text = project.text(item.path);
304
+ if (text === void 0 || text.slice(item.range.start, item.range.end) !== item.original) throw new X18nError("X18N4004", `${item.path} changed since the plan was made`);
305
+ return {
306
+ path: item.path,
307
+ range: item.range,
308
+ key: project.parseKey(item.key),
309
+ value: item.value,
310
+ replacement: item.replacement,
311
+ imports: item.imports
312
+ };
313
+ });
314
+ }
315
+ //#endregion
316
+ export { defaultTemplates, extractItems, extractableHardStrings, findCandidates, rules, shouldExtract, slugify, toPlanFile };
@@ -0,0 +1,197 @@
1
+ //#region src/finders/pattern.ts
2
+ const localeLike = /^[a-z]{2,3}(?:[-_][a-z\d]{2,8})*$/i;
3
+ function isLocaleCode(text) {
4
+ if (!localeLike.test(text)) return false;
5
+ try {
6
+ return Intl.getCanonicalLocales(text.replace(/_/g, "-")).length === 1;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+ function escapeRegExp(text) {
12
+ return text.replace(/[.+^$()|[\]\\]/g, "\\$&");
13
+ }
14
+ /**
15
+ * Compiles a resource pattern. Tokens: `{locale}`, `{locale?}`, `{namespace}`,
16
+ * `{namespaces}` (several segments, joined with `.`), `{ext}`; plus `*` and `**`.
17
+ */
18
+ function compilePattern(pattern, extensions) {
19
+ const normalized = pattern.replace(/^\.\//, "");
20
+ const ext = `(?<ext>${extensions.map(escapeRegExp).join("|")})`;
21
+ let source = "";
22
+ let glob = "";
23
+ const tokens = /\{[^}]*\}|\*\*\/|\*\*|\*|\?/g;
24
+ let last = 0;
25
+ for (const match of normalized.matchAll(tokens)) {
26
+ const literal = normalized.slice(last, match.index);
27
+ source += escapeRegExp(literal);
28
+ glob += literal;
29
+ last = match.index + match[0].length;
30
+ switch (match[0]) {
31
+ case "{locale}":
32
+ source += "(?<locale>[\\w-]+)";
33
+ glob += "*";
34
+ break;
35
+ case "{locale?}":
36
+ source += "(?<locale>[\\w-]+)?";
37
+ glob += "*";
38
+ break;
39
+ case "{namespace}":
40
+ source += "(?<namespace>[^/]+)";
41
+ glob += "*";
42
+ break;
43
+ case "{namespaces}":
44
+ source += "(?<namespace>.+)";
45
+ glob += "**";
46
+ break;
47
+ case "{ext}":
48
+ source += ext;
49
+ glob += extensions.length === 1 ? extensions[0] : `{${extensions.join(",")}}`;
50
+ break;
51
+ case "**/":
52
+ source += "(?:.*/)?";
53
+ glob += "**/";
54
+ break;
55
+ case "**":
56
+ source += ".*";
57
+ glob += "**";
58
+ break;
59
+ case "*":
60
+ source += "[^/]*";
61
+ glob += "*";
62
+ break;
63
+ case "?":
64
+ source += "[^/]";
65
+ glob += "?";
66
+ break;
67
+ default: {
68
+ const options = match[0].slice(1, -1).split(",");
69
+ source += `(?:${options.map(escapeRegExp).join("|")})`;
70
+ glob += match[0];
71
+ }
72
+ }
73
+ }
74
+ source += escapeRegExp(normalized.slice(last));
75
+ glob += normalized.slice(last);
76
+ const regex = new RegExp(`^${source}$`);
77
+ return {
78
+ glob,
79
+ match(path) {
80
+ const found = regex.exec(path);
81
+ if (!found) return void 0;
82
+ const groups = found.groups ?? {};
83
+ const extension = groups.ext ?? path.slice(path.lastIndexOf(".") + 1);
84
+ return {
85
+ locale: groups.locale,
86
+ namespace: groups.namespace,
87
+ ext: extension
88
+ };
89
+ }
90
+ };
91
+ }
92
+ const localeDirNames = [
93
+ "locales",
94
+ "locale",
95
+ "i18n",
96
+ "lang",
97
+ "langs",
98
+ "language",
99
+ "languages",
100
+ "translations",
101
+ "messages",
102
+ "_locales",
103
+ "l10n"
104
+ ];
105
+ /** Guesses resource patterns from directories named like `locales`, as i18n-ally did without `localesPaths`. */
106
+ async function guessResources(ctx) {
107
+ const exts = ctx.extensions.length === 1 ? ctx.extensions[0] : `{${ctx.extensions.join(",")}}`;
108
+ const files = await ctx.host.glob([`**/{${localeDirNames.join(",")}}/**/*.${exts}`], {
109
+ ignore: [
110
+ "**/node_modules/**",
111
+ "**/dist/**",
112
+ "**/.git/**",
113
+ "**/.nuxt/**",
114
+ "**/.output/**",
115
+ "**/build/**"
116
+ ],
117
+ gitignore: true
118
+ });
119
+ const dirs = /* @__PURE__ */ new Map();
120
+ for (const file of files) {
121
+ const segments = file.split("/");
122
+ const index = segments.findLastIndex((segment) => localeDirNames.includes(segment));
123
+ const dir = segments.slice(0, index + 1).join("/");
124
+ const list = dirs.get(dir) ?? [];
125
+ dirs.set(dir, list);
126
+ list.push(segments.slice(index + 1).join("/"));
127
+ }
128
+ const specs = [];
129
+ for (const [dir, rest] of dirs) {
130
+ const stem = (file) => file.slice(0, file.lastIndexOf("."));
131
+ const fileLayout = rest.filter((file) => !file.includes("/") && isLocaleCode(stem(file))).length;
132
+ const dirLayout = rest.filter((file) => file.includes("/") && isLocaleCode(file.split("/")[0])).length;
133
+ if (fileLayout === 0 && dirLayout === 0) continue;
134
+ specs.push({ pattern: fileLayout >= dirLayout ? `${dir}/{locale}.{ext}` : `${dir}/{locale}/{namespaces}.{ext}` });
135
+ }
136
+ return specs;
137
+ }
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) {
145
+ const out = [];
146
+ for (const spec of specs) {
147
+ const compiled = compilePattern(spec.pattern, ctx.extensions);
148
+ const fixed = spec.locale ?? (spec.pattern.includes("{locale") ? void 0 : ctx.config.sourceLocale);
149
+ const paths = await ctx.host.glob([compiled.glob], {
150
+ ignore: ["**/node_modules/**"],
151
+ gitignore: false
152
+ });
153
+ for (const path of paths) {
154
+ const matched = compiled.match(path);
155
+ if (!matched) continue;
156
+ const locale = fixed ?? matched.locale ?? (spec.pattern.includes("{locale?}") ? ctx.config.sourceLocale : void 0);
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));
159
+ out.push({
160
+ path,
161
+ locale,
162
+ namespace,
163
+ format: spec.format,
164
+ readonly: spec.execute === true
165
+ });
166
+ }
167
+ }
168
+ return out;
169
+ }
170
+ const patternFinder = {
171
+ id: "pattern",
172
+ resolveNew(target, ctx) {
173
+ const style = ctx.config.namespace.fromPath;
174
+ for (const spec of ctx.config.resources) {
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");
178
+ if (!/[*?{}[\]]/.test(path)) return path;
179
+ }
180
+ },
181
+ async find(ctx) {
182
+ const configured = ctx.config.resources;
183
+ const fromUser = ctx.config.origins.resources === "user" || ctx.config.origins.resources === "parent";
184
+ const found = configured.length > 0 ? await findBySpecs(ctx, configured, !fromUser) : [];
185
+ if (found.length > 0 || fromUser) return found;
186
+ return findBySpecs(ctx, await guessResources(ctx), true);
187
+ }
188
+ };
189
+ /** The `pattern` locale finder: files that match `resources[].pattern`, or guessed locale directories. */
190
+ function pattern() {
191
+ return {
192
+ name: "x18n:finder-pattern",
193
+ localeFinders: [patternFinder]
194
+ };
195
+ }
196
+ //#endregion
197
+ export { patternFinder as a, pattern as i, guessResources as n, namespaceFromPath as r, compilePattern as t };
@@ -0,0 +1,31 @@
1
+ import { FinderContext, LocaleFinder, NamespaceFromPath, Plugin, ResourceSpec } from "@x18n/core";
2
+ //#region src/finders/pattern.d.ts
3
+ interface CompiledPattern {
4
+ /** Glob for the host. */
5
+ readonly glob: string;
6
+ readonly match: (path: string) => {
7
+ locale: string | undefined;
8
+ namespace: string | undefined;
9
+ ext: string;
10
+ } | undefined;
11
+ }
12
+ /**
13
+ * Compiles a resource pattern. Tokens: `{locale}`, `{locale?}`, `{namespace}`,
14
+ * `{namespaces}` (several segments, joined with `.`), `{ext}`; plus `*` and `**`.
15
+ */
16
+ export declare function compilePattern(pattern: string, extensions: readonly string[]): CompiledPattern;
17
+ /** Guesses resource patterns from directories named like `locales`, as i18n-ally did without `localesPaths`. */
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;
21
+ export declare const patternFinder: LocaleFinder;
22
+ /** The `pattern` locale finder: files that match `resources[].pattern`, or guessed locale directories. */
23
+ export declare function pattern(): Plugin;
24
+ //#endregion
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). */
27
+ export declare function sfcBlocks(): Plugin;
28
+ /** fluent-vue `<fluent locale="en">` blocks in Vue SFCs, read with the `ftl` format. */
29
+ export declare function sfcFluentBlocks(): Plugin;
30
+ //#endregion
31
+ export type { CompiledPattern };
@@ -0,0 +1,3 @@
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 };