feat: 默认放宽i18n检查

This commit is contained in:
Pan Qiancheng 2026-01-08 10:33:02 +08:00
parent c649d131e3
commit 23abd05811
3 changed files with 103 additions and 10 deletions

View File

@ -9,6 +9,8 @@ interface OakBuildChecksConfig {
locale?: {
checkI18nKeys?: boolean;
tFunctionModules?: string[];
checkTemplateLiterals?: boolean;
warnStringKeys?: boolean;
};
}
interface CustomDiagnostic {

View File

@ -14,6 +14,12 @@ const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap'];
const PROMISE_METHODS = ['then', 'catch', 'finally'];
const PROMISE_STATIC_METHODS = ['all', 'race', 'allSettled', 'any'];
const LOCALE_FILE_NAMES = ['zh_CN.json', 'zh-CN.json'];
// 需要忽略的 i18n key 字段模式
const IGNORED_I18N_KEY_PATTERNS = {
startsWith: ['$', '$$'], // 以 $ 或 $$ 开头的字段
endsWith: ['Id', 'id'], // 以 Id 或 id 结尾的字段
exact: ['id', 'seq', 'createAt', 'updateAt', 'deleteAt'] // 精确匹配的字段名
};
// 判断是否是函数类声明
const isFunctionLikeDeclaration = (node) => {
// FunctionDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | FunctionExpression | ArrowFunction;
@ -326,6 +332,8 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
const enableTCheck = customConfig.locale?.checkI18nKeys !== false;
console.log(`Custom AsyncContext checks are ${enableAsyncContextCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`);
console.log(`Custom i18n key checks are ${enableTCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`);
const checkTemplateLiterals = customConfig?.locale?.checkTemplateLiterals ?? false;
const warnStringKeys = customConfig?.locale?.warnStringKeys ?? false;
// 数据结构定义
const functionsWithContextCalls = new Set();
const functionDeclarations = new Map();
@ -1737,8 +1745,10 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
// 其他表达式(变量、属性访问等)
const analysis = analyzeExpressionType(expr, typeChecker);
if (analysis.isLiteralUnion) {
// 新增:过滤掉需要忽略的字段值
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
return [{
values: analysis.literalValues,
values: filteredValues.length > 0 ? filteredValues : null,
isLiteral: false,
expression: expr,
analysis
@ -1941,6 +1951,24 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
generate(0, head);
return results;
};
/**
* 检查字段名是否匹配忽略模式
*/
const shouldIgnoreI18nKeyField = (fieldName) => {
// 检查精确匹配
if (IGNORED_I18N_KEY_PATTERNS.exact.includes(fieldName)) {
return true;
}
// 检查前缀匹配
if (IGNORED_I18N_KEY_PATTERNS.startsWith.some(prefix => fieldName.startsWith(prefix))) {
return true;
}
// 检查后缀匹配
if (IGNORED_I18N_KEY_PATTERNS.endsWith.some(suffix => fieldName.endsWith(suffix))) {
return true;
}
return false;
};
/**
* 检查模板表达式中的 i18n key
*/
@ -1956,9 +1984,10 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
spanAnalyses.push({ span, analysis });
if (analysis.isLiteralUnion) {
// 字面量联合类型,可以检查
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
spans.push({
text: span.literal.text,
values: analysis.literalValues
values: filteredValues.length > 0 ? filteredValues : null // 如果过滤后为空,设为 null
});
}
else if (analysis.isString) {
@ -2194,6 +2223,11 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
const varName = identifier.getText(sourceFile);
if (analysis.isLiteralUnion) {
// 字面量联合类型,检查所有可能的值
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
// 如果过滤后没有值了,直接返回
if (filteredValues.length === 0) {
return;
}
const missingKeys = [];
const foundKeys = [];
for (const value of analysis.literalValues) {
@ -2247,7 +2281,7 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
}
else if (analysis.isString) {
// string 类型,无法检查
addDiagnostic(callNode, `变量 ${varName} 的类型为 string范围太大无法检查 i18n key 是否存在。`, 9203, {
warnStringKeys && addDiagnostic(callNode, `变量 ${varName} 的类型为 string范围太大无法检查 i18n key 是否存在。`, 9203, {
reason: 'UncheckableVariable',
reasonDetails: [
`✘ 变量 ${varName} 的类型为 string`,
@ -2407,7 +2441,7 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
}
else if (ts.isTemplateExpression(firstArg)) {
// 模板字符串
checkTemplateExpressionKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString);
checkTemplateLiterals && checkTemplateExpressionKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString);
}
else if (ts.isNoSubstitutionTemplateLiteral(firstArg)) {
// 无替换的模板字面量 `key`
@ -2421,7 +2455,7 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
else if (ts.isBinaryExpression(firstArg) &&
firstArg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
// 字符串拼接表达式
checkBinaryExpressionKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString);
warnStringKeys && checkBinaryExpressionKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString);
}
else if (ts.isPropertyAccessExpression(firstArg) || ts.isElementAccessExpression(firstArg)) {
// 属性访问或元素访问,尝试分析类型
@ -2430,6 +2464,11 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
const exprText = firstArg.getText(sourceFile);
if (analysis.isLiteralUnion) {
// 可以检查
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
// 如果过滤后没有值了,直接返回
if (filteredValues.length === 0) {
return;
}
const missingKeys = [];
const foundKeys = [];
for (const value of analysis.literalValues) {

View File

@ -10,6 +10,12 @@ const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap'];
const PROMISE_METHODS = ['then', 'catch', 'finally'];
const PROMISE_STATIC_METHODS = ['all', 'race', 'allSettled', 'any'];
const LOCALE_FILE_NAMES = ['zh_CN.json', 'zh-CN.json'];
// 需要忽略的 i18n key 字段模式
const IGNORED_I18N_KEY_PATTERNS = {
startsWith: ['$', '$$'], // 以 $ 或 $$ 开头的字段
endsWith: ['Id', 'id'], // 以 Id 或 id 结尾的字段
exact: ['id', 'seq', 'createAt', 'updateAt', 'deleteAt'] // 精确匹配的字段名
};
// 判断是否是函数类声明
const isFunctionLikeDeclaration = (node: ts.Node): node is ts.FunctionLikeDeclaration => {
@ -224,6 +230,8 @@ interface OakBuildChecksConfig {
locale?: {
checkI18nKeys?: boolean; // 是否启用国际化键检查,默认启用
tFunctionModules?: string[]; // t 函数所在模块,默认 ['oak-frontend-base']
checkTemplateLiterals?: boolean; // 是否检查模板字符串中的 i18n 键,默认禁用
warnStringKeys?: boolean; // 是否警告字符串形式的 i18n 键,默认禁用
}
}
@ -443,6 +451,9 @@ export function performCustomChecks(
console.log(`Custom AsyncContext checks are ${enableAsyncContextCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`);
console.log(`Custom i18n key checks are ${enableTCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`);
const checkTemplateLiterals = customConfig?.locale?.checkTemplateLiterals ?? false;
const warnStringKeys = customConfig?.locale?.warnStringKeys ?? false;
// 数据结构定义
const functionsWithContextCalls = new Set<ts.Symbol>();
const functionDeclarations = new Map<ts.Symbol, ts.FunctionLikeDeclaration>();
@ -2127,8 +2138,11 @@ export function performCustomChecks(
const analysis = analyzeExpressionType(expr, typeChecker);
if (analysis.isLiteralUnion) {
// 新增:过滤掉需要忽略的字段值
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
return [{
values: analysis.literalValues,
values: filteredValues.length > 0 ? filteredValues : null,
isLiteral: false,
expression: expr,
analysis
@ -2387,6 +2401,28 @@ export function performCustomChecks(
return results;
};
/**
*
*/
const shouldIgnoreI18nKeyField = (fieldName: string): boolean => {
// 检查精确匹配
if (IGNORED_I18N_KEY_PATTERNS.exact.includes(fieldName)) {
return true;
}
// 检查前缀匹配
if (IGNORED_I18N_KEY_PATTERNS.startsWith.some(prefix => fieldName.startsWith(prefix))) {
return true;
}
// 检查后缀匹配
if (IGNORED_I18N_KEY_PATTERNS.endsWith.some(suffix => fieldName.endsWith(suffix))) {
return true;
}
return false;
};
/**
* i18n key
*/
@ -2413,10 +2449,12 @@ export function performCustomChecks(
if (analysis.isLiteralUnion) {
// 字面量联合类型,可以检查
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
spans.push({
text: span.literal.text,
values: analysis.literalValues
values: filteredValues.length > 0 ? filteredValues : null // 如果过滤后为空,设为 null
});
} else if (analysis.isString) {
// string 类型,无法检查
hasUncheckableSpan = true;
@ -2716,6 +2754,13 @@ export function performCustomChecks(
if (analysis.isLiteralUnion) {
// 字面量联合类型,检查所有可能的值
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
// 如果过滤后没有值了,直接返回
if (filteredValues.length === 0) {
return;
}
const missingKeys: string[] = [];
const foundKeys: string[] = [];
@ -2780,7 +2825,7 @@ export function performCustomChecks(
);
} else if (analysis.isString) {
// string 类型,无法检查
addDiagnostic(
warnStringKeys && addDiagnostic(
callNode,
`变量 ${varName} 的类型为 string范围太大无法检查 i18n key 是否存在。`,
9203,
@ -2979,7 +3024,7 @@ export function performCustomChecks(
checkWithCommonString(i18nData, key, callNode, localePath);
} else if (ts.isTemplateExpression(firstArg)) {
// 模板字符串
checkTemplateExpressionKey(
checkTemplateLiterals && checkTemplateExpressionKey(
firstArg,
callNode,
i18nData,
@ -3006,7 +3051,7 @@ export function performCustomChecks(
} else if (ts.isBinaryExpression(firstArg) &&
firstArg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
// 字符串拼接表达式
checkBinaryExpressionKey(
warnStringKeys && checkBinaryExpressionKey(
firstArg,
callNode,
i18nData,
@ -3023,6 +3068,13 @@ export function performCustomChecks(
if (analysis.isLiteralUnion) {
// 可以检查
const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
// 如果过滤后没有值了,直接返回
if (filteredValues.length === 0) {
return;
}
const missingKeys: string[] = [];
const foundKeys: string[] = [];