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();
相关推荐
小兔子8 分钟前
Python 的 GIL 与 free-threading:3.13 之后「去 GIL」走到哪一步了
前端
IT_陈寒14 分钟前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
guslegend1 小时前
脚手架原理与本地调试:从 bin 软链接到 npm link
前端·npm·node.js·脚手架·前端工程化
海码事务所2 小时前
Google Play 新个人开发者账号上架指南:12 人连续 14 天封闭测试怎么做?
前端
田威AI2 小时前
图片内文字翻译的规格:输入输出、保真、自动化、时间与费用
前端·计算机视觉
不可能片场2 小时前
命令行中文变问号 我用环境变量救了场
前端·electron
lerhxx2 小时前
AI 应用中的上下文管理 —— 从"上下文窗口"到"分层压缩"的工程实践
前端·javascript
骑着蜗牛撵大象3272 小时前
多 Agent 分治协作:MapReduce 模式下的并行扇出与结果归并
前端
用户55318297322 小时前
Flutter iOS 热更新深入 Dart VM:读懂函数入口、解释执行与调用桥接
前端
前端snow3 小时前
为什么大厂要用Postgres + mogodb的框架?
前端