perf(ui): defer locale and overlay bundles (#238)
## Summary - defer locale and overlay loading work away from the first critical render path - seed locale state from the bootstrap preload so the first render can use the preloaded language immediately - keep bootstrap cache and locale fallback behavior consistent on subsequent launches ## Testing - npm run build --workspace @codenomad/ui
This commit is contained in:
@@ -2,11 +2,6 @@ import { createContext, createEffect, createMemo, createSignal, onCleanup, onMou
|
|||||||
import type { ParentComponent } from "solid-js"
|
import type { ParentComponent } from "solid-js"
|
||||||
import { useConfig } from "../../stores/preferences"
|
import { useConfig } from "../../stores/preferences"
|
||||||
import { enMessages } from "./messages/en"
|
import { enMessages } from "./messages/en"
|
||||||
import { esMessages } from "./messages/es"
|
|
||||||
import { frMessages } from "./messages/fr"
|
|
||||||
import { ruMessages } from "./messages/ru"
|
|
||||||
import { jaMessages } from "./messages/ja"
|
|
||||||
import { zhHansMessages } from "./messages/zh-Hans"
|
|
||||||
|
|
||||||
type Messages = Record<string, string>
|
type Messages = Record<string, string>
|
||||||
|
|
||||||
@@ -15,14 +10,18 @@ export type TranslateParams = Record<string, unknown>
|
|||||||
export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans"
|
export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans"
|
||||||
|
|
||||||
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
||||||
|
const SUPPORTED_LOCALES_BY_LOWER = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
||||||
|
|
||||||
const messagesByLocale: Record<Locale, Messages> = {
|
const localeMessagesCache = new Map<Locale, Messages>([["en", enMessages]])
|
||||||
en: enMessages,
|
const localeMessagesPromises = new Map<Locale, Promise<Messages>>()
|
||||||
es: esMessages,
|
|
||||||
fr: frMessages,
|
const localeLoaders: Record<Locale, () => Promise<Messages>> = {
|
||||||
ru: ruMessages,
|
en: async () => enMessages,
|
||||||
ja: jaMessages,
|
es: async () => (await import("./messages/es")).esMessages,
|
||||||
"zh-Hans": zhHansMessages,
|
fr: async () => (await import("./messages/fr")).frMessages,
|
||||||
|
ru: async () => (await import("./messages/ru")).ruMessages,
|
||||||
|
ja: async () => (await import("./messages/ja")).jaMessages,
|
||||||
|
"zh-Hans": async () => (await import("./messages/zh-Hans")).zhHansMessages,
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLocaleTag(value: string): string {
|
function normalizeLocaleTag(value: string): string {
|
||||||
@@ -34,8 +33,7 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
|||||||
|
|
||||||
const normalized = normalizeLocaleTag(value)
|
const normalized = normalizeLocaleTag(value)
|
||||||
const lower = normalized.toLowerCase()
|
const lower = normalized.toLowerCase()
|
||||||
const supportedLower = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
const exact = SUPPORTED_LOCALES_BY_LOWER.get(lower)
|
||||||
const exact = supportedLower.get(lower)
|
|
||||||
if (exact) return exact
|
if (exact) return exact
|
||||||
|
|
||||||
const parts = lower.split("-")
|
const parts = lower.split("-")
|
||||||
@@ -43,11 +41,11 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
|||||||
if (!base) return null
|
if (!base) return null
|
||||||
|
|
||||||
if (base === "zh") {
|
if (base === "zh") {
|
||||||
const zhHans = supportedLower.get("zh-hans")
|
const zhHans = SUPPORTED_LOCALES_BY_LOWER.get("zh-hans")
|
||||||
return zhHans ?? null
|
return zhHans ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseMatch = supportedLower.get(base)
|
const baseMatch = SUPPORTED_LOCALES_BY_LOWER.get(base)
|
||||||
return baseMatch ?? null
|
return baseMatch ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +82,54 @@ function translateFrom(messages: Messages, key: string, params?: TranslateParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [globalRevision, setGlobalRevision] = createSignal(0)
|
const [globalRevision, setGlobalRevision] = createSignal(0)
|
||||||
const initialGlobalLocale: Locale = detectNavigatorLocale() ?? "en"
|
let globalMessages: Messages = enMessages
|
||||||
let globalMessages: Messages = messagesByLocale[initialGlobalLocale]
|
let globalLocale: Locale = "en"
|
||||||
|
|
||||||
|
function getMessagesForLocale(locale: Locale): Messages {
|
||||||
|
return localeMessagesCache.get(locale) ?? enMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLocaleMessages(locale: Locale): Promise<Messages> {
|
||||||
|
const cached = localeMessagesCache.get(locale)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = localeMessagesPromises.get(locale)
|
||||||
|
if (pending) {
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = localeLoaders[locale]
|
||||||
|
const promise = loader()
|
||||||
|
.then((messages) => {
|
||||||
|
localeMessagesCache.set(locale, messages)
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
return messages
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
localeMessagesPromises.set(locale, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preloadLocaleMessages(preferredLocale?: string | null): Promise<Locale> {
|
||||||
|
const resolvedLocale = matchSupportedLocale(preferredLocale ?? undefined) ?? detectNavigatorLocale() ?? "en"
|
||||||
|
try {
|
||||||
|
globalMessages = await loadLocaleMessages(resolvedLocale)
|
||||||
|
globalLocale = resolvedLocale
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
return resolvedLocale
|
||||||
|
} catch {
|
||||||
|
globalMessages = enMessages
|
||||||
|
globalLocale = "en"
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function tGlobal(key: string, params?: TranslateParams): string {
|
export function tGlobal(key: string, params?: TranslateParams): string {
|
||||||
globalRevision()
|
globalRevision()
|
||||||
@@ -101,9 +145,10 @@ const I18nContext = createContext<I18nContextValue>()
|
|||||||
|
|
||||||
export const I18nProvider: ParentComponent = (props) => {
|
export const I18nProvider: ParentComponent = (props) => {
|
||||||
const { preferences } = useConfig()
|
const { preferences } = useConfig()
|
||||||
const [detectedLocale, setDetectedLocale] = createSignal<Locale>("en")
|
const [detectedLocale, setDetectedLocale] = createSignal<Locale>(globalLocale)
|
||||||
|
const [resolvedLocale, setResolvedLocale] = createSignal<Locale>(globalLocale)
|
||||||
const previousMessages = globalMessages
|
const previousGlobalMessages = globalMessages
|
||||||
|
const previousGlobalLocale = globalLocale
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const detected = detectNavigatorLocale()
|
const detected = detectNavigatorLocale()
|
||||||
@@ -115,19 +160,44 @@ export const I18nProvider: ParentComponent = (props) => {
|
|||||||
return configured ?? detectedLocale() ?? "en"
|
return configured ?? detectedLocale() ?? "en"
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = createMemo<Messages>(() => messagesByLocale[locale()])
|
const messages = createMemo<Messages>(() => getMessagesForLocale(resolvedLocale()))
|
||||||
|
|
||||||
function t(key: string, params?: TranslateParams): string {
|
function t(key: string, params?: TranslateParams): string {
|
||||||
return translateFrom(messages(), key, params)
|
return translateFrom(messages(), key, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
globalMessages = messages()
|
const nextLocale = locale()
|
||||||
setGlobalRevision((value) => value + 1)
|
let cancelled = false
|
||||||
|
|
||||||
|
void loadLocaleMessages(nextLocale)
|
||||||
|
.then((loadedMessages) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResolvedLocale(nextLocale)
|
||||||
|
globalLocale = nextLocale
|
||||||
|
globalMessages = loadedMessages
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResolvedLocale("en")
|
||||||
|
globalMessages = enMessages
|
||||||
|
globalLocale = "en"
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
cancelled = true
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
globalMessages = previousMessages
|
globalMessages = previousGlobalMessages
|
||||||
|
globalLocale = previousGlobalLocale
|
||||||
setGlobalRevision((value) => value + 1)
|
setGlobalRevision((value) => value + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ThemeProvider } from "./lib/theme"
|
|||||||
import { ConfigProvider } from "./stores/preferences"
|
import { ConfigProvider } from "./stores/preferences"
|
||||||
import { InstanceConfigProvider } from "./stores/instance-config"
|
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { I18nProvider } from "./lib/i18n"
|
import { I18nProvider, preloadLocaleMessages } from "./lib/i18n"
|
||||||
import { storage } from "./lib/storage"
|
import { storage } from "./lib/storage"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
@@ -31,15 +31,19 @@ async function bootstrap() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const uiConfig = await storage.loadConfigOwner("ui")
|
const uiConfig = await storage.loadConfigOwner("ui")
|
||||||
const theme = (uiConfig as any)?.theme ?? "system"
|
const theme = (uiConfig as any)?.theme
|
||||||
|
const locale = typeof (uiConfig as any)?.settings?.locale === "string" ? (uiConfig as any).settings.locale : undefined
|
||||||
|
|
||||||
if (theme === "system") {
|
if (theme === "light" || theme === "dark") {
|
||||||
document.documentElement.removeAttribute("data-theme")
|
|
||||||
} else {
|
|
||||||
document.documentElement.setAttribute("data-theme", theme)
|
document.documentElement.setAttribute("data-theme", theme)
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute("data-theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await preloadLocaleMessages(locale)
|
||||||
} catch {
|
} catch {
|
||||||
// If config fails to load, fall back to CSS defaults.
|
// If config fails to load, fall back to CSS defaults.
|
||||||
|
await preloadLocaleMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user