mirror of
https://github.com/BeamMP/BeamMP-Website.git
synced 2026-02-16 02:30:47 +00:00
Fix language selection to persist between pages and allow for sharing of specfic pages in chosen lang
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "beammp-website",
|
"name": "beammp-website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
@@ -12,8 +12,10 @@ import { cn } from '@/lib/utils'
|
|||||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||||
import LanguageSelector from '@/components/LanguageSelector.vue'
|
import LanguageSelector from '@/components/LanguageSelector.vue'
|
||||||
import { Menu, X } from 'lucide-vue-next'
|
import { Menu, X } from 'lucide-vue-next'
|
||||||
|
import { getLocalizedPath } from '@/utils/locale'
|
||||||
|
|
||||||
const mobileMenuOpen = ref(false)
|
const mobileMenuOpen = ref(false)
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
function toggleMobileMenu() {
|
function toggleMobileMenu() {
|
||||||
mobileMenuOpen.value = !mobileMenuOpen.value
|
mobileMenuOpen.value = !mobileMenuOpen.value
|
||||||
@@ -22,12 +24,21 @@ function toggleMobileMenu() {
|
|||||||
function closeMobileMenu() {
|
function closeMobileMenu() {
|
||||||
mobileMenuOpen.value = false
|
mobileMenuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate localized route
|
||||||
|
function localRoute(path) {
|
||||||
|
return getLocalizedPath(path, route.params.locale)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="border-b border-neutral-200 dark:border-neutral-800 bg-white/80 dark:bg-neutral-900/70 backdrop-blur sticky top-0 z-20">
|
<header class="border-b border-neutral-200 dark:border-neutral-800 bg-white/80 dark:bg-neutral-900/70 backdrop-blur sticky top-0 z-20">
|
||||||
<nav class="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
|
<nav class="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
|
||||||
<RouterLink to="/" class="flex items-center gap-2 shrink-0" @click="closeMobileMenu">
|
<RouterLink
|
||||||
|
:to="localRoute('')"
|
||||||
|
class="flex items-center gap-2 shrink-0"
|
||||||
|
@click="closeMobileMenu"
|
||||||
|
>
|
||||||
<!-- Light mode logo (black) -->
|
<!-- Light mode logo (black) -->
|
||||||
<img
|
<img
|
||||||
src="/src/assets/BeamMP_blk.png"
|
src="/src/assets/BeamMP_blk.png"
|
||||||
@@ -74,7 +85,7 @@ function closeMobileMenu() {
|
|||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuLink as-child>
|
<NavigationMenuLink as-child>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/communities"
|
:to="localRoute('communities')"
|
||||||
:class="cn(navigationMenuTriggerStyle(), 'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white')"
|
:class="cn(navigationMenuTriggerStyle(), 'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white')"
|
||||||
>
|
>
|
||||||
{{ $t('message.nav.communities') }}
|
{{ $t('message.nav.communities') }}
|
||||||
@@ -84,7 +95,7 @@ function closeMobileMenu() {
|
|||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuLink as-child>
|
<NavigationMenuLink as-child>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/servers"
|
:to="localRoute('servers')"
|
||||||
:class="cn(navigationMenuTriggerStyle(), 'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white')"
|
:class="cn(navigationMenuTriggerStyle(), 'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white')"
|
||||||
>
|
>
|
||||||
{{ $t('message.nav.servers') }}
|
{{ $t('message.nav.servers') }}
|
||||||
@@ -94,7 +105,7 @@ function closeMobileMenu() {
|
|||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuLink as-child>
|
<NavigationMenuLink as-child>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/stats"
|
:to="localRoute('stats')"
|
||||||
:class="cn(navigationMenuTriggerStyle(), 'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white')"
|
:class="cn(navigationMenuTriggerStyle(), 'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white')"
|
||||||
>
|
>
|
||||||
{{ $t('message.nav.statistics') }}
|
{{ $t('message.nav.statistics') }}
|
||||||
@@ -138,9 +149,9 @@ function closeMobileMenu() {
|
|||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<button
|
<button
|
||||||
@click="toggleMobileMenu"
|
|
||||||
class="p-2 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
class="p-2 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
>
|
>
|
||||||
<Menu v-if="!mobileMenuOpen" class="w-6 h-6" />
|
<Menu v-if="!mobileMenuOpen" class="w-6 h-6" />
|
||||||
<X v-else class="w-6 h-6" />
|
<X v-else class="w-6 h-6" />
|
||||||
@@ -181,21 +192,21 @@ function closeMobileMenu() {
|
|||||||
{{ $t('message.nav.docs') }}
|
{{ $t('message.nav.docs') }}
|
||||||
</a>
|
</a>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/communities"
|
:to="localRoute('communities')"
|
||||||
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
||||||
@click="closeMobileMenu"
|
@click="closeMobileMenu"
|
||||||
>
|
>
|
||||||
{{ $t('message.nav.communities') }}
|
{{ $t('message.nav.communities') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/servers"
|
:to="localRoute('servers')"
|
||||||
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
||||||
@click="closeMobileMenu"
|
@click="closeMobileMenu"
|
||||||
>
|
>
|
||||||
{{ $t('message.nav.servers') }}
|
{{ $t('message.nav.servers') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/stats"
|
:to="localRoute('stats')"
|
||||||
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
||||||
@click="closeMobileMenu"
|
@click="closeMobileMenu"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { LANGUAGES, loadLocaleMessages } from '@/i18n'
|
import { LANGUAGES, loadLocaleMessages } from '@/i18n'
|
||||||
|
import { switchLocale, getCurrentLocale } from '@/utils/locale'
|
||||||
import { ChevronDown } from 'lucide-vue-next'
|
import { ChevronDown } from 'lucide-vue-next'
|
||||||
|
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
|
||||||
const currentLanguage = () => {
|
const currentLanguage = () => {
|
||||||
@@ -22,6 +26,11 @@ const selectLanguage = async (langCode) => {
|
|||||||
locale.value = langCode
|
locale.value = langCode
|
||||||
localStorage.setItem('lang', langCode)
|
localStorage.setItem('lang', langCode)
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
|
|
||||||
|
// Navigate to the new locale route
|
||||||
|
const currentPath = route.fullPath
|
||||||
|
const newPath = switchLocale(langCode, currentPath)
|
||||||
|
router.push(newPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@@ -29,7 +38,7 @@ const toggleDropdown = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const saved = localStorage.getItem('lang')
|
const saved = localStorage.getItem('lang') || getCurrentLocale()
|
||||||
if (saved && saved !== locale.value) {
|
if (saved && saved !== locale.value) {
|
||||||
try {
|
try {
|
||||||
await loadLocaleMessages(window.i18n, saved)
|
await loadLocaleMessages(window.i18n, saved)
|
||||||
@@ -45,7 +54,7 @@ onMounted(async () => {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-2 px-3 py-2 rounded-lg bg-neutral-200 dark:bg-neutral-800/50 hover:bg-neutral-300 dark:hover:bg-neutral-800 transition-colors text-neutral-900 dark:text-white text-sm font-medium"
|
class="flex items-center gap-2 px-3 py-2 rounded-lg bg-neutral-200 dark:bg-neutral-800/50 hover:bg-neutral-300 dark:hover:bg-neutral-800 transition-colors text-neutral-900 dark:text-white text-sm font-medium"
|
||||||
style="height: 40px;"
|
style="height: 40px"
|
||||||
:aria-expanded="isOpen"
|
:aria-expanded="isOpen"
|
||||||
:aria-label="$t('message.nav.language')"
|
:aria-label="$t('message.nav.language')"
|
||||||
@click="toggleDropdown"
|
@click="toggleDropdown"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import './style.css'
|
|||||||
//const i18n = createI18n({
|
//const i18n = createI18n({
|
||||||
const initialLocale = localStorage.getItem('lang') || 'en'
|
const initialLocale = localStorage.getItem('lang') || 'en'
|
||||||
const i18n = setupI18n({
|
const i18n = setupI18n({
|
||||||
|
legacy: false,
|
||||||
locale: initialLocale,
|
locale: initialLocale,
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
messages: {
|
messages: {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { loadLocaleMessages, setI18nLanguage, SUPPORT_LOCALES } from '@/i18n'
|
import { loadLocaleMessages, setI18nLanguage, SUPPORT_LOCALES } from '@/i18n'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import NotFound from '@/views/NotFound.vue'
|
import NotFound from '@/views/NotFound.vue'
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
|
||||||
const routes = [
|
// Base routes without locale prefix
|
||||||
|
const baseRoutes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
component: () => import('@/views/Home.vue'),
|
component: () => import('@/views/Home.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -14,7 +16,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: 'about',
|
||||||
name: 'About',
|
name: 'About',
|
||||||
component: () => import('@/views/About.vue'),
|
component: () => import('@/views/About.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -23,18 +25,8 @@ const routes = [
|
|||||||
requiresAuth: false,
|
requiresAuth: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*{
|
|
||||||
path: '/contact',
|
|
||||||
name: 'Contact',
|
|
||||||
component: Contact,
|
|
||||||
meta: {
|
|
||||||
title: 'Contact - BeamMP',
|
|
||||||
description: 'Get in touch with us',
|
|
||||||
requiresAuth: false
|
|
||||||
}
|
|
||||||
},*/
|
|
||||||
{
|
{
|
||||||
path: '/communities',
|
path: 'communities',
|
||||||
name: 'Communities',
|
name: 'Communities',
|
||||||
component: () => import('@/views/Communities.vue'),
|
component: () => import('@/views/Communities.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -44,7 +36,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/servers',
|
path: 'servers',
|
||||||
name: 'Servers',
|
name: 'Servers',
|
||||||
component: () => import('@/views/Servers.vue'),
|
component: () => import('@/views/Servers.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -54,7 +46,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/stats',
|
path: 'stats',
|
||||||
name: 'Statistics',
|
name: 'Statistics',
|
||||||
component: () => import('@/views/Statistics.vue'),
|
component: () => import('@/views/Statistics.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -63,6 +55,28 @@ const routes = [
|
|||||||
requiresAuth: false,
|
requiresAuth: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: `/:locale(${SUPPORT_LOCALES.join('|')})`,
|
||||||
|
component: RouterView,
|
||||||
|
beforeEnter: (to) => {
|
||||||
|
console.log('Entering locale route:', to.params.locale)
|
||||||
|
// Validate locale
|
||||||
|
if (!SUPPORT_LOCALES.includes(to.params.locale)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: baseRoutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: () => {
|
||||||
|
const locale = localStorage.getItem('lang') || 'en'
|
||||||
|
return `/${locale}`
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'NotFound',
|
name: 'NotFound',
|
||||||
@@ -80,14 +94,16 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Global navigation guard for meta data
|
// Global navigation guard for meta data and locale
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const paramsLocale = to.params.locale || 'en'
|
const paramsLocale = to.params.locale || 'en'
|
||||||
|
const i18n = window.i18n
|
||||||
|
|
||||||
// use locale if paramsLocale is not in SUPPORT_LOCALES
|
// Ensure i18n is available
|
||||||
/*if (!SUPPORT_LOCALES.includes(paramsLocale)) {
|
if (!i18n) {
|
||||||
return next(`/${locale}`)
|
next()
|
||||||
}*/
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// load locale messages
|
// load locale messages
|
||||||
if (!i18n.global.availableLocales.includes(paramsLocale)) {
|
if (!i18n.global.availableLocales.includes(paramsLocale)) {
|
||||||
@@ -97,6 +113,9 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
// set i18n language
|
// set i18n language
|
||||||
setI18nLanguage(i18n, paramsLocale)
|
setI18nLanguage(i18n, paramsLocale)
|
||||||
|
|
||||||
|
// Store current locale in localStorage
|
||||||
|
localStorage.setItem('lang', paramsLocale)
|
||||||
|
|
||||||
// Set page title
|
// Set page title
|
||||||
document.title = to.meta.title || 'BeamMP'
|
document.title = to.meta.title || 'BeamMP'
|
||||||
|
|
||||||
|
|||||||
42
src/utils/locale.js
Normal file
42
src/utils/locale.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Get the current locale from localStorage or window.i18n
|
||||||
|
* @returns {string} The current locale code
|
||||||
|
*/
|
||||||
|
export function getCurrentLocale() {
|
||||||
|
if (window.i18n) {
|
||||||
|
const locale = window.i18n.global.locale
|
||||||
|
return locale.value || locale
|
||||||
|
}
|
||||||
|
return localStorage.getItem('lang') || 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a localized route path
|
||||||
|
* @param {string} route - The route name or path (without leading slash)
|
||||||
|
* @param {string} locale - Optional locale override
|
||||||
|
* @returns {string} The full path including locale
|
||||||
|
*/
|
||||||
|
export function getLocalizedPath(route, locale = null) {
|
||||||
|
const currentLocale = locale || getCurrentLocale()
|
||||||
|
const path = route.startsWith('/') ? route.slice(1) : route
|
||||||
|
return `/${currentLocale}/${path}`.replace(/\/+/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a redirect to the current route in a different locale
|
||||||
|
* @param {string} newLocale - The new locale to switch to
|
||||||
|
* @param {string} currentPath - The current route path (can include locale and query params)
|
||||||
|
* @returns {string} The new path with the different locale
|
||||||
|
*/
|
||||||
|
export function switchLocale(newLocale, currentPath) {
|
||||||
|
// Split path from query string
|
||||||
|
const [pathOnly] = currentPath.split('?')
|
||||||
|
|
||||||
|
// Remove locale from the beginning: /en/servers -> /servers, /en/ -> /
|
||||||
|
const pathWithoutLocale = pathOnly.replace(/^\/[a-z]{2}(?:\/|$)/, '/')
|
||||||
|
|
||||||
|
// Remove trailing slashes and ensure we have the right format
|
||||||
|
const cleanPath = pathWithoutLocale.replace(/\/+/g, '/').replace(/\/$/, '') || ''
|
||||||
|
|
||||||
|
return `/${newLocale}${cleanPath}`.replace(/\/+/g, '/')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user