267 lines
6.5 KiB
Markdown
267 lines
6.5 KiB
Markdown
|
# 国际化
|
|||
|
|
|||
|
## 基础概念
|
|||
|
|
|||
|
多语言国际化支持根据服务端和客户端分为两大部分,各自有相应的实现。
|
|||
|
|
|||
|
语言配置均为普通的 JSON 对象键值对,如果没有翻译文件(或省略编写)的话会直接输出键名的字符串。
|
|||
|
|
|||
|
### 服务端
|
|||
|
|
|||
|
服务端的多语言国际化基于 npm 包 [i18next](https://npmjs.com/package/i18next) 实现,在服务端应用初始化时会创建一个 i18next 实例,同时也会将此实例注入到请求上下文(`context`)中,以供在各处方便的使用。
|
|||
|
|
|||
|
在创建服务端 Application 实例时可以传入配置对应的初始化参数:
|
|||
|
|
|||
|
```ts
|
|||
|
import { Application } from '@nocobase/server';
|
|||
|
|
|||
|
const app = new Application({
|
|||
|
i18n: {
|
|||
|
defaultNS: 'test',
|
|||
|
resources: {
|
|||
|
'en-US': {
|
|||
|
test: {
|
|||
|
hello: 'Hello',
|
|||
|
},
|
|||
|
},
|
|||
|
'zh-CN': {
|
|||
|
test: {
|
|||
|
hello: '你好',
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
```
|
|||
|
|
|||
|
或者在插件中对已存在的 app 实例添加语言数据至对应命名空间:
|
|||
|
|
|||
|
```ts
|
|||
|
app.i18n.addResources('zh-CN', 'test', {
|
|||
|
Hello: '你好',
|
|||
|
World: '世界',
|
|||
|
});
|
|||
|
|
|||
|
app.i18n.addResources('en-US', 'test', {
|
|||
|
Hello: 'Hello',
|
|||
|
World: 'World',
|
|||
|
});
|
|||
|
```
|
|||
|
|
|||
|
基于应用:
|
|||
|
|
|||
|
```ts
|
|||
|
app.i18n.t('World') // “世界”或“World”
|
|||
|
```
|
|||
|
|
|||
|
基于请求:
|
|||
|
|
|||
|
```ts
|
|||
|
app.resource({
|
|||
|
name: 'test',
|
|||
|
actions: {
|
|||
|
async get(ctx, next) {
|
|||
|
ctx.body = `${ctx.t('Hello')} ${ctx.t('World')}`;
|
|||
|
await next();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
```
|
|||
|
|
|||
|
通常服务端的多语言处理主要用于错误信息的输出。
|
|||
|
|
|||
|
### 客户端
|
|||
|
|
|||
|
客户端的多语言国际化基于 npm 包 [react-i18next](https://npmjs.com/package/react-i18next) 实现,在应用顶层提供了 `<I18nextProvider>` 组件的包装,可以在任意位置直接使用相关的方法。
|
|||
|
|
|||
|
添加语言包:
|
|||
|
|
|||
|
```tsx | pure
|
|||
|
import { i18n } from '@nocobase/client';
|
|||
|
|
|||
|
i18n.addResources('zh-CN', 'test', {
|
|||
|
Hello: '你好',
|
|||
|
World: '世界',
|
|||
|
});
|
|||
|
```
|
|||
|
|
|||
|
注:这里第二个参数填写的 `'test'` 是语言的命名空间,通常插件自己定义的语言资源都应该按自己插件包名创建特定的命名空间,以避免和其他语音资源冲突。NocoBase 中默认的命名空间是 `'client'`,大部分常用和基础的语言翻译都放置在此命名空间,当没有提供所需语言时,可在插件自身的命名空间内进行扩展定义。
|
|||
|
|
|||
|
在组件中调用翻译函数:
|
|||
|
|
|||
|
```tsx | pure
|
|||
|
import React from 'react';
|
|||
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
|||
|
export default function MyComponent() {
|
|||
|
// 使用之前定义的命名空间
|
|||
|
const { t } = useTranslation('test');
|
|||
|
|
|||
|
return (
|
|||
|
<div>
|
|||
|
<p>{t('World')}</p>
|
|||
|
</div>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
在 SchemaComponent 组件中可以直接使用模板方法 `'{{t(<languageKey>)}}'`,模板中的翻译函数会自动被执行:
|
|||
|
|
|||
|
```tsx | pure
|
|||
|
import React from 'react';
|
|||
|
import { SchemaComponent } from '@nocobase/client';
|
|||
|
|
|||
|
export default function MySchemaComponent() {
|
|||
|
return (
|
|||
|
<SchemaComponent
|
|||
|
schema={{
|
|||
|
type: 'string',
|
|||
|
'x-component': 'Input',
|
|||
|
'x-component-props': {
|
|||
|
value: '{{t("Hello", { ns: "test" })}}'
|
|||
|
},
|
|||
|
}}
|
|||
|
/>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
在某些特殊情况下也需要以模板的方式定义多语言时,可以使用 NocoBase 内置的 `compile()` 方法编译为多语言结果:
|
|||
|
|
|||
|
```tsx | pure
|
|||
|
import React from 'react';
|
|||
|
import { useCompile } from '@nocobase/client';
|
|||
|
|
|||
|
const title = '{{t("Hello", { ns: "test" })}}';
|
|||
|
|
|||
|
export default function MyComponent() {
|
|||
|
const { compile } = useCompile();
|
|||
|
|
|||
|
return (
|
|||
|
<div>{compile(title)}</div>
|
|||
|
);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### 建议配置
|
|||
|
|
|||
|
当添加语言资源时,推荐在 JSON 配置模板中将语言串的键名设置为默认语言,更方便统一处理,且可以省去默认语言的翻译。例如以英语为默认语言:
|
|||
|
|
|||
|
```ts
|
|||
|
i18n.addResources('zh-CN', 'your-namespace', {
|
|||
|
'Show dialog': '显示对话框',
|
|||
|
'Hide dialog': '隐藏对话框'
|
|||
|
});
|
|||
|
```
|
|||
|
|
|||
|
语言内容如果比较多,推荐在插件中创建一个 `locals` 目录,并把对应语言文件都放置在其中方便管理:
|
|||
|
|
|||
|
```
|
|||
|
- server
|
|||
|
| - locals
|
|||
|
| | - zh-CN.ts
|
|||
|
| | - en-US.ts
|
|||
|
| | - ...
|
|||
|
| - index.ts
|
|||
|
```
|
|||
|
|
|||
|
## 示例
|
|||
|
|
|||
|
### 服务端错误提示
|
|||
|
|
|||
|
例如用户在店铺对某个商品下单时,如果商品的库存不够,或者未上架,那么下单接口被调用时,应该返回相应的错误。
|
|||
|
|
|||
|
```ts
|
|||
|
const namespace = 'shop';
|
|||
|
|
|||
|
export default class ShopPlugin extends Plugin {
|
|||
|
async load() {
|
|||
|
this.app.i18n.addResources('zh-CN', namespace, {
|
|||
|
'No such product': '商品不存在',
|
|||
|
'Product not on sale': '商品已下架',
|
|||
|
'Out of stock': '库存不足',
|
|||
|
});
|
|||
|
|
|||
|
this.app.resource({
|
|||
|
name: 'orders',
|
|||
|
actions: {
|
|||
|
async create(ctx, next) {
|
|||
|
const productRepo = ctx.db.getRepository('products');
|
|||
|
const product = await productRepo.findOne({
|
|||
|
filterByTk: ctx.action.params.values.productId
|
|||
|
});
|
|||
|
|
|||
|
if (!product) {
|
|||
|
return ctx.throw(404, ctx.t('No such product'));
|
|||
|
}
|
|||
|
|
|||
|
if (!product.enabled) {
|
|||
|
return ctx.throw(400, ctx.t('Product not on sale'));
|
|||
|
}
|
|||
|
|
|||
|
if (!product.inventory) {
|
|||
|
return ctx.throw(400, ctx.t('Out of stock'));
|
|||
|
}
|
|||
|
|
|||
|
const orderRepo = ctx.db.getRepository('orders');
|
|||
|
ctx.body = await orderRepo.create({
|
|||
|
values: {
|
|||
|
productId: product.id,
|
|||
|
quantity: 1,
|
|||
|
totalPrice: product.price,
|
|||
|
userId: ctx.state.currentUser.id
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
next();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### 客户端组件多语言
|
|||
|
|
|||
|
例如订单状态的组件,根据不同值有不同的文本显示:
|
|||
|
|
|||
|
```tsx
|
|||
|
import React from 'react';
|
|||
|
import { Select } from 'antd';
|
|||
|
import { i18n } from '@nocobase/client';
|
|||
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
|||
|
i18n.addResources('zh-CN', '@nocobase/plugin-sample-shop-i18n', {
|
|||
|
Pending: '已下单',
|
|||
|
Paid: '已支付',
|
|||
|
Delivered: '已发货',
|
|||
|
Received: '已签收'
|
|||
|
});
|
|||
|
|
|||
|
const ORDER_STATUS_LIST = [
|
|||
|
{ value: -1, label: 'Canceled (untranslated)' },
|
|||
|
{ value: 0, label: 'Pending' },
|
|||
|
{ value: 1, label: 'Paid' },
|
|||
|
{ value: 2, label: 'Delivered' },
|
|||
|
{ value: 3, label: 'Received' },
|
|||
|
]
|
|||
|
|
|||
|
function OrderStatusSelect() {
|
|||
|
const { t } = useTranslation('@nocobase/plugin-sample-shop-i18n');
|
|||
|
|
|||
|
return (
|
|||
|
<Select style={{ minWidth: '8em' }}>
|
|||
|
{ORDER_STATUS_LIST.map(item => (
|
|||
|
<Select.Option value={item.value}>{t(item.label)}</Select.Option>
|
|||
|
))}
|
|||
|
</Select>
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
export default function () {
|
|||
|
return (
|
|||
|
<OrderStatusSelect />
|
|||
|
);
|
|||
|
}
|
|||
|
```
|