UNPKG

@shelchin/svelte-i18n

Version:

The last Svelte i18n library you'll ever need. Type-safe, AI-powered, zero-config.

239 lines (238 loc) 11.4 kB
/** * Enhanced auto-discovery system based on index.json configuration * This is a supplementary mechanism - only works when index.json exists */ import { DEV } from '../utils/env.js'; // Cache for index.json configuration to avoid multiple fetches let indexConfigCache = null; let indexConfigPromise = null; // Cache for loaded translations to avoid duplicate fetches const translationCache = new Map(); const translationPromises = new Map(); /** * Get the cached auto-discovery configuration * This is useful for other modules that need to check what languages are configured */ export function getCachedAutoDiscoveryConfig() { return indexConfigCache; } /** * Load the auto-discovery configuration (with caching) */ export async function loadAutoDiscoveryConfig(translationsPath = '/translations', indexFile = 'index.json') { // Return cached if available if (indexConfigCache) { return indexConfigCache; } // Wait for existing promise if in progress if (indexConfigPromise) { return await indexConfigPromise; } // Start loading const { buildAssetUrl } = await import('../utils/base-path.js'); const indexPath = `${translationsPath}/${indexFile}`; const absoluteIndexUrl = typeof window !== 'undefined' && !indexPath.startsWith('http') ? window.location.origin + buildAssetUrl(indexPath) : indexPath; indexConfigPromise = (async () => { try { const response = await fetch(absoluteIndexUrl); if (!response.ok) { return null; } const config = await response.json(); indexConfigCache = config; return config; } catch { return null; } })(); return await indexConfigPromise; } /** * Load translations based on auto-discovery configuration * @param i18n The i18n instance to load translations into * @param options Configuration options */ export async function autoDiscoverTranslations(i18n, options = {}) { const { translationsPath = '/translations', // Don't include base path here indexFile = 'index.json', namespace, onError = () => { }, // Silent by default onLoaded = () => { } // Silent by default } = options; try { // Step 1: Load index.json (with caching) const config = await loadAutoDiscoveryConfig(translationsPath, indexFile); if (!config) { console.warn('Auto-discovery: No index.json found or failed to load'); return; } if (!config.autoDiscovery) { console.warn('Auto-discovery: No configuration found in index.json'); return; } console.log('[Auto-discovery] Config loaded:', config.autoDiscovery); console.log('[Auto-discovery] Looking for namespace:', namespace); // Step 3: Determine what to discover const discoveryTargets = []; // If this is for a specific package namespace if (namespace && config.autoDiscovery.packages?.[namespace]) { console.log(`Auto-discovery: Found package namespace "${namespace}" with languages:`, config.autoDiscovery.packages[namespace]); discoveryTargets.push({ type: namespace, languages: config.autoDiscovery.packages[namespace] }); } // If this is for the main app (no namespace) else if (!namespace && config.autoDiscovery.app) { console.log('[Auto-discovery] Found app translations with languages:', config.autoDiscovery.app); discoveryTargets.push({ type: 'app', languages: config.autoDiscovery.app }); } // IMPORTANT: namespace 'app' should also match app translations else if (namespace === 'app' && config.autoDiscovery.app) { console.log('[Auto-discovery] Found app translations for namespace "app" with languages:', config.autoDiscovery.app); discoveryTargets.push({ type: 'app', languages: config.autoDiscovery.app }); } else { console.log('[Auto-discovery] No matching targets found. Namespace:', namespace, 'Has app config:', !!config.autoDiscovery.app); } // Step 4: Load discovered translations for (const target of discoveryTargets) { for (const locale of target.languages) { // Note: We do NOT skip if locale is already loaded // Auto-discovered translations have higher priority than built-in // They will override any existing translations try { // Determine the file path based on target type let filePath; if (target.type === 'app') { // App translations also in subdirectory for consistency filePath = `${translationsPath}/app/${locale}.json`; } else { // Package translations in subdirectory filePath = `${translationsPath}/${target.type}/${locale}.json`; } const cacheKey = `${target.type}:${locale}`; // Check if already cached if (translationCache.has(cacheKey)) { const cachedTranslations = translationCache.get(cacheKey); const isOverride = i18n.locales.includes(locale); await i18n.loadLanguage(locale, cachedTranslations); onLoaded(target.type, locale); if (DEV) { if (isOverride) { console.debug(`Auto-discovery: Used cached ${locale} to override built-in for ${target.type}`); } else { console.debug(`Auto-discovery: Used cached ${locale} for ${target.type}`); } } continue; } // Check if already being fetched if (translationPromises.has(cacheKey)) { const existingPromise = translationPromises.get(cacheKey); const translations = await existingPromise; if (translations) { const isOverride = i18n.locales.includes(locale); await i18n.loadLanguage(locale, translations); onLoaded(target.type, locale); if (DEV) { if (isOverride) { console.debug(`Auto-discovery: Reused fetch to override ${locale} for ${target.type}`); } else { console.debug(`Auto-discovery: Reused fetch for ${locale} for ${target.type}`); } } } continue; } // Create a promise for this fetch const fetchPromise = (async () => { // Use buildAssetUrl to handle base path correctly const { buildAssetUrl } = await import('../utils/base-path.js'); const absoluteUrl = typeof window !== 'undefined' && !filePath.startsWith('http') ? window.location.origin + buildAssetUrl(filePath) : filePath; console.log(`[Auto-discovery] Fetching ${locale} from: ${absoluteUrl}`); const response = await fetch(absoluteUrl); if (response.ok) { const translations = await response.json(); console.log(`[Auto-discovery] Successfully fetched ${locale}, keys:`, Object.keys(translations)); translationCache.set(cacheKey, translations); return translations; } else if (response.status === 404) { // Expected - language declared but file not yet added console.warn(`[Auto-discovery] ${locale} declared for ${target.type} but file not found at ${absoluteUrl} (404)`); if (DEV) { console.debug(`Auto-discovery: ${locale} declared for ${target.type} but file not found at ${filePath}`); } return null; } else { console.error(`[Auto-discovery] Failed to fetch ${locale}: HTTP ${response.status}`); throw new Error(`HTTP ${response.status}`); } })(); translationPromises.set(cacheKey, fetchPromise); const translations = await fetchPromise; if (translations) { const isOverride = i18n.locales.includes(locale); console.log(`[Auto-discovery] Loading ${locale} for ${target.type}, override=${isOverride}`); await i18n.loadLanguage(locale, translations); console.log(`[Auto-discovery] Successfully loaded ${locale} into i18n store`); onLoaded(target.type, locale); if (DEV) { if (isOverride) { console.debug(`Auto-discovery: Overrode built-in ${locale} for ${target.type} with auto-discovered version`); } else { console.debug(`Auto-discovery: Loaded ${locale} for ${target.type}`); } } } } catch (error) { onError(target.type, locale, error); } finally { // Clean up the promise cache after completion const cacheKey = `${target.type}:${locale}`; translationPromises.delete(cacheKey); } } } } catch (error) { // Configuration loading error if (DEV) { console.error('Auto-discovery configuration error:', error); } } } /** * Helper function to check if auto-discovery is available */ export async function isAutoDiscoveryAvailable(translationsPath = '/translations') { try { const { buildAssetUrl } = await import('../utils/base-path.js'); const indexPath = `${translationsPath}/index.json`; const absoluteIndexUrl = typeof window !== 'undefined' && !indexPath.startsWith('http') ? window.location.origin + buildAssetUrl(indexPath) : indexPath; const response = await fetch(absoluteIndexUrl, { method: 'HEAD' }); return response.ok; } catch { return false; } }