Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/account-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
"types": "./dist/ui/assets/index.d.ts",
"import": "./dist/ui/assets/index.js",
"require": "./dist/ui/assets/index.js"
},
"./i18n": {
"types": "./dist/core/i18n/index.d.ts",
"import": "./dist/core/i18n/index.js",
"require": "./dist/core/i18n/index.js"
}
},
"sideEffects": false,
Expand Down
210 changes: 210 additions & 0 deletions packages/account-sdk/src/core/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Supported locales
export type Locale = 'en' | 'es' | 'fr' | 'de' | 'ja' | 'ko' | 'zh-CN' | 'zh-TW' | 'vi' | 'pt';

// Generic translation keys type - can be extended by any package
export type TranslationKeys = Record<string, string>;

// Translation data structure for any key set
export type Translations<T extends TranslationKeys> = Record<Locale, T>;

// Default locale
export const DEFAULT_LOCALE: Locale = 'en';

// I18n instance interface
export interface I18nInstance<T extends TranslationKeys> {
setLocale(locale: Locale): void;
getLocale(): Locale;
registerTranslations(locale: Locale, messages: T): void;
t(key: keyof T, params?: Record<string, string>): string;
detectAndSetBrowserLocale(): void;
}

// Global translation storage for multiple namespaces
interface TranslationStore {
[namespace: string]: {
currentLocale: Locale;
translations: Record<string, TranslationKeys>;
};
}

const globalStore: TranslationStore = {};

/**
* Create a new i18n instance with custom translation keys
*/
export function createI18n<T extends TranslationKeys>(
namespace: string = 'default',
defaultTranslations?: Partial<Translations<T>>
): I18nInstance<T> {
// Initialize namespace if it doesn't exist
if (!globalStore[namespace]) {
globalStore[namespace] = {
currentLocale: DEFAULT_LOCALE,
translations: {},
};
}

const store = globalStore[namespace];

// Register default translations if provided
if (defaultTranslations) {
Object.entries(defaultTranslations).forEach(([locale, messages]) => {
if (messages) {
store.translations[locale] = messages;
}
});
}

return {
setLocale(locale: Locale): void {
store.currentLocale = locale;
},

getLocale(): Locale {
return store.currentLocale;
},

registerTranslations(locale: Locale, messages: T): void {
store.translations[locale] = messages;
},

t(key: keyof T, params?: Record<string, string>): string {
const localeTranslations = store.translations[store.currentLocale] as T | undefined;
let message: string;

if (localeTranslations && localeTranslations[key as string]) {
message = localeTranslations[key as string];
} else {
// Fallback to English
const fallbackTranslations = store.translations[DEFAULT_LOCALE] as T | undefined;
if (fallbackTranslations && fallbackTranslations[key as string]) {
message = fallbackTranslations[key as string];
} else {
// Ultimate fallback - return the key
return key as string;
}
}

// Simple interpolation
if (params) {
Object.entries(params).forEach(([param, value]) => {
message = message.replace(new RegExp(`{${param}}`, 'g'), value);
});
}

return message;
},

detectAndSetBrowserLocale(): void {
const detectedLocale = detectBrowserLocale();
this.setLocale(detectedLocale);
},
};
}

/**
* Helper function to create translation objects for all supported locales
*/
export function createTranslationSet<T extends TranslationKeys>(
translations: Translations<T>
): Translations<T> {
return translations;
}

/**
* Utility function to detect browser language
* Checks multiple sources in order of preference:
* 1. HTML lang attribute (most specific to current page)
* 2. Navigator language (browser preference)
* 3. Navigator userLanguage (IE fallback)
* 4. Default locale (ultimate fallback)
*/
export function detectBrowserLocale(): Locale {
// Map browser language codes to supported locales
const langMap: Record<string, Locale> = {
en: 'en',
'en-US': 'en',
'en-GB': 'en',
es: 'es',
'es-ES': 'es',
'es-MX': 'es',
fr: 'fr',
'fr-FR': 'fr',
de: 'de',
'de-DE': 'de',
ja: 'ja',
'ja-JP': 'ja',
ko: 'ko',
'ko-KR': 'ko',
zh: 'zh-CN',
'zh-CN': 'zh-CN',
'zh-TW': 'zh-TW',
'zh-HK': 'zh-TW',
vi: 'vi',
'vi-VN': 'vi',
pt: 'pt',
'pt-BR': 'pt',
'pt-PT': 'pt',
};

// Helper function to map language code to supported locale
const mapLanguage = (lang: string): Locale | null => {
if (!lang) return null;

// Try exact match first
if (langMap[lang]) {
return langMap[lang];
}

// Try language without region (e.g., 'en' from 'en-US')
const baseLanguage = lang.split('-')[0];
if (langMap[baseLanguage]) {
return langMap[baseLanguage];
}

return null;
};

// 1. Check HTML lang attribute (most specific)
if (typeof document !== 'undefined') {
const htmlLang = document.documentElement.lang || document.documentElement.getAttribute('lang');
if (htmlLang) {
const mappedLang = mapLanguage(htmlLang);
if (mappedLang) {
return mappedLang;
}
}
}

// 2. Check navigator.language (modern browsers)
if (typeof navigator !== 'undefined') {
if (navigator.language) {
const mappedLang = mapLanguage(navigator.language);
if (mappedLang) {
return mappedLang;
}
}

// 3. Check navigator.userLanguage (IE fallback)
const userLanguage = (navigator as any).userLanguage;
if (userLanguage) {
const mappedLang = mapLanguage(userLanguage);
if (mappedLang) {
return mappedLang;
}
}

// 4. Check navigator.languages array (if available)
if (navigator.languages && navigator.languages.length > 0) {
for (const lang of navigator.languages) {
const mappedLang = mapLanguage(lang);
if (mappedLang) {
return mappedLang;
}
}
}
}

// 5. Ultimate fallback
return DEFAULT_LOCALE;
}
62 changes: 62 additions & 0 deletions packages/account-sdk/src/core/i18n/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { I18nInstance, Locale, TranslationKeys, Translations, createI18n } from './index.js';

/**
* Quick setup function for packages that want to get started immediately
* Usage:
* ```typescript
* interface MyKeys extends TranslationKeys {
* 'button.save': string;
* 'error.invalid': string;
* }
*
* const { t, i18n } = quickSetup<MyKeys>('my-package', {
* en: { 'button.save': 'Save', 'error.invalid': 'Invalid input' },
* es: { 'button.save': 'Guardar', 'error.invalid': 'Entrada inválida' }
* });
* ```
*/
export function quickSetup<T extends TranslationKeys>(
namespace: string,
translations: Partial<Translations<T>>
): {
t: (key: keyof T, params?: Record<string, string>) => string;
i18n: I18nInstance<T>;
setLocale: (locale: Locale) => void;
getLocale: () => Locale;
} {
const i18n = createI18n<T>(namespace, translations);
i18n.detectAndSetBrowserLocale();

return {
t: i18n.t.bind(i18n),
i18n,
setLocale: i18n.setLocale.bind(i18n),
getLocale: i18n.getLocale.bind(i18n),
};
}

/**
* Utility to merge multiple translation namespaces
* Useful when a package wants to extend another package's translations
*/
export function mergeTranslations<T extends TranslationKeys, U extends TranslationKeys>(
base: Partial<Translations<T>>,
extension: Partial<Translations<U>>
): Partial<Translations<T & U>> {
const merged: Partial<Translations<T & U>> = {};

// Get all unique locales from both translation sets
const allLocales = new Set([
...(Object.keys(base) as Locale[]),
...(Object.keys(extension) as Locale[]),
]);

allLocales.forEach((locale) => {
merged[locale] = {
...(base[locale] || {}),
...(extension[locale] || {}),
} as T & U;
});

return merged;
}
52 changes: 52 additions & 0 deletions packages/account-sdk/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { TranslationKeys } from ':core/i18n/index.js';
import { createI18n, createTranslationSet } from ':core/i18n/index.js';
import { de } from './locales/de.js';
import { en } from './locales/en.js';
import { es } from './locales/es.js';
import { fr } from './locales/fr.js';
import { ja } from './locales/ja.js';
import { ko } from './locales/ko.js';
import { pt } from './locales/pt.js';
import { vi } from './locales/vi.js';
import { zhCN } from './locales/zh-CN.js';
import { zhTW } from './locales/zh-TW.js';

// Account-SDK specific translation keys
export interface AccountSDKTranslationKeys extends TranslationKeys {
// Dialog messages
'dialog.base_account': string;
'dialog.signed_in_as': string;
'dialog.popup_blocked.title': string;
'dialog.popup_blocked.message': string;
'dialog.insufficient_balance.title': string;
'dialog.insufficient_balance.message': string;
'dialog.reauthorize.title': string;
'dialog.reauthorize.message': string;

// Button text
'button.try_again': string;
'button.cancel': string;
'button.edit_spend_permission': string;
'button.use_primary_account': string;
'button.continue': string;
'button.not_now': string;
}

const accountSDKTranslations = createTranslationSet<AccountSDKTranslationKeys>({
en: en,
es: es,
fr: fr,
de: de,
ja: ja,
ko: ko,
vi: vi,
pt: pt,
'zh-CN': zhCN,
'zh-TW': zhTW,
});

const accountSDKi18n = createI18n<AccountSDKTranslationKeys>('account-sdk', accountSDKTranslations);

accountSDKi18n.detectAndSetBrowserLocale();

export const { t } = accountSDKi18n;
24 changes: 24 additions & 0 deletions packages/account-sdk/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AccountSDKTranslationKeys } from '../index.js';

export const de: AccountSDKTranslationKeys = {
// Dialog messages
'dialog.base_account': 'Base Account',
'dialog.signed_in_as': 'Angemeldet als {username}',
'dialog.popup_blocked.title': '{app} möchte in Base Account fortfahren',
'dialog.popup_blocked.message':
'Diese Aktion erfordert Ihre Erlaubnis, ein neues Fenster zu öffnen.',
'dialog.insufficient_balance.title': 'Unzureichende Ausgabenberechtigung',
'dialog.insufficient_balance.message':
'Das verbleibende Guthaben Ihrer Ausgabenberechtigung kann diese Transaktion nicht abdecken. Bitte wählen Sie, wie Sie fortfahren möchten:',
'dialog.reauthorize.title': '{app} erneut autorisieren',
'dialog.reauthorize.message':
'{app} hat den Zugang zu Ihrem Konto verloren. Bitte unterschreiben Sie im nächsten Schritt, um {app} erneut zu autorisieren',

// Button text
'button.try_again': 'Erneut versuchen',
'button.cancel': 'Abbrechen',
'button.edit_spend_permission': 'Ausgabenberechtigung bearbeiten',
'button.use_primary_account': 'Hauptkonto verwenden',
'button.continue': 'Fortfahren',
'button.not_now': 'Nicht jetzt',
};
23 changes: 23 additions & 0 deletions packages/account-sdk/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AccountSDKTranslationKeys } from '../index.js';

export const en: AccountSDKTranslationKeys = {
// Dialog messages
'dialog.base_account': 'Base Account',
'dialog.signed_in_as': 'Signed in as {username}',
'dialog.popup_blocked.title': '{app} wants to continue in Base Account',
'dialog.popup_blocked.message': 'This action requires your permission to open a new window.',
'dialog.insufficient_balance.title': 'Insufficient spend permission',
'dialog.insufficient_balance.message':
"Your spend permission's remaining balance cannot cover this transaction. Please choose how to proceed:",
'dialog.reauthorize.title': 'Re-authorize {app}',
'dialog.reauthorize.message':
'{app} has lost access to your account. Please sign at the next step to re-authorize {app}',

// Button text
'button.try_again': 'Try again',
'button.cancel': 'Cancel',
'button.edit_spend_permission': 'Edit spend permission',
'button.use_primary_account': 'Use primary account',
'button.continue': 'Continue',
'button.not_now': 'Not now',
};
Loading