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, a Matrix's question, columns and
row labels, Checkbox text, <Submit> text, <Heading> and <Paragraph>
text, and messages returned by validate. A Tel country picker names the
countries in the form's language on its own.
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 { Form, T, useFieldValue, useTranslate } from '@open-form-build/core';
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>
);
}
export default function Workshop() {
return <Form aside={<Summary />}>{/* … */}</Form>;
}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 top-right corner lets respondents switch. The built-in layouts
leave room for it, and a custom layout must too (see
Layouts and chrome).
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
npx 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. It
also renders the form once per sample answer (each option, a ticked box, each
score on a scale of up to 11 points), so text that only some answers show, such
as {score <= 6 && <T>What could we do better?</T>}, is counted too. Entries it
didn't see rendered are counted as not seen, and the first 8 are shown. Text
that depends on an answer it didn't try, 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.