完善官网、管理后台与后端的多语言内容编辑和发布流程,调整站点页面及相关接口,并同步数据库脚本。 新增容器部署配置,改用环境变量注入数据库凭据;补充仓库忽略规则,并停止跟踪本地环境文件和 Maven 构建产物。
821 lines
28 KiB
JavaScript
821 lines
28 KiB
JavaScript
/**
|
||
* 门户多语言(主流 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: []
|
||
}
|
||
const LOCALE_LABEL_MAP = {
|
||
zh_cn: '中',
|
||
en_us: '英',
|
||
ja_jp: '日'
|
||
}
|
||
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()
|
||
}
|