feat(i18n): Enhance internationalization with dynamic locale management

- Add comprehensive locale management with `useLocale` composable
- Implement dynamic locale loading and persistent storage
- Create type-safe internationalization infrastructure
- Add flag emojis and locale selection utilities
- Expand English locale with more comprehensive message schemas
This commit is contained in:
padreug 2025-03-09 13:27:45 +01:00
parent b359838f2a
commit f02576d94a
5 changed files with 1436 additions and 150 deletions

1381
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,43 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { AvailableLocale } from '@/i18n'
import { AVAILABLE_LOCALES, changeLocale, isAvailableLocale } from '@/i18n'
export function useLocale() {
const { locale, t } = useI18n()
const currentLocale = computed(() => locale.value as AvailableLocale)
const locales = computed(() =>
AVAILABLE_LOCALES.map(code => ({
code,
name: t(`locales.${code}`),
flag: getFlagEmoji(code)
}))
)
async function setLocale(newLocale: string) {
if (isAvailableLocale(newLocale)) {
await changeLocale(newLocale)
}
}
// Helper function to get flag emoji from locale code
function getFlagEmoji(locale: string): string {
const flagMap: Record<string, string> = {
'en': '🇬🇧',
'es': '🇪🇸',
'fr': '🇫🇷',
'de': '🇩🇪',
'zh': '🇨🇳'
}
return flagMap[locale] || '🌐'
}
return {
currentLocale,
locales,
setLocale,
isAvailableLocale
}
}

View file

@ -1,13 +1,63 @@
import { createI18n } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
import { useStorage } from '@vueuse/core'
// Import base locale
import en from './locales/en'
import es from './locales/es'
// Define available locales
export const AVAILABLE_LOCALES = ['en', 'es', 'fr', 'de', 'zh'] as const
export type AvailableLocale = typeof AVAILABLE_LOCALES[number]
// Type for our messages
export type MessageSchema = typeof en
// Create persistent storage for user's locale preference
const savedLocale = useStorage<AvailableLocale>('user-locale', 'en')
// Async locale loading
async function loadLocale(locale: AvailableLocale): Promise<MessageSchema> {
try {
const messages = await import(`./locales/${locale}.ts`)
return messages.default
} catch (error) {
console.error(`Failed to load locale ${locale}:`, error)
return en // Fallback to English
}
}
export const i18n = createI18n({
legacy: false,
locale: 'en',
locale: savedLocale.value,
fallbackLocale: 'en',
messages: {
en,
es
en // Load English by default
}
})
})
// Function to change locale
export async function changeLocale(locale: AvailableLocale) {
try {
// Only load if it's not already loaded
if (!i18n.global.availableLocales.includes(locale)) {
const messages = await loadLocale(locale)
i18n.global.setLocaleMessage(locale, messages)
}
i18n.global.locale.value = locale
savedLocale.value = locale
document.querySelector('html')?.setAttribute('lang', locale)
} catch (error) {
console.error(`Failed to change locale to ${locale}:`, error)
}
}
// Type guard for available locales
export function isAvailableLocale(locale: string): locale is AvailableLocale {
return AVAILABLE_LOCALES.includes(locale as AvailableLocale)
}
// Initialize with saved locale if different from default
if (savedLocale.value !== 'en') {
changeLocale(savedLocale.value)
}

View file

@ -1,6 +1,8 @@
export default {
import type { LocaleMessages } from '../types'
const messages: LocaleMessages = {
nav: {
title: 'Title Here',
title: 'Application Title',
home: 'Home',
directory: 'Directory',
faq: 'FAQ',
@ -8,4 +10,45 @@ export default {
login: 'Login',
logout: 'Logout'
},
}
common: {
loading: 'Loading...',
error: 'An error occurred',
success: 'Operation successful'
},
errors: {
notFound: 'Page not found',
serverError: 'Server error occurred',
networkError: 'Network connection error'
},
dateTimeFormats: {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric'
}
},
numberFormats: {
currency: {
style: 'currency',
currencyDisplay: 'symbol'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2
},
percent: {
style: 'percent',
useGrouping: false
}
}
}
export default messages

53
src/i18n/types.ts Normal file
View file

@ -0,0 +1,53 @@
export interface LocaleMessages {
nav: {
title: string
home: string
directory: string
faq: string
support: string
login: string
logout: string
}
// Add more message categories here
common: {
loading: string
error: string
success: string
}
errors: {
notFound: string
serverError: string
networkError: string
}
// Add date/time formats
dateTimeFormats: {
short: {
year: 'numeric'
month: 'short'
day: 'numeric'
}
long: {
year: 'numeric'
month: 'long'
day: 'numeric'
weekday: 'long'
hour: 'numeric'
minute: 'numeric'
}
}
// Add number formats
numberFormats: {
currency: {
style: 'currency'
currencyDisplay: 'symbol'
}
decimal: {
style: 'decimal'
minimumFractionDigits: 2
}
percent: {
style: 'percent'
useGrouping: false
}
}
}