From 5cb7c17eaee9646545e0703b13fd2226f56d00f8 Mon Sep 17 00:00:00 2001 From: Felitendo Date: Fri, 18 Sep 2026 11:11:52 +0200 Subject: [PATCH] feat: start in the system's language --- ...in-the-language-the-system-is-set-to.patch | 129 ++++++++++++++++++ scripts/check.sh | 5 + 2 files changed, 134 insertions(+) create mode 100644 patches/0028-Start-in-the-language-the-system-is-set-to.patch diff --git a/patches/0028-Start-in-the-language-the-system-is-set-to.patch b/patches/0028-Start-in-the-language-the-system-is-set-to.patch new file mode 100644 index 0000000..be84ff0 --- /dev/null +++ b/patches/0028-Start-in-the-language-the-system-is-set-to.patch @@ -0,0 +1,129 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Modrinth Enhanced +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 2bd8eb3..18724bb 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 { +@@ -938,9 +939,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 = { 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 { ++ 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 ++} diff --git a/scripts/check.sh b/scripts/check.sh index fe7bf73..7ea7d34 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -216,6 +216,11 @@ check "the settings tabs scroll instead of covering the version" \ check "NVIDIA under Wayland does not crash the webview" \ contains "$WORKTREE/apps/app/src/main.rs" 'set_var("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1")' +log "Language" +check "the first start takes the system's language" \ + contains "$WORKTREE/apps/app-frontend/src/App.vue" 'localeFromSystem(locale)' +check "a barely translated language is left alone" \ + contains "$WORKTREE/apps/app-frontend/src/helpers/system-locale.ts" 'MIN_COVERAGE = 70' log "Updates" check "updates do not come from Modrinth" \ missing "$WORKTREE/apps/app-frontend/src/App.vue" 'launcher-files.modrinth.com/updates.json'