• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上节我们学了用 react-intl 做国际化。

我们会把文案抽离出来,放在不同的资源包里维护。

比如 zh-CN.json:

en-US.json:

而这个文案的翻译一般是产品经理做的。

那怎么把这个资源包给产品经理编辑呢?

直接给他 json 文件么?

这样并不好。

一般我们都是导出 excel。

来写一下:

mkdir excel-export
cd excel-export
npm init -y

进入项目,安装 exceljs:

npm install --save exceljs

写下 index.js

const { Workbook } = require("exceljs");

async function main() {
    const workbook = new Workbook();

    const worksheet = workbook.addWorksheet("guang111");

    worksheet.columns = [
        { header: "ID", key: "id", width: 20 },
        { header: "姓名", key: "name", width: 30 },
        { header: "出生日期", key: "birthday", width: 30 },
        { header: "手机号", key: "phone", width: 50 },
    ];

    const data = [
        {
            id: 1,
            name: "光光",
            birthday: new Date("1994-07-07"),
            phone: "13255555555",
        },
        {
            id: 2,
            name: "东东",
            birthday: new Date("1994-04-14"),
            phone: "13222222222",
        },
        {
            id: 3,
            name: "小刚",
            birthday: new Date("1995-08-08"),
            phone: "13211111111",
        },
    ];
    worksheet.addRows(data);

    workbook.xlsx.writeFile("./data.xlsx");
}

main();

就是按照 workbook(工作簿) > worksheet(工作表)> row (行)的层次来添加数据。

跑一下:

生成了 excel 文件。

打开看下:

可以看到 worksheet 的名字,还有每行的数据都是对的。

这样,就完成了 excel 的生成。

那我们就可以把 zh-CN.json、en-US.json 等的内容整合到 excel 文件里:

把 zh-CN.json 和 en-US.json 复制过来:

zh-CN.json

{
    "username": "用户名 <bbb>{name}</bbb>",
    "password": "密码",
    "rememberMe": "记住我",
    "submit": "提交",
    "inputYourUsername": "请输入你的用户名!",
    "inputYourPassword": "请输入你的密码!"
}

en-US.json

{
    "username": "Username <bbb>{name}</bbb>",
    "password": "Password",
    "rememberMe": "Remember Me",
    "submit": "Submit",
    "inputYourUsername": "Please input your username!",
    "inputYourPassword": "Please input your password!"
}

然后写下 index2.js

const { Workbook } = require("exceljs");
const fs = require("fs");

const languages = ["zh-CN", "en-US"];

async function main() {
    const workbook = new Workbook();

    const worksheet = workbook.addWorksheet("test");

    const bundleData = languages.map((item) => {
        return JSON.parse(fs.readFileSync(`./${item}.json`));
    });

    const data = [];

    bundleData.forEach((item, index) => {
        for (let key in item) {
            const foundItem = data.find((item) => item.id === key);
            if (foundItem) {
                foundItem[languages[index]] = item[key];
            } else {
                data.push({
                    id: key,
                    [languages[index]]: item[key],
                });
            }
        }
    });

    console.log(data);

    worksheet.columns = [
        { header: "ID", key: "id", width: 30 },
        ...languages.map((item) => {
            return {
                header: item,
                key: item,
                width: 30,
            };
        }),
    ];

    worksheet.addRows(data);

    workbook.xlsx.writeFile("./bundle.xlsx");
}

main();

这里我们读取了 en-US.json 和 zh-CN.json 的内容,然后按照 id、en-US、zh-CN 的 column 来写入 excel。

跑一下:

看下生成的 excel:

现在这个 excel 已经可以交给产品经理去编辑了,但是还少了一些描述。

可能产品经理看到某个 key 并不知道这个文案是在哪里用的,干啥的。

所以我们最好加一些描述。

打开上节的项目,再次执行 extract 命令:

npx formatjs extract "src/**/*.tsx" --out-file messages.json

现在有 defaultMessage,没有 description,我们在 defineMessages 的时候加一下:

const messsages = defineMessages({
    username: {
        id: "username",
        defaultMessage: "用户名",
        description: "这是登录的用户名",
    },
    password: {
        id: "password",
        defaultMessage: "密码",
        description: "这是登录的密码",
    },
    rememberMe: {
        id: "rememberMe",
        defaultMessage: "记住我",
        description: "登录页的记住我复选框",
    },
    submit: {
        id: "submit",
        defaultMessage: "提交",
        description: "登录页的提交按钮",
    },
    inputYourUsername: {
        id: "inputYourUsername",
        defaultMessage: "请输入用户名!",
        description: "登录页的用户名为空的提示",
    },
    inputYourPassword: {
        id: "inputYourPassword",
        defaultMessage: "请输入密码!",
        description: "登录页的密码为空的提示",
    },
});

重新 extract 生成 messages.json

npx formatjs extract "src/**/*.tsx" --out-file messages.json

上节我们把这个文件删掉了,其实没必要删掉,可以用它来生成 excel。

把 messages.json 复制过去,我们改下 index2.js

const { Workbook } = require("exceljs");
const fs = require("fs");

const languages = ["zh-CN", "en-US"];

async function main() {
    const workbook = new Workbook();

    const worksheet = workbook.addWorksheet("test");

    const bundleData = languages.map((item) => {
        return JSON.parse(fs.readFileSync(`./${item}.json`));
    });

    const data = [];

    const messages = JSON.parse(fs.readFileSync("./messages.json"));

    bundleData.forEach((item, index) => {
        for (let key in messages) {
            const foundItem = data.find((item) => item.id === key);
            if (foundItem) {
                foundItem[languages[index]] = item[key];
            } else {
                data.push({
                    id: key,
                    defaultMessage: messages[key].defaultMessage,
                    description: messages[key].description,
                    [languages[index]]: item[key],
                });
            }
        }
    });

    console.log(data);

    worksheet.columns = [
        { header: "ID", key: "id", width: 30 },
        { header: "defaultMessage", key: "defaultMessage", width: 30 },
        { header: "description", key: "description", width: 50 },
        ...languages.map((item) => {
            return {
                header: item,
                key: item,
                width: 30,
            };
        }),
    ];

    worksheet.addRows(data);

    workbook.xlsx.writeFile("./bundle.xlsx");
}

main();

现在生成的是这样的:

这样产品经理就知道每个 key 是哪里的文案,什么意思,就知道怎么翻译了。

改一下:

然后改完之后要用这个生成 en-US.json 和 zh-CN.json 在项目里引入用。

写一下这个脚本:

index3.js

const { Workbook } = require("exceljs");

async function main() {
    const workbook = new Workbook();

    const workbook2 = await workbook.xlsx.readFile("./bundle.xlsx");

    workbook2.eachSheet((sheet, index1) => {
        console.log("工作表" + index1);

        sheet.eachRow((row, index2) => {
            const rowData = [];

            row.eachCell((cell, index3) => {
                rowData.push(cell.value);
            });

            console.log("行" + index2, rowData);
        });
    });
}

main();

解析也是按照 workbook(工作簿) > worksheet(工作表)> row (行)的层次,调用 eachSheet、eachRow、eachCell 就好了。

然后生成 json:

const { Workbook } = require("exceljs");
const fs = require("fs");

async function main() {
    const workbook = new Workbook();

    const workbook2 = await workbook.xlsx.readFile("./bundle.xlsx");

    const zhCNBundle = {};
    const enUSBundle = {};

    workbook2.eachSheet((sheet) => {
        sheet.eachRow((row, index) => {
            if (index === 1) {
                return;
            }
            const key = row.getCell(1).value;
            const zhCNValue = row.getCell(4).value;
            const enUSValue = row.getCell(5).value;

            zhCNBundle[key] = zhCNValue;
            enUSBundle[key] = enUSValue;
        });
    });

    console.log(zhCNBundle);
    console.log(enUSBundle);
    fs.writeFileSync("zh-CN.json", JSON.stringify(zhCNBundle, null, 2));
    fs.writeFileSync("en-US.json", JSON.stringify(enUSBundle, null, 2));
}

main();

跑一下:

这样就把产品经理编辑后的 excel 生成了国际化资源包:

项目里直接用这个资源包就好了。

现在这样的工作流是可以的,但是不能协同编辑。

如果能够像在线文档一样协同编辑这个 excel 就好了。

可以的,用 google sheets.

打开 google sheets: https://docs.google.com/spreadsheets/

登录之后创建一个新的 sheet:

它可以导入 csv 格式的文件:

选择 replace 替换当前工作表:

这样,就导入了 csv 的数据:

可以在线编辑了。

把这个 url 分享出去就行。

比如这个 url:

https://docs.google.com/spreadsheets/d/1FgCNmoTz9FWuR6Jv1SJ9ioWd2bBfrtRAeoi5CYpmXBA/edit?usp=sharing

接下来的问题就变成了如何用 node 生成和解析 csv 文件。

这个可以用 csv-parse 和 csv-stringify 来做。

安装 csv-stringify:

npm install --save-dev csv-stringify

然后写下 index4.js

const { stringify } = require("csv-stringify");
const fs = require("fs");

const languages = ["zh-CN", "en-US"];

async function main() {
    const bundleData = languages.map((item) => {
        return JSON.parse(fs.readFileSync(`./${item}.json`));
    });

    const data = [];

    const messages = JSON.parse(fs.readFileSync("./messages.json"));

    bundleData.forEach((item, index) => {
        for (let key in messages) {
            const foundItem = data.find((item) => item.id === key);
            if (foundItem) {
                foundItem[languages[index]] = item[key];
            } else {
                data.push({
                    id: key,
                    defaultMessage: messages[key].defaultMessage,
                    description: messages[key].description,
                    [languages[index]]: item[key],
                });
            }
        }
    });

    console.log(data);

    const columns = {
        id: "Message ID",
        defaultMessage: "Default Message",
        description: "Description",
        "zh-CN": "zh-CN",
        "en-US": "en-US",
    };

    stringify(data, { header: true, columns }, function (err, output) {
        fs.writeFileSync("./messages.csv", output);
    });
}

main();

也是定义 columns 和 column 对应的 data,调用 stringify 来转成 csv 文件。

跑一下:

可以看到,生成了 message.csv 文件。

然后在 google sheet 里导入:

你可以点开这个链接看一下:

https://docs.google.com/spreadsheets/d/1FgCNmoTz9FWuR6Jv1SJ9ioWd2bBfrtRAeoi5CYpmXBA/edit?usp=sharing

改一下这个文案:

然后导出到本地再转成 json 就好了。

怎么导出呢?

在现在的 url 后加一个 export?format=csv 就好了:

比如这个链接: https://docs.google.com/spreadsheets/d/1FgCNmoTz9FWuR6Jv1SJ9ioWd2bBfrtRAeoi5CYpmXBA/export?format=csv

然后在代码里下载下导出的 csv:

index5.js

const { execSync } = require("child_process");
const { parse } = require("csv-parse/sync");
const fs = require("fs");

const sheetUrl =
    "https://docs.google.com/spreadsheets/d/1FgCNmoTz9FWuR6Jv1SJ9ioWd2bBfrtRAeoi5CYpmXBA";

execSync(`curl -L ${sheetUrl}/export?format=csv -o ./message2.csv`, {
    stdio: "ignore",
});

const input = fs.readFileSync("./message2.csv");

const records = parse(input, { columns: true });

console.log(records);

这里用 curl 命令来下载,-L 是自动跳转的意思,因为访问这个 url 会跳转一个新的地址。

安装用到的包:

npm install --save-dev csv-parse

跑一下:

可以看到,message2.csv 下载了下来,并且还解析出了其中的数据。

接下来用这个生成 zh-CN.json 和 en-US.json,然后在项目里用就好了。

const { execSync } = require("child_process");
const { parse } = require("csv-parse/sync");
const fs = require("fs");

const sheetUrl =
    "https://docs.google.com/spreadsheets/d/1FgCNmoTz9FWuR6Jv1SJ9ioWd2bBfrtRAeoi5CYpmXBA";

execSync(`curl -L ${sheetUrl}/export?format=csv -o ./message2.csv`, {
    stdio: "ignore",
});

const input = fs.readFileSync("./message2.csv");

const data = parse(input, { columns: true });

const zhCNBundle = {};
const enUSBundle = {};

data.forEach((item) => {
    const keys = Object.keys(item);
    const key = item[keys[0]];
    const valueZhCN = item[keys[3]];
    const valueEnUS = item[keys[4]];

    zhCNBundle[key] = valueZhCN;
    enUSBundle[key] = valueEnUS;
});

console.log(zhCNBundle);
console.log(enUSBundle);

fs.writeFileSync("zh-CN.json", JSON.stringify(zhCNBundle, null, 2));
fs.writeFileSync("en-US.json", JSON.stringify(enUSBundle, null, 2));

跑一下:

这样,就完成了资源包在 google sheet 的在线编辑,以及编辑完以后下载并解析生成资源包的功能。

相比用 exceljs 生成 excel 文件的方式,google sheet 可以把 url 分享出去,可以协同编辑,更方便一点。

案例代码上传了小册仓库

总结

国际化资源包需要交给产品经理去翻译,我们会把 json 转成 excel 交给他。

我们先用 exceljs 实现了 excel 的解析和生成,编辑完之后再转成 en-US.json、zh-CN.json 的资源包。

然后用 google sheet 实现了在线编辑和分享,编辑完之后下载并解析 csv,然后转成 en-US.json、zh-CN.json 的资源包。

用到了 csv-parse、csv-stingify。

这两种方案都可以,确定好方案之后把这些脚本内置到项目里就可以了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
49.用react-intl实现国际化
Next
51.基于react-dnd实现拖拽排序