UNPKG

@cond/i18n-editor

Version:

A web-based i18n translation editor to manage locale files easily.

208 lines (185 loc) 8.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>i18n Editor</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <script src="https://unpkg.com/mithril/mithril.js"></script> <style> /* Sticky Table Header */ .table-container { max-height: calc(100vh - 180px); overflow-y: auto; background: #f8f9fa; border-radius: 5px; padding: 15px; box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1); position: relative; } .table thead { position: sticky; top: 0; z-index: 10; background: white; box-shadow: 0px 2px 5px rgba(220, 219, 219, 0.1); } .table th, .table td { vertical-align: middle; text-align: center; } </style> </head> <body> <div class="header text-center py-3 bg-primary text-white"> 🌍 i18n Editor - Manage Your Translations </div> <div id="app" class="container mt-4"></div> <!-- Toast Notification --> <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 5"> <div id="toastContainer" class="toast align-items-center text-bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true"> <div class="d-flex"> <div class="toast-body"> </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> </div> </div> </div> <script> const App = { translations: {}, languages: [], newKey: "", newValue: "", loading: false, loadTranslations: async function () { this.loading = true; const res = await fetch("/translations"); const data = await res.json(); this.translations = data.translations; this.languages = data.languages; this.loading = false; m.redraw(); // Restore scroll position safely requestAnimationFrame(() => { const tableContainer = document.querySelector(".table-container"); if (tableContainer) { const scrollPosition = localStorage.getItem("tableScrollPosition"); if (scrollPosition) { tableContainer.scrollTop = parseInt(scrollPosition, 10); } } }); }, addNewKey: async function () { if (!App.newKey.trim()) return; await fetch(`/translations/${App.newKey}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ defaultValue: App.newValue || "" }) }); App.showToast(`Added new key: ${App.newKey}`); App.newKey = ""; App.newValue = ""; App.loadTranslations(); }, updateTranslation: async function (lang, file, key, value) { this.translations[key][lang][file] = value; await fetch(`/translations/${lang}/${file || "global"}/${key}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ value }) }); this.showToast(`Updated ${lang}/${file || "global"}`); }, showToast: function (message) { const toastElement = document.getElementById("toastContainer"); if (toastElement) { toastElement.querySelector(".toast-body").textContent = message; const toast = new bootstrap.Toast(toastElement); toast.show(); } }, view: function () { return m(".container", [ m("h2", "Translations"), // New Key Input m(".row.mb-3", [ m(".col-md-4", [ m("input.form-control", { placeholder: "New key name...", value: App.newKey, oninput: (e) => App.newKey = e.target.value }) ]), m(".col-md-4", [ m("input.form-control", { placeholder: "Default value...", value: App.newValue, oninput: (e) => App.newValue = e.target.value }) ]), m(".col-md-4", [ m("button.btn.btn-success.w-100", { onclick: this.addNewKey }, "➕ Add Key") ]) ]), this.loading ? m("p", "Loading...") : m(".table-container", { onscroll: (e) => { localStorage.setItem("tableScrollPosition", e.target.scrollTop); } }, [ m("table.table.table-striped.table-bordered", [ // Table Header m("thead", [ m("tr", [ m("th", "Key"), ...this.languages.flatMap(lang => Object.keys(this.translations[Object.keys(this.translations)[0]][lang] || {}).map(file => m("th", file ? `${lang}/${file}` : lang) ) ) ]) ]), // Table Body m("tbody", Object.keys(this.translations).map(key => m("tr", [ m("td", key), ...this.languages.flatMap(lang => Object.keys(this.translations[key][lang] || {}).map(file => m("td", [ m("input", { class: "form-control", value: this.translations[key][lang][file] || "", oninput: (e) => this.updateTranslation(lang, file, key, e.target.value) }) ]) ) ), m("td", [ m("button.btn.btn-danger.btn-sm", { onclick: async () => { await fetch(`/translations/${key}`, { method: "DELETE" }); App.showToast(`Deleted key: ${key}`); App.loadTranslations(); } }, "x") ]) ]) ) ) ]) ]) ]); } }; App.loadTranslations(); m.mount(document.getElementById("app"), App); </script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>