/** * bilingual.ts — split a narrated case-file body into a single language. * * The case-writer emits each scene twice: an English `## N. ` section * followed by its Brazilian-Portuguese twin `## N. <título>` (CLAUDE.md §3). * The reader, however, must see ONE language at a time — otherwise an EN * visitor reads the EN scene immediately followed by the same scene in PT, * and vice-versa. * * We classify each `## ` section by language (PT-BR carries diacritics and * Portuguese function words; the English scene text does not) and keep only * the sections matching the active locale. This is more robust than relying * on EN-then-PT ordering: it survives a missing twin or an out-of-order * section. If a body can't be split (no `## ` sections, or none match the * locale), we return it unchanged — better to show everything than to hide * content. */ const PT_DIACRITICS = /[áàâãéêíóôõúüç]/gi; const PT_WORDS = /\b(não|que|com|para|sobre|uma|um|também|após|às|aos|nos|nas|pelo|pela|seu|sua|foi|era|está|são|sem|entre|quando|onde|índice|registro|relatório|noite|céu)\b/gi; const EN_WORDS = /\b(the|and|with|what|which|after|over|about|into|from|that|this|their|was|were|been|when|where|night|sky|report|record)\b/gi; /** * Heuristic: does this markdown block read as Brazilian Portuguese? * Diacritics are the strongest signal — English narration here has none, * while PT-BR is accent-rich. Function-word counts break ties. */ export function isPortuguese(block: string): boolean { const diacritics = (block.match(PT_DIACRITICS) || []).length; const pt = (block.match(PT_WORDS) || []).length; const en = (block.match(EN_WORDS) || []).length; // Diacritics weigh heavily: a few accents reliably mark PT-BR even when an // English place name (e.g. "Áden") sneaks one into an EN block. return diacritics * 2 + pt > en; } /** Section number token ("I", "IV", "3") from an H2 like "## IV. Cables". */ function sectionKey(section: string): string | null { const m = section.match(/^##\s+([ivxlcdm\d]+)[.)]/i); return m ? m[1].toUpperCase() : null; } /** * Return only the portion of a bilingual case body that matches `locale`. * Operates on a body whose H1 title lines have already been stripped. * * Primary signal is structural, not linguistic: the narrator emits each * numbered scene as an English `## N.` section immediately followed by its * Brazilian-Portuguese twin `## N.`. So for a section number seen twice, the * first occurrence is English and the second is Portuguese — a far more * reliable split than language sniffing (an English scene quoting accented * names would otherwise be misread as Portuguese). Language detection is the * fallback only for unpaired or unnumbered sections. */ export function pickLocaleBody(body: string, locale: "en" | "pt-br"): string { const parts = body.split(/(?=^##\s)/m); // Content before the first H2 (rare intro/orphan) is shown in both languages. const head = parts.length > 0 && !/^##\s/.test(parts[0]) ? parts[0].trimEnd() : ""; const sections = parts.filter((p) => /^##\s/.test(p)); if (sections.length === 0) return body; const counts = new Map<string, number>(); for (const s of sections) { const k = sectionKey(s); if (k) counts.set(k, (counts.get(k) ?? 0) + 1); } const wantPt = locale === "pt-br"; const seen = new Map<string, number>(); const picked: string[] = []; for (const s of sections) { const k = sectionKey(s); let isPt: boolean; if (k && (counts.get(k) ?? 0) >= 2) { const order = seen.get(k) ?? 0; seen.set(k, order + 1); isPt = order >= 1; // first = EN, second = PT-BR } else { isPt = isPortuguese(s); // unpaired / unnumbered — sniff the language } if (isPt === wantPt) picked.push(s.trimEnd()); } // Can't isolate the requested language — fall back to the full body so no // content silently disappears. if (picked.length === 0) return body; const out = picked.join("\n\n"); return head ? `${head}\n\n${out}` : out; }