场景:在平时开发过程中国际化一般是最费时间和手指的事,所以弄了这么一个工具来减少大部分工作量

直接上代码
import * as fs from 'fs';
import * as path from 'path';
import { parse, ParserOptions } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import * as t from '@babel/types';
const DOUBLE_BYTE_REGEX = /[\u4E00-\u9FFF]/g;
const entry = './src/login';
const defaultLangs = ['zh', 'en', 'ja'];
const importName = `trans`;
const importSource = `@/utils/select`;

function readTSXFiles(dir: string): string[] {
  const files = fs.readdirSync(dir);
  let tsxFiles: string[] = [];
  files.forEach((file) => {
    const fullPath = path.join(dir, file);
    const stat = fs.statSync(fullPath);
    if (stat.isDirectory()) {
      tsxFiles = tsxFiles.concat(readTSXFiles(fullPath));
    } else if (file.endsWith('.tsx')) {
      tsxFiles.push(fullPath);
    }
  });
  return tsxFiles;
}

function generateJSON(locale: string, data: Record<string, string>, filePath: string) {
  const dir = path.join('_locales', locale);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  const parentDir = path.basename(path.dirname(filePath));
  const fileName = `${parentDir}-${path.basename(filePath, '.tsx')}.json`;
  const jsonPath = path.join(dir, fileName);

  // 读取已存在的内容
  let existingData: Record<string, string> = {};
  if (fs.existsSync(jsonPath)) {
    try {
      const raw = fs.readFileSync(jsonPath, 'utf8');
      existingData = JSON.parse(raw);
    } catch (err) {
      console.warn(`读取 ${jsonPath} 失败,使用空内容覆盖`);
    }
  }

  // 合并新旧数据(新内容覆盖旧内容)
  const mergedData = { ...existingData, ...data };

  fs.writeFileSync(jsonPath, JSON.stringify(mergedData, null, 2), 'utf8');
}


function isAlreadyTranslated(path: any) {
  return (
    path.parentPath.isCallExpression() &&
    t.isIdentifier(path.parentPath.node.callee, { name: importName })
  );
}

function extractI18nFromTSX(filePath: string) {
  const sourceCode = fs.readFileSync(filePath, 'utf8');
  const plugins: ParserOptions['plugins'] = ['typescript', 'jsx'];
  const ast = parse(sourceCode, { sourceType: 'module', plugins });

  const translations: Record<string, string> = {};

  traverse(ast, {
    enter(path) {
      if (path.isStringLiteral()) {
        if (isAlreadyTranslated(path)) return;
        const value = path.node.value;
        if (DOUBLE_BYTE_REGEX.test(value)) {
          translations[value] = value;
          path.replaceWith(t.callExpression(t.identifier(importName), [t.stringLiteral(value)]));
        }
      }
      if (path.isJSXText()) {
        const value = path.node.value.trim();
        if (value && DOUBLE_BYTE_REGEX.test(value)) {
          translations[value] = value;
          path.replaceWith(
            t.jsxExpressionContainer(t.callExpression(t.identifier(importName), [t.stringLiteral(value)]))
          );
        }
      }
      if (path.isJSXAttribute() && t.isStringLiteral(path.node.value)) {
        const value = path.node.value.value;
        if (DOUBLE_BYTE_REGEX.test(value)) {
          translations[value] = value;
          path.node.value = t.jsxExpressionContainer(
            t.callExpression(t.identifier(importName), [t.stringLiteral(value)])
          );
        }
      }
    },
    Program: {
      exit(path) {
        const hasImport = path.node.body.some(
          node => t.isImportDeclaration(node) && node.source.value === importSource
        );
        if (!hasImport) {
          path.node.body.unshift(
            t.importDeclaration(
              [t.importSpecifier(t.identifier(importName), t.identifier(importName))],
              t.stringLiteral(importSource)
            )
          );
        }
      }
    }
  });

  const { code } = generate(ast, { retainLines: true, jsescOption: { minimal: true }, });
  fs.writeFileSync(filePath, code, 'utf8');

  defaultLangs.forEach(locale => {
    const content = Object.fromEntries(
      Object.entries(translations).map(([key, value]) => [key, key])
    );
    generateJSON(locale, content, filePath);
  });
}

function extractAndReplaceFromDir() {
  const files = readTSXFiles(entry);
  files.forEach(file => extractI18nFromTSX(file));
  console.log(`✅ 提取完成,共处理 ${files.length} 个文件`);
}
extractAndReplaceFromDir()

简要描述所做的事

  1. 扫描入口文件下所有的.tsx文件提取出中文,将提取的中文以键值对格式保存为json文档
  2. 根据设置生成多个国际化文件夹,每个json文档对应一个组件
  3. 根据设置的方法替换原本的中文如:"翻译"=>t("翻译")或者{t("翻译")}

运行结果展示

原tsx文件
import { sendValidCodeUsingGet } from "@/services/swagger/common";
import { recoverPasswordUsingPost } from "@/services/swagger/login";
import { modifyPasswordPhoneUsingPost, } from "@/services/swagger/userProfile";
import { Modal, Form, Input, Space, Button, message } from "antd";
import { useForm } from "antd/es/form/Form";
import { useState } from "react";
import { Md5 } from "ts-md5";
interface Props {
    open: boolean;
    setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const formItemLayout = {
    labelCol: {
        xs: { span: 24 },
        sm: { span: 6 },
    },
    wrapperCol: {
        xs: { span: 24 },
        sm: { span: 16 },
    },
};
const ForgetPwd = (props: Props) => {
    const { open, setOpen, } = props
    const [confirmLoading, setConfirmLoading] = useState(false);
    const { count, start, timeText } = useCount()
    const [form] = useForm()

    const sendCode = () => {
        if (count !== 0) {
            return false
        }
        form.validateFields(['phone']).then((res) => {
            sendValidCodeUsingGet({
                mobile: res.phone
            }).then(res => {
                if (res.code == "OK") {
                    start(60)
                    message.success('验证码已发送')
                }
            }).catch(err => { })
        })

    }
    const handleOk = () => {
        form.validateFields().then((res: API.UserProfileModifyPsdPhoneReqDto) => {
            setConfirmLoading(true);
            recoverPasswordUsingPost({
                newPassword: Md5.hashStr(res.newPassword as string),
                phone: res.phone,
                verifyCode: res.verifyCode
            }).then(res => {
                if (res.code == "OK") {
                    message.success('修改成功')
                    setConfirmLoading(false);
                    setOpen(false);
                }
            })
        })

    };
    return <Modal
        title="找回登录密码"
        open={open}
        centered
        onOk={handleOk}
        confirmLoading={confirmLoading}
        onCancel={() => setOpen(false)}
        width={400}

    >
        <Form size="large" form={form} {...formItemLayout} labelAlign="left" >
            <Form.Item label="手机号" name="phone" rules={[
                {
                    required: true, message: '请输入手机号!'
                }
            ]} >
                <Input placeholder="请输入手机号" />
            </Form.Item>
            <Form.Item label="验证码" name="verifyCode" rules={[
                {
                    required: true, message: '请输入验证码!'
                }
            ]} >
                <Space.Compact>
                    <Input placeholder="请输入验证码" />
                    <Button style={{ width: 100 }} type="primary" onClick={sendCode} >{timeText}</Button>
                </Space.Compact>
            </Form.Item>
            <Form.Item label="新密码" name="newPassword" rules={[{ required: true }]}>
                <Input type="password" placeholder="请输入新密码" />
            </Form.Item>
            <Form.Item
                label="确认密码"
                name="confirmPassword"
                dependencies={['newPassword']}
                validateTrigger="onBlur"
                rules={[
                    {
                        required: true,
                    },
                    ({ getFieldValue }) => ({
                        validator(_, value) {
                            if (!value || getFieldValue('newPassword') === value) {
                                return Promise.resolve();
                            }
                            return Promise.reject(new Error('两次密码不一致'));
                        },
                    }),
                ]}
            >
                <Input type="password" placeholder="请再次输入新密码" />
            </Form.Item>
        </Form >
        为了您的账号安全,需对现手机进行短信验证
    </Modal >
}
export default ForgetPwd
执行后的tsx文件
import { trans } from "@/utils/select";
import { useCount } from "@/hooks";
import { sendValidCodeUsingGet } from "@/services/swagger/common";
import { recoverPasswordUsingPost } from "@/services/swagger/login";
import { modifyPasswordPhoneUsingPost } from "@/services/swagger/userProfile";
import { Modal, Form, Input, Space, Button, message } from "antd";
import { useForm } from "antd/es/form/Form";
import { useState } from "react";
import { Md5 } from "ts-md5";
interface Props {
  open: boolean;
  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const formItemLayout = {
  labelCol: {
    xs: { span: 24 },
    sm: { span: 6 }
  },
  wrapperCol: {
    xs: { span: 24 },
    sm: { span: 16 }
  }
};
const ForgetPwd = (props: Props) => {
  const { open, setOpen } = props;
  const [confirmLoading, setConfirmLoading] = useState(false);
  const { count, start, timeText } = useCount();
  const [form] = useForm();

  const sendCode = () => {
    if (count !== 0) {
      return false;
    }
    form.validateFields(['phone']).then((res) => {
      sendValidCodeUsingGet({
        mobile: res.phone
      }).then((res) => {
        if (res.code == "OK") {
          start(60);
          message.success(trans("验证码已发送"));
        }
      }).catch((err) => { });
    });

  };
  const handleOk = () => {
    form.validateFields().then((res: API.UserProfileModifyPsdPhoneReqDto) => {
      setConfirmLoading(true);
      recoverPasswordUsingPost({
        newPassword: Md5.hashStr(res.newPassword as string),
        phone: res.phone,
        verifyCode: res.verifyCode
      }).then((res) => {
        if (res.code == "OK") {
          message.success(trans("修改成功"));
          setConfirmLoading(false);
          setOpen(false);
        }
      });
    });

  };
  return <Modal
    title={trans("找回登录密码")}
    open={open}
    centered
    onOk={handleOk}
    confirmLoading={confirmLoading}
    onCancel={() => setOpen(false)}
    width={400}>


    <Form size="large" form={form} {...formItemLayout} labelAlign="left">
      <Form.Item label={trans("手机号")} name="phone" rules={[
        {
          required: true, message: trans("请输入手机号!")
        }]
      }>
        <Input placeholder={trans("请输入手机号")} />
      </Form.Item>
      <Form.Item label={trans("验证码")} name="verifyCode" rules={[
        {
          required: true, message: trans("请输入验证码!")
        }]
      }>
        <Space.Compact>
          <Input placeholder={trans("请输入验证码")} />
          <Button style={{ width: 100 }} type="primary" onClick={sendCode}>{timeText}</Button>
        </Space.Compact>
      </Form.Item>
      <Form.Item label={trans("新密码")} name="newPassword" rules={[{ required: true }]}>
        <Input type="password" placeholder={trans("请输入新密码")} />
      </Form.Item>
      <Form.Item
        label={trans("确认密码")}
        name="confirmPassword"
        dependencies={['newPassword']}
        validateTrigger="onBlur"
        rules={[
          {
            required: true
          },
          ({ getFieldValue }) => ({
            validator(_, value) {
              if (!value || getFieldValue('newPassword') === value) {
                return Promise.resolve();
              }
              return Promise.reject(new Error(trans("两次密码不一致")));
            }
          })]
        }>

        <Input type="password" placeholder={trans("请再次输入新密码")} />
      </Form.Item>
    </Form>{trans("为了您的账号安全,需对现手机进行短信验证")}

  </Modal>;
};
export default ForgetPwd;

针对某些特殊的中文 还是需要手动修改一下 如需要传参的国际化文案 国际化方法 以及生成的键值对的名字均可修改 这里直接将中文作为key了

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐