From cab121bf48b194b7144ce2ba30fd13db648b62d4 Mon Sep 17 00:00:00 2001 From: nguyenduc071912 Date: Wed, 27 May 2026 10:17:12 +0700 Subject: [PATCH] Add Spanish localization support --- index.html | 20 ++++++++++---- src/app.js | 13 +++++---- src/i18n.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 17 ++++++++++++ 4 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 src/i18n.js diff --git a/index.html b/index.html index b6b00fb..4f1a0cd 100644 --- a/index.html +++ b/index.html @@ -9,10 +9,19 @@
-

TaskForge

-

A tiny task list that lives in your browser.

+

TaskForge

+

+ A tiny task list that lives in your browser. +

+ Tasks are stored on this device. @@ -32,22 +41,23 @@

TaskForge

type="text" name="title" placeholder="What needs doing?" + data-i18n-placeholder="taskPlaceholder" autocomplete="off" required /> - +
    diff --git a/src/app.js b/src/app.js index 46dd724..ddbf020 100644 --- a/src/app.js +++ b/src/app.js @@ -6,6 +6,7 @@ import { } from "./auth.js"; import { load, loadLocal, save } from "./storage.js"; import { createTask, toggleTask, removeTask } from "./tasks.js"; +import { initI18n, t } from "./i18n.js"; const form = document.getElementById("task-form"); const input = document.getElementById("task-input"); @@ -15,11 +16,13 @@ const authStatus = document.getElementById("auth-status"); const authAction = document.getElementById("auth-action"); const authAvatar = document.getElementById("auth-avatar"); const authMessage = document.getElementById("auth-message"); +const localeSelect = document.getElementById("locale-select"); let tasks = []; let user = null; async function init() { + initI18n(localeSelect, renderAuth); const localTasks = loadLocal(); let completedSignIn = false; try { @@ -73,7 +76,7 @@ function render() { del.type = "button"; del.className = "task-delete"; del.textContent = "✕"; - del.setAttribute("aria-label", `Delete task: ${task.title}`); + del.setAttribute("aria-label", t("deleteTask", { title: task.title })); del.addEventListener("click", async () => { tasks = removeTask(tasks, task.id); await save(tasks, user); @@ -120,16 +123,16 @@ function renderAuth() { authAvatar.hidden = true; authAvatar.removeAttribute("src"); authAvatar.removeAttribute("alt"); - authStatus.textContent = "Tasks are stored on this device."; - authAction.textContent = "Sign in with GitHub"; + authStatus.textContent = t("localStorage"); + authAction.textContent = t("signIn"); return; } authAvatar.hidden = false; authAvatar.src = user.avatarUrl; authAvatar.alt = `${user.login}'s avatar`; - authStatus.textContent = `Signed in as ${user.login}`; - authAction.textContent = "Logout"; + authStatus.textContent = t("signedInAs", { login: user.login }); + authAction.textContent = t("logout"); setAuthMessage(""); } diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..cef1bd2 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,78 @@ +const LOCALE_KEY = "taskforge.locale"; +const DEFAULT_LOCALE = "en"; + +const STRINGS = { + en: { + add: "Add", + deleteTask: "Delete task: {title}", + emptyState: "No tasks yet. Add one above to get started.", + githubLink: "github.com/CodeBountyOrg/taskforge-demo", + language: "Language", + localStorage: "Tasks are stored on this device.", + logout: "Logout", + signIn: "Sign in with GitHub", + signedInAs: "Signed in as {login}", + tagline: "A tiny task list that lives in your browser.", + taskPlaceholder: "What needs doing?", + title: "TaskForge", + }, + es: { + add: "Agregar", + deleteTask: "Eliminar tarea: {title}", + emptyState: "Todavia no hay tareas. Agrega una arriba para empezar.", + githubLink: "github.com/CodeBountyOrg/taskforge-demo", + language: "Idioma", + localStorage: "Las tareas se guardan en este dispositivo.", + logout: "Cerrar sesion", + signIn: "Iniciar sesion con GitHub", + signedInAs: "Sesion iniciada como {login}", + tagline: "Una lista de tareas pequena que vive en tu navegador.", + taskPlaceholder: "Que hay que hacer?", + title: "TaskForge", + }, +}; + +let currentLocale = detectInitialLocale(); + +export function initI18n(selectElement, onChange) { + if (selectElement) { + selectElement.value = currentLocale; + selectElement.addEventListener("change", () => { + setLocale(selectElement.value); + onChange?.(); + }); + } + applyTranslations(); +} + +export function setLocale(locale) { + currentLocale = STRINGS[locale] ? locale : DEFAULT_LOCALE; + window.localStorage.setItem(LOCALE_KEY, currentLocale); + applyTranslations(); +} + +export function t(key, values = {}) { + const template = + STRINGS[currentLocale]?.[key] ?? STRINGS[DEFAULT_LOCALE][key] ?? key; + return template.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? ""); +} + +function applyTranslations() { + document.documentElement.lang = currentLocale; + + for (const element of document.querySelectorAll("[data-i18n]")) { + element.textContent = t(element.dataset.i18n); + } + + for (const element of document.querySelectorAll("[data-i18n-placeholder]")) { + element.setAttribute("placeholder", t(element.dataset.i18nPlaceholder)); + } +} + +function detectInitialLocale() { + const savedLocale = window.localStorage.getItem(LOCALE_KEY); + if (STRINGS[savedLocale]) return savedLocale; + + const browserLanguage = navigator.language?.toLowerCase() || ""; + return browserLanguage.startsWith("es") ? "es" : DEFAULT_LOCALE; +} diff --git a/styles.css b/styles.css index a6c7afc..e5e3c8b 100644 --- a/styles.css +++ b/styles.css @@ -51,6 +51,23 @@ body { min-width: 16rem; } +.locale-control { + display: flex; + align-items: center; + gap: 0.35rem; + color: var(--muted); + font-size: 0.9rem; +} + +.locale-control select { + padding: 0.35rem 0.5rem; + font: inherit; + color: var(--fg); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); +} + .auth-avatar { width: 2rem; height: 2rem;