Files
Modrinth-Enhanced/patches/0028-Start-in-the-language-the-system-is-set-to.patch

130 lines
5.3 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Modrinth Enhanced <patches@modrinth-enhanced.invalid>
Date: Fri, 18 Sep 2026 10:24:21 +0200
Subject: [PATCH] Start in the language the system is set to
The app started in English until someone found the language setting,
even on a desktop that is not in English. The first start now takes the
language from the system, as long as upstream has that one translated to
at least 70 percent - below that the app would be half English anyway,
which reads worse than all of it being. The system is only asked once,
and never over a language that was picked in the settings.
---
apps/app-frontend/src/App.vue | 17 ++++-
.../app-frontend/src/helpers/system-locale.ts | 71 +++++++++++++++++++
2 files changed, 85 insertions(+), 3 deletions(-)
create mode 100644 apps/app-frontend/src/helpers/system-locale.ts
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index cb688fc..bb4aa89 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -166,6 +166,7 @@ import {
syncedServersQueryOptions,
} from '@/helpers/synced-options'
import { syncedPackQueryOptions } from '@/helpers/synced-packs'
+import { localeFromSystem } from '@/helpers/system-locale'
import { get_user_preferences } from '@/helpers/user-preferences.ts'
import { parse_modrinth_user_link } from '@/helpers/users'
import {
@@ -952,9 +953,19 @@ 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))
+ // Initialize locale from saved settings, or on the very first start from the
+ // language the desktop is set to.
+ const startupLocale =
+ (await traceStartupStep('Read the system language', () => localeFromSystem(locale)).catch(
+ () => undefined,
+ )) ?? locale
+ if (startupLocale) {
+ await traceStartupStep('Apply startup locale', () => setLocale(startupLocale))
+ }
+ if (startupLocale !== locale) {
+ await getSettings()
+ .then((settings) => setSettings({ ...settings, locale: startupLocale }))
+ .catch(handleError)
}
Object.assign(appSettings.featureFlags, feature_flags)
diff --git a/apps/app-frontend/src/helpers/system-locale.ts b/apps/app-frontend/src/helpers/system-locale.ts
new file mode 100644
index 0000000..2e98fa7
--- /dev/null
+++ b/apps/app-frontend/src/helpers/system-locale.ts
@@ -0,0 +1,71 @@
+/**
+ * The language the app starts in the first time it runs, taken from the one the
+ * desktop is set to.
+ *
+ * Only languages upstream has translated far enough are taken. Below that the
+ * app would be half English anyway, which reads worse than all of it being.
+ */
+import { LOCALES } from '@modrinth/ui'
+import { languageCoverage } from '@modrinth/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated'
+import { locale as osLocale } from '@tauri-apps/plugin-os'
+
+/** How much of the app has to be in a language for it to be picked. */
+const MIN_COVERAGE = 70
+
+/** Written once the system has been asked, so that only ever happens once. */
+const ASKED_KEY = 'modrinth-enhanced:language-from-system'
+
+/** Windows says zh-Hans-CN; the script is what tells the two Chinese apart. */
+const BY_SCRIPT: Record<string, string> = { hans: 'zh-CN', hant: 'zh-TW' }
+
+function coverage(code: string): number {
+ return languageCoverage.app[code]?.percentage ?? 0
+}
+
+/** The language the app calls a tag that is not one of its own codes. */
+function preferred(language: string, region: string | undefined, script: string | undefined) {
+ if (script) return BY_SCRIPT[script]
+ // Spanish outside Spain is the Latin American one.
+ if (language === 'es' && region && region !== 'es') return 'es-419'
+ // Otherwise the country the language is named after: de → de-DE, fr → fr-FR.
+ return `${language}-${language.toUpperCase()}`
+}
+
+/** The best translated language the app has for `tag`, if it has one at all. */
+function bestMatch(tag: string): string | undefined {
+ // de_DE.UTF-8 is a language, a country and how it is encoded.
+ const wanted = tag
+ .replace(/[.@].*$/, '')
+ .replaceAll('_', '-')
+ .toLowerCase()
+ const parts = wanted.split('-')
+ const [language, region] = [parts[0], parts.at(-1)]
+ const second = preferred(
+ language,
+ region,
+ parts.find((part) => part in BY_SCRIPT),
+ )
+
+ const rank = (code: string) => (code.toLowerCase() === wanted ? 2 : code === second ? 1 : 0)
+
+ return LOCALES.map((locale) => locale.code)
+ .filter((code) => code.split('-')[0] === language)
+ .sort((a, b) => rank(b) - rank(a) || coverage(b) - coverage(a))
+ .find((code) => coverage(code) >= MIN_COVERAGE)
+}
+
+/**
+ * The language to start in, or nothing to stay with `current`.
+ *
+ * The system is only asked once, and never over a language that was picked in
+ * the settings.
+ */
+export async function localeFromSystem(current: string): Promise<string | undefined> {
+ if (localStorage.getItem(ASKED_KEY) || (current && current !== 'en-US')) return undefined
+
+ const system = await osLocale().catch(() => null)
+ localStorage.setItem(ASKED_KEY, new Date().toISOString())
+
+ const match = bestMatch(system ?? navigator.language ?? '')
+ return match && match !== current ? match : undefined
+}