mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
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:
+24
-15
@@ -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=="],
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
"---Documentation---",
|
||||
"index",
|
||||
"using-sofa",
|
||||
"admin",
|
||||
"configuration",
|
||||
"integrations",
|
||||
"admin",
|
||||
"troubleshooting",
|
||||
"mobile-app",
|
||||
"translations",
|
||||
"telemetry",
|
||||
"---API Reference---",
|
||||
"...api",
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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> — 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’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>
|
||||
|
||||
Reference in New Issue
Block a user