mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
Add E2E tests for internationalization across multiple pages
- Implemented navigation.spec.ts to test language switching and translation of navigation elements. - Created sessions-page.spec.ts to verify translations on the sessions page, including headers, status badges, and date formatting. - Developed settings-page.spec.ts to ensure settings page content is translated and persists across sessions. - Added skills-page.spec.ts to validate translations for skill categories, action buttons, and empty states.
This commit is contained in:
156
ccw/frontend/src/lib/i18n.ts
Normal file
156
ccw/frontend/src/lib/i18n.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// ========================================
|
||||
// i18n Configuration
|
||||
// ========================================
|
||||
// Internationalization setup with react-intl
|
||||
|
||||
import { createIntl, createIntlCache } from '@formatjs/intl';
|
||||
|
||||
// Supported locales
|
||||
export type Locale = 'en' | 'zh';
|
||||
|
||||
// Available locales with display names
|
||||
export const availableLocales: Record<Locale, string> = {
|
||||
en: 'English',
|
||||
zh: '中文',
|
||||
};
|
||||
|
||||
// Browser language detection
|
||||
function getBrowserLocale(): Locale {
|
||||
if (typeof window === 'undefined') return 'zh';
|
||||
|
||||
const browserLang = navigator.language.toLowerCase();
|
||||
if (browserLang.startsWith('zh')) return 'zh';
|
||||
if (browserLang.startsWith('en')) return 'en';
|
||||
|
||||
// Default to Chinese for unsupported languages
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
// Get initial locale from localStorage or browser detection
|
||||
export function getInitialLocale(): Locale {
|
||||
if (typeof window === 'undefined') return 'zh';
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem('ccw-app-store');
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed.state?.locale && (parsed.state.locale === 'en' || parsed.state.locale === 'zh')) {
|
||||
return parsed.state.locale as Locale;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
return getBrowserLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translation messages for a locale
|
||||
* Dynamically imports the consolidated translation file
|
||||
* NOTE: This dynamic import relies on Vite's glob import feature
|
||||
* to bundle the locale index.ts files.
|
||||
*/
|
||||
async function loadMessages(locale: Locale): Promise<Record<string, string>> {
|
||||
try {
|
||||
// Dynamic import with .ts extension for Vite compatibility
|
||||
const messagesModule = await import(`../locales/${locale}/index.ts`);
|
||||
return messagesModule.default || {};
|
||||
} catch (error) {
|
||||
console.error(`Failed to load messages for locale "${locale}":`, error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Translation messages (will be populated by loading message files)
|
||||
const messages: Record<Locale, Record<string, string>> = {
|
||||
en: {},
|
||||
zh: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize translation messages for all locales
|
||||
* Call this during app initialization
|
||||
*/
|
||||
export async function initMessages(): Promise<void> {
|
||||
// Load messages for both locales in parallel
|
||||
const [enMessages, zhMessages] = await Promise.all([
|
||||
loadMessages('en'),
|
||||
loadMessages('zh'),
|
||||
]);
|
||||
|
||||
messages.en = enMessages;
|
||||
messages.zh = zhMessages;
|
||||
|
||||
// Update current intl instance with loaded messages
|
||||
const currentLocale = getInitialLocale();
|
||||
updateIntl(currentLocale);
|
||||
}
|
||||
|
||||
// Cache for intl instances to avoid recreating on every render
|
||||
const intlCache = createIntlCache();
|
||||
|
||||
// Current intl instance (will be updated when locale changes)
|
||||
let currentIntl = createIntl(
|
||||
{
|
||||
locale: getInitialLocale(),
|
||||
messages: messages[getInitialLocale()],
|
||||
},
|
||||
intlCache
|
||||
);
|
||||
|
||||
/**
|
||||
* Get translation messages for a locale
|
||||
* This will be used to load messages dynamically
|
||||
*/
|
||||
export function getMessages(locale: Locale): Record<string, string> {
|
||||
return messages[locale];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current intl instance with a new locale
|
||||
*/
|
||||
export function updateIntl(locale: Locale): void {
|
||||
currentIntl = createIntl(
|
||||
{
|
||||
locale,
|
||||
messages: messages[locale],
|
||||
},
|
||||
intlCache
|
||||
);
|
||||
|
||||
// Update document lang attribute
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.lang = locale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current intl instance
|
||||
*/
|
||||
export function getIntl() {
|
||||
return currentIntl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register messages for a locale
|
||||
* This can be used to dynamically load translation files
|
||||
*/
|
||||
export function registerMessages(locale: Locale, newMessages: Record<string, string>): void {
|
||||
messages[locale] = { ...messages[locale], ...newMessages };
|
||||
|
||||
// Update current intl if this is the active locale
|
||||
if (currentIntl.locale === locale) {
|
||||
updateIntl(locale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a message using the current intl instance
|
||||
*/
|
||||
export function formatMessage(
|
||||
id: string,
|
||||
values?: Record<string, string | number | boolean | Date | null | undefined>
|
||||
): string {
|
||||
return currentIntl.formatMessage({ id }, values);
|
||||
}
|
||||
Reference in New Issue
Block a user