共享字符串

如何对跨多个组件和文件使用的字符串进行国际化

共享字符串是指在应用的多个位置使用的文本值,例如导航标签、表单消息或配置数据。与其在各处重复编写翻译逻辑,不如使用 msg 标注需要翻译的字符串,并配合 useMessages 进行解码。

共享内容的问题

设想一份在整个应用中通用的导航配置:

// navData.ts
export const navData = [
  {
    label: '首页',
    description: '首页',
    href: '/'
  },
  {
    label: '关于', 
    description: '关于公司',
    href: '/about'
  }
];

要实现国际化,你通常需要:

  1. 将其改写为可接受翻译函数的函数
  2. 将所有使用处更新为用 t 调用该函数
  3. 在整个代码库中管理随之而来的复杂性

这会增加维护负担,并让代码更难阅读。msg 函数通过支持在原位标记待翻译字符串,并在需要时再解码,来解决这个问题。

快速开始

使用 msg 标记字符串,使用 useMessages 解码它们:

// navData.ts - 标记字符串以供翻译
import { msg } from 'gt-react';

export const navData = [
  {
    label: msg('首页'),
    description: msg('首页'), 
    href: '/'
  },
  {
    label: msg('关于'),
    description: msg('关于公司的信息'),
    href: '/about'
  }
];
// 组件用法 - 解码已标记的字符串
import { useMessages } from 'gt-react';
import { navData } from './navData';

function Navigation() {
  const m = useMessages();
  
  return (
    <nav>
      {navData.map((item) => (
        <a key={item.href} href={item.href} title={m(item.description)}>
          {m(item.label)}
        </a>
      ))}
    </nav>
  );
}

共享字符串的工作原理

共享字符串系统分为两个阶段:

  1. 标记阶段msg 使用翻译元数据对字符串进行编码
  2. 解码阶段useMessages 对字符串进行解码并翻译
// msg() 使用元数据对字符串进行编码
const encoded = msg('Hello, world!');
console.log(encoded); // "Hello, world!:eyIkX2hhc2giOiJkMjA3MDliZGExNjNlZmM2In0="

// useMessages() 解码并翻译
const m = useMessages();
const translated = m(encoded); // 以用户的语言显示 "Hello, world!"

来自msg的 encoded string 无法直接使用,必须通过 useMessages 解码后方可使用。

组件

使用 useMessages 钩子:

import { useMessages } from 'gt-react';

const encodedString = msg('Hello, world!');

function MyComponent() {
  const m = useMessages();
  return <div>{m(encodedString)}</div>;
}

使用 decodeMsg 获取原始字符串

有时你需要直接访问未翻译的原始字符串,例如用于日志、调试或对比。使用 decodeMsg 提取原始文本:

import { decodeMsg } from 'gt-react';

const encoded = msg('Hello, world!');
const original = decodeMsg(encoded); // "Hello, world!" (original)
const translated = m(encoded); // "Hello, world!" (in user's language)

// 用于日志记录或调试
console.log('原始字符串:', decodeMsg(encoded));
console.log('已翻译字符串:', m(encoded));

decodeMsg 的使用场景

  • 开发与调试:记录原始字符串以便排查问题
  • 回退处理:翻译失败时使用原文
  • 字符串比较:与已知原始值进行比对
  • 分析:追踪原始字符串的使用情况
// 示例:回退处理
function getDisplayText(encodedStr) {
  const m = useMessages();
  try {
    return m(encodedStr);
  } catch (error) {
    console.warn('翻译失败,使用原文:', decodeMsg(encodedStr));
    return decodeMsg(encodedStr);
  }
}

使用变量

对于包含动态内容的字符串,请使用占位符并传入变量:

// Mark string with variables
const items = 100;
export const pricing = [
  {
    name: '基础版',
    price: 100,
    description: msg('基础版包含 {items} 个项目', { items })
  }
];
// 在组件中使用
function PricingCard() {
  const m = useMessages();
  
  return (
    <div>
      <h3>{pricing[0].name}</h3>
      <p>{m(pricing[0].description)}</p>
    </div>
  );
}

ICU 消息格式

要进行更高级的格式化,请使用 ICU 语法:

const count = 10;
const message = msg('购物车中有 {count, plural, =0 {0 件商品} =1 {1 件商品} other {{count} 件商品}}', { count });

Unicode 文档中进一步了解 ICU 消息格式。

示例

导航配置

// config/navigation.ts
import { msg } from 'gt-react';

export const mainNav = [
  {
    label: msg('首页'),
    href: '/',
    icon: 'home'
  },
  {
    label: msg('产品'),
    href: '/products', 
    icon: 'package'
  },
  {
    label: msg('关于我们'),
    href: '/about',
    icon: 'info'
  }
];

export const footerLinks = [
  {
    title: msg('公司'),
    links: [
      { label: msg('关于我们'), href: '/about' },
      { label: msg('加入我们'), href: '/careers' },
      { label: msg('联系我们'), href: '/contact' }
    ]
  },
  {
    title: msg('支持'), 
    links: [
      { label: msg('帮助中心'), href: '/help' },
      { label: msg('文档'), href: '/docs' },
      { label: msg('API 参考'), href: '/api' }
    ]
  }
];
// components/Navigation.tsx
import { useMessages } from 'gt-react';
import { mainNav } from '../config/navigation';

function Navigation() {
  const m = useMessages();
  
  return (
    <nav>
      {mainNav.map((item) => (
        <a key={item.href} href={item.href}>
          <Icon name={item.icon} />
          {m(item.label)}
        </a>
      ))}
    </nav>
  );
}

表单配置

// config/forms.ts
import { msg } from 'gt-react';

export const formMessages = {
  placeholders: {
    email: msg('请输入您的电子邮箱地址'),
    password: msg('请输入您的密码'),
    message: msg('在此输入您的消息…')
  },
  actions: {
    send: msg('发送消息'),
    save: msg('保存更改'),
    cancel: msg('取消')
  },
  validation: {
    required: msg('此字段为必填'),
    email: msg('请输入有效的电子邮箱地址'),
    minLength: msg('至少需要 {min} 个字符', { min: 8 }),
    maxLength: msg('不得超过 {max} 个字符', { max: 100 })
  },
  success: {
    saved: msg('更改已保存'),
    sent: msg('消息已发送'),
    updated: msg('个人资料已更新')
  },
  errors: {
    network: msg('网络错误—请重试'),
    server: msg('服务器错误—请联系支持'),
    timeout: msg('请求超时—请重试')
  }
};
// components/ContactForm.tsx
import { useMessages } from 'gt-react';
import { formMessages } from '../config/forms';

function ContactForm() {
  const m = useMessages();
  const [errors, setErrors] = useState({});
  
  return (
    <form>
      <input 
        type="email"
        placeholder={m(formMessages.placeholders.email)}
        required
      />
      {errors.email && <span>{m(formMessages.validation.email)}</span>}
      
      <button type="submit">
        {m(formMessages.actions.send)}
      </button>
    </form>
  );
}

动态内容生成

// utils/productData.ts
import { msg } from 'gt-react';

function mockProducts() {
  return [
    { name: 'iPhone 15', company: 'Apple', category: '电子产品' },
    { name: 'Galaxy S24', company: 'Samsung', category: '电子产品' }
  ];
}

export function getProductData() {
  const products = mockProducts();
  
  return products.map(product => ({
    ...product,
    description: msg('{name} 是 {company} 推出的{category}产品', {
      name: product.name,
      category: product.category,
      company: product.company
    })
  }));
}
// components/ProductList.tsx
import { useMessages } from 'gt-react';
import { getProductData } from '../utils/productData';

function ProductList() {
  const m = useMessages();
  const products = getProductData();
  
  return (
    <div>
      {products.map(product => (
        <div key={product.name}>
          <h3>{product.name}</h3>
          <p>{m(product.description)}</p>
        </div>
      ))}
    </div>
  );
}

常见问题

直接使用编码字符串

切勿直接使用msg的输出:

// ❌ 错误 - 直接使用编码字符串
const encoded = msg('Hello, world!');
return <div>{encoded}</div>; // 显示编码字符串,而非翻译

// ✅ 正确 - 先解码字符串
const encoded = msg('Hello, world!');
const m = useMessages();
return <div>{m(encoded)}</div>; // 显示正确翻译

msg() 中的动态内容

字符串必须在构建阶段已确定:

// ❌ 错误 - 动态模板字面量
const name = 'John';
const message = msg(`Hello, ${name}`); // 构建时错误

// ✅ 正确 - 使用变量  
const name = 'John';
const message = msg('Hello, {name}', { name });

忘记解码

每个 msg 字符串都需要解码:

// ❌ 缺少解码
const config = {
  title: msg('Dashboard'),
  subtitle: msg('欢迎回来')
};

// 在组件中稍后使用时——忘记解码
return <h1>{config.title}</h1>; // 显示为已编码的字符串

// ✅ 正确做法——在使用时解码
const m = useMessages();
return <h1>{m(config.title)}</h1>; // 显示已翻译的标题

后续步骤

本指南如何?