Internationalization
Build bilingual and multilingual FormPlatform interfaces with stable message keys, localized metadata and substitution-aware runtime text.
Internationalization
FORMPLATFORM separates platform UI text from form-authored business content.
The platform locale is stored in `localStorage` as `formplatform.locale`.
English (`en`) is the default and Simplified Chinese (`zh-CN`) is currently
supported.
Platform Vue code
Platform views and components use stable keys from `ClientApp/src/i18n.js`.
Never translate rendered DOM text and never use a source sentence as the key.
<script setup>
import { useI18n } from '../i18n.js'
const { t } = useI18n()
</script>
<template>
<button>{{ t('customer.save') }}</button>
<p>{{ t('customer.updated', { name: customer.name }) }}</p>
</template>
Add both catalog entries:
// zh-CN
'customer.save': '保存客户',
'customer.updated': '已更新客户 {name}',
// en
'customer.save': 'Save customer',
'customer.updated': 'Customer {name} was updated',
The legacy DOM localizer is not installed. This is intentional: substring
replacement cannot understand context and caused corrupt text such as `adm在`.
System and layout forms
Platform-owned forms can store an `xxxKey` next to any display property. The
renderer recursively converts the key into the real property.
{
"type": "button",
"props": {
"textKey": "common.save"
}
}
This works in nested objects too, including Menu items and Breadcrumb items.
System and Functional forms that implement platform UI should use catalog keys.
Form definition names and descriptions
Form-owned `displayName` and `description` translations can be edited directly
in **Form settings / Action Code**. The default text remains at the top level and
locale overrides live in the same form definition:
{
"name": "Acme_Customer",
"displayName": "Customer registration",
"description": "Register a customer",
"translations": {
"zh-CN": {
"displayName": "客户登记",
"description": "登记客户资料"
}
}
}
Embedded overrides are authored text: they do not need an `@` prefix, and a
value such as `@common.save` is deliberately rendered literally. A missing field
falls back independently to the top-level default. Form lists, Form Center,
Submission Center, survey cards, and management selectors consume these
overrides. The technical form `name` remains a literal stable identifier.
Relational storage keeps the object in `form_definitions.translations_json`;
file and memory stores keep it inside the same definition. Deleting a form
therefore deletes its translations and cannot leave runtime-catalog orphans.
Shared platform or module messages may still use an explicit `@key` in the
top-level default, such as `@form.SystemHeader.displayName`. An unprefixed
default never causes a catalog lookup, and unprefixed legacy `form.*` keys are
not supported. Existing form-specific keys in `FormPlatform.I18n` are not
automatically migrated or removed because the platform cannot prove that no
other form consumes them; copy and verify them first, then remove them explicitly.
Survey and other business-form content
Business content is authored content, not platform UI. Its default language
stays in the normal component properties. Explicit locale overrides are stored
on the component:
{
"id": "customerName",
"type": "input",
"props": {
"label": "Customer name",
"placeholder": "Enter your name"
},
"other": {
"required": true,
"requiredMessage": "Customer name is required"
},
"tooltip": {
"helpEnabled": true,
"helpType": "always",
"helpContent": "Use the name on your account."
},
"translations": {
"zh-CN": {
"props": {
"label": "客户姓名",
"placeholder": "请输入姓名"
},
"other": {
"requiredMessage": "客户姓名为必填项"
},
"tooltip": {
"helpContent": "请使用账户上的姓名。"
}
}
}
}
The renderer deep-merges the selected locale over the default component.
Missing translations fall back to the default content. Control values, IDs,
property names, validation code, URLs, and database mappings must not be
translated.
Designer Translations tab
The component property dialog provides a **Translations** tab for author-facing
form content. Select a target language and edit the fields exposed for that
component. The editor currently covers ordinary display properties such as
Label, Placeholder, text, content, headings and button captions; tooltip title
and content; required and custom-validation messages; and display text in
static options and schema-owned collections such as menu items, panes, and grid
columns.
The editor writes the result directly to `component.translations[locale]` when
the component and form are saved. An empty translation is omitted and therefore
falls back to the source property. **Clear this language** removes only the
selected component locale; it does not change the source text or other locales.
These business-form translations are stored with the form definition, so adding
or changing them does not require adding a platform catalog key or rebuilding
the main application.
Only display properties are editable in this tab. Structural and executable
properties—including option values, IDs, property names, code, URLs, mappings,
and static business-data rows—are deliberately excluded. For example, a static
option is stored with its stable value as an identity key:
{
"props": {
"options": [
{ "text": "Active", "value": "active" }
]
},
"translations": {
"zh-CN": {
"props": {
"options": [
{ "value": "active", "text": "启用" }
]
}
}
}
}
At runtime, translated collection entries are merged into the source collection
by a stable identity such as `id`, `value`, or `field`. The source collection
continues to define its order and membership, and the submitted option value is
unchanged. A collection uses identity matching only when every source entry has
a non-empty identity; otherwise the editor writes a positional array. Identity
values should be unique within the source collection.
Legacy hand-authored positional translation arrays are still supported when
they contain no stable identity fields.
Property-panel interface text
The labels, legends, choices, help text, accessible titles, and natural-language
placeholders that make up the Designer property panel are platform interface
text, not form content. They must call `t(...)` and have matching `en` and
`zh-CN` entries in `ClientApp/src/i18n.js`. This includes every section of the
General tab, including detailed appearance editors for search boxes, buttons,
menus, tables, grids, images, and layout containers. Dynamic choice values keep
their stable schema value while their visible label is translated.
Do not translate executable or structural examples such as CSS declarations,
Tailwind classes, date-format tokens, URLs, JSON, expressions, component IDs,
or Data Model names. Adding a new General-tab setting is incomplete until its
visible interface text is present in both catalogs; the component's own
author-entered text belongs in `component.translations`, not in this platform
catalog.
The same rule applies to the Events tab. Its instructions, parameter editor,
target selector, buttons, placeholders, and built-in event labels use the
`events.*` catalog. The technical event name shown beside the label (for example
`onChange`) and Action function/chain identifiers are never translated. A
third-party event automatically looks up `events.<eventName>` and falls back to
the label supplied by its event registration, so an extension can localize the
label through its runtime message catalog without changing the event contract.
Action Code and form JavaScript
Action modules receive `args.api.t()` and `args.api.getLocale()`. A module may
export its own message catalogs. Catalogs are registered only while that form's
Action Code is active and are removed when the form is unloaded.
export const messages = {
en: {
'list.saved': 'Record {id} was saved'
},
'zh-CN': {
'list.saved': '记录 {id} 已保存'
}
}
export default {
messages,
async init(args) {
console.info('locale:', args.api.getLocale())
},
async onSaved(args) {
args.api.notify({
type: 'success',
messageKey: 'list.saved',
messageParams: { id: args.data.id },
message: `Record ${args.data.id} was saved`
})
}
}
Use message keys in action-chain parameters as data. Resolve them only at the
point where text is displayed:
args.api.notify({
type: args.parameters.type,
messageKey: args.parameters.messageKey,
messageParams: args.parameters.messageParams
})
Server responses
API responses should make a stable machine-readable `code` or `messageKey`
authoritative. The optional English `message` is a fallback for old clients,
logs, and untranslated keys.
{
"code": "customer.notFound",
"messageKey": "errors.customerNotFound",
"messageParams": { "id": "42" },
"message": "Customer 42 was not found"
}
Server notifications use the same shape:
{
"type": "warning",
"titleKey": "warning.title",
"messageKey": "customer.profileIncomplete",
"messageParams": { "name": "Mike" },
"durationMs": 7000
}
The client resolves `messageKey`, `titleKey`, and validation-error descriptor
objects. Requests send the current locale in `Accept-Language`. Domain and
validation APIs should normally return keys instead of localized prose so that
one response is deterministic and the browser can change language immediately.
Use ASP.NET `IStringLocalizer` only for server-owned rendered output such as
emails, exported reports, or text that cannot be translated by the client.
Adding another language
1. Add the locale to `supportedLocales` in `ClientApp/src/i18n.js`.
2. Add a complete platform catalog.
3. Add the language to `LanguageSwitcher.vue`.
4. Add explicit `translations[locale]` values to business forms that require it.
5. Test key fallback, interpolation, validation messages, Action Code
notifications, and server error responses.