/* translate.jsx — Translate entire resume to another language */ const LANGUAGES = [ { code: "ar", label: "Arabic", rtl: true }, { code: "fr", label: "French" }, { code: "es", label: "Spanish" }, { code: "bn", label: "Bengali" }, { code: "de", label: "German" }, { code: "pt", label: "Portuguese" }, { code: "zh", label: "Chinese (Simplified)" }, { code: "hi", label: "Hindi" }, { code: "ms", label: "Malay" }, { code: "tr", label: "Turkish" }, { code: "ru", label: "Russian" }, { code: "ja", label: "Japanese" }, ]; function TranslateDrawer({ data, patch, onClose }) { const { useState: S, useEffect: E } = React; const [lang, setLang] = S("fr"); const [stage, setStage] = S("idle"); // idle | loading | done | error const [errMsg, setErrMsg] = S(""); const [limitHit, setLimitHit] = S(false); const [preview, setPreview] = S(null); E(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const translate = async () => { if (!window.groqStream) { setErrMsg("AI not available."); setStage("error"); return; } setStage("loading"); setPreview(null); setErrMsg(""); const langLabel = LANGUAGES.find(l => l.code === lang)?.label || lang; // Collect all translatable text fields into a flat map const fields = {}; if (data.summary) fields["summary"] = data.summary; (data.experience || []).forEach((x, xi) => { (x.bullets || []).forEach((b, bi) => { if (b.trim()) fields[`exp_${xi}_${bi}`] = b; }); }); (data.projects || []).forEach((x, xi) => { if (x.detail) fields[`proj_${xi}_detail`] = x.detail; (x.bullets || []).forEach((b, bi) => { if (b.trim()) fields[`proj_${xi}_${bi}`] = b; }); }); (data.education || []).forEach((x, xi) => { if (x.detail) fields[`edu_${xi}_detail`] = x.detail; }); (data.activities || []).forEach((a, ai) => { if (a.detail) fields[`act_${ai}_detail`] = a.detail; }); (data.certs || []).forEach((c, ci) => { if (c) fields[`cert_${ci}`] = c; }); const payload = JSON.stringify(fields); let raw = ""; await window.groqStream({ messages: [ { role: "system", content: "You are a professional resume translator. Preserve resume formatting, action verbs style, and professional tone. Translate all values but keep keys identical. Return ONLY valid JSON, no explanation." }, { role: "user", content: `Translate all values in this JSON object to ${langLabel}. Keep all JSON keys exactly the same. Return ONLY the translated JSON object:\n\n${payload}` }, ], maxTokens: 3000, onChunk: (_, full) => { raw = full; }, onDone: () => { try { let s = raw.trim().replace(/^```json\s*/i,"").replace(/^```\s*/i,"").replace(/\s*```$/i,""); const translated = JSON.parse(s.match(/\{[\s\S]*\}/)?.[0] || s); // Rebuild data with translated fields const nd = JSON.parse(JSON.stringify(data)); // deep clone if (translated.summary) nd.summary = translated.summary; (nd.experience || []).forEach((x, xi) => { (x.bullets || []).forEach((b, bi) => { const key = `exp_${xi}_${bi}`; if (translated[key]) x.bullets[bi] = translated[key]; }); }); (nd.projects || []).forEach((x, xi) => { if (translated[`proj_${xi}_detail`]) x.detail = translated[`proj_${xi}_detail`]; (x.bullets || []).forEach((b, bi) => { if (translated[`proj_${xi}_${bi}`]) x.bullets[bi] = translated[`proj_${xi}_${bi}`]; }); }); (nd.education || []).forEach((x, xi) => { if (translated[`edu_${xi}_detail`]) x.detail = translated[`edu_${xi}_detail`]; }); (nd.activities || []).forEach((a, ai) => { if (translated[`act_${ai}_detail`]) a.detail = translated[`act_${ai}_detail`]; }); (nd.certs || []).forEach((c, ci) => { if (translated[`cert_${ci}`]) nd.certs[ci] = translated[`cert_${ci}`]; }); setPreview(nd); setStage("done"); } catch(_) { setErrMsg("Could not parse translation. Try again."); setStage("error"); } }, onError: (e) => { if (e === "free_limit_reached") { setLimitHit(true); } else { setErrMsg(typeof e === "string" ? e : "AI error."); } setStage("error"); }, }); }; const applyTranslation = () => { if (!preview) return; patch(() => preview); onClose(); }; const selectedLang = LANGUAGES.find(l => l.code === lang); return ( <>
AI rewrites content in any language
Translating to {selectedLang?.label}…
{errMsg}
Applying will replace your current resume content with the {selectedLang?.label} translation. Your names, companies, and dates remain unchanged.