export type Message = { key: string; text: string; context: string; ambiguous: boolean; placeholders: string[]; }; export type Proposal = { key: string; translation: string | null; issues: string[]; needsContext: boolean; }; export function placeholders(text: string): string[] { const rest = text.replace(/\{[a-zA-Z_][a-zA-Z0-9_]*\}/g, ""); if (/[{}]/.test(rest)) throw Error("Only simple {name} placeholders are supported"); return (text.match(/\{[a-zA-Z_][a-zA-Z0-9_]*\}/g) ?? []).sort(); } export function readCatalog(value: unknown): Message[] { if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("Catalog must be a keyed object"); return Object.entries(value).map(([key, v]: [string, any]) => { if ( !/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(key) || ["constructor", "prototype", "__proto__"].includes(key) ) throw Error("Invalid message key"); if (!v || typeof v !== "object" || Array.isArray(v)) throw Error("Message needs text and context"); const text = nonempty(v.text, "message text"), context = nonempty(v.context, "context"); if (v.ambiguous !== undefined && typeof v.ambiguous !== "boolean") throw Error("ambiguous must be boolean"); return { key, text, context, ambiguous: v.ambiguous ?? false, placeholders: placeholders(text), }; }); } export function proposeTranslations( messages: Message[], recorded: unknown, locale: string, ): Proposal[] { try { new Intl.Locale(locale); } catch { throw Error("Invalid locale"); } if (!recorded || typeof recorded !== "object" || Array.isArray(recorded)) throw Error("Proposals must be a keyed object"); if (Object.keys(recorded).some((k) => !messages.some((m) => m.key === k))) throw Error("Unknown translation key"); return messages.map((m) => { const t = (recorded as any)[m.key]; if (t === undefined) return { key: m.key, translation: null, issues: ["Missing recorded proposal"], needsContext: m.ambiguous, }; if (typeof t !== "string" || !t.trim() || t.length > 2000) throw Error("Proposal must contain 1..2000 characters"); const issues: string[] = []; try { if (JSON.stringify(placeholders(t)) !== JSON.stringify(m.placeholders)) issues.push("Placeholder multiset changed"); } catch { issues.push("Unsupported placeholder syntax"); } return { key: m.key, translation: t, issues, needsContext: m.ambiguous }; }); } export function previewMessage( template: string, values: Record, ): string { placeholders(template); return template.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_, name) => Object.hasOwn(values, name) ? String(values[name]) : "{" + name + "}", ); } export function reviewTranslations( messages: Message[], proposals: Proposal[], value?: unknown, ): { catalog: Record; unresolved: { key: string; reasons: string[] }[]; review: any; } { const rows = value === undefined ? {} : (value as any)?.decisions; if ( value !== undefined && ((value as any)?.schemaVersion !== 1 || !rows || typeof rows !== "object" || Array.isArray(rows) || Object.keys(rows).some((k) => !messages.some((m) => m.key === k))) ) throw Error("Invalid translation review"); const catalog: Record = {}, unresolved: { key: string; reasons: string[] }[] = [], decisions: Record = {}; for (const msg of messages) { const p = proposals.find((p) => p.key === msg.key); if (!p) throw Error("Missing proposal record"); const d = rows[msg.key] ?? { decision: "pending", translation: p.translation, acknowledgeContext: false, }; if ( !["pending", "approve", "reject"].includes(d.decision) || d.translation !== p.translation || typeof d.acknowledgeContext !== "boolean" ) throw Error("Stale or invalid translation decision"); const reasons = [...p.issues]; if (d.decision === "approve") { if ( p.translation === null || p.issues.length || (p.needsContext && !d.acknowledgeContext) ) throw Error( "Resolve translation issues or context before approving " + msg.key, ); catalog[msg.key] = p.translation; } else { reasons.push( d.decision === "reject" ? "Proposal rejected" : "Awaiting review", ); if (p.needsContext) reasons.push("Ambiguous context requires acknowledgement"); unresolved.push({ key: msg.key, reasons }); } decisions[msg.key] = structuredClone(d); } return { catalog, unresolved, review: { schemaVersion: 1, decisions } }; } export function renderWorkbench( messages: Message[], proposals: Proposal[], state: ReturnType, locale: string, values: Record = {}, ): string { return page( "UI String Localization Workbench", `

Recorded proposals for ${escapeHTML(locale)}. Every approved key must keep its placeholder multiset. Ambiguous context needs an explicit acknowledgement.

${messages .map((msg) => { const p = proposals.find((p) => p.key === msg.key)!; return `

${escapeHTML(msg.key)}

Source: ${escapeHTML(msg.text)}

Context: ${escapeHTML(msg.context)}

Proposal: ${escapeHTML(p.translation ?? "No proposal")}

Preview:

${escapeHTML(p.issues.join("; ") || "Placeholder contract preserved")}

${msg.ambiguous ? `` : ""}
`; }) .join("")}`, `const messages=${embedded(messages)},proposals=${embedded(proposals)},state=${embedded(state.review)};const placeholders=${placeholders.toString()},previewMessage=${previewMessage.toString()};function update(){try{const values=JSON.parse(document.querySelector('#variables').value);if(!values||typeof values!=='object'||Array.isArray(values))throw Error('Use a JSON object');document.querySelectorAll('[data-preview]').forEach(x=>{x.textContent=previewMessage(proposals.find(p=>p.key===x.dataset.preview).translation||'',values)});document.querySelector('#error').textContent=''}catch(e){document.querySelector('#error').textContent=e.message}}document.querySelector('#variables').oninput=update;document.querySelectorAll('[data-key]').forEach(s=>{s.value=state.decisions[s.dataset.key].decision;s.onchange=()=>{state.decisions[s.dataset.key].decision=s.value}});document.querySelectorAll('[data-context]').forEach(c=>{c.checked=state.decisions[c.dataset.context].acknowledgeContext;c.onchange=()=>{state.decisions[c.dataset.context].acknowledgeContext=c.checked}});document.querySelector('#save').onclick=()=>{for(const p of proposals){const d=state.decisions[p.key];if(d.decision==='approve'&&(p.issues.length||p.needsContext&&!d.acknowledgeContext)){document.querySelector('#error').textContent='Resolve issues and acknowledge context for '+p.key;return}}download('locale-review.json',state)};update();`, ); } export function escapeHTML(value: unknown): string { return String(value).replace( /[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ c ]!, ); } export function embedded(value: unknown): string { return JSON.stringify(value) .replace(/${escapeHTML(title)}

AI Engineering from Scratch ยท local project

${escapeHTML(title)}

${content}
`; } export function array(value: unknown, label: string): any[] { if (!Array.isArray(value)) throw new Error(label + " must be an array"); return value; } export function nonempty(value: unknown, label: string): string { if (typeof value !== "string" || !value.trim()) throw new Error(label + " must be a nonempty string"); return value; }