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 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
xiaotao1317 小时前
第九章:Vite API 参考手册
前端·vite·前端打包
午安~婉7 小时前
Electron桌面应用聊天(续)
前端·javascript·electron
彧翎Pro7 小时前
基于 RO1 noetic 配置 robosense Helios 32(速腾) & xsense mti 300
前端·jvm
小码哥_常7 小时前
解锁系统设置新姿势:Activity嵌入全解析
前端
之歆8 小时前
前端存储方案对比:Cookie-Session-LocalStorage-IndexedDB
前端
哟哟耶耶8 小时前
vue3-单文件组件css功能(:deep,:slotted,:global,useCssModule,v-bind)
前端·javascript·css
是罐装可乐8 小时前
深入理解“句柄(Handle)“:从浏览器安全到文件系统访问
前端·javascript·安全
华科易迅8 小时前
Vue如何集成封装Axios
前端·javascript·vue.js
康一夏8 小时前
Next.js 13变化有多大?
前端·react·nextjs