# Client-Side i18n

> Read when adding internationalization (translations) to client-side plugin code.

## Locale File Structure

```
my-plugin/
└── src/
    ├── locale/
    │   ├── zh-CN.json
    │   └── en-US.json
    └── client-v2/
        ├── locale.ts          # auto-generated, provides tExpr and useT
        └── plugin.tsx
```

## Template: Locale Files

```json
// src/locale/zh-CN.json
{
  "My Feature": "我的功能",
  "Save": "保存",
  "Created successfully": "创建成功",
  "Your name is {{name}}": "你的名字是 {{name}}"
}
```

```json
// src/locale/en-US.json
{
  "My Feature": "My Feature",
  "Save": "Save",
  "Created successfully": "Created successfully",
  "Your name is {{name}}": "Your name is {{name}}"
}
```

Keys are English originals -- this provides automatic English fallback if a translation is missing.

## Template: locale.ts (Auto-Generated)

This file is generated by the plugin scaffold. Do NOT modify unless necessary.

```ts
// src/client-v2/locale.ts
import { tExpr as _tExpr, useFlowEngine } from '@nocobase/flow-engine';
// @ts-ignore
import pkg from './../../package.json';

export function useT() {
  const engine = useFlowEngine();
  return (str: string) => engine.context.t(str, { ns: [pkg.name, 'client'] });
}

export function tExpr(key: string) {
  return _tExpr(key, { ns: [pkg.name, 'client'] });
}
```

## Three Usage Patterns

### 1. `this.t()` in Plugin load()

Auto-injects plugin package name as namespace. Use for runtime strings in load().

```ts
import { Plugin, Application } from '@nocobase/client-v2';

export class MyPlugin extends Plugin<any, Application> {
  async load() {
    this.pluginSettingsManager.addMenuItem({
      key: 'my-feature',
      title: this.t('My Feature'),  // runtime translation
      icon: 'SettingOutlined',
    });
    this.pluginSettingsManager.addPageTabItem({
      menuKey: 'my-feature',
      key: 'index',
      title: this.t('My Feature'),
      componentLoader: () => import('./pages/MySettingsPage'),
    });
  }
}
```

### 2. `useT()` in React Components

Returns a `t` function bound to plugin namespace. Use inside component render.

```tsx
import React from 'react';
import { Button } from 'antd';
import { useT } from '../locale';

export default function MyPage() {
  const t = useT();

  return (
    <div>
      <h2>{t('My Feature')}</h2>
      <p>{t('Your name is {{name}}', { name: 'NocoBase' })}</p>
      <Button>{t('Save')}</Button>
    </div>
  );
}
```

### 3. `tExpr()` in Static Definitions

Generates a deferred translation expression (`'{{t("key")}}'`). Use in `define()`, `registerFlow()`, and `defaultProps` -- anywhere that executes at module load time before i18n is initialized.

```tsx
import { BlockModel } from '@nocobase/client-v2';
import { tExpr } from '../locale';  // IMPORTANT: from locale.ts, not @nocobase/flow-engine

export class MyBlockModel extends BlockModel {
  renderComponent() {
    return <div>My block</div>;
  }
}

MyBlockModel.define({
  label: tExpr('My block'),          // deferred translation
});

MyBlockModel.registerFlow({
  key: 'settings',
  title: tExpr('My block settings'), // deferred translation
  on: 'beforeRender',
  steps: {
    edit: {
      title: tExpr('Edit content'),   // deferred translation
      uiSchema: {
        content: {
          type: 'string',
          title: tExpr('Content'),     // deferred translation
          'x-decorator': 'FormItem',
          'x-component': 'Input.TextArea',
        },
      },
      handler(ctx, params) {
        ctx.model.props.content = params.content;
      },
    },
  },
});
```

## Quick Reference

| Context | Function | Source | Namespace |
|---|---|---|---|
| Plugin `load()` | `this.t('key')` | Plugin base class | Auto-injected |
| React component | `const t = useT(); t('key')` | `../locale` | Bound in locale.ts |
| Static definition (`define`, `registerFlow`, `defaultProps`) | `tExpr('key')` | `../locale` | Bound in locale.ts |
| Flow handler | `ctx.t('key')` | FlowRuntimeContext | Requires manual `{ ns: 'pkg-name' }` |

## IMPORTANT: tExpr Import Source

Always import `tExpr` from the plugin's own `locale.ts`, NOT directly from `@nocobase/flow-engine`.

```ts
// CORRECT -- bound to plugin namespace
import { tExpr } from '../locale';

// WRONG -- no namespace binding, translations won't match
import { tExpr } from '@nocobase/flow-engine';
```

The `locale.ts` file wraps the flow-engine's `tExpr` with the plugin's package name as namespace. Without this binding, translations cannot be resolved.

## Key Points

- First-time locale file addition requires app restart. Subsequent content changes hot-reload.
- Keys use English originals for automatic fallback.
- Variables use `{{name}}` syntax (i18next interpolation).
- NocoBase auto-namespaces by plugin package name -- different plugins' translations never collide.
- `ctx.t()` in flow handlers does NOT auto-inject namespace -- prefer `useT()` in components or pass `{ ns: 'your-plugin-name' }` explicitly.

## Deep Reference

- https://docs.nocobase.com/cn/plugin-development/client/component/i18n.md

## Related

- [./plugin.md](./plugin.md) -- this.t() in Plugin load()
- [./component.md](./component.md) -- useT() in React components
- [./flow.md](./flow.md) -- tExpr in registerFlow titles and uiSchema
- [./block.md](./block.md) -- tExpr in define() labels
- [./action.md](./action.md) -- tExpr in defaultProps children
- [./field.md](./field.md) -- tExpr in define() labels
- [../server/i18n.md](../server/i18n.md) -- server-side i18n
