Internationalization
NocoBase 3 supports multiple languages. This page covers writing translatable text, organizing locale files, and overriding a plugin's wording. If all you want is to switch language or add one, Built-in capabilities / Language switching is enough.

How a translation works
For a piece of text on screen to be translatable, it has to be written in a locale file and referenced from the component by its key.
You can tell a Coding Agent directly:
It adds the wording to client/locales/en-US.ts and zh-CN.ts, then edits the component.
The wording looks like this:
A name like orders.title is the key: the same key in both files addresses the same sentence in each language.
And the component:
To include a value, pass it in rather than concatenating. The wording holds a {{name}} placeholder:
Where the locale files are
An application's own translations live in client/locales/:
en-US.ts is the source. It exports a type that every other locale is annotated with, so a key you forget to translate is a compile error that pnpm typecheck reports:
Keys nest, and a dot addresses them: t('orders.title').
There is a matching server/locales/, built the same way. Text the server produces — API errors, the body of an email — goes there; plain interface text does not.
Overriding a plugin's wording
A plugin's wording will not always match how your business talks, and you can override it from your own locale file.
When handing this to a Coding Agent, point at the text on screen:
State two things: where on screen, and what it should say instead. Where matters, because one word can map to several keys inside a plugin — the menu entry and the page title are two of them — and without that the wrong one gets changed, or one gets missed.
The Agent adds an overrides block to your locale file, grouped by the plugin's package name:
Overrides are applied after every plugin has registered, so your wording wins regardless of load order.
Overriding is for a difference in what something is called — the plugin says "Workflow", your company says "Approvals". If a plugin has not translated a language at all, that belongs in an issue against the plugin rather than here.
Language on the server and in background jobs
The interface follows the browser. The server does not.
An API error follows the request. Take a translator inside the route; the middleware has already resolved the language:
A background job has no request to follow, so it has to decide for itself. Scheduled jobs, one-off jobs, and webhooks are all in this position, and there is one rule that is easy to get wrong:
Outbound content follows its recipient, not whoever triggered it.
A submits a request, which notifies B. That notification belongs in B's language. Sending it in A's language, or in the server's default, is wrong.
Skipping ensureLocaleLoaded does not throw — it quietly falls back to the default language, which a test will not necessarily catch, so it has to be remembered while writing.
Sending to several people means resolving each recipient's language separately; one language for a batch is not it.
Adding a language
Add a locale file under client/locales/ and register it in index.ts; add one under server/locales/ too when the server has wording of its own. Those files are what decides the languages an application offers — there is no second list to keep in step.
The full procedure, and what to ask a Coding Agent for, is in Built-in capabilities / Language switching.
Checking your work
Common questions
I changed the wording in a component and the page looks the same
Check that you edited the file for the language you are viewing. Also check whether the text belongs to a plugin — a plugin's wording is changed through overrides, and editing your own key does nothing.
A component renders its keys, or fails, inside a test
Rendered on its own, that component has no i18n runtime. A defaultValue keeps it readable either way:
A component a plugin exports resolves the wrong translation
Say @acme/app-plugin-crm exports a component using the key orders.title, and the application happens to have a key by the same name. A translation is resolved by where a component renders, not by which package wrote it, so without anything further the application's wording wins.
Such a component has to name its own namespace — which is the plugin's package name:
The test to apply: will this component ever render outside its own plugin's pages? Then name the namespace.
Related
- Built-in capabilities / Language switching — switching language, adding one, setting the default
- API endpoints — writing server-side errors
- Scheduled tasks — language in scheduled jobs

