import { AssistantModeConfig } from './conversation'; /** * Every translation key the shared assistant can request, with its English * default (BOFF-7293). * * ★★ WHY THIS EXISTS. Each product seeds the assistant's copy under its own * namespace (`.assistant.`, see `i18n.tsx`), and the i18n seeder * marks every key ABSENT from a product's bundle as DEPRECATED — which * `exportTranslations` then silently omits. Nothing enumerated the keys, so * each product's bundle was hand-extracted from whatever its assistant looked * like at the time. Measured through `exportTranslations` on 2026-09-14, against * the 171 literal keys the assistant requested that day (173 here, after the * `message.copyShort` and `pendingWrite.subtitleReply` splits): HealthyBowl * served 94, VibeControls 15, FluidGrids and BigConsole none. A bundle built * from a partial list does not merely leave the rest English; it DEPRECATES * keys that were live. This is the list. * * Two halves, so neither can drift: * * - `ASSISTANT_I18N_LITERAL_COPY` — every `tr("", "")` call * written out in the source. Those strings live inline in JSX, so there is no * table to import; `i18nManifest.test.ts` parses every module under * `shared/assistant`, and every other module in `src` that names * `useAssistantTr` or `AssistantTr`, and fails on a key missing here, a * default that differs, or an entry no call requests. * - `ASSISTANT_I18N_COMPUTED_COPY` — keys the UI resolves through a variable: * the turn stages and thinking phrases, the voice status labels, the note that * closes an unfinished turn, the attachment sources. It is READ from the same * tables the components render from, so a new stage, phrase or source is in * the manifest with no edit here. * * ★★ PRODUCT COPY STAYS OUT. What a product passes in is the product's to * translate: * * - A mode's `label` is rendered through `mode.`, but its English is the * PRODUCT's label, not the shared "Ask". It is deliberately not in the * manifest: a seeded English value WINS over the label at render time, so * seeding the shared fallback would put "Ask" on both of a product's `general` * and `assistant` pills — the exact defect `AssistantModeSelector` fixed. Use * `assistantModeI18nCopy(modes)`, which takes the words from the product. * - A mode's `description` and `suggestions`, and the page label from * `describeCurrentPage()`, are rendered exactly as passed and never reach * `tr()`. Translate them in the product before passing them. * * HOW A PRODUCT'S EXTRACTOR USES IT * * import { assistantI18nSeedCopy } from * "@burdenoff/fe-libs/shared/assistant/i18nManifest"; * import { MY_ASSISTANT_MODES } from "…"; // the array given to AssistantProductProvider * * const assistantEn = assistantI18nSeedCopy({ * namespace: "healthybowl", // the provider's `product` * modes: MY_ASSISTANT_MODES, * // The product's own English for shared keys, by relative key. It wins. * overrides: { "header.title": "HealthyBowl AI" }, * }); * // { "healthybowl.assistant.header.title": "HealthyBowl AI", * // "healthybowl.assistant.panel.ariaLabel": "AI Assistant", …, * // "healthybowl.assistant.mode.general": "Ask" } * * Merge that into the product's English source alongside its own keys, nest * the dotted keys the way the bundle builder expects, and seed. Then: * * - Build from the fe-libs version the app PINS, not a local checkout. Keys * come and go between releases, and the seeder deprecates what is absent. * - ★★ A product's existing English for a shared key is KEPT. Where its source * already holds one of these keys with different words, that is usually a * deliberate customisation: HealthyBowl's `header.title` is "HealthyBowl AI", * and its `apiCalls.consentBody` names "your HealthyBowl data — creating * crops, logging harvests". Pass those as `overrides`. Taking this English * instead would rename HB's header to "AI Assistant" in English while its * es/zh/hi/ta/ar translations of the same key still say HealthyBowl. * Only a stale copy of an OLD fe-libs default — the product never chose those * words, fe-libs has since reworded them — is replaced by this English, and * only after review, because every existing translation of that key still * says the old words. Keep the product's other keys too: omitting them * deprecates them. * - `overrides` accepts only keys this seed holds, each with non-empty English. * A misspelt key would otherwise leave the shared English in place and add a * key nothing requests — the customisation lost with no error — so it throws. * - Every key must end up `status: ACTIVE`; a published translation of a * DEPRECATED key is still omitted from the export. * - `{{name}}` placeholders are interpolated by `useAssistantTr` and must * survive translation verbatim. * - No key here is both a leaf and a parent (`a.b` and `a.b.c`), which a nested * translation dictionary could not hold; the test pins that too. * * ★★ IMPORT THE DEEP PATH, as above — not the barrel. The barrel re-exports * this module too (its completeness test requires every module to be), and the * re-export survives the build, but importing the barrel evaluates the entire * assistant. Outside a browser that THROWS: the markdown renderer's bundled * entity decoder calls `document.createElement` at load, so an extractor script * importing `@burdenoff/fe-libs/shared/assistant` dies with `document is not * defined` (measured against the built `dist`, 2026-09-14). This module's own * runtime graph is a handful of modules: data tables and the pure mode-id and * mode-capability checks, plus `i18n.tsx` for * `assistantTranslationKey` and the I18n and product providers that module * brings. Its one package is `react`, which every app already has, and it * loads under plain `bun`; the test walks the graph so no UI module or other * package joins it unnoticed. * * ★ `assistantTranslationKey` stays in `i18n.tsx` rather than a pure module. * The build (`preserveModules`) does not keep a re-export in the re-exporting * module, so moving it would drop it from the published * `@burdenoff/fe-libs/shared/assistant/i18n` path, which microfe-healthybowl's * key-compatibility test imports it from (measured against a scratch build, * 2026-09-14). */ /** * Every literal `tr()` call's key and English default. * * ★ Sorted by key. `i18nManifest.test.ts` is the authority on its contents: * add a `tr()` call and the test names the entry to add here, with its default. */ export declare const ASSISTANT_I18N_LITERAL_COPY: { readonly "apiCalls.consentBody": "In Take action mode the assistant can read AND change your data — creating records, logging activity, updating details, and more, on your behalf."; readonly "apiCalls.consentBody2": "It will describe what it is about to do and ask you to confirm before making a change. Only continue if you want the assistant to take actions for you."; readonly "apiCalls.consentCancel": "Stay in Ask"; readonly "apiCalls.consentConfirm": "Enable Take action"; readonly "apiCalls.consentTitle": "Switch to Take action mode?"; readonly "attachments.blockedMany": "{{count}} attachments didn't upload. Retry or remove them before sending."; readonly "attachments.blockedOne": "{{name}} didn't upload. Retry or remove it before sending."; readonly "attachments.empty": "That file is empty."; readonly "attachments.failed": "Upload failed"; readonly "attachments.imagesNotSent": "_Your photo was too large to include with this message, so the answer above is from the text and the file link only. Try a smaller or more tightly cropped photo._"; readonly "attachments.listLabel": "Attached files"; readonly "attachments.messageLabel": "Files sent with this message"; readonly "attachments.open": "Open {{name}}"; readonly "attachments.preparing": "Reading file…"; readonly "attachments.remove": "Remove {{name}}"; readonly "attachments.retry": "Retry uploading {{name}}"; readonly "attachments.retrying": "Retrying {{attempt}} of {{max}}"; readonly "attachments.tooLarge": "That file is larger than 10 MB."; readonly "attachments.tooMany": "You can attach up to {{max}} files per message."; readonly "attachments.unreadable": "That file could not be read."; readonly "attachments.unsupportedImage": "This device can't read that image format, so it can't be resized to send. Save it as JPEG and try again."; readonly "attachments.uploading": "Uploading…"; readonly "attachments.waking": "File service is starting up — retry {{attempt}} of {{max}}"; readonly "chatArea.placeholderActions": "What would you like me to do?"; readonly "chatArea.placeholderGeneral": "Ask me anything…"; readonly "chatInput.ariaLabel": "Message input"; readonly "chatInput.send": "Send message"; readonly "composer.attach": "Attach files or photos"; readonly "composer.attachFull": "Attachment limit reached ({{max}})"; readonly "composer.attachSource": "Attach from"; readonly "composer.connectSource": "Connect a source"; readonly "composer.dropHere": "Drop files to attach"; readonly "composer.hint": "Enter to send · Shift+Enter for newline · drag or paste files to attach"; readonly "composer.placeholder": "Ask me anything…"; readonly "composer.voiceStart": "Dictate a message"; readonly "composer.voiceStarting": "Starting the microphone — tap to cancel"; readonly "composer.voiceStop": "Stop dictation"; readonly "composer.writeAccessDenied": "Changes may not save — your role in this workspace does not currently allow edits. You can still plan and review an action."; readonly "conversationList.voiceOnly": "Spoken conversation"; readonly "conversations.collapse": "Collapse history"; readonly "conversations.collapseLabel": "Collapse"; readonly "conversations.delete": "Delete conversation"; readonly "conversations.empty": "No conversations yet"; readonly "conversations.new": "New conversation"; readonly "conversations.rename": "Rename conversation"; readonly "conversations.renameLabel": "Conversation title"; readonly "conversations.saveTitle": "Save title"; readonly "conversations.startNew": "Start a new conversation"; readonly "conversations.title": "History"; readonly "empty.subtitle": "You're viewing {{page}}. Ask about it, or pick a suggestion below."; readonly "empty.subtitleNoPage": "Ask me anything, or pick a suggestion below."; readonly "empty.title": "How can I help?"; readonly "export.json": "Export as JSON"; readonly "export.markdown": "Export as Markdown"; readonly "header.autoRead": "Auto-read replies"; readonly "header.autoSpeakOff": "Stop reading replies aloud"; readonly "header.autoSpeakOn": "Read replies aloud automatically"; readonly "header.close": "Close assistant"; readonly "header.context": "Context: {{page}}"; readonly "header.dock": "Dock to side panel"; readonly "header.float": "Float chat window"; readonly "header.history": "Conversation history"; readonly "header.more": "More options"; readonly "header.newConversation": "New conversation"; readonly "header.preview": "Preview"; readonly "header.ready": "Ready"; readonly "header.title": "AI Assistant"; readonly "language.inputLabel": "Language"; readonly "language.replyLabel": "Assistant replies in"; readonly "language.replyMatch": "Match my language"; readonly "markdown.closePreview": "Close image preview"; readonly "markdown.copied": "Copied"; readonly "markdown.copy": "Copy"; readonly "markdown.copyCode": "Copy code"; readonly "markdown.enlargeImage": "Enlarge image"; readonly "markdown.enlargeImageNamed": "Enlarge image: {{alt}}"; readonly "markdown.imagePreview": "Image preview"; readonly "markdown.imagePreviewNamed": "Image preview: {{alt}}"; readonly "markdown.imageUnavailable": "Image unavailable"; readonly "markdown.showRemoteImage": "Show remote image"; readonly "markdown.showRemoteImageNamed": "Show image from {{host}}"; readonly "message.copied": "Copied"; readonly "message.copy": "Copy message"; readonly "message.copyShort": "Copy"; readonly "message.downloads": "Downloads"; readonly "message.retry": "Retry"; readonly "message.speak": "Read this reply aloud"; readonly "message.speakShort": "Listen"; readonly "message.stopSpeaking": "Stop reading aloud"; readonly "message.stopSpeakingShort": "Stop"; readonly "messageList.ariaLabel": "Conversation messages"; readonly "modeSelector.ariaLabel": "Select assistant mode"; readonly "overlay.launcher": "AI Assistant"; readonly "overlay.launcherTitle": "AI Assistant ({{shortcut}})"; readonly "panel.ariaLabel": "AI Assistant"; readonly "pendingWrite.approve": "Approve this change"; readonly "pendingWrite.approvedPrompt": "I approved that change — go ahead."; readonly "pendingWrite.approving": "Approving…"; readonly "pendingWrite.aria": "Change awaiting your approval"; readonly "pendingWrite.noValues": "No field values were included with this request."; readonly "pendingWrite.subtitle": "The assistant asked to make this change and was stopped. Approve it, or say what to change."; readonly "pendingWrite.subtitleReply": "The assistant asked to make this change and was stopped. Reply to approve or decline."; readonly "pendingWrite.title": "Nothing has changed yet"; readonly "preview.close": "Close preview"; readonly "preview.failed": "The preview could not be displayed."; readonly "preview.title": "Preview"; readonly "sheet.collapse": "Shrink the assistant"; readonly "sheet.expand": "Expand the assistant"; readonly "speech.codeBlock": "Code block"; readonly "speech.emailAt": "at"; readonly "speech.emailDot": "dot"; readonly "speech.image": "image"; readonly "speech.largeTable": "Table with {{rows}} rows and {{columns}} columns"; readonly "speech.link": "link"; readonly "speech.nestedItem": "Sub-item {{ordinal}}"; readonly "speech.noVoiceForReply": "This reply is in {{language}}, and no {{language}} voice is installed on this device."; readonly "speech.noVoicesOnDevice": "This device has no speech voices installed. Add one in your device speech or accessibility settings."; readonly "speech.truncated": "The rest of this message is not read aloud"; readonly "spokenTurns.many": "{{count}} spoken turns are kept out of this thread"; readonly "spokenTurns.one": "1 spoken turn is kept out of this thread"; readonly "spokenTurns.view": "View transcript"; readonly "tool.details": "Details"; readonly "tool.done": "Done"; readonly "tool.failed": "Some steps failed"; readonly "tool.working": "Working on it…"; readonly "voice.denied": "Microphone access is blocked. Allow it in your browser settings to dictate."; readonly "voice.failed": "Dictation stopped unexpectedly."; readonly "voice.languageFollowApp": "Same as interface"; readonly "voice.languageLabel": "Language you speak to the assistant"; readonly "voice.languageNotSupported": "This browser cannot recognise spoken {{language}}. Try a different browser, or type instead."; readonly "voice.listening": "Listening… speak now, tap the square to stop"; readonly "voice.network": "Speech recognition is offline right now."; readonly "voice.noMic": "No microphone was found."; readonly "voice.preview": "Hear this voice"; readonly "voice.previewShort": "Preview"; readonly "voice.starting": "Starting the microphone…"; readonly "voice.voiceAuto": "Best available"; readonly "voice.voiceLabel": "Voice that reads replies"; readonly "voiceSession.collapse": "Shrink to the compact controller"; readonly "voiceSession.connecting": "Starting the microphone…"; readonly "voiceSession.continueInChat": "Continue in chat"; readonly "voiceSession.continuedIntro": "Continued from a voice conversation. You asked about:"; readonly "voiceSession.continuedMore": "…and {{count}} more"; readonly "voiceSession.continuedNothing": "Continued from a voice conversation."; readonly "voiceSession.earlierLabel": "Earlier"; readonly "voiceSession.end": "End the voice conversation"; readonly "voiceSession.endShort": "End"; readonly "voiceSession.endpointGrace": "Still listening — take your time"; readonly "voiceSession.expand": "Expand the conversation to full screen"; readonly "voiceSession.expandPanel": "Show the conversation in the panel"; readonly "voiceSession.interrupt": "Stop the assistant and speak"; readonly "voiceSession.interrupting": "Stopped — go ahead"; readonly "voiceSession.listening": "Listening…"; readonly "voiceSession.micDenied": "Microphone access is blocked. Allow it in your browser settings to talk."; readonly "voiceSession.mute": "Turn the microphone off"; readonly "voiceSession.muted": "Microphone off"; readonly "voiceSession.newSession": "New voice session"; readonly "voiceSession.noSpeechSupport": "This browser cannot listen. Try a different browser, or type instead."; readonly "voiceSession.noVoiceSupport": "This device has no voice that can speak this language."; readonly "voiceSession.noticeNetwork": "Speech recognition lost its connection. Check your network — it will retry."; readonly "voiceSession.noticeNotCapturing": "The microphone did not start. Check that this site is allowed to use it, then try again."; readonly "voiceSession.noticeNothingHeard": "Nothing heard yet. Check your microphone is not muted or in use by another app."; readonly "voiceSession.noticeRecognitionFailed": "Speech recognition stopped unexpectedly. It will retry — if it keeps happening, reload the page."; readonly "voiceSession.save": "Save transcript"; readonly "voiceSession.savedAt": "Saved"; readonly "voiceSession.sessionLabel": "Session"; readonly "voiceSession.speakerAssistant": "Assistant"; readonly "voiceSession.speakerYou": "You"; readonly "voiceSession.speaking": "Speaking — tap to stop"; readonly "voiceSession.start": "Start a voice conversation"; readonly "voiceSession.title": "Voice conversation"; readonly "voiceSession.transcript": "Voice conversation"; readonly "voiceSession.transcriptClose": "Close the transcript"; readonly "voiceSession.transcriptEmpty": "Nothing spoken yet in this session."; readonly "voiceSession.transcriptHint": "Spoken turns in this conversation — kept out of your typed chat."; readonly "voiceSession.transcriptTitle": "Voice transcript"; readonly "voiceSession.unmute": "Turn the microphone on"; readonly "writeConsent.body": "In {{mode}} mode the assistant can read AND change your data — creating records, logging activity, updating details, and more, on your behalf."; readonly "writeConsent.cancel": "Stay in {{mode}}"; readonly "writeConsent.confirm": "Enable {{mode}}"; readonly "writeConsent.title": "Switch to {{mode}} mode?"; readonly "writeConsent.unconfirmed": "It does not stop to ask before each change — it makes changes as soon as it decides to. Only continue if you want the assistant to change your data without asking first."; }; /** A relative key written out in a `tr()` call. */ export type AssistantI18nLiteralKey = keyof typeof ASSISTANT_I18N_LITERAL_COPY; /** * Keys the UI resolves through a variable, read from the tables it renders * from. */ export declare const ASSISTANT_I18N_COMPUTED_COPY: Readonly>; /** * The manifest: every relative key whose English fe-libs owns, literal and * computed. Mode labels are the product's — see `assistantModeI18nCopy` — and * so is any shared key a product has customised: see `overrides` on * `assistantI18nSeedCopy`. */ export declare const ASSISTANT_I18N_MANIFEST: Readonly>; /** The part of a mode that decides its pill's key and English. */ export type AssistantModeCopySource = Pick; /** * `mode.` for each mode a product offers, in the product's own words. * * Mirrors the pill, which asks for `mode.` with the product's `label` as * the English, or the shared name when the label is empty * (`assistantModeFallbackLabel`, which also names a product-declared mode). * Modes the product does not offer are never requested, so they are not here. * * @throws when a mode id is malformed. Its key could not be seeded — a `.` * splits it, a capital or space makes it unmatchable — and a seed that * silently carried it would deprecate nothing and translate nothing. */ export declare function assistantModeI18nCopy(modes: readonly AssistantModeCopySource[]): Record; /** * Everything a product seeds for the shared assistant, as FULL keys under its * namespace: the manifest, its own mode labels, and its own English for any * shared key it has customised. * * `namespace` is the id given to `AssistantProductProvider` / * `AssistantI18nProvider`; the keys are built by `assistantTranslationKey`, the * same function `useAssistantTr` looks them up with. * * `overrides` is applied last, by RELATIVE key, so the product's words win — * see the recipe in the module docblock for which words those are. Each * replaces exactly its own key: HealthyBowl's `header.title` leaves * `panel.ariaLabel`, whose English is also "AI Assistant", untouched. * * @throws when an override names a key this seed does not hold, or gives it * blank English — a blank value would render as nothing, not as a fallback. */ export declare function assistantI18nSeedCopy(args: { namespace: string; modes: readonly AssistantModeCopySource[]; overrides?: Readonly>; }): Record; //# sourceMappingURL=i18nManifest.d.ts.map