generated from Seekra/repository-template
Compare commits
8 Commits
9e4c9febdb
...
46d92675eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
46d92675eb
|
|||
|
dc532d3848
|
|||
|
cb42c9d368
|
|||
|
6fa3ba6298
|
|||
|
7c8cf6406d
|
|||
|
010d29c74e
|
|||
|
5a04e2a2f1
|
|||
|
a7ff5e2bf4
|
Generated
+1
@@ -2167,6 +2167,7 @@
|
|||||||
"version": "11.4.4",
|
"version": "11.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.4.tgz",
|
||||||
"integrity": "sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==",
|
"integrity": "sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.4.4",
|
"@intlify/core-base": "11.4.4",
|
||||||
"@intlify/devtools-types": "11.4.4",
|
"@intlify/devtools-types": "11.4.4",
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<!--
|
||||||
|
Copyright 2026 Seekra
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { loadLanguage } from '@/i18n';
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ code: 'en', label: 'English', flag: '🇬🇧' },
|
||||||
|
{ code: 'de', label: 'Deutsch', flag: '🇩🇪' },
|
||||||
|
{ code: 'fr', label: 'Français', flag: '🇫🇷' },
|
||||||
|
{ code: 'es', label: 'Español', flag: '🇪🇸' },
|
||||||
|
{ code: 'it', label: 'Italiano', flag: '🇮🇹' },
|
||||||
|
{ code: 'pt', label: 'Português', flag: '🇵🇹' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentLanguage = computed(
|
||||||
|
() => languages.find(l => l.code === locale.value) ?? languages[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
async function selectLanguage(code) {
|
||||||
|
await loadLanguage(code);
|
||||||
|
localStorage.setItem('locale', code);
|
||||||
|
document.documentElement.lang = code;
|
||||||
|
document.documentElement.dir = code === 'ar' ? 'rtl' : 'ltr';
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOnBlur() {
|
||||||
|
// Kleines Delay damit Click auf Option noch registriert wird
|
||||||
|
setTimeout(() => { isOpen.value = false; }, 150);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="language-switch" @blur="closeOnBlur" tabindex="-1">
|
||||||
|
<button
|
||||||
|
class="language-button button"
|
||||||
|
@click="toggle"
|
||||||
|
:aria-expanded="isOpen"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
>
|
||||||
|
<span class="flag">{{ currentLanguage.flag }}</span>
|
||||||
|
<span class="lang-code">{{ currentLanguage.code.toUpperCase() }}</span>
|
||||||
|
<span class="chevron" :class="{ open: isOpen }">▾</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul v-if="isOpen" class="language-dropdown" role="listbox">
|
||||||
|
<li
|
||||||
|
v-for="lang in languages"
|
||||||
|
:key="lang.code"
|
||||||
|
role="option"
|
||||||
|
:aria-selected="lang.code === locale"
|
||||||
|
:class="{ active: lang.code === locale }"
|
||||||
|
@click="selectLanguage(lang.code)"
|
||||||
|
>
|
||||||
|
<span class="flag">{{ lang.flag }}</span>
|
||||||
|
<span class="lang-label">{{ lang.label }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.language-switch {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--dark);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-button:hover {
|
||||||
|
background-color: var(--light-d-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
border: 1px solid var(--light-d-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 0;
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown li:hover {
|
||||||
|
background-color: var(--light-d-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown li.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,15 +16,19 @@ limitations under the License.
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
|
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
|
||||||
|
import LanguageSwitchButton from '@/features/language/components/LanguageSwitchButton.vue';
|
||||||
import logo from '@/assets/logo.svg';
|
import logo from '@/assets/logo.svg';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="global-nav">
|
<nav class="global-nav">
|
||||||
<RouterLink to="/" class="link button link">
|
<RouterLink to="/" class="link button link">
|
||||||
<img :src="logo" alt="Seekra" class="nav-logo" />
|
<img :src="logo" alt="Seekra" class="nav-logo" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<ul class="right-links">
|
<ul class="right-links">
|
||||||
|
<li>
|
||||||
|
<LanguageSwitchButton />
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<ColorSchemeButton />
|
<ColorSchemeButton />
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import getCurrentLanguage from './utils/currentLanguage';
|
|||||||
export const fallbackLocale = 'en';
|
export const fallbackLocale = 'en';
|
||||||
|
|
||||||
const SUPPORTED_LANGUAGES = [
|
const SUPPORTED_LANGUAGES = [
|
||||||
'en'
|
'en', 'de', 'fr', 'es', 'it', 'pt', 'zh', 'ja', 'ar', 'hi'
|
||||||
];
|
];
|
||||||
|
|
||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"search": {
|
||||||
|
"searchBar": {
|
||||||
|
"submit": "Suchen",
|
||||||
|
"placeholder": "Suchen..."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"searchNotAvailable": "Die Suche ist momentan nicht verfügbar."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"tryAgainToAnotherTime": "Bitte versuche es zu einem anderen Zeitpunkt erneut.",
|
||||||
|
"pageNotFound": "Die gesuchte Seite existiert nicht. Bitte überprüfe die URL oder kehre zur Suchseite zurück."
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"back": {
|
||||||
|
"search": "Zurück zur Suche"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"colorScheme": {
|
||||||
|
"switch": {
|
||||||
|
"light": "Zum hellen Modus wechseln",
|
||||||
|
"dark": "Zum dunklen Modus wechseln",
|
||||||
|
"auto": "Zum System-Farbschema wechseln"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slogan": "Gebaut zum Suchen."
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"search": {
|
||||||
|
"searchBar": {
|
||||||
|
"submit": "Buscar",
|
||||||
|
"placeholder": "Buscar..."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"searchNotAvailable": "La búsqueda no está disponible en este momento."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"tryAgainToAnotherTime": "Por favor, inténtalo de nuevo más tarde.",
|
||||||
|
"pageNotFound": "La página que buscas no existe. Comprueba la URL o vuelve a la página de búsqueda."
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"back": {
|
||||||
|
"search": "Volver a la búsqueda"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"colorScheme": {
|
||||||
|
"switch": {
|
||||||
|
"light": "Cambiar al modo claro",
|
||||||
|
"dark": "Cambiar al modo oscuro",
|
||||||
|
"auto": "Usar el esquema de color del sistema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slogan": "Hecho para buscar."
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"search": {
|
||||||
|
"searchBar": {
|
||||||
|
"submit": "Rechercher",
|
||||||
|
"placeholder": "Rechercher..."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"searchNotAvailable": "La recherche n'est pas disponible pour le moment."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"tryAgainToAnotherTime": "Veuillez réessayer ultérieurement.",
|
||||||
|
"pageNotFound": "La page que vous recherchez n'existe pas. Vérifiez l'URL ou retournez à la page de recherche."
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"back": {
|
||||||
|
"search": "Retour à la recherche"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"colorScheme": {
|
||||||
|
"switch": {
|
||||||
|
"light": "Passer en mode clair",
|
||||||
|
"dark": "Passer en mode sombre",
|
||||||
|
"auto": "Utiliser le thème système"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slogan": "Conçu pour chercher."
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"search": {
|
||||||
|
"searchBar": {
|
||||||
|
"submit": "Cerca",
|
||||||
|
"placeholder": "Cerca..."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"searchNotAvailable": "La ricerca non è disponibile al momento."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"tryAgainToAnotherTime": "Per favore riprova più tardi.",
|
||||||
|
"pageNotFound": "La pagina che cerchi non esiste. Controlla l'URL o torna alla pagina di ricerca."
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"back": {
|
||||||
|
"search": "Torna alla ricerca"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"colorScheme": {
|
||||||
|
"switch": {
|
||||||
|
"light": "Passa alla modalità chiara",
|
||||||
|
"dark": "Passa alla modalità scura",
|
||||||
|
"auto": "Usa la combinazione colori di sistema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slogan": "Costruito per cercare."
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"search": {
|
||||||
|
"searchBar": {
|
||||||
|
"submit": "Pesquisar",
|
||||||
|
"placeholder": "Pesquisar..."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"searchNotAvailable": "A pesquisa não está disponível no momento."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"tryAgainToAnotherTime": "Por favor, tente novamente mais tarde.",
|
||||||
|
"pageNotFound": "A página que você procura não existe. Verifique o URL ou volte à página de pesquisa."
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"back": {
|
||||||
|
"search": "Voltar à pesquisa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"colorScheme": {
|
||||||
|
"switch": {
|
||||||
|
"light": "Mudar para modo claro",
|
||||||
|
"dark": "Mudar para modo escuro",
|
||||||
|
"auto": "Usar esquema de cores do sistema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slogan": "Feito para pesquisar."
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default function getCurrentLanguage () {
|
export default function getCurrentLanguage () {
|
||||||
|
const saved = localStorage.getItem('locale');
|
||||||
|
if (saved) return saved;
|
||||||
const locale = new Intl.Locale(navigator.language);
|
const locale = new Intl.Locale(navigator.language);
|
||||||
return locale.language;
|
return locale.language;
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user