feat: 默认放宽i18n检查
This commit is contained in:
parent
c649d131e3
commit
23abd05811
|
|
@ -9,6 +9,8 @@ interface OakBuildChecksConfig {
|
|||
locale?: {
|
||||
checkI18nKeys?: boolean;
|
||||
tFunctionModules?: string[];
|
||||
checkTemplateLiterals?: boolean;
|
||||
warnStringKeys?: boolean;
|
||||
};
|
||||
}
|
||||
interface CustomDiagnostic {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue