From f5040ccd26cfe74007ca27c8a9044c15da350f68 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Fri, 25 Jul 2025 14:42:38 +0300 Subject: [PATCH 1/4] Add language selection to web-eid.eu WE2-1100 Signed-off-by: Sven Mitt --- .../src/main/resources/static/css/main.css | 81 +++++++++++++++++++ example/src/main/resources/static/js/index.js | 65 +++++++++++++++ .../src/main/resources/templates/index.html | 36 ++++++++- 3 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 example/src/main/resources/static/js/index.js diff --git a/example/src/main/resources/static/css/main.css b/example/src/main/resources/static/css/main.css index 3de3421c..658720a0 100644 --- a/example/src/main/resources/static/css/main.css +++ b/example/src/main/resources/static/css/main.css @@ -67,3 +67,84 @@ body { .eu-logo-fixed img { height: 86px; } + +.language-dropdown { + position: relative; + display: inline-block; +} + +.language-btn { + background-color: #f8f9fa; + color: #212529; + border: 1px solid #e9ecef; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + min-width: 60px; + justify-content: space-between; + transition: background-color 0.2s, border-color 0.2s; +} + +.language-btn:hover { + background-color: #e9ecef; +} + +.dropdown-arrow { + font-size: 10px; + transition: transform 0.2s; +} + +.language-btn.active .dropdown-arrow { + transform: rotate(180deg); +} + +.language-menu { + position: absolute; + top: 100%; + left: 0; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + padding: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); + z-index: 1000; + display: none; + min-width: 200px; + margin-top: 2px; +} + +.language-menu.show { + display: block; +} + +.language-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} + +.language-option { + background: none; + border: none; + color: #212529; + padding: 8px 12px; + text-align: left; + cursor: pointer; + border-radius: 2px; + font-size: 14px; + white-space: nowrap; + transition: background-color 0.2s; +} + +.language-option:hover { + background-color: #e9ecef; +} + +.language-option.selected { + font-weight: bold; +} diff --git a/example/src/main/resources/static/js/index.js b/example/src/main/resources/static/js/index.js new file mode 100644 index 00000000..feb0c58e --- /dev/null +++ b/example/src/main/resources/static/js/index.js @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +"use strict"; + +const languageUi = { + selectedLang: document.querySelector("#selected-lang"), + languageButton: document.querySelector("#language-button"), + languageMenu: document.querySelector("#language-menu"), + languageOptions: document.querySelectorAll(".language-option") +}; + +export function setupLanguageSelection(currentLang, onLangChange) { + const selectedOption = Array.from(languageUi.languageOptions) + .find(option => option.dataset.lang === currentLang); + + languageUi.selectedLang.textContent = selectedOption ? selectedOption.dataset.display : "EN"; + + languageUi.languageOptions.forEach(option => + option.classList.toggle('selected', option.dataset.lang === currentLang) + ); + + languageUi.languageButton.onclick = e => { + e.stopPropagation(); + languageUi.languageMenu.classList.toggle('show'); + e.target.classList.toggle('active'); + }; + + document.onclick = () => { + languageUi.languageMenu.classList.remove('show'); + languageUi.languageButton.classList.remove('active'); + }; + + languageUi.languageMenu.onclick = e => e.stopPropagation(); + + languageUi.languageOptions.forEach(option => { + option.onclick = () => { + onLangChange(option.dataset.lang); + languageUi.selectedLang.textContent = option.dataset.display; + languageUi.languageOptions.forEach(o => o.classList.remove('selected')); + option.classList.add('selected'); + languageUi.languageMenu.classList.remove('show'); + languageUi.languageButton.classList.remove('active'); + }; + }); +} diff --git a/example/src/main/resources/templates/index.html b/example/src/main/resources/templates/index.html index 051ab855..5105025e 100644 --- a/example/src/main/resources/templates/index.html +++ b/example/src/main/resources/templates/index.html @@ -87,15 +87,41 @@

Usage

Attach a smart card reader to the computer and insert the eID card into the reader.
  • Click Authenticate below.
  • +
  • + Select the Web eID application language from the dropdown menu. + If you have previously configured a language preference within the Web eID application dialog, that setting will take precedence over the language selection made here. + Please note that this language setting does not affect the website interface. +
  • -

    + +

    -

    +
    + +
    +
    + + + + + + + + + + +
    +
    +
    +

    The privacy policy of the test service is available here.

    @@ -250,6 +276,7 @@

    For developers

    "use strict"; import * as webeid from "/js/web-eid.js"; import {hideErrorMessage, showErrorMessage, checkHttpError} from "/js/errors.js"; + import {setupLanguageSelection} from "/js/index.js"; hideErrorMessage(); @@ -258,7 +285,8 @@

    For developers

    const csrfToken = document.querySelector('#csrftoken').content; const csrfHeaderName = document.querySelector('#csrfheadername').content; - const lang = new URLSearchParams(window.location.search).get("lang") || "en"; + let lang = new URLSearchParams(window.location.search).get("lang") || "en"; + setupLanguageSelection(lang, changedLang => lang = changedLang); authButton.addEventListener("click", async () => { hideErrorMessage(); @@ -289,7 +317,7 @@

    For developers

    console.log("Authentication successful! Result:", authTokenResult); - window.location.href = "/welcome"; + window.location.href = "/welcome?lang=" + lang; } catch (error) { showErrorMessage(error); From 786513837d14f8b62e0fe3edcfb30349ef845398 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Tue, 23 Sep 2025 13:53:16 +0300 Subject: [PATCH 2/4] feat: Add AUTO option, no lang is selected and web-eid-app uses language from host WE2-1110 Signed-off-by: Sven Mitt --- example/src/main/resources/static/js/index.js | 117 ++++++++++++++---- .../src/main/resources/templates/index.html | 21 ++-- 2 files changed, 98 insertions(+), 40 deletions(-) diff --git a/example/src/main/resources/static/js/index.js b/example/src/main/resources/static/js/index.js index feb0c58e..9d2881ae 100644 --- a/example/src/main/resources/static/js/index.js +++ b/example/src/main/resources/static/js/index.js @@ -22,44 +22,109 @@ "use strict"; -const languageUi = { +const NO_LANGUAGE_SELECTED = { lang: "auto", display: "AUTO", name: "Auto" }; + +const SUPPORTED_LANGUAGES = [ + { lang: "et", display: "ET", name: "Eesti" }, + { lang: "en", display: "EN", name: "English" }, + { lang: "ru", display: "RU", name: "Русский" }, + { lang: "fi", display: "FI", name: "Suomi" }, + { lang: "hr", display: "HR", name: "Hrvatska" }, + { lang: "de", display: "DE", name: "Deutsch" }, + { lang: "fr", display: "FR", name: "Française" }, + { lang: "nl", display: "NL", name: "Nederlands" }, + { lang: "cs", display: "CS", name: "Čeština" }, + { lang: "sk", display: "SK", name: "Slovenština" }, + NO_LANGUAGE_SELECTED +]; + +const languageComponent = { selectedLang: document.querySelector("#selected-lang"), languageButton: document.querySelector("#language-button"), languageMenu: document.querySelector("#language-menu"), - languageOptions: document.querySelectorAll(".language-option") + languageGrid: document.querySelector(".language-grid"), + languageOptions: () => document.querySelectorAll(".language-option") }; -export function setupLanguageSelection(currentLang, onLangChange) { - const selectedOption = Array.from(languageUi.languageOptions) - .find(option => option.dataset.lang === currentLang); +/** + * Reads the `lang` query parameter from the current URL and validates it. + * + * - Returns the language code (e.g. "en", "et") if present and included in SUPPORTED_LANGUAGES. + * - Returns `null` if the parameter is missing, empty, or not in the supported list. + * + * This ensures that only recognized languages are passed to the app. If `null` + * is returned, the Web eID application will fall back to the OS default locale. + */ +export function getValidatedLangFromUrl() { + const lang = new URLSearchParams(window.location.search).get("lang"); + if (!lang) { + return null; + } + const normalizedLang = lang.trim().toLowerCase(); + const language = SUPPORTED_LANGUAGES.find(lang => lang.lang === normalizedLang); + return language ? language.lang : null; +} + +/** + * Creates a language selections in UI component + * + * @param lang Language to be selected in component, example en + * @param onLangChange Action to be executed when language is changed + */ +export function setupLanguageSelection(lang, onLangChange) { + const language = SUPPORTED_LANGUAGES.find(supportedLanguage => supportedLanguage.lang === lang) ?? NO_LANGUAGE_SELECTED; + + SUPPORTED_LANGUAGES.forEach(supportedLanguage => { + if (supportedLanguage === NO_LANGUAGE_SELECTED) { + return; + } + + const button = document.createElement("button"); + button.className = "language-option"; + button.textContent = supportedLanguage.name; + + if (supportedLanguage === language) { + button.classList.add("selected"); + setSelectedLanguage(supportedLanguage); + } - languageUi.selectedLang.textContent = selectedOption ? selectedOption.dataset.display : "EN"; + button.addEventListener("click", () => { + languageComponent.languageOptions().forEach(o => o.classList.remove("selected")); + button.classList.add("selected"); - languageUi.languageOptions.forEach(option => - option.classList.toggle('selected', option.dataset.lang === currentLang) - ); + setSelectedLanguage(supportedLanguage); + hideLanguageSelectionMenu(); - languageUi.languageButton.onclick = e => { + onLangChange(supportedLanguage.lang); + }); + + languageComponent.languageGrid.appendChild(button); + }); + + setSelectedLanguage(language); + + languageComponent.languageButton.onclick = e => { e.stopPropagation(); - languageUi.languageMenu.classList.toggle('show'); - e.target.classList.toggle('active'); + showLanguageSelectionMenu(); }; document.onclick = () => { - languageUi.languageMenu.classList.remove('show'); - languageUi.languageButton.classList.remove('active'); + hideLanguageSelectionMenu(); }; - languageUi.languageMenu.onclick = e => e.stopPropagation(); - - languageUi.languageOptions.forEach(option => { - option.onclick = () => { - onLangChange(option.dataset.lang); - languageUi.selectedLang.textContent = option.dataset.display; - languageUi.languageOptions.forEach(o => o.classList.remove('selected')); - option.classList.add('selected'); - languageUi.languageMenu.classList.remove('show'); - languageUi.languageButton.classList.remove('active'); - }; - }); + languageComponent.languageMenu.onclick = e => e.stopPropagation(); +} + +function setSelectedLanguage(supportedLanguage) { + languageComponent.selectedLang.textContent = supportedLanguage.display; +} + +function showLanguageSelectionMenu() { + languageComponent.languageMenu.classList.toggle("show"); + languageComponent.languageButton.classList.toggle("active"); +} + +function hideLanguageSelectionMenu() { + languageComponent.languageMenu.classList.remove("show"); + languageComponent.languageButton.classList.remove("active"); } diff --git a/example/src/main/resources/templates/index.html b/example/src/main/resources/templates/index.html index 5105025e..13a37445 100644 --- a/example/src/main/resources/templates/index.html +++ b/example/src/main/resources/templates/index.html @@ -108,16 +108,6 @@

    Usage

    - - - - - - - - - -
    @@ -276,7 +266,7 @@

    For developers

    "use strict"; import * as webeid from "/js/web-eid.js"; import {hideErrorMessage, showErrorMessage, checkHttpError} from "/js/errors.js"; - import {setupLanguageSelection} from "/js/index.js"; + import {setupLanguageSelection, getValidatedLangFromUrl} from "/js/index.js"; hideErrorMessage(); @@ -285,7 +275,7 @@

    For developers

    const csrfToken = document.querySelector('#csrftoken').content; const csrfHeaderName = document.querySelector('#csrfheadername').content; - let lang = new URLSearchParams(window.location.search).get("lang") || "en"; + let lang = getValidatedLangFromUrl(); setupLanguageSelection(lang, changedLang => lang = changedLang); authButton.addEventListener("click", async () => { @@ -317,8 +307,11 @@

    For developers

    console.log("Authentication successful! Result:", authTokenResult); - window.location.href = "/welcome?lang=" + lang; - + const welcomePageUrl = new URL("/welcome", window.location.origin); + if (lang) { + welcomePageUrl.searchParams.set("lang", lang); + } + window.location.href = welcomePageUrl.toString(); } catch (error) { showErrorMessage(error); throw error; From b885b52c943429acade5b4da05d87d5ee476b76b Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Tue, 23 Sep 2025 13:53:40 +0300 Subject: [PATCH 3/4] fix: logout keeps lang WE2-1110 Signed-off-by: Sven Mitt --- example/src/main/resources/templates/welcome.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/example/src/main/resources/templates/welcome.html b/example/src/main/resources/templates/welcome.html index 67b31311..eae33ff7 100644 --- a/example/src/main/resources/templates/welcome.html +++ b/example/src/main/resources/templates/welcome.html @@ -52,6 +52,7 @@

    Digital signing

    "use strict"; import * as webeid from "/js/web-eid.js"; import {hideErrorMessage, showErrorMessage, checkHttpError} from "/js/errors.js"; + import {getValidatedLangFromUrl} from "/js/index.js"; const signButton = document.querySelector("#webeid-sign-button"); const downloadButton = document.querySelector("#webeid-download-button"); @@ -70,7 +71,12 @@

    Digital signing

    [csrfHeaderName]: csrfToken } }); - window.location.href = "/"; + const lang = getValidatedLangFromUrl(); + const indexPageUrl = new URL("/", window.location.origin); + if (lang) { + indexPageUrl.searchParams.set("lang", lang); + } + window.location.href = indexPageUrl.toString(); }); downloadButton.addEventListener("click", async () => { From 465dc0462784f3bee6daa67379e0b4c89fb3e910 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Mon, 29 Sep 2025 10:06:58 +0300 Subject: [PATCH 4/4] fix: languages in UI WE2-1113 Signed-off-by: Sven Mitt --- example/src/main/resources/static/js/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/src/main/resources/static/js/index.js b/example/src/main/resources/static/js/index.js index 9d2881ae..ab0103b7 100644 --- a/example/src/main/resources/static/js/index.js +++ b/example/src/main/resources/static/js/index.js @@ -29,12 +29,12 @@ const SUPPORTED_LANGUAGES = [ { lang: "en", display: "EN", name: "English" }, { lang: "ru", display: "RU", name: "Русский" }, { lang: "fi", display: "FI", name: "Suomi" }, - { lang: "hr", display: "HR", name: "Hrvatska" }, + { lang: "hr", display: "HR", name: "Hrvatski" }, { lang: "de", display: "DE", name: "Deutsch" }, - { lang: "fr", display: "FR", name: "Française" }, + { lang: "fr", display: "FR", name: "Français" }, { lang: "nl", display: "NL", name: "Nederlands" }, { lang: "cs", display: "CS", name: "Čeština" }, - { lang: "sk", display: "SK", name: "Slovenština" }, + { lang: "sk", display: "SK", name: "Slovenčina" }, NO_LANGUAGE_SELECTED ];