/** * 门户多语言(主流 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() }