/** * Multi-language phrase system * * Loads phrase files from a configurable directory. * Falls back to built-in English phrases if language files are not found. */ import type { MessagingChannel } from "../types"; /** * A phrase entry: a single string, or an array to rotate through (a random * variant is picked per use). */ export type PhraseValue = string | string[]; /** * Phrase file structure for each language */ export interface Phrases { greeting: PhraseValue; didNotCatch: PhraseValue; /** Unused by the no-speech ladder, which speaks didNotHearRetry/didNotHearFinal instead. Kept for backward compatibility. */ didNotHear: PhraseValue; didNotHearRetry: PhraseValue; didNotHearFinal: PhraseValue; transfer: PhraseValue; acknowledgment: PhraseValue; farewell: { morning: PhraseValue; afternoon: PhraseValue; evening: PhraseValue; }; error: PhraseValue; timeout: PhraseValue; lostQuestion: PhraseValue; /** Spoken when the caller trips the per-number rate limit. */ rateLimited: PhraseValue; /** Spoken or sent when the chat backend fails or returns nothing usable. */ chatError: PhraseValue; flow: { cancelled: PhraseValue; error: PhraseValue; /** Asked when the engine fills some parameters but returns no prompt of its own. */ needMoreDetails: PhraseValue; /** * Words and phrases that end an in-progress flow in this language. A list, * not a rotation: every entry is matched, none is spoken. Matching is * whole-word, case-insensitive and accent-insensitive, so ASCII entries * still match accented speech-to-text output. */ cancellationKeywords: PhraseValue; }; sms: { greeting: PhraseValue; greetingShort: PhraseValue; callForHelp: PhraseValue; processingError: PhraseValue; genericError: PhraseValue; }; whatsapp: { greeting: PhraseValue; greetingShort: PhraseValue; callForHelp: PhraseValue; processingError: PhraseValue; genericError: PhraseValue; }; voice: { overCapPerNumber: PhraseValue; overCapGlobal: PhraseValue; limitUnavailable: PhraseValue; unintelligible: PhraseValue; answerFailed: PhraseValue; }; /** * Per-language instructions handed to the LLM, never spoken or sent to a * caller. Kept in its own namespace because everything else in this file is * copy a host might reasonably rewrite for tone, while these are directions * the pipeline depends on: mixing them into the top-level shape invited a * host to "translate" an instruction into a greeting. */ prompts: { /** * Appended to the outgoing prompt's instructions when * `TalkerConfig.replyLanguages` narrows the reply away from the caller's * detected language. Written in this file's own language (the language the * reply actually lands in), so it needs no runtime substitution - it tells * the LLM to briefly acknowledge the mismatch, then continue in the * language the prompt already named (`Respond in: ...`), rather than * naming a language directly: `loadPhrases` falls a missing key back to * this English copy, and a fallback that named "English" would contradict * a `Respond in: pt` instruction on the same prompt for a language whose * file predates this key, or one that was never shipped at all. */ replyLanguageMismatch: PhraseValue; }; } /** * Load phrases for a language from a directory. * * The code becomes a filename, so it is normalized before it is joined with * a directory - an unvalidated code turns this lookup into an arbitrary-JSON * read (`../package`). Anything malformed loads English instead. */ export declare function loadPhrases(requestedLanguage: string, languageDir?: string): Phrases; type SimplePhraseKey = "greeting" | "didNotCatch" | "didNotHear" | "didNotHearRetry" | "didNotHearFinal" | "transfer" | "acknowledgment" | "error" | "timeout" | "lostQuestion" | "rateLimited" | "chatError"; /** * Get a simple phrase by key */ export declare function getPhrase(language: string, key: SimplePhraseKey, languageDir?: string): string; /** * Get a time-of-day farewell phrase */ export declare function getFarewellPhrase(language: string, languageDir?: string): string; /** * Every `flow` key that is a phrase to speak. `cancellationKeywords` is a * list to match against, so picking one at random would be meaningless; * `getCancellationKeywords` reads it instead. */ type FlowPhraseKey = Exclude; /** * Get a flow-related phrase. * `loadPhrases` merges every language file over the built-in English * fallback at load time, so a phrase file that predates a given `flow` key * (e.g. `error` added after `cancelled`) already resolves to the English * copy for the missing key - no per-call fallback needed here. */ export declare function getFlowPhrase(language: string, key: FlowPhraseKey, languageDir?: string): string; /** * Get the cancellation keywords for a language. * * Returns the whole list rather than one entry: these are matched against * what the caller said, never spoken back. A language file may provide a * lone string for a single keyword; it is normalized to a one-entry list so * callers always get an array, and a fresh one, since the cached phrase tree * must not be mutable through this accessor. * * Blank entries are dropped and an all-blank list falls back to the built-in * English one. The load-time merge already rejects a missing or empty * `cancellationKeywords`, but not a `""` inside it, and an empty keyword * compiles to a pattern that matches nearly every message - which would end * every flow on its first turn. */ export declare function getCancellationKeywords(language: string, languageDir?: string): string[]; /** * Get a phrase for a messaging channel (SMS or WhatsApp). * `loadPhrases` resolves each `whatsapp` key in this precedence: the * language file's own `whatsapp` entry, then its `sms` entry (same * language), then the built-in English `whatsapp` copy - so this reads the * namespace directly. */ export declare function getChannelPhrase(channel: MessagingChannel, language: string, key: keyof Phrases["sms"], languageDir?: string): string; /** * Get an SMS-specific phrase. * @deprecated Removed in 1.0.0. Use `getChannelPhrase("sms", language, key, languageDir)`. */ export declare function getSmsPhrase(language: string, key: keyof Phrases["sms"], languageDir?: string): string; /** * Get a WhatsApp-specific phrase. * Falls back to this language's sms copy, then to English - see * `getChannelPhrase`. * @deprecated Removed in 1.0.0. Use `getChannelPhrase("whatsapp", language, key, languageDir)`. */ export declare function getWhatsAppPhrase(language: string, key: keyof Phrases["whatsapp"], languageDir?: string): string; /** * Get a voice-reply-ladder phrase. * `loadPhrases` fills any language file's missing `voice` keys from the * built-in English `voice` copy at load time, so this reads the namespace * directly - including for files that predate the `voice` namespace. */ export declare function getVoicePhrase(language: string, key: keyof Phrases["voice"], languageDir?: string): string; /** * Get an LLM-facing prompt instruction for a language. * * Separate from `getPhrase` on purpose: nothing under `prompts` is ever spoken * to or sent to a caller, so a host reading the phrase surface can tell at a * glance which strings are copy and which are directions to the model. * `loadPhrases` fills a missing `prompts` key from the built-in English copy * at load time, so this reads the namespace directly - including for phrase * files that predate it. */ export declare function getPromptPhrase(language: string, key: keyof Phrases["prompts"], languageDir?: string): string; export {};