feat: remove nuxt i18n for in house i18n for web (#5131)

* feat: remove nuxt i18n for in house

* cleanup: remove old nuxt/i18n patch

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-01-15 23:49:38 +00:00
committed by GitHub
parent 4497131206
commit a903e46be9
16 changed files with 275 additions and 787 deletions

View File

@@ -8,7 +8,7 @@ import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from '@/App.vue'
import i18n from '@/i18n.config'
import i18nPlugin from '@/plugins/i18n'
import router from '@/routes'
const vueScan = new VueScanPlugin({
@@ -43,6 +43,6 @@ app.use(FloatingVue, {
},
},
})
app.use(i18n)
app.use(i18nPlugin)
app.mount('#app')

View File

@@ -0,0 +1,23 @@
import { I18N_INJECTION_KEY, type I18nContext } from '@modrinth/ui'
import type { App } from 'vue'
import i18n from '@/i18n.config'
export default {
install(app: App) {
// Install vue-i18n as before
app.use(i18n)
// Wrap it in our I18nContext interface
const context: I18nContext = {
locale: i18n.global.locale,
t: (key, values) => i18n.global.t(key, values ?? {}) as string,
setLocale: (newLocale) => {
i18n.global.locale.value = newLocale
},
}
// Provide the context at app-level
app.provide(I18N_INJECTION_KEY, context)
},
}

View File

@@ -1,10 +0,0 @@
import { createMessageCompiler } from '@modrinth/ui'
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en-US',
fallbackLocale: 'en-US',
messageCompiler: createMessageCompiler(),
missingWarn: false,
fallbackWarn: false,
}))

View File

@@ -1,17 +0,0 @@
import { type CrowdinMessages, transformCrowdinMessages } from '@modrinth/ui'
// eager:false - only loads the locale requested
const localeModules = import.meta.glob<{ default: CrowdinMessages }>(
'../src/locales/*/index.json',
{ eager: false },
)
export default defineI18nLocale(async (locale) => {
const loader = localeModules[`../src/locales/${locale}/index.json`]
if (!loader) {
console.warn(`Locale ${locale} not found`)
return {}
}
const messages = await loader()
return transformCrowdinMessages(messages.default)
})

View File

@@ -1,5 +1,4 @@
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
import { LOCALES } from '@modrinth/ui/src/composables/i18n.ts'
import serverSidedVue from '@vitejs/plugin-vue'
import fs from 'fs/promises'
import { defineNuxtConfig } from 'nuxt/config'
@@ -223,7 +222,6 @@ export default defineNuxtConfig({
},
},
modules: [
'@nuxtjs/i18n',
'@pinia/nuxt',
'floating-vue/nuxt',
// Sentry causes rollup-plugin-inject errors in dev, only enable in production
@@ -243,25 +241,6 @@ export default defineNuxtConfig({
},
},
},
i18n: {
defaultLocale: 'en-US',
lazy: true,
langDir: '.',
locales: LOCALES.map((locale) => ({
...locale,
file: 'locale-loader.ts',
})),
strategy: 'no_prefix',
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'locale',
fallbackLocale: 'en-US',
},
vueI18n: './i18n.config.ts',
bundle: {
optimizeTranslationDirective: false,
},
},
nitro: {
rollupConfig: {
// @ts-expect-error because of rolldown-vite - completely fine though

View File

@@ -18,7 +18,6 @@
"devDependencies": {
"@formatjs/cli": "^6.2.12",
"@modrinth/tooling-config": "workspace:*",
"@nuxtjs/i18n": "^9.0.0",
"@types/dompurify": "^3.0.5",
"@types/iso-3166-2": "^1.0.4",
"@types/js-yaml": "^4.0.9",
@@ -66,6 +65,7 @@
"iso-3166-2": "1.0.0",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"lru-cache": "^11.2.4",
"markdown-it": "14.1.0",
"pathe": "^1.1.2",
"pinia": "^3.0.0",

View File

@@ -2,6 +2,7 @@
import {
Admonition,
commonSettingsMessages,
injectI18n,
IntlFormatted,
LanguageSelector,
languageSelectorMessages,
@@ -10,8 +11,7 @@ import {
} from '@modrinth/ui'
const { formatMessage } = useVIntl()
const { $i18n } = useNuxtApp()
const { locale, setLocale } = $i18n
const { locale, setLocale } = injectI18n()
const platform = formatMessage(languageSelectorMessages.platformSite)

View File

@@ -0,0 +1,112 @@
import {
type CrowdinMessages,
I18N_INJECTION_KEY,
type I18nContext,
LOCALES,
transformCrowdinMessages,
} from '@modrinth/ui'
import IntlMessageFormat from 'intl-messageformat'
import { LRUCache } from 'lru-cache'
const DEFAULT_LOCALE = 'en-US'
const localeModules = import.meta.glob<{ default: CrowdinMessages }>('../locales/*/index.json', {
eager: false,
})
const messageCache = new LRUCache<string, Record<string, string>>({ max: 10 })
const formatterCache = new LRUCache<string, IntlMessageFormat>({ max: 1000 })
const loadingPromises = new Map<string, Promise<void>>() // Dedupe concurrent loads
async function loadLocale(code: string): Promise<void> {
if (messageCache.has(code)) return
// Dedupe concurrent requests for the same locale
const existing = loadingPromises.get(code)
if (existing) return existing
const promise = (async () => {
const loader = localeModules[`../locales/${code}/index.json`]
if (!loader) return
const raw = await loader()
messageCache.set(code, transformCrowdinMessages(raw.default))
})()
loadingPromises.set(code, promise)
try {
await promise
} finally {
loadingPromises.delete(code)
}
}
function parseAcceptLanguage(header: string): string | null {
try {
for (const lang of header
.split(',')
.map((l) => l.split(';')[0]?.trim())
.filter(Boolean)) {
const exact = LOCALES.find((loc) => loc.code === lang)
if (exact) return exact.code
const prefix = LOCALES.find((loc) => loc.code.startsWith(lang.split('-')[0] + '-'))
if (prefix) return prefix.code
}
} catch {
// Malformed header, ignore
}
return null
}
export default defineNuxtPlugin({
name: 'i18n',
enforce: 'pre',
async setup(nuxtApp) {
// ONLY locale needs request-scoping (what language this request uses)
const locale = useState<string>('i18n-locale', () => DEFAULT_LOCALE)
function t(key: string, values?: Record<string, unknown>): string {
const msg = messageCache.get(locale.value)?.[key] ?? messageCache.get(DEFAULT_LOCALE)?.[key]
if (!msg) return key
if (!values || Object.keys(values).length === 0) return msg
const cacheKey = `${locale.value}:${msg}`
let formatter = formatterCache.get(cacheKey)
if (!formatter) {
formatter = new IntlMessageFormat(msg, locale.value)
formatterCache.set(cacheKey, formatter)
}
try {
return formatter.format(values) as string
} catch {
return msg
}
}
async function setLocale(newLocale: string): Promise<void> {
if (!LOCALES.some((l) => l.code === newLocale)) return
await loadLocale(newLocale)
locale.value = newLocale
useCookie('locale', { maxAge: 31536000, path: '/' }).value = newLocale
}
// Detect initial locale (cookie > Accept-Language > default)
const cookieLocale = useCookie('locale').value
let detectedLocale = DEFAULT_LOCALE
if (cookieLocale && LOCALES.some((l) => l.code === cookieLocale)) {
detectedLocale = cookieLocale
} else if (import.meta.server) {
const acceptLang = useRequestHeaders(['accept-language'])['accept-language']
if (acceptLang) {
detectedLocale = parseAcceptLanguage(acceptLang) ?? DEFAULT_LOCALE
}
}
// Load locales (hits cache after first request)
await loadLocale(DEFAULT_LOCALE)
if (detectedLocale !== DEFAULT_LOCALE) await loadLocale(detectedLocale)
locale.value = detectedLocale
const context: I18nContext = { locale, t, setLocale }
nuxtApp.vueApp.provide(I18N_INJECTION_KEY, context)
},
})