i18n Internationalization

NocoBase plugins manage multilingual files through the src/locale/ directory. After writing the translation files, use this.t() in the Plugin, useT() hook in components, and tExpr() in FlowModel definitions to get translated text.

Translation Files

Create JSON files by language under the plugin's src/locale/. The key is the English original text, and the value is the translation for the corresponding language:

my-plugin/
└── src/
    └── locale/
        ├── zh-CN.json
        └── en-US.json
// src/locale/zh-CN.json
{
  "Hello": "你好",
  "Save": "保存",
  "Your name is {{name}}": "你的名字是 {{name}}",
  "{{count}} February items": "{{count}} 条记录"
}
// src/locale/en-US.json
{
  "Hello": "Hello",
  "Save": "Save",
  "Your name is {{name}}": "Your name is {{name}}",
  "{{count}} February items": "{{count}} items"
}

A few things to note:

  • Use English text as the key, so even if a translation is missing it falls back to English
  • Use double curly braces for variables {{name}}, consistent with i18next syntax
  • Adding language files for the first time requires an application restart to take effect; subsequent content changes will hot reload
  • NocoBase automatically uses the plugin's package name as the translation namespace, so translations from different plugins won't conflict

Using in Plugin: this.t()

In the Plugin class, this.t() automatically injects the current plugin's package name as the namespace — no need to manually pass ns:

// src/client-v2/plugin.tsx
import { Plugin, Application } from '@nocobase/client-v2';

export class MyPlugin extends Plugin<any, Application> {
  async load() {
    // Automatically uses the current plugin's package name as ns
    console.log(this.t('Hello')); // -> "你好"

    // Use this.t() to translate titles when registering settings pages
    this.pluginSettingsManager.addMenuItem({
      key: 'my-settings',
      title: this.t('My Settings'),
      icon: 'SettingOutlined',
    });
    this.pluginSettingsManager.addPageTabItem({
      menuKey: 'my-settings',
      key: 'index',
      title: this.t('My Settings'),
      componentLoader: () => import('./pages/MySettingsPage'),
    });
  }
}

Using in Components: useT()

You cannot use this.t() directly in React components. The plugin scaffolding automatically generates a locale.ts file that provides the useT() hook:

// src/client-v2/locale.ts (auto-generated by scaffolding)
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'] });
}

Use it in components like this:

// src/client-v2/pages/MySettingsPage.tsx
import React from 'react';
import { Button } from 'antd';
import { useT } from '../locale';

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

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

The t function returned by useT() is already bound to the plugin's namespace — just pass the key directly.

Using in FlowModel: tExpr()

FlowModel.define() and registerFlow() are executed at module load time, when i18n has not yet been initialized, so you cannot call t() directly. In this scenario, use tExpr() — it generates a deferred translation expression string that is resolved at runtime:

// src/client-v2/models/MyBlockModel.tsx
import { BlockModel } from '@nocobase/client-v2';
import { tExpr } from '../locale';

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

// tExpr generates a string like '{{t("My block")}}', translated at runtime
MyBlockModel.define({
  label: tExpr('My block'),
});

MyBlockModel.registerFlow({
  key: 'flow1',
  title: tExpr('My Block Settings'),
  on: 'beforeRender',
  steps: {
    editTitle: {
      title: tExpr('Edit Title'),
      uiSchema: {
        title: {
          type: 'string',
          title: tExpr('Title'),
          'x-decorator': 'FormItem',
          'x-component': 'Input',
        },
      },
      handler(ctx, params) {
        ctx.model.props.title = params.title;
      },
    },
  },
});

In short: this.t() and useT() are for runtime translation, while tExpr() is for deferred translation in static definitions.

Tip

tExpr has two sources: the auto-generated locale.ts from the plugin and @nocobase/flow-engine. The difference is that the tExpr from locale.ts is already bound to the plugin's package name as namespace, while the one imported directly from @nocobase/flow-engine has no namespace binding. In plugin code, always use the tExpr exported from locale.ts so that translations correctly match the plugin's own language files.

Quick Reference for the Three Methods

ContextUsageSource
In Plugin load()this.t('key')Built into Plugin base class
In React componentsconst t = useT(); t('key')locale.ts
In FlowModel static definitionstExpr('key')locale.ts