···11+import { describe, expect, test } from "bun:test";
22+33+import { resolveAppLocale } from "@/lib/i18n";
44+55+describe("resolveAppLocale", () => {
66+ test("uses the explicit override before request locale", () => {
77+ expect(resolveAppLocale("en", "ru")).toBe("ru");
88+ });
99+1010+ test("falls back to the preferred locale when no override is set", () => {
1111+ expect(resolveAppLocale("ru", null)).toBe("ru");
1212+ });
1313+1414+ test("falls back to English for unsupported override values", () => {
1515+ expect(resolveAppLocale("ru", "de")).toBe("en");
1616+ });
1717+});
···161161 publicRouteHelp: Published forms accept anonymous submissions. Changes to a published form go live immediately.
162162 appearance: Appearance
163163 appearanceDescription: Control what respondents see while moving through the form.
164164+ respondentLanguage: Respondent interface language
165165+ respondentLanguageHelp: Choose the language used for buttons, validation, progress, and other interface text in the public form.
166166+ respondentLanguageOptions:
167167+ visitor: Use visitor's language
164168 showProgress: Show form progress
165169 copyLink: Copy link
166170 openRunner: Open runner
+4
locales/ru.yml
···161161 publicRouteHelp: Опубликованные формы принимают анонимные ответы. Изменения в опубликованной форме становятся доступны сразу.
162162 appearance: Внешний вид
163163 appearanceDescription: Управляйте тем, что респонденты видят во время прохождения формы.
164164+ respondentLanguage: Язык интерфейса для респондента
165165+ respondentLanguageHelp: Выберите язык кнопок, валидации, прогресса и другого интерфейсного текста в публичной форме.
166166+ respondentLanguageOptions:
167167+ visitor: Использовать язык посетителя
164168 showProgress: Показывать прогресс формы
165169 copyLink: Скопировать ссылку
166170 openRunner: Открыть раннер
···11+## Context
22+33+Public form routes currently render localized interface strings from the active request locale, which is derived from supported browser or user-preference signals and then normalized to a supported language. That keeps general localization simple, but it does not give creators a way to keep a specific form's respondent-facing interface aligned with the language of the form content itself. This change crosses creator settings, stored form metadata, public-form loading, and locale resolution, so a short design is useful before implementation.
44+55+## Goals / Non-Goals
66+77+**Goals:**
88+- Let a creator choose a respondent interface locale override per form.
99+- Preserve existing visitor-language detection when no override is configured.
1010+- Keep locale handling limited to the existing supported locales and fallback-to-English behavior.
1111+- Make the public runner, validation copy, progress labels, and completion state honor the saved override consistently.
1212+1313+**Non-Goals:**
1414+- Auto-translating form content authored by creators.
1515+- Adding new supported languages in this change.
1616+- Introducing per-block or per-section locale settings.
1717+- Changing creator-dashboard locale behavior outside the form settings needed to edit the override.
1818+1919+## Decisions
2020+2121+### 1. Store locale override as form metadata, not as a transient request preference
2222+The system will persist a nullable form-level locale override alongside other form settings. `null` means the `Use visitor's language` mode; a supported locale value such as `en` or `ru` means the public runner must use that locale for respondent-facing UI.
2323+2424+**Why:** The override is part of the form's authored behavior, similar to title, completion content, and slug. It must survive draft saves, publication, and later edits.
2525+2626+**Alternatives considered:**
2727+- Store override only in published snapshots: rejected because creators need to preview and edit draft behavior.
2828+- Store override in browser local state: rejected because the setting belongs to the form, not the current creator session.
2929+3030+### 2. Apply the override only on public respondent-facing form surfaces
3131+The override will affect public form routes and the locale used to prepare public runner strings. Creator-facing builder chrome will continue to follow the creator/session locale.
3232+3333+**Why:** The problem is respondent experience mismatch, not creator workspace localization. Keeping creator surfaces on their own locale avoids surprising workspace-wide language switches while editing a form.
3434+3535+**Alternatives considered:**
3636+- Also force the builder locale while editing that form: rejected because it would couple one form's language to the whole creator session and make mixed-language workspaces harder to use.
3737+3838+### 3. Resolve locale with clear precedence: form override first, then existing detection, then fallback
3939+For public forms, locale resolution will first check the saved form override. If absent, the system will continue using the current supported-locale detection flow. Unsupported or malformed values will still normalize to English.
4040+4141+**Why:** This preserves the existing localization model while adding one explicit author-controlled override point.
4242+4343+**Alternatives considered:**
4444+- Let browser locale override the form setting: rejected because it does not solve the reported inconsistency.
4545+- Remove browser locale detection entirely: rejected because visitor-language localization remains desirable for forms without an override.
4646+4747+### 4. Keep the saved value constrained to supported locales
4848+The builder setting and persistence layer will only allow `null`, `en`, or `ru` initially.
4949+5050+**Why:** The app already validates locale support centrally. Reusing that contract keeps runtime behavior predictable and avoids publishing forms with unusable locale values.
5151+5252+**Alternatives considered:**
5353+- Allow arbitrary BCP-47 locale codes for future-proofing: rejected because the application only ships `en` and `ru` resources today.
5454+5555+## Risks / Trade-offs
5656+5757+- [Creators may expect form content itself to be translated] → Mitigation: label the setting as interface language for respondent UI, not content translation.
5858+- [Existing public-form locale loading may have multiple entry points] → Mitigation: centralize override-aware resolution in the shared public-form loading/i18n path instead of duplicating precedence logic.
5959+- [Stored invalid values from manual edits or future bugs could break rendering] → Mitigation: normalize persisted locale override through the existing supported-locale fallback helper.
6060+- [Previewing draft behavior may differ from published behavior if override is not included in all serialization paths] → Mitigation: update both creator draft payloads and public form payload generation to carry the same field.
6161+6262+## Migration Plan
6363+6464+- Add a nullable persisted locale override field or config value with default `null` for existing forms.
6565+- Treat missing values on existing records as `Use visitor's language`.
6666+- Rollback is low risk: code can ignore the field and continue using current locale detection, with existing saved `null`/supported values remaining harmless.
6767+6868+## Open Questions
6969+7070+- None currently. The product intent is clear enough to proceed with a form-level override using existing supported locales.
···11+## Why
22+33+Public form runners currently localize respondent-facing interface strings from browser or system locale. That can produce a mixed-language experience when a creator builds a form in one language but the respondent's device prefers another, especially for labels such as navigation, validation, progress, and completion UI. Adding a per-form locale override makes respondent-facing copy consistent with the language the creator intends for that form.
44+55+## What Changes
66+77+- Add a form-level setting that lets creators choose whether a form's respondent-facing interface uses `Use visitor's language` or forces a specific supported locale.
88+- Persist the selected locale override as part of form metadata so draft and published forms keep the same respondent-language behavior.
99+- Apply the form-level locale override on public form routes so runner UI, validation messages, progress text, and completion-state interface strings render in the configured locale.
1010+- Keep supported-locale fallback behavior intact so invalid or unsupported saved values resolve safely to English.
1111+1212+## Capabilities
1313+1414+### New Capabilities
1515+- None.
1616+1717+### Modified Capabilities
1818+- `conversational-form-builder`: creators can configure a per-form respondent interface locale override in form settings.
1919+- `anonymous-form-runner`: public form routes resolve respondent-facing UI locale from the form override before using browser locale detection.
2020+- `site-localization`: locale resolution supports a saved form-specific override for public form surfaces while preserving supported-locale fallback rules.
2121+2222+## Impact
2323+2424+- Affected code: creator form settings UI, form serialization and persistence, public form route loading, i18n locale resolution, and respondent runner rendering.
2525+- APIs: form metadata payloads and public form data loading will include the saved locale override.
2626+- Data model: forms need a persisted locale override field or equivalent config value.
2727+- Dependencies: no new external dependencies expected.
···11+## ADDED Requirements
22+33+### Requirement: Public form runner honors form-specific interface locale overrides
44+The system SHALL resolve respondent-facing interface strings for a public form from the form's saved locale override when one is configured. When no override is configured, the system SHALL continue using the existing automatic locale-detection flow.
55+66+#### Scenario: Respondent opens a form with a Russian locale override
77+- **WHEN** a respondent opens a published form whose saved respondent interface locale override is `ru`
88+- **THEN** the public runner renders navigation, validation, progress, and completion interface strings in Russian regardless of the respondent's browser locale
99+1010+#### Scenario: Respondent opens a form with no locale override
1111+- **WHEN** a respondent opens a published form that has no saved respondent interface locale override
1212+- **THEN** the public runner resolves interface strings using the existing automatic locale-detection behavior
···11+## ADDED Requirements
22+33+### Requirement: Creator can configure respondent interface locale per form
44+The system SHALL allow an authenticated creator to configure a form-level respondent interface locale in form settings for any accessible form. The setting SHALL support a `Use visitor's language` option and any currently supported explicit locale override.
55+66+#### Scenario: Creator uses visitor-language mode
77+- **WHEN** an authenticated creator leaves the respondent interface locale setting on `Use visitor's language`
88+- **THEN** the system saves the form without an explicit locale override
99+1010+#### Scenario: Creator selects a supported explicit locale
1111+- **WHEN** an authenticated creator chooses a supported locale such as English or Russian in form settings and saves
1212+- **THEN** the system persists that locale as the form's respondent interface override
1313+1414+#### Scenario: Creator reopens form settings with an existing locale override
1515+- **WHEN** an authenticated creator opens settings for a form that already has a saved respondent interface locale override
1616+- **THEN** the system shows the saved override as the current selected value
···11+## MODIFIED Requirements
22+33+### Requirement: Supported locale resolution falls back to English
44+The system SHALL support `en` and `ru` locales initially and SHALL normalize unsupported locale values to English. For public form surfaces, the system SHALL honor a saved supported form-level locale override before using browser or user-state locale detection.
55+66+#### Scenario: Supported locale is requested
77+- **WHEN** the active locale is `en` or `ru`
88+- **THEN** the system renders strings from that supported locale resource set
99+1010+#### Scenario: Public form has a supported locale override
1111+- **WHEN** a public form surface is loaded for a form whose saved locale override is `en` or `ru`
1212+- **THEN** the system uses that saved form locale for rendering instead of browser locale detection
1313+1414+#### Scenario: Unauthenticated user has a supported browser locale
1515+- **WHEN** an unauthenticated creator or respondent opens the app and no form-level locale override applies while the browser locale resolves to a supported locale
1616+- **THEN** the system uses that supported browser locale for rendering without requiring sign-in
1717+1818+#### Scenario: Unsupported locale is requested
1919+- **WHEN** the system receives an unsupported locale value from user state, OAuth data, environment detection, or a saved form override
2020+- **THEN** the system falls back to English as the active locale
···11+## 1. Form metadata and persistence
22+33+- [x] 1.1 Add a nullable respondent interface locale override field to form metadata/domain types with supported-locale validation and `Use visitor's language` as the no-override/default mode.
44+- [x] 1.2 Include the locale override in form settings load/save flows and any form serialization used by the builder.
55+- [x] 1.3 Add or update tests covering persisted form locale override normalization and round-tripping.
66+77+## 2. Creator settings UI
88+99+- [x] 2.1 Add a form settings control that lets creators choose `Use visitor's language` or a supported explicit respondent interface locale.
1010+- [x] 2.2 Ensure the builder restores the saved selection when reopening form settings and persists changes through the existing save action.
1111+- [x] 2.3 Add UI tests covering selecting, saving, and reloading the form locale override.
1212+1313+## 3. Public locale resolution
1414+1515+- [x] 3.1 Update public form loading/i18n resolution so a saved form locale override takes precedence over automatic locale detection on respondent-facing routes.
1616+- [x] 3.2 Ensure public runner strings, validation copy, progress text, and completion UI all read from the resolved override-aware locale.
1717+- [x] 3.3 Add tests covering explicit override precedence, `Use visitor's language` fallback behavior, and unsupported-value fallback to English.
+11
openspec/specs/anonymous-form-runner/spec.md
···142142#### Scenario: Respondent views a formatted question description
143143- **WHEN** a published form contains a question description with supported Markdown
144144- **THEN** the runner shows the formatted description consistently with the shared authored Markdown rules
145145+146146+### Requirement: Public form runner honors form-specific interface locale overrides
147147+The system SHALL resolve respondent-facing interface strings for a public form from the form's saved locale override when one is configured. When no override is configured, the system SHALL continue using the existing automatic locale-detection flow.
148148+149149+#### Scenario: Respondent opens a form with a Russian locale override
150150+- **WHEN** a respondent opens a published form whose saved respondent interface locale override is `ru`
151151+- **THEN** the public runner renders navigation, validation, progress, and completion interface strings in Russian regardless of the respondent's browser locale
152152+153153+#### Scenario: Respondent opens a form with no locale override
154154+- **WHEN** a respondent opens a published form that has no saved respondent interface locale override
155155+- **THEN** the public runner resolves interface strings using the existing automatic locale-detection behavior
···129129- **WHEN** an authenticated creator leaves the follow-up link label or URL empty
130130- **THEN** the system does not save or expose a partial follow-up link for the form
131131132132+### Requirement: Creator can configure respondent interface locale per form
133133+The system SHALL allow an authenticated creator to configure a form-level respondent interface locale in form settings for any accessible form. The setting SHALL support a `Use visitor's language` option and any currently supported explicit locale override.
134134+135135+#### Scenario: Creator uses visitor-language mode
136136+- **WHEN** an authenticated creator leaves the respondent interface locale setting on `Use visitor's language`
137137+- **THEN** the system saves the form without an explicit locale override
138138+139139+#### Scenario: Creator selects a supported explicit locale
140140+- **WHEN** an authenticated creator chooses a supported locale such as English or Russian in form settings and saves
141141+- **THEN** the system persists that locale as the form's respondent interface override
142142+143143+#### Scenario: Creator reopens form settings with an existing locale override
144144+- **WHEN** an authenticated creator opens settings for a form that already has a saved respondent interface locale override
145145+- **THEN** the system shows the saved override as the current selected value
146146+132147### Requirement: Builder reflects the active workspace context
133148The system SHALL show the active workspace context when a creator creates or edits a form so they can tell whether the form belongs to their personal workspace or an organization.
134149
+7-3
openspec/specs/site-localization/spec.md
···1212- **THEN** the system can render that surface in the additional language without requiring the component to hardcode translated copy
13131414### Requirement: Supported locale resolution falls back to English
1515-The system SHALL support `en` and `ru` locales initially and SHALL normalize unsupported locale values to English.
1515+The system SHALL support `en` and `ru` locales initially and SHALL normalize unsupported locale values to English. For public form surfaces, the system SHALL honor a saved supported form-level locale override before using browser or user-state locale detection.
16161717#### Scenario: Supported locale is requested
1818- **WHEN** the active locale is `en` or `ru`
1919- **THEN** the system renders strings from that supported locale resource set
20202121+#### Scenario: Public form has a supported locale override
2222+- **WHEN** a public form surface is loaded for a form whose saved locale override is `en` or `ru`
2323+- **THEN** the system uses that saved form locale for rendering instead of browser locale detection
2424+2125#### Scenario: Unauthenticated user has a supported browser locale
2222-- **WHEN** an unauthenticated creator or respondent opens the app and the browser locale resolves to a supported locale
2626+- **WHEN** an unauthenticated creator or respondent opens the app and no form-level locale override applies while the browser locale resolves to a supported locale
2327- **THEN** the system uses that supported browser locale for rendering without requiring sign-in
24282529#### Scenario: Unsupported locale is requested
2626-- **WHEN** the system receives an unsupported locale value from user state, OAuth data, or environment detection
3030+- **WHEN** the system receives an unsupported locale value from user state, OAuth data, environment detection, or a saved form override
2731- **THEN** the system falls back to English as the active locale
28322933### Requirement: Missing translations are surfaced during build