huayang/frontend/src/api/language.js

822 lines
28 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.

/**
* 门户多语言(主流 i18n 标准实践)
* 1) 本地 JSON 作为基础文案
* 2) 后端词典按语言增量覆盖
* 3) 语言来源优先级:用户选择(localStorage) > 浏览器语言 > 默认语言
*/
import { ref } from 'vue'
import i18n, { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../i18n'
const STORAGE_KEY = 'huayang_lang'
const API_BASE = (import.meta.env.VITE_SITE_API_BASE || '/site').replace(/\/$/, '')
const FALLBACK_BY_LOCALE = {
en_us: ['zh_cn'],
ja_jp: ['en_us', 'zh_cn'],
zh_cn: []
}
/* 顶栏语言切换用英文缩写展示(ZH/EN/JA,与 locales 文件名短码一致),与 generated 页面模板内静态文案保持一致 */
const LOCALE_LABEL_MAP = {
zh_cn: 'ZH',
en_us: 'EN',
ja_jp: 'JA'
}
const HTML_LANG_MAP = {
zh_cn: 'zh-CN',
en_us: 'en-US',
ja_jp: 'ja-JP'
}
const currentLang = ref(DEFAULT_LOCALE)
const languageList = ref([])
const translationCache = {}
export function normalizeLangCode(input) {
const raw = String(input || '').trim().toLowerCase().replace('-', '_')
if (!raw) return DEFAULT_LOCALE
if (raw.startsWith('zh')) return 'zh_cn'
if (raw.startsWith('en')) return 'en_us'
if (raw.startsWith('ja') || raw.startsWith('jp')) return 'ja_jp'
return SUPPORTED_LOCALES.includes(raw) ? raw : DEFAULT_LOCALE
}
function toHtmlLang(locale) {
return HTML_LANG_MAP[normalizeLangCode(locale)] || 'zh-CN'
}
function getBrowserLocale() {
const languages = Array.isArray(navigator.languages) ? navigator.languages : []
if (languages.length > 0) return normalizeLangCode(languages[0])
return normalizeLangCode(navigator.language || DEFAULT_LOCALE)
}
function getLocalMessages(locale) {
try {
const msg = i18n.global.getLocaleMessage(locale)
return msg && typeof msg === 'object' ? msg : {}
} catch {
return {}
}
}
function mergeLocaleMessages(base, patch) {
if (!patch || typeof patch !== 'object') return { ...(base || {}) }
const out = { ...(base || {}) }
Object.keys(patch).forEach((k) => {
const pv = patch[k]
const bv = out[k]
if (
pv &&
typeof pv === 'object' &&
!Array.isArray(pv) &&
bv &&
typeof bv === 'object' &&
!Array.isArray(bv)
) {
out[k] = mergeLocaleMessages(bv, pv)
} else {
out[k] = pv
}
})
return out
}
function expandDotKeys(flatMap) {
const out = {}
if (!flatMap || typeof flatMap !== 'object') return out
Object.keys(flatMap).forEach((key) => {
const rawValue = flatMap[key]
if (!key) return
const parts = String(key).split('.').filter(Boolean)
if (!parts.length) return
let cur = out
for (let i = 0; i < parts.length; i += 1) {
const part = parts[i]
const isLast = i === parts.length - 1
if (isLast) {
cur[part] = rawValue
} else {
if (!cur[part] || typeof cur[part] !== 'object' || Array.isArray(cur[part])) {
cur[part] = {}
}
cur = cur[part]
}
}
})
return out
}
function resolveLangCode(item) {
return normalizeLangCode(item?.langCode || item?.lang_code || item?.code || '')
}
function resolveLangName(item) {
return item?.langName || item?.lang_name || item?.name || ''
}
function syncLocale(langCode) {
const locale = normalizeLangCode(langCode)
currentLang.value = locale
i18n.global.locale.value = locale
document.documentElement.lang = toHtmlLang(locale)
localStorage.setItem(STORAGE_KEY, locale)
}
async function fetchRemoteTranslationMap(langCode, options = {}) {
const forceRefresh = Boolean(options.forceRefresh)
const locale = normalizeLangCode(langCode)
if (!forceRefresh && translationCache[locale]) {
return translationCache[locale]
}
const res = await fetch(`${API_BASE}/i18n/${locale}`, { credentials: 'omit', cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const body = await res.json()
if (body.code !== 200) throw new Error(body.msg || 'load i18n failed')
const map = {}
const rows = Array.isArray(body.data) ? body.data : []
rows.forEach((row) => {
const key = row?.i18nKey || row?.i18n_key
const value = row?.i18nValue ?? row?.i18n_value
if (key && value != null && String(value).trim() !== '') {
map[key] = value
}
})
translationCache[locale] = map
return map
}
function applyLocaleMessagesWithFallback(locale) {
const norm = normalizeLangCode(locale)
let merged = { ...getLocalMessages(norm) }
const fallbackLocales = FALLBACK_BY_LOCALE[norm] || []
fallbackLocales.forEach((fbLocale) => {
merged = mergeLocaleMessages(getLocalMessages(fbLocale), merged)
})
const remote = translationCache[norm] || {}
merged = mergeLocaleMessages(merged, expandDotKeys(remote))
i18n.global.setLocaleMessage(norm, merged)
}
/**
* 获取语言列表(后端可配置启用语种)
*/
export async function fetchLanguageList() {
const res = await fetch(`${API_BASE}/language/list`, { credentials: 'omit', cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const body = await res.json()
if (body.code !== 200) throw new Error(body.msg || 'load language list failed')
const source = Array.isArray(body.data) ? body.data : []
languageList.value = source
.map((item) => {
const langCode = resolveLangCode(item)
return {
...item,
langCode,
langName: resolveLangName(item)
}
})
.filter((item) => SUPPORTED_LOCALES.includes(item.langCode))
if (!languageList.value.length) {
languageList.value = SUPPORTED_LOCALES.map((code) => ({ langCode: code, langName: LOCALE_LABEL_MAP[code] }))
}
return languageList.value
}
/**
* 加载远端翻译并合并到当前 locale message
*/
export async function fetchTranslations(langCode, options = {}) {
const locale = normalizeLangCode(langCode)
/* fetch 长期挂起会导致 runLanguageSwitch 内锁无法释放(若保留锁);此处限时放弃远端词典 */
const timeoutMs = 8000
try {
await Promise.race([
fetchRemoteTranslationMap(locale, options),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('i18n fetch timeout')), timeoutMs)
})
])
} catch {
/* 超时或接口失败:仅用本地 JSON + 已有缓存 */
}
applyLocaleMessagesWithFallback(locale)
return translationCache[locale]
}
/**
* 按 key 获取翻译
*/
export async function t(langCode, key) {
const locale = normalizeLangCode(langCode)
if (!translationCache[locale]) {
await fetchTranslations(locale)
}
const hit = i18n.global.t(key)
if (hit && hit !== key) return hit
return translationCache[locale]?.[key] || key
}
/**
* 设置当前语言
*/
export function setCurrentLang(langCode) {
syncLocale(langCode)
}
/**
* 初始化语言:URL ?lang= 优先(可分享链接),其次 localStorage,再次浏览器语言。
* 须在应用挂载前调用一次(main.js),避免 vue-i18n 默认 zh_cn 导致刷新后仍像「回到中文」。
*/
export function initLanguage() {
let fromUrl = ''
try {
if (typeof window !== 'undefined' && window.location?.search) {
const q = new URLSearchParams(window.location.search).get('lang')
if (q) fromUrl = String(q).trim()
}
} catch {
/* ignore */
}
const saved =
typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) || '' : ''
const initial = normalizeLangCode(fromUrl || saved || getBrowserLocale() || DEFAULT_LOCALE)
syncLocale(initial)
}
function syncLangQueryToUrl(locale) {
if (typeof window === 'undefined' || !window.history?.replaceState) return
try {
const u = new URL(window.location.href)
u.searchParams.set('lang', String(locale))
window.history.replaceState(window.history.state, '', u.toString())
} catch {
/* ignore */
}
}
/**
* 注入页面语言切换器
*/
export async function injectLanguageSwitcher() {
let languages = []
try {
languages = await fetchLanguageList()
} catch {
languages = SUPPORTED_LOCALES.map((code) => ({ langCode: code, langName: LOCALE_LABEL_MAP[code] }))
languageList.value = languages
}
/** 必须与 Logo 旁「微信公众号」容器 .lang 区分:优先顶栏 .topbar/.header 内的 .user01 */
const userTopContainer =
document.querySelector('.header .topbar .user01') ||
document.querySelector('.e_navigationA-12 .topbar .user01') ||
document.querySelector('.header .user01') ||
document.querySelector('.user01')
const legacyLangContainer =
document.querySelector('.header .topbar .lang') || document.querySelector('.topbar .lang')
const container = userTopContainer || legacyLangContainer
if (!container) return
container.innerHTML = ''
container.style.pointerEvents = 'auto'
container.style.position = container.style.position || 'relative'
container.style.zIndex = container.style.zIndex || '9999'
languages.forEach((item, index) => {
const code = resolveLangCode(item)
const span = document.createElement('span')
span.textContent = LOCALE_LABEL_MAP[code] || (resolveLangName(item).slice(0, 1) || code)
span.dataset.lang = code
span.setAttribute('role', 'button')
span.setAttribute('tabindex', '0')
span.style.cursor = 'pointer'
span.style.pointerEvents = 'auto'
if (currentLang.value === code) {
span.style.color = '#3b82f6'
span.style.fontWeight = 'bold'
}
container.appendChild(span)
if (index < languages.length - 1) container.appendChild(document.createTextNode(' | '))
})
hydrateUser01Accessibility()
bindNativeLanguageSwitcher(languages)
ensureLanguageSwitcherPointerHitFix()
}
/**
* 应用翻译到归档 DOM + locale
*/
export async function applyTranslations(langCode, options = {}) {
const forceRefresh = options.forceRefresh !== false
const locale = normalizeLangCode(langCode)
syncLocale(locale)
try {
await fetchTranslations(locale, { forceRefresh })
} catch {
applyLocaleMessagesWithFallback(locale)
}
document.querySelectorAll('[data-i18n]').forEach((el) => {
const key = el.getAttribute('data-i18n')
const value = i18n.global.t(key)
if (value && value !== key) el.textContent = value
})
document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
const key = el.getAttribute('data-i18n-placeholder')
const value = i18n.global.t(key)
if (value && value !== key) el.placeholder = value
})
applyCommonTranslations()
}
export { currentLang }
export function tr(key, fallback = '') {
const value = i18n.global.t(key)
if (value && value !== key) return value
if (fallback) return fallback
const locale = normalizeLangCode(currentLang.value || DEFAULT_LOCALE)
return translationCache[locale]?.[key] || key
}
/**
* 从节点解析语言码:data-lang、外链 host、或「中/英/日」文案(兼容 "| 英")
*/
function detectLangCodeFromNode(node) {
if (node.dataset && node.dataset.lang) {
return node.dataset.lang
}
const href = String(node.getAttribute('href') || '').toLowerCase()
if (href.includes('en.haihofoods') || href.includes('//en.')) {
return 'en_us'
}
if (href.includes('jp.haihofoods') || href.includes('//jp.')) {
return 'ja_jp'
}
if (href.includes('haihofoods.com') && !href.includes('en.haihofoods') && !href.includes('jp.haihofoods')) {
return 'zh_cn'
}
const t = String(node.textContent || '')
.replace(/\|/g, '')
.replace(/\s+/g, '')
.trim()
.toLowerCase()
if (t.includes('简体') || t.includes('中文') || t === '中') return 'zh_cn'
if (t.includes('english') || t.includes('en') || t === '英') return 'en_us'
if (t.includes('日本') || t.includes('ja') || t === '日') return 'ja_jp'
return ''
}
/** 任意页切换语言后须重灌页脚联系块与站点配置 */
async function refreshFooterAfterLangSwitch() {
try {
const { injectFooterContact } = await import('./contact.js')
const { injectFooterCopyright } = await import('./copyright.js')
const { injectSiteGlobalConfig } = await import('./siteConfig.js')
await injectFooterContact()
await injectSiteGlobalConfig()
await injectFooterCopyright()
} catch (err) {
console.warn('[language] footer refresh failed', err)
}
}
async function runLanguageSwitch(code) {
const locale = normalizeLangCode(code)
if (!SUPPORTED_LOCALES.includes(locale)) return
setCurrentLang(locale)
syncLangQueryToUrl(locale)
await applyTranslations(locale)
await injectLanguageSwitcher()
document.dispatchEvent(new CustomEvent('langchange', { detail: { lang: locale } }))
await refreshFooterAfterLangSwitch()
}
function collectLangHitFromPoint(clientX, clientY) {
if (typeof document.elementsFromPoint !== 'function') return null
const stack = document.elementsFromPoint(clientX, clientY)
if (!stack?.length) return null
const langHit = stack.find(
(node) =>
node &&
node.nodeType === 1 &&
typeof node.closest === 'function' &&
node.getAttribute?.('data-lang') &&
node.closest('.user01')
)
if (!langHit) return null
const top = stack[0]
if (top === langHit || langHit.contains(top)) return null
const code = normalizeLangCode(langHit.getAttribute('data-lang'))
if (!SUPPORTED_LOCALES.includes(code)) return null
return code
}
/** 顶栏语言常被正文通栏、Teleport、透明层盖住;捕获阶段 + 命中栈穿透 */
let languagePointerHitFixBound = false
function ensureLanguageSwitcherPointerHitFix() {
if (languagePointerHitFixBound || typeof document.elementsFromPoint !== 'function') return
languagePointerHitFixBound = true
const maybeSwitch = (e, clientX, clientY) => {
if (e.defaultPrevented) return
if (typeof clientX !== 'number' || typeof clientY !== 'number') return
const code = collectLangHitFromPoint(clientX, clientY)
if (!code) return
e.preventDefault()
e.stopImmediatePropagation()
void runLanguageSwitch(code)
}
document.addEventListener(
'click',
(e) => {
maybeSwitch(e, e.clientX, e.clientY)
},
true
)
}
/** 点击逻辑见 ensureUser01LanguageDelegation(document 捕获委托,不依赖每次 inject 绑 onclick) */
function hydrateUser01Accessibility() {
document.querySelectorAll('.user01 [data-lang]').forEach((node) => {
node.style.cursor = 'pointer'
if (!node.getAttribute('role')) node.setAttribute('role', 'button')
if (node.getAttribute('tabindex') == null) node.setAttribute('tabindex', '0')
})
}
/** 顶栏中英日:归档页常有透明层盖住 span,单靠各节点 onclick 不可靠;与 collectLangHitFromPoint 穿透配合 */
let user01DelegationBound = false
function ensureUser01LanguageDelegation() {
if (user01DelegationBound) return
user01DelegationBound = true
document.addEventListener(
'click',
(e) => {
const el = e.target?.closest?.('.user01 [data-lang]')
if (!el) return
const code = normalizeLangCode(el.getAttribute('data-lang'))
if (!SUPPORTED_LOCALES.includes(code)) return
e.preventDefault()
e.stopPropagation()
void runLanguageSwitch(code)
},
true
)
document.addEventListener(
'keydown',
(e) => {
if (e.key !== 'Enter' && e.key !== ' ') return
const el = e.target?.closest?.('.user01 [data-lang]')
if (!el) return
const code = normalizeLangCode(el.getAttribute('data-lang'))
if (!SUPPORTED_LOCALES.includes(code)) return
e.preventDefault()
void runLanguageSwitch(code)
},
true
)
}
ensureUser01LanguageDelegation()
function bindNativeLanguageSwitcher(languages) {
const languageNodes = Array.from(document.querySelectorAll('.lang span, .lang a, .lang li, .lang option'))
if (!languageNodes.length) return
const supportedCodes = new Set(languages.map((l) => resolveLangCode(l)).filter(Boolean))
languageNodes.forEach((node) => {
const code = detectLangCodeFromNode(node)
if (!code || !supportedCodes.has(code)) return
node.style.cursor = 'pointer'
node.addEventListener('click', async (event) => {
event.preventDefault()
event.stopPropagation()
await runLanguageSwitch(code)
})
})
}
function setTextBySelectors(selectors, value) {
if (!value) return
for (const selector of selectors) {
const el = document.querySelector(selector)
if (el) {
el.textContent = value
return
}
}
}
function setAllTextBySelectors(selectors, value) {
if (!value) return
selectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((el) => {
if (el) el.textContent = value
})
})
}
function setPlaceholderBySelectors(selectors, value) {
if (!value) return
selectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((el) => {
if (el && 'placeholder' in el) {
el.placeholder = value
}
})
})
}
function applyCommonTranslations() {
const safeT = (key) => {
const v = i18n.global.t(key)
return v && v !== key ? v : ''
}
// 顶部主导航(与后台一级菜单顺序一致:首页、关于、产品、文化、联系)
setTextBySelectors(['.header .p_level1Item:nth-child(1) .p_menuItem span'], safeT('common.title.site_home'))
setTextBySelectors(['.header .p_level1Item:nth-child(2) .p_menuItem span'], safeT('common.title.about'))
setTextBySelectors(['.header .p_level1Item:nth-child(3) .p_menuItem span'], safeT('common.title.product'))
setTextBySelectors(['.header .p_level1Item:nth-child(4) .p_menuItem span'], safeT('common.title.culture_nav'))
setTextBySelectors(
[
'.header .navitem .p_level1Item:nth-child(5) .p_menuItem span',
'.header .p_level1Item:nth-child(5) .p_menuItem span'
],
safeT('common.title.contact')
)
/** 华阳文化归档页:顶部导航、移动折叠菜单与 Banner 标题均来自静态模板,需要单独覆盖所有副本。 */
const cultureNavText = safeT('common.title.culture_nav')
if (cultureNavText) {
setAllTextBySelectors(
[
'body[data-pagekey="culture"] .header .p_level1Box > .navitem > .p_level1Item:nth-child(4) > .p_menuItem > a > span',
'body[data-pagekey="culture"] .header .p_level1Item.actived > .p_menuItem.cntitle > a > span',
'body[data-pagekey="culture"] .header .p_level1Item.open > .p_menuItem.s_templatetitle > a > span',
'body[data-pagekey="culture"] .e_bottomNav-47 .p_level1Item:nth-child(4) > .p_menuItem span'
],
cultureNavText
)
document.querySelectorAll('body[data-pagekey="culture"] .e_bannerA-1 .p_btitle').forEach((titleEl) => {
const link = titleEl.querySelector('a')
if (link) link.textContent = cultureNavText
else titleEl.textContent = cultureNavText
})
}
// 关于我们二级导航
setTextBySelectors(['.header .p_level1Item:nth-child(2) .p_level2Item:nth-child(1) .p_menuItem span'], safeT('common.title.company_intro'))
setTextBySelectors(['.header .p_level1Item:nth-child(2) .p_level2Item:nth-child(2) .p_menuItem span'], safeT('common.title.honor'))
setTextBySelectors(['.header .p_level1Item:nth-child(2) .p_level2Item:nth-child(3) .p_menuItem span'], safeT('common.title.timeline'))
setTextBySelectors(['.header .p_level1Item:nth-child(2) .p_level2Item:nth-child(4) .p_menuItem span'], safeT('common.title.store'))
/** 首页各屏「了解更多 >」按钮(归档为多套 .e_button-*;injectHomeContent 仅覆盖其中一处) */
if (cultureNavText) {
const homeCultureTitle = safeT('home.section4.main.title') || cultureNavText
document.querySelectorAll('body[data-pagekey="home"] [id^="c_static_001_P_33409-"] .e_text-28').forEach((titleEl) => {
const link = titleEl.querySelector('a')
if (link) link.textContent = homeCultureTitle
else titleEl.textContent = homeCultureTitle
})
}
const learnMoreLabel = safeT('home.section3.more')
if (learnMoreLabel) {
document
.querySelectorAll(
'body[data-pagekey="home"] .e_button-62 span, body[data-pagekey="home"] .e_button-17 span, body[data-pagekey="home"] .e_button-29 span'
)
.forEach((span) => {
const raw = (span.textContent || '').replace(/\s+/g, ' ').trim()
if (
raw === '了解更多 >' ||
raw === '了解更多>' ||
raw === 'Learn more >' ||
raw === 'Learn More >' ||
raw === '詳しく見る >'
) {
span.textContent = learnMoreLabel
}
})
}
/** 联系我们页:面包屑、右侧「联系我们」标题、左侧「在线留言」与说明 */
const contactCrumbUl = document.querySelector(
'body[data-pagekey="contact"] .e_breadcrumb-4 ul.p_breadcrumb'
)
if (contactCrumbUl) {
const crumbItems = contactCrumbUl.querySelectorAll(':scope > li.p_breadcrumbItem')
const homeCrumbTitle = crumbItems[0]?.querySelector('.p_title')
const homeText = safeT('common.title.site_home')
if (homeCrumbTitle && homeText) homeCrumbTitle.textContent = homeText
const contactCrumb = safeT('common.title.contact')
if (crumbItems[1] && contactCrumb) crumbItems[1].textContent = contactCrumb
}
const contactSectionRoot = 'body[data-pagekey="contact"] #c_static_001-1669701780184'
setTextBySelectors(
[`${contactSectionRoot} .cbox-19-0 .e_text-17.s_summary`],
safeT('common.title.contact')
)
setTextBySelectors(
[`${contactSectionRoot} .cbox-2-0 .e_text-3.s_summary`],
safeT('contact.page.online_title')
)
const onlineHintEl = document.querySelector(`${contactSectionRoot} .cbox-2-0 .e_richText-4 p`)
const onlineHintText = safeT('contact.page.online_hint')
if (onlineHintEl && onlineHintText) onlineHintEl.textContent = onlineHintText
/**
* 归档页「关于我们」模块顶部的横向 Tab(公司简介 / 资质荣誉 / 品质技术…)
* 与页头 .header 主导航不是同一套 DOM,切换语言时必须单独替换文案。
*/
const aboutStripList = document.querySelector(
'#c_static_001_P_8883-17121276215300 .e_loop-3 .p_list'
)
if (aboutStripList) {
const tabKeys = [
'common.title.company_intro',
'common.title.honor',
'common.title.timeline',
'common.title.store'
]
const tabAnchors = aboutStripList.querySelectorAll(':scope > .p_loopitem a')
tabKeys.forEach((key, i) => {
const a = tabAnchors[i]
if (!a) return
const text = safeT(key)
if (text) a.textContent = text
})
}
/** 同上模块内的面包屑第二项随归档子页变化:关于我们 / 发展历程 / 品质技术 / 店面展示 */
const aboutCrumbUl = document.querySelector(
'#c_static_001_P_8883-17121276215300 .e_breadcrumb-2 ul.p_breadcrumb'
)
if (aboutCrumbUl) {
const crumbItems = aboutCrumbUl.querySelectorAll(':scope > li.p_breadcrumbItem')
const homeCrumbTitle = crumbItems[0]?.querySelector('.p_title')
const homeText = safeT('common.title.site_home')
if (homeCrumbTitle && homeText) homeCrumbTitle.textContent = homeText
const pageKey = typeof document !== 'undefined' ? document.body?.getAttribute('data-pagekey') || '' : ''
let secondCrumbKey = 'common.title.about'
if (pageKey === 'history') secondCrumbKey = 'common.title.timeline'
else if (pageKey === 'store') secondCrumbKey = 'common.title.store'
const secondText = safeT(secondCrumbKey)
if (crumbItems[1] && secondText) crumbItems[1].textContent = secondText
}
/** 发展历程归档页主标题 */
if (typeof document !== 'undefined' && document.body?.getAttribute('data-pagekey') === 'history') {
const timelineH1 = document.querySelector(
'#c_static_001_P_4578-1660723020064 > .e_container-5:first-child h1.e_h1-6'
)
const timelineTitle = safeT('common.title.timeline')
if (timelineH1 && timelineTitle) timelineH1.textContent = timelineTitle
}
/** 全国门店页左侧列表上方标题「店面位置」 */
setTextBySelectors(
[
'body[data-pagekey="store"] #c_introl_list_003-1753691901047 .e_text-25.s_title',
'body[data-pagekey="store"] .e_container-23 .cbox-23-0 .e_text-25.s_title'
],
safeT('common.title.store_location')
)
/** 全国门店页底部图集区块标题「店面展示」(该页仅此一处 .e_text-14) */
setTextBySelectors(
['body[data-pagekey="store"] #c_static_001_P_4578-16941414442160 .e_text-14.s_title'],
safeT('common.title.store')
)
/** 关于我们页「资质荣誉」轮播区块主标题(同 id 在全国门店页为「店面展示」的 p,此处限定 about + h1) */
setTextBySelectors(
[
'body[data-pagekey="about"] #c_static_001_P_4578-16941414442160 > .e_container-5:first-child .cbox-5-0 > h1.e_h1-6'
],
safeT('common.title.honor')
)
// 底部导航一级(与归档 footer 顺序一致:首页、关于、产品、文化、商城、联系)
setTextBySelectors(['.e_bottomNav-47 .p_level1Item:nth-child(1) .p_menuItem span'], safeT('common.title.site_home'))
setTextBySelectors(['.e_bottomNav-47 .p_level1Item:nth-child(2) .p_menuItem span'], safeT('common.title.about'))
setTextBySelectors(['.e_bottomNav-47 .p_level1Item:nth-child(3) .p_menuItem span'], safeT('common.title.product'))
setTextBySelectors(['.e_bottomNav-47 .p_level1Item:nth-child(4) .p_menuItem span'], safeT('common.title.culture_nav'))
setTextBySelectors(['.e_bottomNav-47 .p_level1Item:nth-child(5) .p_menuItem span'], safeT('common.title.contact'))
// 常见按钮(在线留言表单使用「提交留言」,勿与通用「提交」混用)
document.querySelectorAll('.e_formBtn-16, .e_formBtn-96').forEach((btn) => {
if (btn.closest?.('.e_form-10')) {
const msg = safeT('ui.contact.submit_message')
if (msg) btn.textContent = msg
return
}
const text = safeT('common.button.submit')
if (text) {
btn.textContent = text
}
})
// 常见输入框占位符(中英日归档占位均需命中)
setPlaceholderBySelectors(
[
'input[placeholder*="姓名"]',
'input[placeholder*="联系人"]',
'input[placeholder*="Your name"]',
'input[placeholder*="your name"]',
'input[placeholder*="お名前"]'
],
safeT('common.placeholder.name')
)
setPlaceholderBySelectors(
[
'input[placeholder*="电话"]',
'input[placeholder*="手机"]',
'input[placeholder*="联系方式"]',
'input[placeholder*="Phone"]',
'input[placeholder*="phone"]',
'input[placeholder*="電話"]'
],
safeT('common.placeholder.phone')
)
setPlaceholderBySelectors(
[
'input[placeholder*="邮箱"]',
'input[placeholder*="email"]',
'input[placeholder*="Email"]',
'input[placeholder*="メール"]'
],
safeT('common.placeholder.email')
)
setPlaceholderBySelectors(
[
'textarea[placeholder*="留言"]',
'input[placeholder*="需求"]',
'textarea[placeholder*="内容"]',
'textarea[placeholder*="message"]',
'textarea[placeholder*="Message"]',
'textarea[placeholder*="メッセージ"]'
],
safeT('common.placeholder.message')
)
// 页脚「关于我们」下「发展历程」及上轮切换后的英文/日文占位
const timelineFoot = safeT('common.title.timeline')
if (timelineFoot) {
document.querySelectorAll('.p_level2Box .p_menuItem > a > span').forEach((span) => {
const raw = (span.textContent || '').replace(/\s+/g, ' ').trim()
if (raw === '发展历程' || raw === 'Timeline' || raw === '沿革') {
span.textContent = timelineFoot
}
})
}
// 页脚「关于我们」下「资质荣誉」及上轮占位(含旧版 en Honors / ja 認証・受賞)
const honorFoot = safeT('common.title.honor')
if (honorFoot) {
document.querySelectorAll('.p_level2Box .p_menuItem > a > span').forEach((span) => {
const raw = (span.textContent || '').replace(/\s+/g, ' ').trim()
if (
raw === '资质荣誉' ||
raw === 'Honors' ||
raw === 'Qualifications & Honors' ||
raw === '認証・受賞' ||
raw === '資格・栄誉'
) {
span.textContent = honorFoot
}
})
}
// 页脚「关于我们」下「店面展示」及上轮语言缓存(含旧版 en Stores / ja 店舗)
const storeFoot = safeT('common.title.store')
if (storeFoot) {
document.querySelectorAll('.p_level2Box .p_menuItem > a > span').forEach((span) => {
const raw = (span.textContent || '').replace(/\s+/g, ' ').trim()
if (
raw === '店面展示' ||
raw === 'Stores' ||
raw === 'Store Showcase' ||
raw === '店舗' ||
raw === '店舗展示'
) {
span.textContent = storeFoot
}
})
}
}
/**
* 归档页整段 HTML 经 bootstrapLegacyPage 重挂后,静态模板会还原为中文;再执行一遍 DOM 文案替换。
* runLanguageSwitch 里 applyTranslations 先于 langchange → bootstrap,故需在重挂后补调用。
*/
export function reapplyArchiveStaticTranslations() {
applyCommonTranslations()
}