/* U-Claw browser-side i18n. * * A classic script, not an ES module, and the catalogue is a script rather than * JSON on purpose: these pages are opened from file:// (the launcher opens * loading.html with a file:// URL, Welcome.html links to its siblings), and from * a file:// origin both fetch() and ES-module imports of local files are blocked. * That is the exact bug that left the old Config.html silently broken. A plain * optional, written by the launcher * * * * Then mark text with data-i18n="key". Attributes use data-i18n-attr="placeholder:key". * The HTML keeps English as its literal fallback, so a missing catalogue degrades * to readable English instead of raw keys. */ (function (global) { 'use strict'; var SUPPORTED = ['en', 'zh-CN']; var FALLBACK = 'en'; function normalise(tag) { if (!tag) return null; var lower = String(tag).toLowerCase(); if (lower.indexOf('zh') === 0) return 'zh-CN'; if (lower.indexOf('en') === 0) return 'en'; return null; } // The language travels with the drive, not the machine: someone who set the // drive up in Chinese and plugs it into a colleague's English Windows should // still see Chinese. The launcher writes data/.openclaw/locale.js on startup; // ?lang= is the manual override, and the browser language is the last resort. function resolveLocale() { var fromQuery = null; try { fromQuery = new URLSearchParams(global.location.search).get('lang'); } catch (e) { /* older browser or opaque URL */ } return normalise(fromQuery) || normalise(global.UCLAW_LOCALE) || normalise(global.navigator && global.navigator.language) || FALLBACK; } var locale = resolveLocale(); var catalogues = global.UCLAW_I18N_MESSAGES || {}; var active = catalogues[SUPPORTED.indexOf(locale) === -1 ? FALLBACK : locale] || {}; var fallback = catalogues[FALLBACK] || {}; function t(key, vars) { var template = active[key]; if (template === undefined) template = fallback[key]; if (template === undefined) return key; if (!vars) return template; return template.replace(/\{(\w+)\}/g, function (match, name) { return Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match; }); } function apply(root) { var scope = root || global.document; scope.querySelectorAll('[data-i18n]').forEach(function (node) { var value = t(node.getAttribute('data-i18n')); // Keep the literal in the HTML when a key has no translation, rather than // replacing readable English with the key name. if (value !== node.getAttribute('data-i18n')) node.textContent = value; }); scope.querySelectorAll('[data-i18n-attr]').forEach(function (node) { node.getAttribute('data-i18n-attr').split(',').forEach(function (pair) { var parts = pair.split(':'); if (parts.length !== 2) return; var attr = parts[0].trim(); var value = t(parts[1].trim()); if (value !== parts[1].trim()) node.setAttribute(attr, value); }); }); global.document.documentElement.lang = locale; } // Region defaults for Singapore. The old pages assumed China: Asia/Shanghai, // YYYY-MM-DD, 24-hour, ¥. SG uses DD/MM/YYYY (neither Chinese nor American), // 12-hour with am/pm, and S$. var REGION = { en: { locale: 'en-SG', timeZone: 'Asia/Singapore', currency: 'SGD', hour12: true }, 'zh-CN': { locale: 'zh-SG', timeZone: 'Asia/Singapore', currency: 'SGD', hour12: true }, }; function region() { return REGION[locale] || REGION.en; } function formatDate(date) { var r = region(); try { return new Intl.DateTimeFormat(r.locale, { timeZone: r.timeZone, day: '2-digit', month: '2-digit', year: 'numeric', }).format(date); } catch (e) { return String(date); } } function formatTime(date) { var r = region(); try { return new Intl.DateTimeFormat(r.locale, { timeZone: r.timeZone, hour: 'numeric', minute: '2-digit', hour12: r.hour12, }).format(date); } catch (e) { return String(date); } } // ICU renders SGD as a bare "$" in an en-SG locale, which is ambiguous next to // USD prices. Prices are written S$ throughout the product, so format the // number with Intl and put the symbol on ourselves rather than depending on // whichever ICU build the browser or Node happens to ship. function formatMoney(amount) { var r = region(); try { return 'S$' + new Intl.NumberFormat(r.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(amount); } catch (e) { return 'S$' + amount; } } global.UClawI18n = { locale: locale, supported: SUPPORTED, t: t, apply: apply, region: region, formatDate: formatDate, formatTime: formatTime, formatMoney: formatMoney, }; if (global.document.readyState === 'loading') { global.document.addEventListener('DOMContentLoaded', function () { apply(); }); } else { apply(); } })(window);