hyapp-h5/common/i18n.js
2026-07-12 17:38:53 +08:00

287 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
var STORAGE_KEY = 'hyapp_h5_lang';
var DEFAULT_LANG = 'en';
var SUPPORTED = ['en', 'ar', 'tr', 'es', 'zh', 'id'];
var LANG_QUERY_KEYS = [
'language',
'lang',
'locale',
'app_lang',
'appLanguage',
];
var scriptURL =
document.currentScript && document.currentScript.src
? document.currentScript.src
: '../common/i18n.js';
var localeBaseURL = new URL('locales/', scriptURL).toString();
var messages = {};
var currentLang = '';
function scopedSupported() {
var scoped =
window.HyAppI18nSupported || window.HyAppSupportedLanguages || null;
if (!Array.isArray(scoped)) return SUPPORTED;
var normalized = [];
scoped.forEach(function (value) {
var lang = String(value || '')
.trim()
.toLowerCase()
.replace(/_/g, '-');
if (lang.indexOf('en') === 0) lang = 'en';
else if (lang.indexOf('ar') === 0) lang = 'ar';
else if (lang.indexOf('tr') === 0) lang = 'tr';
else if (lang.indexOf('es') === 0) lang = 'es';
else if (lang.indexOf('zh') === 0) lang = 'zh';
else if (lang.indexOf('id') === 0) lang = 'id';
if (SUPPORTED.indexOf(lang) >= 0 && normalized.indexOf(lang) < 0) {
normalized.push(lang);
}
});
return normalized.length ? normalized : SUPPORTED;
}
function storageGet(key) {
try {
return window.localStorage ? window.localStorage.getItem(key) : '';
} catch (error) {
return '';
}
}
function storageSet(key, value) {
try {
if (window.localStorage) {
window.localStorage.setItem(key, value);
}
} catch (error) {
// Some in-app browser contexts disable localStorage. Language still works for the current page session.
}
}
function currentParams() {
var parsed =
window.HyAppParams &&
window.HyAppParams.current &&
window.HyAppParams.current.raw;
if (parsed && parsed.get) return parsed;
var params = new URLSearchParams(window.location.search);
var hash = window.location.hash || '';
if (hash.indexOf('?') >= 0) {
new URLSearchParams(hash.split('?').slice(1).join('?')).forEach(
function (value, key) {
if (!params.has(key)) params.set(key, value);
}
);
}
return params;
}
function queryLang() {
var params = currentParams();
for (var i = 0; i < LANG_QUERY_KEYS.length; i += 1) {
var value = params.get(LANG_QUERY_KEYS[i]);
if (value) return value;
}
return '';
}
function normalizeLang(value) {
var lang = String(value || '')
.trim()
.toLowerCase()
.replace(/_/g, '-');
if (!lang) return '';
if (lang.indexOf(',') >= 0) lang = lang.split(',')[0];
if (lang.indexOf(';') >= 0) lang = lang.split(';')[0];
lang = lang.trim();
if (lang.indexOf('en') === 0) lang = 'en';
else if (lang.indexOf('ar') === 0) lang = 'ar';
else if (lang.indexOf('tr') === 0) lang = 'tr';
else if (lang.indexOf('es') === 0) lang = 'es';
else if (lang.indexOf('zh') === 0) lang = 'zh';
else if (lang.indexOf('id') === 0) lang = 'id';
return scopedSupported().indexOf(lang) >= 0 ? lang : '';
}
function resolveLang() {
var requestedLang = queryLang();
if (requestedLang) return normalizeLang(requestedLang) || DEFAULT_LANG;
return DEFAULT_LANG;
}
function localeSources() {
var sources = [localeBaseURL];
var extras = Array.isArray(window.HyAppI18nExtraSources)
? window.HyAppI18nExtraSources
: [];
extras.forEach(function (source) {
var normalized = String(source || '').trim();
if (!normalized) return;
if (normalized.charAt(normalized.length - 1) !== '/') {
normalized += '/';
}
if (sources.indexOf(normalized) < 0) sources.push(normalized);
});
return sources;
}
function fetchLocale(source, lang) {
return window
.fetch(source + lang + '.json', { cache: 'no-store' })
.then(function (response) {
if (!response.ok) throw new Error('locale_not_found');
return response.json();
})
.catch(function () {
// 产品语言包是可选叠加层;单个包发布延迟不能让 common 基础文案也一起失效。
return null;
});
}
function load(lang) {
var normalized = normalizeLang(lang) || DEFAULT_LANG;
return Promise.all(
localeSources().map(function (source) {
return fetchLocale(source, normalized);
})
)
.then(function (localeMessages) {
var found = false;
messages = {};
localeMessages.forEach(function (sourceMessages) {
if (!sourceMessages) return;
found = true;
// 产品包后加载并覆盖同名 key允许品牌化文案但 common 仍提供跨页面兜底。
Object.assign(messages, sourceMessages);
});
if (!found) throw new Error('locale_not_found');
currentLang = normalized;
storageSet(STORAGE_KEY, currentLang);
apply();
return messages;
})
.catch(function () {
if (normalized === DEFAULT_LANG) {
messages = {};
currentLang = DEFAULT_LANG;
apply();
return messages;
}
return load(DEFAULT_LANG);
});
}
function t(key, fallback) {
return Object.prototype.hasOwnProperty.call(messages, key)
? messages[key]
: fallback || key;
}
function apply(root) {
var scope = root || document;
document.documentElement.lang = currentLang || DEFAULT_LANG;
document.documentElement.dir = currentLang === 'ar' ? 'rtl' : 'ltr';
scope.querySelectorAll('[data-i18n]').forEach(function (node) {
node.textContent = t(
node.getAttribute('data-i18n'),
node.textContent
);
});
scope
.querySelectorAll('[data-i18n-placeholder]')
.forEach(function (node) {
node.setAttribute(
'placeholder',
t(
node.getAttribute('data-i18n-placeholder'),
node.getAttribute('placeholder') || ''
)
);
});
scope.querySelectorAll('[data-i18n-aria]').forEach(function (node) {
node.setAttribute(
'aria-label',
t(
node.getAttribute('data-i18n-aria'),
node.getAttribute('aria-label') || ''
)
);
});
scope.querySelectorAll('[data-lang-option]').forEach(function (node) {
node.classList.toggle(
'is-active',
node.getAttribute('data-lang-option') === currentLang
);
});
scope.querySelectorAll('[data-current-lang]').forEach(function (node) {
node.textContent = (currentLang || DEFAULT_LANG).toUpperCase();
});
window.dispatchEvent(
new CustomEvent('hyapp:i18n-ready', {
detail: { lang: currentLang, messages: messages },
})
);
}
function initLanguageMenu() {
document.addEventListener('click', function (event) {
var toggle = closest(event.target, '[data-language-toggle]');
var menu = document.querySelector('[data-language-menu]');
if (toggle && menu) {
var expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute(
'aria-expanded',
expanded ? 'false' : 'true'
);
menu.hidden = expanded;
return;
}
var option = closest(event.target, '[data-lang-option]');
if (option) {
if (menu) menu.hidden = true;
var button = document.querySelector('[data-language-toggle]');
if (button) button.setAttribute('aria-expanded', 'false');
load(option.getAttribute('data-lang-option'));
return;
}
if (menu && !closest(event.target, '.language-switcher')) {
menu.hidden = true;
var languageButton = document.querySelector(
'[data-language-toggle]'
);
if (languageButton)
languageButton.setAttribute('aria-expanded', 'false');
}
});
}
function closest(node, selector) {
while (node && node !== document) {
if (node.matches && node.matches(selector)) return node;
node = node.parentNode;
}
return null;
}
window.HyAppI18n = {
load: load,
apply: apply,
t: t,
lang: function () {
return currentLang || DEFAULT_LANG;
},
supported: scopedSupported(),
};
function init() {
initLanguageMenu();
load(resolveLang());
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();