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:
parent
b359838f2a
commit
f02576d94a
5 changed files with 1436 additions and 150 deletions
1381
package-lock.json
generated
1381
package-lock.json
generated
File diff suppressed because it is too large
Load diff
43
src/composables/useLocale.ts
Normal file
43
src/composables/useLocale.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,63 @@
|
||||||
import { createI18n } from 'vue-i18n'
|
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 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({
|
export const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: 'en',
|
locale: savedLocale.value,
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
messages: {
|
messages: {
|
||||||
en,
|
en // Load English by default
|
||||||
es
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
export default {
|
import type { LocaleMessages } from '../types'
|
||||||
|
|
||||||
|
const messages: LocaleMessages = {
|
||||||
nav: {
|
nav: {
|
||||||
title: 'Title Here',
|
title: 'Application Title',
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
directory: 'Directory',
|
directory: 'Directory',
|
||||||
faq: 'FAQ',
|
faq: 'FAQ',
|
||||||
|
|
@ -8,4 +10,45 @@ export default {
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
logout: 'Logout'
|
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
53
src/i18n/types.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue