Languages
Write a form in one language, translate it into others, and keep every response in one table.
A form is written in one language, its locale, and can be translated into
others. Open Form supports 'en' (the default), 'zh-TW', 'zh-CN' and 'ja'.
The form's language
Set locale in defineForm(), or in open-form.config.ts for every form:
export const config = defineForm({
title: '產品回饋',
locale: 'zh-TW',
});locale covers the framework's own text: Back / Next / Submit, built-in
validation errors, the success and closed screens, and dates, which are formatted
with Intl for that language. It also picks the right CJK fonts: themes end their
font stacks with var(--of-cjk), and the runtime fills it with Traditional
Chinese, Simplified Chinese or Japanese faces to match. Your questions are written
by you, in that language.
The server uses the same message catalog, so a validation error reads the same whether the browser or the server caught it.
A single-language form never switches with the browser: the questions are in one language, and translating only the buttons would be inconsistent.
Translations
Translations live beside the form, one JSON file per extra language, keyed by the exact source text:
forms/feedback/
├── index.tsx # written in 繁體中文, locale: 'zh-TW'
└── locales/
├── en.json
└── ja.json{
"產品回饋": "Product feedback",
"你最常用的功能": "Which features do you use most?"
}import en from './locales/en.json';
import ja from './locales/ja.json';
export const config = defineForm({
title: '產品回饋',
locale: 'zh-TW', // required once there are translations
translations: { en, ja },
});The form's code stays in one readable language, an agent can produce a whole
translation in one pass, and check can list exactly what's missing. A missing
entry falls back to the source text, so the form never breaks.
What's translated automatically
The title, description, success copy and policy messages (fullMessage, …),
<Page> titles and descriptions, field label, description, placeholder and
unit, option labels, Rating end labels, Checkbox text, <Submit> text,
<Heading> and <Paragraph> text, and messages returned by validate.
Your own markup
Wrap text in custom components with <T>, or call useTranslate(). Placeholders
in braces are filled after translating, so a translation can move them:
import { T, useFieldValue, useTranslate } from '@open-form-build/core';
export function Summary() {
const tr = useTranslate();
const n = Number(useFieldValue('workshops') ?? 0);
return (
<div>
<T>Early-bird pricing ends Friday.</T>
<p>{tr('Workshops × {n}', { n })}</p>
</div>
);
}The key for the second string is "Workshops × {n}". useFormLanguage() returns
{ locale, source, available, setLocale } for a custom language picker.
Values stay put
Translation happens only in what respondents see. Field names, option values
and <When> conditions are never translated, so every language stores the same
answers in the same columns. Give options explicit values when their labels are
translated:
<RadioGroup
name="role"
label="職稱"
options={[
{ value: 'engineer', label: '工程師' },
{ value: 'designer', label: '設計師' },
]}
/>Responses, CSV headers and Studio use the source language. Each response records the language it was submitted in; see Responses & export.
Which language respondents see
For a translated form, the runtime picks the first match:
?lang=in the URL, for example/f/feedback?lang=ja.- The language the respondent last chose on that form (saved in their browser).
- The browser's preferred languages.
- The source language.
A picker in the corner lets respondents switch. languageSwitch: false hides it,
and a LanguageSwitch component replaces it. The submission carries the chosen
language; the server accepts only languages the form offers and uses it for its
error messages.
Check coverage
pnpm exec open-form check feedbackcheck renders the form like the server does and prints, per language,
N/N translated and the first 8 missing entries; --json lists them all.
Entries it didn't see rendered are counted as not seen, and the first 3 are
shown. Text that appears only for certain answers, or comes from validate, is
expected there. A key left behind after you edited the source text is not:
update it in every locales/*.json.
Studio's Settings → Languages shows the translated count and the missing entries for each language, but not the not-seen ones. The preview's language picker reads each translation in place, and Edit mode renames keys in the form's translations when you change source text.