test(i18n): every locale key must be read by the app; 75 dead keys removed
A contract test scans the source (tests excluded) for each key as a whole literal, or under a
dynamic prefix like `plex.sort.${k}`, with plural forms counted through their base key. It found 75
keys that had outlived the code showing them (the old sidebar sort vocabulary, the old login page, a
pager the DataTable replaced, …); they are gone from EN and HU. A key read in a way the scan cannot
see goes in an explicit ALLOWED map with its reason.
This commit is contained in:
@@ -72,11 +72,6 @@
|
||||
},
|
||||
"loading": "Loading channels…",
|
||||
"empty": "No channels.",
|
||||
"pager": {
|
||||
"prev": "Previous",
|
||||
"next": "Next",
|
||||
"page": "Page {{page}} of {{total}}"
|
||||
},
|
||||
"cols": {
|
||||
"priority": "Prio",
|
||||
"channel": "Channel",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"loading": "Loading…",
|
||||
"language": "Language",
|
||||
"somethingWrong": "Something went wrong.",
|
||||
"itemCount_one": "{{formattedCount}} item",
|
||||
"itemCount_other": "{{formattedCount}} items",
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
"willReset": "will reset to default on save",
|
||||
"reset": "Reset",
|
||||
"resetHint": "Remove this override and fall back to the file/env default.",
|
||||
"resetDone": "Reset to default",
|
||||
"usingDefault": "using default",
|
||||
"overridden": "overridden in DB",
|
||||
"secretSet": "•••••••• stored",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"pager": {
|
||||
"prev": "Previous",
|
||||
"next": "Next",
|
||||
"page": "Page {{page}} of {{total}}",
|
||||
"pageLabel": "Page",
|
||||
"ofTotal": "of {{total}}"
|
||||
}
|
||||
|
||||
@@ -112,9 +112,6 @@
|
||||
},
|
||||
"share": {
|
||||
"title": "Share download",
|
||||
"label": "Recipient email",
|
||||
"placeholder": "user@example.com",
|
||||
"submit": "Share",
|
||||
"done": "Shared with {{email}}",
|
||||
"failed": "Couldn’t share.",
|
||||
"userSection": "Share with a user",
|
||||
@@ -162,7 +159,6 @@
|
||||
"redownloadAction": "Re-download"
|
||||
},
|
||||
"profiles": {
|
||||
"manage": "Manage formats",
|
||||
"title": "Download formats",
|
||||
"builtin": "Built-in",
|
||||
"yours": "Your formats",
|
||||
@@ -186,14 +182,11 @@
|
||||
"deleteConfirm": "Delete the format “{{name}}”?"
|
||||
},
|
||||
"admin": {
|
||||
"storageTitle": "Storage",
|
||||
"readyFiles": "Ready files",
|
||||
"totalSize": "Total size",
|
||||
"cap": "Cache cap",
|
||||
"noCap": "no cap",
|
||||
"perUser": "Per-user footprint",
|
||||
"user": "User",
|
||||
"footprint": "Footprint",
|
||||
"editQuota": "Edit quota",
|
||||
"quotaTitle": "Download quota — {{email}}",
|
||||
"maxBytes": "Storage limit (GB)",
|
||||
@@ -202,20 +195,9 @@
|
||||
"unlimited": "Unlimited (bypass storage limit)",
|
||||
"reset": "Reset to default",
|
||||
"save": "Save",
|
||||
"custom": "custom",
|
||||
"default": "default",
|
||||
"quotaPickPlaceholder": "Set a user's quota…",
|
||||
"noFootprint": "No downloads yet."
|
||||
},
|
||||
"cols": {
|
||||
"title": "Title",
|
||||
"channel": "Channel",
|
||||
"format": "Format",
|
||||
"size": "Size",
|
||||
"status": "Status",
|
||||
"added": "Added",
|
||||
"user": "User"
|
||||
},
|
||||
"clipBadge": "Clip",
|
||||
"errors": {
|
||||
"quota": {
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
"noMatches": "No videos match these filters.",
|
||||
"channelScopeHint": "This channel isn't in your Feed — it may be hidden. Its videos are in the shared library.",
|
||||
"viewInLibrary": "View in Library",
|
||||
"videoCount_one": "{{formattedCount}} video",
|
||||
"videoCount_other": "{{formattedCount}} videos",
|
||||
"sortLabel": "Sort",
|
||||
"viewLabel": "View",
|
||||
"showLabel": "Watch state",
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
{
|
||||
"feed": "Feed",
|
||||
"searchPlaceholder": "Search your subscriptions…",
|
||||
"clearSearch": "Clear search",
|
||||
"searchYoutube": "Search YouTube",
|
||||
"searchYoutubeTip": "Search live on YouTube for this term (uses shared API quota). You can also press Enter.",
|
||||
"go": "Go",
|
||||
"goTip": "Search (or press Enter)",
|
||||
"prevModule": "Previous module",
|
||||
"nextModule": "Next module",
|
||||
"channelManager": "Channel manager",
|
||||
"usageStats": "Usage & stats",
|
||||
"scheduler": "Scheduler",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"navLabel": "Notifications",
|
||||
"title": "Notifications",
|
||||
"subtitle": "Updates from your library and the system.",
|
||||
"tabInbox": "Notifications",
|
||||
"empty": "You're all caught up.",
|
||||
"markAllRead": "Mark all read",
|
||||
"clearAll": "Clear all",
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
{
|
||||
"tagline": "A self-hosted, filterable Feed of your YouTube subscriptions — sort, tag, and tune out the noise. Sign in with Google to get started; YouTube access is an optional, separate step you control.",
|
||||
"signIn": "Sign in with Google",
|
||||
"approved": "You're already approved — just sign in with Google above.",
|
||||
"requested": "Thanks — your request is in. We'll email you when it's approved.",
|
||||
"noAccessYet": "No access yet?",
|
||||
"denied": "That Google account isn't approved. Request access below.",
|
||||
"emailPlaceholder": "you@gmail.com",
|
||||
"sending": "Sending…",
|
||||
"requestAccess": "Request access",
|
||||
"submitError": "Couldn't submit — check the address and try again."
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"tab": "Messages",
|
||||
"navLabel": "Messages",
|
||||
"popOut": "Open in pop-out",
|
||||
"expand": "Expand",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"primary": "Primary navigation",
|
||||
"pin": "Pin sidebar open",
|
||||
"unpin": "Unpin sidebar",
|
||||
"adminSection": "Admin",
|
||||
"role": {
|
||||
"admin": "Admin",
|
||||
"user": "User",
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
{
|
||||
"title": "Notifications",
|
||||
"clearAll": "Clear all",
|
||||
"clear": "Clear",
|
||||
"empty": "No notifications yet.",
|
||||
"actionNeeded": "Action needed",
|
||||
"dismiss": "Dismiss",
|
||||
"findInFeed": "Find in Feed",
|
||||
"unhide": "Unhide",
|
||||
"unwatch": "Unwatch",
|
||||
"openInManager": "Channel manager",
|
||||
"openOnYouTube": "Open on YouTube",
|
||||
"unhidden": "Unhidden “{{title}}”",
|
||||
"unwatched": "Unwatched “{{title}}”"
|
||||
"openOnYouTube": "Open on YouTube"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"syncYoutube": "Sync from YouTube",
|
||||
"syncedToast": "Synced {{count}} playlists from YouTube",
|
||||
"syncFailed": "Couldn't sync from YouTube",
|
||||
"ytReadonly": "Synced from YouTube — edits sync back",
|
||||
"noneYet": "No playlists yet",
|
||||
"newPlaylist": "New playlist…",
|
||||
"searchPlaceholder": "Search playlists…",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"navLabel": "Plex",
|
||||
"backToFeed": "YouTube Feed",
|
||||
"backToLibrary": "Back to library",
|
||||
"collection": {
|
||||
"playAll": "Play all",
|
||||
@@ -113,8 +112,6 @@
|
||||
"transcode_sw": "Transcode · SW"
|
||||
}
|
||||
},
|
||||
"count": "{{count}} titles",
|
||||
"searchCount": "{{count}} matches",
|
||||
"noMatches": "No matches.",
|
||||
"noMatchesFiltered_one": "No matches — a filter is also narrowing the results.",
|
||||
"noMatchesFiltered_other": "No matches — {{count}} filters are also narrowing the results.",
|
||||
@@ -303,10 +300,8 @@
|
||||
},
|
||||
"playlist": {
|
||||
"section": "Playlists",
|
||||
"newPlaceholder": "New playlist…",
|
||||
"pickerPlaceholder": "Find or create a playlist…",
|
||||
"createNamed": "Create \"{{name}}\"",
|
||||
"create": "Create playlist",
|
||||
"none": "No playlists yet.",
|
||||
"deleteConfirm": "Delete the playlist \"{{title}}\"?",
|
||||
"rename": "Rename",
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
"customize": "Customize sidebar",
|
||||
"done": "Done",
|
||||
"editHint": "Drag to reorder · eye to show/hide",
|
||||
"channel": "Channel",
|
||||
"loading": "Loading…",
|
||||
"channelCount": "{{count}} channel",
|
||||
"channelCount_other": "{{count}} channels",
|
||||
"dragToReorder": "Drag to reorder",
|
||||
"showWidget": "Show widget",
|
||||
"hideWidget": "Hide widget",
|
||||
@@ -19,8 +15,6 @@
|
||||
"unpin": "Unpin panel",
|
||||
"any": "Any",
|
||||
"all": "All",
|
||||
"tagModeTooltip": "Match any vs all selected tags",
|
||||
"custom": "Custom",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"clearDates": "clear dates",
|
||||
@@ -56,17 +50,6 @@
|
||||
"saved": "Saved",
|
||||
"hidden": "Hidden"
|
||||
},
|
||||
"sort": {
|
||||
"newest": "Newest",
|
||||
"oldest": "Oldest",
|
||||
"views": "Most viewed",
|
||||
"duration_desc": "Longest",
|
||||
"duration_asc": "Shortest",
|
||||
"title": "Name (A–Z)",
|
||||
"subscribers": "Channel subscribers",
|
||||
"priority": "Channel priority",
|
||||
"shuffle": "Surprise me"
|
||||
},
|
||||
"content": {
|
||||
"normal": "Normal",
|
||||
"shorts": "Shorts",
|
||||
@@ -91,7 +74,5 @@
|
||||
"untilShort": "Until {{date}}"
|
||||
},
|
||||
"channelSearch": "Search channels…",
|
||||
"noChannelMatch": "No channel matches.",
|
||||
"channelOrder": "Priority first, then A–Z",
|
||||
"channelRemove": "Remove channel: {{name}}"
|
||||
"noChannelMatch": "No channel matches."
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
{
|
||||
"save": "Save current view",
|
||||
"namePlaceholder": "View name",
|
||||
"saveAction": "Save",
|
||||
"cancel": "Cancel",
|
||||
"empty": "No saved views yet. Apply some filters, then save them.",
|
||||
"apply": "Apply this view",
|
||||
"makeDefault": "Set as default (loads on start)",
|
||||
"isDefault": "Default view — loads on start",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Delete the saved view “{{name}}”?",
|
||||
|
||||
@@ -72,11 +72,6 @@
|
||||
},
|
||||
"loading": "Csatornák betöltése…",
|
||||
"empty": "Nincsenek csatornák.",
|
||||
"pager": {
|
||||
"prev": "Előző",
|
||||
"next": "Következő",
|
||||
"page": "{{page}}. oldal / {{total}}"
|
||||
},
|
||||
"cols": {
|
||||
"priority": "Prio",
|
||||
"channel": "Csatorna",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"undo": "Visszavonás",
|
||||
"redo": "Újra",
|
||||
"loading": "Betöltés…",
|
||||
"language": "Nyelv",
|
||||
"somethingWrong": "Hiba történt.",
|
||||
"itemCount_one": "{{formattedCount}} elem",
|
||||
"itemCount_other": "{{formattedCount}} elem",
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
"willReset": "mentéskor visszaáll alapértékre",
|
||||
"reset": "Visszaállítás",
|
||||
"resetHint": "Eltávolítja ezt a felülírást, és visszatér a fájl/env alapértékhez.",
|
||||
"resetDone": "Visszaállítva az alapértékre",
|
||||
"usingDefault": "alapérték használatban",
|
||||
"overridden": "felülírva az adatbázisban",
|
||||
"secretSet": "•••••••• tárolva",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"pager": {
|
||||
"prev": "Előző",
|
||||
"next": "Következő",
|
||||
"page": "{{page}}. oldal / {{total}}",
|
||||
"pageLabel": "Oldal",
|
||||
"ofTotal": "/ {{total}}"
|
||||
}
|
||||
|
||||
@@ -112,9 +112,6 @@
|
||||
},
|
||||
"share": {
|
||||
"title": "Letöltés megosztása",
|
||||
"label": "Címzett e-mail címe",
|
||||
"placeholder": "user@example.com",
|
||||
"submit": "Megosztás",
|
||||
"done": "Megosztva vele: {{email}}",
|
||||
"failed": "A megosztás nem sikerült.",
|
||||
"userSection": "Megosztás felhasználóval",
|
||||
@@ -162,7 +159,6 @@
|
||||
"redownloadAction": "Újraletöltés"
|
||||
},
|
||||
"profiles": {
|
||||
"manage": "Formátumok kezelése",
|
||||
"title": "Letöltési formátumok",
|
||||
"builtin": "Beépített",
|
||||
"yours": "Saját formátumaid",
|
||||
@@ -186,14 +182,11 @@
|
||||
"deleteConfirm": "Törlöd a(z) „{{name}}” formátumot?"
|
||||
},
|
||||
"admin": {
|
||||
"storageTitle": "Tárhely",
|
||||
"readyFiles": "Kész fájlok",
|
||||
"totalSize": "Teljes méret",
|
||||
"cap": "Gyorsítótár-korlát",
|
||||
"noCap": "nincs korlát",
|
||||
"perUser": "Felhasználónkénti tárhelyhasználat",
|
||||
"user": "Felhasználó",
|
||||
"footprint": "Használat",
|
||||
"editQuota": "Kvóta szerkesztése",
|
||||
"quotaTitle": "Letöltési kvóta — {{email}}",
|
||||
"maxBytes": "Tárhelykorlát (GB)",
|
||||
@@ -202,20 +195,9 @@
|
||||
"unlimited": "Korlátlan (tárhelykorlát kikapcsolása)",
|
||||
"reset": "Visszaállítás alapértékre",
|
||||
"save": "Mentés",
|
||||
"custom": "egyedi",
|
||||
"default": "alapértelmezett",
|
||||
"quotaPickPlaceholder": "Kvóta beállítása egy felhasználónak…",
|
||||
"noFootprint": "Még nincs letöltés."
|
||||
},
|
||||
"cols": {
|
||||
"title": "Cím",
|
||||
"channel": "Csatorna",
|
||||
"format": "Formátum",
|
||||
"size": "Méret",
|
||||
"status": "Állapot",
|
||||
"added": "Hozzáadva",
|
||||
"user": "Felhasználó"
|
||||
},
|
||||
"clipBadge": "Klip",
|
||||
"errors": {
|
||||
"quota": {
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
"noMatches": "Egy videó sem felel meg ezeknek a szűrőknek.",
|
||||
"channelScopeHint": "Ez a csatorna nincs a Feededben – lehet, hogy el van rejtve. A videói a közös könyvtárban vannak.",
|
||||
"viewInLibrary": "Megnézés a Könyvtárban",
|
||||
"videoCount_one": "{{formattedCount}} videó",
|
||||
"videoCount_other": "{{formattedCount}} videó",
|
||||
"sortLabel": "Rendezés",
|
||||
"viewLabel": "Nézet",
|
||||
"showLabel": "Megtekintési állapot",
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
{
|
||||
"feed": "Feed",
|
||||
"searchPlaceholder": "Keresés a feliratkozásaid között…",
|
||||
"clearSearch": "Keresés törlése",
|
||||
"searchYoutube": "Keresés a YouTube-on",
|
||||
"searchYoutubeTip": "Élő keresés a YouTube-on erre a kifejezésre (a közös API-kvótát fogyasztja). Entert is nyomhatsz.",
|
||||
"go": "Mehet",
|
||||
"goTip": "Keresés (vagy Enter)",
|
||||
"prevModule": "Előző modul",
|
||||
"nextModule": "Következő modul",
|
||||
"channelManager": "Csatornakezelő",
|
||||
"usageStats": "Használat és statisztika",
|
||||
"scheduler": "Ütemező",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"navLabel": "Értesítések",
|
||||
"title": "Értesítések",
|
||||
"subtitle": "Frissítések a könyvtáradból és a rendszertől.",
|
||||
"tabInbox": "Értesítések",
|
||||
"empty": "Nincs új értesítés.",
|
||||
"markAllRead": "Összes olvasott",
|
||||
"clearAll": "Összes törlése",
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
{
|
||||
"tagline": "Saját üzemeltetésű, szűrhető Feed a YouTube-feliratkozásaidból — rendezd, címkézd, és szűrd ki a zajt. A kezdéshez jelentkezz be Google-fiókkal; a YouTube-hozzáférés külön, általad vezérelt lépés.",
|
||||
"signIn": "Bejelentkezés Google-fiókkal",
|
||||
"approved": "Már jóvá vagy hagyva — csak jelentkezz be a fenti Google-gombbal.",
|
||||
"requested": "Köszönjük — a kérésedet rögzítettük. E-mailben értesítünk, ha jóváhagytuk.",
|
||||
"noAccessYet": "Még nincs hozzáférésed?",
|
||||
"denied": "Ez a Google-fiók nincs jóváhagyva. Lent kérhetsz hozzáférést.",
|
||||
"emailPlaceholder": "te@gmail.com",
|
||||
"sending": "Küldés…",
|
||||
"requestAccess": "Hozzáférés kérése",
|
||||
"submitError": "Nem sikerült elküldeni — ellenőrizd a címet, és próbáld újra."
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"tab": "Üzenetek",
|
||||
"navLabel": "Üzenetek",
|
||||
"popOut": "Megnyitás felugró ablakban",
|
||||
"expand": "Kinyitás",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"primary": "Fő navigáció",
|
||||
"pin": "Oldalsáv rögzítése nyitva",
|
||||
"unpin": "Oldalsáv rögzítésének feloldása",
|
||||
"adminSection": "Admin",
|
||||
"role": {
|
||||
"admin": "Admin",
|
||||
"user": "Felhasználó",
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
{
|
||||
"title": "Értesítések",
|
||||
"clearAll": "Összes törlése",
|
||||
"clear": "Törlés",
|
||||
"empty": "Még nincs értesítés.",
|
||||
"actionNeeded": "Teendő",
|
||||
"dismiss": "Elvetés",
|
||||
"findInFeed": "Keresés a Feedben",
|
||||
"unhide": "Megjelenítés",
|
||||
"unwatch": "Megtekintés visszavonása",
|
||||
"openInManager": "Csatornakezelő",
|
||||
"openOnYouTube": "Megnyitás a YouTube-on",
|
||||
"unhidden": "Megjelenítve: „{{title}}”",
|
||||
"unwatched": "Megtekintés visszavonva: „{{title}}”"
|
||||
"openOnYouTube": "Megnyitás a YouTube-on"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"syncYoutube": "Szinkron YouTube-ról",
|
||||
"syncedToast": "{{count}} lista szinkronizálva a YouTube-ról",
|
||||
"syncFailed": "Nem sikerült a YouTube-szinkron",
|
||||
"ytReadonly": "YouTube-ról szinkronizálva — a módosítások visszaszinkronizálhatók",
|
||||
"noneYet": "Még nincs lejátszási lista",
|
||||
"newPlaylist": "Új lista…",
|
||||
"searchPlaceholder": "Listák keresése…",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"navLabel": "Plex",
|
||||
"backToFeed": "YouTube Feed",
|
||||
"backToLibrary": "Vissza a könyvtárhoz",
|
||||
"collection": {
|
||||
"playAll": "Összes lejátszása",
|
||||
@@ -113,8 +112,6 @@
|
||||
"transcode_sw": "Átkódolás · SW"
|
||||
}
|
||||
},
|
||||
"count": "{{count}} cím",
|
||||
"searchCount": "{{count}} találat",
|
||||
"noMatches": "Nincs találat.",
|
||||
"noMatchesFiltered_one": "Nincs találat — egy szűrő is szűkíti az eredményt.",
|
||||
"noMatchesFiltered_other": "Nincs találat — {{count}} szűrő is szűkíti az eredményt.",
|
||||
@@ -303,10 +300,8 @@
|
||||
},
|
||||
"playlist": {
|
||||
"section": "Lejátszási listák",
|
||||
"newPlaceholder": "Új lista…",
|
||||
"pickerPlaceholder": "Lista keresése vagy létrehozása…",
|
||||
"createNamed": "„{{name}}” létrehozása",
|
||||
"create": "Lista létrehozása",
|
||||
"none": "Még nincs lista.",
|
||||
"deleteConfirm": "Törlöd a(z) „{{title}}” listát?",
|
||||
"rename": "Átnevezés",
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
"customize": "Oldalsáv testreszabása",
|
||||
"done": "Kész",
|
||||
"editHint": "Húzd az átrendezéshez · szem a megjelenítéshez/elrejtéshez",
|
||||
"channel": "Csatorna",
|
||||
"loading": "Betöltés…",
|
||||
"channelCount": "{{count}} csatorna",
|
||||
"channelCount_other": "{{count}} csatorna",
|
||||
"dragToReorder": "Húzd az átrendezéshez",
|
||||
"showWidget": "Modul megjelenítése",
|
||||
"hideWidget": "Modul elrejtése",
|
||||
@@ -19,8 +15,6 @@
|
||||
"unpin": "Panel rögzítésének feloldása",
|
||||
"any": "Bármelyik",
|
||||
"all": "Összes",
|
||||
"tagModeTooltip": "Bármelyik vagy az összes kijelölt címke illeszkedjen",
|
||||
"custom": "Egyéni",
|
||||
"from": "Ettől",
|
||||
"to": "Eddig",
|
||||
"clearDates": "dátumok törlése",
|
||||
@@ -56,17 +50,6 @@
|
||||
"saved": "Mentett",
|
||||
"hidden": "Rejtett"
|
||||
},
|
||||
"sort": {
|
||||
"newest": "Legújabb",
|
||||
"oldest": "Legrégebbi",
|
||||
"views": "Legnézettebb",
|
||||
"duration_desc": "Leghosszabb",
|
||||
"duration_asc": "Legrövidebb",
|
||||
"title": "Név (A–Z)",
|
||||
"subscribers": "Csatorna feliratkozói",
|
||||
"priority": "Csatorna prioritás",
|
||||
"shuffle": "Lepj meg"
|
||||
},
|
||||
"content": {
|
||||
"normal": "Normál",
|
||||
"shorts": "Shorts",
|
||||
@@ -91,7 +74,5 @@
|
||||
"untilShort": "{{date}}-ig"
|
||||
},
|
||||
"channelSearch": "Csatornák keresése…",
|
||||
"noChannelMatch": "Nincs találat.",
|
||||
"channelOrder": "Prioritás szerint, majd A–Z",
|
||||
"channelRemove": "Csatorna eltávolítása: {{name}}"
|
||||
"noChannelMatch": "Nincs találat."
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
{
|
||||
"save": "Aktuális nézet mentése",
|
||||
"namePlaceholder": "Nézet neve",
|
||||
"saveAction": "Mentés",
|
||||
"cancel": "Mégse",
|
||||
"empty": "Még nincs mentett nézet. Állíts be szűrőket, majd mentsd el.",
|
||||
"apply": "Nézet alkalmazása",
|
||||
"makeDefault": "Beállítás alapértelmezettként (indításkor betölt)",
|
||||
"isDefault": "Alapértelmezett nézet — indításkor betölt",
|
||||
"rename": "Átnevezés",
|
||||
"delete": "Törlés",
|
||||
"deleteConfirm": "Törlöd a(z) „{{name}}” mentett nézetet?",
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// Every locale key is used by the app. The parity test proves EN and HU agree; nothing proved either
|
||||
// one is still READ — 75 keys had outlived the code that showed them (a whole sidebar sort vocabulary,
|
||||
// the old login page, a pager the DataTable replaced), each a string a translator would keep in step
|
||||
// for nobody, and each one making "is this key live?" a grep instead of a fact.
|
||||
//
|
||||
// A key counts as used when the source (tests excluded — a test naming a key does not show it to
|
||||
// anyone) holds it as a whole string literal, or holds a string that ends in `.` right before an
|
||||
// interpolation or a concatenation — a DYNAMIC prefix like `plex.sort.${k}` — which then covers every
|
||||
// key under it. Plural forms count through their base key. A key read in a way this cannot see goes
|
||||
// in ALLOWED with the reason, rather than the check being loosened for everyone.
|
||||
const ALLOWED: Record<string, string> = {};
|
||||
|
||||
const SRC = fileURLToPath(new URL("../", import.meta.url));
|
||||
|
||||
function flatten(obj: Record<string, unknown>, prefix: string, out: string[]): string[] {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
const key = `${prefix}.${k}`;
|
||||
if (v && typeof v === "object" && !Array.isArray(v))
|
||||
flatten(v as Record<string, unknown>, key, out);
|
||||
else out.push(key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function sources(dir: string, out: string[] = []): string[] {
|
||||
for (const f of readdirSync(dir)) {
|
||||
const p = join(dir, f);
|
||||
if (statSync(p).isDirectory()) sources(p, out);
|
||||
else if (/\.(ts|tsx)$/.test(f) && !/\.test\./.test(f)) out.push(readFileSync(p, "utf-8"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const locales = join(SRC, "i18n/locales/en");
|
||||
const keys = readdirSync(locales).flatMap((f) =>
|
||||
flatten(JSON.parse(readFileSync(join(locales, f), "utf-8")), f.replace(/\.json$/, ""), []),
|
||||
);
|
||||
const code = sources(SRC).join("\n");
|
||||
const literals = new Set([...code.matchAll(/["'`]([\w.-]+)["'`]/g)].map((m) => m[1]!));
|
||||
const prefixes = [...code.matchAll(/["'`]([\w.]+\.)(?:\$\{|["'`]\s*\+)/g)].map((m) => m[1]!);
|
||||
const base = (k: string) => k.replace(/_(zero|one|two|few|many|other)$/, "");
|
||||
const used = (k: string) =>
|
||||
literals.has(k) || literals.has(base(k)) || prefixes.some((p) => k.startsWith(p));
|
||||
|
||||
describe("i18n keys", () => {
|
||||
it("finds keys and code to check them against", () => {
|
||||
expect(keys.length).toBeGreaterThan(1000);
|
||||
expect(literals.has("home.hero.reroll")).toBe(true);
|
||||
expect(prefixes).toContain("plex.sort.");
|
||||
});
|
||||
|
||||
it("are all read somewhere in the app", () => {
|
||||
expect(keys.filter((k) => !used(k) && !(k in ALLOWED))).toEqual([]);
|
||||
});
|
||||
|
||||
it("allow only keys that exist and the scan really cannot see", () => {
|
||||
for (const k of Object.keys(ALLOWED)) {
|
||||
expect({ k, exists: keys.includes(k), scanSees: used(k) }).toEqual({
|
||||
k,
|
||||
exists: true,
|
||||
scanSees: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user