前端项目开发中的多语言检测

一、检测方案设计

目前项目中采用了i18n多语言库,代码编写中为了方便开发,提高代码可读性采用中文作为翻译key值,该方案针对代码中的中文文案进行识别检测,以避免上线后多语言未翻译问题的出现。

  • 检测文件范围:只检测 src/components 和 src/pages 目录下的文件。

  • 检测内容:组件和 JSX 中以字面量形式出现的中文字符。

  • 注释排除:排除单行和多行注释中的中文字符,避免干扰检测。

  • 翻译函数标准:检查中文是否被 transStr()t() 或 getTranslateOptions() 包裹进行翻译,未翻译的中文需要在控制台输出提示。

  • 开发过程中的实时提示:

  • 在开发过程中,遍历所有页面组件和配置文件,检测出现的中文文案是否通过翻译函数处理(如 transStr()t() 等)。

  • 如果没有翻译,立即在控制台打印错误,并且弹窗提示,指出文件名和未翻译的文案。

  • 打包前的最终检查:

  • 在资源打包前,统一进行一次翻译检查。如果发现未被翻译的文案,打包过程会停止,直到所有问题得到解决。

二、初步方案和存在的问题

  • 逻辑

    • 按行读取文件,通过 \n 分隔行,检查每一行中的中文字符是否已通过 transStr()t() 或 getTranslateOptions() 进行翻译处理。

    • 在读取文件时,使用正则表达式移除文件中的注释部分(包括单行和多行注释)。

  •  问题

    • 对于跨行文本和复杂注释的检测较为困难,正则表达式处理可能存在误判,因此考虑放弃此方案。

三、优化版方案

(一)核心思路
  1. AST解析:使用 Abstract Syntax Tree(AST)解析代码,精确识别每一行代码的结构和中文字符串。通过 AST 解析可以有效识别跨行的文本内容和 JSX 中的中文字符,避免传统正则的复杂性和不准确性。

  2. 翻译函数检测:通过判断中文字符串是否作为 transStr()t() 或 getTranslateOptions() 函数的参数来确定是否已经进行了翻译。

  3. 过滤注释:过滤掉代码中的注释(包括单行注释 // 和多行注释 /* */),避免干扰检测。

  4. 处理特殊逻辑:针对需要根据 lang 值条件渲染模板的地方(比如在 UncheckJsx.js 中),考虑到该部分的特殊逻辑,不对这些部分进行检测。该部分的内容可以通过配置文件进行管理,不必进行翻译函数的自动化检测。

(二)具体步骤
  1. 使用 AST 解析代码:通过使用 babel-parser 或其他 AST 解析工具,将代码转化为抽象语法树,方便对代码中的节点进行处理。

  2. 判断中文字符串是否已处理:

    • 查找 JSXText 类型的节点,或字符串类型的节点。

    • 判断该中文字符串是否作为翻译函数的参数之一(transStr()t() 或 getTranslateOptions())。

    • 如果未找到翻译函数,记录该中文字符串。

  1. 注释过滤:在解析 AST 时,跳过注释节点,确保不会对注释内容进行翻译检测。

  2. 特殊处理:对于条件渲染和特殊逻辑(如 UncheckJsx.js 中的特殊翻译逻辑),通过配置文件 config.js管理,脚本可以根据配置文件中的特殊处理规则跳过这些部分。

  3. 全局检测key值是否存在

为了实现全局检测 i18n 的 key 值是否存在,并在缺少某个 key 时抛出错误或进行提示

  • 使用 i18n 的 missingKey 方法:检测在国际化文件中是否缺少某些翻译键(key),如果缺少则触发错误提示。

  • 控制台输出:在开发环境中,将缺少的 key 打印到控制台,以便开发人员及时发现问题。

  • 生产环境提示:在生产环境中,不将错误打印到控制台,而是通过应用内消息提示(如 Message 组件)提醒用户或开发者。

假设你使用的 i18n 库是 i18next,以下是实现代码:

import i18n from 'i18next';
import { Message } from 'element-ui';  // 假设使用 Element UI 的 Message 组件
import { isProd } from './env';  // 假设有一个判断当前环境是否是生产环境的工具函数

// 初始化 i18n
i18n.init({
  lng: 'en',  // 默认语言
  fallbackLng: 'en',  // 回退语言
  resources: {
    en: {
      translation: {
        // 你的英文翻译
      }
    },
    zh: {
      translation: {
        // 你的中文翻译
      }
    }
  },
  missingKeyHandler: function(lng, ns, key, fallbackValue) {
    handleMissingKey(key);  // 处理缺失的 key
  }
});

// 处理缺失 key 的方法
function handleMissingKey(key) {
  // 在开发环境中,将缺失的 key 输出到控制台
  if (process.env.NODE_ENV !== 'production') {
    console.error(`缺失的翻译 key: ${key}`);
  }

  // 在生产环境中,通过 Message 提示用户或开发者
  if (isProd()) {
    Message.error(`缺失翻译:${key}`);
  }
}

// 在应用中需要使用的地方进行i18n翻译
function translate(key) {
  return i18n.t(key);
}

export { translate, i18n };

(三)可优化部分
  1. 按模块处理缺失的 key:可以根据不同的模块或页面,灵活配置不同的 missingKeyHandler,以便在不同的区域进行更精细的错误管理。

  2. 支持日志记录:除了控制台输出或消息提示,可以集成日志系统(如 Sentry、LogRocket 等)来记录生产环境中的翻译缺失错误,更好地追踪问题。

PS:当前的监测方案基本覆盖了开发需求,后续有时间可进一步调研

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const fs = require("fs");
const path = require("path");

const t = require("@babel/types"); 

// 检测文件夹
const dirPaths = [
  path.join(__dirname, "src/pages"),
  path.join(__dirname, "src/components"),
];
// const dirPath = path.join(__dirname, "src/pages/test/");
// 排除文件名
const excludeFiles = ["AppMenu.js", "UncheckJsx.js"];
function isChinese(str) {
  return /[\u4e00-\u9fa5]/.test(str);
}
function isInsideTCall(path) {
  let parentPath = path.parentPath;
  while (parentPath) {
    if (
      parentPath.isCallExpression() &&
      (t.isIdentifier(parentPath.node.callee, { name: "t" }) ||
        t.isIdentifier(parentPath.node.callee, {
          name: "transStr",
        }) ||
        t.isIdentifier(parentPath.node.callee, {
          name: "getTranslateOptions",
        }))
    ) {
      return true;
    }
    parentPath = parentPath.parentPath;
  }
  return false;
}
let isValid = true;
// 递归读取文件夹内所有文件
async function checkFiles(dirPath) {
  try {
    const files = fs.readdirSync(dirPath, { withFileTypes: true });
    files.forEach((file) => {
      if (file.isDirectory()) {
        checkFiles(path.join(dirPath, file.name));
      } else if (file.isFile() && file.name.endsWith(".js")) {
        const filePath = path.join(dirPath, file.name);
        if (!excludeFiles.includes(file.name)) {
          try {
            const data = fs.readFileSync(filePath, "utf8");
            const ast = parser.parse(data, {
              sourceType: "module",
              plugins: ["jsx"],
            });

            traverse(ast, {
              enter(path) {
                if (
                  (path.isStringLiteral() || path.node.type === "JSXText") &&
                  isChinese(path.node.value) &&
                  !isInsideTCall(path)
                ) {
                  isValid = false;
                  console.error(filePath, path.node.value);
                }
              },
            });
          } catch (err) {
            throw err;
          }
        }
      }
    });
  } catch (err) {
    throw err;
  }
}
dirPaths.forEach((dirPath) => {
  checkFiles(dirPath);
  !isValid && process.exit(1);
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值