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:
@@ -1,10 +1,16 @@
|
||||
<p align="center">
|
||||
<a href="https://sofa.watch"><img width="72" height="72" alt="Sofa" src="https://sofa.watch/icon.svg" /></a>
|
||||
<br/>
|
||||
<a href="https://sofa.watch"><strong>Sofa</strong></a> — Self-hosted movie & TV tracker
|
||||
<a href="https://sofa.watch"><img width="72" height="72" alt="Sofa" src="https://sofa.watch/icon.svg" /></a><br />
|
||||
<a href="https://sofa.watch"><strong>Sofa</strong></a> — Self-hosted movie & TV tracker
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/jakejarvis/sofa"></a>
|
||||
<a href="https://github.com/jakejarvis/sofa/actions/workflows/docker.yml"><img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/jakejarvis/sofa/docker.yml" /></a>
|
||||
<a href="https://github.com/jakejarvis/sofa/releases/latest"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/jakejarvis/sofa"></a>
|
||||
<a href="https://github.com/jakejarvis/sofa"><img alt="GitHub top language" src="https://img.shields.io/github/languages/top/jakejarvis/sofa"></a>
|
||||
<a href="https://crowdin.com/project/sofa"><img alt="Crowdin" src="https://badges.crowdin.net/sofa/localized.svg" /></a>
|
||||
</p>
|
||||
|
||||
Sofa is a self-hosted movie and TV tracker for nerds. Track what you've watched, discover what's next, and plug your data into your existing home media stack.
|
||||
[Sofa](https://sofa.watch) is a self-hosted movie and TV tracker for nerds. Track what you've watched, discover what's next, and plug your data into your existing home media stack. All without anything leaving your homelab. 🍿
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-hotkeys": "0.4.1",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.167.4",
|
||||
"@tanstack/react-router": "1.167.5",
|
||||
"better-auth": "catalog:",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
@@ -54,7 +54,7 @@
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@tanstack/react-router-devtools": "1.166.9",
|
||||
"@tanstack/router-plugin": "1.166.13",
|
||||
"@tanstack/router-plugin": "1.166.14",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"@lingui/format-po": "catalog:",
|
||||
"oxfmt": "0.41.0",
|
||||
"oxlint": "1.56.0",
|
||||
"turbo": "2.8.18",
|
||||
"turbo": "2.8.19",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
@@ -156,7 +156,7 @@
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-hotkeys": "0.4.1",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.167.4",
|
||||
"@tanstack/react-router": "1.167.5",
|
||||
"better-auth": "catalog:",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
@@ -182,7 +182,7 @@
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@tanstack/react-router-devtools": "1.166.9",
|
||||
"@tanstack/router-plugin": "1.166.13",
|
||||
"@tanstack/router-plugin": "1.166.14",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
@@ -318,7 +318,7 @@
|
||||
"@orpc/contract": "1.13.8",
|
||||
"@orpc/tanstack-query": "1.13.8",
|
||||
"@tanstack/react-query": "5.91.0",
|
||||
"@types/bun": "1.3.10",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/node": "25.5.0",
|
||||
"@types/react": "19.2.14",
|
||||
"better-auth": "1.5.5",
|
||||
@@ -359,11 +359,11 @@
|
||||
|
||||
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
|
||||
|
||||
"@azure/msal-browser": ["@azure/msal-browser@4.29.1", "", { "dependencies": { "@azure/msal-common": "15.16.1" } }, "sha512-1Vrt27du1cl4QHkzLc6L4aeXqliPIDIs5l/1I4hWWMXkXccY/EznJT1+pBdoVze0azTAI8sCyq5B4cBVYG1t9w=="],
|
||||
"@azure/msal-browser": ["@azure/msal-browser@4.30.0", "", { "dependencies": { "@azure/msal-common": "15.17.0" } }, "sha512-HBBKfbZkMVzzF5bofvS1cXuNHFVc+gt4/HOnCmG/0hsHuZRJvJvDg/+7nTwIpoqvJc8BQp5o23rBUfisOLxR+w=="],
|
||||
|
||||
"@azure/msal-common": ["@azure/msal-common@15.16.1", "", {}, "sha512-qxUG9TCl+TVSSX58onVDHDWrvT5CE0+NeeUAbkQqaESpSm79u5IePLnPWMMjCUnUR2zJd4+Bt9vioVRzLmJb2g=="],
|
||||
"@azure/msal-common": ["@azure/msal-common@15.17.0", "", {}, "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw=="],
|
||||
|
||||
"@azure/msal-node": ["@azure/msal-node@3.8.9", "", { "dependencies": { "@azure/msal-common": "15.16.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-jZ0pw/BbdEUWGhomCaAiVDfXRI/9K56m5hTNqB/CzcbZEYhXm5qpK1cDngN1iXfwSfmUMorOUQ2FC0dyuQ9uRg=="],
|
||||
"@azure/msal-node": ["@azure/msal-node@3.8.10", "", { "dependencies": { "@azure/msal-common": "15.17.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-0Hz7Kx4hs70KZWep/Rd7aw/qOLUF92wUOhn7ZsOuB5xNR/06NL1E2RAI9+UKH1FtvN8nD6mFjH7UKSjv6vOWvQ=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
@@ -1287,19 +1287,19 @@
|
||||
|
||||
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.90.25", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.92.2" }, "peerDependencies": { "@tanstack/react-query": "^5.91.0", "react": "^18 || ^19" } }, "sha512-ZQElYJSIOS7fq7jv4VYhTZpY4ZLkmur9tSyeE+MW+kWxkWyzAE/h4FOODKP6Ii9N3+4IsGIrUgsMRJuSgRJBeQ=="],
|
||||
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.167.4", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.167.4", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-VpbZh382zX3WF4+X2Z+EUyd8eJhJyjg9C6ByYwrVZiWbhgbMK4+zQQIG2+lCAlIlDi7SV8fDcGL09NA8Z2kpGQ=="],
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.167.5", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.167.5", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-s1nP6l/7BYZfSwhoNbB7/rUmZ07q/AvkmhBoiDQl3tgy5dpb9Q1qjtIapYdvCOrao1aA/QCaWqxcbGc2Ct1bvQ=="],
|
||||
|
||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.166.9", "", { "dependencies": { "@tanstack/router-devtools-core": "1.166.9" }, "peerDependencies": { "@tanstack/react-router": "^1.167.2", "@tanstack/router-core": "^1.167.2", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-O49eZmaeEKB5YnKH/qd61AbxV/lW8ICm4stfZ4GNQNpzQQ6rhPIB0p3PMZDIgX+6DoMivdNvLRmXAOOpzpIpDg=="],
|
||||
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.9.2", "", { "dependencies": { "@tanstack/store": "0.9.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="],
|
||||
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.167.4", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Gk5V9Zr5JFJ4SbLyCheQLJ3MnXddccENPA+DJRz+9g3QxtN8DJB8w8KCUCgDeYlWp4LvmO4nX3fy3tupqVP2Pw=="],
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.167.5", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "bin": { "intent": "bin/intent.js" } }, "sha512-8fRgJ0zNJf77R4grCaJQ5Imatjyc4YT5v8rlsPkYYYeUlcFNLbuFRhLlAMdND9gRUMznpnbRDXngpTPgx2K7HQ=="],
|
||||
|
||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.166.9", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.167.2", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-PNlA7GmOUX9wY7LUG709Pk3Lg33dfHBztQwzjzrOiOsuf4ggp2R6bwarF8nYGNjG79z/MaB5PN+5yvkCVk8jGw=="],
|
||||
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.12", "", { "dependencies": { "@tanstack/router-core": "1.167.4", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-2HdxSTbCkbU9JeYogKVigIlXoLtIJE1x5rbEov+ZLTPjGCO9kicNQuljqg9Js+u2/ahtWewNrE5u1QCAyxmpIg=="],
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.13", "", { "dependencies": { "@tanstack/router-core": "1.167.5", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-ALxSs6OzimiSgpOuIm+AXmc7eUx/oGPwSPpdQbpZ/kX7WHRh6qM7lv8DAN0K3jWcBpzF8eeOIdryWryX8gH+Yg=="],
|
||||
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.13", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.167.4", "@tanstack/router-generator": "1.166.12", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.167.4", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-xG3ND3AlMe6DN9PihJAYUbQJevqJvVdzN1QpZbfU1/jkHurL97ynP2yXfmMTh8Qgi1K+SWRko4bi7iZlYP9SUw=="],
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.14", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.167.5", "@tanstack/router-generator": "1.166.13", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.167.5", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-hypyj0qlsAbJf60/glmVYqSVwnRB4hKRrMCUsSXjrPdO2g6gs3z6xHmcWsHQ831C4G9+bSFEK9Uy5EjO3A4THQ=="],
|
||||
|
||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="],
|
||||
|
||||
@@ -1313,17 +1313,17 @@
|
||||
|
||||
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
||||
|
||||
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-3QNJ8VR2y1+AZX1HRB2uAvBWHWYpyWMYuDPAPHwhmpC74YweQmPh41dQZ9fkNcgT1eZNgYKf6Q3Hq7i0XK3Ybg=="],
|
||||
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-WNzpqJV7rO/oKmsxjtSFXuV87fZb/qxzeGWXJng4oHFz7iPOFSWrPb2gTazJgoTvXUfe1kOH2o0F+zFrerwzRw=="],
|
||||
|
||||
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kiFEHQ+l64yzRtFcBSdHG9s/EF3aEzzmkuR8e1ygv7ol1YnWwz5EQEbZz6Ajt1sNYYRlo0GXw/d1YanVS9KWPA=="],
|
||||
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KTq8/PH0/ml9bNFomZ2VjmOqW6RYPRqBqV5CfNeD5ekJMuNwJUdeQz0qXguxCzW4OkHCnTdPeKIehR0cdQObPg=="],
|
||||
|
||||
"@turbo/linux-64": ["@turbo/linux-64@2.8.18", "", { "os": "linux", "cpu": "x64" }, "sha512-GlMHBGfob6pOQv7t6imx11O0WJapVVNcJxw+HkliIr1ez8vr8SskM7njqOtqKBTaooCmGA71BFInNRZNYO9qlg=="],
|
||||
"@turbo/linux-64": ["@turbo/linux-64@2.8.19", "", { "os": "linux", "cpu": "x64" }, "sha512-d0xcXiGUt1Q/HzMfgkqai5KyXrassLRuUZhxmUs4ZX99tQFbqRL2/CkSKCnMqyDv7c0K5eugLerVUY+DYr1DQg=="],
|
||||
|
||||
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-ogWEZ8K/NznjnJU1idHYOA62Y7xYyMnt+kKTZzRNJ9Ye2japtjDkixC31yHaA9QJayb+yS5V8gNDjVfh2yGrIw=="],
|
||||
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-UX3r1iwqsOK8dMgKFtZpDqZk7feqHXZJ/EQAknh0SVQrnc4vkm6mZqjX4j2j3DVoSo4Ho8xGITYlor9Xxsyomw=="],
|
||||
|
||||
"@turbo/windows-64": ["@turbo/windows-64@2.8.18", "", { "os": "win32", "cpu": "x64" }, "sha512-29g6qryc18ls7BKUmVsjjHAbx2eDgab5CUSPhbCKZYCBfzaW+i79wCzyVoC0NJ8IRNwbLch2QEzgGEoRC6uGOQ=="],
|
||||
"@turbo/windows-64": ["@turbo/windows-64@2.8.19", "", { "os": "win32", "cpu": "x64" }, "sha512-3MrvD/gMYWfQKZVZOeilKm1629zPG0+KlErZbI0n9Nr3L32+5qrZOF/cgWErukLnOSR38whgf8t2WaPJQALqxg=="],
|
||||
|
||||
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-hLsgNDzDDO+Tq2YJ9uvmB2PFTYCgeNj6cyAOOBZP9rsOx0kxNBo4/OvDIgYl/cfo7wsoRBxNQ9giYR+rZMcAjA=="],
|
||||
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-j+8mi7kyEgT7cUTPOtPJS8KbTWxu4+OQBiDIo+zklz/RKV4hoN+k4+J8iCM9Nj+o+SVnzc01xSI+s3eBG0MDSA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
@@ -1337,7 +1337,7 @@
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
@@ -1537,7 +1537,7 @@
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
@@ -1737,6 +1737,8 @@
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||
@@ -2091,7 +2093,7 @@
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"jotai": ["jotai@2.18.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-e0NOzK+yRFwHo7DOp0DS0Ycq74KMEAObDWFGmfEL28PD9nLqBTt3/Ug7jf9ca72x0gC9LQZG9zH+0ISICmy3iA=="],
|
||||
|
||||
@@ -2131,7 +2133,7 @@
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"kysely": ["kysely@0.28.12", "", {}, "sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw=="],
|
||||
"kysely": ["kysely@0.28.13", "", {}, "sha512-jCkYDvlfzOyHaVsrvR4vnNZxG30oNv2jbbFBjTQAUG8n0h07HW0sZJHk4KAQIRyu9ay+Rg+L8qGa3lwt8Gve9w=="],
|
||||
|
||||
"lan-network": ["lan-network@0.2.0", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-EZgbsXMrGS+oK+Ta12mCjzBFse+SIewGdwrSTr5g+MSymnjpox2x05ceI20PQejJOFvOgzcXrfDk/SdY7dSCtw=="],
|
||||
|
||||
@@ -2775,7 +2777,7 @@
|
||||
|
||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||
|
||||
"turbo": ["turbo@2.8.18", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.18", "@turbo/darwin-arm64": "2.8.18", "@turbo/linux-64": "2.8.18", "@turbo/linux-arm64": "2.8.18", "@turbo/windows-64": "2.8.18", "@turbo/windows-arm64": "2.8.18" }, "bin": { "turbo": "bin/turbo" } }, "sha512-ZO5aYR8wXIcZ+KRL8W7RFWbqIRDyYHtesF/g9zPMndmq+KkX34qrZgG9M+YswJ6BGX4jlYC1FlJjleKENagJdw=="],
|
||||
"turbo": ["turbo@2.8.19", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.19", "@turbo/darwin-arm64": "2.8.19", "@turbo/linux-64": "2.8.19", "@turbo/linux-arm64": "2.8.19", "@turbo/windows-64": "2.8.19", "@turbo/windows-arm64": "2.8.19" }, "bin": { "turbo": "bin/turbo" } }, "sha512-9NTKvQZ/02XnJCSU4mF5gvAPK+nuLbvCtft/NE8dJeOwaup0bJ14rYx7TxmSXp8JJuAN8nAjqfXWFsWwoQSFHg=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
project_id: "881052"
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
|
||||
preserve_hierarchy: true
|
||||
|
||||
files:
|
||||
- source: /packages/i18n/src/po/en.po
|
||||
translation: /packages/i18n/src/po/%two_letters_code%.po
|
||||
|
||||
+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>
|
||||
|
||||
+5
-4
@@ -12,7 +12,7 @@
|
||||
"@orpc/contract": "1.13.8",
|
||||
"@orpc/tanstack-query": "1.13.8",
|
||||
"@tanstack/react-query": "5.91.0",
|
||||
"@types/bun": "1.3.10",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/node": "25.5.0",
|
||||
"@types/react": "19.2.14",
|
||||
"better-auth": "1.5.5",
|
||||
@@ -45,7 +45,8 @@
|
||||
"test": "turbo run test",
|
||||
"generate:openapi": "bun scripts/generate-openapi-spec.ts && cd docs && bun run generate:api-docs",
|
||||
"i18n:extract": "lingui extract",
|
||||
"i18n:compile": "lingui compile --typescript"
|
||||
"i18n:compile": "lingui compile --typescript",
|
||||
"i18n:claude": "bun packages/i18n/scripts/claude.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "catalog:",
|
||||
@@ -53,8 +54,8 @@
|
||||
"@lingui/format-po": "catalog:",
|
||||
"oxfmt": "0.41.0",
|
||||
"oxlint": "1.56.0",
|
||||
"turbo": "2.8.18",
|
||||
"turbo": "2.8.19",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"packageManager": "bun@1.3.10"
|
||||
"packageManager": "bun@1.3.11"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Translates empty PO file entries using Claude Code CLI.
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/translate.ts <locale|all> [--model <model>] [--batch-size <n>] [--dry-run]
|
||||
*
|
||||
* Examples:
|
||||
* bun scripts/translate.ts fr
|
||||
* bun scripts/translate.ts all --model sonnet
|
||||
* bun scripts/translate.ts de --dry-run
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { CatalogType } from "@lingui/conf";
|
||||
import { formatter } from "@lingui/format-po";
|
||||
|
||||
import linguiConfig from "../../../lingui.config";
|
||||
|
||||
const poFormat = formatter();
|
||||
|
||||
// ─── Config ──────────────────────────────────────────────
|
||||
|
||||
const SOURCE_LOCALE = linguiConfig.sourceLocale ?? "en";
|
||||
const LOCALES = (linguiConfig.locales as string[]).filter((l) => l !== SOURCE_LOCALE);
|
||||
|
||||
const DEFAULT_MODEL = "sonnet";
|
||||
const DEFAULT_BATCH_SIZE = 100;
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
const PO_DIR = resolve(import.meta.dir, "../src/po");
|
||||
const CONTEXT_FILE = resolve(import.meta.dir, "../../../crowdin-context.jsonl");
|
||||
|
||||
/** Resolve a locale code to its English display name via Intl */
|
||||
function localeName(code: string): string {
|
||||
return new Intl.DisplayNames(["en"], { type: "language" }).of(code) ?? code;
|
||||
}
|
||||
|
||||
function usage(): never {
|
||||
console.log(
|
||||
"Usage: bun scripts/translate.ts <locale|all> [--model <model>] [--batch-size <n>] [--dry-run]",
|
||||
);
|
||||
console.log(` Locales: ${LOCALES.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function fail(message: string): never {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ─── AI Context ──────────────────────────────────────────
|
||||
|
||||
interface ContextEntry {
|
||||
text: string;
|
||||
ai_context: string;
|
||||
}
|
||||
|
||||
async function loadContext(): Promise<Map<string, string>> {
|
||||
const contextMap = new Map<string, string>();
|
||||
const file = Bun.file(CONTEXT_FILE);
|
||||
|
||||
if (!(await file.exists())) {
|
||||
console.log(" (no crowdin-context.jsonl found, translating without AI context)\n");
|
||||
return contextMap;
|
||||
}
|
||||
|
||||
const text = await file.text();
|
||||
const lines = text.split("\n");
|
||||
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
const entry: ContextEntry = JSON.parse(line);
|
||||
|
||||
if (typeof entry.text !== "string" || typeof entry.ai_context !== "string") {
|
||||
throw new Error("expected { text: string, ai_context: string }");
|
||||
}
|
||||
|
||||
contextMap.set(entry.text, entry.ai_context);
|
||||
} catch (error) {
|
||||
// oxlint-disable-next-line preserve-caught-error
|
||||
throw new Error(
|
||||
`Invalid JSONL entry in ${CONTEXT_FILE} at line ${index + 1}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` Loaded ${contextMap.size} AI context entries from crowdin-context.jsonl\n`);
|
||||
return contextMap;
|
||||
}
|
||||
|
||||
// ─── CLI Args ────────────────────────────────────────────
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let locale: string | undefined;
|
||||
let model = DEFAULT_MODEL;
|
||||
let batchSize = DEFAULT_BATCH_SIZE;
|
||||
let dryRun = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === "--model") {
|
||||
const value = args[++i];
|
||||
if (!value || value.startsWith("--")) {
|
||||
fail("Missing value for --model");
|
||||
}
|
||||
model = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--batch-size") {
|
||||
const value = args[++i];
|
||||
if (!value || value.startsWith("--")) {
|
||||
fail("Missing value for --batch-size");
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
fail(`Invalid --batch-size: ${value}`);
|
||||
}
|
||||
|
||||
batchSize = parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--dry-run") {
|
||||
dryRun = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("--")) {
|
||||
fail(`Unknown flag: ${arg}`);
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
fail(`Unexpected extra positional argument: ${arg}`);
|
||||
}
|
||||
|
||||
locale = arg;
|
||||
}
|
||||
|
||||
if (!locale) {
|
||||
usage();
|
||||
}
|
||||
|
||||
if (locale !== "all" && !LOCALES.includes(locale)) {
|
||||
fail(`Unknown locale: ${locale}. Valid: ${LOCALES.join(", ")}, all`);
|
||||
}
|
||||
|
||||
const locales: string[] = locale === "all" ? [...LOCALES] : [locale];
|
||||
return { locales, model, batchSize, dryRun };
|
||||
}
|
||||
|
||||
// ─── PO Helpers ──────────────────────────────────────────
|
||||
|
||||
interface UntranslatedEntry {
|
||||
/** Lingui catalog key (may be a hashed ID like "nJw77c") */
|
||||
key: string;
|
||||
/** The actual English source text to translate */
|
||||
message: string;
|
||||
}
|
||||
|
||||
function getUntranslatedEntries(catalog: CatalogType): UntranslatedEntry[] {
|
||||
return Object.entries(catalog)
|
||||
.filter(([, entry]) => entry && !entry.translation)
|
||||
.map(([key, entry]) => ({
|
||||
key,
|
||||
// entry.message is the source text; fall back to key for explicit-id catalogs
|
||||
message: entry.message || key,
|
||||
}));
|
||||
}
|
||||
|
||||
// ─── Basic Structural Validation ─────────────────────────
|
||||
// Catches common placeholder/tag damage. NOT a full ICU MessageFormat validator —
|
||||
// does not detect argument type changes, malformed plural/select, or nested corruption.
|
||||
|
||||
function extractSimplePlaceholders(text: string): string[] {
|
||||
// Intentionally conservative. This catches the common placeholder cases:
|
||||
// {0}, {name}, {count}
|
||||
return [...text.matchAll(/\{[a-zA-Z0-9_]+\}/g)].map((m) => m[0]).sort();
|
||||
}
|
||||
|
||||
function extractNumberedTags(text: string): string[] {
|
||||
// Matches <0>, </0>, <0/>, <12>, </12>, <12/>
|
||||
return [...text.matchAll(/<\/?\d+\/?>/g)].map((m) => m[0]).sort();
|
||||
}
|
||||
|
||||
function leadingWhitespace(text: string): string {
|
||||
return text.match(/^\s*/)?.[0] ?? "";
|
||||
}
|
||||
|
||||
function trailingWhitespace(text: string): string {
|
||||
return text.match(/\s*$/)?.[0] ?? "";
|
||||
}
|
||||
|
||||
function validateTranslation(source: string, translated: string): string[] {
|
||||
const issues: string[] = [];
|
||||
|
||||
if (translated.length === 0 && source.length > 0) {
|
||||
issues.push("empty translation");
|
||||
}
|
||||
|
||||
const srcPlaceholders = extractSimplePlaceholders(source);
|
||||
const dstPlaceholders = extractSimplePlaceholders(translated);
|
||||
if (JSON.stringify(srcPlaceholders) !== JSON.stringify(dstPlaceholders)) {
|
||||
issues.push("placeholder mismatch");
|
||||
}
|
||||
|
||||
const srcTags = extractNumberedTags(source);
|
||||
const dstTags = extractNumberedTags(translated);
|
||||
if (JSON.stringify(srcTags) !== JSON.stringify(dstTags)) {
|
||||
issues.push("tag mismatch");
|
||||
}
|
||||
|
||||
// Only enforce whitespace when the source has meaningful edge whitespace
|
||||
if (/^\s|\s$/.test(source)) {
|
||||
if (leadingWhitespace(source) !== leadingWhitespace(translated)) {
|
||||
issues.push("leading whitespace mismatch");
|
||||
}
|
||||
|
||||
if (trailingWhitespace(source) !== trailingWhitespace(translated)) {
|
||||
issues.push("trailing whitespace mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
// ─── Consistency Hints ───────────────────────────────────
|
||||
|
||||
function buildConsistencyHints(accepted: Map<string, string>, limit = 50): string | undefined {
|
||||
if (accepted.size === 0) return undefined;
|
||||
|
||||
const pairs = [...accepted.entries()]
|
||||
.slice(-limit)
|
||||
.map(([source, translated]) => `- ${JSON.stringify(source)} => ${JSON.stringify(translated)}`)
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
"Previously used translations for consistency:",
|
||||
pairs,
|
||||
"Reuse these choices where appropriate.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// ─── Translation via Claude CLI ──────────────────────────
|
||||
|
||||
async function translateBatchRaw(
|
||||
strings: string[],
|
||||
locale: string,
|
||||
model: string,
|
||||
contextMap: Map<string, string>,
|
||||
consistencyHints?: string,
|
||||
): Promise<string[]> {
|
||||
const language = localeName(locale);
|
||||
|
||||
const input = strings.map((s, i) => {
|
||||
const ctx = contextMap.get(s);
|
||||
return ctx ? { index: i, text: s, context: ctx } : { index: i, text: s };
|
||||
});
|
||||
|
||||
const systemPrompt = [
|
||||
"You are a professional translator for a self-hosted movie & TV tracking app called Sofa.",
|
||||
`Translate UI strings from English to ${language}.`,
|
||||
"",
|
||||
"Rules:",
|
||||
"- Return translations in the SAME ORDER as the input array, one per input string.",
|
||||
"- Return ONLY the structured output required by the schema.",
|
||||
"- Preserve leading and trailing whitespace exactly.",
|
||||
"- Preserve line breaks exactly unless the source meaning requires otherwise.",
|
||||
"- Placeholder rules (critical):",
|
||||
" - Every placeholder in the source ({0}, {name}, {count}, etc.) MUST appear in the translation. Do NOT add, remove, or rename placeholders.",
|
||||
" - Do NOT change placeholder types: a simple placeholder like {0} CANNOT become a complex argument like {0, plural, ...}. Keep the same structural skeleton.",
|
||||
' - You MAY reorder placeholders to fit target language grammar (e.g. "{0} {1}" → "{1} {0}").',
|
||||
' - For ICU plural/select blocks that ALREADY EXIST in the source: you MUST adapt plural categories to the target language\'s CLDR rules. E.g. English "one/other" may need "one/few/many/other" in some languages. Add or remove categories as needed.',
|
||||
" - Do NOT translate ICU keywords (one, other, few, many, zero, two) or the # symbol inside plurals.",
|
||||
"- Preserve JSX-like component tags EXACTLY: <0/>, <0>text</0>, <1>…</1> — translate text between tags, keep the tags. You may reorder tags.",
|
||||
"- When adjectives/participles need to agree in number with a dynamic count, move them INSIDE the plural block so each category can have correct agreement.",
|
||||
"- Keep translations concise — these are UI labels, buttons, toasts, and short messages.",
|
||||
"- Do NOT translate brand/product names: Sofa, TMDB, Plex, Jellyfin, Emby, Trakt, Simkl, Overseerr, Jellyseerr.",
|
||||
"- Use natural, idiomatic phrasing for the target language.",
|
||||
"- Consistency: translate the same English term the same way every time.",
|
||||
'- Each input has a "text" field to translate. Some also have a "context" field explaining where/how the string is used — use this to produce more accurate translations.',
|
||||
"- If a string is already a brand name, code token, identifier, or path, keep it unchanged.",
|
||||
...(consistencyHints ? ["", consistencyHints] : []),
|
||||
].join("\n");
|
||||
|
||||
const prompt = JSON.stringify(input);
|
||||
|
||||
const schema = JSON.stringify({
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
translations: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
minItems: strings.length,
|
||||
maxItems: strings.length,
|
||||
},
|
||||
},
|
||||
required: ["translations"],
|
||||
});
|
||||
|
||||
const proc = Bun.spawn(
|
||||
[
|
||||
"claude",
|
||||
"-p",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--json-schema",
|
||||
schema,
|
||||
"--model",
|
||||
model,
|
||||
"--no-session-persistence",
|
||||
"--system-prompt",
|
||||
systemPrompt,
|
||||
"--tools",
|
||||
"",
|
||||
],
|
||||
{ stdout: "pipe", stderr: "pipe", stdin: new Response(prompt) },
|
||||
);
|
||||
|
||||
const [stdout, stderr] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`Claude CLI failed (exit ${exitCode}): ${stderr || stdout}`);
|
||||
}
|
||||
|
||||
let result: { translations: string[] };
|
||||
try {
|
||||
const parsed = JSON.parse(stdout);
|
||||
const structured = parsed.structured_output ?? parsed.result ?? parsed;
|
||||
result = typeof structured === "string" ? JSON.parse(structured) : structured;
|
||||
} catch {
|
||||
throw new Error(`Failed to parse Claude response. Raw output:\n${stdout.slice(0, 1000)}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(result.translations) || result.translations.length !== strings.length) {
|
||||
throw new Error(
|
||||
`Expected ${strings.length} translations, got ${result.translations?.length ?? 0}.\nRaw: ${stdout.slice(0, 1000)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return result.translations;
|
||||
}
|
||||
|
||||
async function translateBatch(
|
||||
strings: string[],
|
||||
locale: string,
|
||||
model: string,
|
||||
contextMap: Map<string, string>,
|
||||
consistencyHints?: string,
|
||||
): Promise<string[]> {
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const translations = await translateBatchRaw(
|
||||
strings,
|
||||
locale,
|
||||
model,
|
||||
contextMap,
|
||||
consistencyHints,
|
||||
);
|
||||
|
||||
const validationErrors: string[] = [];
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
const issues = validateTranslation(strings[i], translations[i]);
|
||||
if (issues.length > 0) {
|
||||
validationErrors.push(
|
||||
`index ${i}: ${issues.join(", ")}\nsource: ${JSON.stringify(strings[i])}\ntranslation: ${JSON.stringify(translations[i])}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
throw new Error(
|
||||
`Translation validation failed:\n${validationErrors.slice(0, 10).join("\n\n")}${
|
||||
validationErrors.length > 10
|
||||
? `\n\n...and ${validationErrors.length - 10} more validation errors`
|
||||
: ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return translations;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
const delayMs = 500 * attempt;
|
||||
console.warn(
|
||||
` attempt ${attempt}/${MAX_RETRIES} failed; retrying in ${delayMs}ms\n ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
await Bun.sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
||||
}
|
||||
|
||||
// ─── Main ────────────────────────────────────────────────
|
||||
|
||||
async function translateLocale(
|
||||
locale: string,
|
||||
model: string,
|
||||
batchSize: number,
|
||||
dryRun: boolean,
|
||||
contextMap: Map<string, string>,
|
||||
) {
|
||||
const filePath = resolve(PO_DIR, `${locale}.po`);
|
||||
const file = Bun.file(filePath);
|
||||
|
||||
if (!(await file.exists())) {
|
||||
throw new Error(`PO file not found for locale "${locale}": ${filePath}`);
|
||||
}
|
||||
|
||||
const content = await file.text();
|
||||
const ctx = { locale, sourceLocale: SOURCE_LOCALE, filename: filePath };
|
||||
const catalog = await poFormat.parse(content, ctx);
|
||||
|
||||
const untranslated = getUntranslatedEntries(catalog);
|
||||
|
||||
if (untranslated.length === 0) {
|
||||
console.log(` ${locale}: all strings already translated`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` ${locale}: ${untranslated.length} strings to translate`);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(` ${locale}: dry run — skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map of catalog key → translated string
|
||||
const translations = new Map<string, string>();
|
||||
// Map of source message → translated string (for consistency hints + validation)
|
||||
const messageTranslations = new Map<string, string>();
|
||||
const totalBatches = Math.ceil(untranslated.length / batchSize);
|
||||
|
||||
for (let i = 0; i < totalBatches; i++) {
|
||||
const batch = untranslated.slice(i * batchSize, (i + 1) * batchSize);
|
||||
const messages = batch.map((e) => e.message);
|
||||
|
||||
console.log(` batch ${i + 1}/${totalBatches} (${batch.length} strings)...`);
|
||||
|
||||
const hints = buildConsistencyHints(messageTranslations);
|
||||
const translated = await translateBatch(messages, locale, model, contextMap, hints);
|
||||
|
||||
for (let j = 0; j < batch.length; j++) {
|
||||
translations.set(batch[j].key, translated[j]);
|
||||
messageTranslations.set(batch[j].message, translated[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Write translations back to catalog using keys
|
||||
for (const [key, msgstr] of translations) {
|
||||
if (catalog[key]) {
|
||||
catalog[key].translation = msgstr;
|
||||
}
|
||||
}
|
||||
const serialized = await poFormat.serialize(catalog, { ...ctx, existing: content });
|
||||
await Bun.write(filePath, serialized);
|
||||
console.log(` ${locale}: wrote ${translations.size} translations`);
|
||||
}
|
||||
|
||||
const { locales, model, batchSize, dryRun } = parseArgs();
|
||||
|
||||
console.log(
|
||||
`Translating PO files via Claude (model: ${model}, batch size: ${batchSize})${dryRun ? " [DRY RUN]" : ""}\n`,
|
||||
);
|
||||
|
||||
const contextMap = await loadContext();
|
||||
|
||||
// Process locales sequentially to reduce CLI/rate-limit pressure and keep logs readable.
|
||||
for (const locale of locales) {
|
||||
await translateLocale(locale, model, batchSize, dryRun, contextMap);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
// Re-extract to restore #. placeholder comments that @lingui/format-po's
|
||||
// serialize() strips during the round-trip.
|
||||
console.log("Re-extracting to restore comments...");
|
||||
const extract = Bun.spawn(["bun", "run", "i18n:extract"], {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
if ((await extract.exited) !== 0) {
|
||||
console.error("Extraction failed");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Compiling catalogs...");
|
||||
const compile = Bun.spawn(["bun", "run", "i18n:compile"], {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
if ((await compile.exited) !== 0) {
|
||||
console.error("Catalog compilation failed");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Done!");
|
||||
+1252
-1252
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+737
-738
File diff suppressed because it is too large
Load Diff
+1257
-1257
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1259
-1259
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1247
-1247
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1258
-1258
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user