react 项目检查国际化配置脚本

说明

运行find:missing-i18n

日志会出事没有完成国际化的组件、行数和原文内容

修改package.json

json 复制代码
...
"find:missing-i18n": "ts-node --compiler-options '{\"module\":\"commonjs\"}' scripts/find-missing-i18n.ts",
...

find-missing-i18n.ts

在scripts文件夹里面增加find-missing-i18n.ts文件

ts 复制代码
import fs from 'fs';
import path from 'path';
import ts from 'typescript';

type Finding = {
  filePath: string;
  line: number;
  column: number;
  snippet: string;
};

const TARGET_COMPONENTS = new Set(['Text', 'ThemedText']);
const IGNORED_DIRS = new Set([
  'node_modules',
  '.git',
  '.expo',
  'android',
  'ios',
  'build',
  'builds',
  'dsyms',
]);

const ROOT_DIR = path.resolve(__dirname, '..');

function walk(dir: string, result: string[]): void {
  const entries = fs.readdirSync(dir, { withFileTypes: true });

  for (const entry of entries) {
    if (entry.name.startsWith('.DS_Store')) {
      continue;
    }

    const fullPath = path.join(dir, entry.name);

    if (entry.isDirectory()) {
      if (IGNORED_DIRS.has(entry.name)) {
        continue;
      }
      walk(fullPath, result);
    } else if (entry.isFile()) {
      if (fullPath.endsWith('.tsx') || fullPath.endsWith('.jsx')) {
        result.push(fullPath);
      }
    }
  }
}

function getTagName(tagName: ts.JsxTagNameExpression): string | null {
  if (ts.isIdentifier(tagName)) {
    return tagName.text;
  }
  return null;
}

function extractTextFromJsxElement(sourceFile: ts.SourceFile, element: ts.JsxElement): Finding[] {
  const tagName = getTagName(element.openingElement.tagName);

  if (!tagName || !TARGET_COMPONENTS.has(tagName)) {
    return [];
  }

  const findings: Finding[] = [];

  for (const child of element.children) {
    if (ts.isJsxText(child)) {
      const raw = child.getText();
      const trimmed = raw.replace(/\s+/g, ' ').trim();
      if (trimmed.length > 0) {
        const { line, character } = sourceFile.getLineAndCharacterOfPosition(child.getStart());
        findings.push({
          filePath: sourceFile.fileName,
          line: line + 1,
          column: character + 1,
          snippet: trimmed.slice(0, 80),
        });
      }
    } else if (ts.isJsxExpression(child) && child.expression) {
      const expr = child.expression;
      if (ts.isStringLiteralLike(expr)) {
        const text = expr.text.replace(/\s+/g, ' ').trim();
        if (text.length > 0) {
          const { line, character } = sourceFile.getLineAndCharacterOfPosition(expr.getStart());
          findings.push({
            filePath: sourceFile.fileName,
            line: line + 1,
            column: character + 1,
            snippet: text.slice(0, 80),
          });
        }
      } else if (ts.isNoSubstitutionTemplateLiteral(expr)) {
        const text = expr.text.replace(/\s+/g, ' ').trim();
        if (text.length > 0) {
          const { line, character } = sourceFile.getLineAndCharacterOfPosition(expr.getStart());
          findings.push({
            filePath: sourceFile.fileName,
            line: line + 1,
            column: character + 1,
            snippet: text.slice(0, 80),
          });
        }
      }
    }
  }

  return findings;
}

function analyzeFile(filePath: string): Finding[] {
  const fileContent = fs.readFileSync(filePath, 'utf8');
  const sourceFile = ts.createSourceFile(
    filePath,
    fileContent,
    ts.ScriptTarget.Latest,
    true,
    ts.ScriptKind.TSX
  );
  const findings: Finding[] = [];

  const visit = (node: ts.Node): void => {
    if (ts.isJsxElement(node)) {
      findings.push(...extractTextFromJsxElement(sourceFile, node));
    }
    ts.forEachChild(node, visit);
  };

  visit(sourceFile);

  return findings;
}

function main(): void {
  const filePaths: string[] = [];
  walk(ROOT_DIR, filePaths);

  const allFindings: Finding[] = [];
  for (const filePath of filePaths) {
    allFindings.push(...analyzeFile(filePath));
  }

  if (allFindings.length === 0) {
    console.log('No hardcoded Text or ThemedText content found.');
    return;
  }

  for (const finding of allFindings) {
    const relativePath = path.relative(ROOT_DIR, finding.filePath);
    console.log(`${relativePath}:${finding.line}:${finding.column} -> ${finding.snippet}`);
  }

  console.log(`\nTotal findings: ${allFindings.length}`);
}

main();
相关推荐
康一夏2 小时前
React面试题,useRef和普通变量的区别
前端·javascript·react.js
前端 贾公子2 小时前
Monorepo + Turbo (6)
前端
冴羽2 小时前
2025 年 HTML 年度调查报告公布!好多不知道!
前端·javascript·html
Apifox2 小时前
Apifox CLI + Claude Skills:将接口自动化测试融入研发工作流
前端·后端·测试
wszy18093 小时前
rn_for_openharmony_空状态与加载状态:别让用户对着白屏发呆
android·javascript·react native·react.js·harmonyos
程序员Agions3 小时前
别再只会 console.log 了!这 15 个 Console 调试技巧,让你的 Debug 效率翻倍
前端·javascript
我的div丢了肿么办3 小时前
vue使用h函数封装dialog组件,以命令的形式使用dialog组件
前端·javascript·vue.js
UIUV3 小时前
Git 提交规范与全栈AI驱动开发实战:从基础到高级应用
前端·javascript·后端