From 93d0a6c22aa94c1743301b0a24a89dd4144299f9 Mon Sep 17 00:00:00 2001 From: FredGuiou Date: Wed, 25 Jun 2025 21:54:53 +0200 Subject: [PATCH] feat(interface): add lang selector in settings page --- i18n/english.js | 17 ++++++++++++----- i18n/french.js | 17 ++++++++++++----- public/components/views/settings/settings.js | 15 ++++++++++++--- public/main.js | 14 ++++++++++++++ src/commands/lang.js | 15 +++++++++++++++ views/index.html | 9 +++++++-- workspaces/cache/index.ts | 2 ++ workspaces/server/src/config.ts | 17 ++++++++++++++--- workspaces/server/test/config.test.ts | 7 ++++++- workspaces/server/test/httpServer.test.ts | 8 +++++--- 10 files changed, 99 insertions(+), 22 deletions(-) diff --git a/i18n/english.js b/i18n/english.js index 7ad9f000..01c5ad45 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -84,6 +84,11 @@ const cli = { } }; +const languages = { + fr: "french", + en: "english" +}; + const ui = { stats: { title: "Global Stats", @@ -101,9 +106,7 @@ const ui = { dependencies: "scripts & dependencies", warnings: "threats in source code", vulnerabilities: "vulnerabilities (CVE)", - licenses: "licenses conformance (SPDX)", - dark: "dark", - light: "light" + licenses: "licenses conformance (SPDX)" }, title: { maintainers: "maintainers", @@ -184,8 +187,12 @@ const ui = { general: { title: "General", save: "save", - defaultPannel: "Default Package Menu", - themePannel: "Interface theme", + dark: "dark", + light: "light", + languages, + defaultPanel: "Default Package Menu", + themePanel: "Interface theme", + langPanel: "Interface language", warnings: "SAST Warnings to ignore", flags: "Flags (emojis) to ignore", network: "Network", diff --git a/i18n/french.js b/i18n/french.js index 72497141..983b1d9b 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -84,6 +84,11 @@ const cli = { } }; +const languages = { + fr: "français", + en: "anglais" +}; + const ui = { stats: { title: "Stats Globales", @@ -101,9 +106,7 @@ const ui = { dependencies: "scripts & dépendances", warnings: "menaces dans le code", vulnerabilities: "vulnérabilités", - licenses: "conformité des licences (SPDX)", - dark: "sombre", - light: "clair" + licenses: "conformité des licences (SPDX)" }, title: { maintainers: "mainteneurs", @@ -184,8 +187,12 @@ const ui = { general: { title: "Général", save: "sauvegarder", - defaultPannel: "Panneau par défaut", - themePannel: "Thème de l'interface", + dark: "sombre", + light: "clair", + languages, + defaultPanel: "Panneau par défaut", + themePanel: "Thème de l'interface", + langPanel: "Langue de l'interface", warnings: "Avertissements à ignorer", flags: "Drapeau (emojis) à ignorer", network: "Réseau", diff --git a/public/components/views/settings/settings.js b/public/components/views/settings/settings.js index f0139a0d..29d11be2 100644 --- a/public/components/views/settings/settings.js +++ b/public/components/views/settings/settings.js @@ -39,6 +39,7 @@ export class Settings { /** @type {HTMLInputElement} */ showFriendlyDependenciesCheckbox: document.querySelector("#show-friendly"), themeSelector: document.querySelector("#theme_selector"), + langSelector: document.querySelector("#lang_selector"), disableExternalRequestsCheckbox: document.querySelector("#disable-external") }; @@ -52,6 +53,7 @@ export class Settings { ...this.dom.flagsCheckbox, this.dom.showFriendlyDependenciesCheckbox, this.dom.themeSelector, + this.dom.langSelector, this.dom.disableExternalRequestsCheckbox ]; for (const formField of formFields) { @@ -203,7 +205,8 @@ export class Settings { ignore: { flags: new Set(), warnings: new Set() }, showFriendlyDependencies: this.dom.showFriendlyDependenciesCheckbox.checked, theme: this.dom.themeSelector.value, - disableExternalRequests: this.dom.disableExternalRequestsCheckbox.checked + disableExternalRequests: this.dom.disableExternalRequestsCheckbox.checked, + lang: this.dom.langSelector.value }; for (const checkbox of this.dom.warningsCheckbox) { @@ -228,15 +231,21 @@ export class Settings { "content-type": "application/json" } }); - this.config = newConfig; + this.config = { ...newConfig, lang: this.config.lang }; this.saveButton.classList.add("disabled"); - window.dispatchEvent(new CustomEvent("settings-saved", { detail: this.config })); + window.dispatchEvent(new CustomEvent("settings-saved", { + detail: { + ...this.config, + lang: newConfig.lang + } + })); } updateSettings() { this.dom.defaultPackageMenu.value = this.config.defaultPackageMenu; this.dom.themeSelector.value = this.config.theme; + this.dom.langSelector.value = this.config.lang; const warnings = new Set(this.config.ignore.warnings); const flags = new Set(this.config.ignore.flags); diff --git a/public/main.js b/public/main.js index 8e0ebfc6..a4d3c8c7 100644 --- a/public/main.js +++ b/public/main.js @@ -32,6 +32,14 @@ document.addEventListener("DOMContentLoaded", async() => { window.navigation = new ViewNavigation(); window.wiki = new Wiki(); + const languages = window.i18n.package_info.navigation.languages; + const langSelector = document.getElementById("lang_selector"); + if (langSelector && languages) { + langSelector.innerHTML = Object.entries(languages) + .map(([key, label]) => ``) + .join(""); + } + await init(); onSettingsSaved(window.settings.config); @@ -209,15 +217,21 @@ async function updateShowInfoMenu(params) { function onSettingsSaved(defaultConfig = null) { async function updateSettings(config) { console.log("[INFO] Settings saved:", config); + if (window.settings.config.lang !== config.lang) { + window.location.reload(); + } + const warningsToIgnore = new Set(config.ignore.warnings); const flagsToIgnore = new Set(config.ignore.flags); const theme = config.theme; + const lang = config.lang; secureDataSet.warningsToIgnore = warningsToIgnore; secureDataSet.flagsToIgnore = flagsToIgnore; secureDataSet.theme = theme; window.settings.config.ignore.warnings = warningsToIgnore; window.settings.config.ignore.flags = flagsToIgnore; window.settings.config.theme = theme; + window.settings.config.lang = lang; window.settings.config.disableExternalRequests = config.disableExternalRequests; if (theme === "dark") { diff --git a/src/commands/lang.js b/src/commands/lang.js index 25f2af76..df6573fa 100644 --- a/src/commands/lang.js +++ b/src/commands/lang.js @@ -1,5 +1,6 @@ // Import Third-party Dependencies import * as i18n from "@nodesecure/i18n"; +import { appCache } from "@nodesecure/cache"; import { select } from "@topcli/prompts"; import kleur from "kleur"; @@ -15,6 +16,20 @@ export async function set() { await i18n.setLocalLang(selectedLang); await i18n.getLocalLang(); + try { + const config = await appCache.getConfig(); + + if (config) { + await appCache.updateConfig({ + ...config, + lang: selectedLang + }); + } + } + catch { + // Config does not exist, do nothing + } + console.log( kleur.white().bold(`\n ${i18n.getTokenSync("cli.commands.lang.new_selection", kleur.yellow().bold(selectedLang))}`) ); diff --git a/views/index.html b/views/index.html index caead651..090e153b 100644 --- a/views/index.html +++ b/views/index.html @@ -118,8 +118,13 @@

[[=z.token('settings.general.title')]]

+ +

[[=z.token('settings.general.network')]]:

diff --git a/workspaces/cache/index.ts b/workspaces/cache/index.ts index c8e6dc83..bdfa5ba5 100644 --- a/workspaces/cache/index.ts +++ b/workspaces/cache/index.ts @@ -5,6 +5,7 @@ import fs from "node:fs"; // Import Third-party Dependencies import cacache from "cacache"; +import * as i18n from "@nodesecure/i18n"; // Import Internal Dependencies import { logger } from "@nodesecure/server"; @@ -30,6 +31,7 @@ export interface AppConfig { }; theme?: "light" | "dark"; disableExternalRequests: boolean; + lang?: i18n.languages; } export interface PayloadsList { diff --git a/workspaces/server/src/config.ts b/workspaces/server/src/config.ts index b4051533..9e471f00 100644 --- a/workspaces/server/src/config.ts +++ b/workspaces/server/src/config.ts @@ -1,6 +1,7 @@ // Import Third-party Dependencies import { warnings, type WarningName } from "@nodesecure/js-x-ray"; import { appCache, type AppConfig } from "@nodesecure/cache"; +import * as i18n from "@nodesecure/i18n"; // Import Internal Dependencies import { logger } from "./logger.js"; @@ -16,6 +17,7 @@ const kDefaultConfig = { }; export async function get(): Promise { + const localLang = await i18n.getLocalLang(); try { const config = await appCache.getConfig(); @@ -26,7 +28,8 @@ export async function get(): Promise { warnings = [] } = {}, theme, - disableExternalRequests = false + disableExternalRequests = false, + lang = localLang } = config; logger.info( // eslint-disable-next-line @stylistic/max-len @@ -40,7 +43,8 @@ export async function get(): Promise { warnings }, theme, - disableExternalRequests + disableExternalRequests, + lang }; } catch (err: any) { @@ -50,7 +54,7 @@ export async function get(): Promise { logger.info(`[config|get](fallback to default: ${JSON.stringify(kDefaultConfig)})`); - return kDefaultConfig; + return { ...kDefaultConfig, lang: localLang }; } } @@ -66,4 +70,11 @@ export async function set(newValue: AppConfig) { throw err; } + + const i18nLocalLang = await i18n.getLocalLang(); + if (i18nLocalLang !== newValue.lang) { + logger.info(`[config|set](updating i18n lang to: ${newValue.lang})`); + await i18n.setLocalLang(newValue.lang!); + await i18n.getLanguages(); + } } diff --git a/workspaces/server/test/config.test.ts b/workspaces/server/test/config.test.ts index a12a90b0..eaed4f3b 100644 --- a/workspaces/server/test/config.test.ts +++ b/workspaces/server/test/config.test.ts @@ -6,6 +6,7 @@ import assert from "node:assert"; import cacache from "cacache"; import { warnings } from "@nodesecure/js-x-ray"; import { AppConfig, CACHE_PATH } from "@nodesecure/cache"; +import * as i18n from "@nodesecure/i18n"; // Import Internal Dependencies import { get, set } from "../src/config.js"; @@ -17,6 +18,7 @@ describe("config", () => { let actualConfig: AppConfig; before(async() => { + await i18n.getLanguages(); actualConfig = await get(); }); @@ -33,7 +35,8 @@ describe("config", () => { ignore: { flags: [], warnings: Object.entries(warnings) .filter(([_, { experimental }]) => experimental) .map(([warning]) => warning) }, - disableExternalRequests: false + disableExternalRequests: false, + lang: await i18n.getLocalLang() }); }); @@ -44,6 +47,7 @@ describe("config", () => { flags: ["foo"], warnings: ["bar"] }, + lang: "english", theme: "galaxy", disableExternalRequests: true }; @@ -60,6 +64,7 @@ describe("config", () => { flags: ["foz"], warnings: ["baz"] }, + lang: "english", theme: "galactic", disableExternalRequests: true }; diff --git a/workspaces/server/test/httpServer.test.ts b/workspaces/server/test/httpServer.test.ts index cd0ad24a..5334b186 100644 --- a/workspaces/server/test/httpServer.test.ts +++ b/workspaces/server/test/httpServer.test.ts @@ -79,7 +79,7 @@ describe("httpServer", { concurrency: 1 }, () => { const result = await get(kHttpURL); assert.equal(result.statusCode, 200); - assert.equal(result.headers["content-type"], "text/html"); + assert.ok(result.headers["content-type"]!.startsWith("text/html")); }); test("'/' should fail", async(ctx) => { @@ -233,6 +233,7 @@ describe("httpServer", { concurrency: 1 }, () => { flags: ["foo"], warnings: ["bar"] }, + lang: "english", theme: "galaxy", disableExternalRequests: true }; @@ -250,19 +251,20 @@ describe("httpServer", { concurrency: 1 }, () => { }); test("PUT '/config' should update the config", async() => { + const lang = await i18n.getLocalLang(); const { data: actualConfig } = await get(new URL("/config", kHttpURL)); // FIXME: use @mynusift/httpie instead of fetch. Atm it throws with put(). // https://github.com/nodejs/undici/issues/583 const { status } = await fetch(new URL("/config", kHttpURL), { method: "PUT", - body: JSON.stringify({ fooz: "baz" }), + body: JSON.stringify({ fooz: "baz", lang }), headers: { "Content-Type": "application/json" } }); assert.equal(status, 204); const inCache = await cacache.get(CACHE_PATH, kConfigKey); - assert.deepEqual(JSON.parse(inCache.data.toString()), { fooz: "baz" }); + assert.deepEqual(JSON.parse(inCache.data.toString()), { fooz: "baz", lang }); await fetch(new URL("/config", kHttpURL), { method: "PUT",