Improve locale and variant handling

This commit is contained in:
Dominik Milacher 2025-10-14 12:46:51 +02:00
parent 93674b23b7
commit cfeda865ab
8 changed files with 142 additions and 89 deletions

View File

@ -9,7 +9,7 @@ const matrix: ContentMatrix = {
{ code: 'de', name: { de: 'Deutsch', en: 'German' }, icon: 'german' },
{ code: 'en', name: { de: 'Englisch', en: 'English' }, icon: 'english' },
],
cookieMaxAge: 60 * 60 * 24 * 365
cookieMaxAge: 60 * 60 * 24 * 365 * 3
},
variant: {
default: 'summer',
@ -17,7 +17,7 @@ const matrix: ContentMatrix = {
{ code: 'summer', name: { de: 'Sommer', en: 'Summer' }, icon: 'i-lucide-sun' },
{ code: 'winter', name: { de: 'Winter', en: 'Winter' }, icon: 'i-lucide-snowflake' },
],
cookieMaxAge: 60 * 60 * 24 * 365
cookieMaxAge: 60 * 60 * 24
}
}

View File

@ -1,39 +0,0 @@
<!-- /error.vue (Nuxt automatically serves this for 404 routes) -->
<template>
<section
class="flex min-h-screen flex-col items-center justify-center
bg-neutral-100 px-4 text-center"
>
<!-- Icon -->
<UIcon
name="i-heroicons-face-frown"
class="h-16 w-16 text-primary-600 mb-6"
/>
<!-- Headline -->
<h1 class="text-4xl font-bold text-neutral-900 mb-2">
Seite nicht gefunden
</h1>
<!-- Sub-copy -->
<p class="max-w-md text-neutral-600 mb-8">
Die gewünschte Seite existiert leider nicht (mehr) oder der Link war
fehlerhaft. Versuchen Sie es über die Startseite noch einmal.
</p>
<!-- CTA -->
<UButton
to="/"
size="lg"
color="primary"
variant="solid"
trailing-icon="i-heroicons-arrow-uturn-left"
>
Zur Startseite
</UButton>
</section>
</template>
<script setup lang="ts">
definePageMeta({layout: false, statusCode: 404})
</script>

View File

@ -1,10 +1,12 @@
<script setup lang="ts">
const { locale, localeCodes, matrixPath } = useContent()
const { preferLocale } = useContentPreference()
const candidates = computed(() => localeCodes.filter(l => l !== locale.value))
</script>
<template>
<NuxtLink v-for="candidate in candidates" :key="candidate" :to="{ path: matrixPath({locale: candidate}), query: { freeze: 'true' } }">
<NuxtLink v-for="candidate in candidates" :key="candidate" :to="{ path: matrixPath({locale: candidate}), query: { freeze: 'true' } }" @click="preferLocale(candidate)">
{{ candidate.toUpperCase() }}
</NuxtLink>
</template>

View File

@ -1,5 +1,7 @@
<script setup lang="ts">
const { variant, variants, matrixPath } = useContent()
const { preferVariant } = useContentPreference()
const candidates = computed(() => variants.filter(v => v.code !== variant.value))
</script>
@ -8,7 +10,8 @@ const candidates = computed(() => variants.filter(v => v.code !== variant.value)
v-for="candidate in candidates"
:key="candidate.code"
:to="{ path: matrixPath({variant: candidate.code}), query: { freeze: 'true' }}"
class="flex items-center justify-center">
class="flex items-center justify-center"
@click="preferVariant(candidate.code)">
<Icon :name="candidate.icon"/>
</NuxtLink>
</template>

View File

@ -70,6 +70,10 @@ function buildSpecialization(locale: string, variant: string): object {
return copy
}
if (anonymous.length < 1) {
console.warn("Unable to parse object", object)
}
anonymous.sort((a, b) => b[0] - a[0])
if (anonymous[0][0] > 2) {
@ -105,13 +109,6 @@ function buildSpecialization(locale: string, variant: string): object {
return filtered
}
function measureTime(fn: () => void): number {
const start = performance.now();
fn();
const end = performance.now();
return end - start; // in milliseconds
}
function getSpecialization(locale: string, variant: string): object {
const key = locale + '/' + variant
@ -125,57 +122,39 @@ function getSpecialization(locale: string, variant: string): object {
export function useContent() {
const config = useAppConfig()
const route = useRoute()
const {getLocaleVariant, buildPrefix} = useContentPrefix()
const locales = config.content.matrix.locale.list.map(l => l.code)
const variants = config.content.matrix.variant.list.map(v => v.code)
const localeCookie = useCookie<string>('locale', {
sameSite: 'lax',
maxAge: config.content.matrix.locale.cookieMaxAge
})
const variantCookie = useCookie<string>('variant', {
sameSite: 'lax',
maxAge: config.content.matrix.locale.cookieMaxAge
})
const currentLocale = computed<string>(() => route.params.locale || localeCookie.value || config.content.matrix.locale.default)
const currentVariant = computed<string>(() => route.params.variant || variantCookie.value || config.content.matrix.locale.default)
function preferLocale(locale: string) {
if (locale in locales) {
localeCookie.value = locale
}
}
function preferVariant(variant: string) {
if (variant in variants) {
variantCookie.value = variant
}
}
const localeVariant = computed(() => getLocaleVariant(route.path)!)
const currentLocale = computed(() => localeVariant.value[0])
const currentVariant = computed(() => localeVariant.value[1])
function matrixPath(options: { page?: string; anchor?: string; locale?: string; variant?: string }): string {
let prefix: string[] = []
let page = options.page
const showLocale = locales.length > 1
const showVariant = variants.length > 1
if (showLocale) {
prefix.push(options.locale ?? currentLocale.value)
if (page && !page.startsWith('/')) {
page = '/' + page
}
if (showVariant) {
prefix.push(options.variant ?? currentVariant.value)
if (page === undefined) {
page = route.fullPath
const [locale, variant] = getLocaleVariant(page)!
const length = buildPrefix(locale, variant).length
page = page.slice(length)
}
// TODO preserve anchor when taking route.path
let page = (options.page ?? route.path).split('/').filter(Boolean)
const prefix = buildPrefix(options.locale ?? currentLocale.value, options.variant ?? currentVariant.value)
if (!options.page) {
page = page.slice([showLocale, showVariant].filter(Boolean).length)
let base = prefix + page
if (base.endsWith('/')) {
base = base.slice(base.length - 1)
}
const base = '/' + [...prefix, ...page].join('/')
return options.anchor ? `${base}#${options.anchor}` : base
}
@ -189,11 +168,9 @@ export function useContent() {
locale: currentLocale,
locales: config.content.matrix.locale.list,
localeCodes: locales,
preferLocale: preferLocale,
variant: currentVariant,
variants: config.content.matrix.variant.list,
variantCodes: variants,
preferVariant: preferVariant,
matrixPath: matrixPath,
p: p,
c: specialization,

View File

@ -0,0 +1,45 @@
import type {CookieRef} from "nuxt/app"
export function useContentPreference() {
const config = useAppConfig()
const locales = config.content.matrix.locale.list.map(l => l.code)
const variants = config.content.matrix.variant.list.map(v => v.code)
const localeCookie = useCookie<string>('locale', {
sameSite: 'lax',
maxAge: config.content.matrix.locale.cookieMaxAge
})
const variantCookie = useCookie<string>('variant', {
sameSite: 'lax',
maxAge: config.content.matrix.variant.cookieMaxAge
})
function prefer(list: string[], cookie: CookieRef<string>) {
return (item: string) => {
if (list.includes(item)) {
cookie.value = item
}
}
}
function preferred(list: string[], cookie: CookieRef<string | undefined>, fallback: string) {
return computed(() => {
if (cookie.value!==undefined && list.includes(cookie.value)) {
return cookie.value
} else if (cookie.value !== undefined) {
cookie.value = undefined
}
return fallback
})
}
return {
preferredLocale: preferred(locales, localeCookie, config.content.matrix.locale.default),
preferLocale: prefer(locales, localeCookie),
preferredVariant: preferred(variants, variantCookie, config.content.matrix.variant.default),
preferVariant: prefer(variants, variantCookie)
}
}

View File

@ -0,0 +1,49 @@
export function useContentPrefix() {
const config = useAppConfig()
const locales: string[] = config.content.matrix.locale.list.map(l => l.code)
const variants: string[] = config.content.matrix.variant.list.map(v => v.code)
type Prefix = [string, string, string]
let prefixes: Prefix[] = []
if (locales.length>1 && variants.length>1) {
prefixes = locales.flatMap(l => variants.map(v => [`/${l}/${v}`, l, v] as Prefix))
} else if (locales.length>1 && variants.length===1) {
prefixes = locales.map(l => [`/${l}`, l, variants[0]!])
} else if (locales.length===1 && variants.length>1) {
prefixes = variants.map(v => [`/${v}`, locales[0]!, v])
} else if (locales.length===1 && variants.length===1) {
prefixes = [['', locales[0]!, variants[0]!]]
} else {
console.warn('Missing locales and variants in AppConfig')
}
const terminators = new Set(['/', '#', '?'])
function getLocaleVariant(path: string): [string, string] | undefined {
for (const [p, l, v] of prefixes) {
if (path.startsWith(p) && (path.length===p.length || terminators.has(path[p.length]))) {
return [l, v]
}
}
return undefined
}
function buildPrefix(locale: string, variant: string): string {
let segments: string[] = []
if (locales.length > 1) {
segments.push(locale)
}
if (variants.length > 1) {
segments.push(variant)
}
return (segments.length ? '/' : '') + segments.join('/')
}
return {getLocaleVariant, buildPrefix}
}

View File

@ -0,0 +1,16 @@
export default defineNuxtRouteMiddleware((to, from) => {
// important: routes do not exist in middleware, never (in)directly use e.g. useRoute
const {getLocaleVariant, buildPrefix} = useContentPrefix()
const localeVariant = getLocaleVariant(to.path)
if (to.matched.length && localeVariant) return
if (!to.matched.length && localeVariant) {
const [locale, variant] = localeVariant
return navigateTo(buildPrefix(locale, variant))
}
const {preferredLocale, preferredVariant} = useContentPreference()
return navigateTo(buildPrefix(preferredLocale.value, preferredVariant.value))
})