Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ import {
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
import i18n, { setLocale } from '@/i18n.config'
import i18n, { detectSystemLocale, setLocale } from '@/i18n.config'
import {
instanceKeys,
instanceListQueryOptions,
Expand Down Expand Up @@ -703,10 +703,7 @@ async function setupApp() {
pending_update_toast_for_version,
} = await traceStartupStep('Read startup settings', getSettings)

// Initialize locale from saved settings
if (locale) {
await traceStartupStep('Apply startup locale', () => setLocale(locale))
}
await traceStartupStep('Apply startup locale', () => setLocale(locale || detectSystemLocale()))

Object.assign(appSettings.featureFlags, feature_flags)
isMaximized.value = await traceStartupStep('Read window maximized state', () =>
Expand Down Expand Up @@ -1009,15 +1006,15 @@ watch(
if (appTheme.syncAcrossDevices && appTheme.preferred !== selectedTheme) {
appTheme.preferred = selectedTheme
}
if (i18n.global.locale.value !== locale) {
if (locale && i18n.global.locale.value !== locale) {
await setLocale(locale)
}

if (appTheme.syncAcrossDevices && settings.theme !== selectedTheme) {
settings.theme = selectedTheme
settingsChanged = true
}
if (settings.locale !== locale) {
if (locale && settings.locale !== locale) {
settings.locale = locale
settingsChanged = true
}
Expand Down
5 changes: 5 additions & 0 deletions apps/app-frontend/src/i18n.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createMessageCompiler,
type CrowdinMessages,
LOCALES,
matchLocale,
} from '@modrinth/ui'
import englishUi from '@modrinth/ui/src/locales/en-US/index.json'
import { createI18n } from 'vue-i18n'
Expand Down Expand Up @@ -42,6 +43,10 @@ async function fetchMessages(url: string): Promise<CrowdinMessages> {
return response.json()
}

export function detectSystemLocale(): string {
return matchLocale(navigator.languages) ?? 'en-US'
}

export async function setLocale(requestedLocale: string): Promise<void> {
const request = ++localeRequest
const locale = LOCALES.some((candidate) => candidate.code === requestedLocale)
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ watch(
rememberStoredAccountAppearance(userId, preferences.appearance)
}

if (locale.value !== preferences.localization.locale) {
void setLocale(preferences.localization.locale)
const accountLocale = preferences.localization.locale
if (accountLocale && locale.value !== accountLocale) {
void setLocale(accountLocale)
}

cosmetics.value.searchDisplayMode.mod = preferences.layouts.mods === 'rows' ? 'list' : 'grid'
Expand Down
16 changes: 2 additions & 14 deletions apps/frontend/src/plugins/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
I18N_INJECTION_KEY,
type I18nContext,
LOCALES,
matchLocale,
transformCrowdinMessages,
uiLocaleModules,
useDebugLogger,
Expand Down Expand Up @@ -127,20 +128,7 @@ async function loadLocale(code: string): Promise<void> {
}

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
return matchLocale(header.split(',').map((l) => l.split(';')[0]))
}

export default defineNuxtPlugin({
Expand Down
14 changes: 4 additions & 10 deletions apps/labrinth/src/models/v3/preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,11 @@ pub enum Theme {
Retro,
}

#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Component)]
#[derive(
Debug, Serialize, Deserialize, ToSchema, Default, PartialEq, Component,
)]
pub struct LocalizationPreferences {
pub locale: String,
}

impl Default for LocalizationPreferences {
fn default() -> Self {
Self {
locale: "en-US".to_owned(),
}
}
pub locale: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Component)]
Expand Down
2 changes: 1 addition & 1 deletion packages/api-client/src/modules/labrinth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1834,7 +1834,7 @@ export namespace Labrinth {
}

export type LocalizationPreferences = {
locale: string
locale: string | null
}

export type LayoutPreferences = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UPDATE settings SET locale = '' WHERE locale = 'en-US';
21 changes: 21 additions & 0 deletions packages/ui/src/composables/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,27 @@ export function transformCrowdinMessages(messages: CrowdinMessages): Record<stri

const LOCALE_CODES = new Set(LOCALES.map((l) => l.code))

/**
* Picks the best supported locale for a list of language tags, most preferred first.
* Falls back to the same language, preferring its home region (e.g. `de` becomes `de-DE`).
*/
export function matchLocale(languages: readonly string[]): string | null {
for (const language of languages) {
const tag = language.trim().toLowerCase()
if (!tag) continue

const exact = LOCALES.find((l) => l.code.toLowerCase() === tag)
if (exact) return exact.code

const base = tag.split('-')[0]
const sameLanguage = LOCALES.filter((l) => l.code.toLowerCase().startsWith(`${base}-`))
const match =
sameLanguage.find((l) => l.code.toLowerCase() === `${base}-${base}`) ?? sameLanguage[0]
if (match) return match.code
}
return null
}

/**
* Builds locale messages from glob-imported modules.
* Only includes locales that are defined in the LOCALES array.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ watch(
preferences,
(value) => {
if (!value || hasChanges.value) return
persistedLocale.value = value.localization.locale
persistedLocale.value = value.localization.locale ?? locale.value
},
{ immediate: true, flush: 'sync' },
)
Expand Down
Loading