feat: start in the system's language
This commit is contained in:
1 parent
56ffa26cf5
commit
5cb7c17eae
2 files changed
+134
No files matched your search
@@ -0,0 +1,129 @@
|
||||
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 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<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
|
||||
+}
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user