Status: Stable · v1.1 (2026-05-12). Optional annex for hosts and clients that need to negotiate locale on human-targeted text — interrupt prompts, error envelopes, and (host-extension) UI strings. Additive to
interrupt.mdand the existing error-envelope contract. Keywords MUST, SHOULD, MAY follow RFC 2119. Seeauth.mdfor the status legend.
Why this exists
openwop is increasingly deployed in workspaces with non-English-speaking end users:
- HITL approval prompts get rendered to humans whose preferred language is not English.
- Clarification questions get rendered to humans.
- Validation-error messages bubble up to operator UIs.
- Conversation-primitive (RFC 0005)
data.promptfields get rendered to humans.
The protocol's status quo is "everything is UTF-8, locale-unspecified" — which works when the server-side workflow author and the human reviewer share a language, and breaks the moment they don't. This annex defines optional locale negotiation: clients tell the host which locale they prefer, hosts emit human-facing text in that locale when they can.
Scope
In scope:
Accept-Languagerequest header semantics on every REST endpoint.- Optional
localefield onInterruptPayloadshapes that carry human-facing prompts (approval / clarification / conversation). - Optional
localefield on theErrorEnvelope.detailsfor messages a client surfaces to humans. - Conventions for host-side locale fallback when the requested locale isn't available.
Out of scope:
- Localization OF the protocol spec itself. This document is the canonical English version.
- Localization of node-pack node names, typeIds, or any machine-readable identifiers — those stay English / ASCII.
- Bidirectional text handling, RTL UI rendering — that's a UI concern.
- Date/time format negotiation — the protocol uses ISO 8601 throughout regardless of locale.
- Currency / number-format negotiation — out of protocol scope.
Accept-Language request header
Clients MAY include the Accept-Language header (per RFC 9110 §12.5.4) on any request. Hosts MAY honor it to localize human-facing text in the response.
GET /v1/runs/{runId} HTTP/1.1
Accept-Language: ja, en;q=0.5
Authorization: Bearer ...
Host MUST:
1. Always parse the header without failing the request. A malformed Accept-Language MUST NOT cause 400; the host falls back to its default locale (typically en) and proceeds. 2. Honor q-values for fallback ordering. Per RFC 9110: the highest q-value the host supports wins; ties broken by request order. 3. Identify the chosen locale in the response. When the host returns localized content, it SHOULD set the Content-Language response header to the canonical BCP 47 tag of the locale used (e.g., Content-Language: ja-JP). When no localization was applied (everything is the host default), Content-Language SHOULD be omitted OR set to the default locale. 4. Use BCP 47 tags throughout. All locale identifiers (Accept-Language, Content-Language, locale body fields) are BCP 47 language tags. Common tags: en, en-US, en-GB, ja, ja-JP, zh-Hans, zh-Hant, es-419, pt-BR, de-DE, fr-FR, ar, he.
Host MAY:
- Cache localized prompt bundles. If localization is expensive (LLM-translated prompts), caching by
(localeTag, sourceText)is RECOMMENDED. - Lazy-localize: return the default-locale text immediately and emit a
node.completedevent when the localized version is ready. Discouraged for HITL prompts (the human needs the localized text now).
Host MUST NOT:
- Reject a request because of an unsupported
Accept-Language. The header is informational; absence or mismatch falls back gracefully. - Sniff locale from the request body (envelopes, agent reasoning, etc.) to override
Accept-Language. The header is authoritative.
locale field on InterruptPayload
Hosts that need to record the locale used to render a prompt MAY add a locale field to the relevant InterruptPayload.data shape:
{
"kind": "approval",
"key": "...",
"data": {
"actions": ["accept", "reject", "clarify"],
"title": "予算承認",
"description": "Q4予算の最終承認をお願いします。",
"locale": "ja-JP"
}
}
The locale field is OPTIONAL on every InterruptPayload.data variant. When present:
- It MUST be a BCP 47 language tag.
- It MUST identify the locale the human-facing text (
title,description,prompt, etc.) is rendered in. - It MUST be consistent with the response's
Content-Languageheader when both are set.
Clients that resume the interrupt via POST /v1/interrupts/{token} or POST /v1/runs/{runId}/interrupts/{nodeId} MAY include a locale field on the resume payload to indicate the human responded in a particular locale (e.g., for audit-log purposes or sentiment analysis). Hosts MAY ignore the resume locale.
locale field on ErrorEnvelope.details
Hosts MAY add locale to the canonical error envelope when the message field is human-facing localized:
{
"error": "validation_error",
"message": "リクエストボディが不正です。",
"details": {
"locale": "ja-JP",
"field": "workflowId"
}
}
Constraints:
- The
errorcode (machine-readable string) MUST remain English / lowercase / underscore-cased regardless of locale. Error codes are not localized — they're identifiers, not human text. - The
message(human-facing prose) MAY be localized. details.<field>keys are not localized — they're machine-readable schema field names.details.<field>VALUES that are human-facing prose MAY be localized, but SHOULD carry an explicitlocalesibling field on the same nested object so consumers know.
Fallback rules (normative)
When a host receives Accept-Language: <X> and cannot localize to <X>:
1. Try matching by q-value order. Walk the list; the first locale the host supports wins. 2. Try the language family. If the client asks for ja-JP and the host has only ja, use ja and set Content-Language: ja. 3. Fall back to the host default. If no negotiated match exists, use the host's default locale. The host's default SHOULD be advertised in /.well-known/openwop capabilities.i18n.defaultLocale (see §"Capability advertisement" below). 4. Set Content-Language to whatever was actually used. Never lie about the response locale.
Capability advertisement
Hosts that participate in locale negotiation SHOULD advertise:
{
"capabilities": {
"i18n": {
"supported": true,
"defaultLocale": "en",
"supportedLocales": ["en", "en-US", "ja", "ja-JP", "es-419", "fr-FR"]
}
}
}
Field semantics:
i18n.supported(boolean): whentrue, host honorsAccept-Languageon every protected route. Absence orfalsemeans "host serves a single locale always."i18n.defaultLocale(BCP 47 string): the locale the host returns when noAccept-LanguagematchessupportedLocales. Default value if omitted:"en".i18n.supportedLocales(array of BCP 47 strings): the locales the host can return for human-facing text. The array MUST containdefaultLocale. Order is informational only.
Hosts that translate via on-demand machine translation (LLM-mediated) SHOULD list the locales they've validated end-to-end, not every locale the model claims to support.
Replay determinism
Locale negotiation is a request-time concern. A run's event log retains whatever locale the host emitted at the moment of emission:
InterruptPayload.data.localeis captured in theinterrupt.requestedevent payload. Replay re-projects the same locale.Content-Languageon the response is request-scoped and not part of the event log. Replays don't re-render localized prompts; they re-project the locale captured at the original request.
When a workflow is forked (POST /v1/runs/{runId}:fork), the new run starts fresh — its Accept-Language is the fork-request header, not the parent run's. This is the desired behavior: a fork in another locale should localize independently.
Conformance
Hosts that advertise capabilities.i18n.supported: true are expected to pass the capability-gated scenario conformance/src/scenarios/i18n-negotiation.test.ts. The scenario verifies:
Accept-Language: <unsupported>→ response is the default locale,Content-Languagereflects the default.Accept-Language: <supported>→ response is localized,Content-Languagematches.- Malformed
Accept-Language→ request succeeds with default locale. InterruptPayload.data.locale(when emitted) is a valid BCP 47 tag.
Hosts that don't advertise the capability skip-equivalent.
Migration from un-localized hosts
This annex is fully additive. Existing v1 hosts that don't honor Accept-Language:
- Continue to return the host default locale on every request.
- Don't break any existing client (clients that don't send
Accept-Languagesee no change). - Don't break replay (un-localized event logs replay identically).
Clients that want to consume localized content MUST pre-flight via /.well-known/openwop to check capabilities.i18n.supported before sending Accept-Language. Sending the header against an i18n-unaware host has no effect (the host ignores it, returns its default).
See also
interrupt.md—InterruptPayloadshape (thelocalefield is an additive extension)rest-endpoints.md§"Error response shape" —ErrorEnvelope.messageis the localizable fieldcapabilities.md— thei18ncapability block lives here once a host advertises itcompliance.md§GDPR — locale negotiation supports data-subject rights in EU deployments- RFC 9110 §12.5.4 —
Accept-Languagesemantics - BCP 47 — language tag syntax