diff --git a/src/js/translations.js b/src/js/translations.js index d7055c0..8cf9301 100644 --- a/src/js/translations.js +++ b/src/js/translations.js @@ -1,6 +1,6 @@ let translations = {}; -async function loadTranslations(lang) { +const loadTranslations = async (lang) => { try { const response = await fetch(`./locales/${lang}.json`); if (!response.ok) throw new Error("Translation file not found"); @@ -13,40 +13,42 @@ async function loadTranslations(lang) { } return {}; } -} +}; -function getNestedProperty(obj, path) { +const getNestedProperty = (obj, path) => { return path.split(".").reduce((current, key) => current?.[key], obj); -} +}; -function getBrowserLanguage() { +const getBrowserLanguage = () => { const browserLang = navigator.language.split("-")[0]; const supportedLangs = ["en", "es", "fr", "pt"]; return supportedLangs.includes(browserLang) ? browserLang : "en"; -} +}; -function getCurrentLanguage() { +const getCurrentLanguage = () => { return localStorage.getItem("language") || getBrowserLanguage(); -} - -async function setLanguage(lang) { - translations = await loadTranslations(lang); - - localStorage.setItem("language", lang); +}; - document.title = translations.title; +const updateDocumentMeta = (lang, translationsData) => { + document.title = translationsData.title; document .querySelector('meta[name="description"]') - .setAttribute("content", translations.metaDescription); + .setAttribute("content", translationsData.metaDescription); document .querySelector('meta[property="og:description"]') - .setAttribute("content", translations.metaDescription); - document.querySelector('meta[property="og:title"]').setAttribute("content", translations.title); + .setAttribute("content", translationsData.metaDescription); + document + .querySelector('meta[property="og:title"]') + .setAttribute("content", translationsData.title); + document.documentElement.setAttribute("lang", lang); +}; + +const updateTranslations = (translationsData) => { document.querySelectorAll("[data-translate]").forEach((element) => { const key = element.getAttribute("data-translate"); - const value = getNestedProperty(translations, key); + const value = getNestedProperty(translationsData, key); if (value) { element.textContent = value; } @@ -54,7 +56,7 @@ async function setLanguage(lang) { document.querySelectorAll("[data-translate-alt]").forEach((element) => { const key = element.getAttribute("data-translate-alt"); - const value = getNestedProperty(translations, key); + const value = getNestedProperty(translationsData, key); if (value) { element.setAttribute("alt", value); } @@ -62,16 +64,41 @@ async function setLanguage(lang) { document.querySelectorAll("[data-translate-html]").forEach((element) => { const key = element.getAttribute("data-translate-html"); - const value = getNestedProperty(translations, key); + const value = getNestedProperty(translationsData, key); if (value) { element.innerHTML = value; } }); +}; - document.documentElement.setAttribute("lang", lang); -} +const setLanguage = async (lang) => { + translations = await loadTranslations(lang); + localStorage.setItem("language", lang); + updateDocumentMeta(lang, translations); + updateTranslations(translations); +}; document.addEventListener("DOMContentLoaded", async () => { const currentLang = getCurrentLanguage(); await setLanguage(currentLang); }); + +// Export for Node.js/Jest (not executed in browser) +/* eslint-disable no-undef */ +if (typeof module !== "undefined" && module.exports) { + module.exports = { + loadTranslations, + getNestedProperty, + getBrowserLanguage, + getCurrentLanguage, + updateDocumentMeta, + updateTranslations, + setLanguage + }; + /* eslint-enable no-undef */ +} + +if (typeof window !== "undefined") { + window.setLanguage = setLanguage; + window.getCurrentLanguage = getCurrentLanguage; +} diff --git a/src/js/translations.test.js b/src/js/translations.test.js index 9cd453a..c19871c 100644 --- a/src/js/translations.test.js +++ b/src/js/translations.test.js @@ -2,6 +2,16 @@ * @jest-environment jsdom */ +const { + loadTranslations, + getNestedProperty, + getBrowserLanguage, + getCurrentLanguage, + updateDocumentMeta, + updateTranslations, + setLanguage +} = require("./translations"); + global.fetch = jest.fn(); console.error = jest.fn(); @@ -9,26 +19,19 @@ describe("Translations Module", () => { beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); + document.body.innerHTML = ""; + document.head.innerHTML = + 'Test'; }); describe("Core Helper Functions", () => { test("getNestedProperty - should handle nested translation keys", () => { - const getNestedProperty = (obj, path) => - path.split(".").reduce((current, key) => current?.[key], obj); - const translations = { user: { profile: { name: "Jane" } } }; - expect(getNestedProperty(translations, "user.profile.name")).toBe("Jane"); expect(getNestedProperty(translations, "missing.key")).toBeUndefined(); }); test("getBrowserLanguage - should return supported language or default to English", () => { - const getBrowserLanguage = () => { - const browserLang = navigator.language.split("-")[0]; - const supportedLangs = ["en", "es", "fr", "pt"]; - return supportedLangs.includes(browserLang) ? browserLang : "en"; - }; - Object.defineProperty(navigator, "language", { value: "es-ES", configurable: true }); expect(getBrowserLanguage()).toBe("es"); @@ -40,73 +43,86 @@ describe("Translations Module", () => { describe("Translation Loading", () => { test("should successfully fetch translations", async () => { const mockData = { title: "Test Title", role: "Developer" }; - global.fetch.mockResolvedValueOnce({ - ok: true, - json: async () => mockData - }); + global.fetch.mockResolvedValueOnce({ ok: true, json: async () => mockData }); - const response = await fetch("./locales/en.json"); - const data = await response.json(); + const data = await loadTranslations("en"); + expect(fetch).toHaveBeenCalledWith("./locales/en.json"); expect(data).toEqual(mockData); }); test("should handle fetch errors gracefully", async () => { global.fetch.mockRejectedValueOnce(new Error("Network error")); + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ title: "English" }) + }); + + const data = await loadTranslations("fr"); - try { - await fetch("./locales/es.json"); - } catch (error) { - expect(error.message).toBe("Network error"); - } + expect(console.error).toHaveBeenCalled(); + expect(data).toEqual({ title: "English" }); }); }); describe("localStorage Integration", () => { test("should store and retrieve selected language", () => { localStorage.setItem("language", "es"); - expect(localStorage.getItem("language")).toBe("es"); + expect(getCurrentLanguage()).toBe("es"); + }); - localStorage.setItem("language", "fr"); - expect(localStorage.getItem("language")).toBe("fr"); + test("should fall back to browser language if no stored language", () => { + Object.defineProperty(navigator, "language", { value: "fr-FR", configurable: true }); + expect(getCurrentLanguage()).toBe("fr"); }); }); describe("DOM Updates", () => { - beforeEach(() => { - document.documentElement.innerHTML = ` - - Original Title - - - -

Original Role

- Original Alt - - `; - }); - test("should update document title and meta tags", () => { - document.title = "New Title"; - const metaDesc = document.querySelector('meta[name="description"]'); - metaDesc.setAttribute("content", "New Description"); + const translationsData = { title: "New Title", metaDescription: "New Description" }; + updateDocumentMeta("es", translationsData); expect(document.title).toBe("New Title"); - expect(metaDesc.getAttribute("content")).toBe("New Description"); + expect(document.querySelector('meta[name="description"]').getAttribute("content")).toBe( + "New Description" + ); + expect(document.documentElement.getAttribute("lang")).toBe("es"); }); test("should update elements with data-translate attribute", () => { - const element = document.querySelector('[data-translate="role"]'); - element.textContent = "Software Developer"; - - expect(element.textContent).toBe("Software Developer"); + document.body.innerHTML = '

Default

'; + updateTranslations({ role: "Software Developer" }); + expect(document.querySelector('[data-translate="role"]').textContent).toBe( + "Software Developer" + ); }); test("should update alt attributes with data-translate-alt", () => { - const img = document.querySelector('[data-translate-alt="imageAlt"]'); - img.setAttribute("alt", "New Alt Text"); + document.body.innerHTML = 'Default'; + updateTranslations({ imageAlt: "Profile picture" }); + expect( + document.querySelector('[data-translate-alt="imageAlt"]').getAttribute("alt") + ).toBe("Profile picture"); + }); + }); + + describe("setLanguage Integration", () => { + test("should load translations and update DOM", async () => { + const mockTranslations = { + title: "Título", + metaDescription: "Descripción", + role: "Desarrollador" + }; + global.fetch.mockResolvedValueOnce({ ok: true, json: async () => mockTranslations }); + document.body.innerHTML = '

Default

'; - expect(img.getAttribute("alt")).toBe("New Alt Text"); + await setLanguage("es"); + + expect(localStorage.getItem("language")).toBe("es"); + expect(document.title).toBe("Título"); + expect(document.querySelector('[data-translate="role"]').textContent).toBe( + "Desarrollador" + ); }); }); });