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();
相关推荐
不简说18 分钟前
JS 代码技巧 vol.8 — 20 个函数式编程实战,把 if/else 拍扁的骚操作
前端·javascript·面试
头茬韭菜34 分钟前
4.9 SSRF 防护 — Web 工具的出站安全与私有 IP 拦截
前端·tcp/ip·安全
Cobyte44 分钟前
模板 DSL 解析器中的状态机设计
前端·javascript·vue.js
颜酱1 小时前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱1 小时前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端
刘卓航众创芯云服务部2 小时前
Kimi K3复杂任务实测:我把团队最头疼的三个场景全跑了一遍
前端
cll_8692418912 小时前
一个好看的Wordpress博客文字css样式
前端·css·ui
武子康2 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
糖果店的幽灵2 小时前
langgraph分支之 - 动态分支(Dynamic Branch)
java·前端·javascript·人工智能·langgraph
meilindehuzi_a2 小时前
Workflow 与 Agent 有什么区别:从 LangChain 流水线到智能体决策
前端·人工智能