add i18n
This commit is contained in:
367
app.js
Normal file
367
app.js
Normal file
@@ -0,0 +1,367 @@
|
||||
import { pins, sites } from "./data.js"
|
||||
import {
|
||||
getDesignThemeLabel,
|
||||
getInitialLanguage,
|
||||
LANGUAGE_KEY,
|
||||
LANGUAGE_NAMES,
|
||||
SUPPORTED_LANGUAGES,
|
||||
t,
|
||||
} from "./i18n.js"
|
||||
import { renderSites } from "./render.js"
|
||||
import {
|
||||
getCurrentDesignTheme,
|
||||
getInitialDesignTheme,
|
||||
getInitialTheme,
|
||||
setDesignTheme,
|
||||
setDesignThemeMenuOpen,
|
||||
setSelectedDesignThemeUI,
|
||||
setTheme,
|
||||
toggleTheme,
|
||||
updateDesignThemeOptions,
|
||||
} from "./theme.js"
|
||||
|
||||
export function initApp() {
|
||||
const CAT_ICON = "/icons/noto--cat-face.svg"
|
||||
const themeToggle = document.getElementById("themeToggle")
|
||||
const designThemeButton = document.getElementById("designThemeButton")
|
||||
const designThemeList = document.getElementById("designThemeList")
|
||||
const languageButton = document.getElementById("languageButton")
|
||||
const languageList = document.getElementById("languageList")
|
||||
const titleEl = document.querySelector(".title")
|
||||
const subtitleEl = document.querySelector(".subtitle")
|
||||
const designThemeLabelEl = document.querySelector(
|
||||
'[data-i18n="designThemeLabel"]',
|
||||
)
|
||||
const languageLabelEl = document.querySelector('[data-i18n="languageLabel"]')
|
||||
const moonIcon = document.querySelector(".theme-icon-moon")
|
||||
const sunIcon = document.querySelector(".theme-icon-sun")
|
||||
const sitesContainer = document.querySelector("#sites")
|
||||
const faviconEl = document.querySelector('link[rel~="icon"]')
|
||||
const beianIcpEl = document.querySelector('[data-i18n="beianIcp"]')
|
||||
const beianMpsEl = document.querySelector('[data-i18n="beianMps"]')
|
||||
|
||||
let currentLanguage = getInitialLanguage()
|
||||
|
||||
const getThemeLabel = (designTheme, language = currentLanguage) =>
|
||||
getDesignThemeLabel(designTheme, language)
|
||||
|
||||
function setSelectedLanguageUI(language) {
|
||||
if (!languageList) return
|
||||
const options = [...languageList.querySelectorAll('[role="option"]')]
|
||||
options.forEach((el) => {
|
||||
const value = el.getAttribute("data-value")
|
||||
const label = LANGUAGE_NAMES[value] || value || ""
|
||||
el.setAttribute("aria-selected", value === language ? "true" : "false")
|
||||
el.textContent = label
|
||||
})
|
||||
if (languageButton) {
|
||||
languageButton.textContent = LANGUAGE_NAMES[language] || language
|
||||
}
|
||||
}
|
||||
|
||||
function setLanguageMenuOpen(open) {
|
||||
if (!languageButton || !languageList) return
|
||||
languageButton.setAttribute("aria-expanded", open ? "true" : "false")
|
||||
languageList.hidden = !open
|
||||
if (open) {
|
||||
languageList.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubtitle(language = currentLanguage) {
|
||||
if (!subtitleEl) return
|
||||
if (pins.length) {
|
||||
subtitleEl.textContent = t("pinnedSubtitle", language)
|
||||
} else {
|
||||
subtitleEl.textContent = subtitleEl.dataset.text || ""
|
||||
}
|
||||
}
|
||||
|
||||
function getDocumentLang(language) {
|
||||
if (language === "zh-Hant") return "zh-Hant"
|
||||
if (language === "zh-Hans") return "zh-Hans"
|
||||
if (language === "ja") return "ja"
|
||||
if (language === "ko") return "ko"
|
||||
return "en"
|
||||
}
|
||||
|
||||
function setSwappableIcon(element, language) {
|
||||
if (!element) return
|
||||
if (!element.dataset.defaultSrc) {
|
||||
element.dataset.defaultSrc = element.getAttribute("src") || ""
|
||||
}
|
||||
element.setAttribute(
|
||||
"src",
|
||||
language === "meow" ? CAT_ICON : element.dataset.defaultSrc,
|
||||
)
|
||||
}
|
||||
|
||||
function setFavicon(language) {
|
||||
if (!faviconEl) return
|
||||
if (!faviconEl.dataset.defaultHref) {
|
||||
faviconEl.dataset.defaultHref = faviconEl.getAttribute("href") || ""
|
||||
}
|
||||
faviconEl.setAttribute(
|
||||
"href",
|
||||
language === "meow" ? CAT_ICON : faviconEl.dataset.defaultHref,
|
||||
)
|
||||
}
|
||||
|
||||
function applyTranslations() {
|
||||
const language = currentLanguage
|
||||
document.documentElement.setAttribute("lang", getDocumentLang(language))
|
||||
document.title = t("appTitle", language)
|
||||
if (titleEl) {
|
||||
const titleText = t("appTitle", language)
|
||||
titleEl.textContent = titleText
|
||||
titleEl.dataset.text = titleText
|
||||
}
|
||||
if (designThemeLabelEl) {
|
||||
designThemeLabelEl.textContent = t("designThemeLabel", language)
|
||||
}
|
||||
if (languageLabelEl) {
|
||||
languageLabelEl.textContent = t("languageLabel", language)
|
||||
}
|
||||
if (beianIcpEl) {
|
||||
beianIcpEl.textContent = t("beianIcp", language)
|
||||
}
|
||||
if (beianMpsEl) {
|
||||
beianMpsEl.textContent = t("beianMps", language)
|
||||
}
|
||||
if (designThemeButton) {
|
||||
designThemeButton.setAttribute(
|
||||
"aria-label",
|
||||
t("designThemeLabel", language),
|
||||
)
|
||||
}
|
||||
if (languageButton) {
|
||||
languageButton.setAttribute("aria-label", t("languageLabel", language))
|
||||
}
|
||||
if (themeToggle) {
|
||||
themeToggle.setAttribute("aria-label", t("themeToggleLabel", language))
|
||||
themeToggle.setAttribute("title", t("themeToggleTitle", language))
|
||||
}
|
||||
if (moonIcon) {
|
||||
moonIcon.setAttribute("alt", t("moonAlt", language))
|
||||
}
|
||||
if (sunIcon) {
|
||||
sunIcon.setAttribute("alt", t("sunAlt", language))
|
||||
}
|
||||
setSwappableIcon(moonIcon, language)
|
||||
setSwappableIcon(sunIcon, language)
|
||||
setFavicon(language)
|
||||
updateDesignThemeOptions({
|
||||
designThemeList,
|
||||
getLabel: getThemeLabel,
|
||||
language,
|
||||
})
|
||||
setSelectedDesignThemeUI({
|
||||
designThemeList,
|
||||
designThemeButton,
|
||||
designTheme: getCurrentDesignTheme(),
|
||||
getLabel: getThemeLabel,
|
||||
})
|
||||
setSelectedLanguageUI(language)
|
||||
updateSubtitle(language)
|
||||
renderSites({ container: sitesContainer, sites, pins, language })
|
||||
}
|
||||
|
||||
function setLanguage(language) {
|
||||
const safeLanguage = SUPPORTED_LANGUAGES.includes(language)
|
||||
? language
|
||||
: "zh-Hans"
|
||||
currentLanguage = safeLanguage
|
||||
localStorage.setItem(LANGUAGE_KEY, safeLanguage)
|
||||
applyTranslations()
|
||||
}
|
||||
|
||||
if (titleEl && !titleEl.dataset.text) {
|
||||
titleEl.dataset.text = titleEl.textContent?.trim() || ""
|
||||
}
|
||||
|
||||
if (subtitleEl && !subtitleEl.dataset.text) {
|
||||
subtitleEl.dataset.text = subtitleEl.textContent?.trim() || ""
|
||||
}
|
||||
|
||||
const initialTheme = getInitialTheme()
|
||||
setTheme(initialTheme)
|
||||
|
||||
const initialDesignTheme = getInitialDesignTheme()
|
||||
setDesignTheme(initialDesignTheme, themeToggle)
|
||||
setSelectedDesignThemeUI({
|
||||
designThemeList,
|
||||
designThemeButton,
|
||||
designTheme: initialDesignTheme,
|
||||
getLabel: getThemeLabel,
|
||||
})
|
||||
setDesignThemeMenuOpen({
|
||||
designThemeButton,
|
||||
designThemeList,
|
||||
open: false,
|
||||
})
|
||||
setLanguage(currentLanguage)
|
||||
|
||||
if (designThemeButton && designThemeList) {
|
||||
designThemeButton.addEventListener("click", () => {
|
||||
const isOpen = designThemeButton.getAttribute("aria-expanded") === "true"
|
||||
setDesignThemeMenuOpen({
|
||||
designThemeButton,
|
||||
designThemeList,
|
||||
open: !isOpen,
|
||||
})
|
||||
})
|
||||
|
||||
designThemeList.addEventListener("click", (e) => {
|
||||
const option = e.target.closest?.('[role="option"][data-value]')
|
||||
if (!option) return
|
||||
const value = option.getAttribute("data-value")
|
||||
setDesignTheme(value, themeToggle)
|
||||
setSelectedDesignThemeUI({
|
||||
designThemeList,
|
||||
designThemeButton,
|
||||
designTheme: value,
|
||||
getLabel: getThemeLabel,
|
||||
})
|
||||
setDesignThemeMenuOpen({
|
||||
designThemeButton,
|
||||
designThemeList,
|
||||
open: false,
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!designThemeButton || !designThemeList) return
|
||||
const clickedInside =
|
||||
designThemeButton.contains(e.target) ||
|
||||
designThemeList.contains(e.target)
|
||||
if (!clickedInside) {
|
||||
setDesignThemeMenuOpen({
|
||||
designThemeButton,
|
||||
designThemeList,
|
||||
open: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const isOpen = designThemeButton.getAttribute("aria-expanded") === "true"
|
||||
if (!isOpen) return
|
||||
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setDesignThemeMenuOpen({
|
||||
designThemeButton,
|
||||
designThemeList,
|
||||
open: false,
|
||||
})
|
||||
designThemeButton.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const options = [...designThemeList.querySelectorAll('[role="option"]')]
|
||||
if (!options.length) return
|
||||
const current = getCurrentDesignTheme()
|
||||
const currentIndex = Math.max(
|
||||
0,
|
||||
options.findIndex((el) => el.getAttribute("data-value") === current),
|
||||
)
|
||||
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
const delta = e.key === "ArrowDown" ? 1 : -1
|
||||
const nextIndex =
|
||||
(currentIndex + delta + options.length) % options.length
|
||||
const nextValue = options[nextIndex].getAttribute("data-value")
|
||||
setDesignTheme(nextValue, themeToggle)
|
||||
setSelectedDesignThemeUI({
|
||||
designThemeList,
|
||||
designThemeButton,
|
||||
designTheme: nextValue,
|
||||
getLabel: getThemeLabel,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
setDesignThemeMenuOpen({
|
||||
designThemeButton,
|
||||
designThemeList,
|
||||
open: false,
|
||||
})
|
||||
designThemeButton.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (languageButton && languageList) {
|
||||
languageButton.addEventListener("click", () => {
|
||||
const isOpen = languageButton.getAttribute("aria-expanded") === "true"
|
||||
setLanguageMenuOpen(!isOpen)
|
||||
})
|
||||
|
||||
languageList.addEventListener("click", (e) => {
|
||||
const option = e.target.closest?.('[role="option"][data-value]')
|
||||
if (!option) return
|
||||
const value = option.getAttribute("data-value")
|
||||
setLanguage(value)
|
||||
setLanguageMenuOpen(false)
|
||||
})
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!languageButton || !languageList) return
|
||||
const clickedInside =
|
||||
languageButton.contains(e.target) || languageList.contains(e.target)
|
||||
if (!clickedInside) setLanguageMenuOpen(false)
|
||||
})
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const isOpen = languageButton.getAttribute("aria-expanded") === "true"
|
||||
if (!isOpen) return
|
||||
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setLanguageMenuOpen(false)
|
||||
languageButton.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const options = [...languageList.querySelectorAll('[role="option"]')]
|
||||
if (!options.length) return
|
||||
const currentIndex = Math.max(
|
||||
0,
|
||||
options.findIndex(
|
||||
(el) => el.getAttribute("data-value") === currentLanguage,
|
||||
),
|
||||
)
|
||||
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
const delta = e.key === "ArrowDown" ? 1 : -1
|
||||
const nextIndex =
|
||||
(currentIndex + delta + options.length) % options.length
|
||||
const nextValue = options[nextIndex].getAttribute("data-value")
|
||||
setLanguage(nextValue)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
setLanguageMenuOpen(false)
|
||||
languageButton.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (e) => {
|
||||
if (!localStorage.getItem("theme")) {
|
||||
setTheme(e.matches ? "dark" : "light")
|
||||
}
|
||||
})
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener("click", toggleTheme)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user