287 lines
9.8 KiB
JavaScript
287 lines
9.8 KiB
JavaScript
(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();
|
||
}
|
||
})();
|