Localized announcements
Connect your existing translations to runtime notices while keeping answer language, host UI, and lifecycle evidence independent.
Use your existing translations
generative-a11y decides when an announcement is eligible and sends it to the browser. Your application owns translated copy. A catalog contains strings or synchronous functions that call your existing translation system; there is no ICU parser, translation service, or additional i18n dependency.
Try localized announcements in the lifecycle lab. The visible answer remains English. Its response-text intents use en, while completion uses the French catalog's fr. The example is illustrative copy, not a reviewed translation pack or proof of screen-reader pronunciation.
import {createRuntime} from "@generative-a11y/core";
import {en, type Messages} from "@generative-a11y/core/messages";
// English customization can reuse the English defaults.
const catalog = {
...en,
id: "my-app.en.v1",
messages: {
...en.messages,
"response.completed": "Your answer is ready.",
},
} satisfies Messages;
const runtime = createRuntime({ messages: catalog });
// Connect runtime to your existing DOM delivery or React provider.
// The owner calls runtime.dispose() at its terminal ownership boundary.For another language, supply all 25 keys. Do not spread English defaults into a catalog tagged French. A formatter receives only typed labels/counts/outcomes; it can call your existing t() function and use your application's pluralization. It never receives response content, backend errors, or framework objects.
The complete French example catalog and adapter copy shows every required key, including retries, workflows and plural counts. Copy that file into your application as announcements.ts, then replace its illustrative strings with your reviewed translations. Intl.PluralRules and Intl.NumberFormat in that example are standard language APIs, not a library-owned formatting engine.
Language and ownership
messages.locale identifies runtime-generated sentences. Event locale continues to identify host content: response text, explicit announcement, summary, message, and request/resolution labels. An English answer can coexist with French UI notices. Labels interpolated into a catalog sentence must be supplied in that sentence's language; this API does not represent language changes inside a single utterance.
Built-in English notices now explicitly carry en; previously a response locale could incorrectly tag English boilerplate as another language. This is an intentional metadata correction. Channels, timing, quiet mode and lifecycle semantics do not change.
Catalog configuration is validated and copied at construction. All keys are required, the ID and locale are limited to 128 UTF-16 code units, and strings/results to 4,096. Use a non-sensitive versioned ID. Formatters must be pure and synchronous. Invalid, empty, overlong or throwing results produce catalog-format-error and a short English fallback tagged en, without exposing arguments or exception text. A formatter cannot mute a notice or alter urgency. Quiet/policy-suppressed messages never invoke it.
React's A11yProvider accepts messages through its existing options. For a supplied runtime, configure the catalog on that runtime; the provider borrows it and does not reconfigure it. Mount the provider with a stable catalog. Change language at an intentional runtime/provider replacement boundary; do not replace an active runtime on every render.
Existing framework adapters
Pass copy to createChatObserver, bindThread, bindAgent, or useChatAccessibility. This optional AdapterCopy contains locale, toolLabel, approvalRequested, approvalResolved (approved/rejected/cancelled), inputRequested, and inputResolved (submitted/cancelled). Each adapter uses only the fields for events it already recognizes.
import { createChatObserver } from "@generative-a11y/ai-sdk";
import { createRuntime } from "@generative-a11y/core";
import { frenchCatalog, frenchAdapterCopy } from "./announcements";
const runtime = createRuntime({ messages: frenchCatalog });
const observer = createChatObserver({
runtime,
scopeId: "conversation-1",
copy: frenchAdapterCopy,
getToolLabel: () => "Recherche", // optional host label wins over toolLabel
});
// Feed documented public snapshots and terminal callbacks, as in the AI SDK guide.
// Cleanup: observer.dispose(), then runtime.dispose() when owned here.The copy is validated, copied and frozen before subscribing. normalizeAdapterCopy(copy) is exported from @generative-a11y/core/messages for custom adapter authors; it returns that validated immutable copy and throws TypeError for invalid configuration. It performs no translation.
Adapter copy applies its locale only to copy-bearing tool/interaction events, never to generated response text. AI SDK getToolLabel must return text in the configured copy language. The React hook captures copy with its observer; a new runtime/scope or keyed component creates a new observer. Its current callback may still supply a changing tool label. Defaults stay unchanged when copy is omitted, including generic labels that avoid exposing backend tool names.
This customization adds no lifecycle inference and makes no new assistant-ui approval-fidelity claim. Use the existing integration guides for provider order, terminal callbacks and documented framework state:
Test the integration
With ManualClock, dispatch an English response start/delta/completion into a runtime using your French catalog, then advance the clock. Assert English text and French completion intents separately, including their locale fields. A zero minimum gap still schedules delivery; dispatch alone does not drain timers. Provider-owned live regions mount under document.body, outside the returned render container.
Replay V1 remains unchanged. Reproduction requires the same catalog implementation/version, adapter copy, policy, clock ordering and relevant Intl environment. Fixtures contain normalized events and may contain host content; they do not serialize callback code. Devtools shows only catalog ID/locale and the format-error reason, never the catalog, arguments or exceptions.
Browser tests verify literal DOM insertion, language metadata before delivery, focus and cleanup. They cannot establish speech, voice switching, pronunciation or comprehension. WCAG language identification does not require translating every application or make a catalog proof of conformance. Record real browser/AT observations separately.
Public declarations: @generative-a11y/core/messages
All exported values and types for this entry point. Import these names from @generative-a11y/core/messages; the signatures below are reference material, not a replacement for the ownership and lifecycle guidance above.
AdapterCopy — type
interface AdapterCopy {
readonly locale: string;
readonly toolLabel: string;
readonly approvalRequested: string;
readonly approvalResolved: Readonly<Record<"approved" | "rejected" | "cancelled", string>>;
readonly inputRequested: string;
readonly inputResolved: Readonly<Record<"submitted" | "cancelled", string>>;
}en — value
declare const en: Messages;MessageKey — type
type MessageKey = keyof MessageParams;MessageMap — type
type MessageMap = {
readonly [K in MessageKey]: string | ((parameters: Readonly<MessageParams[K]>) => string);
};MessageParams — type
/** Parameters contain only existing user-safe labels and bounded lifecycle facts. */
interface MessageParams {
"response.started": Record<never, never>;
"response.completed": Record<never, never>;
"response.interrupted": Record<never, never>;
"response.failed": Record<never, never>;
"response.retrying": {
attempt?: number;
};
"tool.started": {
label: string;
};
"tool.progress": {
label: string;
percent?: number;
};
"tool.completed": {
label: string;
};
"tool.failed": {
label: string;
};
"run.started": {
label?: string;
};
"run.completed": {
completedSteps: number;
failedSteps: number;
};
"run.interrupted": Record<never, never>;
"run.failed": Record<never, never>;
"run.retrying": {
attempt?: number;
};
"step.started": {
label: string;
};
"step.progress": {
label: string;
percent?: number;
};
"step.completed": {
label: string;
};
"step.interrupted": {
label: string;
};
"step.failed": {
label: string;
};
"step.retrying": {
label: string;
attempt?: number;
};
"interaction.resolved": {
kind: InteractionKind;
outcome: "approved" | "rejected" | "submitted" | "cancelled";
};
"approval.resolved": {
outcome: "approved" | "rejected" | "cancelled";
};
"connection.lost": Record<never, never>;
"connection.restored": Record<never, never>;
"citation.available": {
count: number;
};
}Messages — type
/** Construction-time, complete catalog. Callbacks must be pure and synchronous. */
interface Messages {
readonly id: string;
readonly locale: string;
readonly messages: MessageMap;
}normalizeAdapterCopy — value
/** Validate and snapshot shared adapter copy before creating a binding. */
declare function normalizeAdapterCopy(copy: AdapterCopy): AdapterCopy;