From e774b4b528752684f6449cd36323138caafd1e88 Mon Sep 17 00:00:00 2001 From: yossi-computers Date: Tue, 29 Sep 2026 11:27:57 +0300 Subject: [PATCH] feat(i18n): add Hebrew language with RTL layout - Add Hebrew translation (he.json) and Israel flag icon - Mirror the layout for right-to-left languages (dir="rtl"): sidebar on the right, titlebar controls on the left, flipped toggles/selects; the script preview and package ID input stay LTR - Move hardcoded dialog strings (update, OneDrive, restore point, manual packages) into locale keys so every language can translate them - Fix sidebar logo not loading on localized pages (/logo.svg) Co-Authored-By: Claude Opus 5.5 --- app/astro.config.mjs | 2 +- app/public/icons/flags/he.svg | 1 + app/src/assets/js/appinstall.js | 12 +- app/src/assets/js/main.js | 56 ++++-- app/src/assets/styles/style.css | 110 ++++++++++++ app/src/components/MainPage.astro | 11 ++ app/src/components/Sidebar.astro | 2 +- app/src/i18n/locales/en.json | 13 +- app/src/i18n/locales/he.json | 288 ++++++++++++++++++++++++++++++ app/src/layouts/Layout.astro | 2 +- 10 files changed, 471 insertions(+), 26 deletions(-) create mode 100644 app/public/icons/flags/he.svg create mode 100644 app/src/i18n/locales/he.json diff --git a/app/astro.config.mjs b/app/astro.config.mjs index c8cd18f8..eec2d5f2 100644 --- a/app/astro.config.mjs +++ b/app/astro.config.mjs @@ -3,7 +3,7 @@ import { defineConfig } from "astro/config"; export default defineConfig({ i18n: { - locales: ["en", "ru", "tr", "it", "ko", "es", "pt", "fr", "vi", "sv", "de", "hu", "zh-CN"], + locales: ["en", "ru", "tr", "it", "ko", "es", "pt", "fr", "vi", "sv", "de", "hu", "zh-CN", "he"], defaultLocale: "en", }, }); diff --git a/app/public/icons/flags/he.svg b/app/public/icons/flags/he.svg new file mode 100644 index 00000000..5cf32415 --- /dev/null +++ b/app/public/icons/flags/he.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/assets/js/appinstall.js b/app/src/assets/js/appinstall.js index f9d00fe3..858c7444 100644 --- a/app/src/assets/js/appinstall.js +++ b/app/src/assets/js/appinstall.js @@ -1,3 +1,7 @@ +// Runtime strings, rendered into the page by MainPage.astro +const i18n = JSON.parse(document.getElementById("i18n-runtime")?.textContent || "{}"); +const tr = (key, fallback) => i18n[key] || fallback; + function appsInstallChocolatey() { // List of apps const appListChocolatey = [ @@ -274,7 +278,8 @@ function appsInstallChocolatey() { const manualList = document.getElementById("manualList"); if (window.manualURLs.length > 0) { - manualList.innerHTML = "Manual packages added: " + window.manualURLs.join(" | "); + manualList.innerHTML = + tr("install.manualAdded", "Manual packages added: ") + window.manualURLs.join(" | "); } } @@ -562,7 +567,8 @@ function appsInstallWinget() { const manualList = document.getElementById("manualList"); if (window.manualURLs.length > 0) { - manualList.innerHTML = "Manual packages added: " + window.manualURLs.join(" | "); + manualList.innerHTML = + tr("install.manualAdded", "Manual packages added: ") + window.manualURLs.join(" | "); } } @@ -593,7 +599,7 @@ addButton.addEventListener("click", () => { if (manualInput) { if (!isValidManualId(manualInput)) { const manualList = document.getElementById("manualList"); - manualList.innerHTML = "Please enter a valid package ID."; + manualList.innerHTML = tr("install.invalidId", "Please enter a valid package ID."); return; } window.manualURLs.push(manualInput); diff --git a/app/src/assets/js/main.js b/app/src/assets/js/main.js index 52085e7e..43dce082 100644 --- a/app/src/assets/js/main.js +++ b/app/src/assets/js/main.js @@ -10,6 +10,12 @@ import { relaunch } from "@tauri-apps/plugin-process"; import { openUrl } from "@tauri-apps/plugin-opener"; import { getMatches } from "@tauri-apps/plugin-cli"; +// Runtime strings, rendered into the page by MainPage.astro +const i18n = JSON.parse(document.getElementById("i18n-runtime")?.textContent || "{}"); +const tr = (key, fallback) => i18n[key] || fallback; +// Sidebar slides in from the right in RTL layouts +const rtlSuffix = document.documentElement.dir === "rtl" ? "-rtl" : ""; + async function loadConfig() { const matches = await getMatches(); if (matches.args.import) { @@ -89,12 +95,15 @@ async function loadLocale() { async function alertForUpdates() { let updateAsk; - updateAsk = await ask("An update is available. Do you want to update?", { - title: "Update Available", - kind: "info", - okLabel: "Go to GitHub", - cancelLabel: "Later", - }); + updateAsk = await ask( + tr("dialog.update.message", "An update is available. Do you want to update?"), + { + title: tr("dialog.update.title", "Update Available"), + kind: "info", + okLabel: tr("dialog.update.github", "Go to GitHub"), + cancelLabel: tr("dialog.update.later", "Later"), + }, + ); if (updateAsk === true) { await openUrl("https://github.com/flick9000/winscript/releases/latest"); @@ -108,12 +117,15 @@ async function checkForUpdates() { try { const update = await check(); - let updateAsk = await ask("An update is available. Do you want to update?", { - title: "Update Available", - kind: "info", - okLabel: "Update", - cancelLabel: "Later", - }); + let updateAsk = await ask( + tr("dialog.update.message", "An update is available. Do you want to update?"), + { + title: tr("dialog.update.title", "Update Available"), + kind: "info", + okLabel: tr("dialog.update.update", "Update"), + cancelLabel: tr("dialog.update.later", "Later"), + }, + ); if (updateAsk === true) { await update.downloadAndInstall(); @@ -232,12 +244,12 @@ document.querySelector(".nav-icon").addEventListener("click", () => { if (!navbar.classList.contains("responsive")) { navbar.classList.add("responsive"); content.classList.add("responsive"); - navbar.style.animation = "slide-in 0.3s ease forwards"; + navbar.style.animation = `slide-in${rtlSuffix} 0.3s ease forwards`; paragraphs.forEach((p) => { p.style.display = "none"; }); } else { - navbar.style.animation = "slide-out 0.3s ease forwards"; + navbar.style.animation = `slide-out${rtlSuffix} 0.3s ease forwards`; content.classList.remove("responsive"); paragraphs.forEach((p) => { p.style.display = "block"; @@ -401,9 +413,12 @@ onedriveCheckbox.addEventListener("click", async (e) => { if (!onedriveChanging) { const onedriveAsk = await ask( - "Uninstalling OneDrive will delete all of your OneDrive files, make sure to backup if you wish to proceed. Do you want to continue?", + tr( + "dialog.onedrive.message", + "Uninstalling OneDrive will delete all of your OneDrive files, make sure to backup if you wish to proceed. Do you want to continue?", + ), { - title: "Uninstall OneDrive", + title: tr("dialog.onedrive.title", "Uninstall OneDrive"), type: "question", }, ); @@ -535,9 +550,12 @@ document.querySelectorAll(".checkbox-wrapper").forEach((wrapper) => { // Run Button document.getElementById("runBtn").addEventListener("click", async function () { if (!restoreCheckbox.checked) { - let restoreAsk = await ask("Do you want to create a restore point?", { - title: "Restore Point", - }); + let restoreAsk = await ask( + tr("dialog.restore.message", "Do you want to create a restore point?"), + { + title: tr("dialog.restore.title", "Restore Point"), + }, + ); if (restoreAsk === true) { document.querySelector(".restore-container").style.display = "block"; diff --git a/app/src/assets/styles/style.css b/app/src/assets/styles/style.css index bcc7c448..364d2e0d 100644 --- a/app/src/assets/styles/style.css +++ b/app/src/assets/styles/style.css @@ -436,3 +436,113 @@ input:checked + .slider:before { display: block !important; } } + +/* Right-to-left languages (Hebrew): mirror the whole layout */ +@keyframes slide-in-rtl { + from { + transform: translateX(150%); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-out-rtl { + from { + transform: translateX(0); + } + to { + transform: translateX(150%); + } +} + +html[dir="rtl"] .titlebar { + padding-left: 0; + padding-right: 12px; +} + +html[dir="rtl"] .titlebar button.close:hover { + box-shadow: + 0px -5px 0px 0px #c42b1c, + -5px 0px 0px 0px #c42b1c; +} + +html[dir="rtl"] .sidebar { + right: 0; + margin-left: 0; + margin-right: 1rem; +} + +html[dir="rtl"] .sidebar-entry { + border-left: none; + border-right: 4px solid transparent; +} + +html[dir="rtl"] .content { + margin-left: 1rem !important; + margin-right: 342px !important; +} + +html[dir="rtl"] .content-entry .chevron { + margin-right: 0; + margin-left: 8px; +} + +html[dir="rtl"] details > div.content-entry { + padding-left: 12px; + padding-right: 48px; +} + +html[dir="rtl"] .indicator { + margin-left: 0; + margin-right: 1rem; +} + +html[dir="rtl"] .slider:before { + left: auto; + right: 4px; +} + +html[dir="rtl"] input:checked + .slider:before { + transform: translateX(-20px); +} + +html[dir="rtl"] .custom-select .selected-value { + margin-right: 0; + margin-left: 1rem; +} + +html[dir="rtl"] .custom-select .chevron { + right: auto; + left: 0; + margin-left: 0; +} + +html[dir="rtl"] #manualList { + margin-left: 0; + margin-right: 4px; +} + +/* Code and package IDs stay left-to-right */ +html[dir="rtl"] .mockup-code, +html[dir="rtl"] #manualInput { + direction: ltr; +} + +@media screen and (max-width: 800px) { + html[dir="rtl"] .content { + margin-left: 1rem !important; + margin-right: 1.5rem !important; + transition: margin-right 0.3s ease; + } + + html[dir="rtl"] .content.responsive { + margin-right: 342px !important; + } +} + +@media (min-width: 800px) { + html[dir="rtl"] .sidebar { + animation-name: slide-in-rtl; + } +} diff --git a/app/src/components/MainPage.astro b/app/src/components/MainPage.astro index 80b87011..d6bf2c24 100644 --- a/app/src/components/MainPage.astro +++ b/app/src/components/MainPage.astro @@ -18,6 +18,9 @@ interface Props { locale?: string; } const { t, locale } = Astro.props; +const runtimeStrings = Object.fromEntries( + Object.entries(t).filter(([key]) => key.startsWith("dialog.") || key.startsWith("install.")), +); const packageManagers = [ { value: "chocolatey", label: "Chocolatey" }, { value: "winget", label: "Winget" }, @@ -115,10 +118,18 @@ const languages = [ translator: "tyrael-z", translatorUrl: "https://github.com/tyrael-z", }, + { + name: "עברית", + code: "he", + flagCode: "1f1ee-1f1f1", + translator: "yossi-computers", + translatorUrl: "https://github.com/yossi-computers", + }, ]; --- +