为迷你商店实现国际化
一份实操性的 React 教程,使用 GT React 组件、hooks 和共享字符串为一个简单商店实现国际化
启动并本地运行一个小巧的“迷你商店”,并完成翻译——无需外部服务、无需路由、无需 UI 框架。你将端到端使用 GT 的核心 React 特性,并在一个简单且贴近真实的 UI 中了解它们如何协同工作。
先决条件:React、基础 JavaScript/TypeScript
你将构建的内容
- 一个单页“商店”,包含产品网格和简单的内存购物车
- 语言切换器和共享的导航文案
- 正确国际化的数字、货币和复数
- 可选:用于生产构建的本地翻译存储
本教程用到的链接
- Provider:
<GTProvider> - Components:
<T>,<Var>,<Num>,<Currency>,<DateTime>,<Branch>,<Plural>,<LocaleSelector> - Strings 与 Shared Strings:
useGT,msg,useMessages - 指南:Variables、Branching、Strings、Local Translation Storage、Changing Languages
安装并包装应用
安装依赖,并使用 provider 包装你的应用。
npm i gt-react
npm i --save-dev gtx-cliyarn add gt-react
yarn add --dev gtx-clibun add gt-react
bun add --dev gtx-clipnpm 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.tsx、src/App.tsx、src/components/*、src/data.ts、src/nav.ts)。
创建一个最小的 provider 配置。
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 和本地存储会很有用):
{
"defaultLocale": "en",
"locales": ["es", "fr"]
}开发环境 API 密钥
你可以在没有密钥的情况下完成本教程(将仅渲染默认语言)。要在开发环境中查看实时翻译并测试语言切换,请添加开发用密钥。
在生产环境 vs 开发环境中了解更多。
VITE_GT_API_KEY="your-dev-api-key"
VITE_GT_PROJECT_ID="your-project-id"REACT_APP_GT_API_KEY="your-dev-api-key"
REACT_APP_GT_PROJECT_ID="your-project-id"- Dashboard: https://dash.generaltranslation.com/signup
- 或通过命令行界面(CLI):
npx gtx-cli auth
种子数据与应用结构
我们将硬编码一个很小的商品数组,所有逻辑都放在客户端完成。无需服务器,也无需路由。
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()
}
];使用 msg 和 useMessages 的共享导航标签
在配置中用 msg 标记可复用的导航文案,并在渲染时通过 useMessages 进行解析。
import { msg } from 'gt-react'; // 参见:/docs/react/api/strings/msg
export const nav = [
{ label: msg('首页'), href: '#' },
{ label: msg('产品'), href: '#products' },
{ label: msg('购物车'), href: '#cart' }
];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>Mini Shop</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>处理库存状态。
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> 组合使用。
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 标签。
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 }}
/>
);
}综合示例
一个带有内存购物车和简单搜索筛选的单页应用。
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 中添加一个简单的开发脚本,然后启动应用。
{
"scripts": {
"dev": "vite"
}
}运行:
npm run dev{
"scripts": {
"start": "react-scripts start"
}
}运行:
npm start你学到了什么
- 使用
<T>翻译 JSX,并通过<Var>、<Num>、<Currency>、<DateTime>处理动态内容 - 使用
<Branch>表达条件内容,并用<Plural>表达数量变化 - 使用
useGT翻译属性 - 使用
msg共享导航/配置字符串,并通过useMessages解码 - 使用
<LocaleSelector>切换语言
下一步
本指南如何?