Fix language selection to persist between pages and allow for sharing of specfic pages in chosen lang

This commit is contained in:
Starystars67
2025-12-20 17:28:21 +00:00
parent f3269d0bee
commit 07a84f25d5
6 changed files with 115 additions and 33 deletions

View File

@@ -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",

View File

@@ -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"
> >

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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
View 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, '/')
}