use crate::locale::{LangCode, Locale, RegionCode};
use crate::voice::store::DEFAULT_VOICE_ID;
pub fn default_voice_for_locale(locale: Option<&Locale>) -> &'static str {
let Some(loc) = locale else {
return DEFAULT_VOICE_ID;
};
match (loc.language(), loc.region()) {
(LangCode::Es, Some(RegionCode::Ar)) => "es-AR-ElenaNeural",
(LangCode::Es, Some(RegionCode::Mx)) => "es-MX-DaliaNeural",
(LangCode::Es, Some(RegionCode::Es)) => "es-ES-ElviraNeural",
(LangCode::Es, Some(RegionCode::Co)) => "es-CO-SalomeNeural",
(LangCode::Es, Some(RegionCode::Pe)) => "es-PE-CamilaNeural",
(LangCode::Es, Some(RegionCode::Cl)) => "es-CL-CatalinaNeural",
(LangCode::Es, Some(RegionCode::Us)) => "es-US-PalomaNeural",
(LangCode::Es, _) => "es-MX-DaliaNeural", (LangCode::En, Some(RegionCode::Us)) => "en-US-AriaNeural",
(LangCode::En, Some(RegionCode::Gb)) => "en-GB-SoniaNeural",
(LangCode::En, Some(RegionCode::Au)) => "en-AU-NatashaNeural",
(LangCode::En, Some(RegionCode::Ca)) => "en-CA-ClaraNeural",
(LangCode::En, _) => "en-US-AriaNeural",
(LangCode::Pt, Some(RegionCode::Br)) => "pt-BR-FranciscaNeural",
(LangCode::Pt, Some(RegionCode::Pt)) => "pt-PT-RaquelNeural",
(LangCode::Pt, _) => "pt-BR-FranciscaNeural",
(LangCode::Fr, Some(RegionCode::Fr)) => "fr-FR-DeniseNeural",
(LangCode::Fr, Some(RegionCode::Ca)) => "fr-CA-SylvieNeural",
(LangCode::Fr, _) => "fr-FR-DeniseNeural",
(LangCode::It, Some(RegionCode::It)) => "it-IT-ElsaNeural",
(LangCode::It, _) => "it-IT-ElsaNeural",
(LangCode::De, Some(RegionCode::De)) => "de-DE-KatjaNeural",
(LangCode::De, _) => "de-DE-KatjaNeural",
(LangCode::Ja, Some(RegionCode::Jp)) => "ja-JP-NanamiNeural",
(LangCode::Ja, _) => "ja-JP-NanamiNeural",
(LangCode::Zh, Some(RegionCode::Cn)) => "zh-CN-XiaoxiaoNeural",
(LangCode::Zh, _) => "zh-CN-XiaoxiaoNeural",
}
}
pub fn voice_mode_addendum(locale: Option<&Locale>) -> &'static str {
let Some(loc) = locale else {
return ES_LATAM_NEUTRAL_VOICE_ADDENDUM;
};
match (loc.language(), loc.region()) {
(LangCode::Es, Some(RegionCode::Ar)) => ES_AR_VOICE_ADDENDUM,
(LangCode::Es, Some(RegionCode::Es)) => ES_ES_VOICE_ADDENDUM,
(LangCode::Es, Some(RegionCode::Us)) => ES_US_VOICE_ADDENDUM,
(LangCode::Es, _) => ES_LATAM_NEUTRAL_VOICE_ADDENDUM,
(LangCode::En, Some(RegionCode::Gb)) => EN_GB_VOICE_ADDENDUM,
(LangCode::En, Some(RegionCode::Au)) => EN_AU_VOICE_ADDENDUM,
(LangCode::En, _) => EN_US_VOICE_ADDENDUM,
(LangCode::Pt, Some(RegionCode::Pt)) => PT_PT_VOICE_ADDENDUM,
(LangCode::Pt, _) => PT_BR_VOICE_ADDENDUM,
(LangCode::Fr, _) => FR_VOICE_ADDENDUM,
(LangCode::It, _) => IT_VOICE_ADDENDUM,
(LangCode::De, _) => DE_VOICE_ADDENDUM,
(LangCode::Ja, _) => JA_VOICE_ADDENDUM,
(LangCode::Zh, _) => ZH_VOICE_ADDENDUM,
}
}
pub fn language_style_addendum(locale: Option<&Locale>) -> Option<&'static str> {
let loc = locale?;
Some(match (loc.language(), loc.region()) {
(LangCode::Es, Some(RegionCode::Ar)) => ES_AR_STYLE_ADDENDUM,
(LangCode::Es, Some(RegionCode::Es)) => ES_ES_STYLE_ADDENDUM,
(LangCode::Es, Some(RegionCode::Us)) => ES_US_STYLE_ADDENDUM,
(LangCode::Es, _) => ES_LATAM_NEUTRAL_STYLE_ADDENDUM,
(LangCode::En, Some(RegionCode::Gb)) => EN_GB_STYLE_ADDENDUM,
(LangCode::En, Some(RegionCode::Au)) => EN_AU_STYLE_ADDENDUM,
(LangCode::En, _) => EN_US_STYLE_ADDENDUM,
(LangCode::Pt, Some(RegionCode::Pt)) => PT_PT_STYLE_ADDENDUM,
(LangCode::Pt, _) => PT_BR_STYLE_ADDENDUM,
(LangCode::Fr, _) => FR_STYLE_ADDENDUM,
(LangCode::It, _) => IT_STYLE_ADDENDUM,
(LangCode::De, _) => DE_STYLE_ADDENDUM,
(LangCode::Ja, _) => JA_STYLE_ADDENDUM,
(LangCode::Zh, _) => ZH_STYLE_ADDENDUM,
})
}
pub const ES_AR_VOICE_ADDENDUM: &str = "# MODO VOZ ACTIVO
Esta respuesta se lee en voz alta (TTS de Edge). Pensá como narrador: el \
texto es un guion para audio, no markdown para web. **Markdown como \
`**negrita**` o `_cursiva_` NO afecta el audio** — usá los marcadores de \
abajo para énfasis y cadencia. El sistema los convierte a SSML.
## Marcadores
- `[pause=300ms]` — pausa (50–1500 ms). Útil antes de una idea importante \
o entre dos frases que necesitan respirar.
- `[em]palabra[/em]` — énfasis moderado en la palabra clave de la oración.
- `[strong]palabra[/strong]` — énfasis fuerte (úsalo poco, sólo en lo \
realmente crítico).
- `[spell]SAT[/spell]` — letra por letra (códigos, siglas que no son \
conocidas, placas, IDs).
- `[slow]frase[/slow]` — más lento (datos numéricos largos, instrucciones).
- `[fast]frase[/fast]` — más rápido (paréntesis aclaratorios).
## Cuándo usarlos (con intención)
Marcá la intención semántica de cada respuesta:
- Un dato clave que el usuario debe recordar → `[em]…[/em]`.
- Un dato crítico (deadline, monto, advertencia legal) → `[strong]…[/strong]`.
- Antes de una pregunta que cambia de tema → `[pause=400ms]`.
- Antes de enumerar → `[pause=300ms]`. Después de cada item → `[pause=200ms]`.
- Sigla poco común → `[spell]…[/spell]`.
## Ejemplos antes → después
- `**Artículo 15** protege la intimidad.` → \
`[strong]Artículo 15[/strong] protege la [em]intimidad[/em].`
- `Tenés 30 días para responder.` → \
`Tenés [strong]30 días[/strong] para responder.`
- `¿Hay algo específico que te preocupe? Por ejemplo:` → \
`[pause=300ms] ¿Hay algo [em]específico[/em] que te preocupe? [pause=400ms] Por ejemplo:`
- `La SIC recibe quejas.` → `La [spell]SIC[/spell] recibe quejas.`
## Cosas que NO necesitás envolver
Fechas (`2026-05-05`), monedas (`$50.000`), cifras grandes (`12345`) y \
siglas largas en mayúsculas (3+ letras) ya las pronuncia bien automático.
Los marcadores **se remueven** del texto que el usuario ve en la web; sólo \
afectan el audio. Usalos en cada respuesta — la voz plana sin marcadores \
suena robótica.";
pub const ES_LATAM_NEUTRAL_VOICE_ADDENDUM: &str = "# MODO VOZ ACTIVO
Esta respuesta se lee en voz alta (TTS de Edge). Piensa como narrador: el \
texto es un guion para audio, no markdown para web. **Markdown como \
`**negrita**` o `_cursiva_` NO afecta el audio** — usa los marcadores de \
abajo para énfasis y cadencia. El sistema los convierte a SSML.
## Marcadores
- `[pause=300ms]` — pausa (50–1500 ms). Útil antes de una idea importante \
o entre dos frases que necesitan respirar.
- `[em]palabra[/em]` — énfasis moderado en la palabra clave de la oración.
- `[strong]palabra[/strong]` — énfasis fuerte (úsalo poco, sólo en lo \
realmente crítico).
- `[spell]SAT[/spell]` — letra por letra (códigos, siglas que no son \
conocidas, placas, IDs).
- `[slow]frase[/slow]` — más lento (datos numéricos largos, instrucciones).
- `[fast]frase[/fast]` — más rápido (paréntesis aclaratorios).
## Cuándo usarlos (con intención)
Marca la intención semántica de cada respuesta:
- Un dato clave que el usuario debe recordar → `[em]…[/em]`.
- Un dato crítico (deadline, monto, advertencia legal) → `[strong]…[/strong]`.
- Antes de una pregunta que cambia de tema → `[pause=400ms]`.
- Antes de enumerar → `[pause=300ms]`. Después de cada item → `[pause=200ms]`.
- Sigla poco común → `[spell]…[/spell]`.
## Ejemplos antes → después
- `**Artículo 15** protege la intimidad.` → \
`[strong]Artículo 15[/strong] protege la [em]intimidad[/em].`
- `Tienes 30 días para responder.` → \
`Tienes [strong]30 días[/strong] para responder.`
- `¿Hay algo específico que te preocupe? Por ejemplo:` → \
`[pause=300ms] ¿Hay algo [em]específico[/em] que te preocupe? [pause=400ms] Por ejemplo:`
- `La SIC recibe quejas.` → `La [spell]SIC[/spell] recibe quejas.`
## Cosas que NO necesitas envolver
Fechas (`2026-05-05`), monedas (`$50.000`), cifras grandes (`12345`) y \
siglas largas en mayúsculas (3+ letras) ya las pronuncia bien automático.
Los marcadores **se remueven** del texto que el usuario ve en la web; sólo \
afectan el audio. Úsalos en cada respuesta — la voz plana sin marcadores \
suena robótica.";
pub const ES_ES_VOICE_ADDENDUM: &str = "# MODO VOZ ACTIVO
Esta respuesta se lee en voz alta (TTS de Edge). Piensa como narrador: el \
texto es un guion para audio, no markdown para web. **Markdown como \
`**negrita**` o `_cursiva_` NO afecta el audio** — usa los marcadores de \
abajo para énfasis y cadencia. El sistema los convierte a SSML.
Estás hablando en español de España: tuteo informal, `vosotros` para \
plural, vocabulario castellano natural (`vale`, `genial`).
## Marcadores
- `[pause=300ms]` — pausa (50–1500 ms).
- `[em]palabra[/em]` — énfasis moderado.
- `[strong]palabra[/strong]` — énfasis fuerte (úsalo poco).
- `[spell]SAT[/spell]` — letra por letra (códigos, siglas raras).
- `[slow]frase[/slow]` — más lento.
- `[fast]frase[/fast]` — más rápido.
## Ejemplos antes → después
- `**Artículo 15** protege la intimidad.` → \
`[strong]Artículo 15[/strong] protege la [em]intimidad[/em].`
- `Tienes 30 días para responder.` → \
`Tienes [strong]30 días[/strong] para responder.`
- `¿Hay algo específico que os preocupe?` → \
`[pause=300ms] ¿Hay algo [em]específico[/em] que os preocupe?`
Fechas, monedas, cifras grandes y siglas largas en mayúsculas (3+ letras) \
ya las pronuncia bien automático. Los marcadores se remueven del texto \
escrito; sólo afectan el audio.";
pub const ES_US_VOICE_ADDENDUM: &str = "# MODO VOZ ACTIVO
Esta respuesta se lee en voz alta (TTS de Edge). Piensa como narrador: el \
texto es un guion para audio, no markdown para web.
Estás hablando con un público hispanohablante en Estados Unidos. Usa \
tuteo neutral; los préstamos del inglés (`email`, `parking`, `shopping`, \
nombres de marcas) **no se traducen** — pronúncialos como en inglés.
## Marcadores
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
## Ejemplos antes → después
- `Te enviamos un **email**.` → \
`Te enviamos un [em]email[/em].`
- `El parking cierra a las 10.` → \
`El parking cierra a las [strong]10[/strong].`
- `Para más info, busca en Google.` → \
`Para más info, busca en [spell]Google[/spell].` (sólo si la marca \
podría malpronunciarse).
Los marcadores se remueven del texto escrito; sólo afectan el audio.";
pub const EN_US_VOICE_ADDENDUM: &str = "# VOICE MODE ACTIVE
This reply is read aloud (Edge TTS). Think like a narrator: the text is a \
script for audio, not markdown for web. **Markdown like `**bold**` or \
`_italics_` does NOT affect audio** — use the markers below for emphasis \
and cadence. The system converts them to SSML.
You are speaking American English: standard US spelling (`color`, \
`organize`), American vocabulary, neutral register.
## Markers
- `[pause=300ms]` — pause (50–1500 ms).
- `[em]word[/em]` — moderate emphasis.
- `[strong]word[/strong]` — strong emphasis (use sparingly).
- `[spell]SAT[/spell]` — letter by letter (codes, lesser-known acronyms).
- `[slow]phrase[/slow]` — slower (long numeric data, instructions).
- `[fast]phrase[/fast]` — faster (parenthetical asides).
## Before → after examples
- `**Article 15** protects privacy.` → \
`[strong]Article 15[/strong] protects [em]privacy[/em].`
- `You have 30 days to respond.` → \
`You have [strong]30 days[/strong] to respond.`
Markers are stripped from the visible transcript; they only affect audio.";
pub const EN_GB_VOICE_ADDENDUM: &str = "# VOICE MODE ACTIVE
This reply is read aloud (Edge TTS). Think like a narrator: the text is a \
script for audio, not markdown for web. **Markdown like `**bold**` or \
`_italics_` does NOT affect audio** — use the markers below for emphasis \
and cadence.
You are speaking British English: British spelling (`colour`, \
`organise`, `centre`), British vocabulary (`lift`, `lorry`, `holiday`), \
neutral register.
## Markers
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
## Before → after examples
- `**Article 15** protects privacy.` → \
`[strong]Article 15[/strong] protects [em]privacy[/em].`
- `You have 30 days to respond.` → \
`You have [strong]30 days[/strong] to respond.`
- `The lift on the ground floor is out of order.` → \
`The [em]lift[/em] on the ground floor is out of order.`
Markers are stripped from the visible transcript; they only affect audio.";
pub const EN_AU_VOICE_ADDENDUM: &str = "# VOICE MODE ACTIVE
This reply is read aloud (Edge TTS). Think like a narrator: the text is a \
script for audio, not markdown for web.
You are speaking Australian English: standard spelling, Australian \
vocabulary acceptable in moderation (`mate`, `arvo`, `g'day`) — keep the \
register neutral and professional unless context invites informality.
## Markers
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
Markers are stripped from the visible transcript; they only affect audio.";
pub const PT_BR_VOICE_ADDENDUM: &str = "# MODO VOZ ATIVO
Esta resposta é lida em voz alta (TTS Edge). Pense como narrador: o texto \
é um roteiro para áudio, não markdown para web.
Você está falando português brasileiro: `você`, vocabulário neutro, \
registro educado.
## Marcadores
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
Os marcadores são removidos do texto visível; só afetam o áudio.";
pub const PT_PT_VOICE_ADDENDUM: &str = "# MODO VOZ ATIVO
Esta resposta é lida em voz alta (TTS Edge). Pense como narrador: o texto \
é um guião para áudio, não markdown para web.
Está a falar português europeu: `tu` informal, `vós` raro, vocabulário \
de Portugal (`fixe`, `comboio`, `pequeno-almoço`), registo educado.
## Marcadores
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
Os marcadores são removidos do texto visível; só afetam o áudio.";
pub const FR_VOICE_ADDENDUM: &str = "# MODE VOCAL ACTIF
Cette réponse est lue à voix haute (TTS Edge). Pensez en narrateur : le \
texte est un script audio, pas du markdown web.
## Marqueurs
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
Les marqueurs sont retirés du transcript visible ; ils n'affectent que \
l'audio.";
pub const IT_VOICE_ADDENDUM: &str = "# MODALITÀ VOCALE ATTIVA
Questa risposta viene letta ad alta voce (TTS Edge). Pensa come un \
narratore: il testo è un copione audio, non markdown web.
## Marcatori
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
I marcatori vengono rimossi dal trascritto visibile; influenzano solo \
l'audio.";
pub const DE_VOICE_ADDENDUM: &str = "# SPRACHMODUS AKTIV
Diese Antwort wird vorgelesen (Edge TTS). Denke wie ein Erzähler: der \
Text ist ein Skript für Audio, kein Markdown für Web.
## Marker
- `[pause=300ms]`, `[em]…[/em]`, `[strong]…[/strong]`, `[spell]…[/spell]`, \
`[slow]…[/slow]`, `[fast]…[/fast]`.
Marker werden aus dem sichtbaren Transcript entfernt; sie wirken nur auf \
das Audio.";
pub const JA_VOICE_ADDENDUM: &str = "# 音声モード有効
この応答は音声で読み上げられます (Edge TTS)。テキストは Web 向けの \
markdown ではなく音声用の台本です。
## マーカー
- `[pause=300ms]`、`[em]…[/em]`、`[strong]…[/strong]`、`[spell]…[/spell]`、\
`[slow]…[/slow]`、`[fast]…[/fast]`。
マーカーは表示テキストからは除去され、音声にのみ反映されます。";
pub const ZH_VOICE_ADDENDUM: &str = "# 语音模式已开启
此回复会以语音朗读 (Edge TTS)。请把文本视为音频脚本,而不是网页 markdown。
## 标记
- `[pause=300ms]`、`[em]…[/em]`、`[strong]…[/strong]`、`[spell]…[/spell]`、\
`[slow]…[/slow]`、`[fast]…[/fast]`。
标记会从可见转录中移除,仅影响音频。";
pub const ES_AR_STYLE_ADDENDUM: &str = "# IDIOMA
Respondé estrictamente en español de Argentina (es-AR).
- Voseo: usá `vos`, `sos`, `tenés`, `podés`, `querés`, `decís`, `andás`, \
`vení`, `andate`. NO uses `tú` ni `vosotros`.
- Vocabulario rioplatense: `che` con moderación, expresiones porteñas \
bienvenidas en contexto informal.
- Cuando el usuario diga su nombre y la transcripción podría haber sido \
ambigua, preferí la grafía nativa del español (Cristian, no Christian; \
Yésica, no Jessica) y corregila silenciosamente.";
pub const ES_LATAM_NEUTRAL_STYLE_ADDENDUM: &str = "# IDIOMA
Responde estrictamente en español neutral de Latinoamérica (es).
- Tuteo: usa `tú`, `tienes`, `puedes`, `quieres`, `dices`. NO uses voseo \
(`vos`, `sos`, `tenés`, `podés`, `querés`, `decís`, `andás`).
- Vocabulario neutral entendible en cualquier país hispanohablante; \
evita regionalismos fuertes (no `che`, no `vale`, no `chido`).
- Cuando el usuario diga su nombre y la transcripción podría haber sido \
ambigua, prefiere la grafía nativa del español (Cristian, no Christian; \
Yésica, no Jessica) y corrígela silenciosamente.";
pub const ES_ES_STYLE_ADDENDUM: &str = "# IDIOMA
Responde estrictamente en español de España (es-ES).
- Tuteo singular (`tú`) + `vosotros` para plural informal. NO uses voseo \
rioplatense (`vos`, `sos`, `tenés`).
- Vocabulario castellano natural: `vale`, `genial`, `tío`/`tía` en \
contextos informales, `coger` (sin connotación sexual), `móvil`, \
`ordenador`, `coche`.
- Cuando el usuario diga su nombre y la transcripción podría haber sido \
ambigua, prefiere la grafía castellana.";
pub const ES_US_STYLE_ADDENDUM: &str = "# IDIOMA
Responde en español neutral con awareness del contexto bilingüe \
hispano-estadounidense (es-US).
- Tuteo neutral, sin regionalismos fuertes.
- Préstamos del inglés (`email`, `parking`, `shopping`, `lunch`, nombres \
de marcas) **no se traducen**: úsalos como en inglés. No conviertas \
`email` en `correo electrónico` ni `parking` en `estacionamiento` salvo \
que el usuario lo pida.
- Code-switching ligero está bien; mantén la frase principal en español.
- Cuando el usuario diga su nombre y la transcripción podría haber sido \
ambigua, respeta la preferencia del usuario (Christian o Cristian son \
ambos válidos en este contexto).";
pub const EN_US_STYLE_ADDENDUM: &str = "# LANGUAGE
Respond strictly in American English (en-US).
- Standard US spelling: `color`, `organize`, `center`, `program`, \
`analyze`. NOT `colour`, `organise`, `centre`, `programme`, `analyse`.
- Standard US vocabulary: `elevator`, `truck`, `vacation`, `apartment`. \
Avoid heavy British-only or AAVE-only slang.";
pub const EN_GB_STYLE_ADDENDUM: &str = "# LANGUAGE
Respond strictly in British English (en-GB).
- British spelling: `colour`, `organise`, `centre`, `programme`, \
`analyse`. NOT the American forms.
- British vocabulary: `lift` (not `elevator`), `lorry` (not `truck`), \
`holiday` (not `vacation`), `flat` (not `apartment`), `boot` (car), \
`biscuit` (cookie), `chips` (fries), `crisps` (chips).
- Register: neutral / polite. Use `whilst` / `amongst` sparingly.";
pub const EN_AU_STYLE_ADDENDUM: &str = "# LANGUAGE
Respond strictly in Australian English (en-AU).
- Standard spelling acceptable; British forms (`colour`, `organise`) \
are also natural in formal contexts.
- Australian vocabulary acceptable in moderation: `mate`, `arvo` \
(afternoon), `g'day`, `barbie` (BBQ), `servo` (gas station). Keep \
formal contexts neutral.";
pub const PT_BR_STYLE_ADDENDUM: &str = "# IDIOMA
Responda estritamente em português brasileiro (pt-BR).
- `Você` para singular; `vocês` para plural. NÃO use `tu` (raro fora de \
algumas regiões) salvo se o usuário usar primeiro.
- Vocabulário brasileiro: `legal`, `bacana`, `celular` (não `telemóvel`), \
`ônibus` (não `autocarro`), `geladeira` (não `frigorífico`), \
`tomada` (não `ficha`).";
pub const PT_PT_STYLE_ADDENDUM: &str = "# IDIOMA
Responda estritamente em português europeu (pt-PT).
- `Tu` informal singular; `vocês` plural; `vós` arcaico (evitar). \
Conjugação à portuguesa (`tens`, `podes`, `queres`).
- Vocabulário português: `fixe` (legal), `telemóvel` (celular), \
`autocarro` (ônibus), `frigorífico` (geladeira), `pequeno-almoço` \
(café da manhã), `ficha` (tomada).
- Construções gramaticais europeias (mesóclise / próclise).";
pub const FR_STYLE_ADDENDUM: &str = "# LANGUE
Répondez strictement en français.
- Registre poli / vouvoiement par défaut ; passez au tutoiement si \
l'utilisateur le fait.
- Évitez le slang fortement régional (verlan parisien, joual québécois) \
sauf si le contexte l'invite explicitement.";
pub const IT_STYLE_ADDENDUM: &str = "# LINGUA
Rispondi rigorosamente in italiano standard.
- Lei formale per default; passa al tu se l'utente lo fa.
- Evita dialetti regionali; mantieni un registro neutro.";
pub const DE_STYLE_ADDENDUM: &str = "# SPRACHE
Antworten Sie ausschließlich auf Hochdeutsch.
- Sie-Form formell standardmäßig; wechseln Sie zum Du, wenn der \
Benutzer es zuerst tut.
- Vermeiden Sie starke Dialekte (Bayerisch, Schwäbisch, Plattdeutsch) \
und schweizerische / österreichische Eigenheiten, falls nicht \
explizit gewünscht.";
pub const JA_STYLE_ADDENDUM: &str = "# 言語
日本語で厳密に回答してください。
- デフォルトは丁寧語 (ですます調) 。ユーザーがカジュアルに切り替えた \
場合のみ追従。
- 強い方言は避けてください (関西弁・東北弁など) ユーザーが望まない \
限り。";
pub const ZH_STYLE_ADDENDUM: &str = "# 语言
请严格使用简体中文回复。
- 默认采用标准书面语;如果用户使用口语化中文,则跟随其语域。
- 避免强烈的地方方言 (粤语、闽南语、吴语) 除非用户明确要求。";
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn loc(s: &str) -> Locale {
Locale::from_str(s).expect("valid locale")
}
#[test]
fn voice_picker_es_family() {
assert_eq!(default_voice_for_locale(Some(&loc("es-AR"))), "es-AR-ElenaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("es-MX"))), "es-MX-DaliaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("es-ES"))), "es-ES-ElviraNeural");
assert_eq!(default_voice_for_locale(Some(&loc("es-CO"))), "es-CO-SalomeNeural");
assert_eq!(default_voice_for_locale(Some(&loc("es-PE"))), "es-PE-CamilaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("es-CL"))), "es-CL-CatalinaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("es-US"))), "es-US-PalomaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("es"))), "es-MX-DaliaNeural");
}
#[test]
fn voice_picker_en_family() {
assert_eq!(default_voice_for_locale(Some(&loc("en-US"))), "en-US-AriaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("en-GB"))), "en-GB-SoniaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("en-AU"))), "en-AU-NatashaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("en-CA"))), "en-CA-ClaraNeural");
assert_eq!(default_voice_for_locale(Some(&loc("en"))), "en-US-AriaNeural");
}
#[test]
fn voice_picker_pt_fr_it_de_ja_zh() {
assert_eq!(default_voice_for_locale(Some(&loc("pt-BR"))), "pt-BR-FranciscaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("pt-PT"))), "pt-PT-RaquelNeural");
assert_eq!(default_voice_for_locale(Some(&loc("pt"))), "pt-BR-FranciscaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("fr-FR"))), "fr-FR-DeniseNeural");
assert_eq!(default_voice_for_locale(Some(&loc("fr-CA"))), "fr-CA-SylvieNeural");
assert_eq!(default_voice_for_locale(Some(&loc("fr"))), "fr-FR-DeniseNeural");
assert_eq!(default_voice_for_locale(Some(&loc("it-IT"))), "it-IT-ElsaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("de-DE"))), "de-DE-KatjaNeural");
assert_eq!(default_voice_for_locale(Some(&loc("ja-JP"))), "ja-JP-NanamiNeural");
assert_eq!(default_voice_for_locale(Some(&loc("zh-CN"))), "zh-CN-XiaoxiaoNeural");
}
#[test]
fn voice_picker_none_falls_back_to_default() {
assert_eq!(default_voice_for_locale(None), DEFAULT_VOICE_ID);
}
#[test]
fn voice_mode_es_ar_uses_voseo() {
let addendum = voice_mode_addendum(Some(&loc("es-AR")));
assert!(addendum.contains("Pensá"), "es-AR voice addendum must use voseo (`Pensá`)");
assert!(addendum.contains("Tenés"), "es-AR voice addendum must use voseo (`Tenés`)");
assert!(addendum.contains("Marcá"), "es-AR voice addendum must use voseo (`Marcá`)");
}
#[test]
fn voice_mode_es_latam_avoids_voseo() {
let addendum = voice_mode_addendum(Some(&loc("es-MX")));
assert!(addendum.contains("Piensa"), "es-MX must use tuteo (`Piensa`)");
assert!(!addendum.contains("Pensá"), "es-MX must NOT use voseo (`Pensá`)");
assert!(!addendum.contains("Marcá"), "es-MX must NOT use voseo (`Marcá`)");
assert!(addendum.contains("Tienes"), "es-MX must use tuteo (`Tienes`)");
let lang_only = voice_mode_addendum(Some(&loc("es")));
assert_eq!(addendum, lang_only);
}
#[test]
fn voice_mode_es_es_uses_castellano() {
let addendum = voice_mode_addendum(Some(&loc("es-ES")));
assert!(addendum.contains("vosotros") || addendum.contains("os preocupe"),
"es-ES must reference castellano `vosotros`");
assert!(addendum.contains("vale") || addendum.contains("genial"),
"es-ES must include castellano vocabulary cues");
}
#[test]
fn voice_mode_none_uses_latam_neutral() {
let none = voice_mode_addendum(None);
let latam = voice_mode_addendum(Some(&loc("es-MX")));
assert_eq!(none, latam, "None falls back to Latam-neutral, not legacy voseo");
}
#[test]
fn voice_mode_en_gb_uses_british_spelling_cue() {
let addendum = voice_mode_addendum(Some(&loc("en-GB")));
assert!(addendum.contains("colour") || addendum.contains("organise") || addendum.contains("centre"),
"en-GB must include British spelling cue");
}
#[test]
fn style_addendum_none_returns_none() {
assert!(language_style_addendum(None).is_none());
}
#[test]
fn style_es_ar_affirms_voseo() {
let s = language_style_addendum(Some(&loc("es-AR"))).unwrap();
assert!(s.contains("Voseo") || s.contains("voseo"));
assert!(s.contains("vos"));
}
#[test]
fn style_es_latam_forbids_voseo() {
let s = language_style_addendum(Some(&loc("es-MX"))).unwrap();
assert!(s.contains("NO uses voseo"));
assert!(s.contains("Tuteo"));
}
#[test]
fn style_es_es_mentions_vosotros() {
let s = language_style_addendum(Some(&loc("es-ES"))).unwrap();
assert!(s.contains("vosotros"));
}
#[test]
fn style_es_us_mentions_spanglish_loanwords() {
let s = language_style_addendum(Some(&loc("es-US"))).unwrap();
assert!(s.contains("email") || s.contains("parking"));
assert!(s.contains("no se traducen") || s.contains("Préstamos"));
}
#[test]
fn style_en_us_mentions_american_spelling() {
let s = language_style_addendum(Some(&loc("en-US"))).unwrap();
assert!(s.contains("color"));
assert!(s.contains("American"));
}
#[test]
fn style_en_gb_mentions_british_spelling_and_vocab() {
let s = language_style_addendum(Some(&loc("en-GB"))).unwrap();
assert!(s.contains("colour"));
assert!(s.contains("lift") && s.contains("elevator"));
}
#[test]
fn every_supported_locale_has_style_addendum() {
let codes = [
"es-AR", "es-MX", "es-ES", "es-CO", "es-PE", "es-CL", "es-US", "es",
"en-US", "en-GB", "en-AU", "en-CA", "en",
"pt-BR", "pt-PT", "pt",
"fr-FR", "fr-CA", "fr",
"it-IT", "it",
"de-DE", "de",
"ja-JP", "ja",
"zh-CN", "zh",
];
for code in codes {
let l = loc(code);
assert!(
language_style_addendum(Some(&l)).is_some(),
"no style addendum for {code}"
);
let voice = default_voice_for_locale(Some(&l));
assert!(!voice.is_empty(), "empty voice for {code}");
}
}
}