让迷你商店实现国际化

一篇实践向的 React 教程,使用 GT React 的组件、hooks 与共享字符串为一个简单商店实现国际化

运行并翻译一个小型、完全本地的“迷你商店”——无需外部服务、无需路由、无需 UI 框架。你将端到端使用 GT React 的核心功能,并在一个简单且贴近真实的 UI 中了解它们如何协同配合。

前置要求:React、基础 JavaScript/TypeScript

你将构建

  • 一个包含产品网格和简单内存购物车的单页“商店”
  • 语言切换器和共享导航标签
  • 正确国际化的数字、货币与复数
  • 可选:用于生产构建的本地翻译存储

本教程用到的链接


安装并包装你的应用

安装依赖并使用 provider 包装你的应用。

npm i gt-react
npm i --save-dev gtx-cli
yarn add gt-react
yarn add --dev gtx-cli
bun add gt-react
bun add --dev gtx-cli
pnpm add gt-react
pnpm add --save-dev gtx-cli

可选:入门项目(Vite)

如果你从零开始,可先用脚手架创建一个 Vite React + TypeScript 应用,然后安装 GT 包:

npm create vite@latest mini-shop -- --template react-ts
cd mini-shop
npm i gt-react
npm i --save-dev gtx-cli

接着按下方各节添加相应文件(例如 src/main.tsxsrc/App.tsxsrc/components/*src/data.tssrc/nav.ts)。

创建一个最小的 provider 配置。

src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { GTProvider } from 'gt-react'; // 参见:/docs/react/api/components/gtprovider
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <GTProvider locales={["es", "fr"]}> {/* 启用西班牙语和法语 */}
      <App />
    </GTProvider>
  </StrictMode>
);

可选:现在添加一个 gt.config.json(之后用于 CI 和本地存储会很有用):

gt.config.json
{
  "defaultLocale": "en",
  "locales": ["es", "fr"]
}

开发环境 API Keys

即使没有密钥,你也可以按照本教程操作(将渲染默认语言)。若要在开发环境中查看实时翻译并测试语言切换,请添加开发用密钥。

生产环境 vs 开发环境了解更多。

.env.local
VITE_GT_API_KEY="your-dev-api-key"
VITE_GT_PROJECT_ID="your-project-id"
.env.local
REACT_APP_GT_API_KEY="your-dev-api-key"
REACT_APP_GT_PROJECT_ID="your-project-id"

种子数据与应用结构

我们会手写一个很小的产品数组,并且把所有逻辑都放在客户端完成。无需服务器,也无需路由。

src/data.ts
export type Product = {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: 'USD' | 'EUR';
  inStock: boolean;
  addedAt: string; // ISO date string
};

export const products: Product[] = [
  {
    id: 'p-1',
    name: '无线耳机',
    description: '降噪头戴式设计,30小时续航',
    price: 199.99,
    currency: 'USD',
    inStock: true,
    addedAt: new Date().toISOString()
  },
  {
    id: 'p-2',
    name: '旅行杯',
    description: '双壁保温不锈钢(12盎司)',
    price: 24.5,
    currency: 'USD',
    inStock: false,
    addedAt: new Date().toISOString()
  }
];

使用 msguseMessages 的共享导航文案

在配置中使用 msg 标注共享字符串,并在渲染时通过 useMessages 将其解码。

src/nav.ts
import { msg } from 'gt-react'; // See: /docs/react/api/strings/msg

export const nav = [
  { label: msg('首页'), href: '#' },
  { label: msg('产品'), href: '#products' },
  { label: msg('购物车'), href: '#cart' }
];
src/components/Header.tsx
import { LocaleSelector, T } from 'gt-react';
import { useMessages } from 'gt-react'; // 参见:/docs/react/api/strings/useMessages
import { nav } from '../nav';

export default function Header() {
  const m = useMessages();
  return (
    <header style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
      <T><h1>迷你商店</h1></T> {/* 参见:/docs/react/api/components/t */}
      <nav style={{ display: 'flex', gap: 12 }}>
        {nav.map(item => (
          <a key={item.href} href={item.href} title={m(item.label)}>
            {m(item.label)}
          </a>
        ))}
      </nav>
      <div style={{ marginLeft: 'auto' }}>
        <LocaleSelector /> {/* 参见:/docs/react/api/components/localeSelector */}
      </div>
    </header>
  );
}

使用 <T>、变量、Branch 和 Currency 的产品卡片

使用 <T> 进行 JSX 翻译。将动态内容用变量组件包裹,例如 <Var><Num><Currency><DateTime>。通过 <Branch> 处理库存状态。

src/components/ProductCard.tsx
import { T, Var, Num, Currency, DateTime, Branch } from 'gt-react';
import type { Product } from '../data';

export default function ProductCard({ product, onAdd }: { product: Product; onAdd: () => void; }) {
  return (
    <div style={{ border: '1px solid #ddd', padding: 12, borderRadius: 8 }}>
      <T>
        <h3><Var>{product.name}</Var></h3>
        <p><Var>{product.description}</Var></p>
        <p>
          价格:<Currency currency={product.currency}>{product.price}</Currency>
        </p>
        <p>
          添加时间:<DateTime options={{ dateStyle: 'medium', timeStyle: 'short' }}>{product.addedAt}</DateTime>
        </p>
        <Branch
          branch={product.inStock}
          true={<p>现货</p>}
          false={<p style={{ color: 'tomato' }}>缺货</p>}
        />
        <button onClick={onAdd} disabled={!product.inStock}>
          加入购物车
        </button>
      </T>
    </div>
  );
}

购物车的复数化与总计

使用 <Plural> 表达“购物车中有 X 件商品”,并使用 <Currency> 显示总计。可与 <T><Var><Num> 组合使用。

src/components/Cart.tsx
import { T, Plural, Var, Num, Currency } from 'gt-react';
import type { Product } from '../data';

export default function Cart({ items, onClear }: { items: Product[]; onClear: () => void; }) {
  const total = items.reduce((sum, p) => sum + p.price, 0);
  const itemCount = items.length;
  return (
    <div style={{ borderTop: '1px solid #eee', paddingTop: 12 }}>
      <T>
        <h2>购物车</h2>
        <Plural
          n={itemCount}
          zero={<p>你的购物车空空如也</p>}
          one={<p>你有 <Num>{itemCount}</Num> 件商品</p>}
          other={<p>你有 <Num>{itemCount}</Num> 件商品</p>}
        />
        {items.map((p) => (
          <p key={p.id}>
            <Var>{p.name}</Var> — <Currency currency={p.currency}>{p.price}</Currency>
          </p>
        ))}
        <p>
          合计:<Currency currency={items[0]?.currency || 'USD'}>{total}</Currency>
        </p>
        <button onClick={onClear} disabled={itemCount === 0}>清空购物车</button>
      </T>
    </div>
  );
}

使用 useGT 处理属性和占位符

使用 useGT 来翻译纯文本字符串,例如输入占位符和 ARIA 标签。

src/components/Search.tsx
import { useGT } from 'gt-react';

export default function Search({ onQuery }: { onQuery: (q: string) => void; }) {
  const t = useGT();
  return (
    <input
      type="search"
      placeholder={t('搜索产品...')}
      aria-label={t('搜索输入')}
      onChange={(e) => onQuery(e.target.value)}
      style={{ padding: 8, width: '100%', maxWidth: 320 }}
    />
  );
}

综合实现

一个带内存购物车和简单搜索筛选的单页应用。

src/App.tsx
import { useMemo, useState } from 'react';
import Header from './components/Header';
import Search from './components/Search';
import ProductCard from './components/ProductCard';
import Cart from './components/Cart';
import { products } from './data';

export default function App() {
  const [query, setQuery] = useState('');
  const [cart, setCart] = useState<string[]>([]);

  const filtered = useMemo(() => {
    const q = query.toLowerCase();
    return products.filter(p =>
      p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)
    );
  }, [query]);

  const items = products.filter(p => cart.includes(p.id));

  return (
    <div style={{ margin: '24px auto', maxWidth: 960, padding: 16 }}>
      <Header />
      <div style={{ margin: '16px 0' }}>
        <Search onQuery={setQuery} />
      </div>
      <section id="products" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 16 }}>
        {filtered.map(p => (
          <ProductCard
            key={p.id}
            product={p}
            onAdd={() => setCart(c => (p.inStock ? [...new Set([...c, p.id])] : c))}
          />
        ))}
      </section>
      <section id="cart" style={{ marginTop: 24 }}>
        <Cart
          items={items}
          onClear={() => setCart([])}
        />
      </section>
    </div>
  );
}

本地运行

在你的 package.json 中添加一个简单的开发脚本,然后启动应用。

package.json
{
  "scripts": {
    "dev": "vite"
  }
}

执行:

npm run dev
package.json
{
  "scripts": {
    "start": "react-scripts start"
  }
}

执行:

npm start

你学到了什么

后续步骤

这份指南怎么样?

让迷你商店实现国际化