feat(native): follow the system language instead of an in-app picker

Declare the app's 12 languages to iOS and Android via expo-localization,
resolve the language from the device's preferred languages at start-up and
on Android runtime changes, and make Settings > Language open the system
settings page. Drops the MMKV-stored locale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 13:24:59 -04:00
1 parent d012280f20
commit ccaacfeae5
20 files changed
+218 -137

No files matched your search

+18
View File
@@ -145,7 +145,25 @@
"microphonePermission": false "microphonePermission": false
} }
], ],
[
"expo-localization", "expo-localization",
{
"supportedLocales": [
"en",
"fr",
"de",
"es",
"it",
"pt",
"nl",
"ar",
"he",
"zh",
"ja",
"ko"
]
}
],
[ [
"expo-widgets", "expo-widgets",
{ {
+10 -36
View File
@@ -24,7 +24,6 @@ import {
IconWorld, IconWorld,
} from "@tabler/icons-react-native"; } from "@tabler/icons-react-native";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { reloadAppAsync } from "expo";
import * as Application from "expo-application"; import * as Application from "expo-application";
import * as ImagePicker from "expo-image-picker"; import * as ImagePicker from "expo-image-picker";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
@@ -32,6 +31,7 @@ import { useCallback, useState } from "react";
import { import {
Alert, Alert,
Linking, Linking,
Platform,
Pressable, Pressable,
RefreshControl, RefreshControl,
ScrollView, ScrollView,
@@ -47,11 +47,9 @@ import { SettingsSection } from "@/components/settings/settings-section";
import { TmdbLogo } from "@/components/tmdb-logo"; import { TmdbLogo } from "@/components/tmdb-logo";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { ScaledIcon } from "@/components/ui/scaled-icon"; import { ScaledIcon } from "@/components/ui/scaled-icon";
import { SelectModal } from "@/components/ui/select-modal";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { setPersistedLocale } from "@/lib/i18n";
import { orpc } from "@/lib/orpc"; import { orpc } from "@/lib/orpc";
import { isAnalyticsEnabled, setAnalyticsEnabled } from "@/lib/posthog"; import { isAnalyticsEnabled, setAnalyticsEnabled } from "@/lib/posthog";
import { queryClient } from "@/lib/query-client"; import { queryClient } from "@/lib/query-client";
@@ -59,7 +57,6 @@ import { isCrashReportingEnabled, setCrashReportingEnabled } from "@/lib/sentry"
import { authClient, getServerUrl } from "@/lib/server"; import { authClient, getServerUrl } from "@/lib/server";
import { markSessionEnding } from "@/lib/session-end"; import { markSessionEnding } from "@/lib/session-end";
import { toast } from "@/lib/toast"; import { toast } from "@/lib/toast";
import { activateLocale, isLocaleRTL, type SupportedLocale } from "@sofa/i18n";
import { LOCALE_INFO } from "@sofa/i18n/locales"; import { LOCALE_INFO } from "@sofa/i18n/locales";
const settingsContentContainerStyle = { const settingsContentContainerStyle = {
@@ -77,6 +74,14 @@ const removePhotoIcon = Icon.select({
android: import("@expo/material-symbols/delete.xml"), android: import("@expo/material-symbols/delete.xml"),
}); });
// Per-app language is in system settings: iOS 13+ (Settings → Sofa → Language) and Android 13+
// (App info → Language). Older Android only has the device language.
const canOpenLanguageSettings =
process.env.EXPO_OS === "ios" ||
(process.env.EXPO_OS === "android" &&
typeof Platform.Version === "number" &&
Platform.Version >= 33);
export default function SettingsScreen() { export default function SettingsScreen() {
const { t, i18n } = useLingui(); const { t, i18n } = useLingui();
const { push } = useRouter(); const { push } = useRouter();
@@ -93,7 +98,6 @@ export default function SettingsScreen() {
} }
} }
const [languageModalOpen, setLanguageModalOpen] = useState(false);
const languageLabel = LOCALE_INFO.find((o) => o.code === i18n.locale)?.nativeName ?? i18n.locale; const languageLabel = LOCALE_INFO.find((o) => o.code === i18n.locale)?.nativeName ?? i18n.locale;
const [analyticsEnabled, setAnalyticsToggle] = useState(isAnalyticsEnabled); const [analyticsEnabled, setAnalyticsToggle] = useState(isAnalyticsEnabled);
const [crashReportingEnabled, setCrashReportingToggle] = useState(isCrashReportingEnabled); const [crashReportingEnabled, setCrashReportingToggle] = useState(isCrashReportingEnabled);
@@ -428,7 +432,7 @@ export default function SettingsScreen() {
label={t`Language`} label={t`Language`}
value={languageLabel} value={languageLabel}
icon={IconLanguage} icon={IconLanguage}
onPress={() => setLanguageModalOpen(true)} onPress={canOpenLanguageSettings ? () => void Linking.openSettings() : undefined}
/> />
<SettingsRow <SettingsRow
label={t`Anonymous usage reporting`} label={t`Anonymous usage reporting`}
@@ -568,36 +572,6 @@ export default function SettingsScreen() {
</SettingsSection> </SettingsSection>
</Animated.View> </Animated.View>
{/* Language Modal */}
<SelectModal
open={languageModalOpen}
onOpenChange={setLanguageModalOpen}
label={t`Language`}
icon={IconLanguage}
selection={i18n.locale}
options={LOCALE_INFO.map((info) => ({
value: info.code,
label: info.nativeName,
}))}
onSelect={(locale) => {
setLanguageModalOpen(false);
const previousLocale = i18n.locale;
activateLocale(locale as SupportedLocale).then(
() => {
setPersistedLocale(locale as SupportedLocale);
if (isLocaleRTL(locale) !== isLocaleRTL(previousLocale)) {
Alert.alert(
t`Restart Required`,
t`Sofa needs to restart to apply the new layout direction.`,
[{ text: t`Restart`, onPress: () => reloadAppAsync() }],
);
}
},
() => {},
);
}}
/>
{/* Version */} {/* Version */}
<Animated.View entering={FadeInDown.duration(300).delay(400)} className="mt-6 items-center"> <Animated.View entering={FadeInDown.duration(300).delay(400)} className="mt-6 items-center">
<Text className="text-muted-foreground text-xs"> <Text className="text-muted-foreground text-xs">
+3
View File
@@ -22,6 +22,7 @@ import { Uniwind, useResolveClassNames } from "uniwind";
import { OfflineBanner } from "@/components/ui/offline-banner"; import { OfflineBanner } from "@/components/ui/offline-banner";
import { ServerUnreachableBanner } from "@/components/ui/server-unreachable-banner"; import { ServerUnreachableBanner } from "@/components/ui/server-unreachable-banner";
import { useFollowDeviceLocale } from "@/hooks/use-follow-device-locale";
import { useServerConnection } from "@/hooks/use-server-connection"; import { useServerConnection } from "@/hooks/use-server-connection";
import { useWidgetRefresh } from "@/hooks/use-widget-refresh"; import { useWidgetRefresh } from "@/hooks/use-widget-refresh";
import { initLocale } from "@/lib/i18n"; import { initLocale } from "@/lib/i18n";
@@ -68,6 +69,8 @@ function AppContent() {
localeReady.then(() => setLocaleReady(true)).catch(() => setLocaleReady(true)); localeReady.then(() => setLocaleReady(true)).catch(() => setLocaleReady(true));
}, []); }, []);
useFollowDeviceLocale();
// --- Analytics init (sync PostHog opt-in/out from stored preference) --- // --- Analytics init (sync PostHog opt-in/out from stored preference) ---
useEffect(() => { useEffect(() => {
initAnalytics(); initAnalytics();
@@ -0,0 +1,19 @@
import { useLocales } from "expo-localization";
import { useEffect } from "react";
import { applyLocale } from "@/lib/i18n";
import { resolveLocale } from "@/lib/resolve-locale";
/**
* Keep the app language in step with the OS setting. Android delivers per-app and system language
* changes without restarting the activity; iOS relaunches the app instead, so there this only
* confirms the language chosen at start-up.
*/
export function useFollowDeviceLocale(): void {
const deviceLocales = useLocales();
const locale = resolveLocale(deviceLocales.map((l) => l.languageTag));
useEffect(() => {
applyLocale(locale).catch(() => {});
}, [locale]);
}
+21 -28
View File
@@ -2,46 +2,39 @@ import { reloadAppAsync } from "expo";
import * as Localization from "expo-localization"; import * as Localization from "expo-localization";
import { I18nManager } from "react-native"; import { I18nManager } from "react-native";
import { activateLocale, isLocaleRTL, SUPPORTED_LOCALES, type SupportedLocale } from "@sofa/i18n"; import { activateLocale, isLocaleRTL, type SupportedLocale } from "@sofa/i18n";
import { globalStorage } from "./mmkv"; import { globalStorage } from "./mmkv";
import { resolveLocale } from "./resolve-locale";
const LOCALE_STORAGE_KEY = "sofa:locale"; // Written by app versions that had an in-app language picker. The OS language setting is now the
// only source of truth; the key is deleted on launch.
const LEGACY_LOCALE_STORAGE_KEY = "sofa:locale";
export function getPersistedLocale(): SupportedLocale { let appliedLocale: SupportedLocale | null = null;
const stored = globalStorage.getString(LOCALE_STORAGE_KEY);
if (stored && SUPPORTED_LOCALES.includes(stored as SupportedLocale)) { export function getDeviceLocale(): SupportedLocale {
return stored as SupportedLocale; return resolveLocale(Localization.getLocales().map((l) => l.languageTag));
}
const deviceLocales = Localization.getLocales();
for (const loc of deviceLocales) {
const lang = loc.languageCode;
if (lang && SUPPORTED_LOCALES.includes(lang as SupportedLocale)) {
return lang as SupportedLocale;
}
}
return "en";
} }
export function setPersistedLocale(locale: SupportedLocale): void { /**
globalStorage.set(LOCALE_STORAGE_KEY, locale); * Activate `locale`'s catalog. When its layout direction differs from the running one, store the
* new direction and reload: React Native only reads it at start-up. The flag is persisted
* natively, so this also clears a direction forced by the old in-app picker.
*/
export function applyLocale(locale: SupportedLocale): Promise<void> {
if (locale === appliedLocale) return Promise.resolve();
appliedLocale = locale;
const rtl = isLocaleRTL(locale); const rtl = isLocaleRTL(locale);
I18nManager.allowRTL(rtl); I18nManager.allowRTL(rtl);
I18nManager.forceRTL(rtl); I18nManager.forceRTL(rtl);
}
export function initLocale(): Promise<void> {
const locale = getPersistedLocale();
const rtl = isLocaleRTL(locale);
I18nManager.allowRTL(rtl);
I18nManager.forceRTL(rtl);
// forceRTL only takes effect on the next launch — if the current layout
// doesn't match (e.g. fresh install on an RTL device), reload immediately.
if (I18nManager.isRTL !== rtl) { if (I18nManager.isRTL !== rtl) {
reloadAppAsync(); reloadAppAsync();
} }
if (locale !== "en") {
return activateLocale(locale); return activateLocale(locale);
} }
return Promise.resolve();
export function initLocale(): Promise<void> {
globalStorage.remove(LEGACY_LOCALE_STORAGE_KEY);
return applyLocale(getDeviceLocale());
} }
@@ -0,0 +1,48 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, test } from "vitest";
import { resolveLocale } from "@/lib/resolve-locale";
import { SUPPORTED_LOCALES } from "@sofa/i18n/locales";
describe("resolveLocale", () => {
test("matches a supported language with a region", () => {
expect(resolveLocale(["de-DE"])).toBe("de");
});
test("skips an unsupported first language", () => {
expect(resolveLocale(["fa-IR", "fr-FR"])).toBe("fr");
});
test("maps the legacy Android Hebrew code", () => {
expect(resolveLocale(["iw-IL"])).toBe("he");
expect(resolveLocale(["he-IL"])).toBe("he");
});
test("matches on the primary subtag regardless of script, region or case", () => {
expect(resolveLocale(["zh-Hant-TW"])).toBe("zh");
expect(resolveLocale(["pt-BR"])).toBe("pt");
expect(resolveLocale(["EN_us"])).toBe("en");
});
test("falls back to English", () => {
expect(resolveLocale([null, "ko-KR"])).toBe("ko");
expect(resolveLocale(["fa-IR"])).toBe("en");
expect(resolveLocale([])).toBe("en");
});
});
describe("app.json", () => {
test("app.json declares every supported locale to the OS (add new locales to the expo-localization plugin)", () => {
const appJson = JSON.parse(
readFileSync(fileURLToPath(new URL("../../app.json", import.meta.url)), "utf8"),
) as { expo: { plugins: unknown[] } };
const entry = appJson.expo.plugins.find(
(plugin): plugin is [string, { supportedLocales: string[] }] =>
Array.isArray(plugin) && plugin[0] === "expo-localization",
);
expect(entry).toBeDefined();
expect([...(entry?.[1].supportedLocales ?? [])].sort()).toEqual([...SUPPORTED_LOCALES].sort());
});
});
+25
View File
@@ -0,0 +1,25 @@
import { SUPPORTED_LOCALES, type SupportedLocale } from "@sofa/i18n/locales";
const SUBTAG_SEPARATOR = /[-_]/;
// Android's java.util.Locale can still report Hebrew as the legacy "iw".
const LEGACY_LANGUAGE_CODES: Record<string, string> = { iw: "he" };
/**
* The first of the user's preferred languages (BCP-47 tags, in preference order) that Sofa has a
* catalog for, else English. Matches on the primary language subtag: "pt-BR" → "pt",
* "zh-Hant-TW" → "zh".
*/
export function resolveLocale(
languageTags: readonly (string | null | undefined)[],
): SupportedLocale {
for (const tag of languageTags) {
if (!tag) continue;
const primary = (tag.split(SUBTAG_SEPARATOR)[0] ?? "").toLowerCase();
const code = LEGACY_LANGUAGE_CODES[primary] ?? primary;
if ((SUPPORTED_LOCALES as readonly string[]).includes(code)) {
return code as SupportedLocale;
}
}
return "en";
}
+2 -1
View File
@@ -19,7 +19,7 @@ Sofa supports multiple languages for the web and mobile apps. The current suppor
- Portuguese - Portuguese
- Spanish - Spanish
You can switch languages in **Settings > Language** on web or the mobile app. Sofa detects your browser or device language on first launch and defaults to it if supported. This setting is **not** saved to the server — it is only persisted to the current browser or device. On the web, switch languages in **Settings > Language**. Sofa detects your browser language on first visit, and the choice is saved in that browser only. The mobile app follows your device's language. To use a different language for Sofa only, change it in your phone's settings (iOS: **Settings > Apps > Sofa > Language**; Android 13+: **App info > Language**). Language preferences are never saved to the server.
## How translations work ## How translations work
@@ -70,6 +70,7 @@ The script only translates strings with empty `msgstr` — existing translations
2. Run `bun run i18n:extract` — this creates a new `.po` file for the locale 2. Run `bun run i18n:extract` — this creates a new `.po` file for the locale
3. Translate the strings (via Crowdin, manually, or with `bun packages/i18n/scripts/claude.ts <locale>`) 3. Translate the strings (via Crowdin, manually, or with `bun packages/i18n/scripts/claude.ts <locale>`)
4. Add the locale to the `LOCALE_INFO` array in [`packages/i18n/src/locales.ts`](https://github.com/jakejarvis/sofa/blob/main/packages/i18n/src/locales.ts) with its native name 4. Add the locale to the `LOCALE_INFO` array in [`packages/i18n/src/locales.ts`](https://github.com/jakejarvis/sofa/blob/main/packages/i18n/src/locales.ts) with its native name
5. Add the locale to `supportedLocales` in the `expo-localization` plugin in [`apps/native/app.json`](https://github.com/jakejarvis/sofa/blob/main/apps/native/app.json) so iOS and Android offer it (a native test fails until you do)
## Translation guidelines ## Translation guidelines
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "يتطلب Emby Server 4.7.9+ وترخيصاً نشطاً لـ Emby Premiere." msgstr "يتطلب Emby Server 4.7.9+ وترخيصاً نشطاً لـ Emby Premiere."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "إعادة التشغيل" #~ msgstr "إعادة التشغيل"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "إعادة التشغيل مطلوبة" #~ msgstr "إعادة التشغيل مطلوبة"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "تم التخطي" msgstr "تم التخطي"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "يحتاج Sofa إلى إعادة التشغيل لتطبيق اتجاه التخطيط الجديد." #~ msgstr "يحتاج Sofa إلى إعادة التشغيل لتطبيق اتجاه التخطيط الجديد."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Erfordert Emby Server 4.7.9+ und eine aktive Emby Premiere-Lizenz." msgstr "Erfordert Emby Server 4.7.9+ und eine aktive Emby Premiere-Lizenz."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "Neu starten" #~ msgstr "Neu starten"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "Neustart erforderlich" #~ msgstr "Neustart erforderlich"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "Übersprungen" msgstr "Übersprungen"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa muss neu gestartet werden, um die neue Layoutrichtung anzuwenden." #~ msgstr "Sofa muss neu gestartet werden, um die neue Layoutrichtung anzuwenden."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "" msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "Restart" #~ msgstr "Restart"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "Restart Required" #~ msgstr "Restart Required"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "" msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa needs to restart to apply the new layout direction." #~ msgstr "Sofa needs to restart to apply the new layout direction."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Requiere Emby Server 4.7.9+ y una licencia activa de Emby Premiere." msgstr "Requiere Emby Server 4.7.9+ y una licencia activa de Emby Premiere."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "Reiniciar" #~ msgstr "Reiniciar"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "Reinicio requerido" #~ msgstr "Reinicio requerido"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "Omitido" msgstr "Omitido"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa necesita reiniciarse para aplicar la nueva dirección del diseño." #~ msgstr "Sofa necesita reiniciarse para aplicar la nueva dirección del diseño."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Nécessite Emby Server 4.7.9+ et une licence Emby Premiere active." msgstr "Nécessite Emby Server 4.7.9+ et une licence Emby Premiere active."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "Redémarrer" #~ msgstr "Redémarrer"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "Redémarrage requis" #~ msgstr "Redémarrage requis"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "Ignoré" msgstr "Ignoré"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa doit redémarrer pour appliquer la nouvelle direction de mise en page." #~ msgstr "Sofa doit redémarrer pour appliquer la nouvelle direction de mise en page."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "דורש Emby Server 4.7.9+ ורישיון Emby Premiere פעיל." msgstr "דורש Emby Server 4.7.9+ ורישיון Emby Premiere פעיל."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "הפעל מחדש" #~ msgstr "הפעל מחדש"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "נדרשת הפעלה מחדש" #~ msgstr "נדרשת הפעלה מחדש"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "דולג" msgstr "דולג"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "יש להפעיל מחדש את Sofa כדי להחיל את כיוון הפריסה החדש." #~ msgstr "יש להפעיל מחדש את Sofa כדי להחיל את כיוון הפריסה החדש."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Richiede Emby Server 4.7.9+ e una licenza Emby Premiere attiva." msgstr "Richiede Emby Server 4.7.9+ e una licenza Emby Premiere attiva."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "Riavvia" #~ msgstr "Riavvia"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "Riavvio necessario" #~ msgstr "Riavvio necessario"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "Ignorati" msgstr "Ignorati"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa deve essere riavviato per applicare la nuova direzione del layout." #~ msgstr "Sofa deve essere riavviato per applicare la nuova direzione del layout."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Emby Server 4.7.9+と有効なEmby Premiereライセンスが必要です。" msgstr "Emby Server 4.7.9+と有効なEmby Premiereライセンスが必要です。"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "再起動" #~ msgstr "再起動"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "再起動が必要です" #~ msgstr "再起動が必要です"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "スキップ済み" msgstr "スキップ済み"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofaは新しいレイアウト方向を適用するために再起動が必要です。" #~ msgstr "Sofaは新しいレイアウト方向を適用するために再起動が必要です。"
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Emby Server 4.7.9+ 및 활성 Emby Premiere 라이선스가 필요합니다." msgstr "Emby Server 4.7.9+ 및 활성 Emby Premiere 라이선스가 필요합니다."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "재시작" #~ msgstr "재시작"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "재시작 필요" #~ msgstr "재시작 필요"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "건너뜀" msgstr "건너뜀"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa를 재시작해야 새 레이아웃 방향이 적용됩니다." #~ msgstr "Sofa를 재시작해야 새 레이아웃 방향이 적용됩니다."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Vereist Emby Server 4.7.9+ en een actieve Emby Premiere-licentie." msgstr "Vereist Emby Server 4.7.9+ en een actieve Emby Premiere-licentie."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "Herstarten" #~ msgstr "Herstarten"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "Herstart vereist" #~ msgstr "Herstart vereist"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "Overgeslagen" msgstr "Overgeslagen"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa moet herstarten om de nieuwe lay-outrichting toe te passen." #~ msgstr "Sofa moet herstarten om de nieuwe lay-outrichting toe te passen."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "Requer Emby Server 4.7.9+ e uma licença ativa do Emby Premiere." msgstr "Requer Emby Server 4.7.9+ e uma licença ativa do Emby Premiere."
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "Reiniciar" #~ msgstr "Reiniciar"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "Reinício Necessário" #~ msgstr "Reinício Necessário"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "Ignorado" msgstr "Ignorado"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "O Sofa precisa de reiniciar para aplicar a nova direção do layout." #~ msgstr "O Sofa precisa de reiniciar para aplicar a nova direção do layout."
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
+6 -6
View File
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
msgstr "需要 Emby Server 4.7.9+ 以及有效的 Emby Premiere 授权。" msgstr "需要 Emby Server 4.7.9+ 以及有效的 Emby Premiere 授权。"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart" #~ msgid "Restart"
msgstr "重启" #~ msgstr "重启"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Restart Required" #~ msgid "Restart Required"
msgstr "需要重启" #~ msgstr "需要重启"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Restore" msgid "Restore"
@@ -3042,8 +3042,8 @@ msgid "Skipped"
msgstr "已跳过" msgstr "已跳过"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Sofa needs to restart to apply the new layout direction." #~ msgid "Sofa needs to restart to apply the new layout direction."
msgstr "Sofa 需要重启以应用新的布局方向。" #~ msgstr "Sofa 需要重启以应用新的布局方向。"
#: apps/web/src/routes/setup.tsx #: apps/web/src/routes/setup.tsx
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started." msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."