react国际化自动提取中文并替换工具
·
场景:在平时开发过程中国际化一般是最费时间和手指的事,所以弄了这么一个工具来减少大部分工作量
直接上代码
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()
简要描述所做的事
- 扫描入口文件下所有的.tsx文件提取出中文,将提取的中文以键值对格式保存为json文档
- 根据设置生成多个国际化文件夹,每个json文档对应一个组件
- 根据设置的方法替换原本的中文如:"翻译"=>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了
更多推荐
所有评论(0)