@ai-i18n/eslint-plugin 1.0.0-alpha.2 → 1.0.0-alpha.21

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/index.js CHANGED
@@ -1,8 +1,204 @@
1
1
  import { createRequire } from "node:module";
2
+ import { AI_I18N_VIRTUAL_MODULE_ID, Analyzer, diagnosticMessage, extractMessages, findInvalidDefineI18nMessagesReferences, findTranslationCalls, findUnboundCalls, validateRecommendedUsage } from "@ai-i18n/analyzer";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import { AI_I18N_VIRTUAL_MODULE_ID, Analyzer, extractMessages, findUnboundCalls } from "@ai-i18n/analyzer";
5
+ import { createFilesMatcher, createPathsMatcher, parseTsconfig } from "get-tsconfig";
6
+ import picomatch from "picomatch";
5
7
  import { analyzeVueSource } from "@ai-i18n/analyzer/vue";
8
+ import { defaultTreeAdapter, parse, parseFragment } from "parse5";
9
+ //#region src/auto-imports.ts
10
+ const RUNTIME_AUTO_IMPORTS = [
11
+ "t",
12
+ "setLang",
13
+ "getLang",
14
+ "getLangs",
15
+ "getLangLoadState",
16
+ "subscribe"
17
+ ];
18
+ const VUE_AUTO_IMPORTS = [
19
+ "useI18n",
20
+ ...RUNTIME_AUTO_IMPORTS,
21
+ "tRef",
22
+ "i18nComputed",
23
+ "tComputed"
24
+ ];
25
+ const REACT_AUTO_IMPORTS = ["useI18n", ...RUNTIME_AUTO_IMPORTS];
26
+ const ALL_AUTO_IMPORT_APIS = [...VUE_AUTO_IMPORTS];
27
+ //#endregion
28
+ //#region src/tsconfig-projects.ts
29
+ const PROJECT_CACHE_TTL_MS = 250;
30
+ const MAX_PROJECT_CACHE_ENTRIES = 1e3;
31
+ const READ_FILE_CACHE_PREFIX = "readFileSync:";
32
+ const READ_FILE_CACHE_SUFFIX = ":utf8";
33
+ const projectGraphCache = /* @__PURE__ */ new Map();
34
+ const discoveryCache = /* @__PURE__ */ new Map();
35
+ function createTsconfigResolver(tsconfigPath) {
36
+ const explicitRoot = tsconfigPath ? path.normalize(path.resolve(tsconfigPath)) : void 0;
37
+ return (specifier, importer) => {
38
+ const rootConfig = explicitRoot ?? findNearestProjectConfig(importer);
39
+ if (!rootConfig) return [];
40
+ return selectProject(loadProjectGraph(rootConfig), importer)?.pathsMatcher?.(specifier) ?? [];
41
+ };
42
+ }
43
+ function findNearestProjectConfig(importer) {
44
+ let directory = path.dirname(path.resolve(importer));
45
+ const now = Date.now();
46
+ const cached = discoveryCache.get(directory);
47
+ if (cached && cached.expiresAt > now) return cached.configPath;
48
+ const visited = [];
49
+ for (;;) {
50
+ visited.push(directory);
51
+ for (const filename of ["tsconfig.json", "jsconfig.json"]) {
52
+ const candidate = path.join(directory, filename);
53
+ if (isFile$1(candidate)) {
54
+ cacheDiscovery(visited, candidate, now);
55
+ return candidate;
56
+ }
57
+ }
58
+ const parent = path.dirname(directory);
59
+ if (parent === directory) {
60
+ cacheDiscovery(visited, null, now);
61
+ return null;
62
+ }
63
+ directory = parent;
64
+ }
65
+ }
66
+ function cacheDiscovery(directories, configPath, now) {
67
+ if (discoveryCache.size >= MAX_PROJECT_CACHE_ENTRIES) discoveryCache.clear();
68
+ for (const directory of directories) discoveryCache.set(directory, {
69
+ expiresAt: now + PROJECT_CACHE_TTL_MS,
70
+ configPath
71
+ });
72
+ }
73
+ function loadProjectGraph(rootConfig) {
74
+ const cached = projectGraphCache.get(rootConfig);
75
+ if (cached && projectGraphIsFresh(cached)) return cached.projects;
76
+ const projects = [];
77
+ const stamps = /* @__PURE__ */ new Map();
78
+ visitProject(rootConfig, projects, stamps, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
79
+ if (projectGraphCache.size >= MAX_PROJECT_CACHE_ENTRIES) projectGraphCache.clear();
80
+ projectGraphCache.set(rootConfig, {
81
+ expiresAt: Date.now() + PROJECT_CACHE_TTL_MS,
82
+ projects,
83
+ stamps
84
+ });
85
+ return projects;
86
+ }
87
+ function visitProject(filename, projects, stamps, parsingCache, visited) {
88
+ filename = path.normalize(path.resolve(filename));
89
+ if (visited.has(filename)) return;
90
+ visited.add(filename);
91
+ stamps.set(filename, readFileStamp(filename));
92
+ let config;
93
+ try {
94
+ config = {
95
+ path: filename,
96
+ config: parseTsconfig(filename, parsingCache)
97
+ };
98
+ captureParsedConfigStamps(parsingCache, stamps);
99
+ } catch {
100
+ return;
101
+ }
102
+ for (const reference of config.config.references ?? []) {
103
+ const referenced = resolveReference(filename, reference.path);
104
+ if (referenced) visitProject(referenced, projects, stamps, parsingCache, visited);
105
+ }
106
+ try {
107
+ projects.push({
108
+ config,
109
+ filesMatcher: createProjectFilesMatcher(config),
110
+ pathsMatcher: createCompatiblePathsMatcher(config)
111
+ });
112
+ } catch {}
113
+ }
114
+ function createProjectFilesMatcher(config) {
115
+ if (path.basename(config.path).toLowerCase() !== "jsconfig.json" || config.config.compilerOptions?.allowJs !== void 0) return createFilesMatcher(config);
116
+ return createFilesMatcher({
117
+ ...config,
118
+ config: {
119
+ ...config.config,
120
+ compilerOptions: {
121
+ ...config.config.compilerOptions,
122
+ allowJs: true
123
+ }
124
+ }
125
+ });
126
+ }
127
+ function captureParsedConfigStamps(parsingCache, stamps) {
128
+ for (const key of parsingCache.keys()) {
129
+ if (!key.startsWith(READ_FILE_CACHE_PREFIX) || !key.endsWith(READ_FILE_CACHE_SUFFIX)) continue;
130
+ const filename = key.slice(13, -5);
131
+ stamps.set(filename, readFileStamp(filename));
132
+ }
133
+ }
134
+ function createCompatiblePathsMatcher(config) {
135
+ const compilerOptions = config.config.compilerOptions;
136
+ const paths = compilerOptions?.paths;
137
+ if (!paths || compilerOptions.baseUrl) return createPathsMatcher(config);
138
+ const compatiblePaths = Object.fromEntries(Object.entries(paths).map(([pattern, targets]) => [pattern, targets.map((target) => path.isAbsolute(target) || target.startsWith(".") ? target : `./${target}`)]));
139
+ return createPathsMatcher({
140
+ path: config.path,
141
+ config: {
142
+ ...config.config,
143
+ compilerOptions: {
144
+ ...compilerOptions,
145
+ paths: compatiblePaths
146
+ }
147
+ }
148
+ });
149
+ }
150
+ function resolveReference(ownerConfig, referencePath) {
151
+ const candidate = path.resolve(path.dirname(ownerConfig), referencePath);
152
+ return [
153
+ candidate,
154
+ candidate.endsWith(".json") ? candidate : `${candidate}.json`,
155
+ path.join(candidate, "tsconfig.json")
156
+ ].find(isFile$1) ?? null;
157
+ }
158
+ function selectProject(projects, importer) {
159
+ const absoluteImporter = path.resolve(importer);
160
+ return projects.find((project) => projectMatchesImporter(project, absoluteImporter));
161
+ }
162
+ function projectMatchesImporter(project, importer) {
163
+ if (path.extname(importer) !== ".vue") return Boolean(project.filesMatcher(importer));
164
+ return matchesExplicitVueInput(project.config, importer);
165
+ }
166
+ function matchesExplicitVueInput({ config, path: configPath }, importer) {
167
+ const directory = path.dirname(configPath);
168
+ const relative = normalizePath(path.relative(directory, importer));
169
+ if (relative.startsWith("../")) return false;
170
+ if (config.files?.some((file) => path.resolve(directory, file) === path.resolve(importer))) return true;
171
+ if (config.exclude?.some((pattern) => matchesExclude(relative, pattern))) return false;
172
+ return Boolean(config.include?.some((pattern) => pattern.includes(".vue") && picomatch.isMatch(relative, normalizePath(pattern), { dot: true })));
173
+ }
174
+ function matchesExclude(relative, pattern) {
175
+ pattern = normalizePath(pattern).replace(/\/+$/, "");
176
+ return picomatch.isMatch(relative, pattern, { dot: true }) || picomatch.isMatch(relative, `${pattern}/**`, { dot: true });
177
+ }
178
+ function projectGraphIsFresh(cached) {
179
+ if (cached.expiresAt <= Date.now()) return false;
180
+ for (const [filename, stamp] of cached.stamps) if (readFileStamp(filename) !== stamp) return false;
181
+ return true;
182
+ }
183
+ function readFileStamp(filename) {
184
+ try {
185
+ const stat = fs.statSync(filename);
186
+ return stat.isFile() ? `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}` : null;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+ function isFile$1(filename) {
192
+ try {
193
+ return fs.statSync(filename).isFile();
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+ function normalizePath(filename) {
199
+ return filename.replaceAll("\\", "/");
200
+ }
201
+ //#endregion
6
202
  //#region src/resolve-import.ts
7
203
  const SOURCE_EXTENSIONS = [
8
204
  "",
@@ -11,51 +207,69 @@ const SOURCE_EXTENSIONS = [
11
207
  ".js",
12
208
  ".jsx",
13
209
  ".mts",
14
- ".cts",
15
- ".mjs",
16
- ".cjs"
210
+ ".mjs"
17
211
  ];
18
- function createImportResolver(tsconfigPath) {
19
- const aliases = [];
20
- let baseUrl = process.cwd();
21
- if (tsconfigPath) {
22
- const absoluteConfig = path.resolve(tsconfigPath);
23
- const config = readTsConfig(absoluteConfig);
24
- baseUrl = path.resolve(path.dirname(absoluteConfig), config.compilerOptions?.baseUrl ?? ".");
25
- for (const [pattern, targets] of Object.entries(config.compilerOptions?.paths ?? {})) {
26
- const star = pattern.indexOf("*");
27
- aliases.push({
28
- prefix: star < 0 ? pattern : pattern.slice(0, star),
29
- suffix: star < 0 ? "" : pattern.slice(star + 1),
30
- targets
31
- });
32
- }
33
- }
212
+ const DIRECTORY_STAMP_TTL_MS = 250;
213
+ const MAX_PROBE_CACHE_ENTRIES = 1e4;
214
+ const directoryStampCache = /* @__PURE__ */ new Map();
215
+ const probeCache = /* @__PURE__ */ new Map();
216
+ function createImportResolver(tsconfigPath, alias) {
217
+ const aliases = normalizeAliases(alias);
218
+ const resolveTsconfig = createTsconfigResolver(tsconfigPath);
34
219
  return (specifier, importer) => {
35
220
  if (specifier === "virtual:ai-i18n") return specifier;
221
+ const aliasCandidate = resolveAlias(specifier, aliases);
222
+ if (aliasCandidate) return probeSource(aliasCandidate);
36
223
  if (specifier.startsWith(".")) return probeSource(path.resolve(path.dirname(importer), specifier));
37
- for (const alias of aliases) {
38
- if (!specifier.startsWith(alias.prefix) || !specifier.endsWith(alias.suffix)) continue;
39
- const value = specifier.slice(alias.prefix.length, specifier.length - alias.suffix.length || void 0);
40
- for (const target of alias.targets) {
41
- const candidate = target.includes("*") ? target.replace("*", value) : target;
42
- const resolved = probeSource(path.resolve(baseUrl, candidate));
43
- if (resolved) return resolved;
44
- }
224
+ for (const candidate of resolveTsconfig(specifier, importer)) {
225
+ const resolved = probeSource(candidate);
226
+ if (resolved) return resolved;
45
227
  }
46
228
  return null;
47
229
  };
48
230
  }
231
+ function normalizeAliases(alias) {
232
+ return Object.entries(alias ?? {}).map(([find, replacement]) => {
233
+ if (!find) throw new TypeError(diagnosticMessage("ai-i18n alias 的匹配键不能为空。", "The ai-i18n alias match key must not be empty."));
234
+ if (typeof replacement !== "string" || !path.isAbsolute(replacement)) throw new TypeError(diagnosticMessage(`ai-i18n alias "${find}" 的 replacement 必须是绝对路径。`, `The replacement for ai-i18n alias "${find}" must be an absolute path.`));
235
+ return {
236
+ find,
237
+ replacement
238
+ };
239
+ });
240
+ }
241
+ function resolveAlias(specifier, aliases) {
242
+ for (const { find, replacement } of aliases) if (specifier === find || specifier.startsWith(`${find}/`)) return path.normalize(`${replacement}${specifier.slice(find.length)}`);
243
+ return null;
244
+ }
49
245
  function probeSource(candidate) {
246
+ candidate = path.normalize(candidate);
247
+ if (/\.(?:cjs|cts)$/i.test(candidate)) return null;
248
+ const cached = probeCache.get(candidate);
249
+ if (cached && (cached.resolved !== null || cached.expiresAt > Date.now()) && cached.parentDirectory === readDirectoryStamp(path.dirname(candidate)) && cached.candidateDirectory === readDirectoryStamp(candidate)) return cached.resolved;
250
+ let resolved = null;
50
251
  for (const extension of SOURCE_EXTENSIONS) {
51
252
  const file = `${candidate}${extension}`;
52
- if (isFile(file)) return path.normalize(file);
253
+ if (isFile(file)) {
254
+ resolved = path.normalize(file);
255
+ break;
256
+ }
53
257
  }
54
- for (const extension of SOURCE_EXTENSIONS.slice(1)) {
258
+ if (!resolved) for (const extension of SOURCE_EXTENSIONS.slice(1)) {
55
259
  const file = path.join(candidate, `index${extension}`);
56
- if (isFile(file)) return path.normalize(file);
260
+ if (isFile(file)) {
261
+ resolved = path.normalize(file);
262
+ break;
263
+ }
57
264
  }
58
- return null;
265
+ if (probeCache.size >= MAX_PROBE_CACHE_ENTRIES) probeCache.clear();
266
+ probeCache.set(candidate, {
267
+ parentDirectory: readDirectoryStamp(path.dirname(candidate)),
268
+ candidateDirectory: readDirectoryStamp(candidate),
269
+ expiresAt: Date.now() + DIRECTORY_STAMP_TTL_MS,
270
+ resolved
271
+ });
272
+ return resolved;
59
273
  }
60
274
  function isFile(file) {
61
275
  try {
@@ -64,79 +278,103 @@ function isFile(file) {
64
278
  return false;
65
279
  }
66
280
  }
67
- function readTsConfig(filename) {
281
+ function readDirectoryStamp(directory) {
282
+ const now = Date.now();
283
+ const cached = directoryStampCache.get(directory);
284
+ if (cached && cached.expiresAt > now) return cached.value;
285
+ let value = null;
68
286
  try {
69
- return JSON.parse(stripJsonComments(fs.readFileSync(filename, "utf8")));
70
- } catch {
71
- return {};
72
- }
73
- }
74
- function stripJsonComments(source) {
75
- let result = "";
76
- let quote = "";
77
- let escaped = false;
78
- for (let index = 0; index < source.length; index += 1) {
79
- const current = source[index];
80
- const next = source[index + 1];
81
- if (quote) {
82
- result += current;
83
- if (escaped) escaped = false;
84
- else if (current === "\\") escaped = true;
85
- else if (current === quote) quote = "";
86
- continue;
87
- }
88
- if (current === "\"" || current === "'") {
89
- quote = current;
90
- result += current;
91
- continue;
92
- }
93
- if (current === "/" && next === "/") {
94
- while (index < source.length && source[index] !== "\n") index += 1;
95
- result += "\n";
96
- continue;
97
- }
98
- if (current === "/" && next === "*") {
99
- index += 2;
100
- while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) {
101
- if (source[index] === "\n") result += "\n";
102
- index += 1;
103
- }
104
- index += 1;
105
- continue;
106
- }
107
- result += current;
108
- }
109
- return result.replace(/,\s*([}\]])/g, "$1");
287
+ const stat = fs.statSync(directory);
288
+ if (stat.isDirectory()) value = `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;
289
+ } catch {}
290
+ if (directoryStampCache.size >= MAX_PROBE_CACHE_ENTRIES) directoryStampCache.clear();
291
+ directoryStampCache.set(directory, {
292
+ expiresAt: now + DIRECTORY_STAMP_TTL_MS,
293
+ value
294
+ });
295
+ return value;
110
296
  }
111
297
  //#endregion
112
298
  //#region src/analyze.ts
113
- function analyzeStaticArgs(code, filename, tsconfigPath, lang, autoImport = false) {
114
- const resolve = createImportResolver(tsconfigPath);
299
+ const POTENTIAL_TRANSLATION_RE = /virtual:ai-i18n|\b(?:t|tRef|tComputed|useI18n|defineI18nMessages)\b/;
300
+ function analyzeStaticSource(code, filename, tsconfigPath, lang, autoImport = false, maxStaticCandidates = Number.POSITIVE_INFINITY, alias) {
301
+ if (!hasPotentialTranslationCandidate(code)) return {
302
+ warnings: [],
303
+ translationCalls: [],
304
+ messages: []
305
+ };
306
+ const autoImports = normalizeAutoImports$1(autoImport);
307
+ const translationAutoImports = runtimeAutoImports(autoImports);
308
+ const resolve = createImportResolver(tsconfigPath, alias);
115
309
  const analyzer = new Analyzer({ resolve });
116
- analyzer.addFile(AI_I18N_VIRTUAL_MODULE_ID, "export function t(source) { return source }");
310
+ analyzer.addFile(AI_I18N_VIRTUAL_MODULE_ID, "export function t(source) { return source } export function tRef(source) { return source } export function tComputed(source) { return source }");
117
311
  const entryPath = normalizeFilename(filename);
118
312
  const entry = analyzer.addFile(entryPath, code, lang ? { lang } : void 0);
119
- if (!hasTranslationCandidate(entry, autoImport)) return [];
313
+ if (!hasTranslationCandidate(entry, autoImports)) return {
314
+ warnings: [],
315
+ translationCalls: [],
316
+ messages: []
317
+ };
120
318
  loadDependencies(analyzer, entry, resolve);
121
319
  analyzer.link();
122
- return extractMessages(entry, AI_I18N_VIRTUAL_MODULE_ID, translationHooks(autoImport), autoImport).warnings.map(({ code: warningCode, line, column, message }) => ({
123
- code: warningCode,
124
- line,
125
- column,
126
- message
127
- }));
320
+ const hooks = translationHooks(autoImports);
321
+ const extraction = extractMessages(entry, AI_I18N_VIRTUAL_MODULE_ID, hooks, translationAutoImports, maxStaticCandidates);
322
+ const recommended = validateRecommendedUsage(entry, AI_I18N_VIRTUAL_MODULE_ID, hooks, translationAutoImports);
323
+ return {
324
+ warnings: (recommended.length ? [
325
+ ...extraction.warnings.filter((warning) => warning.code === "parse-error"),
326
+ ...extraction.warnings.filter((warning) => warning.code === "static-candidate-limit"),
327
+ ...recommended
328
+ ] : extraction.warnings).map(({ code: warningCode, line, column, message }) => ({
329
+ code: warningCode,
330
+ line,
331
+ column,
332
+ message
333
+ })),
334
+ translationCalls: findTranslationCalls(entry, AI_I18N_VIRTUAL_MODULE_ID, hooks, translationAutoImports),
335
+ messages: extraction.messages
336
+ };
128
337
  }
129
- function hasTranslationCandidate(module, autoImport) {
130
- return module.imports.some((item) => !item.typeOnly && (item.name === "t" || item.name === "useI18n")) || autoImport && findUnboundCalls(module, /* @__PURE__ */ new Set(["t", "useI18n"])).length > 0;
338
+ function hasPotentialTranslationCandidate(code) {
339
+ return POTENTIAL_TRANSLATION_RE.test(code);
340
+ }
341
+ function normalizeAutoImports$1(autoImport) {
342
+ if (typeof autoImport === "boolean") return {
343
+ t: autoImport,
344
+ tRef: false,
345
+ tComputed: false,
346
+ useI18n: autoImport
347
+ };
348
+ return {
349
+ t: autoImport?.includes("t") ?? false,
350
+ tRef: autoImport?.includes("tRef") ?? false,
351
+ tComputed: autoImport?.includes("tComputed") ?? false,
352
+ useI18n: autoImport?.includes("useI18n") ?? false
353
+ };
131
354
  }
132
- function translationHooks(autoImport) {
355
+ function hasTranslationCandidate(module, autoImports) {
356
+ const unbound = ["defineI18nMessages"];
357
+ if (autoImports.t) unbound.push("t");
358
+ if (autoImports.tRef) unbound.push("tRef");
359
+ if (autoImports.tComputed) unbound.push("tComputed");
360
+ if (autoImports.useI18n) unbound.push("useI18n");
361
+ return module.imports.some((item) => !item.typeOnly && (item.specifier === AI_I18N_VIRTUAL_MODULE_ID || item.name === "t" || item.name === "tRef" || item.name === "tComputed" || item.name === "useI18n")) || findInvalidDefineI18nMessagesReferences(module).length > 0 || findUnboundCalls(module, new Set(unbound)).length > 0;
362
+ }
363
+ function translationHooks(autoImports) {
133
364
  return [{
134
365
  module: AI_I18N_VIRTUAL_MODULE_ID,
135
366
  hook: "useI18n",
136
367
  property: "t",
137
- autoImport
368
+ autoImport: autoImports.useI18n
138
369
  }];
139
370
  }
371
+ function runtimeAutoImports(autoImports) {
372
+ const names = /* @__PURE__ */ new Set();
373
+ if (autoImports.t) names.add("t");
374
+ if (autoImports.tRef) names.add("tRef");
375
+ if (autoImports.tComputed) names.add("tComputed");
376
+ return names;
377
+ }
140
378
  function loadDependencies(analyzer, entry, resolve) {
141
379
  const queue = [entry];
142
380
  const visited = /* @__PURE__ */ new Set();
@@ -162,36 +400,1180 @@ function normalizeFilename(filename) {
162
400
  }
163
401
  //#endregion
164
402
  //#region src/vue-sfc.ts
165
- const require = createRequire(import.meta.url);
403
+ const packageRequire = createRequire(import.meta.url);
166
404
  function createVueAnalysisSource(source, filename, parserServices) {
167
- if (!parserServices.getDocumentFragment?.()) throw new Error("检查 .vue 文件需要使用 vue-eslint-parser");
405
+ if (!parserServices.getDocumentFragment?.()) throw new Error(diagnosticMessage("检查 .vue 文件需要配置 vue-eslint-parser", "Configure vue-eslint-parser to lint .vue files."));
168
406
  try {
169
- return analyzeVueSource(source, filename, require("@vue/compiler-sfc"));
407
+ return analyzeVueSource(source, filename, loadVueCompiler(filename));
170
408
  } catch (error) {
171
- if (isMissingVueCompiler(error)) throw new Error("检查 .vue 文件需要安装 @vue/compiler-sfc");
409
+ if (isMissingVueCompiler(error)) throw new Error(diagnosticMessage("检查 .vue 文件需要安装 @vue/compiler-sfc", "Install @vue/compiler-sfc to lint .vue files."));
410
+ throw error;
411
+ }
412
+ }
413
+ function loadVueCompiler(filename) {
414
+ const hostRequire = createRequire(path.resolve(filename));
415
+ const compiler = tryRequire(hostRequire, "vue/compiler-sfc") ?? tryRequire(packageRequire, "vue/compiler-sfc") ?? tryRequire(hostRequire, "@vue/compiler-sfc") ?? packageRequire("@vue/compiler-sfc");
416
+ compiler.registerTS?.(() => {
417
+ const typescript = tryResolve(hostRequire, "typescript") ?? tryResolve(packageRequire, "typescript");
418
+ return packageRequire(typescript ?? "typescript");
419
+ });
420
+ return compiler;
421
+ }
422
+ function tryRequire(require, id) {
423
+ const resolved = tryResolve(require, id);
424
+ return resolved ? require(resolved) : null;
425
+ }
426
+ function tryResolve(require, id) {
427
+ try {
428
+ return require.resolve(id);
429
+ } catch (error) {
430
+ if (isMissingVueCompiler(error)) return null;
172
431
  throw error;
173
432
  }
174
433
  }
175
434
  function isMissingVueCompiler(error) {
176
- return error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND";
435
+ return error instanceof Error && "code" in error && (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED");
436
+ }
437
+ //#endregion
438
+ //#region src/rule-analysis.ts
439
+ const DEFAULT_CANDIDATE_LIMIT = 1e3;
440
+ const cache = /* @__PURE__ */ new WeakMap();
441
+ const reportedAnalysisFailures = /* @__PURE__ */ new WeakSet();
442
+ const EMPTY_ANALYSIS = {
443
+ warnings: [],
444
+ translationCalls: [],
445
+ messages: []
446
+ };
447
+ function analyzeRuleContext(context, options, maxStaticCandidates = Number.POSITIVE_INFINITY) {
448
+ return analyzeRuleContextResult(context, options, maxStaticCandidates).warnings;
449
+ }
450
+ function analyzeTranslationCalls(context, options) {
451
+ return analyzeRuleContextResult(context, options, DEFAULT_CANDIDATE_LIMIT).translationCalls;
452
+ }
453
+ function analyzeTranslationMessages(context, options) {
454
+ return analyzeRuleContextResult(context, options, Number.POSITIVE_INFINITY).messages;
455
+ }
456
+ function reportAnalysisFailureOnce(context, node, error) {
457
+ if (reportedAnalysisFailures.has(context.sourceCode)) return;
458
+ reportedAnalysisFailures.add(context.sourceCode);
459
+ const detail = error instanceof Error ? error.message : String(error);
460
+ context.report({
461
+ node,
462
+ message: diagnosticMessage(`静态分析失败:${detail}`, `Static analysis failed: ${detail}`)
463
+ });
464
+ }
465
+ function analyzeRuleContextResult(context, options, maxStaticCandidates) {
466
+ if (!hasPotentialTranslationCandidate(context.sourceCode.text)) return EMPTY_ANALYSIS;
467
+ const cached = cache.get(context.sourceCode) ?? {
468
+ source: createAnalysisSource(context),
469
+ analyses: /* @__PURE__ */ new Map()
470
+ };
471
+ cache.set(context.sourceCode, cached);
472
+ const alias = readImportAlias(context);
473
+ const analyze = (limit) => {
474
+ const autoImports = normalizeAutoImports$1(options.autoImport);
475
+ const key = JSON.stringify([
476
+ options.tsconfigPath ?? null,
477
+ autoImports.t,
478
+ autoImports.tRef,
479
+ autoImports.tComputed,
480
+ autoImports.useI18n,
481
+ Object.entries(alias ?? {}),
482
+ limit
483
+ ]);
484
+ const existing = cached.analyses.get(key);
485
+ if (existing) return existing;
486
+ const result = analyzeStaticSource(cached.source.code, context.filename, options.tsconfigPath, cached.source.lang, options.autoImport, limit, alias);
487
+ const analysis = {
488
+ warnings: result.warnings.map((warning) => ({
489
+ ...warning,
490
+ ...cached.source.mapLocation(warning)
491
+ })),
492
+ translationCalls: result.translationCalls.map((call) => ({
493
+ ...call,
494
+ ...cached.source.mapLocation(call)
495
+ })),
496
+ messages: result.messages.map((message) => ({
497
+ ...message,
498
+ locations: message.locations.map((location) => ({
499
+ ...location,
500
+ ...cached.source.mapLocation(location)
501
+ }))
502
+ }))
503
+ };
504
+ cached.analyses.set(key, analysis);
505
+ return analysis;
506
+ };
507
+ if (Number.isFinite(maxStaticCandidates)) return analyze(maxStaticCandidates);
508
+ const preflight = analyze(DEFAULT_CANDIDATE_LIMIT);
509
+ return preflight.warnings.some((warning) => warning.code === "static-candidate-limit") ? analyze(Number.POSITIVE_INFINITY) : preflight;
510
+ }
511
+ function readImportAlias(context) {
512
+ const settings = context.settings["ai-i18n"];
513
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) return;
514
+ const alias = settings.alias;
515
+ if (alias === void 0) return void 0;
516
+ if (!alias || typeof alias !== "object" || Array.isArray(alias)) throw new TypeError(diagnosticMessage("ai-i18n settings.alias 必须是字符串到绝对路径的对象。", "ai-i18n settings.alias must be an object mapping strings to absolute paths."));
517
+ return alias;
518
+ }
519
+ function createAnalysisSource(context) {
520
+ return context.filename.endsWith(".vue") ? createVueAnalysisSource(context.sourceCode.text, context.filename, context.sourceCode.parserServices) : {
521
+ code: context.sourceCode.text,
522
+ lang: void 0,
523
+ mapLocation: (location) => location
524
+ };
525
+ }
526
+ //#endregion
527
+ //#region src/rules/common/translation-call.ts
528
+ function matchTranslationCalls(context, options, candidates) {
529
+ const calls = new Map(analyzeTranslationCalls(context, options).map((call) => [locationKey(call.line, call.column, call.kind), call]));
530
+ return candidates.flatMap((candidate) => {
531
+ const location = candidate.node.loc?.start;
532
+ if (!location) return [];
533
+ const call = calls.get(locationKey(location.line, location.column, candidate.kind));
534
+ return call ? [{
535
+ ...candidate,
536
+ call
537
+ }] : [];
538
+ });
539
+ }
540
+ function locationKey(line, column, kind) {
541
+ return `${kind}:${line}:${column}`;
542
+ }
543
+ //#endregion
544
+ //#region src/rules/common/ast-context.ts
545
+ const FUNCTION_TYPES = /* @__PURE__ */ new Set([
546
+ "ArrowFunctionExpression",
547
+ "FunctionDeclaration",
548
+ "FunctionExpression"
549
+ ]);
550
+ const IMMEDIATE_CONSOLE_METHODS = /* @__PURE__ */ new Set([
551
+ "debug",
552
+ "error",
553
+ "info",
554
+ "log",
555
+ "warn"
556
+ ]);
557
+ function isFunctionNode(node) {
558
+ return FUNCTION_TYPES.has(node.type);
559
+ }
560
+ function nearestFunction(node) {
561
+ let current = node;
562
+ while (current.parent) {
563
+ current = current.parent;
564
+ if (isFunctionNode(current)) return current;
565
+ }
566
+ return null;
567
+ }
568
+ function isImmediateConsoleEffect(node) {
569
+ const call = node.parent;
570
+ if (call?.type !== "CallExpression" || call.parent?.type !== "ExpressionStatement" || !call.arguments.some((argument) => argument === node) || call.callee.type !== "MemberExpression" || call.callee.object.type !== "Identifier" || call.callee.object.name !== "console") return false;
571
+ const property = call.callee.property;
572
+ const method = !call.callee.computed && property.type === "Identifier" ? property.name ?? null : call.callee.computed && property.type === "Literal" && typeof property.value === "string" ? property.value : null;
573
+ return method !== null && IMMEDIATE_CONSOLE_METHODS.has(method);
574
+ }
575
+ function isVueTemplateEventHandler(node) {
576
+ let current = node;
577
+ while (current.parent) {
578
+ current = current.parent;
579
+ if (current.type === "VOnExpression") return true;
580
+ }
581
+ return false;
582
+ }
583
+ //#endregion
584
+ //#region src/rules/vue/options.ts
585
+ function isOptionsComputedValue(node, context) {
586
+ let value = node;
587
+ while (value.parent && isTransparentExpression(value.parent) && expressionChild(value.parent) === value) value = value.parent;
588
+ const entry = value.parent;
589
+ if (entry?.type !== "Property" || entry.value !== value) return false;
590
+ const entries = entry.parent;
591
+ if (entries?.type !== "ObjectExpression") return false;
592
+ const computed = entries.parent;
593
+ return computed?.type === "Property" && computed.value === entries && propertyName(computed.key) === "computed" && computed.parent?.type === "ObjectExpression" && isVueComponentOptionsObject(computed.parent, context);
594
+ }
595
+ function isOptionsComputedSpread(node, context) {
596
+ const value = unwrapTransparentParent(node);
597
+ const spread = value.parent;
598
+ if (spread?.type !== "SpreadElement" || spread.argument !== value) return false;
599
+ const entries = spread.parent;
600
+ if (entries?.type !== "ObjectExpression") return false;
601
+ const computed = entries.parent;
602
+ return computed?.type === "Property" && computed.value === entries && propertyName(computed.key) === "computed" && computed.parent?.type === "ObjectExpression" && isVueComponentOptionsObject(computed.parent, context);
603
+ }
604
+ function isInsideOptionsComputedGetter(node, context) {
605
+ const owner = nearestFunction(node);
606
+ if (!owner) return false;
607
+ let current = owner;
608
+ while (current?.parent) {
609
+ const parent = current.parent;
610
+ if (current.type === "ObjectExpression" && parent.type === "Property" && parent.value === current && propertyName(parent.key) === "computed" && parent.parent?.type === "ObjectExpression" && isVueComponentOptionsObject(parent.parent, context)) return true;
611
+ current = current.parent;
612
+ if (current && current !== owner && isFunctionNode(current)) return false;
613
+ }
614
+ return false;
615
+ }
616
+ function vueOptionsSection(node, context) {
617
+ let current = node;
618
+ while (current.parent) {
619
+ const property = current.parent;
620
+ if (property.type === "Property" && property.value === current && property.parent?.type === "ObjectExpression" && isVueComponentOptionsObject(property.parent, context)) return propertyName(property.key) ?? null;
621
+ current = current.parent;
622
+ }
623
+ return null;
624
+ }
625
+ function isDirectOptionsFunction$1(node, context, name, allowBareExportDefault = true) {
626
+ const value = unwrapTransparentParent(node);
627
+ const property = value.parent;
628
+ if (property?.type !== "Property" || property.value !== value || propertyName(property.key) !== name) return false;
629
+ const options = property.parent;
630
+ return options?.type === "ObjectExpression" && isVueComponentOptionsObject(options, context, allowBareExportDefault);
631
+ }
632
+ function isVueComponentSetup(node, context, isVueSfc) {
633
+ const value = unwrapTransparentParent(node);
634
+ const directOwner = value.parent;
635
+ if (directOwner?.type === "CallExpression" && directOwner.arguments?.[0] === value) return isImportedVueDefineComponent(directOwner.callee, context);
636
+ const property = value.parent;
637
+ if (property?.type !== "Property" || property.value !== value || property.computed || propertyName(property.key) !== "setup") return false;
638
+ const options = property.parent;
639
+ if (options?.type !== "ObjectExpression") return false;
640
+ const owner = options.parent;
641
+ if (isVueSfc && owner?.type === "ExportDefaultDeclaration" && owner.declaration === options) return true;
642
+ return owner?.type === "CallExpression" && owner.arguments?.[0] === options && isImportedVueDefineComponent(owner.callee, context);
643
+ }
644
+ function isVueScriptSetupNode(node, context) {
645
+ if (!context.filename.toLowerCase().endsWith(".vue") || !node.range) return false;
646
+ const source = context.sourceCode.text;
647
+ const before = source.slice(0, node.range[0]);
648
+ const opening = before.lastIndexOf("<script");
649
+ if (opening <= before.lastIndexOf("<\/script>")) return false;
650
+ const closing = source.indexOf(">", opening);
651
+ if (closing < 0 || closing >= node.range[0]) return false;
652
+ return /\bsetup(?:\s|=|>)/u.test(source.slice(opening, closing + 1));
653
+ }
654
+ function isImportedVueDefineComponent(node, context) {
655
+ const callee = node;
656
+ if (callee?.type === "Identifier") return isVueImport(callee, context, "ImportSpecifier");
657
+ return callee?.type === "MemberExpression" && !callee.computed && propertyName(callee.property) === "defineComponent" && isVueImport(callee.object, context, "ImportNamespaceSpecifier");
658
+ }
659
+ function propertyName(node) {
660
+ const key = node;
661
+ if (key?.type === "Identifier") return key.name;
662
+ return key?.type === "Literal" && typeof key.value === "string" ? key.value : void 0;
663
+ }
664
+ function isVueComponentOptionsObject(options, context, allowBareExportDefault = true) {
665
+ const value = unwrapTransparentParent(options);
666
+ const owner = value.parent;
667
+ if (allowBareExportDefault && owner?.type === "ExportDefaultDeclaration" && owner.declaration === value) return true;
668
+ return owner?.type === "CallExpression" && owner.arguments?.[0] === value && isImportedVueDefineComponent(owner.callee, context);
669
+ }
670
+ function isVueImport(node, context, specifierType) {
671
+ const identifier = node;
672
+ if (identifier?.type !== "Identifier" || !identifier.name) return false;
673
+ let scope = context.sourceCode.getScope(identifier);
674
+ while (true) {
675
+ const variable = scope.set.get(identifier.name);
676
+ if (!variable) {
677
+ if (!scope.upper) return false;
678
+ scope = scope.upper;
679
+ continue;
680
+ }
681
+ return variable.defs.some((definition) => {
682
+ if (definition.type !== "ImportBinding" || definition.node.type !== specifierType || definition.parent.source.value !== "vue") return false;
683
+ return specifierType === "ImportNamespaceSpecifier" || propertyName(definition.node.imported) === "defineComponent";
684
+ });
685
+ }
686
+ }
687
+ function unwrapTransparentParent(node) {
688
+ let value = node;
689
+ while (value.parent && isTransparentExpression(value.parent) && expressionChild(value.parent) === value) value = value.parent;
690
+ return value;
691
+ }
692
+ function isTransparentExpression(node) {
693
+ return node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "TypeCastExpression";
694
+ }
695
+ function expressionChild(node) {
696
+ return node.expression;
697
+ }
698
+ //#endregion
699
+ //#region src/rules/common/no-eager-translation.ts
700
+ const noEagerTranslation = {
701
+ meta: {
702
+ type: "problem",
703
+ docs: { description: "警告不会随语言切换更新的提前求值翻译结果" },
704
+ schema: [{
705
+ type: "object",
706
+ properties: {
707
+ tsconfigPath: { type: "string" },
708
+ autoImport: { anyOf: [{ type: "boolean" }, {
709
+ type: "array",
710
+ items: { enum: [
711
+ "t",
712
+ "tRef",
713
+ "tComputed",
714
+ "useI18n"
715
+ ] },
716
+ uniqueItems: true
717
+ }] },
718
+ framework: { enum: ["vue"] }
719
+ },
720
+ additionalProperties: false
721
+ }],
722
+ messages: { eagerTranslation: diagnosticMessage("初始化期间保存的 t() 结果不会随语言切换更新。请改为函数或 Getter,在使用时调用 t();Vue setup 中使用 tRef(),纯 Options API 的 computed 使用 tComputed()。", "A t() result stored during initialization will not update when the language changes. Evaluate it lazily in a function or getter; use tRef() in Vue setup and tComputed() in pure Options API computed.") }
723
+ },
724
+ create(context) {
725
+ const options = context.options[0] ?? {};
726
+ const isVueSfc = context.filename.toLowerCase().endsWith(".vue");
727
+ const candidates = [];
728
+ return {
729
+ CallExpression(node) {
730
+ candidates.push({
731
+ kind: "call",
732
+ node
733
+ });
734
+ },
735
+ TaggedTemplateExpression(node) {
736
+ candidates.push({
737
+ kind: "tagged-template",
738
+ node
739
+ });
740
+ },
741
+ "Program:exit"(program) {
742
+ let matches;
743
+ try {
744
+ matches = matchTranslationCalls(context, options, candidates);
745
+ } catch (error) {
746
+ reportAnalysisFailureOnce(context, program, error);
747
+ return;
748
+ }
749
+ for (const { call, node } of matches) {
750
+ if (call.origin === "vue-ref" || call.origin === "vue-computed") continue;
751
+ if (!storesOutsideFunction(node, context, isVueSfc, options.framework === "vue")) continue;
752
+ context.report({
753
+ node,
754
+ messageId: "eagerTranslation"
755
+ });
756
+ }
757
+ }
758
+ };
759
+ }
760
+ };
761
+ function storesOutsideFunction(node, context, isVueSfc, isVueFramework) {
762
+ let current = node;
763
+ let storesResult = false;
764
+ while (current.parent) {
765
+ const parent = current.parent;
766
+ if (isFunctionNode(parent)) {
767
+ const returnsExpression = parent.type === "ArrowFunctionExpression" && parent.body === current;
768
+ return (isVueComponentSetup(parent, context, isVueSfc) || isDirectOptionsFunction$1(parent, context, "data", isVueSfc || isVueFramework)) && (storesResult || returnsExpression);
769
+ }
770
+ if (storesTranslationResult(parent, current)) storesResult = true;
771
+ if (parent.type === "Program") return storesResult;
772
+ current = parent;
773
+ }
774
+ return storesResult;
775
+ }
776
+ function storesTranslationResult(parent, child) {
777
+ if (parent.type === "AccessorProperty" && parent.value === child) return true;
778
+ switch (parent.type) {
779
+ case "VariableDeclarator": return parent.init === child;
780
+ case "AssignmentExpression":
781
+ case "AssignmentPattern": return parent.right === child;
782
+ case "PropertyDefinition": return parent.value === child;
783
+ case "ReturnStatement": return parent.argument === child;
784
+ case "ExportDefaultDeclaration": return parent.declaration === child;
785
+ default: return false;
786
+ }
787
+ }
788
+ //#endregion
789
+ //#region src/rules/common/no-embedded-markup.ts
790
+ const noEmbeddedMarkup = {
791
+ meta: {
792
+ type: "suggestion",
793
+ docs: { description: "警告翻译源文中内嵌的静态 HTML 或 SVG 结构" },
794
+ schema: [{
795
+ type: "object",
796
+ properties: {
797
+ tsconfigPath: { type: "string" },
798
+ autoImport: { anyOf: [{ type: "boolean" }, {
799
+ type: "array",
800
+ items: { enum: [
801
+ "t",
802
+ "tRef",
803
+ "tComputed",
804
+ "useI18n"
805
+ ] },
806
+ uniqueItems: true
807
+ }] }
808
+ },
809
+ additionalProperties: false
810
+ }],
811
+ messages: { embeddedMarkup: diagnosticMessage("翻译源文包含静态 HTML 或 SVG 结构。请保留完整的自然语言,并将 markup 移出翻译调用或作为占位符传入。", "The translation source contains static HTML or SVG structure. Keep the complete natural-language message, and move markup outside the translation call or pass it as a placeholder.") }
812
+ },
813
+ create(context) {
814
+ const options = context.options[0] ?? {};
815
+ return { "Program:exit"(program) {
816
+ let messages;
817
+ try {
818
+ messages = analyzeTranslationMessages(context, options);
819
+ } catch (error) {
820
+ reportAnalysisFailureOnce(context, program, error);
821
+ return;
822
+ }
823
+ const reported = /* @__PURE__ */ new Set();
824
+ for (const message of messages) {
825
+ if (!containsEmbeddedMarkup(message.source)) continue;
826
+ for (const location of message.locations) {
827
+ const key = `${location.line}:${location.column}`;
828
+ if (reported.has(key)) continue;
829
+ reported.add(key);
830
+ context.report({
831
+ node: program,
832
+ loc: {
833
+ start: location,
834
+ end: {
835
+ line: location.line,
836
+ column: location.column + 1
837
+ }
838
+ },
839
+ messageId: "embeddedMarkup"
840
+ });
841
+ }
842
+ }
843
+ } };
844
+ }
845
+ };
846
+ function containsEmbeddedMarkup(source) {
847
+ if (!source.includes("<")) return false;
848
+ const options = { sourceCodeLocationInfo: true };
849
+ return containsLocatedMarkup(parseFragment(source, options)) || containsLocatedMarkup(parse(source, options));
850
+ }
851
+ function containsLocatedMarkup(parent) {
852
+ return parent.childNodes.some((node) => {
853
+ if ((defaultTreeAdapter.isElementNode(node) || defaultTreeAdapter.isCommentNode(node)) && node.sourceCodeLocation) return true;
854
+ return defaultTreeAdapter.isElementNode(node) && containsLocatedMarkup(node);
855
+ });
856
+ }
857
+ //#endregion
858
+ //#region src/rules/common/no-redundant-auto-import.ts
859
+ const RUNTIME_MODULE$2 = "virtual:ai-i18n";
860
+ const noRedundantAutoImport = {
861
+ meta: {
862
+ type: "suggestion",
863
+ docs: { description: "禁止显式导入已由 ai-i18n autoImport 注入的 Runtime API" },
864
+ fixable: "code",
865
+ schema: [{
866
+ type: "object",
867
+ properties: { autoImport: {
868
+ type: "array",
869
+ items: { enum: ALL_AUTO_IMPORT_APIS },
870
+ minItems: 1,
871
+ uniqueItems: true
872
+ } },
873
+ required: ["autoImport"],
874
+ additionalProperties: false
875
+ }],
876
+ messages: { redundantImport: diagnosticMessage("ai-i18n autoImport 已注入以下 API:{{names}}。请删除来自 virtual:ai-i18n 的冗余导入。", "ai-i18n autoImport injects these APIs: {{names}}. Remove the redundant imports from virtual:ai-i18n.") }
877
+ },
878
+ create(context) {
879
+ const enabled = new Set(context.options[0]?.autoImport ?? []);
880
+ return { ImportDeclaration(rawNode) {
881
+ const node = rawNode;
882
+ if (node.importKind === "type" || node.source.value !== RUNTIME_MODULE$2) return;
883
+ const named = node.specifiers.filter((specifier) => specifier.type === "ImportSpecifier");
884
+ const redundant = named.filter((specifier) => {
885
+ const imported = importedName(specifier);
886
+ return specifier.importKind !== "type" && imported !== null && imported === specifier.local.name && enabled.has(imported);
887
+ });
888
+ if (!redundant.length) return;
889
+ const names = redundant.map((specifier) => importedName(specifier)).filter((name) => name !== null);
890
+ const fix = createFix(context, node, named, redundant);
891
+ context.report({
892
+ node,
893
+ messageId: "redundantImport",
894
+ data: { names: names.join(", ") },
895
+ fix
896
+ });
897
+ } };
898
+ }
899
+ };
900
+ function importedName(specifier) {
901
+ if (specifier.imported.type === "Identifier" && "name" in specifier.imported) return specifier.imported.name;
902
+ return typeof specifier.imported.value === "string" ? specifier.imported.value : null;
903
+ }
904
+ function createFix(context, node, named, redundant) {
905
+ if (context.sourceCode.getCommentsInside(node).length) return void 0;
906
+ const redundantSet = new Set(redundant);
907
+ if (!node.specifiers.filter((specifier) => !redundantSet.has(specifier)).length) return (fixer) => fixer.remove(node);
908
+ const remainingNamed = named.filter((specifier) => !redundantSet.has(specifier));
909
+ const firstNamed = named[0];
910
+ const lastNamed = named.at(-1);
911
+ if (!firstNamed || !lastNamed) return void 0;
912
+ const openBrace = context.sourceCode.getTokenBefore(firstNamed);
913
+ const closeBrace = context.sourceCode.getTokenAfter(lastNamed);
914
+ if (openBrace?.value !== "{" || closeBrace?.value !== "}") return void 0;
915
+ if (remainingNamed.length) {
916
+ const [, openEnd] = context.sourceCode.getRange(openBrace);
917
+ const [closeStart] = context.sourceCode.getRange(closeBrace);
918
+ const replacement = remainingNamed.map((specifier) => context.sourceCode.getText(specifier)).join(", ");
919
+ return (fixer) => fixer.replaceTextRange([openEnd, closeStart], ` ${replacement} `);
920
+ }
921
+ const beforeOpen = context.sourceCode.getTokenBefore(openBrace);
922
+ if (beforeOpen?.value !== ",") return void 0;
923
+ const [commaStart] = context.sourceCode.getRange(beforeOpen);
924
+ const [, closeEnd] = context.sourceCode.getRange(closeBrace);
925
+ return (fixer) => fixer.removeRange([commaStart, closeEnd]);
926
+ }
927
+ //#endregion
928
+ //#region src/rules/vue/runtime-state.ts
929
+ function misplacedI18nComputed(node, context, inTemplate, isVueSfc) {
930
+ if (inTemplate || isVueScriptSetupNode(node, context)) return true;
931
+ if (vueOptionsSection(node, context) !== null) return true;
932
+ const owner = nearestFunction(node);
933
+ return Boolean(owner && isVueComponentSetup(owner, context, isVueSfc));
934
+ }
935
+ function vueInitializationSnapshotKind(node, owner, context, isVueSfc) {
936
+ const kind = isVueComponentSetup(owner, context, isVueSfc) ? "setup" : isDirectOptionsFunction$1(owner, context, "data") ? "data" : null;
937
+ if (!kind || !storesResultBeforeOwner(node, owner)) return null;
938
+ return kind;
939
+ }
940
+ function reportVueInitializationSnapshot(context, node, api, kind) {
941
+ context.report({
942
+ node,
943
+ messageId: kind === "data" ? "optionsDataSnapshot" : "vueSetupSnapshot",
944
+ data: {
945
+ api,
946
+ replacement: api === "getLang" ? "currentLang" : "langLoadState"
947
+ }
948
+ });
949
+ }
950
+ function storesResultBeforeOwner(node, owner) {
951
+ let current = node;
952
+ let storesResult = false;
953
+ while (current.parent && current.parent !== owner) {
954
+ const parent = current.parent;
955
+ if (parent.type === "VariableDeclarator" && parent.init === current || (parent.type === "AssignmentExpression" || parent.type === "AssignmentPattern") && parent.right === current || parent.type === "PropertyDefinition" && parent.value === current || parent.type === "ReturnStatement" && parent.argument === current) storesResult = true;
956
+ current = parent;
957
+ }
958
+ return storesResult || owner.type === "ArrowFunctionExpression" && owner.body === current;
177
959
  }
178
960
  //#endregion
179
- //#region src/rules/t-static-args.ts
961
+ //#region src/rules/common/no-unsubscribed-runtime-state.ts
962
+ const RUNTIME_MODULE$1 = "virtual:ai-i18n";
963
+ const STATE_APIS = ["getLang", "getLangLoadState"];
964
+ const VUE_COMPUTED_API = "i18nComputed";
965
+ const TRACKED_APIS = [...STATE_APIS, VUE_COMPUTED_API];
966
+ const noUnsubscribedRuntimeState = {
967
+ meta: {
968
+ type: "problem",
969
+ docs: { description: "检查初始化、模块和组件渲染路径中的 Runtime 状态快照" },
970
+ schema: [{
971
+ type: "object",
972
+ properties: {
973
+ autoImport: { anyOf: [{ type: "boolean" }, {
974
+ type: "array",
975
+ items: { enum: TRACKED_APIS },
976
+ uniqueItems: true
977
+ }] },
978
+ framework: { enum: ["react", "vue"] }
979
+ },
980
+ additionalProperties: false
981
+ }],
982
+ messages: {
983
+ moduleSnapshot: diagnosticMessage("模块顶层调用 {{api}}() 只会保存初始化时的状态快照,后续语言状态变化不会更新该值。请在需要时调用,或通过 subscribe() / useI18n() 建立订阅。", "Calling {{api}}() at module scope stores only the initialization snapshot, which will not update after language state changes. Read it when needed, or subscribe through subscribe() / useI18n()."),
984
+ renderSnapshot: diagnosticMessage("组件渲染期间调用 {{api}}() 不会订阅语言状态。请使用 useI18n() 返回的 {{replacement}}。", "Calling {{api}}() while rendering a component does not subscribe to language state. Use {{replacement}} from useI18n()."),
985
+ vueRenderSnapshot: diagnosticMessage("Vue 渲染期间调用 {{api}}() 不会订阅语言状态。Composition API 请使用 useI18n() 返回的 {{replacement}};纯 Options API 请在 computed 中展开 ...i18nComputed() 后使用 {{replacement}}。", "Calling {{api}}() while rendering Vue does not subscribe to language state. With the Composition API, use {{replacement}} from useI18n(); with the pure Options API, spread ...i18nComputed() into computed and use {{replacement}}."),
986
+ optionsComputedSnapshot: diagnosticMessage("纯 Options API 的 computed 中调用 {{api}}() 仍然只会读取快照。请改为在 computed 中展开 ...i18nComputed(),并直接使用响应式的 {{replacement}}。", "Calling {{api}}() inside a pure Options API computed getter still reads only a snapshot. Spread ...i18nComputed() into computed and use the reactive {{replacement}} instead."),
987
+ vueSetupSnapshot: diagnosticMessage("Vue setup 中保存 {{api}}() 只会保留初始化快照。请使用 useI18n() 返回的响应式 {{replacement}}。", "Storing {{api}}() in Vue setup keeps only the initialization snapshot. Use the reactive {{replacement}} returned by useI18n()."),
988
+ optionsDataSnapshot: diagnosticMessage("纯 Options API 的 data 中保存 {{api}}() 只会保留初始化快照。请在 computed 中展开 ...i18nComputed(),并使用响应式的 {{replacement}}。", "Storing {{api}}() in pure Options API data keeps only the initialization snapshot. Spread ...i18nComputed() into computed and use the reactive {{replacement}}."),
989
+ misplacedI18nComputed: diagnosticMessage("i18nComputed() 只应直接展开到纯 Options API 的 computed,例如 computed: { ...i18nComputed() }。", "Spread i18nComputed() directly into pure Options API computed, for example computed: { ...i18nComputed() }.")
990
+ }
991
+ },
992
+ create(context) {
993
+ const options = context.options[0] ?? {};
994
+ const autoImports = normalizeAutoImports(options.autoImport);
995
+ const importedBindings = /* @__PURE__ */ new Map();
996
+ const jsxOwners = /* @__PURE__ */ new Set();
997
+ const templateCalls = /* @__PURE__ */ new Set();
998
+ const candidates = [];
999
+ const isVueSfc = context.filename.toLowerCase().endsWith(".vue");
1000
+ const collectJsxOwner = (node) => {
1001
+ const owner = nearestFunction(node);
1002
+ if (owner) jsxOwners.add(owner);
1003
+ };
1004
+ const collectCall = (node) => {
1005
+ candidates.push(node);
1006
+ };
1007
+ const scriptVisitor = {
1008
+ CallExpression: collectCall,
1009
+ ImportDeclaration(node) {
1010
+ if (node.source.value !== RUNTIME_MODULE$1) return;
1011
+ for (const specifier of node.specifiers) {
1012
+ if (specifier.type !== "ImportSpecifier") continue;
1013
+ const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
1014
+ if (TRACKED_APIS.includes(imported)) importedBindings.set(specifier.local.name, imported);
1015
+ }
1016
+ },
1017
+ JSXElement: collectJsxOwner,
1018
+ JSXFragment: collectJsxOwner,
1019
+ "Program:exit"() {
1020
+ for (const node of candidates) {
1021
+ const api = runtimeStateApi(context, node, autoImports, importedBindings, templateCalls.has(node));
1022
+ if (!api) continue;
1023
+ if (api === VUE_COMPUTED_API) {
1024
+ if (options.framework === "vue" && !isOptionsComputedSpread(node, context) && misplacedI18nComputed(node, context, templateCalls.has(node), isVueSfc)) context.report({
1025
+ node,
1026
+ messageId: "misplacedI18nComputed"
1027
+ });
1028
+ continue;
1029
+ }
1030
+ if (templateCalls.has(node)) {
1031
+ if (!isVueTemplateEventHandler(node)) reportRenderSnapshot(context, node, api, true);
1032
+ continue;
1033
+ }
1034
+ const owner = nearestFunction(node);
1035
+ if (!owner) {
1036
+ context.report({
1037
+ node,
1038
+ messageId: "moduleSnapshot",
1039
+ data: { api }
1040
+ });
1041
+ continue;
1042
+ }
1043
+ if (options.framework === "vue" && isInsideOptionsComputedGetter(node, context)) {
1044
+ reportOptionsComputedSnapshot(context, node, api);
1045
+ continue;
1046
+ }
1047
+ const initializationKind = options.framework === "vue" ? vueInitializationSnapshotKind(node, owner, context, isVueSfc) : null;
1048
+ if (initializationKind) {
1049
+ reportVueInitializationSnapshot(context, node, api, initializationKind);
1050
+ continue;
1051
+ }
1052
+ if (jsxOwners.has(owner) && !isImmediateConsoleEffect(node)) reportRenderSnapshot(context, node, api, options.framework === "vue");
1053
+ }
1054
+ }
1055
+ };
1056
+ const parserServices = context.sourceCode.parserServices;
1057
+ if (!parserServices.defineTemplateBodyVisitor) return scriptVisitor;
1058
+ return parserServices.defineTemplateBodyVisitor({ CallExpression(node) {
1059
+ candidates.push(node);
1060
+ templateCalls.add(node);
1061
+ } }, scriptVisitor, { templateBodyTriggerSelector: "Program" });
1062
+ }
1063
+ };
1064
+ function normalizeAutoImports(option) {
1065
+ if (option === true) return new Set(TRACKED_APIS);
1066
+ if (!option) return /* @__PURE__ */ new Set();
1067
+ return new Set(option);
1068
+ }
1069
+ function runtimeStateApi(context, rawNode, autoImports, importedBindings, inTemplate) {
1070
+ if (rawNode.type !== "CallExpression") return null;
1071
+ const callee = rawNode.callee;
1072
+ if (callee.type !== "Identifier") return null;
1073
+ if (inTemplate) {
1074
+ if (isTemplateLocalIdentifier(callee)) return null;
1075
+ const imported = importedBindings.get(callee.name);
1076
+ if (imported) return imported;
1077
+ if (hasTopLevelScriptBinding(context, callee.name)) return null;
1078
+ return autoImports.has(callee.name) ? callee.name : null;
1079
+ }
1080
+ const variable = findVariable$1(context.sourceCode.getScope(callee), callee.name);
1081
+ if (variable?.defs.length) return importedRuntimeStateApi(variable);
1082
+ return autoImports.has(callee.name) ? callee.name : null;
1083
+ }
1084
+ function importedRuntimeStateApi(variable) {
1085
+ for (const definition of variable.defs) {
1086
+ if (definition.type !== "ImportBinding") continue;
1087
+ const specifier = definition.node;
1088
+ const declaration = specifier.parent;
1089
+ if (specifier.type !== "ImportSpecifier" || declaration?.type !== "ImportDeclaration" || declaration.source.value !== RUNTIME_MODULE$1) continue;
1090
+ const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
1091
+ return TRACKED_APIS.includes(imported) ? imported : null;
1092
+ }
1093
+ return null;
1094
+ }
1095
+ function hasTopLevelScriptBinding(context, name) {
1096
+ return context.sourceCode.scopeManager.scopes.some((scope) => (scope.type === "global" || scope.type === "module") && scope.variables.some((variable) => variable.name === name && variable.defs.length > 0));
1097
+ }
1098
+ function isTemplateLocalIdentifier(identifier) {
1099
+ let current = identifier;
1100
+ while (current) {
1101
+ if (current.type === "VExpressionContainer") return current.references.some((reference) => reference.id === identifier && reference.variable !== null);
1102
+ current = current.parent;
1103
+ }
1104
+ return false;
1105
+ }
1106
+ function findVariable$1(scope, name) {
1107
+ let current = scope;
1108
+ while (current) {
1109
+ const variable = current.variables.find((item) => item.name === name);
1110
+ if (variable) return variable;
1111
+ current = current.upper;
1112
+ }
1113
+ return null;
1114
+ }
1115
+ function reportRenderSnapshot(context, node, api, isVue) {
1116
+ context.report({
1117
+ node,
1118
+ messageId: isVue ? "vueRenderSnapshot" : "renderSnapshot",
1119
+ data: {
1120
+ api,
1121
+ replacement: api === "getLang" ? "currentLang" : "langLoadState"
1122
+ }
1123
+ });
1124
+ }
1125
+ function reportOptionsComputedSnapshot(context, node, api) {
1126
+ context.report({
1127
+ node,
1128
+ messageId: "optionsComputedSnapshot",
1129
+ data: {
1130
+ api,
1131
+ replacement: api === "getLang" ? "currentLang" : "langLoadState"
1132
+ }
1133
+ });
1134
+ }
1135
+ //#endregion
1136
+ //#region src/rules/react/no-unsubscribed-t.ts
1137
+ function reportReactUnsubscribedT(context, call, node, jsxOwners) {
1138
+ if (call.origin !== "runtime") return;
1139
+ const owner = nearestFunction(node);
1140
+ if (!owner || !jsxOwners.has(owner) || isImmediateConsoleEffect(node)) return;
1141
+ context.report({
1142
+ node,
1143
+ messageId: "unsubscribedT"
1144
+ });
1145
+ }
1146
+ //#endregion
1147
+ //#region src/rules/vue/instance-member.ts
1148
+ const RUNTIME_MODULE = "virtual:ai-i18n";
1149
+ const TRANSPARENT_EXPRESSIONS = /* @__PURE__ */ new Set([
1150
+ "TSAsExpression",
1151
+ "TSSatisfiesExpression",
1152
+ "TSNonNullExpression",
1153
+ "TypeCastExpression"
1154
+ ]);
1155
+ const STATIC_INSTANCE_SOURCES = /* @__PURE__ */ new Set([
1156
+ "computed",
1157
+ "inject",
1158
+ "props"
1159
+ ]);
1160
+ const RETURNED_INSTANCE_SOURCES = /* @__PURE__ */ new Set(["data", "setup"]);
1161
+ const UNCERTAIN_INSTANCE_SOURCES = /* @__PURE__ */ new Set(["extends", "mixins"]);
1162
+ function resolveVueInstanceMemberOrigin(context, node, name, inTemplate) {
1163
+ const options = inTemplate ? defaultComponentOptions(context) : enclosingComponentOptions(node, context);
1164
+ if (options === "unknown") return "unknown";
1165
+ if (!options) return inTemplate ? "missing" : "not-component";
1166
+ if (!inTemplate && !usesComponentThis(node, options)) return "not-component";
1167
+ return resolveOptionsMember(options, name, context);
1168
+ }
1169
+ function resolveOptionsMember(options, name, context) {
1170
+ let methods = null;
1171
+ let localSource = false;
1172
+ let uncertainSource = false;
1173
+ for (const entry of options.properties ?? []) {
1174
+ if (entry.type === "SpreadElement") {
1175
+ methods = "unknown";
1176
+ uncertainSource = true;
1177
+ continue;
1178
+ }
1179
+ if (entry.type !== "Property") continue;
1180
+ const key = staticPropertyName(entry);
1181
+ if (key === null) {
1182
+ methods = "unknown";
1183
+ uncertainSource = true;
1184
+ continue;
1185
+ }
1186
+ if (key === "methods") {
1187
+ const value = unwrapExpression(entry.value);
1188
+ methods = value?.type === "ObjectExpression" ? value : "unknown";
1189
+ continue;
1190
+ }
1191
+ if (STATIC_INSTANCE_SOURCES.has(key)) {
1192
+ const origin = resolveStaticSourceMember(entry.value, name, context);
1193
+ if (origin === "local") localSource = true;
1194
+ if (origin === "unknown") uncertainSource = true;
1195
+ continue;
1196
+ }
1197
+ if (RETURNED_INSTANCE_SOURCES.has(key)) {
1198
+ const origin = resolveReturnedSourceMember(entry.value, name, context);
1199
+ if (origin === "local") localSource = true;
1200
+ if (origin === "unknown") uncertainSource = true;
1201
+ continue;
1202
+ }
1203
+ if (UNCERTAIN_INSTANCE_SOURCES.has(key)) uncertainSource = true;
1204
+ }
1205
+ if (methods === "unknown") return "unknown";
1206
+ const methodOrigin = methods ? resolveObjectMember(methods, name, context) : "missing";
1207
+ if (methodOrigin === "local" || localSource) return "local";
1208
+ if (methodOrigin === "unknown" || uncertainSource) return "unknown";
1209
+ return methodOrigin;
1210
+ }
1211
+ function resolveStaticSourceMember(rawValue, name, context) {
1212
+ const value = unwrapExpression(rawValue);
1213
+ if (!value) return "unknown";
1214
+ if (value.type === "ObjectExpression") {
1215
+ const origin = resolveObjectMember(value, name, context);
1216
+ return origin === "missing" ? "missing" : origin === "unknown" ? "unknown" : "local";
1217
+ }
1218
+ if (value.type !== "ArrayExpression") return "unknown";
1219
+ for (const element of value.elements ?? []) {
1220
+ if (propertyName(element) === name) return "local";
1221
+ if (element.type !== "Literal") return "unknown";
1222
+ }
1223
+ return "missing";
1224
+ }
1225
+ function resolveReturnedSourceMember(rawValue, name, context) {
1226
+ const fn = unwrapExpression(rawValue);
1227
+ if (!fn || fn.type !== "FunctionExpression" && fn.type !== "ArrowFunctionExpression") return "unknown";
1228
+ const rawBody = fn.body;
1229
+ if (!rawBody) return "unknown";
1230
+ if (rawBody.type === "ObjectExpression") return localObjectMemberOrigin(rawBody, name, context);
1231
+ if (rawBody.type !== "BlockStatement") return "unknown";
1232
+ const statements = rawBody.body ?? [];
1233
+ const returned = statements.at(-1);
1234
+ if (returned?.type !== "ReturnStatement" || statements.slice(0, -1).some((statement) => !isStraightLineStatement(statement))) return "unknown";
1235
+ const value = unwrapExpression(returned.argument);
1236
+ return value?.type === "ObjectExpression" ? localObjectMemberOrigin(value, name, context) : "unknown";
1237
+ }
1238
+ function localObjectMemberOrigin(object, name, context) {
1239
+ const origin = resolveObjectMember(object, name, context);
1240
+ return origin === "missing" ? "missing" : origin === "unknown" ? "unknown" : "local";
1241
+ }
1242
+ function isStraightLineStatement(node) {
1243
+ return node.type === "ExpressionStatement" || node.type === "FunctionDeclaration" || node.type === "VariableDeclaration";
1244
+ }
1245
+ function resolveObjectMember(object, name, context) {
1246
+ let origin = "missing";
1247
+ for (const entry of object.properties ?? []) {
1248
+ if (entry.type === "SpreadElement") {
1249
+ origin = "unknown";
1250
+ continue;
1251
+ }
1252
+ if (entry.type !== "Property") continue;
1253
+ const key = staticPropertyName(entry);
1254
+ if (key === null) {
1255
+ origin = "unknown";
1256
+ continue;
1257
+ }
1258
+ if (key !== name) continue;
1259
+ origin = methodValueOrigin(entry.value, context);
1260
+ }
1261
+ return origin;
1262
+ }
1263
+ function methodValueOrigin(rawValue, context) {
1264
+ const value = unwrapExpression(rawValue);
1265
+ if (!value) return "unknown";
1266
+ if (value.type !== "Identifier" || !value.name) return "local";
1267
+ const variable = findVariable(context, value);
1268
+ if (!variable) return "unknown";
1269
+ return variable.defs.some(isRuntimeTImport) ? "ai-i18n" : "local";
1270
+ }
1271
+ function findVariable(context, identifier) {
1272
+ let scope = context.sourceCode.getScope(identifier);
1273
+ while (scope) {
1274
+ const variable = scope.set.get(identifier.name);
1275
+ if (variable) return variable;
1276
+ scope = scope.upper;
1277
+ }
1278
+ return null;
1279
+ }
1280
+ function isRuntimeTImport(definition) {
1281
+ if (definition.type !== "ImportBinding") return false;
1282
+ const specifier = definition.node;
1283
+ const declaration = specifier.parent;
1284
+ return specifier.type === "ImportSpecifier" && declaration?.type === "ImportDeclaration" && declaration.source?.value === RUNTIME_MODULE && propertyName(specifier.imported) === "t";
1285
+ }
1286
+ function enclosingComponentOptions(node, context) {
1287
+ let current = node;
1288
+ while (current.parent) {
1289
+ current = current.parent;
1290
+ if (current.type === "ObjectExpression" && isComponentOptions(current, context)) return current;
1291
+ }
1292
+ return null;
1293
+ }
1294
+ function defaultComponentOptions(context) {
1295
+ const owner = context.sourceCode.ast.body?.find((entry) => entry.type === "ExportDefaultDeclaration");
1296
+ if (!owner) return null;
1297
+ const declaration = unwrapExpression(owner.declaration);
1298
+ if (!declaration) return "unknown";
1299
+ if (declaration.type === "ObjectExpression") return declaration;
1300
+ if (declaration.type !== "CallExpression" || !isImportedVueDefineComponent(declaration.callee, context)) return "unknown";
1301
+ const options = unwrapExpression(declaration.arguments?.[0]);
1302
+ return options?.type === "ObjectExpression" ? options : "unknown";
1303
+ }
1304
+ function isComponentOptions(options, context) {
1305
+ const value = unwrapParent(options);
1306
+ const owner = value.parent;
1307
+ if (owner?.type === "ExportDefaultDeclaration" && owner.declaration === value) return true;
1308
+ return owner?.type === "CallExpression" && owner.arguments?.[0] === value && isImportedVueDefineComponent(owner.callee, context);
1309
+ }
1310
+ function usesComponentThis(node, options) {
1311
+ let current = node;
1312
+ while (current.parent && current.parent !== options) {
1313
+ current = current.parent;
1314
+ if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return isDirectOptionsFunction(current, options);
1315
+ }
1316
+ return false;
1317
+ }
1318
+ function isDirectOptionsFunction(fn, options) {
1319
+ const property = fn.parent;
1320
+ if (property?.type !== "Property" || unwrapExpression(property.value) !== fn) return false;
1321
+ if (property.parent === options) return true;
1322
+ const section = property.parent;
1323
+ const sectionProperty = section?.parent;
1324
+ return section?.type === "ObjectExpression" && sectionProperty?.type === "Property" && unwrapExpression(sectionProperty.value) === section && sectionProperty.parent === options;
1325
+ }
1326
+ function staticPropertyName(property) {
1327
+ return propertyName(property.key) ?? null;
1328
+ }
1329
+ function unwrapExpression(node) {
1330
+ let value = node;
1331
+ while (value && TRANSPARENT_EXPRESSIONS.has(value.type)) value = value.expression;
1332
+ return value;
1333
+ }
1334
+ function unwrapParent(node) {
1335
+ let value = node;
1336
+ while (value.parent && TRANSPARENT_EXPRESSIONS.has(value.parent.type) && value.parent.expression === value) value = value.parent;
1337
+ return value;
1338
+ }
1339
+ //#endregion
1340
+ //#region src/rules/vue/no-unsubscribed-t.ts
1341
+ function reportVueTranslationLifecycle(context, call, node, templateNodes, jsxOwners) {
1342
+ if (call.origin === "vue-computed") {
1343
+ if (!isOptionsComputedValue(node, context)) context.report({
1344
+ node,
1345
+ messageId: "misplacedTComputed"
1346
+ });
1347
+ return true;
1348
+ }
1349
+ if (call.origin !== "vue-ref") return false;
1350
+ if (templateNodes.has(node)) {
1351
+ if (!isVueTemplateEventHandler(node)) context.report({
1352
+ node,
1353
+ messageId: "renderTRef"
1354
+ });
1355
+ return true;
1356
+ }
1357
+ const section = vueOptionsSection(node, context);
1358
+ if (section === "computed" || section === "data" || section === "methods") {
1359
+ context.report({
1360
+ node,
1361
+ messageId: "optionsTRef"
1362
+ });
1363
+ return true;
1364
+ }
1365
+ if (section === "render") {
1366
+ context.report({
1367
+ node,
1368
+ messageId: "renderTRef"
1369
+ });
1370
+ return true;
1371
+ }
1372
+ const owner = nearestFunction(node);
1373
+ if (owner && jsxOwners.has(owner)) context.report({
1374
+ node,
1375
+ messageId: "renderTRef"
1376
+ });
1377
+ return true;
1378
+ }
1379
+ function reportUnsupportedVueInstanceTranslation(context, node, isVueFramework, inTemplate) {
1380
+ if (!isVueFramework) return;
1381
+ const name = vueInstanceTranslationMemberName(node);
1382
+ if (!name) return;
1383
+ const origin = resolveVueInstanceMemberOrigin(context, node, name, inTemplate);
1384
+ if (origin === "local" || origin === "unknown" || origin === "not-component") return;
1385
+ context.report({
1386
+ node,
1387
+ messageId: "unsupportedInstanceTranslation"
1388
+ });
1389
+ }
1390
+ function vueInstanceTranslationMemberName(node) {
1391
+ if (node.type !== "MemberExpression" || node.object.type !== "ThisExpression") return null;
1392
+ const name = !node.computed && node.property.type === "Identifier" ? node.property.name : node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : null;
1393
+ return name === "t" || name === "$t" ? name : null;
1394
+ }
1395
+ //#endregion
1396
+ //#region src/rules/common/no-unsubscribed-t.ts
1397
+ const noUnsubscribedT = {
1398
+ meta: {
1399
+ type: "problem",
1400
+ docs: { description: "检查组件渲染路径中的翻译 API 生命周期" },
1401
+ schema: [{
1402
+ type: "object",
1403
+ properties: {
1404
+ tsconfigPath: { type: "string" },
1405
+ autoImport: { anyOf: [{ type: "boolean" }, {
1406
+ type: "array",
1407
+ items: { enum: [
1408
+ "t",
1409
+ "tRef",
1410
+ "tComputed",
1411
+ "useI18n"
1412
+ ] },
1413
+ uniqueItems: true
1414
+ }] },
1415
+ framework: { enum: ["react", "vue"] }
1416
+ },
1417
+ additionalProperties: false
1418
+ }],
1419
+ messages: {
1420
+ unsubscribedT: diagnosticMessage("组件渲染期间使用顶层 t 不会订阅语言状态,语言切换不会主动刷新,缓存的渲染结果也可能继续使用旧译文。请使用 useI18n() 返回的 t。", "Top-level t does not subscribe the component to language updates, so language changes do not trigger a render and cached render results may remain stale. Use the t returned by useI18n()."),
1421
+ renderTRef: diagnosticMessage("不要在组件渲染或 template 中调用 tRef(),否则每次渲染都会创建新的 computed。请在 setup 中创建一次并使用返回的 Ref;Vue 渲染函数中请直接调用 t。", "Do not call tRef() during component rendering or in a template because each render creates a new computed. Create it once in setup and use the returned Ref; call t directly in Vue render functions."),
1422
+ misplacedTComputed: diagnosticMessage("tComputed() 只应直接作为纯 Options API 的 computed 属性值使用,例如 computed: { label: tComputed(\"保存\") }。setup 中请使用 tRef(),template 或 render 中请直接使用 t()。", "Use tComputed() only as the direct value of a pure Options API computed property, for example computed: { label: tComputed(\"Save\") }. Use tRef() in setup, and call t() directly in templates or render functions."),
1423
+ optionsTRef: diagnosticMessage("不要在纯 Options API 的 computed、data 或 methods 中调用 tRef()。computed 请直接使用 tComputed(),methods 请在执行时调用 t();tRef() 仅用于 setup 或 composable。", "Do not call tRef() in pure Options API computed, data, or methods. Use tComputed() directly in computed properties, call t() when a method runs, and reserve tRef() for setup or composables."),
1424
+ unsupportedInstanceTranslation: diagnosticMessage("ai-i18n 不支持把 Vue 组件实例成员 this.t / this.$t 当作翻译 API。请直接调用词法作用域中的 t();开启自动导入时无需 import,关闭时请从 virtual:ai-i18n 显式导入。", "ai-i18n does not support Vue instance members this.t or this.$t as translation APIs. Call lexical t() directly; no import is needed with auto import enabled, otherwise import it from virtual:ai-i18n.")
1425
+ }
1426
+ },
1427
+ create(context) {
1428
+ const options = context.options[0] ?? {};
1429
+ const candidates = [];
1430
+ const jsxOwners = /* @__PURE__ */ new Set();
1431
+ const templateNodes = /* @__PURE__ */ new Set();
1432
+ const collectJsxOwner = (node) => {
1433
+ const owner = nearestFunction(node);
1434
+ if (owner) jsxOwners.add(owner);
1435
+ };
1436
+ const scriptVisitor = {
1437
+ CallExpression(node) {
1438
+ candidates.push({
1439
+ kind: "call",
1440
+ node
1441
+ });
1442
+ },
1443
+ TaggedTemplateExpression(node) {
1444
+ candidates.push({
1445
+ kind: "tagged-template",
1446
+ node
1447
+ });
1448
+ },
1449
+ MemberExpression(node) {
1450
+ reportUnsupportedVueInstanceTranslation(context, node, options.framework === "vue", false);
1451
+ },
1452
+ JSXElement: collectJsxOwner,
1453
+ JSXFragment: collectJsxOwner,
1454
+ "Program:exit"(program) {
1455
+ let matches;
1456
+ try {
1457
+ matches = matchTranslationCalls(context, options, candidates);
1458
+ } catch (error) {
1459
+ reportAnalysisFailureOnce(context, program, error);
1460
+ return;
1461
+ }
1462
+ for (const { call, node } of matches) {
1463
+ if (reportVueTranslationLifecycle(context, call, node, templateNodes, jsxOwners)) continue;
1464
+ if (call.origin !== "runtime") continue;
1465
+ if (options.framework === "vue") continue;
1466
+ if (templateNodes.has(node)) continue;
1467
+ reportReactUnsubscribedT(context, call, node, jsxOwners);
1468
+ }
1469
+ }
1470
+ };
1471
+ const parserServices = context.sourceCode.parserServices;
1472
+ if (!parserServices.defineTemplateBodyVisitor) return scriptVisitor;
1473
+ return parserServices.defineTemplateBodyVisitor({
1474
+ CallExpression(node) {
1475
+ candidates.push({
1476
+ kind: "call",
1477
+ node
1478
+ });
1479
+ templateNodes.add(node);
1480
+ },
1481
+ TaggedTemplateExpression(node) {
1482
+ candidates.push({
1483
+ kind: "tagged-template",
1484
+ node
1485
+ });
1486
+ templateNodes.add(node);
1487
+ },
1488
+ MemberExpression(node) {
1489
+ reportUnsupportedVueInstanceTranslation(context, node, options.framework === "vue", true);
1490
+ }
1491
+ }, scriptVisitor, { templateBodyTriggerSelector: "Program" });
1492
+ }
1493
+ };
1494
+ //#endregion
1495
+ //#region src/rules/common/static-candidate-limit.ts
1496
+ const staticCandidateLimit = {
1497
+ meta: {
1498
+ type: "suggestion",
1499
+ docs: { description: "警告单个翻译调用展开的静态候选数量过多" },
1500
+ schema: [{
1501
+ type: "object",
1502
+ properties: {
1503
+ tsconfigPath: { type: "string" },
1504
+ autoImport: { anyOf: [{ type: "boolean" }, {
1505
+ type: "array",
1506
+ items: { enum: [
1507
+ "t",
1508
+ "tRef",
1509
+ "tComputed",
1510
+ "useI18n"
1511
+ ] },
1512
+ uniqueItems: true
1513
+ }] },
1514
+ maxStaticCandidates: {
1515
+ type: "integer",
1516
+ minimum: 1,
1517
+ maximum: Number.MAX_SAFE_INTEGER
1518
+ }
1519
+ },
1520
+ additionalProperties: false
1521
+ }],
1522
+ messages: { candidateLimit: "{{reason}}" }
1523
+ },
1524
+ create(context) {
1525
+ const options = context.options[0] ?? {};
1526
+ return { "Program:exit"(node) {
1527
+ try {
1528
+ const warnings = analyzeRuleContext(context, options, options.maxStaticCandidates ?? 1e3);
1529
+ for (const warning of warnings) {
1530
+ if (warning.code !== "static-candidate-limit") continue;
1531
+ context.report({
1532
+ node,
1533
+ loc: {
1534
+ start: warning,
1535
+ end: {
1536
+ line: warning.line,
1537
+ column: warning.column + 1
1538
+ }
1539
+ },
1540
+ messageId: "candidateLimit",
1541
+ data: { reason: warning.message }
1542
+ });
1543
+ }
1544
+ } catch (error) {
1545
+ reportAnalysisFailureOnce(context, node, error);
1546
+ }
1547
+ } };
1548
+ }
1549
+ };
1550
+ //#endregion
1551
+ //#region src/rules/common/t-static-args.ts
180
1552
  const tStaticArgs = {
181
1553
  meta: {
182
1554
  type: "problem",
183
- docs: { description: "要求 virtual:ai-i18n t() 参数可被静态提取" },
1555
+ docs: { description: "要求 virtual:ai-i18n 的翻译 API 参数可被静态提取" },
184
1556
  schema: [{
185
1557
  type: "object",
186
1558
  properties: {
187
1559
  tsconfigPath: { type: "string" },
188
- autoImport: { type: "boolean" }
1560
+ autoImport: { anyOf: [{ type: "boolean" }, {
1561
+ type: "array",
1562
+ items: { enum: [
1563
+ "t",
1564
+ "tRef",
1565
+ "tComputed",
1566
+ "useI18n"
1567
+ ] },
1568
+ uniqueItems: true
1569
+ }] }
189
1570
  },
190
1571
  additionalProperties: false
191
1572
  }],
192
1573
  messages: {
193
- analysisFailed: "ai-i18n 静态分析失败:{{reason}}",
194
- dynamicArg: "t() 参数无法静态提取,请使用字符串字面量、静态模板、条件表达式或可解析的 const 字符串。"
1574
+ analysisFailed: "{{reason}}",
1575
+ dynamicArg: "{{reason}}",
1576
+ invalidUsage: "{{reason}}"
195
1577
  }
196
1578
  },
197
1579
  create(context) {
@@ -199,26 +1581,14 @@ const tStaticArgs = {
199
1581
  return { "Program:exit"(node) {
200
1582
  let warnings;
201
1583
  try {
202
- const source = context.filename.endsWith(".vue") ? createVueAnalysisSource(context.sourceCode.text, context.filename, context.sourceCode.parserServices) : {
203
- code: context.sourceCode.text,
204
- lang: void 0,
205
- mapLocation: (location) => location
206
- };
207
- warnings = analyzeStaticArgs(source.code, context.filename, options.tsconfigPath, source.lang, options.autoImport);
208
- warnings = warnings.map((warning) => ({
209
- ...warning,
210
- ...source.mapLocation(warning)
211
- }));
1584
+ warnings = analyzeRuleContext(context, options);
212
1585
  } catch (error) {
213
- context.report({
214
- node,
215
- messageId: "analysisFailed",
216
- data: { reason: error instanceof Error ? error.message : String(error) }
217
- });
1586
+ reportAnalysisFailureOnce(context, node, error);
218
1587
  return;
219
1588
  }
220
1589
  for (const warning of warnings) {
221
1590
  const analysisFailed = warning.code === "parse-error";
1591
+ const invalidUsage = warning.code !== "dynamic-argument" && warning.code !== "unresolved-argument" && !analysisFailed;
222
1592
  context.report({
223
1593
  node,
224
1594
  loc: {
@@ -228,8 +1598,8 @@ const tStaticArgs = {
228
1598
  column: warning.column + 1
229
1599
  }
230
1600
  },
231
- messageId: analysisFailed ? "analysisFailed" : "dynamicArg",
232
- ...analysisFailed ? { data: { reason: warning.message } } : {}
1601
+ messageId: analysisFailed ? "analysisFailed" : invalidUsage ? "invalidUsage" : "dynamicArg",
1602
+ data: { reason: analysisFailed ? diagnosticMessage(`静态分析失败:${warning.message}`, `Static analysis failed: ${warning.message}`) : invalidUsage ? warning.message : diagnosticMessage("翻译调用的参数无法静态提取。source 请使用静态字符串,options 请使用只包含 comment 的静态对象。", "The translation-call arguments cannot be statically extracted. Use a static string for source and a static object containing only comment for options.") }
233
1603
  });
234
1604
  }
235
1605
  } };
@@ -244,39 +1614,129 @@ const plugin = {
244
1614
  version,
245
1615
  namespace: "ai-i18n"
246
1616
  },
247
- rules: { "t-static-args": tStaticArgs },
1617
+ rules: {
1618
+ "no-embedded-markup": noEmbeddedMarkup,
1619
+ "no-eager-translation": noEagerTranslation,
1620
+ "no-redundant-auto-import": noRedundantAutoImport,
1621
+ "no-unsubscribed-runtime-state": noUnsubscribedRuntimeState,
1622
+ "no-unsubscribed-t": noUnsubscribedT,
1623
+ "static-candidate-limit": staticCandidateLimit,
1624
+ "t-static-args": tStaticArgs
1625
+ },
248
1626
  configs: {}
249
1627
  };
250
1628
  plugin.configs.recommended = [{
251
- ignores: ["**/*.vue"],
1629
+ ignores: ["**/*.{vue,cjs,cts}"],
252
1630
  plugins: { "ai-i18n": plugin },
253
- rules: { "ai-i18n/t-static-args": "error" }
1631
+ languageOptions: { globals: { defineI18nMessages: "readonly" } },
1632
+ rules: {
1633
+ "ai-i18n/t-static-args": "error",
1634
+ "ai-i18n/no-embedded-markup": "warn",
1635
+ "ai-i18n/no-eager-translation": "warn",
1636
+ "ai-i18n/no-unsubscribed-runtime-state": "warn",
1637
+ "ai-i18n/no-unsubscribed-t": "warn",
1638
+ "ai-i18n/static-candidate-limit": "warn"
1639
+ }
254
1640
  }];
255
1641
  plugin.configs.vue = [{
256
- files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}"],
1642
+ files: ["**/*.{js,mjs,ts,mts,jsx,tsx,vue}"],
257
1643
  plugins: { "ai-i18n": plugin },
258
- languageOptions: { globals: { useI18n: "readonly" } },
259
- rules: { "ai-i18n/t-static-args": ["error", { autoImport: true }] }
1644
+ languageOptions: { globals: { defineI18nMessages: "readonly" } },
1645
+ rules: {
1646
+ "ai-i18n/t-static-args": "error",
1647
+ "ai-i18n/no-embedded-markup": "warn",
1648
+ "ai-i18n/no-eager-translation": ["warn", { framework: "vue" }],
1649
+ "ai-i18n/no-unsubscribed-runtime-state": ["warn", { framework: "vue" }],
1650
+ "ai-i18n/no-unsubscribed-t": ["warn", { framework: "vue" }],
1651
+ "ai-i18n/static-candidate-limit": "warn"
1652
+ }
260
1653
  }];
261
- plugin.configs.react = [{
262
- files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
1654
+ plugin.configs["vanilla-auto-import"] = [{
1655
+ files: ["**/*.{js,mjs,ts,mts}"],
263
1656
  plugins: { "ai-i18n": plugin },
264
- languageOptions: { globals: { useI18n: "readonly" } },
265
- rules: { "ai-i18n/t-static-args": ["error", { autoImport: true }] }
1657
+ languageOptions: { globals: {
1658
+ ...Object.fromEntries(RUNTIME_AUTO_IMPORTS.map((name) => [name, "readonly"])),
1659
+ defineI18nMessages: "readonly"
1660
+ } },
1661
+ rules: {
1662
+ "ai-i18n/t-static-args": ["error", { autoImport: ["t"] }],
1663
+ "ai-i18n/no-embedded-markup": ["warn", { autoImport: ["t"] }],
1664
+ "ai-i18n/no-eager-translation": ["warn", { autoImport: ["t"] }],
1665
+ "ai-i18n/no-unsubscribed-runtime-state": ["warn", { autoImport: ["getLang", "getLangLoadState"] }],
1666
+ "ai-i18n/static-candidate-limit": ["warn", { autoImport: ["t"] }]
1667
+ }
1668
+ }];
1669
+ plugin.configs["vue-auto-import"] = [{
1670
+ files: ["**/*.{js,mjs,ts,mts,jsx,tsx,vue}"],
1671
+ plugins: { "ai-i18n": plugin },
1672
+ languageOptions: { globals: {
1673
+ ...Object.fromEntries(VUE_AUTO_IMPORTS.map((name) => [name, "readonly"])),
1674
+ defineI18nMessages: "readonly"
1675
+ } },
1676
+ rules: {
1677
+ "ai-i18n/t-static-args": ["error", { autoImport: [
1678
+ "t",
1679
+ "tRef",
1680
+ "tComputed",
1681
+ "useI18n"
1682
+ ] }],
1683
+ "ai-i18n/no-embedded-markup": ["warn", { autoImport: [
1684
+ "t",
1685
+ "tRef",
1686
+ "tComputed",
1687
+ "useI18n"
1688
+ ] }],
1689
+ "ai-i18n/no-eager-translation": ["warn", {
1690
+ autoImport: [
1691
+ "t",
1692
+ "tRef",
1693
+ "tComputed",
1694
+ "useI18n"
1695
+ ],
1696
+ framework: "vue"
1697
+ }],
1698
+ "ai-i18n/no-unsubscribed-runtime-state": ["warn", {
1699
+ autoImport: [
1700
+ "getLang",
1701
+ "getLangLoadState",
1702
+ "i18nComputed"
1703
+ ],
1704
+ framework: "vue"
1705
+ }],
1706
+ "ai-i18n/no-unsubscribed-t": ["warn", {
1707
+ autoImport: [
1708
+ "t",
1709
+ "tRef",
1710
+ "tComputed",
1711
+ "useI18n"
1712
+ ],
1713
+ framework: "vue"
1714
+ }],
1715
+ "ai-i18n/static-candidate-limit": ["warn", { autoImport: [
1716
+ "t",
1717
+ "tRef",
1718
+ "tComputed",
1719
+ "useI18n"
1720
+ ] }]
1721
+ }
266
1722
  }];
267
- plugin.configs.vanilla = [{
268
- files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
1723
+ plugin.configs["react-auto-import"] = [{
1724
+ files: ["**/*.{js,mjs,ts,mts,jsx,tsx}"],
269
1725
  plugins: { "ai-i18n": plugin },
270
1726
  languageOptions: { globals: {
271
- t: "readonly",
272
- setLang: "readonly",
273
- getLang: "readonly",
274
- getLangs: "readonly",
275
- subscribe: "readonly"
1727
+ ...Object.fromEntries(REACT_AUTO_IMPORTS.map((name) => [name, "readonly"])),
1728
+ defineI18nMessages: "readonly"
276
1729
  } },
277
- rules: { "ai-i18n/t-static-args": ["error", { autoImport: true }] }
1730
+ rules: {
1731
+ "ai-i18n/t-static-args": ["error", { autoImport: ["t", "useI18n"] }],
1732
+ "ai-i18n/no-embedded-markup": ["warn", { autoImport: ["t", "useI18n"] }],
1733
+ "ai-i18n/no-eager-translation": ["warn", { autoImport: ["t", "useI18n"] }],
1734
+ "ai-i18n/no-unsubscribed-runtime-state": ["warn", { autoImport: ["getLang", "getLangLoadState"] }],
1735
+ "ai-i18n/no-unsubscribed-t": ["warn", { autoImport: ["t", "useI18n"] }],
1736
+ "ai-i18n/static-candidate-limit": ["warn", { autoImport: ["t", "useI18n"] }]
1737
+ }
278
1738
  }];
279
1739
  //#endregion
280
- export { plugin as default, tStaticArgs };
1740
+ export { plugin as default, noEagerTranslation, noEmbeddedMarkup, noRedundantAutoImport, noUnsubscribedRuntimeState, noUnsubscribedT, staticCandidateLimit, tStaticArgs };
281
1741
 
282
1742
  //# sourceMappingURL=index.js.map