From b7bae0072f4e769fa88a9beeca39f793a7f2f648 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Tue, 9 Jun 2026 11:46:42 +0200 Subject: [PATCH 1/6] ci(i18n): detect unused & missing translation keys + clean key debt (#1636) --- .github/workflows/linting.yml | 1 + components/login/TVPasswordEntryModal.tsx | 2 +- package.json | 4 +- scripts/check-i18n-keys.mjs | 273 ++++++++++++++++++++++ scripts/i18n-keys.config.json | 46 ++++ translations/en.json | 122 ++-------- 6 files changed, 340 insertions(+), 108 deletions(-) create mode 100644 scripts/check-i18n-keys.mjs create mode 100644 scripts/i18n-keys.config.json diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 50013ba2b..4e1718dc3 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -97,6 +97,7 @@ jobs: - "check" - "format" - "typecheck" + - "i18n:check" steps: - name: "πŸ“₯ Checkout PR code" diff --git a/components/login/TVPasswordEntryModal.tsx b/components/login/TVPasswordEntryModal.tsx index 596bb610a..7db1671e9 100644 --- a/components/login/TVPasswordEntryModal.tsx +++ b/components/login/TVPasswordEntryModal.tsx @@ -280,7 +280,7 @@ export const TVPasswordEntryModal: React.FC = ({ diff --git a/package.json b/package.json index ccabd2f47..3158612e2 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "lint": "biome check --write --unsafe --max-diagnostics 1000", "format": "biome format --write .", "doctor": "expo-doctor", - "test": "bun run typecheck && bun run lint && bun run format && bun run doctor", + "i18n:check": "bun scripts/check-i18n-keys.mjs", + "i18n:fix-unused": "bun scripts/check-i18n-keys.mjs --fix-unused", + "test": "bun run typecheck && bun run lint && bun run format && bun run i18n:check && bun run doctor", "postinstall": "patch-package" }, "dependencies": { diff --git a/scripts/check-i18n-keys.mjs b/scripts/check-i18n-keys.mjs new file mode 100644 index 000000000..0e20e462e --- /dev/null +++ b/scripts/check-i18n-keys.mjs @@ -0,0 +1,273 @@ +#!/usr/bin/env bun +/** + * i18n key checker for Streamyfin. + * + * Detects: + * - MISSING keys: a static `t("a.b.c")` / `i18nKey="a.b.c"` referenced in the code + * that does not exist in the source locale (translations/en.json). These are bugs β€” + * the app renders the raw key. Always fails CI. + * - UNUSED (dead) keys: a key in the source locale that is referenced nowhere in the + * code, neither statically nor via a detected dynamic prefix (`t(`a.b.${x}`)`). + * These are dead weight that also clutter every locale on Crowdin. + * + * Dynamic usage is handled conservatively: + * - `t(`prefix.${x}`)` -> every key starting with `prefix.` is considered used. + * - `t(`${x}`)` -> fully dynamic, reported for manual review, never used to + * whitelist keys (in Streamyfin these are user-defined section + * titles, not translation keys). + * - Edge cases the static scan cannot see can be allow-listed in the config file. + * + * Usage: + * bun scripts/check-i18n-keys.mjs # report + exit 1 on missing OR unused + * bun scripts/check-i18n-keys.mjs --unused=warn # exit 1 only on missing; unused = warning + * bun scripts/check-i18n-keys.mjs --unused=off # ignore unused entirely + * bun scripts/check-i18n-keys.mjs --json # machine-readable output + * bun scripts/check-i18n-keys.mjs --fix-unused # remove dead keys from en.json (Crowdin syncs the rest) + */ + +import { + existsSync, + readdirSync, + readFileSync, + statSync, + writeFileSync, +} from "node:fs"; +import { extname, join, relative } from "node:path"; + +const ROOT = process.cwd(); +const args = process.argv.slice(2); +const flag = (name, def) => { + const a = args.find((x) => x === `--${name}` || x.startsWith(`--${name}=`)); + if (!a) return def; + const [, v] = a.split("="); + return v === undefined ? true : v; +}; +const UNUSED_MODE = String(flag("unused", "error")); // error | warn | off +const JSON_OUT = !!flag("json", false); +const FIX_UNUSED = !!flag("fix-unused", false); + +// ---- config ---- +const CONFIG_PATH = join(ROOT, "scripts", "i18n-keys.config.json"); +const DEFAULT_CONFIG = { + localesDir: "translations", + sourceLocale: "en", + // Scan the whole repo by default so keys referenced outside the obvious dirs + // (e.g. packages/, key constants in utils/atoms) are not wrongly flagged as dead. + srcDirs: ["."], + srcExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"], + excludeDirs: [ + "node_modules", + "ios", + "android", + ".expo", + ".git", + "dist", + "build", + "translations", + "scripts", + ], + // Keys (or glob-ish prefixes ending with .* or *) known to be used dynamically / externally. + ignoreUnused: [], +}; +const config = existsSync(CONFIG_PATH) + ? { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_PATH, "utf8")) } + : DEFAULT_CONFIG; + +// ---- helpers ---- +const flatten = (obj, prefix = "", out = {}) => { + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k; + if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out); + else out[key] = v; + } + return out; +}; + +const globMatch = (key, pattern) => { + if (pattern.endsWith(".*")) + return key === pattern.slice(0, -2) || key.startsWith(pattern.slice(0, -1)); + if (pattern.endsWith("*")) return key.startsWith(pattern.slice(0, -1)); + return key === pattern; +}; + +const walk = (dir, files = []) => { + let entries; + try { + entries = readdirSync(dir); + } catch { + return files; + } + for (const name of entries) { + const full = join(dir, name); + let st; + try { + st = statSync(full); + } catch { + continue; + } + if (st.isDirectory()) { + if (config.excludeDirs.includes(name)) continue; + walk(full, files); + } else if (config.srcExtensions.includes(extname(name))) { + files.push(full); + } + } + return files; +}; + +// ---- load source keys ---- +const sourcePath = join(ROOT, config.localesDir, `${config.sourceLocale}.json`); +const sourceKeys = Object.keys( + flatten(JSON.parse(readFileSync(sourcePath, "utf8"))), +); +const sourceKeySet = new Set(sourceKeys); + +// ---- scan code ---- +const STATIC_RE = /\bt\(\s*(['"])((?:\\.|(?!\1).)+?)\1/g; // t("a.b") / t('a.b') +const TPL_STATIC_RE = /\bt\(\s*`([^`$]+)`/g; // t(`a.b`) no interpolation +const TPL_DYN_RE = /\bt\(\s*`([^`$]*)\$\{/g; // t(`a.b.${x}`) -> prefix "a.b." +const I18NKEY_RE = /\bi18nKey\s*=\s*(?:\{\s*)?(['"])((?:\\.|(?!\1).)+?)\1/g; // +const KEY_SHAPE = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)+$/; // dotted key, e.g. home.x.y + +const usedStatic = new Set(); // keys passed to t(...) / i18nKey β€” used for MISSING detection +const dynamicPrefixes = new Set(); +const fullyDynamic = []; // { file, line } +let codeBlob = ""; // all (comment-stripped) source text β€” searched for delimited key literals + +// Strip comments so keys mentioned in comments (e.g. `// t("old.key")`) are not counted as +// usage. Block comments and JSX {/* */} are blanked (preserving newlines for line numbers); +// line comments are only stripped when `//` follows start/whitespace/punctuation, which keeps +// `://` inside string URLs intact. +const stripComments = (src) => + src + .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")) + .replace(/(^|[\s;{}()[\],=>])\/\/[^\n]*/g, (_m, p) => p); + +const files = config.srcDirs.flatMap((d) => + walk(join(ROOT, d === "." ? "" : d) || ROOT), +); +for (const file of files) { + const text = readFileSync(file, "utf8"); + const clean = stripComments(text); + codeBlob += `\n${clean}`; + for (const m of clean.matchAll(STATIC_RE)) usedStatic.add(m[2]); + for (const m of clean.matchAll(TPL_STATIC_RE)) usedStatic.add(m[1]); + for (const m of clean.matchAll(I18NKEY_RE)) usedStatic.add(m[2]); + for (const m of clean.matchAll(TPL_DYN_RE)) { + const prefix = m[1]; + if (prefix?.includes(".")) dynamicPrefixes.add(prefix); + else { + const idx = clean.slice(0, m.index).split("\n").length; + fullyDynamic.push({ file: relative(ROOT, file), line: idx }); + } + } +} + +const prefixList = [...dynamicPrefixes]; +// A key counts as used if its EXACT delimited literal ("k" / 'k' / `k`) appears anywhere in +// the code (covers t("k"), , and keys stored as bare string constants in +// arrays/config then resolved via t(variable)), or it is reached via a dynamic prefix, or +// explicitly allow-listed. Delimited search avoids substring false-matches (e.g. a.b vs a.b_c). +const literalUsed = (key) => + codeBlob.includes(`"${key}"`) || + codeBlob.includes(`'${key}'`) || + codeBlob.includes(`\`${key}\``); +const isUsed = (key) => + literalUsed(key) || + prefixList.some((p) => key.startsWith(p)) || + config.ignoreUnused.some((g) => globMatch(key, g)); + +// ---- compute ---- +const unused = sourceKeys.filter((k) => !isUsed(k)).sort(); +// Static references are always validated, even under a dynamic prefix: a dynamic prefix only +// affects the UNUSED calculation, never MISSING. +const missing = [...usedStatic] + .filter((k) => KEY_SHAPE.test(k) && !sourceKeySet.has(k)) + .sort(); +// Known limitation: only keys seen in a static t("…") / i18nKey="…" / t(`…`) call are +// validated for MISSING. A key stored as a bare string constant and resolved via t(variable) +// counts as USED (via literalUsed β†’ not flagged unused) but its existence in en.json is not +// checked here β€” static analysis can't resolve which key a runtime variable holds. Streamyfin +// keys are static literals in practice; revisit if dynamic key constants become common. + +// ---- optional fix: strip dead keys from the source locale (en.json) ---- +const removeKey = (obj, parts) => { + const [head, ...rest] = parts; + if (!(head in obj)) return; + if (rest.length === 0) { + delete obj[head]; + return; + } + removeKey(obj[head], rest); + if ( + obj[head] && + typeof obj[head] === "object" && + Object.keys(obj[head]).length === 0 + ) + delete obj[head]; +}; +if (FIX_UNUSED && unused.length) { + // Only edit the SOURCE locale (en.json). Crowdin owns the target locales and removes + // the keys from them automatically on the next sync once they disappear from the source. + const data = JSON.parse(readFileSync(sourcePath, "utf8")); + for (const key of unused) removeKey(data, key.split(".")); + writeFileSync(sourcePath, `${JSON.stringify(data, null, 2)}\n`); + console.log( + `🧹 Removed ${unused.length} dead key(s) from ${config.sourceLocale}.json (Crowdin will sync the other locales).`, + ); +} + +// ---- report ---- +if (JSON_OUT) { + console.log( + JSON.stringify( + { + sourceKeys: sourceKeys.length, + missing, + unused, + dynamicPrefixes: prefixList, + fullyDynamic, + }, + null, + 2, + ), + ); +} else { + console.log( + `πŸ”‘ i18n key check β€” source: ${relative(ROOT, sourcePath)} (${sourceKeys.length} keys), scanned ${files.length} files`, + ); + if (prefixList.length) + console.log( + ` dynamic prefixes treated as used: ${prefixList.map((p) => `${p}*`).join(", ")}`, + ); + if (fullyDynamic.length) + console.log( + ` ⚠️ ${fullyDynamic.length} fully-dynamic t(\`\${…}\`) call(s) (not key-based, manual review): ${fullyDynamic.map((d) => `${d.file}:${d.line}`).join(", ")}`, + ); + + if (missing.length) { + console.log( + `\n❌ MISSING keys (used in code, absent from ${config.sourceLocale}.json) β€” ${missing.length}:`, + ); + for (const k of missing) console.log(` - ${k}`); + } else console.log("\nβœ… No missing keys."); + + if (UNUSED_MODE !== "off") { + if (unused.length) { + console.log( + `\n${UNUSED_MODE === "error" ? "❌" : "⚠️ "} UNUSED keys (in ${config.sourceLocale}.json, referenced nowhere) β€” ${unused.length}:`, + ); + for (const k of unused) console.log(` - ${k}`); + console.log( + `\n β†’ remove with: bun scripts/check-i18n-keys.mjs --fix-unused`, + ); + console.log( + ` β†’ or allow-list a dynamic key in scripts/i18n-keys.config.json ("ignoreUnused").`, + ); + } else console.log("\nβœ… No unused keys."); + } +} + +const fail = + missing.length > 0 || (UNUSED_MODE === "error" && unused.length > 0); +process.exit(fail ? 1 : 0); diff --git a/scripts/i18n-keys.config.json b/scripts/i18n-keys.config.json new file mode 100644 index 000000000..c5196d825 --- /dev/null +++ b/scripts/i18n-keys.config.json @@ -0,0 +1,46 @@ +{ + "localesDir": "translations", + "sourceLocale": "en", + "srcDirs": [ + "app", + "components", + "hooks", + "providers", + "utils", + "modules", + "packages", + "constants" + ], + "srcExtensions": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"], + "excludeDirs": [ + "node_modules", + "ios", + "android", + ".expo", + ".git", + "dist", + "build", + "translations" + ], + "_ignoreUnusedNote": "Keys present in en.json but referenced by no t() call β€” allow-listed so the unused-key cleanup keeps them. Two kinds: (1) ORPHANS of features that already exist but render hardcoded strings or use other keys β€” watchlists.* (add/remove, handled by WatchlistSheet via hooks), pin.* (confirm step of the existing PIN setup flow), player.* (in-player subtitle search, swipe-down hint, stop-playback confirm); these should be wired to the live UI or removed in a follow-up i18n pass, not assumed to be future work. (2) GENUINELY PLANNED, tracked by issues: home.settings.other.show_large_home_carousel (#1702 media-bar), home.settings.logs.delete_all_logs (#1703 iOS logs fix), home.suggested_episodes (#1704).", + "ignoreUnused": [ + "watchlists.add_to_watchlist", + "watchlists.remove_from_watchlist", + "watchlists.create_one_first", + "watchlists.no_compatible_watchlists", + "pin.confirm_pin", + "pin.pins_dont_match", + "player.search_subtitles", + "player.subtitle_search", + "player.subtitle_download_hint", + "player.subtitle_tracks", + "player.using_jellyfin_server", + "player.swipe_down_settings", + "player.stopPlayback", + "player.stopPlayingTitle", + "player.stopPlayingConfirm", + "home.settings.other.show_large_home_carousel", + "home.settings.logs.delete_all_logs", + "home.suggested_episodes" + ] +} diff --git a/translations/en.json b/translations/en.json index 581e82c48..25b978380 100644 --- a/translations/en.json +++ b/translations/en.json @@ -261,43 +261,6 @@ "None": "None", "OnlyForced": "Only forced" }, - "text_color": "Text color", - "background_color": "Background color", - "outline_color": "Outline color", - "outline_thickness": "Outline thickness", - "background_opacity": "Background opacity", - "outline_opacity": "Outline opacity", - "bold_text": "Bold text", - "colors": { - "Black": "Black", - "Gray": "Gray", - "Silver": "Silver", - "White": "White", - "Maroon": "Maroon", - "Red": "Red", - "Fuchsia": "Fuchsia", - "Yellow": "Yellow", - "Olive": "Olive", - "Green": "Green", - "Teal": "Teal", - "Lime": "Lime", - "Purple": "Purple", - "Navy": "Navy", - "Blue": "Blue", - "Aqua": "Aqua" - }, - "thickness": { - "None": "None", - "Thin": "Thin", - "Normal": "Normal", - "Thick": "Thick" - }, - "subtitle_color": "Subtitle color", - "subtitle_background_color": "Background color", - "subtitle_font": "Subtitle font", - "ksplayer_title": "KSPlayer settings", - "hardware_decode": "Hardware decoding", - "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "opensubtitles_title": "OpenSubtitles", "opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.", "opensubtitles_api_key": "API key", @@ -315,25 +278,6 @@ "bottom": "Bottom" } }, - "vlc_subtitles": { - "title": "VLC subtitle settings", - "hint": "Customize subtitle appearance for the VLC player. Changes take effect on next playback.", - "text_color": "Text color", - "background_color": "Background color", - "background_opacity": "Background opacity", - "outline_color": "Outline color", - "outline_opacity": "Outline opacity", - "outline_thickness": "Outline thickness", - "bold": "Bold text", - "margin": "Bottom margin" - }, - "video_player": { - "title": "Video player", - "video_player": "Video player", - "video_player_description": "Choose which video player to use on iOS.", - "ksplayer": "KSPlayer", - "vlc": "VLC" - }, "other": { "other_title": "Other", "video_orientation": "Video orientation", @@ -351,11 +295,6 @@ "UNKNOWN": "Unknown" }, "safe_area_in_controls": "Safe area in controls", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, "show_custom_menu_links": "Show custom menu links", "show_large_home_carousel": "Show large home carousel (beta)", "hide_libraries": "Hide libraries", @@ -367,9 +306,6 @@ "max_auto_play_episode_count": "Max auto-play episode count", "disabled": "Disabled" }, - "downloads": { - "downloads_title": "Downloads" - }, "music": { "title": "Music", "playback_title": "Playback", @@ -413,23 +349,18 @@ "read_more_about_marlin": "Read more about Marlin.", "save_button": "Save", "toasts": { - "saved": "Saved", - "refreshed": "Settings refreshed from server" - }, - "refresh_from_server": "Refresh settings from server" + "saved": "Saved" + } }, "streamystats": { - "enable_streamystats": "Enable Streamystats", "disable_streamystats": "Disable Streamystats", "enable_search": "Use for search", "url": "URL", "server_url_placeholder": "http(s)://streamystats.example.com", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "read_more_about_streamystats": "Read more about Streamystats.", - "save_button": "Save", "save": "Save", "features_title": "Features", - "home_sections_title": "Home sections", "enable_movie_recommendations": "Movie recommendations", "enable_series_recommendations": "Series recommendations", "enable_promoted_watchlists": "Promoted watchlists", @@ -445,8 +376,7 @@ "refresh_from_server": "Refresh settings from server" }, "kefinTweaks": { - "watchlist_enabler": "Enable watchlist integration", - "watchlist_button": "Toggle watchlist integration" + "watchlist_enabler": "Enable watchlist integration" } }, "storage": { @@ -457,7 +387,6 @@ "delete_all_downloaded_files": "Delete all downloaded files", "music_cache_title": "Music cache", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", - "enable_music_cache": "Enable music cache", "clear_music_cache": "Clear music cache", "music_cache_size": "{{size}} cached", "music_cache_cleared": "Music cache cleared", @@ -467,8 +396,6 @@ "clear_all_cache": "Clear all cache", "clear_all_cache_confirm": "Clear all cache?", "clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.", - "clear_all_cache_success": "Cache cleared", - "clear_all_cache_success_desc": "All cache has been cleared successfully.", "clear_all_cache_error_desc": "An error occurred while clearing the cache." }, "intro": { @@ -490,15 +417,12 @@ "system": "System" }, "toasts": { - "error_deleting_files": "Error deleting files", - "background_downloads_enabled": "Background downloads enabled", - "background_downloads_disabled": "Background downloads disabled" + "error_deleting_files": "Error deleting files" }, "security": { "title": "Security", "inactivity_timeout": { "title": "Inactivity timeout", - "description": "Auto logout after inactivity", "disabled": "Disabled", "1_minute": "1 minute", "5_minutes": "5 minutes", @@ -508,6 +432,10 @@ "4_hours": "4 hours", "24_hours": "24 hours" } + }, + "dashboard": { + "title": "Dashboard", + "sessions_title": "Sessions" } }, "sessions": { @@ -518,10 +446,7 @@ "downloads_title": "Downloads", "series": "Series", "movies": "Movies", - "queue": "Queue", "other_media": "Other media", - "queue_hint": "Queue and downloads will be lost on app restart", - "no_items_in_queue": "No items in queue", "no_downloaded_items": "No downloaded items", "delete_all_movies_button": "Delete all movies", "delete_all_series_button": "Delete all series", @@ -546,13 +471,8 @@ "failed_to_delete_all_series": "Failed to delete all series", "deleted_media_successfully": "Deleted other media successfully!", "failed_to_delete_media": "Failed to delete other media", - "download_deleted": "Download deleted", "download_cancelled": "Download cancelled", "could_not_delete_download": "Could not delete download", - "download_paused": "Download paused", - "could_not_pause_download": "Could not pause download", - "download_resumed": "Download resumed", - "could_not_resume_download": "Could not resume download", "download_completed": "Download completed", "download_failed": "Download failed", "download_failed_for_item": "Download failed for {{item}} - {{error}}", @@ -562,10 +482,7 @@ "item_already_downloading": "{{item}} is already downloading", "all_files_deleted": "All downloads deleted successfully", "files_deleted_by_type": "{{count}} {{type}} deleted", - "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", - "failed_to_clean_cache_directory": "Failed to clean cache directory", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", - "go_to_downloads": "Go to Downloads", "file_deleted": "{{item}} deleted" } } @@ -583,16 +500,17 @@ "none": "None", "track": "Track", "cancel": "Cancel", - "stop": "Stop", "delete": "Delete", "ok": "OK", "remove": "Remove", - "next": "Next", "back": "Back", "continue": "Continue", "verifying": "Verifying...", "login": "Login", - "refresh": "Refresh" + "episodes": "Episodes", + "movies": "Movies", + "loading": "Loading…", + "seeAll": "See all" }, "search": { "search": "Search...", @@ -691,10 +609,6 @@ "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "message_from_server": "Message from server: {{message}}", "next_episode": "Next episode", - "refresh_tracks": "Refresh tracks", - "audio_tracks": "Audio tracks:", - "playback_state": "Playback state:", - "index": "Index:", "continue_watching": "Continue watching", "go_back": "Go back", "downloaded_file_title": "You have this file downloaded", @@ -723,7 +637,8 @@ "stopPlayback": "Stop playback", "stopPlayingTitle": "Stop playing \"{{title}}\"?", "stopPlayingConfirm": "Are you sure you want to stop playback?", - "downloaded": "Downloaded" + "downloaded": "Downloaded", + "missing_parameters": "Missing playback parameters" }, "chapters": { "title": "Chapters", @@ -761,7 +676,6 @@ "show_more": "Show more", "show_less": "Show less", "left": "left", - "more_info": "More info", "director": "Director", "cast": "Cast", "technical_details": "Technical details", @@ -784,7 +698,8 @@ "resume_playback": "Resume playback", "resume_playback_description": "Do you want to continue where you left off or start from the beginning?", "play_from_start": "Play from start", - "continue_from": "Continue from {{time}}" + "continue_from": "Continue from {{time}}", + "no_data_available": "No data available" }, "live_tv": { "next": "Next", @@ -888,13 +803,9 @@ "playlists": "Playlists", "tracks": "Songs" }, - "filters": { - "all": "All" - }, "recently_added": "Recently added", "recently_played": "Recently played", "frequently_played": "Frequently played", - "explore": "Explore", "top_tracks": "Top songs", "play": "Play", "shuffle": "Shuffle", @@ -1028,7 +939,6 @@ "pairing": { "pair_with_phone": "Pair with phone", "pair_with_phone_title": "Log in on TV", - "pair_with_phone_description": "Scan the QR code displayed on your TV to log in", "waiting_for_phone": "Waiting for phone...", "scan_with_phone": "Scan with the Streamyfin app on your phone", "logging_in": "Logging in...", From 4eb734c99fbb154aa76c9bd4462f822f14ab53dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:05:23 +0200 Subject: [PATCH 2/6] chore(deps): Update expo monorepo to v1.19.9 (#1606) --- bun.lock | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index cf5844776..97ba4fa23 100644 --- a/bun.lock +++ b/bun.lock @@ -108,7 +108,7 @@ "@types/react": "~19.2.10", "@types/react-test-renderer": "19.1.0", "cross-env": "10.1.0", - "expo-doctor": "1.19.7", + "expo-doctor": "1.19.9", "husky": "9.1.7", "lint-staged": "17.0.5", "react-test-renderer": "19.2.3", @@ -969,7 +969,7 @@ "expo-device": ["expo-device@56.0.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-ucVcGPkvBrl2QHuy7XcYex2Y6BETvJ6TREutZrwLGUDnlvbpKS8KfQoNZOpvkyo5Nmm9RrasYQ0CrXmBHho2mg=="], - "expo-doctor": ["expo-doctor@1.19.7", "", { "bin": { "expo-doctor": "bin/expo-doctor.js" } }, "sha512-pzn7QtCifRlvGIQz8k7kszeYFaI5Yn81WTHlk/20tmd3jwnXxPjlcdyhFSkuRtO2v4a9gA/6aUWVBOosfffj9w=="], + "expo-doctor": ["expo-doctor@1.19.9", "", { "bin": { "expo-doctor": "bin/expo-doctor.js" } }, "sha512-SJW5HxEDQ9f5QdFvrUwfbdJZ4HI0EAAxsrJqrHBFjKBum+uSOcEIZPLRibwNQLTHOwTO1TWNLiMlF9sDUBWeYw=="], "expo-file-system": ["expo-file-system@56.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA=="], @@ -1599,7 +1599,7 @@ "react-native-text-ticker": ["react-native-text-ticker@1.15.0", "", {}, "sha512-d/uK+PIOhsYMy1r8h825iq/nADiHsabz3WMbRJSnkpQYn+K9aykUAXRRhu8ZbTAzk4CgnUWajJEFxS5ZDygsdg=="], - "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd"], + "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd", "sha512-vfkld2jUj7EPkAjIc/Vbx4Q4MtOOLmYtCYCE2dWJsyLnPqgj1f0xVzBxbeVP7dfT+eSh4KIXfdxESXaHgrXIlw=="], "react-native-udp": ["react-native-udp@4.1.7", "", { "dependencies": { "buffer": "^5.6.0", "events": "^3.1.0" } }, "sha512-NUE3zewu61NCdSsLlj+l0ad6qojcVEZPT4hVG/x6DU9U4iCzwtfZSASh9vm7teAcVzLkdD+cO3411LHshAi/wA=="], diff --git a/package.json b/package.json index 3158612e2..588ada9ae 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "@types/react": "~19.2.10", "@types/react-test-renderer": "19.1.0", "cross-env": "10.1.0", - "expo-doctor": "1.19.7", + "expo-doctor": "1.19.9", "husky": "9.1.7", "lint-staged": "17.0.5", "react-test-renderer": "19.2.3", From 02ffac167bf9a9dbe4303ec8fed750f6ac961812 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:58:35 +0200 Subject: [PATCH 3/6] chore(deps): Update dependency xcode to v26.5 (#1609) --- .github/workflows/build-apps.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index fd68e23a1..a9f0ed99d 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -219,7 +219,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: πŸ—οΈ Setup EAS uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main @@ -284,7 +284,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: πŸš€ Build iOS app env: @@ -344,7 +344,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: πŸ—οΈ Setup EAS uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main @@ -412,7 +412,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: πŸš€ Build iOS app env: From d12beee529f5b79d15f4230efcae878af706abbf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:43:22 +0200 Subject: [PATCH 4/6] chore(deps): Update actions/checkout action to v6.0.3 (#1686) --- .github/workflows/build-apps.yml | 12 ++++++------ .github/workflows/check-lockfile.yml | 2 +- .github/workflows/ci-codeql.yml | 2 +- .github/workflows/crowdin.yml | 2 +- .github/workflows/detect-duplicate.yml | 2 +- .github/workflows/linting.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- .github/workflows/trivy-scan.yml | 2 +- .github/workflows/update-issue-form.yml | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index a9f0ed99d..8ce400909 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -33,7 +33,7 @@ jobs: swap-storage: false - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -116,7 +116,7 @@ jobs: swap-storage: false - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -187,7 +187,7 @@ jobs: steps: - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -252,7 +252,7 @@ jobs: steps: - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -312,7 +312,7 @@ jobs: steps: - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -380,7 +380,7 @@ jobs: steps: - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index ae4c0fe02..0cb8afc3a 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -19,7 +19,7 @@ jobs: steps: - name: πŸ“₯ Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} show-progress: false diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index ba1c08dc8..0103ed5ce 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: πŸ“₯ Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: 🏁 Initialize CodeQL uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index feb9a00fe..b0ea48a25 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -23,7 +23,7 @@ jobs: steps: - name: πŸ“₯ Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml index 09aa23565..265f9efe9 100644 --- a/.github/workflows/detect-duplicate.yml +++ b/.github/workflows/detect-duplicate.yml @@ -21,7 +21,7 @@ jobs: contents: read steps: - name: πŸ“₯ Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 4e1718dc3..8edb89169 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -51,7 +51,7 @@ jobs: contents: read steps: - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: πŸ›’ Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive @@ -101,7 +101,7 @@ jobs: steps: - name: "πŸ“₯ Checkout PR code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06dba5e37..d6dcda788 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: steps: - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 submodules: recursive @@ -182,7 +182,7 @@ jobs: actions: read # required for `gh run download` to list/fetch this run's artifacts steps: - name: πŸ“₯ Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 show-progress: false diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 9eea1fbc1..1aded4ba2 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -27,7 +27,7 @@ jobs: security-events: write # upload SARIF to code scanning steps: - name: πŸ“₯ Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # Rotate the DB cache weekly (matches the scheduled scan): cache hits within the week # instead of a fresh immutable entry per run, still refreshing the DB every week. diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index 7cc321977..a23ecdf2c 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -18,7 +18,7 @@ jobs: steps: - name: πŸ“₯ Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: "🟒 Setup Node.js" uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 From 6f0230c2ca2a3fc58edb00d92bd3d899249e2642 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:37:14 +0200 Subject: [PATCH 5/6] chore(deps): Update expo/expo-github-action digest to eab7a23 (#1685) --- .github/workflows/build-apps.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index 8ce400909..7068c2f03 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -222,7 +222,7 @@ jobs: xcode-version: "26.5" - name: πŸ—οΈ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} @@ -347,7 +347,7 @@ jobs: xcode-version: "26.5" - name: πŸ—οΈ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6dcda788..c06e8b348 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,7 +88,7 @@ jobs: bun run submodule-reload - name: πŸ—οΈ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} From 168bf2e54e26c525898cd378fe3cbb8e7fc18789 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:25:26 +0200 Subject: [PATCH 6/6] chore(deps): Update github/codeql-action action to v4.36.2 (#1687) --- .github/workflows/ci-codeql.yml | 6 +++--- .github/workflows/trivy-scan.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index 0103ed5ce..f79cf58a4 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -27,13 +27,13 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: 🏁 Initialize CodeQL - uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} queries: +security-extended,security-and-quality - name: πŸ› οΈ Autobuild - uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - name: πŸ§ͺ Perform CodeQL Analysis - uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 1aded4ba2..4972e14fc 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -54,7 +54,7 @@ jobs: output: trivy-results.sarif - name: πŸ“€ Upload results to code scanning - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: trivy-results.sarif category: trivy-fs