feat:移除了弹窗,服务器添加sls
This commit is contained in:
344
packages/ui/src/composables/i18n.ts
Normal file
344
packages/ui/src/composables/i18n.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import IntlMessageFormat from 'intl-messageformat'
|
||||
import type { Ref } from 'vue'
|
||||
import type { CompileError, MessageCompiler, MessageContext } from 'vue-i18n'
|
||||
|
||||
import { injectI18n } from '../providers/i18n'
|
||||
import { injectI18nDebug } from './i18n-debug'
|
||||
|
||||
export interface MessageDescriptor {
|
||||
id: string
|
||||
defaultMessage?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type MessageDescriptorMap<K extends string> = Record<K, MessageDescriptor>
|
||||
|
||||
export type CrowdinMessages = Record<string, { message?: string; defaultMessage?: string } | string>
|
||||
|
||||
export function defineMessage<T extends MessageDescriptor>(descriptor: T): T {
|
||||
return descriptor
|
||||
}
|
||||
|
||||
export function defineMessages<K extends string, T extends MessageDescriptorMap<K>>(
|
||||
descriptors: T,
|
||||
): T {
|
||||
return descriptors
|
||||
}
|
||||
|
||||
export interface LocaleDefinition {
|
||||
code: string
|
||||
name: string
|
||||
translatedName: MessageDescriptor
|
||||
numeric?: Intl.RelativeTimeFormatNumeric
|
||||
compactNumberPlural?: number
|
||||
dir?: 'ltr' | 'rtl'
|
||||
serverLanguageCode?: string
|
||||
}
|
||||
|
||||
export const LOCALES: LocaleDefinition[] = [
|
||||
// Commented out as it's RTL - will enable when we have better RTL support
|
||||
// {
|
||||
// code: 'ar-SA',
|
||||
// name: 'العربية (السعودية)',
|
||||
// translatedName: defineMessage({ id: 'locale.ar-SA', defaultMessage: 'Arabic' }),
|
||||
// dir: 'rtl',
|
||||
// },
|
||||
{
|
||||
code: 'cs-CZ',
|
||||
name: 'Čeština',
|
||||
translatedName: defineMessage({ id: 'locale.cs-CZ', defaultMessage: 'Czech' }),
|
||||
},
|
||||
{
|
||||
code: 'da-DK',
|
||||
name: 'Dansk',
|
||||
translatedName: defineMessage({ id: 'locale.da-DK', defaultMessage: 'Danish' }),
|
||||
},
|
||||
{
|
||||
code: 'de-CH',
|
||||
name: 'Deutsch (Schweiz)',
|
||||
translatedName: defineMessage({ id: 'locale.de-CH', defaultMessage: 'German (Switzerland)' }),
|
||||
},
|
||||
{
|
||||
code: 'de-DE',
|
||||
name: 'Deutsch (Deutschland)',
|
||||
translatedName: defineMessage({ id: 'locale.de-DE', defaultMessage: 'German (Germany)' }),
|
||||
},
|
||||
{
|
||||
code: 'en-US',
|
||||
name: 'English (United States)',
|
||||
translatedName: defineMessage({
|
||||
id: 'locale.en-US',
|
||||
defaultMessage: 'English (United States)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
code: 'es-419',
|
||||
name: 'Español (Latinoamérica)',
|
||||
translatedName: defineMessage({
|
||||
id: 'locale.es-419',
|
||||
defaultMessage: 'Spanish (Latin America)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
code: 'es-ES',
|
||||
name: 'Español (España)',
|
||||
translatedName: defineMessage({ id: 'locale.es-ES', defaultMessage: 'Spanish (Spain)' }),
|
||||
},
|
||||
{
|
||||
code: 'fi-FI',
|
||||
name: 'Suomi',
|
||||
translatedName: defineMessage({ id: 'locale.fi-FI', defaultMessage: 'Finnish' }),
|
||||
},
|
||||
{
|
||||
code: 'fil-PH',
|
||||
name: 'Filipino',
|
||||
translatedName: defineMessage({ id: 'locale.fil-PH', defaultMessage: 'Filipino' }),
|
||||
compactNumberPlural: 1,
|
||||
serverLanguageCode: 'tl',
|
||||
},
|
||||
{
|
||||
code: 'fr-FR',
|
||||
name: 'Français',
|
||||
translatedName: defineMessage({ id: 'locale.fr-FR', defaultMessage: 'French' }),
|
||||
},
|
||||
{
|
||||
code: 'he-IL',
|
||||
name: 'עברית',
|
||||
translatedName: defineMessage({ id: 'locale.he-IL', defaultMessage: 'Hebrew' }),
|
||||
dir: 'rtl',
|
||||
},
|
||||
{
|
||||
code: 'hu-HU',
|
||||
name: 'Magyar',
|
||||
translatedName: defineMessage({ id: 'locale.hu-HU', defaultMessage: 'Hungarian' }),
|
||||
},
|
||||
{
|
||||
code: 'id-ID',
|
||||
name: 'Bahasa Indonesia',
|
||||
translatedName: defineMessage({ id: 'locale.id-ID', defaultMessage: 'Indonesian' }),
|
||||
},
|
||||
{
|
||||
code: 'it-IT',
|
||||
name: 'Italiano (Italia)',
|
||||
translatedName: defineMessage({ id: 'locale.it-IT', defaultMessage: 'Italian (Italy)' }),
|
||||
numeric: 'always',
|
||||
},
|
||||
{
|
||||
code: 'ja-JP',
|
||||
name: '日本語',
|
||||
translatedName: defineMessage({ id: 'locale.ja-JP', defaultMessage: 'Japanese' }),
|
||||
},
|
||||
{
|
||||
code: 'ko-KR',
|
||||
name: '한국어',
|
||||
translatedName: defineMessage({ id: 'locale.ko-KR', defaultMessage: 'Korean' }),
|
||||
},
|
||||
{
|
||||
code: 'ms-MY',
|
||||
name: 'Bahasa Melayu',
|
||||
translatedName: defineMessage({ id: 'locale.ms-MY', defaultMessage: 'Malay' }),
|
||||
},
|
||||
{
|
||||
code: 'nl-NL',
|
||||
name: 'Nederlands',
|
||||
translatedName: defineMessage({ id: 'locale.nl-NL', defaultMessage: 'Dutch' }),
|
||||
},
|
||||
{
|
||||
code: 'no-NO',
|
||||
name: 'Norsk (Bokmål)',
|
||||
translatedName: defineMessage({ id: 'locale.no-NO', defaultMessage: 'Norwegian Bokmål' }),
|
||||
},
|
||||
{
|
||||
code: 'pl-PL',
|
||||
name: 'Polski',
|
||||
translatedName: defineMessage({ id: 'locale.pl-PL', defaultMessage: 'Polish' }),
|
||||
},
|
||||
{
|
||||
code: 'pt-BR',
|
||||
name: 'Português (Brasil)',
|
||||
translatedName: defineMessage({ id: 'locale.pt-BR', defaultMessage: 'Portuguese (Brazil)' }),
|
||||
},
|
||||
{
|
||||
code: 'pt-PT',
|
||||
name: 'Português (Portugal)',
|
||||
translatedName: defineMessage({ id: 'locale.pt-PT', defaultMessage: 'Portuguese (Portugal)' }),
|
||||
},
|
||||
{
|
||||
code: 'ro-RO',
|
||||
name: 'Română',
|
||||
translatedName: defineMessage({ id: 'locale.ro-RO', defaultMessage: 'Romanian' }),
|
||||
},
|
||||
{
|
||||
code: 'ru-RU',
|
||||
name: 'Русский',
|
||||
translatedName: defineMessage({ id: 'locale.ru-RU', defaultMessage: 'Russian' }),
|
||||
numeric: 'always',
|
||||
},
|
||||
{
|
||||
code: 'sr-CS',
|
||||
name: 'Srpski (latinica)',
|
||||
translatedName: defineMessage({ id: 'locale.sr-CS', defaultMessage: 'Serbian (Latin)' }),
|
||||
},
|
||||
{
|
||||
code: 'sv-SE',
|
||||
name: 'Svenska',
|
||||
translatedName: defineMessage({ id: 'locale.sv-SE', defaultMessage: 'Swedish' }),
|
||||
},
|
||||
{
|
||||
code: 'th-TH',
|
||||
name: 'ไทย',
|
||||
translatedName: defineMessage({ id: 'locale.th-TH', defaultMessage: 'Thai' }),
|
||||
},
|
||||
{
|
||||
code: 'tr-TR',
|
||||
name: 'Türkçe',
|
||||
translatedName: defineMessage({ id: 'locale.tr-TR', defaultMessage: 'Turkish' }),
|
||||
},
|
||||
{
|
||||
code: 'uk-UA',
|
||||
name: 'Українська',
|
||||
translatedName: defineMessage({ id: 'locale.uk-UA', defaultMessage: 'Ukrainian' }),
|
||||
},
|
||||
{
|
||||
code: 'vi-VN',
|
||||
name: 'Tiếng Việt',
|
||||
translatedName: defineMessage({ id: 'locale.vi-VN', defaultMessage: 'Vietnamese' }),
|
||||
},
|
||||
{
|
||||
code: 'zh-CN',
|
||||
name: '简体中文',
|
||||
translatedName: defineMessage({ id: 'locale.zh-CN', defaultMessage: 'Chinese (Simplified)' }),
|
||||
},
|
||||
{
|
||||
code: 'zh-TW',
|
||||
name: '繁體中文',
|
||||
translatedName: defineMessage({ id: 'locale.zh-TW', defaultMessage: 'Chinese (Traditional)' }),
|
||||
},
|
||||
]
|
||||
|
||||
export function transformCrowdinMessages(messages: CrowdinMessages): Record<string, string> {
|
||||
const result: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(messages)) {
|
||||
if (typeof value === 'string') {
|
||||
result[key] = value
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
const msg = value.message ?? value.defaultMessage
|
||||
if (msg) {
|
||||
result[key] = msg
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const LOCALE_CODES = new Set(LOCALES.map((l) => l.code))
|
||||
|
||||
/**
|
||||
* Builds locale messages from glob-imported modules.
|
||||
* Only includes locales that are defined in the LOCALES array.
|
||||
* Usage: buildLocaleMessages(import.meta.glob('./locales/* /index.json', { eager: true }))
|
||||
*/
|
||||
export function buildLocaleMessages(
|
||||
...allModules: Record<string, { default: CrowdinMessages }>[]
|
||||
): Record<string, Record<string, string>> {
|
||||
const messages: Record<string, Record<string, string>> = {}
|
||||
for (const modules of allModules) {
|
||||
for (const [path, module] of Object.entries(modules)) {
|
||||
// Extract locale code from path like './locales/en-US/index.json', './src/locales/en-US/index.json' or './locales/en-US/meta.json'
|
||||
const match = path.match(/\/([^/]+)\/(index|meta)\.json$/)
|
||||
if (match) {
|
||||
const locale = match[1]
|
||||
// Only include locales that are in our LOCALES list
|
||||
if (LOCALE_CODES.has(locale)) {
|
||||
const mergedMessages = messages[locale] ?? {}
|
||||
messages[locale] = Object.assign(mergedMessages, transformCrowdinMessages(module.default))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a vue-i18n message compiler that uses IntlMessageFormat for ICU syntax support.
|
||||
* This enables pluralization, select, and other ICU message features.
|
||||
*/
|
||||
export function createMessageCompiler(): MessageCompiler {
|
||||
return (msg, { locale, key, onError }) => {
|
||||
let messageString: string
|
||||
|
||||
if (typeof msg === 'string') {
|
||||
messageString = msg
|
||||
} else if (typeof msg === 'object' && msg !== null && 'message' in msg) {
|
||||
messageString = (msg as { message: string }).message
|
||||
} else {
|
||||
onError?.(new Error('Invalid message format') as CompileError)
|
||||
return () => key
|
||||
}
|
||||
|
||||
try {
|
||||
const formatter = new IntlMessageFormat(messageString, locale)
|
||||
return (ctx: MessageContext) => {
|
||||
try {
|
||||
return formatter.format(ctx.values as Record<string, unknown>) as string
|
||||
} catch {
|
||||
return messageString
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
onError?.(e as CompileError)
|
||||
return () => key
|
||||
}
|
||||
}
|
||||
}
|
||||
export interface VIntlFormatters {
|
||||
formatMessage(descriptor: MessageDescriptor, values?: Record<string, unknown>): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable that provides formatMessage() with the same API as @vintl/vintl.
|
||||
* Uses the injected I18nContext from the provider.
|
||||
*/
|
||||
export function useVIntl(): VIntlFormatters & { locale: Ref<string> } {
|
||||
const { t, locale } = injectI18n()
|
||||
const debugContext = injectI18nDebug()
|
||||
|
||||
function formatMessage(descriptor: MessageDescriptor, values?: Record<string, unknown>): string {
|
||||
// Read locale.value to ensure Vue tracks this as a reactive dependency
|
||||
// when formatMessage is called during component render
|
||||
void locale.value
|
||||
|
||||
const key = descriptor.id
|
||||
const translation = t(key, values ?? {})
|
||||
|
||||
let result: string
|
||||
if (translation && translation !== key) {
|
||||
result = translation as string
|
||||
} else {
|
||||
// Fallback to defaultMessage if key not found
|
||||
const defaultMsg = descriptor.defaultMessage ?? key
|
||||
try {
|
||||
const formatter = new IntlMessageFormat(defaultMsg, locale.value)
|
||||
result = formatter.format(values ?? {}) as string
|
||||
} catch {
|
||||
result = defaultMsg
|
||||
}
|
||||
}
|
||||
|
||||
if (debugContext?.enabled.value) {
|
||||
debugContext.registry.set(key, {
|
||||
key,
|
||||
value: result,
|
||||
defaultMessage: descriptor.defaultMessage,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
if (debugContext.keyReveal.value) {
|
||||
return `\u300C${key}\u300D`
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return { formatMessage, locale }
|
||||
}
|
||||
Reference in New Issue
Block a user