1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { a as api, r as reloadI18nResources } from "./index-w5lXls15.js"; 3import { b as Drawer, D as DevDebugInfo } from "./AdminShared-BP_vxG_A.js"; 4import { a as ai } from "./ai-B0Mlroaa.js"; 5import { r as extractJson } from "./utils-CEVb7nN1.js"; 6import { e1 as Languages, cf as Plus, fg as ToggleRight, fh as ToggleLeft, fV as Code, b5 as Trash2, cd as Save, bi as Sparkles } from "./vendor-D0P4XgNj.js"; 7import "./vendor-i18n-BC7ZMXWQ.js"; 8import "./vendor-ai-gQv1oOjO.js"; 9const AddLanguageModal = ({ 10 isOpen, 11 onClose, 12 sourceLocale, 13 onApply 14}) => { 15 const [mode, setMode] = reactExports.useState("manual"); 16 const [targetLang, setTargetLang] = reactExports.useState(""); 17 const [targetCode, setTargetCode] = reactExports.useState(""); 18 const [pastedJson, setPastedJson] = reactExports.useState(""); 19 const [generating, setGenerating] = reactExports.useState(false); 20 const [error, setError] = reactExports.useState(""); 21 const handleGenerate = async () => { 22 if (!targetLang || !targetCode) return; 23 setGenerating(true); 24 setError(""); 25 try { 26 const resources = await api.getTranslation("en", "translation"); 27 const prompt = `You are a professional translator for a music education platform. 28 Translate the values of the following JSON object from English to ${targetLang} (${targetCode}). 29 Maintain the exact key structure. Return ONLY the valid JSON object, no markdown formatting. 30 31 Source JSON (English): 32 ${JSON.stringify(resources, null, 2)}`; 33 const response = await ai.models.generateContent({ 34 model: "gemini-3-flash-preview", 35 // Simple translation task 36 contents: prompt 37 }); 38 const text = response.text || ""; 39 const clean = extractJson(text); 40 JSON.parse(clean); 41 setPastedJson(clean); 42 } catch (e) { 43 setError(`Translation failed: ${e.message}`); 44 } finally { 45 setGenerating(false); 46 } 47 }; 48 const handleApply = async () => { 49 try { 50 let resources; 51 if (mode === "ai") { 52 const clean = extractJson(pastedJson); 53 resources = JSON.parse(clean); 54 } else { 55 resources = await api.getTranslation("en", "translation"); 56 } 57 onApply(resources, targetLang, targetCode); 58 onClose(); 59 } catch (e) { 60 setError("Invalid JSON or error processing."); 61 } 62 }; 63 if (!isOpen) return null; 64 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4", children: [ 65 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/60 backdrop-blur-sm", onClick: onClose }), 66 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-2xl bg-white dark:bg-zinc-900 rounded-xl shadow-2xl p-6 border border-zinc-200 dark:border-zinc-800 animate-in zoom-in-95", children: [
67 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-6", children: [ 68 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white", children: "Add New Language" }), 69 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex bg-zinc-100 dark:bg-zinc-800 rounded-lg p-1", children: [ 70 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setMode("manual"), className: `px-3 py-1 rounded-md text-xs font-bold transition-all ${mode === "manual" ? "bg-white dark:bg-zinc-700 shadow" : "text-zinc-500"}`, children: "Manual" }), 71 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setMode("ai"), className: `px-3 py-1 rounded-md text-xs font-bold transition-all ${mode === "ai" ? "bg-white dark:bg-zinc-700 shadow text-indigo-500" : "text-zinc-500"}`, children: "AI Assist" }) 72 ] }) 73 ] }), 74 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
75 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [ 76 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 77 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Language Name" }), 78 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { value: targetLang, onChange: (e) => setTargetLang(e.target.value), className: "w-full bg-zinc-50 dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white", placeholder: "e.g. Spanish" }) 79 ] }), 80 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 81 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "ISO Code" }), 82 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { value: targetCode, onChange: (e) => setTargetCode(e.target.value), className: "w-full bg-zinc-50 dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white font-mono", placeholder: "e.g. es" }) 83 ] }) 84 ] }),
85 mode === "ai" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 86 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-100 dark:border-indigo-800", children: [ 87 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-2", children: [ 88 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-sm text-indigo-900 dark:text-indigo-200", children: "Auto-Translation" }), 89 /* @__PURE__ */ jsxRuntimeExports.jsxs( 90 "button", 91 { 92 onClick: handleGenerate, 93 disabled: !targetLang || !targetCode || generating, 94 className: "text-xs flex items-center gap-1 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white px-3 py-1.5 rounded font-bold transition-all", 95 children: [ 96 generating ? /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 12, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 12 }), 97 generating ? "Translating..." : "Generate with Gemini" 98 ] 99 } 100 ) 101 ] }),
102 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-indigo-700 dark:text-indigo-300", children: "Uses Gemini 3 Flash to generate translation keys based on the English source." }) 103 ] }), 104 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 105 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "JSON Output" }), 106 /* @__PURE__ */ jsxRuntimeExports.jsx( 107 "textarea", 108 { 109 value: pastedJson, 110 onChange: (e) => { 111 setPastedJson(e.target.value); 112 setError(""); 113 }, 114 className: "w-full h-32 bg-zinc-50 dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-3 text-xs font-mono outline-none focus:border-indigo-500 text-zinc-900 dark:text-white resize-none", 115 placeholder: "{ ... }", 116 readOnly: generating 117 } 118 ), 119 error && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-red-500 text-xs mt-1", children: error }) 120 ] }) 121 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-200 dark:border-zinc-800 text-sm text-zinc-600 dark:text-zinc-400", children: "This will create a new language entry copying the English resources structure. You can then edit the values manually." }) 122 ] }),
123 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 pt-6 border-t border-zinc-200 dark:border-zinc-800 mt-6", children: [ 124 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "px-4 py-2 rounded-lg text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800 font-bold text-sm", children: "Cancel" }), 125 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleApply, disabled: !targetCode || !targetLang || mode === "ai" && !pastedJson, className: "px-4 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-sm disabled:opacity-50", children: "Create Language" }) 126 ] }) 127 ] }) 128 ] }); 129}; 130const LocaleEditor = ({ locale, onClose, onSave, activeRole }) => { 131 const [json, setJson] = reactExports.useState(""); 132 const [error, setError] = reactExports.useState(""); 133 reactExports.useEffect(() => { 134 if (locale && locale.resources) setJson(JSON.stringify(locale.resources, null, 2)); 135 }, [locale]); 136 const handleSave = () => { 137 try { 138 const parsed = JSON.parse(json); 139 onSave({ ...locale, resources: parsed }); 140 onClose(); 141 } catch (e) { 142 setError("Invalid JSON syntax"); 143 } 144 }; 145 return /* @__PURE__ */ jsxRuntimeExports.jsx(Drawer, { isOpen: !!locale, onClose, title: `Edit ${locale.name} (${locale.code})`, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full", children: [ 146 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4", children: [
147 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400 mb-2", children: "Directly edit the translation JSON below. Keys must match the default language structure." }), 148 error && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-red-500 text-xs font-bold bg-red-100 dark:bg-red-900/20 p-2 rounded", children: error }) 149 ] }), 150 /* @__PURE__ */ jsxRuntimeExports.jsx( 151 "textarea", 152 { 153 value: json, 154 onChange: (e) => { 155 setJson(e.target.value); 156 setError(""); 157 }, 158 className: "flex-1 bg-zinc-50 dark:bg-black border border-zinc-300 dark:border-zinc-800 rounded-lg p-4 font-mono text-xs outline-none focus:border-indigo-500 text-zinc-900 dark:text-white resize-none", 159 spellCheck: false 160 } 161 ), 162 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-4 flex gap-2", children: [ 163 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "flex-1 px-4 py-2 rounded-lg border border-zinc-300 dark:border-zinc-700 font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800", children: "Cancel" }),
164 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: handleSave, className: "flex-1 px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white font-bold flex items-center justify-center gap-2", children: [ 165 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 16 }), 166 " Save" 167 ] }) 168 ] }), 169 /* @__PURE__ */ jsxRuntimeExports.jsx(DevDebugInfo, { table: "Locale", keyName: locale.code, value: locale.resources, isSuperAdmin: activeRole == null ? void 0 : activeRole.isSuperAdmin }) 170 ] }) }); 171}; 172const TranslationManager = ({ activeRole }) => { 173 const [locales, setLocales] = reactExports.useState([]); 174 const [editingLocale, setEditingLocale] = reactExports.useState(null); 175 const [isAddOpen, setIsAddOpen] = reactExports.useState(false); 176 const [localRefresh, setLocalRefresh] = reactExports.useState(0); 177 const refresh = () => setLocalRefresh((p) => p + 1); 178 reactExports.useEffect(() => { 179 api.getLocales().then(setLocales); 180 }, [localRefresh]); 181 const handleAddLanguage = async (resources, name, code) => { 182 await api.createLocale({ id: Date.now(), code, name, resources, isActive: true }); 183 reloadI18nResources(); 184 refresh(); 185 }; 186 const handleUpdateLocale = async (updated) => { 187 await api.updateLocale(updated.code, updated); 188 reloadI18nResources(); 189 refresh(); 190 }; 191 const handleDelete = async (code) => { 192 if (confirm(`Delete ${code}?`)) { 193 await api.deleteLocale(code); 194 refresh(); 195 } 196 }; 197 const toggleActive = async (l) => { 198 await api.updateLocale(l.code, { ...l, isActive: !l.isActive }); 199 refresh(); 200 }; 201 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col space-y-6", children: [ 202 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center bg-white dark:bg-zinc-900 p-4 rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-sm", children: [
203 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 204 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 205 /* @__PURE__ */ jsxRuntimeExports.jsx(Languages, { className: "text-indigo-500" }), 206 " Translation Manager" 207 ] }), 208 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Manage supported languages and translations." }) 209 ] }), 210 /* @__PURE__ */ jsxRuntimeExports.jsxs( 211 "button", 212 { 213 onClick: () => setIsAddOpen(true), 214 className: "flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-bold shadow-md shadow-indigo-500/20", 215 children: [ 216 /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 16 }), 217 " Add Language" 218 ] 219 } 220 ) 221 ] }), 222 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6", children: locales.map((locale) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl p-5 shadow-sm hover:border-indigo-300 dark:hover:border-indigo-700 transition-all ${!locale.isActive ? "opacity-60" : ""}`, children: [
223 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-start mb-4", children: [ 224 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 225 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-10 h-10 rounded-full bg-indigo-50 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold text-sm", children: locale.code.toUpperCase() }), 226 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 227 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-zinc-900 dark:text-white", children: locale.name }), 228 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-500", children: [ 229 Object.keys(locale.resources || {}).length, 230 " keys" 231 ] }) 232 ] }) 233 ] }), 234 locale.code !== "en" && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => toggleActive(locale), className: `text-2xl ${locale.isActive ? "text-emerald-500" : "text-zinc-300 dark:text-zinc-700"}`, children: locale.isActive ? /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleRight, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleLeft, {}) }) 235 ] }),
236 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 237 /* @__PURE__ */ jsxRuntimeExports.jsxs( 238 "button", 239 { 240 onClick: () => setEditingLocale(locale), 241 className: "flex-1 py-2 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg text-xs font-bold text-zinc-700 dark:text-zinc-300 flex items-center justify-center gap-2", 242 children: [ 243 /* @__PURE__ */ jsxRuntimeExports.jsx(Code, { size: 14 }), 244 " Edit JSON" 245 ] 246 } 247 ), 248 locale.code !== "en" && /* @__PURE__ */ jsxRuntimeExports.jsx( 249 "button", 250 { 251 onClick: () => handleDelete(locale.code), 252 className: "p-2 bg-red-50 dark:bg-red-900/10 hover:bg-red-100 dark:hover:bg-red-900/30 text-red-500 rounded-lg", 253 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 16 }) 254 } 255 ) 256 ] }) 257 ] }, locale.code)) }), 258 editingLocale && /* @__PURE__ */ jsxRuntimeExports.jsx( 259 LocaleEditor, 260 { 261 locale: editingLocale, 262 onClose: () =>
262 setEditingLocale(null), 263 onSave: handleUpdateLocale, 264 activeRole 265 } 266 ), 267 /* @__PURE__ */ jsxRuntimeExports.jsx( 268 AddLanguageModal, 269 { 270 isOpen: isAddOpen, 271 onClose: () => setIsAddOpen(false), 272 sourceLocale: locales.find((l) => l.code === "en"), 273 onApply: handleAddLanguage 274 } 275 ) 276 ] }); 277}; 278export { 279 TranslationManager as default 280};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.