chore(i18n): add Claude-powered translation script and fill missing PO translations

- Add `packages/i18n/scripts/claude.ts` — a Bun script that translates empty PO entries via the Claude Code CLI, supporting batch mode, dry-run, optional AI context from `crowdin-context.jsonl`, and structured JSON output validation
- Add `i18n:claude` root script (`bun packages/i18n/scripts/claude.ts`) as a convenience alias
- Fill all previously empty msgstr entries across de, es, fr, it, pt PO catalogs and recompile `.ts` catalogs
- Polish README with badges (license, CI status, release, language, Crowdin localization) and minor formatting fixes
This commit is contained in:
2026-03-18 20:42:01 -04:00
parent 9b2d7caa3f
commit 7b0c07ad3c
23 changed files with 7775 additions and 7110 deletions
+24 -15
View File
@@ -12,18 +12,19 @@
"fumadocs-openapi": "^10.3.18",
"fumadocs-ui": "^16.6.17",
"lucide-react": "^0.577.0",
"next": "16.1.7",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"next": "16.2.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"shiki": "^4.0.2",
"tailwind-merge": "^3.5.0",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",
"@types/bun": "1.3.11",
"@types/mdx": "^2.0.13",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"oxfmt": "0.41.0",
"oxlint": "1.56.0",
"postcss": "^8.5.8",
@@ -32,6 +33,10 @@
},
},
},
"overrides": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -169,23 +174,23 @@
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@next/env": ["@next/env@16.1.7", "", {}, "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg=="],
"@next/env": ["@next/env@16.2.0", "", {}, "sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA=="],
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
@@ -421,6 +426,8 @@
"@takumi-rs/wasm": ["@takumi-rs/wasm@0.73.1", "", { "dependencies": { "@takumi-rs/helpers": "0.73.1" } }, "sha512-WDrstyQettPQSmYDyj7lrJMKlm3E5dg0ZC+PFvKTdfP4aMTZgWBchIL3MfdUwpRIRf96sdL+pAdrwI42X9iRXg=="],
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -467,6 +474,8 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
@@ -763,7 +772,7 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@16.1.7", "", { "dependencies": { "@next/env": "16.1.7", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.7", "@next/swc-darwin-x64": "16.1.7", "@next/swc-linux-arm64-gnu": "16.1.7", "@next/swc-linux-arm64-musl": "16.1.7", "@next/swc-linux-x64-gnu": "16.1.7", "@next/swc-linux-x64-musl": "16.1.7", "@next/swc-win32-arm64-msvc": "16.1.7", "@next/swc-win32-x64-msvc": "16.1.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg=="],
"next": ["next@16.2.0", "", { "dependencies": { "@next/env": "16.2.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.0", "@next/swc-darwin-x64": "16.2.0", "@next/swc-linux-arm64-gnu": "16.2.0", "@next/swc-linux-arm64-musl": "16.2.0", "@next/swc-linux-x64-gnu": "16.2.0", "@next/swc-linux-x64-musl": "16.2.0", "@next/swc-win32-arm64-msvc": "16.2.0", "@next/swc-win32-x64-msvc": "16.2.0", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
+2 -1
View File
@@ -3,11 +3,12 @@
"---Documentation---",
"index",
"using-sofa",
"admin",
"configuration",
"integrations",
"admin",
"troubleshooting",
"mobile-app",
"translations",
"telemetry",
"---API Reference---",
"...api",
+76
View File
@@ -0,0 +1,76 @@
---
title: Translations
description: Language support and how to contribute translations.
icon: Languages
---
Sofa supports multiple languages for the web and mobile apps. The current supported locales are:
- English (source)
- French
- German
- Spanish
- Italian
- Portuguese
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.
## How translations work
Sofa uses [Lingui](https://lingui.dev) for internationalization. All translatable strings are extracted from the source code into PO files (an industry-standard format), which live in [`packages/i18n/src/po/`](https://github.com/jakejarvis/sofa/tree/main/packages/i18n/src/po).
The translation pipeline:
1. React (web) and React Native (mobile) apps wrap UI strings in `<Trans>` components or `` msg`...` `` macros
2. `bun run i18n:extract` scans the codebase and updates `en.po` with new strings
3. Translations are added to each locale's `.po` file (e.g. `fr.po`, `de.po`)
4. `bun run i18n:compile` compiles `.po` files into TypeScript for use at runtime
5. Non-English locales are lazy-loaded on demand
## Contributing via Crowdin
The easiest way to contribute translations is through [Crowdin](https://crowdin.com/project/sofa), a community translation platform. No development setup required — just sign up and start translating in the browser!
When source strings change on `main`, they are automatically uploaded to Crowdin [via GitHub Actions](https://github.com/jakejarvis/sofa/blob/main/.github/workflows/crowdin.yml). To pull completed translations back into the repo, a maintainer triggers the download workflow which creates a PR with the updated `.po` files.
## AI translation with Claude
For maintainers, Sofa includes a script, [`claude.ts`](https://github.com/jakejarvis/sofa/blob/main/packages/i18n/scripts/claude.ts), that uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to roughly translate any yet-to-be-translated strings.
```bash
# Translate a single locale
bun packages/i18n/scripts/claude.ts fr
# Translate all locales (runs in parallel)
bun packages/i18n/scripts/claude.ts all
# Preview without translating
bun packages/i18n/scripts/claude.ts all --dry-run
# Use a specific model
bun packages/i18n/scripts/claude.ts all --model opus
```
The script only translates strings with empty `msgstr` — existing translations are never overwritten. It reads the locale list from [`lingui.config.ts`](https://github.com/jakejarvis/sofa/blob/main/lingui.config.ts) automatically and compiles catalogs after translating.
<Callout type="info">
Using this script really only makes sense if you have a [Claude Max
subscription](https://claude.ai/upgrade) with Anthropic's cash to burn.
</Callout>
## Adding a new language
1. Add the locale code to the `locales` array in [`lingui.config.ts`](https://github.com/jakejarvis/sofa/blob/main/lingui.config.ts)
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. Run `bun run i18n:compile`
## Translation guidelines
Whether translating via Crowdin, AI, or manually, these rules apply:
- **Keep it concise** — these are UI labels, buttons, and short messages
- **Preserve placeholders** — `{0}`, `{name}`, `{count, plural, ...}`, `<0>text</0>` must appear in the translation exactly as in the source
- **Adapt plurals** — ICU plural blocks should use the correct plural categories for your language (e.g. French needs `one`/`other`, Czech needs `one`/`few`/`many`/`other`)
- **Don't translate brand names** — Sofa, TMDB, Plex, Jellyfin, etc. stay as-is
+11 -6
View File
@@ -21,23 +21,28 @@
"fumadocs-openapi": "^10.3.18",
"fumadocs-ui": "^16.6.17",
"lucide-react": "^0.577.0",
"next": "16.1.7",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"next": "16.2.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"shiki": "^4.0.2",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",
"@types/bun": "1.3.11",
"@types/mdx": "^2.0.13",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"oxfmt": "0.41.0",
"oxlint": "1.56.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3"
},
"packageManager": "bun@1.3.10"
"overrides": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3"
},
"packageManager": "bun@1.3.11"
}
+78 -36
View File
@@ -99,6 +99,7 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type"
},
"title": {
@@ -555,6 +556,7 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type"
},
"title": {
@@ -901,6 +903,7 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type"
},
"title": {
@@ -992,6 +995,7 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type"
},
"title": {
@@ -1083,6 +1087,7 @@
"webhook",
"list"
],
"type": "string",
"description": "Integration type: webhook (Plex/Jellyfin/Emby) or list (Sonarr/Radarr)"
},
"token": {
@@ -1166,6 +1171,7 @@
"ignored",
"error"
],
"type": "string",
"description": "Event processing outcome"
},
"receivedAt": {
@@ -1204,6 +1210,7 @@
"scheduled",
"pre-restore"
],
"type": "string",
"description": "How the backup was created: manual, scheduled, or automatic pre-restore"
}
},
@@ -1273,7 +1280,8 @@
"running",
"success",
"error"
]
],
"type": "string"
},
{
"type": "null"
@@ -1781,7 +1789,8 @@
"enum": [
"in_progress",
"completed"
]
],
"type": "string"
},
{
"type": "null"
@@ -1983,7 +1992,8 @@
"watchlist",
"in_progress",
"completed"
]
],
"type": "string"
},
{
"type": "null"
@@ -2072,7 +2082,8 @@
"watchlist",
"in_progress",
"completed"
]
],
"type": "string"
},
"description": "Map of title ID to the user's tracking status"
}
@@ -2517,7 +2528,8 @@
"watchlist",
"in_progress",
"completed"
]
],
"type": "string"
},
"description": "Map of title ID to the user's tracking status"
},
@@ -2898,6 +2910,7 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type"
},
"title": {
@@ -2966,7 +2979,8 @@
"watchlist",
"in_progress",
"completed"
]
],
"type": "string"
},
{
"type": "null"
@@ -3079,10 +3093,9 @@
"movie",
"episode"
],
"type": "string",
"description": "What to count: movie watches or episode watches"
},
"style": "deepObject",
"explode": true,
"allowEmptyValue": true,
"allowReserved": true
},
@@ -3097,10 +3110,9 @@
"this_month",
"this_year"
],
"type": "string",
"description": "Time range for the histogram"
},
"style": "deepObject",
"explode": true,
"allowEmptyValue": true,
"allowReserved": true
}
@@ -3176,10 +3188,9 @@
"movie",
"tv"
],
"type": "string",
"description": "Trending category: all, movie, or tv"
},
"style": "deepObject",
"explode": true,
"allowEmptyValue": true,
"allowReserved": true
},
@@ -3231,6 +3242,7 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type"
},
"title": {
@@ -3283,7 +3295,8 @@
"watchlist",
"in_progress",
"completed"
]
],
"type": "string"
},
"description": "Map of title ID to the user's tracking status"
},
@@ -3433,10 +3446,9 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type filter"
},
"style": "deepObject",
"explode": true,
"allowEmptyValue": true,
"allowReserved": true
},
@@ -3479,7 +3491,8 @@
"watchlist",
"in_progress",
"completed"
]
],
"type": "string"
},
"description": "Map of title ID to the user's tracking status"
},
@@ -3628,10 +3641,9 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type filter"
},
"style": "deepObject",
"explode": true,
"allowEmptyValue": true,
"allowReserved": true
}
@@ -3784,10 +3796,9 @@
"tv",
"person"
],
"type": "string",
"description": "Optional type filter to narrow results"
},
"style": "deepObject",
"explode": true,
"allowEmptyValue": true,
"allowReserved": true
},
@@ -3833,6 +3844,7 @@
"tv",
"person"
],
"type": "string",
"description": "Result type"
},
"title": {
@@ -4057,10 +4069,9 @@
"movie",
"tv"
],
"type": "string",
"description": "Media type to discover"
},
"style": "deepObject",
"explode": true,
"allowEmptyValue": true,
"allowReserved": true
},
@@ -4116,7 +4127,8 @@
"watchlist",
"in_progress",
"completed"
]
],
"type": "string"
},
"description": "Map of title ID to the user's tracking status"
},
@@ -4432,6 +4444,7 @@
"webhook",
"list"
],
"type": "string",
"description": "Integration type: webhook (Plex/Jellyfin/Emby) or list (Sonarr/Radarr)"
},
"token": {
@@ -4515,6 +4528,7 @@
"sonarr",
"radarr"
],
"type": "string",
"description": "Media server provider to integrate"
},
"enabled": {
@@ -4570,6 +4584,7 @@
"sonarr",
"radarr"
],
"type": "string",
"description": "Media server provider type"
}
}
@@ -4617,6 +4632,7 @@
"sonarr",
"radarr"
],
"type": "string",
"description": "Media server provider type"
}
}
@@ -5110,6 +5126,7 @@
"1d",
"7d"
],
"type": "string",
"description": "Backup interval"
},
"time": {
@@ -5165,6 +5182,7 @@
"1d",
"7d"
],
"type": "string",
"description": "Backup interval"
},
"time": {
@@ -5526,6 +5544,7 @@
"updateCheck",
"telemetryReport"
],
"type": "string",
"description": "Cron job to trigger"
}
},
@@ -5881,6 +5900,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"file": {
@@ -5904,6 +5924,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"file": {
@@ -5936,6 +5957,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"movies": {
@@ -6043,7 +6065,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
}
},
"required": [
@@ -6077,7 +6100,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
},
"rating": {
"type": "integer",
@@ -6273,6 +6297,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"movies": {
@@ -6380,7 +6405,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
}
},
"required": [
@@ -6414,7 +6440,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
},
"rating": {
"type": "integer",
@@ -6472,6 +6499,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"movies": {
@@ -6579,7 +6607,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
}
},
"required": [
@@ -6613,7 +6642,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
},
"rating": {
"type": "integer",
@@ -6738,6 +6768,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"movies": {
@@ -6845,7 +6876,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
}
},
"required": [
@@ -6879,7 +6911,8 @@
"enum": [
"movie",
"tv"
]
],
"type": "string"
},
"rating": {
"type": "integer",
@@ -6960,6 +6993,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"status": {
@@ -6969,7 +7003,8 @@
"success",
"error",
"cancelled"
]
],
"type": "string"
},
"totalItems": {
"type": "number"
@@ -7239,6 +7274,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"status": {
@@ -7248,7 +7284,8 @@
"success",
"error",
"cancelled"
]
],
"type": "string"
},
"totalItems": {
"type": "number"
@@ -7386,6 +7423,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"status": {
@@ -7395,7 +7433,8 @@
"success",
"error",
"cancelled"
]
],
"type": "string"
},
"totalItems": {
"type": "number"
@@ -7598,7 +7637,8 @@
"progress",
"complete",
"timeout"
]
],
"type": "string"
},
"job": {
"type": "object",
@@ -7612,6 +7652,7 @@
"simkl",
"letterboxd"
],
"type": "string",
"description": "External service to import from"
},
"status": {
@@ -7621,7 +7662,8 @@
"success",
"error",
"cancelled"
]
],
"type": "string"
},
"totalItems": {
"type": "number"
+9 -2
View File
@@ -8,8 +8,8 @@ export const metadata: Metadata = {
export default function PrivacyPolicyPage() {
return (
<article className="prose prose-sm prose-invert [&_a]:text-fd-primary hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h3]:font-display [&_p]:text-fd-muted-foreground [&_li]:text-fd-muted-foreground [&_strong]:text-fd-foreground mx-auto max-w-3xl px-6 py-16 [&_a]:underline [&_a]:underline-offset-4 [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h2]:text-2xl [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-xl [&_li]:text-[15px] [&_li]:leading-relaxed [&_p]:text-[15px] [&_p]:leading-relaxed [&_strong]:font-medium [&_ul]:space-y-1">
<h1 className="font-display mb-2 text-3xl tracking-tight">Privacy Policy</h1>
<article className="prose prose-sm prose-invert [&_a]:text-fd-primary hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h3]:font-display [&_p]:text-fd-muted-foreground [&_li]:text-fd-muted-foreground [&_strong]:text-fd-foreground mx-auto max-w-3xl px-6 py-16 [&_a]:underline [&_a]:underline-offset-4 [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h2]:text-2xl [&_h2]:font-semibold [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-xl [&_h3]:font-normal [&_li]:text-[15px] [&_li]:leading-relaxed [&_p]:text-[15px] [&_p]:leading-relaxed [&_strong]:font-medium [&_ul]:space-y-1">
<h1 className="font-display mb-2 text-3xl font-semibold tracking-tight">Privacy Policy</h1>
<p className="text-fd-muted-foreground/60 !mt-0 mb-8 text-sm">Last updated: March 16, 2026</p>
<p>
@@ -95,6 +95,13 @@ export default function PrivacyPolicyPage() {
<strong>disabled by default</strong> and must be explicitly enabled by an admin. See the{" "}
<Link href="/docs/telemetry">telemetry documentation</Link> for full details.
</li>
<li>
<strong>Import helper</strong> &mdash; when importing watch history from Trakt or Simkl,
your server proxies OAuth device-code authorization through{" "}
<code>public-api.sofa.watch</code> so that a single OAuth client ID can be shared across
all instances. Only the provider&rsquo;s device code and token responses are relayed; no
personal data, watch history, or instance identifiers are included in the requests.
</li>
</ul>
<h3>PostHog (Mobile App Only)</h3>