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:
| Item | Responsibility |
|---|---|
| `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 path | Authentication | Purpose |
|---|---|---|
| `GET /api/i18n/catalog` | Anonymous | Returns only a validated locale catalog; `no-store` |
| `GET /api/i18n/messages` | System user + form Read ACL | Returns raw JSON, ETag, validity, issues, and last modification time |
| `PUT /api/i18n/messages` | System user + form Read ACL | Validates and explicitly replaces the catalog |
| `POST /api/i18n/messages/import` | System user + form Read ACL | Merges with `reject`, `keep-existing`, or `overwrite` |
| `GET /api/i18n/messages/export` | System user + form Read ACL | Downloads 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.