feat: add i18n with LingUI, Crowdin integration, and typed error codes (#16)

* feat: add i18n with LingUI, Crowdin integration, and typed error codes

- Add `@sofa/i18n` shared package with LingUI v5 (v6-ready config using
  `defineConfig` + `@lingui/format-po`), eager English + lazy-loaded
  fr/de/es/it/pt catalogs, `Intl`-based date/number format utilities,
  and test helpers
- Wire `@lingui/vite-plugin` + babel macro plugin for web, and
  `@lingui/metro-transformer` + babel config for native
- Add `I18nProvider` to both app roots with locale auto-detection
  (navigator.language / expo-localization) and persistence
  (localStorage / MMKV)
- Wrap all ~512 user-facing strings across web and native with LingUI
  macros (`<Trans>`, `useLingui`/`t`, `i18n._(msg`...`)`, `plural`)
- Add language switcher to Settings in both apps
- Add `@sofa/api/errors` with 13 typed `AppErrorCode` values and
  `appErrorData()` helper for contract `.errors()` schemas
- Update all oRPC contract error definitions with typed `data` fields;
  convert import procedure `throw new Error()` to `ORPCError` with codes
- Add per-app `error-messages.ts` utilities that map error codes to
  localized strings; update global `QueryCache.onError` handlers to stop
  leaking raw `error.message` to users
- Add `crowdin.yml` config and `.github/workflows/crowdin.yml` for
  automated source upload on merge and translation pull via dispatch
- Add `lingui.config.ts` at repo root with `bun run i18n:extract` and
  `bun run i18n:compile` convenience scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add Crowdin language mapping and ignore context file

* fix(i18n): address 20 localization quality issues

- Stop leaking raw error.message to users in change-password and
  account-section; use localized fallbacks instead
- Add typed error data to discover contract's .errors() schema
- Replace all manual English plural suffixes with LingUI plural() macro
  in episode counts, backup counts, star ratings, title/image counts
- Replace date-fns formatDistanceToNow and hardcoded date patterns with
  locale-aware formatDate/formatRelativeTime from @sofa/i18n/format
- Convert integration-configs from module-scope translations (frozen at
  import time) to lazy getIntegrationConfigs(i18n) function
- Combine split Trans fragments into single translatable units in
  backup-schedule retention sentence and stats-display period labels
- Localize TV media type badge and full episode accessibilityLabel
- Replace Android-incompatible Alert.alert language picker (max 3
  buttons) with zeego DropdownMenu
- Await async locale initialization before hiding splash screen so
  non-English users don't see English flash on cold start
- Add @lingui/core as direct native app dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(i18n): add Intl polyfills for Hermes, replace DropdownMenu language picker, and remove date-fns
- Add `@formatjs/intl-*` polyfills loaded at app startup in `intl-polyfills.ts` so Hermes on Android has full `Intl` support for locale-aware formatting
- Alias `@formatjs/icu-messageformat-parser` to its `no-parser` variant in metro config to reduce bundle size
- Replace the zeego `DropdownMenu` language picker in Settings with a new `SelectModal` component that works reliably on all platforms without the Android 3-button limit
- Replace `date-fns` `format`/`parseISO` in `person/[id].tsx` with `formatDate` from `@sofa/i18n/format`; remove `date-fns` from native and web `package.json`
- Refactor `StatusActionButton` to use a `StatusLabel` component with `<Trans>` rather than a `getStatusConfig(t)` factory so labels are always reactive to locale changes
- Fix crowdin workflow to use root `bun run i18n:compile` script instead of `cd packages/i18n && bun run compile`
- Wrap root layout `GestureHandlerRootView` in `SafeAreaProvider` and add `.catch` to locale-ready promise to prevent unhandled rejections blocking the splash screen

* fix(i18n): localize integration status helpers, fix stats-display Trans wrapping, and clean up SelectModal
- Localize `webhookStatus` and `listStatus` in `integration-card.tsx` using `i18n._(msg`...`)` so status strings are translated instead of always rendering in English
- Wrap "Movies {select}" and "Episodes {select}" in a single `<Trans>` in `stats-display.tsx` so the period selector element is embedded inside the translatable unit rather than concatenated outside it
- Fix locale activation order in Settings: close the modal first, then `activateLocale`, and only call `setPersistedLocale` on success to avoid persisting a locale that failed to load
- Remove the redundant `SafeAreaProvider`/`SafeAreaView` wrapper from `SelectModal` — the root layout already provides `SafeAreaProvider`
- Recompile all six `.po`/`.ts` catalogs to pick up new and updated message strings

* chore(i18n): crowdin sync

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 4.6 authored and GitHub committed 2026-03-18 12:16:10 -04:00
1 parent e50ab44783
commit ad0497ccd6
142 files changed
+18002 -1243

No files matched your search

+1
View File
@@ -6,6 +6,7 @@
"exports": {
"./contract": "./src/contract.ts",
"./schemas": "./src/schemas.ts",
"./errors": "./src/errors.ts",
"./utils": "./src/utils.ts"
},
"scripts": {
+74 -15
View File
@@ -1,5 +1,6 @@
import { eventIterator, oc } from "@orpc/contract";
import { z } from "zod";
import { AppErrorCode, appErrorData } from "./errors";
import {
AuthConfigOutput,
BackupCreateOutput,
@@ -78,7 +79,10 @@ export const contract = {
.input(IdParam)
.output(TitleDetailOutput)
.errors({
NOT_FOUND: { message: "Title not found" },
NOT_FOUND: {
message: "Title not found",
data: appErrorData(AppErrorCode.TITLE_NOT_FOUND),
},
}),
updateStatus: oc
.route({
@@ -163,7 +167,10 @@ export const contract = {
.input(IdParam)
.output(QuickAddOutput)
.errors({
NOT_FOUND: { message: "Title not found" },
NOT_FOUND: {
message: "Title not found",
data: appErrorData(AppErrorCode.TITLE_NOT_FOUND),
},
}),
},
episodes: {
@@ -238,7 +245,10 @@ export const contract = {
.input(IdParam.merge(PaginatedInput))
.output(PersonDetailOutput)
.errors({
NOT_FOUND: { message: "Person not found" },
NOT_FOUND: {
message: "Person not found",
data: appErrorData(AppErrorCode.PERSON_NOT_FOUND),
},
}),
},
dashboard: {
@@ -315,7 +325,10 @@ export const contract = {
.input(TrendingTypeParam.merge(PageParam))
.output(TrendingOutput)
.errors({
PRECONDITION_FAILED: { message: "TMDB API key is not configured" },
PRECONDITION_FAILED: {
message: "TMDB API key is not configured",
data: appErrorData(AppErrorCode.TMDB_NOT_CONFIGURED),
},
}),
popular: oc
.route({
@@ -330,7 +343,10 @@ export const contract = {
.input(MediaTypeParam.merge(PageParam))
.output(PopularOutput)
.errors({
PRECONDITION_FAILED: { message: "TMDB API key is not configured" },
PRECONDITION_FAILED: {
message: "TMDB API key is not configured",
data: appErrorData(AppErrorCode.TMDB_NOT_CONFIGURED),
},
}),
genres: oc
.route({
@@ -345,7 +361,10 @@ export const contract = {
.input(MediaTypeParam)
.output(GenresOutput)
.errors({
PRECONDITION_FAILED: { message: "TMDB API key is not configured" },
PRECONDITION_FAILED: {
message: "TMDB API key is not configured",
data: appErrorData(AppErrorCode.TMDB_NOT_CONFIGURED),
},
}),
},
search: oc
@@ -376,7 +395,10 @@ export const contract = {
.input(DiscoverInput)
.output(DiscoverOutput)
.errors({
PRECONDITION_FAILED: { message: "TMDB API key is not configured" },
PRECONDITION_FAILED: {
message: "TMDB API key is not configured",
data: appErrorData(AppErrorCode.TMDB_NOT_CONFIGURED),
},
}),
system: {
publicInfo: oc
@@ -461,7 +483,10 @@ export const contract = {
.input(ProviderParam)
.output(IntegrationOutput)
.errors({
NOT_FOUND: { message: "Integration not found" },
NOT_FOUND: {
message: "Integration not found",
data: appErrorData(AppErrorCode.INTEGRATION_NOT_FOUND),
},
}),
},
admin: {
@@ -500,8 +525,14 @@ export const contract = {
.input(FilenameParam)
.output(z.void())
.errors({
NOT_FOUND: { message: "Backup not found" },
BAD_REQUEST: { message: "Failed to delete backup" },
NOT_FOUND: {
message: "Backup not found",
data: appErrorData(AppErrorCode.BACKUP_NOT_FOUND),
},
BAD_REQUEST: {
message: "Failed to delete backup",
data: appErrorData(AppErrorCode.BACKUP_DELETE_FAILED),
},
}),
restore: oc
.route({
@@ -515,7 +546,10 @@ export const contract = {
.input(RestoreBackupInput)
.output(z.void())
.errors({
BAD_REQUEST: { message: "Backup restoration failed" },
BAD_REQUEST: {
message: "Backup restoration failed",
data: appErrorData(AppErrorCode.BACKUP_RESTORE_FAILED),
},
}),
schedule: oc
.route({
@@ -616,7 +650,10 @@ export const contract = {
.input(TriggerJobInput)
.output(TriggerJobOutput)
.errors({
NOT_FOUND: { message: "Job not found" },
NOT_FOUND: {
message: "Job not found",
data: appErrorData(AppErrorCode.JOB_NOT_FOUND),
},
}),
purgeMetadataCache: oc
.route({
@@ -701,7 +738,13 @@ export const contract = {
successDescription: "Preview of importable items with counts",
})
.input(ParseFileInput)
.output(ImportPreviewSchema),
.output(ImportPreviewSchema)
.errors({
BAD_REQUEST: {
message: "Invalid import file",
data: appErrorData(AppErrorCode.IMPORT_INVALID_FILE),
},
}),
parsePayload: oc
.route({
method: "POST",
@@ -725,7 +768,17 @@ export const contract = {
successDescription: "Created import job",
})
.input(CreateImportJobInput)
.output(ImportJobSchema),
.output(ImportJobSchema)
.errors({
BAD_REQUEST: {
message: "Import payload too large",
data: appErrorData(AppErrorCode.IMPORT_PAYLOAD_TOO_LARGE),
},
CONFLICT: {
message: "An import is already in progress",
data: appErrorData(AppErrorCode.IMPORT_ALREADY_RUNNING),
},
}),
getJob: oc
.route({
method: "GET",
@@ -748,7 +801,13 @@ export const contract = {
successDescription: "Updated job state",
})
.input(IdParam)
.output(ImportJobSchema),
.output(ImportJobSchema)
.errors({
BAD_REQUEST: {
message: "Import cannot be cancelled",
data: appErrorData(AppErrorCode.IMPORT_CANNOT_CANCEL),
},
}),
jobEvents: oc
.route({
method: "GET",
+23
View File
@@ -0,0 +1,23 @@
import { z } from "zod";
export const AppErrorCode = {
TITLE_NOT_FOUND: "TITLE_NOT_FOUND",
PERSON_NOT_FOUND: "PERSON_NOT_FOUND",
INTEGRATION_NOT_FOUND: "INTEGRATION_NOT_FOUND",
BACKUP_NOT_FOUND: "BACKUP_NOT_FOUND",
BACKUP_DELETE_FAILED: "BACKUP_DELETE_FAILED",
BACKUP_RESTORE_FAILED: "BACKUP_RESTORE_FAILED",
JOB_NOT_FOUND: "JOB_NOT_FOUND",
TMDB_NOT_CONFIGURED: "TMDB_NOT_CONFIGURED",
IMPORT_INVALID_FILE: "IMPORT_INVALID_FILE",
IMPORT_PAYLOAD_TOO_LARGE: "IMPORT_PAYLOAD_TOO_LARGE",
IMPORT_ALREADY_RUNNING: "IMPORT_ALREADY_RUNNING",
IMPORT_CANNOT_CANCEL: "IMPORT_CANNOT_CANCEL",
REGISTRATION_CLOSED: "REGISTRATION_CLOSED",
} as const;
export type AppErrorCode = (typeof AppErrorCode)[keyof typeof AppErrorCode];
/** Typed error data schema for use in contract `.errors()` */
export const appErrorData = <T extends AppErrorCode>(code: T) =>
z.object({ code: z.literal(code) });
-1
View File
@@ -38,7 +38,6 @@
"@sofa/logger": "workspace:*",
"@sofa/tmdb": "workspace:*",
"adm-zip": "0.5.16",
"date-fns": "catalog:",
"node-vibrant": "4.0.4",
"sharp": "0.34.5",
"thumbhash": "catalog:",
+7 -3
View File
@@ -7,7 +7,11 @@ import { closeDatabase, db } from "@sofa/db/client";
import { sql } from "@sofa/db/helpers";
import { runMigrations } from "@sofa/db/migrate";
import { createLogger } from "@sofa/logger";
import { format } from "date-fns";
function formatTimestamp(date: Date): string {
const p = (n: number, len = 2) => String(n).padStart(len, "0");
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}${p(date.getMilliseconds(), 3)}`;
}
const log = createLogger("backup");
@@ -138,7 +142,7 @@ export function isValidBackupFilename(filename: string): boolean {
async function createBackupInternal(prefix: BackupPrefix): Promise<BackupInfo> {
await ensureBackupDir();
const timestamp = format(new Date(), "yyyy-MM-dd-HHmmssSSS");
const timestamp = formatTimestamp(new Date());
const filename = `${prefix}-${timestamp}.db`;
const dest = path.join(BACKUP_DIR, filename);
@@ -223,7 +227,7 @@ export async function restoreFromBackup(
const dbDir = path.dirname(DATABASE_URL);
await mkdir(dbDir, { recursive: true });
const timestamp = format(new Date(), "yyyy-MM-dd-HHmmssSSS");
const timestamp = formatTimestamp(new Date());
const tempPath = path.join(
dbDir,
`.restore-temp-${timestamp}-${crypto.randomUUID()}.db`,
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@sofa/i18n",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./locales": "./src/locales.ts",
"./format": "./src/format.ts",
"./test-utils": "./src/test-utils.tsx"
},
"scripts": {
"lint": "biome check",
"format": "biome format --write",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@lingui/core": "catalog:",
"@lingui/react": "catalog:"
},
"devDependencies": {
"@lingui/cli": "catalog:",
"@lingui/conf": "catalog:",
"@lingui/format-po": "catalog:",
"@types/bun": "catalog:",
"typescript": "catalog:"
}
}
+70
View File
@@ -0,0 +1,70 @@
import { i18n } from "./index";
/** Whether a string looks like a date-only value (no time component). */
function isDateOnly(value: string): boolean {
return /^\d{4}-\d{2}-\d{2}$/.test(value);
}
function toDate(date: Date | string): Date {
return typeof date === "string" ? new Date(date) : date;
}
export function formatDate(
date: Date | string,
options?: Intl.DateTimeFormatOptions,
): string {
// Date-only ISO strings (e.g. "1990-05-15") are parsed as UTC midnight.
// Format in UTC so users west of UTC don't see the previous day.
const useUTC =
typeof date === "string" && isDateOnly(date) && !options?.timeZone;
return new Intl.DateTimeFormat(i18n.locale, {
year: "numeric",
month: "long",
day: "numeric",
...(useUTC ? { timeZone: "UTC" } : {}),
...options,
}).format(toDate(date));
}
export function formatShortDate(date: Date | string): string {
return new Intl.DateTimeFormat(i18n.locale, {
month: "short",
day: "numeric",
year: "numeric",
}).format(toDate(date));
}
export function formatRelativeTime(date: Date | string): string {
const d = toDate(date);
const now = Date.now();
const diffMs = d.getTime() - now;
const absSec = Math.abs(Math.round(diffMs / 1000));
const absMin = Math.abs(Math.round(diffMs / 60_000));
const absHour = Math.abs(Math.round(diffMs / 3_600_000));
const absDay = Math.abs(Math.round(diffMs / 86_400_000));
const sign = diffMs < 0 ? -1 : 1;
const rtf = new Intl.RelativeTimeFormat(i18n.locale, { numeric: "auto" });
if (absSec < 60) return rtf.format(sign * absSec, "second");
if (absMin < 60) return rtf.format(sign * absMin, "minute");
if (absHour < 24) return rtf.format(sign * absHour, "hour");
if (absDay < 30) return rtf.format(sign * absDay, "day");
if (absDay < 365) return rtf.format(sign * Math.round(absDay / 30), "month");
return rtf.format(sign * Math.round(absDay / 365), "year");
}
export function formatNumber(
n: number,
options?: Intl.NumberFormatOptions,
): string {
return new Intl.NumberFormat(i18n.locale, options).format(n);
}
export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const value = bytes / k ** i;
return `${new Intl.NumberFormat(i18n.locale, { maximumFractionDigits: 1 }).format(value)} ${sizes[i]}`;
}
+34
View File
@@ -0,0 +1,34 @@
import { i18n, type Messages } from "@lingui/core";
import { messages as enMessages } from "./po/en";
export const SUPPORTED_LOCALES = ["en", "fr", "de", "es", "it", "pt"] as const;
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
// English always bundled — zero async on default locale
i18n.load("en", enMessages);
i18n.activate("en");
const loaders: Record<string, () => Promise<{ messages: Messages }>> = {
fr: () => import("./po/fr"),
de: () => import("./po/de"),
es: () => import("./po/es"),
it: () => import("./po/it"),
pt: () => import("./po/pt"),
};
export async function activateLocale(locale: SupportedLocale): Promise<void> {
if (locale === "en") {
i18n.activate("en");
return;
}
const loader = loaders[locale];
if (!loader) {
i18n.activate("en");
return;
}
const { messages } = await loader();
i18n.load(locale, messages);
i18n.activate(locale);
}
export { i18n };
+16
View File
@@ -0,0 +1,16 @@
import type { SupportedLocale } from "./index";
export interface LocaleInfo {
code: SupportedLocale;
name: string;
nativeName: string;
}
export const LOCALE_INFO: LocaleInfo[] = [
{ code: "en", name: "English", nativeName: "English" },
{ code: "fr", name: "French", nativeName: "Fran\u00e7ais" },
{ code: "de", name: "German", nativeName: "Deutsch" },
{ code: "es", name: "Spanish", nativeName: "Espa\u00f1ol" },
{ code: "it", name: "Italian", nativeName: "Italiano" },
{ code: "pt", name: "Portuguese", nativeName: "Portugu\u00eas" },
];
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+5kO8P\":[\"Saturday\"],\"+6i0lS\":[\"Retrieving your watch history, watchlist, and ratings...\"],\"+Cv+V9\":[\"Remove from library\"],\"+JkEpu\":[\"This page was left on the cutting room floor. It may have been moved, removed, or never made it past the screenplay.\"],\"+K0AvT\":[\"Disconnect\"],\"+N0l5/\":[\"Connected to <0>\",[\"serverHost\"],\"</0>. Tap to change.\"],\"+gLHYi\":[\"Failed to load title\"],\"+j1ex/\":[\"Failed to remove from library\"],\"+nF1ZO\":[\"Remove Photo\"],\"/+6dvC\":[\"Errors (\",[\"0\"],\")\"],\"/BBXeA\":[\"Connecting to server...\"],\"/DwR+n\":[\"Creating…\"],\"/XtbPO\":[[\"0\"],\" titles\"],\"/cg+QV\":[\"Watch all\"],\"/gQXGv\":[\"Failed to update status\"],\"/nT6AE\":[\"New password\"],\"/sHc1/\":[\"Update check\"],\"0+dyau\":[\"Failed to update retention setting\"],\"0BFJKK\":[\"Authorization was denied. Please try again.\"],\"0GHb20\":[\"Purge metadata cache?\"],\"0IBW21\":[\"Failed to purge caches\"],\"0gH/sc\":[\"Remove profile picture\"],\"1+P9RR\":[\"Switch to \",[\"0\"]],\"12cc1j\":[\"Watching\"],\"12lVOl\":[\"Self-hosted movie & TV tracker\"],\"1B4z0M\":[\"Filmography\"],\"1J4Ek0\":[\"Automatically back up your database on a schedule\"],\"1JhxXW\":[\"Episode \",[\"0\"],\", \",[\"episodeLabel\"]],\"1N6wNP\":[\"Import options\"],\"1Qz4uG\":[\"Enable the \\\"Playback\\\" event category\"],\"1hMWR6\":[\"Create account\"],\"1jHIjh\":[\"Watched all of \",[\"seasonName\"]],\"1vSYsG\":[\"Download backup\"],\"1wL1tj\":[\"TV show\"],\"2FletP\":[\"Purged \",[\"0\",\"plural\",{\"one\":[\"#\",\" title\"],\"other\":[\"#\",\" titles\"]}],\", \",[\"1\",\"plural\",{\"one\":[\"#\",\" person\"],\"other\":[\"#\",\" persons\"]}],\", \",[\"2\",\"plural\",{\"one\":[\"#\",\" file\"],\"other\":[\"#\",\" files\"]}],\" (\",[\"freed\"],\" freed)\"],\"2Fsd9r\":[\"This Month\"],\"2MPcep\":[\"Import complete\"],\"2Mu33Z\":[\"Cache management\"],\"2POOFK\":[\"Free\"],\"2Pt2NY\":[\"This will remove all items from your history.\"],\"2fCpt5\":[\"Return home\"],\"2pPBp6\":[\"Trending today\"],\"39y5bn\":[\"Friday\"],\"3Blefz\":[\"Ratings\"],\"3JTlG8\":[\"Recent Searches\"],\"3Jy8bM\":[\"Movies \",[\"select\"]],\"3L9OuQ\":[\"Episode \",[\"0\"]],\"3T8ziB\":[\"Create an account\"],\"3hELxX\":[\"Enter your Sofa server URL to get started\"],\"4fxLkp\":[\"Open Sonarr, go to <0>Settings > Import Lists</0>\"],\"4kpBqM\":[\"Last event \",[\"0\"]],\"4uUjVO\":[\"Producer\"],\"4x+A56\":[\"Anonymous usage reporting\"],\"5IShvp\":[\"Import \",[\"totalItems\"],\" items\"],\"5IYJSv\":[\"Explore titles\"],\"5Y4mym\":[\"Import from \",[\"0\"]],\"5ZzgbQ\":[\"Connect \",[\"0\"]],\"5fEnbK\":[\"Failed to update scheduled backup setting\"],\"5lWFkC\":[\"Sign in\"],\"5v9C16\":[\"Connected to \",[\"source\"]],\"623bR4\":[\"Failed to trigger job\"],\"6HN0yh\":[\"Open Plex, go to Settings > Webhooks\"],\"6TNjOJ\":[\"Failed to update rating\"],\"6TfUy6\":[\"last \",[\"value\"]],\"6V3Ea3\":[\"Copied\"],\"6YtxFj\":[\"Name\"],\"6exX+8\":[\"Regenerate\"],\"6gRgw8\":[\"Retry\"],\"6lGV3K\":[\"Show less\"],\"76++pR\":[\"Warnings\"],\"77DIAu\":[\"Go to Dashboard > Plugins > Webhook\"],\"7Bj3x9\":[\"Failed\"],\"7D50KC\":[[\"label\"],\" disconnected\"],\"7GfM5w\":[\"Recently Viewed\"],\"7L01XJ\":[\"Actions\"],\"7eMo+U\":[\"Go Home\"],\"7p5kLi\":[\"Dashboard\"],\"85eoJ1\":[\"Schedule updated\"],\"8B9E2D\":[\"Day:\"],\"8YwF1J\":[\"Go to <0>Dashboard > Plugins > Webhook</0>\"],\"8ZsakT\":[\"Password\"],\"8tjQCz\":[\"Explore\"],\"8vNtLy\":[\"Paste the Radarr URL above into the List URL field\"],\"8wYDMp\":[\"Already have an account?\"],\"9AE3vb\":[\"Open Plex, go to <0>Settings > Webhooks<1/></0>\"],\"9GW0ZB\":[\"Import is still running in the background. Check back later.\"],\"9NyAH9\":[\"Skipped\"],\"9Xhrps\":[\"Failed to connect\"],\"9ZLGbA\":[\"backups.\"],\"9eF5oV\":[\"Welcome back\"],\"9rG25a\":[\"Server URL\"],\"A+0rLe\":[\"Marked as completed\"],\"AOHgZp\":[\"Episodes\"],\"AOddWK\":[\"Connect \",[\"label\"]],\"ATfxL/\":[\"Trailer\"],\"AVlZoM\":[\"Environment\"],\"Acf6vF\":[\"Save name\"],\"AdoUfN\":[\"Failed to add to watchlist\"],\"AeXO77\":[\"Account\"],\"AjtYj0\":[\"On Watchlist\"],\"Avee+B\":[\"Failed to update name\"],\"B2R3xD\":[\"Toggle scheduled backups\"],\"BEVzjL\":[\"Import failed\"],\"BQnS5I\":[\"Open \",[\"source\"],\" to enter code\"],\"BUnoSB\":[[\"0\"],\" \",[\"1\",\"plural\",{\"one\":[\"backup\"],\"other\":[\"backups\"]}],\" stored\"],\"BpttUI\":[\"Trending Today\"],\"BrrIs8\":[\"Storage\"],\"BskWMl\":[\"Unreachable\"],\"BzEFor\":[\"or\"],\"CGDHFb\":[\"Add a <0>Generic Destination</0> and paste the URL above\"],\"CGExDN\":[\"Failed to purge image cache\"],\"CHeXFE\":[\"Status updated\"],\"CKyk7Q\":[\"Go back\"],\"CaB/+I\":[\"Welcome back, \",[\"name\"]],\"CodnUh\":[\"Removed from library\"],\"CpzGJY\":[\"This may take a few minutes for large libraries. Please don't close this tab.\"],\"CuPxpd\":[\"Requires an active <0>PlexLine truncated
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+5kO8P\":[\"Saturday\"],\"+6i0lS\":[\"Retrieving your watch history, watchlist, and ratings...\"],\"+Cv+V9\":[\"Remove from library\"],\"+JkEpu\":[\"This page was left on the cutting room floor. It may have been moved, removed, or never made it past the screenplay.\"],\"+K0AvT\":[\"Disconnect\"],\"+N0l5/\":[\"Connected to <0>\",[\"serverHost\"],\"</0>. Tap to change.\"],\"+gLHYi\":[\"Failed to load title\"],\"+j1ex/\":[\"Failed to remove from library\"],\"+nF1ZO\":[\"Remove Photo\"],\"/+6dvC\":[\"Errors (\",[\"0\"],\")\"],\"/BBXeA\":[\"Connecting to server...\"],\"/DwR+n\":[\"Creating…\"],\"/XtbPO\":[[\"0\"],\" titles\"],\"/cg+QV\":[\"Watch all\"],\"/gQXGv\":[\"Failed to update status\"],\"/nT6AE\":[\"New password\"],\"/sHc1/\":[\"Update check\"],\"0+dyau\":[\"Failed to update retention setting\"],\"0BFJKK\":[\"Authorization was denied. Please try again.\"],\"0GHb20\":[\"Purge metadata cache?\"],\"0IBW21\":[\"Failed to purge caches\"],\"0gH/sc\":[\"Remove profile picture\"],\"1+P9RR\":[\"Switch to \",[\"0\"]],\"12cc1j\":[\"Watching\"],\"12lVOl\":[\"Self-hosted movie & TV tracker\"],\"1B4z0M\":[\"Filmography\"],\"1J4Ek0\":[\"Automatically back up your database on a schedule\"],\"1JhxXW\":[\"Episode \",[\"0\"],\", \",[\"episodeLabel\"]],\"1N6wNP\":[\"Import options\"],\"1Qz4uG\":[\"Enable the \\\"Playback\\\" event category\"],\"1hMWR6\":[\"Create account\"],\"1jHIjh\":[\"Watched all of \",[\"seasonName\"]],\"1vSYsG\":[\"Download backup\"],\"1wL1tj\":[\"TV show\"],\"2FletP\":[\"Purged \",[\"0\",\"plural\",{\"one\":[\"#\",\" title\"],\"other\":[\"#\",\" titles\"]}],\", \",[\"1\",\"plural\",{\"one\":[\"#\",\" person\"],\"other\":[\"#\",\" persons\"]}],\", \",[\"2\",\"plural\",{\"one\":[\"#\",\" file\"],\"other\":[\"#\",\" files\"]}],\" (\",[\"freed\"],\" freed)\"],\"2Fsd9r\":[\"This Month\"],\"2MPcep\":[\"Import complete\"],\"2Mu33Z\":[\"Cache management\"],\"2POOFK\":[\"Free\"],\"2Pt2NY\":[\"This will remove all items from your history.\"],\"2fCpt5\":[\"Return home\"],\"2pPBp6\":[\"Trending today\"],\"39y5bn\":[\"Friday\"],\"3Blefz\":[\"Ratings\"],\"3JTlG8\":[\"Recent Searches\"],\"3Jy8bM\":[\"Movies \",[\"select\"]],\"3L9OuQ\":[\"Episode \",[\"0\"]],\"3T8ziB\":[\"Create an account\"],\"3hELxX\":[\"Enter your Sofa server URL to get started\"],\"4fxLkp\":[\"Open Sonarr, go to <0>Settings > Import Lists</0>\"],\"4kpBqM\":[\"Last event \",[\"0\"]],\"4uUjVO\":[\"Producer\"],\"4x+A56\":[\"Anonymous usage reporting\"],\"5IShvp\":[\"Import \",[\"totalItems\"],\" items\"],\"5IYJSv\":[\"Explore titles\"],\"5Y4mym\":[\"Import from \",[\"0\"]],\"5ZzgbQ\":[\"Connect \",[\"0\"]],\"5fEnbK\":[\"Failed to update scheduled backup setting\"],\"5lWFkC\":[\"Sign in\"],\"5v9C16\":[\"Connected to \",[\"source\"]],\"623bR4\":[\"Failed to trigger job\"],\"6HN0yh\":[\"Open Plex, go to Settings > Webhooks\"],\"6TNjOJ\":[\"Failed to update rating\"],\"6TfUy6\":[\"last \",[\"value\"]],\"6V3Ea3\":[\"Copied\"],\"6YtxFj\":[\"Name\"],\"6exX+8\":[\"Regenerate\"],\"6gRgw8\":[\"Retry\"],\"6lGV3K\":[\"Show less\"],\"76++pR\":[\"Warnings\"],\"77DIAu\":[\"Go to Dashboard > Plugins > Webhook\"],\"7Bj3x9\":[\"Failed\"],\"7D50KC\":[[\"label\"],\" disconnected\"],\"7GfM5w\":[\"Recently Viewed\"],\"7L01XJ\":[\"Actions\"],\"7eMo+U\":[\"Go Home\"],\"7p5kLi\":[\"Dashboard\"],\"85eoJ1\":[\"Schedule updated\"],\"8B9E2D\":[\"Day:\"],\"8YwF1J\":[\"Go to <0>Dashboard > Plugins > Webhook</0>\"],\"8ZsakT\":[\"Password\"],\"8tjQCz\":[\"Explore\"],\"8vNtLy\":[\"Paste the Radarr URL above into the List URL field\"],\"8wYDMp\":[\"Already have an account?\"],\"9AE3vb\":[\"Open Plex, go to <0>Settings > Webhooks<1/></0>\"],\"9GW0ZB\":[\"Import is still running in the background. Check back later.\"],\"9NyAH9\":[\"Skipped\"],\"9Xhrps\":[\"Failed to connect\"],\"9ZLGbA\":[\"backups.\"],\"9eF5oV\":[\"Welcome back\"],\"9rG25a\":[\"Server URL\"],\"A+0rLe\":[\"Marked as completed\"],\"AOHgZp\":[\"Episodes\"],\"AOddWK\":[\"Connect \",[\"label\"]],\"ATfxL/\":[\"Trailer\"],\"AVlZoM\":[\"Environment\"],\"Acf6vF\":[\"Save name\"],\"AdoUfN\":[\"Failed to add to watchlist\"],\"AeXO77\":[\"Account\"],\"AjtYj0\":[\"On Watchlist\"],\"Avee+B\":[\"Failed to update name\"],\"B2R3xD\":[\"Toggle scheduled backups\"],\"BEVzjL\":[\"Import failed\"],\"BQnS5I\":[\"Open \",[\"source\"],\" to enter code\"],\"BUnoSB\":[[\"0\"],\" \",[\"1\",\"plural\",{\"one\":[\"backup\"],\"other\":[\"backups\"]}],\" stored\"],\"BpttUI\":[\"Trending Today\"],\"BrrIs8\":[\"Storage\"],\"BskWMl\":[\"Unreachable\"],\"BzEFor\":[\"or\"],\"CGDHFb\":[\"Add a <0>Generic Destination</0> and paste the URL above\"],\"CGExDN\":[\"Failed to purge image cache\"],\"CHeXFE\":[\"Status updated\"],\"CKyk7Q\":[\"Go back\"],\"CaB/+I\":[\"Welcome back, \",[\"name\"]],\"CodnUh\":[\"Removed from library\"],\"CpzGJY\":[\"This may take a few minutes for large libraries. Please don't close this tab.\"],\"CuPxpd\":[\"Requires an active <0>PlexLine truncated
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+5kO8P\":[\"Saturday\"],\"+6i0lS\":[\"Retrieving your watch history, watchlist, and ratings...\"],\"+Cv+V9\":[\"Remove from library\"],\"+JkEpu\":[\"This page was left on the cutting room floor. It may have been moved, removed, or never made it past the screenplay.\"],\"+K0AvT\":[\"Disconnect\"],\"+N0l5/\":[\"Connected to <0>\",[\"serverHost\"],\"</0>. Tap to change.\"],\"+gLHYi\":[\"Failed to load title\"],\"+j1ex/\":[\"Failed to remove from library\"],\"+nF1ZO\":[\"Remove Photo\"],\"/+6dvC\":[\"Errors (\",[\"0\"],\")\"],\"/BBXeA\":[\"Connecting to server...\"],\"/DwR+n\":[\"Creating…\"],\"/XtbPO\":[[\"0\"],\" titles\"],\"/cg+QV\":[\"Watch all\"],\"/gQXGv\":[\"Failed to update status\"],\"/nT6AE\":[\"New password\"],\"/sHc1/\":[\"Update check\"],\"0+dyau\":[\"Failed to update retention setting\"],\"0BFJKK\":[\"Authorization was denied. Please try again.\"],\"0GHb20\":[\"Purge metadata cache?\"],\"0IBW21\":[\"Failed to purge caches\"],\"0gH/sc\":[\"Remove profile picture\"],\"1+P9RR\":[\"Switch to \",[\"0\"]],\"12cc1j\":[\"Watching\"],\"12lVOl\":[\"Self-hosted movie & TV tracker\"],\"1B4z0M\":[\"Filmography\"],\"1J4Ek0\":[\"Automatically back up your database on a schedule\"],\"1JhxXW\":[\"Episode \",[\"0\"],\", \",[\"episodeLabel\"]],\"1N6wNP\":[\"Import options\"],\"1Qz4uG\":[\"Enable the \\\"Playback\\\" event category\"],\"1hMWR6\":[\"Create account\"],\"1jHIjh\":[\"Watched all of \",[\"seasonName\"]],\"1vSYsG\":[\"Download backup\"],\"1wL1tj\":[\"TV show\"],\"2FletP\":[\"Purged \",[\"0\",\"plural\",{\"one\":[\"#\",\" title\"],\"other\":[\"#\",\" titles\"]}],\", \",[\"1\",\"plural\",{\"one\":[\"#\",\" person\"],\"other\":[\"#\",\" persons\"]}],\", \",[\"2\",\"plural\",{\"one\":[\"#\",\" file\"],\"other\":[\"#\",\" files\"]}],\" (\",[\"freed\"],\" freed)\"],\"2Fsd9r\":[\"This Month\"],\"2MPcep\":[\"Import complete\"],\"2Mu33Z\":[\"Cache management\"],\"2POOFK\":[\"Free\"],\"2Pt2NY\":[\"This will remove all items from your history.\"],\"2fCpt5\":[\"Return home\"],\"2pPBp6\":[\"Trending today\"],\"39y5bn\":[\"Friday\"],\"3Blefz\":[\"Ratings\"],\"3JTlG8\":[\"Recent Searches\"],\"3Jy8bM\":[\"Movies \",[\"select\"]],\"3L9OuQ\":[\"Episode \",[\"0\"]],\"3T8ziB\":[\"Create an account\"],\"3hELxX\":[\"Enter your Sofa server URL to get started\"],\"4fxLkp\":[\"Open Sonarr, go to <0>Settings > Import Lists</0>\"],\"4kpBqM\":[\"Last event \",[\"0\"]],\"4uUjVO\":[\"Producer\"],\"4x+A56\":[\"Anonymous usage reporting\"],\"5IShvp\":[\"Import \",[\"totalItems\"],\" items\"],\"5IYJSv\":[\"Explore titles\"],\"5Y4mym\":[\"Import from \",[\"0\"]],\"5ZzgbQ\":[\"Connect \",[\"0\"]],\"5fEnbK\":[\"Failed to update scheduled backup setting\"],\"5lWFkC\":[\"Sign in\"],\"5v9C16\":[\"Connected to \",[\"source\"]],\"623bR4\":[\"Failed to trigger job\"],\"6HN0yh\":[\"Open Plex, go to Settings > Webhooks\"],\"6TNjOJ\":[\"Failed to update rating\"],\"6TfUy6\":[\"last \",[\"value\"]],\"6V3Ea3\":[\"Copied\"],\"6YtxFj\":[\"Name\"],\"6exX+8\":[\"Regenerate\"],\"6gRgw8\":[\"Retry\"],\"6lGV3K\":[\"Show less\"],\"76++pR\":[\"Warnings\"],\"77DIAu\":[\"Go to Dashboard > Plugins > Webhook\"],\"7Bj3x9\":[\"Failed\"],\"7D50KC\":[[\"label\"],\" disconnected\"],\"7GfM5w\":[\"Recently Viewed\"],\"7L01XJ\":[\"Actions\"],\"7eMo+U\":[\"Go Home\"],\"7p5kLi\":[\"Dashboard\"],\"85eoJ1\":[\"Schedule updated\"],\"8B9E2D\":[\"Day:\"],\"8YwF1J\":[\"Go to <0>Dashboard > Plugins > Webhook</0>\"],\"8ZsakT\":[\"Password\"],\"8tjQCz\":[\"Explore\"],\"8vNtLy\":[\"Paste the Radarr URL above into the List URL field\"],\"8wYDMp\":[\"Already have an account?\"],\"9AE3vb\":[\"Open Plex, go to <0>Settings > Webhooks<1/></0>\"],\"9GW0ZB\":[\"Import is still running in the background. Check back later.\"],\"9NyAH9\":[\"Skipped\"],\"9Xhrps\":[\"Failed to connect\"],\"9ZLGbA\":[\"backups.\"],\"9eF5oV\":[\"Welcome back\"],\"9rG25a\":[\"Server URL\"],\"A+0rLe\":[\"Marked as completed\"],\"AOHgZp\":[\"Episodes\"],\"AOddWK\":[\"Connect \",[\"label\"]],\"ATfxL/\":[\"Trailer\"],\"AVlZoM\":[\"Environment\"],\"Acf6vF\":[\"Save name\"],\"AdoUfN\":[\"Failed to add to watchlist\"],\"AeXO77\":[\"Account\"],\"AjtYj0\":[\"On Watchlist\"],\"Avee+B\":[\"Failed to update name\"],\"B2R3xD\":[\"Toggle scheduled backups\"],\"BEVzjL\":[\"Import failed\"],\"BQnS5I\":[\"Open \",[\"source\"],\" to enter code\"],\"BUnoSB\":[[\"0\"],\" \",[\"1\",\"plural\",{\"one\":[\"backup\"],\"other\":[\"backups\"]}],\" stored\"],\"BpttUI\":[\"Trending Today\"],\"BrrIs8\":[\"Storage\"],\"BskWMl\":[\"Unreachable\"],\"BzEFor\":[\"or\"],\"CGDHFb\":[\"Add a <0>Generic Destination</0> and paste the URL above\"],\"CGExDN\":[\"Failed to purge image cache\"],\"CHeXFE\":[\"Status updated\"],\"CKyk7Q\":[\"Go back\"],\"CaB/+I\":[\"Welcome back, \",[\"name\"]],\"CodnUh\":[\"Removed from library\"],\"CpzGJY\":[\"This may take a few minutes for large libraries. Please don't close this tab.\"],\"CuPxpd\":[\"Requires an active <0>PlexLine truncated
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+5kO8P\":[\"Saturday\"],\"+6i0lS\":[\"Retrieving your watch history, watchlist, and ratings...\"],\"+Cv+V9\":[\"Remove from library\"],\"+JkEpu\":[\"This page was left on the cutting room floor. It may have been moved, removed, or never made it past the screenplay.\"],\"+K0AvT\":[\"Disconnect\"],\"+N0l5/\":[\"Connected to <0>\",[\"serverHost\"],\"</0>. Tap to change.\"],\"+gLHYi\":[\"Failed to load title\"],\"+j1ex/\":[\"Failed to remove from library\"],\"+nF1ZO\":[\"Remove Photo\"],\"/+6dvC\":[\"Errors (\",[\"0\"],\")\"],\"/BBXeA\":[\"Connecting to server...\"],\"/DwR+n\":[\"Creating…\"],\"/XtbPO\":[[\"0\"],\" titles\"],\"/cg+QV\":[\"Watch all\"],\"/gQXGv\":[\"Failed to update status\"],\"/nT6AE\":[\"New password\"],\"/sHc1/\":[\"Update check\"],\"0+dyau\":[\"Failed to update retention setting\"],\"0BFJKK\":[\"Authorization was denied. Please try again.\"],\"0GHb20\":[\"Purge metadata cache?\"],\"0IBW21\":[\"Failed to purge caches\"],\"0gH/sc\":[\"Remove profile picture\"],\"1+P9RR\":[\"Switch to \",[\"0\"]],\"12cc1j\":[\"Watching\"],\"12lVOl\":[\"Self-hosted movie & TV tracker\"],\"1B4z0M\":[\"Filmography\"],\"1J4Ek0\":[\"Automatically back up your database on a schedule\"],\"1JhxXW\":[\"Episode \",[\"0\"],\", \",[\"episodeLabel\"]],\"1N6wNP\":[\"Import options\"],\"1Qz4uG\":[\"Enable the \\\"Playback\\\" event category\"],\"1hMWR6\":[\"Create account\"],\"1jHIjh\":[\"Watched all of \",[\"seasonName\"]],\"1vSYsG\":[\"Download backup\"],\"1wL1tj\":[\"TV show\"],\"2FletP\":[\"Purged \",[\"0\",\"plural\",{\"one\":[\"#\",\" title\"],\"other\":[\"#\",\" titles\"]}],\", \",[\"1\",\"plural\",{\"one\":[\"#\",\" person\"],\"other\":[\"#\",\" persons\"]}],\", \",[\"2\",\"plural\",{\"one\":[\"#\",\" file\"],\"other\":[\"#\",\" files\"]}],\" (\",[\"freed\"],\" freed)\"],\"2Fsd9r\":[\"This Month\"],\"2MPcep\":[\"Import complete\"],\"2Mu33Z\":[\"Cache management\"],\"2POOFK\":[\"Free\"],\"2Pt2NY\":[\"This will remove all items from your history.\"],\"2fCpt5\":[\"Return home\"],\"2pPBp6\":[\"Trending today\"],\"39y5bn\":[\"Friday\"],\"3Blefz\":[\"Ratings\"],\"3JTlG8\":[\"Recent Searches\"],\"3Jy8bM\":[\"Movies \",[\"select\"]],\"3L9OuQ\":[\"Episode \",[\"0\"]],\"3T8ziB\":[\"Create an account\"],\"3hELxX\":[\"Enter your Sofa server URL to get started\"],\"4fxLkp\":[\"Open Sonarr, go to <0>Settings > Import Lists</0>\"],\"4kpBqM\":[\"Last event \",[\"0\"]],\"4uUjVO\":[\"Producer\"],\"4x+A56\":[\"Anonymous usage reporting\"],\"5IShvp\":[\"Import \",[\"totalItems\"],\" items\"],\"5IYJSv\":[\"Explore titles\"],\"5Y4mym\":[\"Import from \",[\"0\"]],\"5ZzgbQ\":[\"Connect \",[\"0\"]],\"5fEnbK\":[\"Failed to update scheduled backup setting\"],\"5lWFkC\":[\"Sign in\"],\"5v9C16\":[\"Connected to \",[\"source\"]],\"623bR4\":[\"Failed to trigger job\"],\"6HN0yh\":[\"Open Plex, go to Settings > Webhooks\"],\"6TNjOJ\":[\"Failed to update rating\"],\"6TfUy6\":[\"last \",[\"value\"]],\"6V3Ea3\":[\"Copied\"],\"6YtxFj\":[\"Name\"],\"6exX+8\":[\"Regenerate\"],\"6gRgw8\":[\"Retry\"],\"6lGV3K\":[\"Show less\"],\"76++pR\":[\"Warnings\"],\"77DIAu\":[\"Go to Dashboard > Plugins > Webhook\"],\"7Bj3x9\":[\"Failed\"],\"7D50KC\":[[\"label\"],\" disconnected\"],\"7GfM5w\":[\"Recently Viewed\"],\"7L01XJ\":[\"Actions\"],\"7eMo+U\":[\"Go Home\"],\"7p5kLi\":[\"Dashboard\"],\"85eoJ1\":[\"Schedule updated\"],\"8B9E2D\":[\"Day:\"],\"8YwF1J\":[\"Go to <0>Dashboard > Plugins > Webhook</0>\"],\"8ZsakT\":[\"Password\"],\"8tjQCz\":[\"Explore\"],\"8vNtLy\":[\"Paste the Radarr URL above into the List URL field\"],\"8wYDMp\":[\"Already have an account?\"],\"9AE3vb\":[\"Open Plex, go to <0>Settings > Webhooks<1/></0>\"],\"9GW0ZB\":[\"Import is still running in the background. Check back later.\"],\"9NyAH9\":[\"Skipped\"],\"9Xhrps\":[\"Failed to connect\"],\"9ZLGbA\":[\"backups.\"],\"9eF5oV\":[\"Welcome back\"],\"9rG25a\":[\"Server URL\"],\"A+0rLe\":[\"Marked as completed\"],\"AOHgZp\":[\"Episodes\"],\"AOddWK\":[\"Connect \",[\"label\"]],\"ATfxL/\":[\"Trailer\"],\"AVlZoM\":[\"Environment\"],\"Acf6vF\":[\"Save name\"],\"AdoUfN\":[\"Failed to add to watchlist\"],\"AeXO77\":[\"Account\"],\"AjtYj0\":[\"On Watchlist\"],\"Avee+B\":[\"Failed to update name\"],\"B2R3xD\":[\"Toggle scheduled backups\"],\"BEVzjL\":[\"Import failed\"],\"BQnS5I\":[\"Open \",[\"source\"],\" to enter code\"],\"BUnoSB\":[[\"0\"],\" \",[\"1\",\"plural\",{\"one\":[\"backup\"],\"other\":[\"backups\"]}],\" stored\"],\"BpttUI\":[\"Trending Today\"],\"BrrIs8\":[\"Storage\"],\"BskWMl\":[\"Unreachable\"],\"BzEFor\":[\"or\"],\"CGDHFb\":[\"Add a <0>Generic Destination</0> and paste the URL above\"],\"CGExDN\":[\"Failed to purge image cache\"],\"CHeXFE\":[\"Status updated\"],\"CKyk7Q\":[\"Go back\"],\"CaB/+I\":[\"Welcome back, \",[\"name\"]],\"CodnUh\":[\"Removed from library\"],\"CpzGJY\":[\"This may take a few minutes for large libraries. Please don't close this tab.\"],\"CuPxpd\":[\"Requires an active <0>PlexLine truncated
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+5kO8P\":[\"Saturday\"],\"+6i0lS\":[\"Retrieving your watch history, watchlist, and ratings...\"],\"+Cv+V9\":[\"Remove from library\"],\"+JkEpu\":[\"This page was left on the cutting room floor. It may have been moved, removed, or never made it past the screenplay.\"],\"+K0AvT\":[\"Disconnect\"],\"+N0l5/\":[\"Connected to <0>\",[\"serverHost\"],\"</0>. Tap to change.\"],\"+gLHYi\":[\"Failed to load title\"],\"+j1ex/\":[\"Failed to remove from library\"],\"+nF1ZO\":[\"Remove Photo\"],\"/+6dvC\":[\"Errors (\",[\"0\"],\")\"],\"/BBXeA\":[\"Connecting to server...\"],\"/DwR+n\":[\"Creating…\"],\"/XtbPO\":[[\"0\"],\" titles\"],\"/cg+QV\":[\"Watch all\"],\"/gQXGv\":[\"Failed to update status\"],\"/nT6AE\":[\"New password\"],\"/sHc1/\":[\"Update check\"],\"0+dyau\":[\"Failed to update retention setting\"],\"0BFJKK\":[\"Authorization was denied. Please try again.\"],\"0GHb20\":[\"Purge metadata cache?\"],\"0IBW21\":[\"Failed to purge caches\"],\"0gH/sc\":[\"Remove profile picture\"],\"1+P9RR\":[\"Switch to \",[\"0\"]],\"12cc1j\":[\"Watching\"],\"12lVOl\":[\"Self-hosted movie & TV tracker\"],\"1B4z0M\":[\"Filmography\"],\"1J4Ek0\":[\"Automatically back up your database on a schedule\"],\"1JhxXW\":[\"Episode \",[\"0\"],\", \",[\"episodeLabel\"]],\"1N6wNP\":[\"Import options\"],\"1Qz4uG\":[\"Enable the \\\"Playback\\\" event category\"],\"1hMWR6\":[\"Create account\"],\"1jHIjh\":[\"Watched all of \",[\"seasonName\"]],\"1vSYsG\":[\"Download backup\"],\"1wL1tj\":[\"TV show\"],\"2FletP\":[\"Purged \",[\"0\",\"plural\",{\"one\":[\"#\",\" title\"],\"other\":[\"#\",\" titles\"]}],\", \",[\"1\",\"plural\",{\"one\":[\"#\",\" person\"],\"other\":[\"#\",\" persons\"]}],\", \",[\"2\",\"plural\",{\"one\":[\"#\",\" file\"],\"other\":[\"#\",\" files\"]}],\" (\",[\"freed\"],\" freed)\"],\"2Fsd9r\":[\"This Month\"],\"2MPcep\":[\"Import complete\"],\"2Mu33Z\":[\"Cache management\"],\"2POOFK\":[\"Free\"],\"2Pt2NY\":[\"This will remove all items from your history.\"],\"2fCpt5\":[\"Return home\"],\"2pPBp6\":[\"Trending today\"],\"39y5bn\":[\"Friday\"],\"3Blefz\":[\"Ratings\"],\"3JTlG8\":[\"Recent Searches\"],\"3Jy8bM\":[\"Movies \",[\"select\"]],\"3L9OuQ\":[\"Episode \",[\"0\"]],\"3T8ziB\":[\"Create an account\"],\"3hELxX\":[\"Enter your Sofa server URL to get started\"],\"4fxLkp\":[\"Open Sonarr, go to <0>Settings > Import Lists</0>\"],\"4kpBqM\":[\"Last event \",[\"0\"]],\"4uUjVO\":[\"Producer\"],\"4x+A56\":[\"Anonymous usage reporting\"],\"5IShvp\":[\"Import \",[\"totalItems\"],\" items\"],\"5IYJSv\":[\"Explore titles\"],\"5Y4mym\":[\"Import from \",[\"0\"]],\"5ZzgbQ\":[\"Connect \",[\"0\"]],\"5fEnbK\":[\"Failed to update scheduled backup setting\"],\"5lWFkC\":[\"Sign in\"],\"5v9C16\":[\"Connected to \",[\"source\"]],\"623bR4\":[\"Failed to trigger job\"],\"6HN0yh\":[\"Open Plex, go to Settings > Webhooks\"],\"6TNjOJ\":[\"Failed to update rating\"],\"6TfUy6\":[\"last \",[\"value\"]],\"6V3Ea3\":[\"Copied\"],\"6YtxFj\":[\"Name\"],\"6exX+8\":[\"Regenerate\"],\"6gRgw8\":[\"Retry\"],\"6lGV3K\":[\"Show less\"],\"76++pR\":[\"Warnings\"],\"77DIAu\":[\"Go to Dashboard > Plugins > Webhook\"],\"7Bj3x9\":[\"Failed\"],\"7D50KC\":[[\"label\"],\" disconnected\"],\"7GfM5w\":[\"Recently Viewed\"],\"7L01XJ\":[\"Actions\"],\"7eMo+U\":[\"Go Home\"],\"7p5kLi\":[\"Dashboard\"],\"85eoJ1\":[\"Schedule updated\"],\"8B9E2D\":[\"Day:\"],\"8YwF1J\":[\"Go to <0>Dashboard > Plugins > Webhook</0>\"],\"8ZsakT\":[\"Password\"],\"8tjQCz\":[\"Explore\"],\"8vNtLy\":[\"Paste the Radarr URL above into the List URL field\"],\"8wYDMp\":[\"Already have an account?\"],\"9AE3vb\":[\"Open Plex, go to <0>Settings > Webhooks<1/></0>\"],\"9GW0ZB\":[\"Import is still running in the background. Check back later.\"],\"9NyAH9\":[\"Skipped\"],\"9Xhrps\":[\"Failed to connect\"],\"9ZLGbA\":[\"backups.\"],\"9eF5oV\":[\"Welcome back\"],\"9rG25a\":[\"Server URL\"],\"A+0rLe\":[\"Marked as completed\"],\"AOHgZp\":[\"Episodes\"],\"AOddWK\":[\"Connect \",[\"label\"]],\"ATfxL/\":[\"Trailer\"],\"AVlZoM\":[\"Environment\"],\"Acf6vF\":[\"Save name\"],\"AdoUfN\":[\"Failed to add to watchlist\"],\"AeXO77\":[\"Account\"],\"AjtYj0\":[\"On Watchlist\"],\"Avee+B\":[\"Failed to update name\"],\"B2R3xD\":[\"Toggle scheduled backups\"],\"BEVzjL\":[\"Import failed\"],\"BQnS5I\":[\"Open \",[\"source\"],\" to enter code\"],\"BUnoSB\":[[\"0\"],\" \",[\"1\",\"plural\",{\"one\":[\"backup\"],\"other\":[\"backups\"]}],\" stored\"],\"BpttUI\":[\"Trending Today\"],\"BrrIs8\":[\"Storage\"],\"BskWMl\":[\"Unreachable\"],\"BzEFor\":[\"or\"],\"CGDHFb\":[\"Add a <0>Generic Destination</0> and paste the URL above\"],\"CGExDN\":[\"Failed to purge image cache\"],\"CHeXFE\":[\"Status updated\"],\"CKyk7Q\":[\"Go back\"],\"CaB/+I\":[\"Welcome back, \",[\"name\"]],\"CodnUh\":[\"Removed from library\"],\"CpzGJY\":[\"This may take a few minutes for large libraries. Please don't close this tab.\"],\"CuPxpd\":[\"Requires an active <0>PlexLine truncated
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+5kO8P\":[\"Saturday\"],\"+6i0lS\":[\"Retrieving your watch history, watchlist, and ratings...\"],\"+Cv+V9\":[\"Remove from library\"],\"+JkEpu\":[\"This page was left on the cutting room floor. It may have been moved, removed, or never made it past the screenplay.\"],\"+K0AvT\":[\"Disconnect\"],\"+N0l5/\":[\"Connected to <0>\",[\"serverHost\"],\"</0>. Tap to change.\"],\"+gLHYi\":[\"Failed to load title\"],\"+j1ex/\":[\"Failed to remove from library\"],\"+nF1ZO\":[\"Remove Photo\"],\"/+6dvC\":[\"Errors (\",[\"0\"],\")\"],\"/BBXeA\":[\"Connecting to server...\"],\"/DwR+n\":[\"Creating…\"],\"/XtbPO\":[[\"0\"],\" titles\"],\"/cg+QV\":[\"Watch all\"],\"/gQXGv\":[\"Failed to update status\"],\"/nT6AE\":[\"New password\"],\"/sHc1/\":[\"Update check\"],\"0+dyau\":[\"Failed to update retention setting\"],\"0BFJKK\":[\"Authorization was denied. Please try again.\"],\"0GHb20\":[\"Purge metadata cache?\"],\"0IBW21\":[\"Failed to purge caches\"],\"0gH/sc\":[\"Remove profile picture\"],\"1+P9RR\":[\"Switch to \",[\"0\"]],\"12cc1j\":[\"Watching\"],\"12lVOl\":[\"Self-hosted movie & TV tracker\"],\"1B4z0M\":[\"Filmography\"],\"1J4Ek0\":[\"Automatically back up your database on a schedule\"],\"1JhxXW\":[\"Episode \",[\"0\"],\", \",[\"episodeLabel\"]],\"1N6wNP\":[\"Import options\"],\"1Qz4uG\":[\"Enable the \\\"Playback\\\" event category\"],\"1hMWR6\":[\"Create account\"],\"1jHIjh\":[\"Watched all of \",[\"seasonName\"]],\"1vSYsG\":[\"Download backup\"],\"1wL1tj\":[\"TV show\"],\"2FletP\":[\"Purged \",[\"0\",\"plural\",{\"one\":[\"#\",\" title\"],\"other\":[\"#\",\" titles\"]}],\", \",[\"1\",\"plural\",{\"one\":[\"#\",\" person\"],\"other\":[\"#\",\" persons\"]}],\", \",[\"2\",\"plural\",{\"one\":[\"#\",\" file\"],\"other\":[\"#\",\" files\"]}],\" (\",[\"freed\"],\" freed)\"],\"2Fsd9r\":[\"This Month\"],\"2MPcep\":[\"Import complete\"],\"2Mu33Z\":[\"Cache management\"],\"2POOFK\":[\"Free\"],\"2Pt2NY\":[\"This will remove all items from your history.\"],\"2fCpt5\":[\"Return home\"],\"2pPBp6\":[\"Trending today\"],\"39y5bn\":[\"Friday\"],\"3Blefz\":[\"Ratings\"],\"3JTlG8\":[\"Recent Searches\"],\"3Jy8bM\":[\"Movies \",[\"select\"]],\"3L9OuQ\":[\"Episode \",[\"0\"]],\"3T8ziB\":[\"Create an account\"],\"3hELxX\":[\"Enter your Sofa server URL to get started\"],\"4fxLkp\":[\"Open Sonarr, go to <0>Settings > Import Lists</0>\"],\"4kpBqM\":[\"Last event \",[\"0\"]],\"4uUjVO\":[\"Producer\"],\"4x+A56\":[\"Anonymous usage reporting\"],\"5IShvp\":[\"Import \",[\"totalItems\"],\" items\"],\"5IYJSv\":[\"Explore titles\"],\"5Y4mym\":[\"Import from \",[\"0\"]],\"5ZzgbQ\":[\"Connect \",[\"0\"]],\"5fEnbK\":[\"Failed to update scheduled backup setting\"],\"5lWFkC\":[\"Sign in\"],\"5v9C16\":[\"Connected to \",[\"source\"]],\"623bR4\":[\"Failed to trigger job\"],\"6HN0yh\":[\"Open Plex, go to Settings > Webhooks\"],\"6TNjOJ\":[\"Failed to update rating\"],\"6TfUy6\":[\"last \",[\"value\"]],\"6V3Ea3\":[\"Copied\"],\"6YtxFj\":[\"Name\"],\"6exX+8\":[\"Regenerate\"],\"6gRgw8\":[\"Retry\"],\"6lGV3K\":[\"Show less\"],\"76++pR\":[\"Warnings\"],\"77DIAu\":[\"Go to Dashboard > Plugins > Webhook\"],\"7Bj3x9\":[\"Failed\"],\"7D50KC\":[[\"label\"],\" disconnected\"],\"7GfM5w\":[\"Recently Viewed\"],\"7L01XJ\":[\"Actions\"],\"7eMo+U\":[\"Go Home\"],\"7p5kLi\":[\"Dashboard\"],\"85eoJ1\":[\"Schedule updated\"],\"8B9E2D\":[\"Day:\"],\"8YwF1J\":[\"Go to <0>Dashboard > Plugins > Webhook</0>\"],\"8ZsakT\":[\"Password\"],\"8tjQCz\":[\"Explore\"],\"8vNtLy\":[\"Paste the Radarr URL above into the List URL field\"],\"8wYDMp\":[\"Already have an account?\"],\"9AE3vb\":[\"Open Plex, go to <0>Settings > Webhooks<1/></0>\"],\"9GW0ZB\":[\"Import is still running in the background. Check back later.\"],\"9NyAH9\":[\"Skipped\"],\"9Xhrps\":[\"Failed to connect\"],\"9ZLGbA\":[\"backups.\"],\"9eF5oV\":[\"Welcome back\"],\"9rG25a\":[\"Server URL\"],\"A+0rLe\":[\"Marked as completed\"],\"AOHgZp\":[\"Episodes\"],\"AOddWK\":[\"Connect \",[\"label\"]],\"ATfxL/\":[\"Trailer\"],\"AVlZoM\":[\"Environment\"],\"Acf6vF\":[\"Save name\"],\"AdoUfN\":[\"Failed to add to watchlist\"],\"AeXO77\":[\"Account\"],\"AjtYj0\":[\"On Watchlist\"],\"Avee+B\":[\"Failed to update name\"],\"B2R3xD\":[\"Toggle scheduled backups\"],\"BEVzjL\":[\"Import failed\"],\"BQnS5I\":[\"Open \",[\"source\"],\" to enter code\"],\"BUnoSB\":[[\"0\"],\" \",[\"1\",\"plural\",{\"one\":[\"backup\"],\"other\":[\"backups\"]}],\" stored\"],\"BpttUI\":[\"Trending Today\"],\"BrrIs8\":[\"Storage\"],\"BskWMl\":[\"Unreachable\"],\"BzEFor\":[\"or\"],\"CGDHFb\":[\"Add a <0>Generic Destination</0> and paste the URL above\"],\"CGExDN\":[\"Failed to purge image cache\"],\"CHeXFE\":[\"Status updated\"],\"CKyk7Q\":[\"Go back\"],\"CaB/+I\":[\"Welcome back, \",[\"name\"]],\"CodnUh\":[\"Removed from library\"],\"CpzGJY\":[\"This may take a few minutes for large libraries. Please don't close this tab.\"],\"CuPxpd\":[\"Requires an active <0>PlexLine truncated
+13
View File
@@ -0,0 +1,13 @@
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import type { ReactNode } from "react";
import { messages } from "./po/en";
i18n.load("en", messages);
i18n.activate("en");
export function TestI18nProvider({ children }: { children: ReactNode }) {
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
}
export { i18n as testI18n };
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedModules": true,
"esModuleInterop": true,
"types": ["bun"]
},
"include": ["src"]
}
+5
View File
@@ -6,6 +6,11 @@
"exports": {
".": "./src/index.ts"
},
"scripts": {
"lint": "biome check",
"format": "biome format --write",
"check-types": "tsc --noEmit"
},
"dependencies": {
"pino": "10.3.1",
"pino-pretty": "13.1.3"