Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)

This commit is contained in:
InnovA AI
2026-03-16 21:47:37 +00:00
commit 42772a31ed
365 changed files with 103572 additions and 0 deletions

297
static/js/i18n.js Normal file
View File

@@ -0,0 +1,297 @@
/**
* Lightweight i18n (internationalization) system for Speakr
* Handles loading and managing translations with template variable support
*/
class I18n {
constructor() {
this.translations = {};
this.currentLocale = 'en';
this.fallbackLocale = 'en';
this.loadedLocales = new Set();
}
/**
* Initialize i18n with default locale
* @param {string} locale - Initial locale code (e.g., 'en', 'es', 'fr', 'zh')
*/
async init(locale = 'en') {
// Get saved locale from localStorage or use browser language
const savedLocale = localStorage.getItem('preferredLanguage');
const browserLocale = navigator.language.split('-')[0];
this.currentLocale = savedLocale || locale || browserLocale || 'en';
// Load the initial locale
await this.loadLocale(this.currentLocale);
// Load fallback locale if different
if (this.currentLocale !== this.fallbackLocale) {
await this.loadLocale(this.fallbackLocale);
}
}
/**
* Load translations for a specific locale
* @param {string} locale - Locale code to load
*/
async loadLocale(locale) {
if (this.loadedLocales.has(locale)) {
return; // Already loaded
}
try {
const response = await fetch(`/static/locales/${locale}.json`);
if (!response.ok) {
throw new Error(`Failed to load locale: ${locale}`);
}
const translations = await response.json();
this.translations[locale] = translations;
this.loadedLocales.add(locale);
console.log(`Loaded locale: ${locale}`);
} catch (error) {
console.error(`Error loading locale ${locale}:`, error);
// If failed to load requested locale and it's not the fallback, try fallback
if (locale !== this.fallbackLocale) {
console.log(`Failed to load ${locale}, will use ${this.fallbackLocale} as fallback`);
// Don't change currentLocale - keep user's preference
// Just ensure fallback translations are available
await this.loadLocale(this.fallbackLocale);
}
}
}
/**
* Change the current locale
* @param {string} locale - New locale code
*/
async setLocale(locale) {
await this.loadLocale(locale);
this.currentLocale = locale;
localStorage.setItem('preferredLanguage', locale);
// Dispatch custom event for locale change
window.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale } }));
}
/**
* Get the current locale
* @returns {string} Current locale code
*/
getLocale() {
return this.currentLocale;
}
/**
* Get available locales
* @returns {Array} List of available locale codes
*/
getAvailableLocales() {
return [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
{ code: 'fr', name: 'French', nativeName: 'Français' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
{ code: 'ru', name: 'Russian', nativeName: 'Русский' },
];
}
/**
* Translate a key with optional parameters
* @param {string} key - Translation key (e.g., 'common.save' or 'nav.upload')
* @param {Object} params - Optional parameters for template replacement
* @param {string} locale - Optional specific locale (defaults to current)
* @returns {string} Translated text
*/
t(key, params = {}, locale = null) {
const targetLocale = locale || this.currentLocale;
// Get translation from current locale or fallback
let translation = this.getNestedTranslation(targetLocale, key);
if (!translation && targetLocale !== this.fallbackLocale) {
translation = this.getNestedTranslation(this.fallbackLocale, key);
}
if (!translation) {
console.warn(`Translation not found for key: ${key}`);
return key; // Return the key itself as fallback
}
// Replace template variables
return this.interpolate(translation, params);
}
/**
* Get nested translation value from object
* @param {string} locale - Locale to search in
* @param {string} key - Dot-separated key path
* @returns {string|null} Translation value or null
*/
getNestedTranslation(locale, key) {
if (!this.translations[locale]) {
return null;
}
const keys = key.split('.');
let value = this.translations[locale];
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return null;
}
}
return typeof value === 'string' ? value : null;
}
/**
* Replace template variables in translation string
* @param {string} text - Text with placeholders like {{variable}}
* @param {Object} params - Parameters to replace
* @returns {string} Interpolated text
*/
interpolate(text, params) {
return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return params.hasOwnProperty(key) ? params[key] : match;
});
}
/**
* Handle pluralization
* @param {string} key - Base translation key
* @param {number} count - Count for pluralization
* @param {Object} params - Additional parameters
* @returns {string} Translated text with proper pluralization
*/
tc(key, count, params = {}) {
const pluralKey = count === 1 ? key : `${key}Plural`;
return this.t(pluralKey, { ...params, count });
}
/**
* Format date according to locale
* @param {Date|string} date - Date to format
* @param {Object} options - Intl.DateTimeFormat options
* @returns {string} Formatted date string
*/
formatDate(date, options = {}) {
const d = date instanceof Date ? date : new Date(date);
return new Intl.DateTimeFormat(this.currentLocale, options).format(d);
}
/**
* Format number according to locale
* @param {number} number - Number to format
* @param {Object} options - Intl.NumberFormat options
* @returns {string} Formatted number string
*/
formatNumber(number, options = {}) {
return new Intl.NumberFormat(this.currentLocale, options).format(number);
}
/**
* Format file size with appropriate unit
* @param {number} bytes - Size in bytes
* @returns {string} Formatted file size
*/
formatFileSize(bytes) {
const units = ['bytes', 'kilobytes', 'megabytes', 'gigabytes'];
const unitValues = [1, 1024, 1048576, 1073741824];
let unitIndex = 0;
for (let i = unitValues.length - 1; i >= 0; i--) {
if (bytes >= unitValues[i]) {
unitIndex = i;
break;
}
}
const value = Math.round(bytes / unitValues[unitIndex] * 10) / 10;
return this.t(`fileSize.${units[unitIndex]}`, { count: value });
}
/**
* Format duration with appropriate unit
* @param {number} seconds - Duration in seconds
* @returns {string} Formatted duration
*/
formatDuration(seconds) {
if (seconds < 60) {
return this.tc('duration.seconds', Math.round(seconds), { count: Math.round(seconds) });
} else if (seconds < 3600) {
const minutes = Math.round(seconds / 60);
return this.tc('duration.minutes', minutes, { count: minutes });
} else {
const hours = Math.round(seconds / 3600 * 10) / 10;
return this.tc('duration.hours', hours, { count: hours });
}
}
/**
* Format relative time (e.g., "2 hours ago")
* @param {Date|string} date - Date to format
* @returns {string} Formatted relative time
*/
formatRelativeTime(date) {
const d = date instanceof Date ? date : new Date(date);
const now = new Date();
const diffSeconds = Math.floor((now - d) / 1000);
if (diffSeconds < 60) {
return this.t('time.justNow');
} else if (diffSeconds < 3600) {
const minutes = Math.floor(diffSeconds / 60);
return minutes === 1
? this.t('time.minuteAgo')
: this.t('time.minutesAgo', { count: minutes });
} else if (diffSeconds < 86400) {
const hours = Math.floor(diffSeconds / 3600);
return hours === 1
? this.t('time.hourAgo')
: this.t('time.hoursAgo', { count: hours });
} else if (diffSeconds < 604800) {
const days = Math.floor(diffSeconds / 86400);
return days === 1
? this.t('time.dayAgo')
: this.t('time.daysAgo', { count: days });
} else if (diffSeconds < 2592000) {
const weeks = Math.floor(diffSeconds / 604800);
return weeks === 1
? this.t('time.weekAgo')
: this.t('time.weeksAgo', { count: weeks });
} else if (diffSeconds < 31536000) {
const months = Math.floor(diffSeconds / 2592000);
return months === 1
? this.t('time.monthAgo')
: this.t('time.monthsAgo', { count: months });
} else {
const years = Math.floor(diffSeconds / 31536000);
return years === 1
? this.t('time.yearAgo')
: this.t('time.yearsAgo', { count: years });
}
}
}
// Create global i18n instance
const i18n = new I18n();
// Create a fallback t function immediately
if (typeof window !== 'undefined') {
// Ensure window.i18n exists with at least a basic t function
window.i18n = i18n;
// Add a fallback t function if the class method isn't ready
if (!window.i18n.t) {
window.i18n.t = function(key, params) {
console.warn('i18n.t called before initialization, returning key:', key);
return key;
};
}
}