mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 22:55:42 -04:00
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:
20 files changed
+218
-137
No files matched your search
@@ -145,7 +145,25 @@
|
||||
"microphonePermission": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-localization",
|
||||
{
|
||||
"supportedLocales": [
|
||||
"en",
|
||||
"fr",
|
||||
"de",
|
||||
"es",
|
||||
"it",
|
||||
"pt",
|
||||
"nl",
|
||||
"ar",
|
||||
"he",
|
||||
"zh",
|
||||
"ja",
|
||||
"ko"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-widgets",
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
IconWorld,
|
||||
} from "@tabler/icons-react-native";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { reloadAppAsync } from "expo";
|
||||
import * as Application from "expo-application";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { useRouter } from "expo-router";
|
||||
@@ -32,6 +31,7 @@ import { useCallback, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Linking,
|
||||
Platform,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
@@ -47,11 +47,9 @@ import { SettingsSection } from "@/components/settings/settings-section";
|
||||
import { TmdbLogo } from "@/components/tmdb-logo";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||
import { SelectModal } from "@/components/ui/select-modal";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { setPersistedLocale } from "@/lib/i18n";
|
||||
import { orpc } from "@/lib/orpc";
|
||||
import { isAnalyticsEnabled, setAnalyticsEnabled } from "@/lib/posthog";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
@@ -59,7 +57,6 @@ import { isCrashReportingEnabled, setCrashReportingEnabled } from "@/lib/sentry"
|
||||
import { authClient, getServerUrl } from "@/lib/server";
|
||||
import { markSessionEnding } from "@/lib/session-end";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { activateLocale, isLocaleRTL, type SupportedLocale } from "@sofa/i18n";
|
||||
import { LOCALE_INFO } from "@sofa/i18n/locales";
|
||||
|
||||
const settingsContentContainerStyle = {
|
||||
@@ -77,6 +74,14 @@ const removePhotoIcon = Icon.select({
|
||||
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() {
|
||||
const { t, i18n } = useLingui();
|
||||
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 [analyticsEnabled, setAnalyticsToggle] = useState(isAnalyticsEnabled);
|
||||
const [crashReportingEnabled, setCrashReportingToggle] = useState(isCrashReportingEnabled);
|
||||
@@ -428,7 +432,7 @@ export default function SettingsScreen() {
|
||||
label={t`Language`}
|
||||
value={languageLabel}
|
||||
icon={IconLanguage}
|
||||
onPress={() => setLanguageModalOpen(true)}
|
||||
onPress={canOpenLanguageSettings ? () => void Linking.openSettings() : undefined}
|
||||
/>
|
||||
<SettingsRow
|
||||
label={t`Anonymous usage reporting`}
|
||||
@@ -568,36 +572,6 @@ export default function SettingsScreen() {
|
||||
</SettingsSection>
|
||||
</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 */}
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(400)} className="mt-6 items-center">
|
||||
<Text className="text-muted-foreground text-xs">
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Uniwind, useResolveClassNames } from "uniwind";
|
||||
|
||||
import { OfflineBanner } from "@/components/ui/offline-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 { useWidgetRefresh } from "@/hooks/use-widget-refresh";
|
||||
import { initLocale } from "@/lib/i18n";
|
||||
@@ -68,6 +69,8 @@ function AppContent() {
|
||||
localeReady.then(() => setLocaleReady(true)).catch(() => setLocaleReady(true));
|
||||
}, []);
|
||||
|
||||
useFollowDeviceLocale();
|
||||
|
||||
// --- Analytics init (sync PostHog opt-in/out from stored preference) ---
|
||||
useEffect(() => {
|
||||
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
@@ -2,46 +2,39 @@ import { reloadAppAsync } from "expo";
|
||||
import * as Localization from "expo-localization";
|
||||
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 { 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 {
|
||||
const stored = globalStorage.getString(LOCALE_STORAGE_KEY);
|
||||
if (stored && SUPPORTED_LOCALES.includes(stored as SupportedLocale)) {
|
||||
return stored as SupportedLocale;
|
||||
}
|
||||
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";
|
||||
let appliedLocale: SupportedLocale | null = null;
|
||||
|
||||
export function getDeviceLocale(): SupportedLocale {
|
||||
return resolveLocale(Localization.getLocales().map((l) => l.languageTag));
|
||||
}
|
||||
|
||||
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);
|
||||
I18nManager.allowRTL(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) {
|
||||
reloadAppAsync();
|
||||
}
|
||||
if (locale !== "en") {
|
||||
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());
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
}
|
||||
@@ -19,7 +19,7 @@ Sofa supports multiple languages for the web and mobile apps. The current suppor
|
||||
- Portuguese
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "إعادة التشغيل"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "إعادة التشغيل"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "إعادة التشغيل مطلوبة"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "إعادة التشغيل مطلوبة"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "تم التخطي"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "يحتاج Sofa إلى إعادة التشغيل لتطبيق اتجاه التخطيط الجديد."
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "يحتاج Sofa إلى إعادة التشغيل لتطبيق اتجاه التخطيط الجديد."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "Neu starten"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "Neu starten"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "Neustart erforderlich"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "Neustart erforderlich"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "Übersprungen"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofa muss neu gestartet werden, um die neue Layoutrichtung anzuwenden."
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "Sofa muss neu gestartet werden, um die neue Layoutrichtung anzuwenden."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -2711,12 +2711,12 @@ msgid "Requires Emby Server 4.7.9+ and an active Emby Premiere license."
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "Restart"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "Restart"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "Restart Required"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "Restart Required"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "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."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "Reiniciar"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "Reiniciar"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "Reinicio requerido"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "Reinicio requerido"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "Omitido"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofa necesita reiniciarse para aplicar la nueva dirección del diseño."
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "Sofa necesita reiniciarse para aplicar la nueva dirección del diseño."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "Redémarrer"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "Redémarrer"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "Redémarrage requis"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "Redémarrage requis"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "Ignoré"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
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."
|
||||
#~ 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."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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 פעיל."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "הפעל מחדש"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "הפעל מחדש"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "נדרשת הפעלה מחדש"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "נדרשת הפעלה מחדש"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "דולג"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "יש להפעיל מחדש את Sofa כדי להחיל את כיוון הפריסה החדש."
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "יש להפעיל מחדש את Sofa כדי להחיל את כיוון הפריסה החדש."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "Riavvia"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "Riavvia"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "Riavvio necessario"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "Riavvio necessario"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "Ignorati"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofa deve essere riavviato per applicare la nuova direzione del layout."
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "Sofa deve essere riavviato per applicare la nuova direzione del layout."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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ライセンスが必要です。"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "再起動"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "再起動"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "再起動が必要です"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "再起動が必要です"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "スキップ済み"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofaは新しいレイアウト方向を適用するために再起動が必要です。"
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "Sofaは新しいレイアウト方向を適用するために再起動が必要です。"
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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 라이선스가 필요합니다."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "재시작"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "재시작"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "재시작 필요"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "재시작 필요"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "건너뜀"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofa를 재시작해야 새 레이아웃 방향이 적용됩니다."
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "Sofa를 재시작해야 새 레이아웃 방향이 적용됩니다."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "Herstarten"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "Herstarten"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "Herstart vereist"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "Herstart vereist"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "Overgeslagen"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofa moet herstarten om de nieuwe lay-outrichting toe te passen."
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "Sofa moet herstarten om de nieuwe lay-outrichting toe te passen."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "Reiniciar"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "Reiniciar"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "Reinício Necessário"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "Reinício Necessário"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "Ignorado"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
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."
|
||||
#~ 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."
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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 授权。"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart"
|
||||
msgstr "重启"
|
||||
#~ msgid "Restart"
|
||||
#~ msgstr "重启"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Restart Required"
|
||||
msgstr "需要重启"
|
||||
#~ msgid "Restart Required"
|
||||
#~ msgstr "需要重启"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restore"
|
||||
@@ -3042,8 +3042,8 @@ msgid "Skipped"
|
||||
msgstr "已跳过"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofa 需要重启以应用新的布局方向。"
|
||||
#~ msgid "Sofa needs to restart to apply the new layout direction."
|
||||
#~ msgstr "Sofa 需要重启以应用新的布局方向。"
|
||||
|
||||
#: 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."
|
||||
|
||||
Reference in new issue
Block a user