OpenWOP openwop.dev

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.md and the existing error-envelope contract. Keywords MUST, SHOULD, MAY follow RFC 2119. See auth.md for 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.prompt fields 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-Language request header semantics on every REST endpoint.
  • Optional locale field on InterruptPayload shapes that carry human-facing prompts (approval / clarification / conversation).
  • Optional locale field on the ErrorEnvelope.details for 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.completed event 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-Language header 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 error code (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 explicit locale sibling 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): when true, host honors Accept-Language on every protected route. Absence or false means "host serves a single locale always."
  • i18n.defaultLocale (BCP 47 string): the locale the host returns when no Accept-Language matches supportedLocales. 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 contain defaultLocale. 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.locale is captured in the interrupt.requested event payload. Replay re-projects the same locale.
  • Content-Language on 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-Language reflects the default.
  • Accept-Language: <supported> → response is localized, Content-Language matches.
  • 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-Language see 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.mdInterruptPayload shape (the locale field is an additive extension)
  • rest-endpoints.md §"Error response shape" — ErrorEnvelope.message is the localizable field
  • capabilities.md — the i18n capability block lives here once a host advertises it
  • compliance.md §GDPR — locale negotiation supports data-subject rights in EU deployments
  • RFC 9110 §12.5.4Accept-Language semantics
  • BCP 47 — language tag syntax