Merge pull request 'implement language selector component and update internationalization logic' (#1) from dev into main
Reviewed-on: #1
This commit is contained in:
commit
eb6bdb4a92
|
|
@ -166,12 +166,9 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var langSelect = document.getElementById('');
|
function persistSelectedLang(selectedLang, isAuthenticated) {
|
||||||
if (langSelect) {
|
|
||||||
langSelect.addEventListener('change', function () {
|
|
||||||
var selectedLang = normalizeLang(langSelect.value);
|
|
||||||
localStorage.setItem(STORAGE_KEY, selectedLang);
|
localStorage.setItem(STORAGE_KEY, selectedLang);
|
||||||
if (langSelect.dataset.authenticated === '1') {
|
if (!isAuthenticated) return;
|
||||||
fetch('/preferences/lang', {
|
fetch('/preferences/lang', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
|
@ -180,6 +177,24 @@
|
||||||
// Keep UI responsive even if persistence fails.
|
// Keep UI responsive even if persistence fails.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var langSelect = document.getElementById('lang-select');
|
||||||
|
if (langSelect) {
|
||||||
|
langSelect.addEventListener('change', function () {
|
||||||
|
var selectedLang = normalizeLang(langSelect.value);
|
||||||
|
persistSelectedLang(selectedLang, langSelect.dataset.authenticated === '1');
|
||||||
|
applyTranslations(document);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var langSelectorElement = document.querySelector('lang-selector');
|
||||||
|
if (langSelectorElement) {
|
||||||
|
langSelectorElement.addEventListener('lang-changed', function (event) {
|
||||||
|
var eventLang = event && event.detail ? event.detail.langCode : '';
|
||||||
|
var selectedLang = normalizeLang(eventLang);
|
||||||
|
if (!isSupportedLang(selectedLang)) return;
|
||||||
|
var authAttr = langSelectorElement.getAttribute('authenticated');
|
||||||
|
persistSelectedLang(selectedLang, authAttr === '1' || authAttr === 'true');
|
||||||
applyTranslations(document);
|
applyTranslations(document);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,3 @@
|
||||||
{{define "language_dropdown"}}
|
{{define "language_dropdown"}}
|
||||||
<div class="relative flex items-center gap-2 text-sm">
|
<lang-selector authenticated="{{if .CurrentUser}}1{{else}}0{{end}}"></lang-selector>
|
||||||
<img id="lang-flag" class="rounded object-cover dark:outline" src="/static/vendor/flags/it.svg" alt="Italiano" style="width:32px;height:22px;">
|
|
||||||
<select id="lang-select" name="lang" data-authenticated="{{if .CurrentUser}}1{{else}}0{{end}}" class="inline-flex h-8 min-w-[95px] items-center rounded-lg bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700">
|
|
||||||
<option value="it">Italiano</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
<option value="en_us">English USA</option>
|
|
||||||
<option value="de">Deutsch</option>
|
|
||||||
<option value="de_ch">Deutsch CH</option>
|
|
||||||
<option value="fr">Français</option>
|
|
||||||
<option value="fr_ch">Français CH</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,15 @@ Output in `dist/`:
|
||||||
- `user-menu.es.js`
|
- `user-menu.es.js`
|
||||||
- `user-menu.iife.js`
|
- `user-menu.iife.js`
|
||||||
|
|
||||||
|
Componenti registrati dal bundle:
|
||||||
|
- `<user-menu>`
|
||||||
|
- `<lang-selector>`
|
||||||
|
|
||||||
## Uso nel browser
|
## Uso nel browser
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="/path/user-menu.css" />
|
<link rel="stylesheet" href="/path/user-menu.css" />
|
||||||
<script type="module" src="/path/user-menu.es.js"></script>
|
<script type="module" src="/path/user-menu.es.js"></script>
|
||||||
<trustcontact-greeting name="Fabio"></trustcontact-greeting>
|
<user-menu target="user-dropdown" pos="bl" sr="Open user menu"></user-menu>
|
||||||
|
<lang-selector authenticated="1"></lang-selector>
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
<h1>Web Components Playground</h1>
|
<h1>Web Components Playground</h1>
|
||||||
<user-menu target="user-dropdown" pos="bl" sr="Open user menu"></user-menu>
|
<user-menu target="user-dropdown" pos="bl" sr="Open user menu"></user-menu>
|
||||||
|
|
||||||
|
<div class="m-4 border">
|
||||||
|
<lang-selector authenticated="1" flagspath="public/flags/" id="test"></lang-selector>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="user-dropdown" style="outline:auto;" class="z-50 my-4 w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm dark:divide-gray-700 dark:bg-gray-800 ">
|
<div id="user-dropdown" style="outline:auto;" class="z-50 my-4 w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm dark:divide-gray-700 dark:bg-gray-800 ">
|
||||||
<div class="px-4 py-3">
|
<div class="px-4 py-3">
|
||||||
<span class="block truncate text-sm text-gray-900 dark:text-gray-100">Admin User</span>
|
<span class="block truncate text-sm text-gray-900 dark:text-gray-100">Admin User</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Swiss flag">
|
||||||
|
<rect width="32" height="32" fill="#d52b1e"/>
|
||||||
|
<rect x="13" y="6" width="6" height="20" fill="#ffffff"/>
|
||||||
|
<rect x="6" y="13" width="20" height="6" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 271 B |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="German flag">
|
||||||
|
<rect width="48" height="10.67" x="0" y="0" fill="#000000"/>
|
||||||
|
<rect width="48" height="10.67" x="0" y="10.67" fill="#dd0000"/>
|
||||||
|
<rect width="48" height="10.66" x="0" y="21.34" fill="#ffce00"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 301 B |
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="English flag">
|
||||||
|
<rect width="48" height="32" fill="#012169"/>
|
||||||
|
<path d="M0 0 48 32M48 0 0 32" stroke="#ffffff" stroke-width="6"/>
|
||||||
|
<path d="M0 0 48 32M48 0 0 32" stroke="#c8102e" stroke-width="3"/>
|
||||||
|
<rect x="20" y="0" width="8" height="32" fill="#ffffff"/>
|
||||||
|
<rect x="0" y="12" width="48" height="8" fill="#ffffff"/>
|
||||||
|
<rect x="22" y="0" width="4" height="32" fill="#c8102e"/>
|
||||||
|
<rect x="0" y="14" width="48" height="4" fill="#c8102e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 531 B |
|
|
@ -0,0 +1,53 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="United States flag">
|
||||||
|
<rect width="48" height="32" fill="#b22234"/>
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<rect y="2.46" width="48" height="2.46"/>
|
||||||
|
<rect y="7.38" width="48" height="2.46"/>
|
||||||
|
<rect y="12.30" width="48" height="2.46"/>
|
||||||
|
<rect y="17.22" width="48" height="2.46"/>
|
||||||
|
<rect y="22.14" width="48" height="2.46"/>
|
||||||
|
<rect y="27.06" width="48" height="2.46"/>
|
||||||
|
</g>
|
||||||
|
<rect width="20" height="17.23" fill="#3c3b6e"/>
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<circle cx="2.2" cy="2.2" r="0.7"/>
|
||||||
|
<circle cx="5.4" cy="2.2" r="0.7"/>
|
||||||
|
<circle cx="8.6" cy="2.2" r="0.7"/>
|
||||||
|
<circle cx="11.8" cy="2.2" r="0.7"/>
|
||||||
|
<circle cx="15.0" cy="2.2" r="0.7"/>
|
||||||
|
<circle cx="18.2" cy="2.2" r="0.7"/>
|
||||||
|
<circle cx="3.8" cy="4.4" r="0.7"/>
|
||||||
|
<circle cx="7.0" cy="4.4" r="0.7"/>
|
||||||
|
<circle cx="10.2" cy="4.4" r="0.7"/>
|
||||||
|
<circle cx="13.4" cy="4.4" r="0.7"/>
|
||||||
|
<circle cx="16.6" cy="4.4" r="0.7"/>
|
||||||
|
<circle cx="2.2" cy="6.6" r="0.7"/>
|
||||||
|
<circle cx="5.4" cy="6.6" r="0.7"/>
|
||||||
|
<circle cx="8.6" cy="6.6" r="0.7"/>
|
||||||
|
<circle cx="11.8" cy="6.6" r="0.7"/>
|
||||||
|
<circle cx="15.0" cy="6.6" r="0.7"/>
|
||||||
|
<circle cx="18.2" cy="6.6" r="0.7"/>
|
||||||
|
<circle cx="3.8" cy="8.8" r="0.7"/>
|
||||||
|
<circle cx="7.0" cy="8.8" r="0.7"/>
|
||||||
|
<circle cx="10.2" cy="8.8" r="0.7"/>
|
||||||
|
<circle cx="13.4" cy="8.8" r="0.7"/>
|
||||||
|
<circle cx="16.6" cy="8.8" r="0.7"/>
|
||||||
|
<circle cx="2.2" cy="11" r="0.7"/>
|
||||||
|
<circle cx="5.4" cy="11" r="0.7"/>
|
||||||
|
<circle cx="8.6" cy="11" r="0.7"/>
|
||||||
|
<circle cx="11.8" cy="11" r="0.7"/>
|
||||||
|
<circle cx="15.0" cy="11" r="0.7"/>
|
||||||
|
<circle cx="18.2" cy="11" r="0.7"/>
|
||||||
|
<circle cx="3.8" cy="13.2" r="0.7"/>
|
||||||
|
<circle cx="7.0" cy="13.2" r="0.7"/>
|
||||||
|
<circle cx="10.2" cy="13.2" r="0.7"/>
|
||||||
|
<circle cx="13.4" cy="13.2" r="0.7"/>
|
||||||
|
<circle cx="16.6" cy="13.2" r="0.7"/>
|
||||||
|
<circle cx="2.2" cy="15.4" r="0.7"/>
|
||||||
|
<circle cx="5.4" cy="15.4" r="0.7"/>
|
||||||
|
<circle cx="8.6" cy="15.4" r="0.7"/>
|
||||||
|
<circle cx="11.8" cy="15.4" r="0.7"/>
|
||||||
|
<circle cx="15.0" cy="15.4" r="0.7"/>
|
||||||
|
<circle cx="18.2" cy="15.4" r="0.7"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="French flag">
|
||||||
|
<rect width="16" height="32" x="0" y="0" fill="#0055a4"/>
|
||||||
|
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
|
||||||
|
<rect width="16" height="32" x="32" y="0" fill="#ef4135"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 286 B |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Italian flag">
|
||||||
|
<rect width="16" height="32" x="0" y="0" fill="#009246"/>
|
||||||
|
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
|
||||||
|
<rect width="16" height="32" x="32" y="0" fill="#ce2b37"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 287 B |
|
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<div class="min-w-[160px] relative inline-block">
|
||||||
|
<button @click="toggleDropdown" @blur="closeDropdown" class="min-w-[60px] inline-flex h-8 w-full items-center rounded-lg bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-gray-200 rounded-lg border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-200">
|
||||||
|
<img
|
||||||
|
id="lang-flag"
|
||||||
|
class="mr-4 rounded object-cover dark:border-gray-700"
|
||||||
|
:src="selectedFlag"
|
||||||
|
alt="Italiano"
|
||||||
|
:style="selectedStyle"
|
||||||
|
>
|
||||||
|
{{ langs[select].name }}
|
||||||
|
</button>
|
||||||
|
<div v-if="visible" class="min-w-[160px] fixed z-50 mt-2 list-none rounded-lg border border-gray-200 bg-white shadow-sm dark:border-2 dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800 " >
|
||||||
|
<ul class="list-none">
|
||||||
|
<li v-for="(lang, index) in langs" :key="index" :value="lang.code">
|
||||||
|
<button v-on:click="selectLang(index)" class="h-8 w-full inline-flex items-center rounded-lg bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 " >
|
||||||
|
<img
|
||||||
|
class="flag-min rounded object-cover mr-4 dark:border-gray-700"
|
||||||
|
:src="defaultFlagSrc + lang.flag"
|
||||||
|
:style="lang.style"
|
||||||
|
>
|
||||||
|
{{ lang.name }}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getCurrentInstance, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
|
||||||
|
const langs = [
|
||||||
|
{ code: "it", name: "Italiano", flag: "it.svg", style:"width:32px;height:22px;" },
|
||||||
|
{ code: "en", name: "English", flag: "en.svg", style:"width:32px;height:22px;" },
|
||||||
|
{ code: "en_us", name: "English USA", flag: "en_us.svg", style:"width:32px;height:22px;" },
|
||||||
|
{ code: "de", name: "Deutsch", flag: "de.svg", style:"width:32px;height:22px;" },
|
||||||
|
{ code: "de_ch", name: "Deutsch CH", flag: "ch.svg", style:"width:22px;height:22px;" },
|
||||||
|
{ code: "fr", name: "Français", flag: "fr.svg", style:"width:32px;height:22px;" },
|
||||||
|
{ code: "fr_ch", name: "Français CH", flag: "ch.svg", style:"width:22px;height:22px;" },
|
||||||
|
];
|
||||||
|
const select = ref(0);
|
||||||
|
const visible = ref(false);
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const storedLang = localStorage.getItem("tc_lang");
|
||||||
|
if (storedLang) {
|
||||||
|
const index = langs.findIndex((lang) => lang.code === storedLang);
|
||||||
|
if (index !== -1) {
|
||||||
|
selectLang(index, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleDropdown = (event: MouseEvent) => {
|
||||||
|
visible.value = !visible.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
if(visible.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = false;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitLangChanged = (langCode: string) => {
|
||||||
|
const host = instance?.vnode.el as HTMLElement | null;
|
||||||
|
if (!host) return;
|
||||||
|
host.dispatchEvent(
|
||||||
|
new CustomEvent("lang-changed", {
|
||||||
|
detail: { langCode },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectLang = (index: number, shouldEmit = true) => {
|
||||||
|
select.value = index;
|
||||||
|
selectedFlag.value = defaultFlagSrc + langs[index].flag;
|
||||||
|
selectedStyle.value = langs[index].style;
|
||||||
|
localStorage.setItem("tc_lang", langs[index].code);
|
||||||
|
visible.value = false;
|
||||||
|
if (shouldEmit) {
|
||||||
|
emitLangChanged(langs[index].code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
authenticated?: string | boolean;
|
||||||
|
flagspath?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const defaultFlagSrc = props.flagspath ? props.flagspath : "/static/vendor/flags/";
|
||||||
|
const selectedFlag = ref(defaultFlagSrc+langs[0].flag);
|
||||||
|
const selectedStyle = ref(langs[0].style)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
import { registerWebComponents } from './register';
|
import { registerWebComponents } from './register';
|
||||||
|
|
||||||
registerWebComponents();
|
registerWebComponents();
|
||||||
|
|
||||||
|
const test = document.getElementById('test');
|
||||||
|
test?.addEventListener('lang-changed', (e) => {
|
||||||
|
console.log('Language changed to:', (e as CustomEvent).detail);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { defineCustomElement } from 'vue';
|
import { defineCustomElement } from 'vue';
|
||||||
import UserMenuElement from './components/UserMenu.ce.vue';
|
import UserMenuElement from './components/UserMenu.ce.vue';
|
||||||
|
import LangSelectorElement from './components/LangSelector.ce.vue';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
const TAG_NAME = 'user-menu';
|
const USER_MENU_TAG = 'user-menu';
|
||||||
|
const LANG_SELECTOR_TAG = 'lang-selector';
|
||||||
|
|
||||||
export function registerWebComponents(): void {
|
export function registerWebComponents(): void {
|
||||||
if (!customElements.get(TAG_NAME)) {
|
if (!customElements.get(USER_MENU_TAG)) {
|
||||||
customElements.define(
|
customElements.define(
|
||||||
TAG_NAME,
|
USER_MENU_TAG,
|
||||||
defineCustomElement(UserMenuElement, {
|
defineCustomElement(UserMenuElement, {
|
||||||
// Tailwind is generated as global CSS; without Shadow DOM
|
// Tailwind is generated as global CSS; without Shadow DOM
|
||||||
// utility classes apply correctly inside the custom element.
|
// utility classes apply correctly inside the custom element.
|
||||||
|
|
@ -15,6 +17,15 @@ export function registerWebComponents(): void {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!customElements.get(LANG_SELECTOR_TAG)) {
|
||||||
|
customElements.define(
|
||||||
|
LANG_SELECTOR_TAG,
|
||||||
|
defineCustomElement(LangSelectorElement, {
|
||||||
|
shadowRoot: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWebComponents();
|
registerWebComponents();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
img.falg{
|
||||||
|
width:32px;
|
||||||
|
height:22px;
|
||||||
|
}
|
||||||
|
img.falg-min{
|
||||||
|
width:22px;
|
||||||
|
height:14px;
|
||||||
|
}
|
||||||
|
.flag-ch{
|
||||||
|
width:22px;
|
||||||
|
height:22px;
|
||||||
|
}
|
||||||
|
.flag-ch-min{
|
||||||
|
width:22px;
|
||||||
|
height:14px;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue