FForm Platform
enzh-CN

Runtime internationalization module

Add namespaced messages through an installed module and JSON-managed content without rebuilding the Host.

Runtime i18n module developer guide

Purpose and boundaries

`FormPlatform.I18n` separates customer-authored UI messages from compiled

application bundles. The server module, validation rules, management form, and

fixed browser loader are compiled and installed once. Adding or changing a

translation afterwards is a data operation: no source code, application build,

or module rebuild is required.

This catalog is for public UI text. It is not a secret store, content-management

system, arbitrary JavaScript registry, or HTML template repository. Catalog

values are returned by an anonymous read-only endpoint so login and other

pre-authentication screens can use them.

Package and runtime storage

The deployed package contains:

ItemResponsibility
`FormPlatform.I18n.dll`Validation, persistence, ACL-backed API, and form initialization
`module.json`Module identity, compatibility, navigation, and owned form declaration
`client/index.js`Fixed loader that fetches the catalog with `cache: 'no-store'` and calls `registerMessages`
`client/messages.json`Immutable initial seed copied only when no runtime catalog exists
`appsettings.i18n.json`Runtime catalog path configuration

The default writable catalog is

`App_Data/FormPlatform.I18n/client/messages.json`. It deliberately does not live

under `Modules`: application identities need write permission only to data, a

module redeployment cannot overwrite customer entries, and executable assets

can remain read-only.

`FormPlatformI18n:CatalogPath` accepts an absolute or content-root-relative

path. For a multi-node deployment, configure every node to the same durable

shared location and grant the final IIS application-pool or Windows Service

identity read/write/create/delete rights there. Writers use an inter-process

lock file and atomic replacement. If nodes use separate local `App_Data`

directories, their catalogs are intentionally independent and must be

synchronized operationally. The deploy script preserves an existing deployed

`appsettings.i18n.json`; review new defaults manually during a module upgrade.

Catalog contract

The JSON shape is `locale -> flat key -> string`:

{
  "en": {
    "acme.crm.customer.name": "Customer name",
    "acme.crm.customer.email": "Email address"
  },
  "zh-CN": {
    "acme.crm.customer.name": "客户姓名",
    "acme.crm.customer.email": "电子邮箱"
  }
}

Do not use nested key objects. The client registry looks up the complete dotted

key directly. A form can reference a message with an authored value such as:

{
  "props": {
    "label": "@acme.crm.customer.name"
  }
}

Do not put form-specific `displayName` and `description` text in the runtime

catalog. Edit it in Designer's **Form settings / Action Code** dialog instead;

the platform stores literal overrides in that form definition's

`translations[locale]` and deletes them with the form. Embedded values do not

use `@`, and each missing field falls back to its top-level default.

Use the runtime catalog only for stable named messages shared by multiple forms

or modules, or content that must deliberately outlive one form. A top-level

default `displayName` or `description` may still opt into that catalog with an

explicit `@key`; an unprefixed default never triggers a lookup. Existing catalog

keys cannot be deleted automatically with a form because the platform cannot

prove that they have no other consumers. After moving a key into the definition,

an administrator must remove the old catalog key explicitly. Component-owned

`translations` continue to live in the form schema.

Validation is fail-closed:

  • Locale names use a BCP-47-style syntax and are unique case-insensitively.
  • Keys are case-sensitive, no longer than 256 characters, and have at least

three dot-separated segments.

  • The first segment is a vendor-owned namespace. The roots `formplatform`,

`common`, `auth`, `validation`, `menu`, `designer`, `survey`, `respondent`,

`system`, `error`, `http`, `license`, `offline`, and `i18ncatalog` are reserved

case-insensitively.

  • Values accept any valid JSON string, including Unicode, control characters

represented with JSON escapes, angle brackets, and markup-like text. A value

is limited to 10,000 characters.

  • The catalog is limited to 2 MiB UTF-8, 32 locales, and 50,000 total entries.
  • JSON comments and trailing commas are rejected. Duplicate locale properties

and duplicate keys are detected before deserialization can collapse them.

The client falls back to `en` when the active locale has no value, matching the

normal FormPlatform dynamic-message behavior.

Management and authorization

At startup the module creates `System_I18n_Catalog` once:

f2000000-0000-0000-0000-000000000001

The form has `department_id = NULL`, so it is a shared system form under the

platform's department rule. It is not anonymous. Every management endpoint

also evaluates `IFormAccessAuthorizer` with the form's **Read** operation, which

combines its form ACL and department scope. There is no hard-coded

Administrator check. Administrator remains the platform's implicit recovery

principal; other operators need an explicit form Read grant.

The initializer does not generally overwrite an existing form with that ID.

Designer changes, Action Code changes, metadata, ACLs, and the null department

assignment therefore survive restarts and module upgrades. Version 1.0.1 has

one narrow compatibility repair: an existing display name or description that

still exactly equals the unprefixed 1.0.0 default key is changed to its explicit

`@key` reference. Any customized value is preserved. If the technical name

already belongs to another ID, initialization logs a warning and stops instead

of replacing either form.

The management form provides:

  • a complete JSON editor with validation output;
  • optimistic ETag matching on every save/import;
  • UTF-8 JSON file upload or pasted JSON import;
  • explicit import policies;
  • JSON export;
  • immediate re-registration in the current browser after a successful change.

Duplicate and conflict behavior

The default import mode is `reject`. A collision is the same locale and exact

key already present in the stored catalog, even when both values are identical.

The entire import is rejected with HTTP 409 and a visible conflict report; no

file is changed. Every conflict is counted and at most 500 details are returned.

`keep-existing` adds new entries and retains stored values at collisions.

`overwrite` adds new entries and replaces stored values at collisions. These

modes are never selected implicitly. The complete editor is an explicit

replacement operation, so it may deliberately change or remove existing keys.

An ETag is a SHA-256 of the exact stored JSON. A missing or stale ETag returns

HTTP 409. The operator must reload, review the new content, and retry; the

server never silently applies last-write-wins.

HTTP API

Method and pathAuthenticationPurpose
`GET /api/i18n/catalog`AnonymousReturns only a validated locale catalog; `no-store`
`GET /api/i18n/messages`System user + form Read ACLReturns raw JSON, ETag, validity, issues, and last modification time
`PUT /api/i18n/messages`System user + form Read ACLValidates and explicitly replaces the catalog
`POST /api/i18n/messages/import`System user + form Read ACLMerges with `reject`, `keep-existing`, or `overwrite`
`GET /api/i18n/messages/export`System user + form Read ACLDownloads the exact stored UTF-8 JSON

An invalid runtime file is never returned from the anonymous endpoint; it

returns 503 and the browser registers no new catalog. The authenticated editor

still returns the raw text and validation issues, allowing an operator to

repair it with a valid replacement. Detailed invalid content is not disclosed

to anonymous callers.

Translation storage does not guess how a consumer will render a value. Normal

controls render localized values as text. A form/control that explicitly uses

`allowHtml` treats the resulting translation as trusted HTML; restrict catalog

management to trusted content operators and sanitize at that HTML-consuming

boundary when untrusted markup must be supported.

Third-party workflow

1. Choose and document a vendor root that nobody else uses, such as `acme`.

2. Prepare a UTF-8 JSON file with all required locales and flat dotted keys.

3. Open Runtime translations and import with the default `reject` policy.

4. Review every reported collision. Rename keys where ownership is ambiguous;

choose an override policy only when replacement is intentional.

5. Reference the key from form JSON/Designer fields with `@key`, or from trusted

code with the normal i18n API.

6. Refresh other already-open browser sessions. A page load always fetches with

`cache: 'no-store'`; saving in the management form refreshes that browser

immediately.

Directly editing the runtime file is supported for controlled deployment

automation. Write a complete valid file atomically and then refresh browsers.

The management API is preferred because it performs raw duplicate detection,

ETag concurrency checks, and canonical formatting.

Deployment, backup, and rollback

Run the module's `deploy.ps1`, then restart FormPlatform once to load the DLL

and create the form/catalog. Later message changes need no application restart.

Back up the configured runtime catalog with the rest of `App_Data`; the package

seed is not a backup of customer data.

Before a large import, export the current catalog. Rollback is an import using

the explicit `overwrite` policy plus removal of unwanted keys in the complete

editor, or a complete editor replacement from the exported file.

Uninstall preserves runtime data by default. `-RemoveMenuEntries` and

`-RemoveSystemForms` queue normal host-side cleanup for the next startup.

`-RemoveData` permanently removes the module's App_Data directory and should be

used only after a verified backup. A custom absolute catalog path is not deleted

by the uninstaller and must be retained or removed by the operator explicitly.

Collision boundary

The module prevents silent duplicates within its single runtime catalog.

Separately compiled client extensions still own independent message registries;

FormPlatform does not currently expose all of those registries to this server

module. Vendors must therefore keep globally unique roots. Installing two

compiled extensions that intentionally register the same external key remains

an extension-packaging conflict, outside this JSON catalog's validation scope.