feat: implement new feature for enhanced user experience

This commit is contained in:
2026-02-11 16:36:56 +00:00
parent 62f1906276
commit dc00d24899
75 changed files with 2456 additions and 35 deletions
+168
View File
@@ -0,0 +1,168 @@
import { writable, derived, get } from "svelte/store"
import { location } from "./store"
/**
* Supported languages configuration.
* Add more languages as needed for your project.
*/
export const SUPPORTED_LANGUAGES = ["de", "en"] as const
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]
export const DEFAULT_LANGUAGE: SupportedLanguage = "de"
export const LANGUAGE_LABELS: Record<SupportedLanguage, string> = {
de: "Deutsch",
en: "English",
}
/**
* Route translations for localized URLs.
* Add entries for routes that need translated slugs.
* Example: { about: { de: "ueber-uns", en: "about" } }
*/
export const ROUTE_TRANSLATIONS: Record<string, Record<SupportedLanguage, string>> = {
// Add your route translations here:
// about: { de: "ueber-uns", en: "about" },
}
export const getLocalizedRoute = (canonicalRoute: string, lang: SupportedLanguage): string => {
const translations = ROUTE_TRANSLATIONS[canonicalRoute]
if (translations && translations[lang]) {
return translations[lang]
}
return canonicalRoute
}
export const getCanonicalRoute = (localizedSegment: string): string => {
for (const [canonical, translations] of Object.entries(ROUTE_TRANSLATIONS)) {
for (const translated of Object.values(translations)) {
if (translated === localizedSegment) {
return canonical
}
}
}
return localizedSegment
}
/**
* Extract the language code from a URL path.
* Returns null if no valid language prefix is found.
*/
export const extractLanguageFromPath = (path: string): SupportedLanguage | null => {
const match = path.match(/^\/([a-z]{2})(\/|$)/)
if (match && SUPPORTED_LANGUAGES.includes(match[1] as SupportedLanguage)) {
return match[1] as SupportedLanguage
}
return null
}
export const stripLanguageFromPath = (path: string): string => {
const lang = extractLanguageFromPath(path)
if (lang) {
const stripped = path.slice(3)
return stripped || "/"
}
return path
}
export const getRoutePath = (fullPath: string): string => {
const stripped = stripLanguageFromPath(fullPath)
if (stripped === "/" || stripped === "") {
return "/"
}
const segments = stripped.split("/").filter(Boolean)
if (segments.length > 0) {
const canonicalFirst = getCanonicalRoute(segments[0])
if (canonicalFirst !== segments[0]) {
segments[0] = canonicalFirst
return "/" + segments.join("/")
}
}
return stripped
}
/**
* Derived store: current language based on URL.
*/
export const currentLanguage = derived(location, ($location) => {
const path = $location.path || "/"
return extractLanguageFromPath(path) || DEFAULT_LANGUAGE
})
/**
* Writable store for the selected language.
*/
export const selectedLanguage = writable<SupportedLanguage>(DEFAULT_LANGUAGE)
if (typeof window !== "undefined") {
const initialLang = extractLanguageFromPath(window.location.pathname)
if (initialLang) {
selectedLanguage.set(initialLang)
}
}
if (typeof window !== "undefined") {
location.subscribe(($loc) => {
const lang = extractLanguageFromPath($loc.path)
if (lang) {
selectedLanguage.set(lang)
}
})
}
/**
* Get the localized path for a given route and language.
*/
export const localizedPath = (path: string, lang?: SupportedLanguage): string => {
const language = lang || get(currentLanguage)
if (path === "/" || path === "") {
return `/${language}`
}
let cleanPath = stripLanguageFromPath(path)
cleanPath = cleanPath.startsWith("/") ? cleanPath : `/${cleanPath}`
const segments = cleanPath.split("/").filter(Boolean)
if (segments.length > 0) {
const canonicalFirst = getCanonicalRoute(segments[0])
const localizedFirst = getLocalizedRoute(canonicalFirst, language)
segments[0] = localizedFirst
}
const translatedPath = "/" + segments.join("/")
return `/${language}${translatedPath}`
}
/**
* Derived store for localized href.
*/
export const localizedHref = (path: string) => {
return derived(currentLanguage, ($lang) => localizedPath(path, $lang))
}
export const isValidLanguage = (lang: string): lang is SupportedLanguage => {
return SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)
}
/**
* Get the browser's preferred language, falling back to default.
*/
export const getBrowserLanguage = (): SupportedLanguage => {
if (typeof navigator === "undefined") {
return DEFAULT_LANGUAGE
}
const browserLangs = navigator.languages || [navigator.language]
for (const lang of browserLangs) {
const code = lang.split("-")[0].toLowerCase()
if (isValidLanguage(code)) {
return code
}
}
return DEFAULT_LANGUAGE
}
/**
* Get URL for switching to a different language.
*/
export const getLanguageSwitchUrl = (newLang: SupportedLanguage): string => {
const currentPath = typeof window !== "undefined" ? window.location.pathname : "/"
const canonicalPath = getRoutePath(currentPath)
return localizedPath(canonicalPath, newLang)
}
+75
View File
@@ -0,0 +1,75 @@
import { register, init, locale, waitLocale, getLocaleFromNavigator } from "svelte-i18n"
import { get } from "svelte/store"
import {
SUPPORTED_LANGUAGES,
DEFAULT_LANGUAGE,
extractLanguageFromPath,
selectedLanguage,
type SupportedLanguage,
} from "../i18n"
// Re-export svelte-i18n utilities for convenience
export { _, format, time, date, number, json } from "svelte-i18n"
export { locale, isLoading, addMessages } from "svelte-i18n"
// Register locale files for lazy loading
register("de", () => import("./locales/de.json"))
register("en", () => import("./locales/en.json"))
/**
* Determine the initial locale from URL, browser, or fallback.
*/
function getInitialLocale(url?: string): SupportedLanguage {
// 1. Try URL path
if (url) {
const langFromUrl = extractLanguageFromPath(url)
if (langFromUrl) return langFromUrl
}
if (typeof window !== "undefined") {
const langFromPath = extractLanguageFromPath(window.location.pathname)
if (langFromPath) return langFromPath
}
// 2. Try browser settings
if (typeof navigator !== "undefined") {
const browserLocale = getLocaleFromNavigator()
if (browserLocale) {
const langCode = browserLocale.split("-")[0] as SupportedLanguage
if (SUPPORTED_LANGUAGES.includes(langCode)) {
return langCode
}
}
}
// 3. Fallback
return DEFAULT_LANGUAGE
}
/**
* Initialize i18n for client-side rendering.
* Call this before mounting the Svelte app.
*/
export async function setupI18n(url?: string): Promise<void> {
const initialLocale = getInitialLocale(url)
init({
fallbackLocale: DEFAULT_LANGUAGE,
initialLocale,
})
selectedLanguage.set(initialLocale)
// Keep svelte-i18n locale and selectedLanguage store in sync
locale.subscribe((newLocale) => {
if (newLocale && SUPPORTED_LANGUAGES.includes(newLocale as SupportedLanguage)) {
selectedLanguage.set(newLocale as SupportedLanguage)
}
})
selectedLanguage.subscribe((newLang) => {
const currentLocale = get(locale)
if (newLang && newLang !== currentLocale) {
locale.set(newLang)
}
})
await waitLocale()
}
+23
View File
@@ -0,0 +1,23 @@
{
"nav": {
"home": "Startseite",
"about": "Über uns",
"contact": "Kontakt"
},
"page": {
"home": {
"title": "Willkommen",
"text": "Dies ist der Tibi Svelte Starter. Passe diese Seite an dein Projekt an."
},
"about": {
"title": "Über uns",
"text": "Hier kannst du Informationen über dein Projekt darstellen."
},
"contact": {
"title": "Kontakt",
"text": "Hier kannst du ein Kontaktformular oder Kontaktdaten anzeigen."
}
},
"welcome": "Willkommen",
"language": "Sprache"
}
+23
View File
@@ -0,0 +1,23 @@
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"page": {
"home": {
"title": "Welcome",
"text": "This is the Tibi Svelte Starter. Customise this page for your project."
},
"about": {
"title": "About",
"text": "Use this page to present information about your project."
},
"contact": {
"title": "Contact",
"text": "Use this page to display a contact form or contact details."
}
},
"welcome": "Welcome",
"language": "Language"
}