FForm Platform
enzh-CN

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.