Loading

React lingui.js 多语言自动提取翻译键 - ast node

背景介绍

目前有个项目,页面已上百,突然说要做国际化,懵了,页面这么多,那不得累滩,之前接触过的国际化是 react-intl,原理:每个国家的语言维护一份js字典,字典里有很多key,都是唯一,使用时通过读取这个key就能拿到对应国家的语言文本内容

例如

import { IntlProvider, FormattedMessage } from 'react-intl';

const messages = {
  en: {
    greeting: 'Hello, {name}!',
  },
  fr: {
    greeting: 'Bonjour, {name}!',
  },
};

const App = () => {
  return (
    <IntlProvider locale="en" messages={messages['en']}>
      <h1><FormattedMessage id="greeting" values={{ name: 'John' }} /></h1>
    </IntlProvider>
  );
};

如果是前期一开始就考虑了做国际化,这么这个方案没什么问题,顶多在写固定文本时把数据维护进字典里面就好了,但是,现在上百个页面已经有了中文,用这种方式,成本太大了,要修改每个文件

并且它有个比较大的弊端,对于英文不懂的人来说,写 <FormattedMessage id="greeting" values={{ name: 'John' }} />都不懂啥意思,起码没中文看起来那么直观,于是乎,开启调研模式

前期调研

目前比较流行的国际化方案如下

1. react-intl

react-intl 是一个由 formatjs 提供的库,它是 React 中最流行的国际化解决方案之一。它提供了格式化日期、数字和消息的功能,以及支持在应用中切换语言。

特点:

支持日期、时间、数字格式化。
支持消息格式化,带有插值。
支持多语言切换。
支持本地化规则(如日期、货币、数字)。

使用方法

npm install react-intl

使用

import { IntlProvider, FormattedMessage } from 'react-intl';

const messages = {
  en: {
    greeting: 'Hello, {name}!',
  },
  fr: {
    greeting: 'Bonjour, {name}!',
  },
};

const App = () => {
  return (
    <IntlProvider locale="en" messages={messages['en']}>
      <h1><FormattedMessage id="greeting" values={{ name: 'John' }} /></h1>
    </IntlProvider>
  );
};

2. react-i18next

react-i18next 是一个基于 i18next 的 React 国际化库,具有灵活的配置和插件系统,支持语言切换、动态加载语言包等功能。

特点:
支持语言切换,自动切换 UI 语言。
支持动态加载语言包。
支持插值和后端加载。
插件丰富,支持多种扩展。

安装

npm install react-i18next i18next

使用

import { useTranslation } from 'react-i18next';

const App = () => {
  const { t, i18n } = useTranslation();

  return (
    <div>
      <h1>{t('greeting', { name: 'John' })}</h1>
      <button onClick={() => i18n.changeLanguage('fr')}>Switch to French</button>
    </div>
  );
};

配置

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translation: {
        greeting: 'Hello, {{name}}!',
      },
    },
    fr: {
      translation: {
        greeting: 'Bonjour, {{name}}!',
      },
    },
  },
  lng: 'en',
  fallbackLng: 'en',
});

3. lingui.js

lingui.js 是一个轻量级的国际化库,专注于简单易用和优化性能。它支持提取翻译文件、按需加载等功能。

支持自动提取翻译键。
按需加载翻译文件。
性能较好,支持静态优化。
提供高效的格式化功能。

安装

npm install @lingui/react @lingui/core

使用

import { I18nProvider, Trans } from '@lingui/react';
import { i18n } from '@lingui/core';
import enMessages from './locales/en/messages';
import frMessages from './locales/fr/messages';

i18n.load({
  en: enMessages,
  fr: frMessages,
});

i18n.activate('en');

const App = () => {
  return (
    <I18nProvider i18n={i18n}>
      <h1><Trans>greeting</Trans></h1>
    </I18nProvider>
  );
};

我的选择

在看到 lingui.js 时,有个支持自动提取翻译键,功能吸引到了我,什么叫自动提取翻译键,看下面代码

jsx源代码

<div title={`测试`} data-content={test(`测试`)}>
  测试 {test(`测试`)}
</div>

如果要使用 支持自动提取翻译键 功能,字符串用 t`测试` 方式包裹起来,节点内容则用 <Trans>测试</Trans> 包裹起来

<div title={t`测试`} data-content={test(t`测试`)}>
  <Trans>测试 {test(t`测试`)}</Trans>
</div>

执行命令

npx lingui extract

它会自动提取  t`测试`  <Trans>测试</Trans> 作为一个唯一key,然后对应生成两份对应的字典源代码,分别是中文和英文(我这里只配置了中文和英文)

生成的文件如下

中文文件 /locales/zh/messages.po

msgid ""
msgstr ""
"POT-Creation-Date: 2024-12-09 18:45+0800\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"

#: src/pages/Test/Test2/index.tsx
#: src/pages/Test/Test2/index.tsx
msgid "测试"
msgstr "测试"

英文文件 /locales/en/messages.po

msgid ""
msgstr ""
"POT-Creation-Date: 2024-12-09 18:45+0800\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"

#: src/pages/Test/Test2/index.tsx
#: src/pages/Test/Test2/index.tsx
msgid "测试"
msgstr "test"

字段解释

msgid:字典中的唯一标识,程序通过 t`测试` 来匹配msgid 值,然后提取对应的 msgstr 值

msgstr:字典翻译的内容

我们可以看到中文字典,msgstr 有对应的值,而英文字段里没有,因为我是以中文为默认语言,所以脚本自动补充了中文翻译,英文值空缺了,后续我们需要做的就是把英文翻译给补全

执行命令

npx lingui compile

它会将locales/*.po 文件转成对应的js文件(已压缩)

中文文件 /locales/zh/messages.ts

/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"0qIdia\":[\"测试\"]}")as Messages;

英文文件 /locales/en/messages.ts

/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"0qIdia\":[\"test\"]}")as Messages;

结论

最终其实我们只需要维护 locales/*.po 文件 ,然后通过 npx lingui compile 来编译输出我们所需已翻译好的js字典

抛出问题

如何让所有的中文都用 t`测试`  或者 <Trans>测试</Trans> 包裹呢?  

需求分析

源代码

const Test = () => {
  const test = (t: string) => {
    return t;
  };
  return (
    <div>
      <div title="测试" data-content={test('测试')}>
        测试 {test('测试')}
      </div>
      <QkProTable headerTitle="测试" />
    </div>
  );
};

转换完之后的代码

import { useLingui, Trans } from '@lingui/react/macro';
const Test = () => {
  const { t } = useLingui();
  const test = (t: string) => {
    return t;
  };
  return (
    <div>
      <div title={t`测试`} data-content={test(t`测试`)}>
        <Trans>测试 {test(t`测试`)}</Trans>
      </div>
      <QkProTable headerTitle={t`测试`} />
    </div>
  );
};

分析

1. 将 x="xx" 转换成 x={t`xx`}

2. 将 <div>测试</div> 转换成 <div><Trans>测试</Trans></div>

3. 如果没有引入 import { useLingui, Trans } from '@lingui/react/macro'; 则引入

4. 组件如果没用hooks const {  t } = useLingui(); 则需要使用hooks

5. 转换过的内容不能再继续转换

6. 引入过的插件不能再引入

7. hook使用了不能再使用,hook的使用针对的是组件不是文件

最终代码

transform-lingui-ast.mjs

import { createRequire } from 'module';
import * as parser from '@babel/parser';
import * as t from '@babel/types';
import fs from 'fs';
import path from 'path';

const require = createRequire(import.meta.url);
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

// 判断是否为中文字符
function containsChinese(str) {
  return /[\u4e00-\u9fa5]/.test(str);
}

// 创建Trans元素
function createTransElement(text) {
  return t.jsxElement(
    t.jsxOpeningElement(t.jsxIdentifier('Trans'), [], false),
    t.jsxClosingElement(t.jsxIdentifier('Trans')),
    [t.jsxText(text)],
    false
  );
}

// 创建t表达式
function createTExpression(text) {
  return t.taggedTemplateExpression(
    t.identifier('t'),
    t.templateLiteral(
      [t.templateElement({ raw: text, cooked: text }, true)],
      []
    )
  );
}

// 创建i18n._(msg)表达式
function createMsgExpression(text) {
  return t.callExpression(
    t.memberExpression(t.identifier('i18n'), t.identifier('_')),
    [
      t.taggedTemplateExpression(
        t.identifier('msg'),
        t.templateLiteral(
          [t.templateElement({ raw: text, cooked: text }, true)],
          []
        )
      )
    ]
  );
}

// 创建JSX表达式容器
function createJSXExpressionContainer(expression) {
  return t.jsxExpressionContainer(expression);
}

// 检查是否是React组件声明
function isReactComponentDeclaration(path) {
  // 检查是否在枚举声明中
  let currentPath = path;
  while (currentPath) {
    if (currentPath.node.type === 'TSEnumDeclaration') {
      return false;
    }
    currentPath = currentPath.parentPath;
  }

  // 函数声明组件
  if (
    path.type === 'FunctionDeclaration' &&
    path.node.id &&
    /^[A-Z]/.test(path.node.id.name)
  ) {
    return true;
  }

  // 箭头函数组件
  if (
    path.type === 'VariableDeclarator' &&
    path.node.id &&
    /^[A-Z]/.test(path.node.id.name) &&
    !t.isObjectExpression(path.node.init)
  ) {
    return true;
  }

  // forwardRef组件 - 函数声明形式
  if (
    path.type === 'FunctionExpression' &&
    path.parent?.type === 'CallExpression' &&
    path.parent.callee?.name === 'forwardRef'
  ) {
    return true;
  }

  // forwardRef组件 - 箭头函数形式
  if (
    path.type === 'ArrowFunctionExpression' &&
    path.parent?.type === 'CallExpression' &&
    path.parent.callee?.name === 'forwardRef'
  ) {
    return true;
  }

  // export default forwardRef组件
  if (
    (path.type === 'FunctionExpression' || path.type === 'ArrowFunctionExpression') &&
    path.parent?.type === 'CallExpression' &&
    path.parent.callee?.name === 'forwardRef' &&
    path.parent.parent?.type === 'ExportDefaultDeclaration'
  ) {
    return true;
  }

  // 类组件
  if (
    path.type === 'ClassDeclaration' &&
    path.node.superClass &&
    ((path.node.superClass.type === 'MemberExpression' &&
      path.node.superClass.object.name === 'React' &&
      path.node.superClass.property.name === 'Component') ||
      (path.node.superClass.type === 'Identifier' &&
        path.node.superClass.name === 'Component'))
  ) {
    return true;
  }

  return false;
}

// 检查是否在React组件内部
function isInsideReactComponent(path) {
  let currentPath = path;
  while (currentPath) {
    // 检查是否在枚举声明中
    if (currentPath.node.type === 'TSEnumDeclaration') {
      return false;
    }

    // 检查函数声明组件
    if (
      currentPath.node.type === 'FunctionDeclaration' &&
      currentPath.node.id &&
      /^[A-Z]/.test(currentPath.node.id.name)
    ) {
      return true;
    }

    // 检查箭头函数组件
    if (
      currentPath.node.type === 'VariableDeclarator' &&
      currentPath.node.id &&
      /^[A-Z]/.test(currentPath.node.id.name) &&
      !t.isObjectExpression(currentPath.node.init)
    ) {
      return true;
    }

    // 检查forwardRef组件 - 函数声明形式
    if (
      currentPath.node.type === 'FunctionExpression' &&
      currentPath.parent?.type === 'CallExpression' &&
      currentPath.parent.callee?.name === 'forwardRef'
    ) {
      return true;
    }
    // 检查forwardRef组件 - 箭头函数形式
    if (
      currentPath.node.type === 'ArrowFunctionExpression' &&
      currentPath.parent?.type === 'CallExpression' &&
      currentPath.parent.callee?.name === 'forwardRef'
    ) {
      return true;
    }
    // 检查export default forwardRef组件
    if (
      (currentPath.node.type === 'FunctionExpression' || currentPath.node.type === 'ArrowFunctionExpression') &&
      currentPath.parent?.type === 'CallExpression' &&
      currentPath.parent.callee?.name === 'forwardRef' &&
      currentPath.parent.parent?.type === 'ExportDefaultDeclaration'
    ) {
      return true;
    }
    // 检查类组件
    if (
      currentPath.node.type === 'ClassDeclaration' &&
      currentPath.node.superClass &&
      ((currentPath.node.superClass.type === 'MemberExpression' &&
        currentPath.node.superClass.object.name === 'React' &&
        currentPath.node.superClass.property.name === 'Component') ||
        (currentPath.node.superClass.type === 'Identifier' &&
          currentPath.node.superClass.name === 'Component'))
    ) {
      return true;
    }
    currentPath = currentPath.parentPath;
  }
  return false;
}

// 创建导入声明
function createImportDeclaration(specifiers, source) {
  return t.importDeclaration(
    specifiers.map((name) =>
      t.importSpecifier(t.identifier(name), t.identifier(name))
    ),
    t.stringLiteral(source)
  );
}

// 创建useLingui hook声明
function createUseLinguiDeclaration() {
  return t.variableDeclaration('const', [
    t.variableDeclarator(
      t.objectPattern([
        t.objectProperty(
          t.identifier('i18n'),
          t.identifier('i18n'),
          false,
          true
        ),
        t.objectProperty(t.identifier('t'), t.identifier('t'), false, true)
      ]),
      t.callExpression(t.identifier('useLingui'), [])
    )
  ]);
}

// 处理单个文件
function transformFile(filePath) {
  try {
    // 检查文件是否为 TypeScript/React 文件
    if (!/\.(tsx?|jsx?)$/.test(filePath)) {
      return;
    }

    console.log('处理文件:', filePath);

    const sourceCode = fs.readFileSync(filePath, 'utf-8');
    // 获取文件名(不带扩展名)
    const fileName = path.basename(filePath).split('.')[0];

    // 解析代码生成 AST
    const ast = parser.parse(sourceCode, {
      sourceType: 'module',
      plugins: ['jsx', 'typescript', 'decorators-legacy'],
      tokens: true,
      attachComment: true
    });

    // 用于跟踪需要添加的导入和hooks
    let needsLinguiMacro = false;
    let needsLinguiReact = false;
    let needsLinguiCore = false;
    let hasI18nUsage = false;
    let componentsNeedingUseLingui = new Set();
    let hasExistingLinguiImports = false;

    // 第一次遍历:检查代码中的 i18n 使用情况
    traverse(ast, {
      MemberExpression(path) {
        if (path.node.object.name === 'i18n') {
          hasI18nUsage = true;
          needsLinguiCore = true;
        }
      },
      CallExpression(path) {
        if (
          path.node.callee?.type === 'MemberExpression' &&
          path.node.callee.object.name === 'i18n'
        ) {
          hasI18nUsage = true;
          needsLinguiCore = true;
        }
      }
    });

    // 第二次遍历:检查现有的导入并移除
    traverse(ast, {
      ImportDeclaration(path) {
        const source = path.node.source.value;
        if (
          source === '@lingui/macro' ||
          source === '@lingui/react' ||
          source === '@lingui/react/macro' ||
          source === '@lingui/core/macro' ||
          source === '@lingui/core'
        ) {
          hasExistingLinguiImports = true;
          path.remove();
        }
      }
    });

    // 转换JSX中的中文内容
    traverse(ast, {
      // 处理组件声明
      'FunctionDeclaration|VariableDeclarator|CallExpression'(path) {
        if (!isReactComponentDeclaration(path)) return;

        let componentName;
        if (path.type === 'FunctionDeclaration') {
          componentName = path.node.id.name;
        } else if (path.type === 'VariableDeclarator') {
          componentName = path.node.id.name;
        } else if (
          path.type === 'CallExpression' &&
          path.node.callee.name === 'forwardRef'
        ) {
          // 获取forwardRef组件的名称
          if (path.parent?.type === 'VariableDeclarator') {
            componentName = path.parent.id.name;
          }
        }

        if (componentName) {
          componentsNeedingUseLingui.add(componentName);
          needsLinguiReact = true;
        }
      },

      // 处理JSX属性中的中文
      JSXAttribute: {
        exit(path) {
          if (!isInsideReactComponent(path)) return;

          const value = path.node.value;
          if (
            value &&
            value.type === 'StringLiteral' &&
            containsChinese(value.value)
          ) {
            path.node.value = t.jsxExpressionContainer(
              createTExpression(value.value)
            );
            needsLinguiMacro = true;
            needsLinguiReact = true;
          }
        }
      },

      // 处理JSX文本中的中文
      JSXText: {
        exit(path) {
          if (!isInsideReactComponent(path)) return;

          const text = path.node.value.trim();
          if (containsChinese(text) && text.length > 0) {
            const parent = path.parent;
            if (
              parent.type === 'JSXElement' &&
              parent.openingElement.name.name === 'Trans'
            ) {
              needsLinguiMacro = true; // 即使已经是Trans标签,也需要确保导入
              return;
            }
            path.replaceWith(createTransElement(path.node.value));
            needsLinguiMacro = true;
          }
        }
      },

      // 检查是否使用了Trans组件
      JSXElement(path) {
        if (path.node.openingElement.name.name === 'Trans') {
          needsLinguiMacro = true;
        }
      },

      // 处理字符串字面量
      StringLiteral: {
        exit(path) {
          // 检查是否在枚举声明中
          if (path.findParent((p) => p.type === 'TSEnumDeclaration')) {
            return;
          }

          if (
            !path.node.value ||
            !containsChinese(path.node.value) ||
            path.findParent((p) => p.isImportDeclaration()) ||
            path.findParent(
              (p) =>
                p.isJSXElement() && p.node.openingElement.name.name === 'Trans'
            )
          ) {
            if (
              path.findParent(
                (p) =>
                  p.isJSXElement() &&
                  p.node.openingElement.name.name === 'Trans'
              )
            ) {
              needsLinguiMacro = true;
            }
            return;
          }

          const isInComponent = isInsideReactComponent(path);

          if (isInComponent && path.parent.type === 'JSXAttribute') {
            path.replaceWith(createJSXExpressionContainer(createTExpression(path.node.value)));
            needsLinguiMacro = true;
            needsLinguiReact = true;
            return;
          }

          if (isInComponent) {
            path.replaceWith(createTExpression(path.node.value));
            needsLinguiMacro = true;
            needsLinguiReact = true;
          } else {
            const msgExpression = createMsgExpression(path.node.value);
            // 如果在JSX属性中,需要包装在JSXExpressionContainer中
            if (path.parent.type === 'JSXAttribute') {
              path.replaceWith(createJSXExpressionContainer(msgExpression));
            } else {
              path.replaceWith(msgExpression);
            }
            needsLinguiCore = true;
            needsLinguiMacro = true;
          }
        }
      }
    });

    // 第三次遍历:添加useLingui hook到需要的组件
    traverse(ast, {
      'FunctionDeclaration|ArrowFunctionExpression'(path) {
        let componentName;
        let isForwardRef = false;

        // 处理普通函数组件
        if (path.node.type === 'FunctionDeclaration') {
          componentName = path.node.id?.name;
        }
        // 处理箭头函数组件
        else if (path.parent?.type === 'VariableDeclarator') {
          componentName = path.parent.id?.name;
        }
        // 处理forwardRef组件
        else if (
          path.parent?.type === 'CallExpression' &&
          path.parent.callee?.name === 'forwardRef' &&
          path.parent.parent?.type === 'VariableDeclarator'
        ) {
          componentName = path.parent.parent.id?.name;
          isForwardRef = true;
        }

        if (!componentName || !componentsNeedingUseLingui.has(componentName))
          return;

        const body = path.node.body;
        if (t.isBlockStatement(body)) {
          // 检查是否已经有useLingui声明
          const hasUseLingui = body.body.some(
            (node) =>
              t.isVariableDeclaration(node) &&
              node.declarations.some(
                (dec) =>
                  dec.init?.type === 'CallExpression' &&
                  dec.init.callee.name === 'useLingui'
              )
          );

          if (!hasUseLingui) {
            body.body.unshift(createUseLinguiDeclaration());
          }
        } else if (t.isJSXElement(body)) {
          // 如果直接返回JSX,需要包装在代码块中
          path.node.body = t.blockStatement([
            createUseLinguiDeclaration(),
            t.returnStatement(body)
          ]);
        }
      },

      // 专门处理forwardRef的箭头函数组件
      CallExpression(path) {
        if (
          path.node.callee.name === 'forwardRef' &&
          path.node.arguments.length > 0
        ) {
          const functionArg = path.node.arguments[0];
          let componentName;

          // 获取组件名称
          if (path.parent?.type === 'VariableDeclarator') {
            componentName = path.parent.id.name;
          } else if (path.parent?.type === 'ExportDefaultDeclaration') {
            // 对于export default的情况,使用文件名作为组件名
            componentName = fileName;
          }

          if (componentName) {
            componentsNeedingUseLingui.add(componentName);
            needsLinguiReact = true;

            // 处理函数体
            if (t.isArrowFunctionExpression(functionArg) || t.isFunctionExpression(functionArg)) {
              const body = functionArg.body;
              if (t.isBlockStatement(body)) {
                // 检查是否已经有useLingui声明
                const hasUseLingui = body.body.some(
                  (node) =>
                    t.isVariableDeclaration(node) &&
                    node.declarations.some(
                      (dec) =>
                        dec.init?.type === 'CallExpression' &&
                        dec.init.callee.name === 'useLingui'
                    )
                );

                if (!hasUseLingui) {
                  body.body.unshift(createUseLinguiDeclaration());
                }
              } else if (t.isJSXElement(body)) {
                // 如果直接返回JSX,需要包装在代码块中
                functionArg.body = t.blockStatement([
                  createUseLinguiDeclaration(),
                  t.returnStatement(body)
                ]);
              }
            }
          }
        }
      }
    });

    // 添加必要的导入语句
    const imports = [];

    // 如果有组件外部的 i18n 使用,确保添加相关导入
    if (hasI18nUsage || needsLinguiCore) {
      imports.push(
        createImportDeclaration(['msg'], '@lingui/core/macro'),
        createImportDeclaration(['i18n'], '@lingui/core')
      );
    }

    if (needsLinguiMacro || needsLinguiReact) {
      imports.push(
        createImportDeclaration(['useLingui', 'Trans'], '@lingui/react/macro')
      );
    }

    // 在文件开头添加导入语句
    if (imports.length > 0) {
      ast.program.body.unshift(...imports);
    }

    // 生成转换后的代码
    const output = generate(ast, {
      retainLines: true,
      compact: 'auto',
      concise: false,
      jsescOption: {
        minimal: true
      },
      sourceMaps: false,
      comments: true
    });

    // 直接覆盖源文件
    fs.writeFileSync(filePath, output.code);
    console.log('已更新文件:', filePath);
  } catch (error) {
    console.error(`处理文件 ${filePath} 时出错:`, error);
  }
}

// 处理文件夹
function transformDirectory(dirPath) {
  try {
    const files = fs.readdirSync(dirPath);

    files.forEach((file) => {
      const fullPath = path.join(dirPath, file);
      const stat = fs.statSync(fullPath);

      if (stat.isDirectory()) {
        // 递归处理子文件夹
        transformDirectory(fullPath);
      } else {
        // 处理文件
        transformFile(fullPath);
      }
    });
  } catch (error) {
    console.error(`处理文件夹 ${dirPath} 时出错:`, error);
  }
}

// 获取命令行参数
const targetPath = process.argv[2];

if (!targetPath) {
  console.error('请提供文件或文件夹路径!');
  console.log('使用方法: node transform-lingui-ast.js <文件或文件夹路径>');
  process.exit(1);
}

// 检查路径是否存在
if (!fs.existsSync(targetPath)) {
  console.error('指定的路径不存在!');
  process.exit(1);
}

// 判断是文件还是文件夹
const stat = fs.statSync(targetPath);
if (stat.isDirectory()) {
  transformDirectory(targetPath);
} else {
  transformFile(targetPath);
}

如何使用

执行下面命令,它就会把对应的中文用特定的标签进行包裹

npm run scripts/transform-lingui-ast.mjs

组件依赖版本

{
    "scripts": {
      "i18n:extract": "lingui extract",
      "i18n:compile": "lingui compile"
    },
    "dependencies": {
      "@lingui/core": "^5.1.0",
      "@lingui/macro": "^5.1.0",
      "@lingui/react": "^5.1.0",
    },
    "devDependencies": {
      "@lingui/babel-plugin-lingui-macro": "^5.1.0",
      "@lingui/cli": "^5.1.0",
      "@lingui/vite-plugin": "^5.1.0",
      "@babel/parser": "^7.26.3",
      "@babel/types": "^7.26.3",
    }
}

  

posted @ 2024-12-10 16:29  冯叶青  阅读(339)  评论(0)    收藏  举报