feat(design): the Copy rule — names, verbs, dates and units (E28 S3g)
The owner's picks (all the recommended options) become DESIGN.md's Copy chapter and a sweep of both languages (742 values, no key drift): - One name per feature: the rail's label, which may shorten it (Alerts -> Notifications, Fiókok -> Felhasználók, Download Center -> Downloads, Channel manager -> Channels); one "sync from YouTube" verb. - Four verbs, four meanings (Delete / Remove / Dismiss / Discard, and their Hungarian pairs), checked against what each action does. - Sentence case, the app's places capitalised; "…" on a command that asks first (a popover or form title got its own key without it); one-sentence messages without a stop or a tick, "Couldn't …"; curly quotes. - Dates only from lib/format.ts, in the UI language: formatDateTime names the month and stops at the minute (it printed "9/27/2026, 8:30:14 AM"), formatDateTimeSec for the log, the audit and the recorder; ui/RelativeTime pairs "3d ago" with the exact minute on hover. local/date-format refuses toLocale*String and Intl date formatters elsewhere. - Units from Intl, short (formatUnit): runtimes, totals, the Scheduler countdown, the stats range, the player's tap skip. "2h 13m" was English in both languages. - src/i18n/copyRule.test.ts holds the machine-readable part (quotes, "...", ticks, bare unit letters).
This commit is contained in:
+102
-78
@@ -497,11 +497,34 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen #
|
||||
placed with `ModalFooter` from any depth of the body. A section's own buttons (a sub-form inside a
|
||||
manager, a list row's Delete) stay in the body; a manager with no commit row has no footer.
|
||||
|
||||
## Copy
|
||||
## Copy — _Decided_ (S3g)
|
||||
|
||||
- Every user-facing string is in both Hungarian and English with zero key drift, and a user-facing error is
|
||||
a structured `{code, reason}` translated on the client.
|
||||
- A label says what happens ("Delete playlist"), not "OK". A destructive confirm names the object.
|
||||
a structured `{code, reason}` translated on the client. Hungarian speaks to the user as "te".
|
||||
- **One name per feature.** The nav rail's label is the feature's name; page titles, menus, shortcuts,
|
||||
settings tabs and onboarding use the same words. The rail may shorten them ("Config" for
|
||||
"Configuration", "Listák" for "Lejátszási listák"), never swap in another word. [Nielsen #4]
|
||||
- **One verb per meaning.** Delete / Törlés: gone for good. Remove / Eltávolítás: out of a list, the
|
||||
thing itself survives. Dismiss / Elrejtés: a notice leaves the view. Discard / Elvetés: unsaved edits
|
||||
are thrown away. A confirm uses its action's verb. [Nielsen #4]
|
||||
- **Sentence case.** Capitals only for brand names (YouTube, Plex, Google) and the app's places used as
|
||||
places ("Hide from Feed", "Megnézés a Könyvtárban"). [Material, HIG]
|
||||
- **"…" when the command asks first.** A label ends in "…" when it opens a dialog, form or picker before
|
||||
anything happens ("Edit tags…"); not on a submenu (its chevron says it) or an immediate action. Progress
|
||||
text ("Loading…") keeps its own. [HIG, Windows]
|
||||
- **Messages.** A toast or error of one sentence has no full stop and no "✓": "Couldn't sync
|
||||
subscriptions" / "Nem sikerült szinkronizálni". Two sentences keep their stops. Help text is prose.
|
||||
- **Quotes** around a name are curly: “abc” in English, „abc” in Hungarian.
|
||||
- **Dates**, in the UI language, only from `lib/format.ts` (lint: `local/date-format`): recency in a list
|
||||
is relative ("3d ago" / "3 napja") through `ui/RelativeTime`, which carries the exact minute on hover;
|
||||
a date is "Sep 27, 2026" / "2026. szept. 27."; a moment "Sep 27, 2026, 08:30 AM" /
|
||||
"2026. szept. 27. 08:30"; seconds only in the server log, the audit and the recorder.
|
||||
- **Units.** A figure with a unit (a length, a countdown, a range of days) is the browser's short form
|
||||
(`Intl`), through `formatUnit` / `formatRuntime`: "90 min", "2 hr 13 min" / "90 p", "2 ó 13 p"; a fixed
|
||||
label holding one is written in that same form ("Under 90 min"). Never a bare letter ("90m", "24h",
|
||||
"7d"; test-held). In a sentence, and in a list of time spans named in words ("24 hours", "1 week"), the
|
||||
unit is a word. A clock-style length on a picture stays "2:34:11".
|
||||
- A label says what happens ("Delete playlist"), not "OK"; a destructive confirm names the object.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
@@ -514,78 +537,79 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen #
|
||||
|
||||
## Decision log
|
||||
|
||||
| Date | Decision | Chosen | Over | Why |
|
||||
| ---------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 2026-09-24 | Floating button | Back to top: a round 48 glass FAB; "round" = floats over something (media or the page) | A Material 3 rounded square; a labelled pill; the old 64 circle | The common web convention for back to top; round tells it apart from every toolbar button; 64 was oversized |
|
||||
| 2026-09-24 | Device contract | Tablet portrait + landscape and up; phones out | A 1024px floor with a "too small" message | Portrait tablet use is real, and the app already renders at 820 portrait |
|
||||
| 2026-09-24 | Card actions on touch | Fewer visible, the rest in a "…" menu | 44px buttons that no longer fit the card | Six 44px targets need more width than a card has |
|
||||
| 2026-09-24 | Action placement | One menu with several triggers, never gesture-only, a desktop background menu that mirrors the toolbar | Visible buttons for everything | Fewer non-critical buttons on screen, without hiding any function |
|
||||
| 2026-09-24 | Text selection | Text stays selectable; a click that ends a selection does nothing | Click-to-play on titles | The user must be able to copy any text the app shows |
|
||||
| 2026-09-24 | A1 Button shape | r8 + a rung per context; glass is a Button variant | r12; a pill | Nested corners (r8 in r12 in r16); the pill already means "a filter or a choice" |
|
||||
| 2026-09-24 | A2 Icon buttons | Two sizes, r8; a round button only on media | One size; all round | A lone toolbar icon at the row size looks lost; round rows read as tags |
|
||||
| 2026-09-24 | A3 Touch size | 44 for every control primitive | 36 + gaps; Button only | One rule (HIG 44) instead of two sizes nobody remembers |
|
||||
| 2026-09-24 | A4 Input | Bordered, a 55% fg-into-bg edge (≥ 3:1) | Filled; today's look as a primitive | The only option that clears WCAG 1.4.11 without a heavy fill on glass |
|
||||
| 2026-09-24 | A5 Dropdown trigger | Pill in a toolbar, r8 in a form | Pill everywhere; r8 everywhere | Matches the neighbours in each context |
|
||||
| 2026-09-24 | A6 Checkbox, switch, slider | Native, accent-tinted, the row is the target | Drawn primitives | Free keyboard / screen-reader / forced-colours support; only 3 sites |
|
||||
| 2026-09-24 | B1 Density | Compact 32 / 28 / 24 by explicit height | Standard 38 / 32 / 26 | The owner's read of the lab; touch is covered separately (A3), and one toolbar row lines up at 28 |
|
||||
| 2026-09-24 | B2 Scheme set | Midnight, Forest→cyan, Slate→fuchsia, Matrix→lime | Today's six | The accent collided with danger (YouTube), warn (Slate) and success (Forest, Matrix); Midnight and Starship were one scheme twice; 4 of 6 light accents failed 4.5:1 |
|
||||
| 2026-09-24 | B3 State layers | Text colour at 10% / 16% over anything | Material's 8 / 12%; today's opacity + bg-card | Visible in every scheme and mode (≥ 1.2:1); today's row hover was 1.00:1 on a card |
|
||||
| 2026-09-24 | B4 Page chrome | One sticky band + fade; filters fold when short | A floating glass bar; everything scrolls | Tabs and filters always in reach, in one place; a keyboard-up tablet keeps its content |
|
||||
| 2026-09-24 | B5 Surfaces | One class + radius per role | Today's per-site mix | Nested corners and no per-site alpha drift |
|
||||
| 2026-09-24 | B6 /watch | The app's surfaces, native video controls | Flat and minimal | The page strangers see should look like the product; native controls work on any device |
|
||||
| 2026-09-24 | B7 Nav rail | Soft tint + edge bar, optional labels (a user setting), slim | Solid accent (recommended); a Material rail | The owner's combination: calmer than a solid fill, labels for who wants them, no width cost |
|
||||
| 2026-09-24 | B8 Loading | Skeleton by shape, spinner in a control | A spinner everywhere | Nothing jumps when data lands; a long wait doesn't look stuck |
|
||||
| 2026-09-24 | B9 Fade | Toolbars / headers, panel tabs, dialog edges, chip rows | Thumbnails; buttons | The north-star depth where content scrolls under chrome; not on content or small controls |
|
||||
| 2026-09-24 | C1 Action classification | The table (lab C1): state + primary at rest, everyday toggles on hover, "…" on touch, destructive into the menu | Visible buttons per site, as today | One action list per object, nothing gesture-only, fewer buttons without losing a function |
|
||||
| 2026-09-24 | C2 Side panels | A tap opens an overlay drawer; pin is a separate button ≥ 1024 | A tap pins and the content reflows | Below 768 the filters were unreachable; on touch a tap pinned and pushed the grid |
|
||||
| 2026-09-24 | C3 Players on touch | Tap = show the controls; 44px seek hit area; 44 buttons; the YT description behind ⓘ | Today (tap also pauses; 7.5px seek bar) | Measured live: you couldn't reach the seek bar without pausing, and the hover-only description never opened on a tap |
|
||||
| 2026-09-24 | C4 Dialogs | Focus by purpose; a pinned footer | Focus on Close; the footer scrolls | A form starts in its first field, and Save stays on screen with the keyboard up |
|
||||
| 2026-09-24 | C5 Plex player controls | The owner's lean set: no Stop / Restart / ±10 s buttons, Back by position, Next only with a next item, Resume + From the start on the card, touch double-tap ±10 s | Every control in one row (it ran off-screen below 1024); Back by a press counter; ±10 s keyboard-only | The row fits a portrait tablet; the decision to restart is made on the card; a tablet keeps the 10 s skip |
|
||||
| 2026-09-24 | Glass tuner | Retired; the schemes are the only colour choice | Keep it with ranges clamped to the rules | Free tuning is the drift the constitution exists to stop (owner) |
|
||||
| 2026-09-24 | Matrix toned down | Moss #86a05a at Midnight's lightness, near-neutral greys; every white-on-accent part → `--accent-fg` | The B2 lime #a3e635 | The owner found the lime neon and tiring; it was the loudest accent (L 0.85, C 0.21), and fixed white parts sat on it at 1.5:1 |
|
||||
| 2026-09-24 | Field primitives | `Input` / `Textarea` / `InnerInput` + `fieldClass`, `SelectTrigger`, the native controls; the lint rule "no raw `<input>`" | `inputCls` and ~40 hand-styled boxes | Six edges, four radii and two focus looks for one control; the ≥ 3:1 edge held nowhere |
|
||||
| 2026-09-25 | Surface roles | `SURFACE` roles in `ui/surfaces.ts`; the lint rule "no bare glass class"; menus p-1 with r8 rows | Three tiers paired ad hoc with five radii and three menu paddings | ~140 sites drifted; nested corners only hold if the radius comes with the role |
|
||||
| 2026-09-25 | Static labels | Flat tonal `Badge`, no edge; a filtering tag is an edged chip | A glass pill with an edge, like the chips | Edged = pressable at a glance; a static tag that looks like a chip gets clicked and does nothing |
|
||||
| 2026-09-25 | Header search | A 40 glass pill with the field edge (`lg`); what sits inside it is round | 44, r16, a glass box with a faint edge | The one rounded box among pills, and its edge under 3:1; the hero search is the YouTube/Gmail pill |
|
||||
| 2026-09-25 | Switch row (F12) | The whole settings row toggles (`SwitchRow`; a hand-laid row points a label at the switch) | Only the 36×24 switch toggles | The iOS / Material settings row; the text was a dead click beside the control it names |
|
||||
| 2026-09-25 | Edges and hovers (F14) | The ≥ 3:1 edge on the field family (fields, the form trigger); labelled pills keep the faint edge and the accent-edge hover | The ≥ 3:1 edge and a state-layer hover on every control | 1.4.11 asks an edge only where nothing else identifies the control; a heavy edge on every chip and tab weighs the whole UI down |
|
||||
| 2026-09-25 | Rating and live (F16) | Own tokens: `--rating` (theme-scoped gold), `--live`/`--live-fg`, `--rating-media`, `--media-chrome`; palette classes lint-refused | Map them to `--warn` / `--danger` | A gold star and a red LIVE are what the eye expects; as warn/danger they would read as a status. The old amber score was 2:1 on light |
|
||||
| 2026-09-25 | Fjord trial (B2 exception) | A fifth scheme, temporary: Midnight's neutrals, Playnite's steel accent (#337599 lifted to #5aa1cb) and steel rules; exit = fold into Midnight or remove | Retune Midnight straight away; borrow only the qualities | The owner wants to live with the calmer palette app-wide before it replaces the default; it breaks "own hue family" on purpose, and only until that call |
|
||||
| 2026-09-25 | Whole-card row (owner) | A card row's B3 layer is the card's `::before` under its content; the open button's hit layer spans the card, the actions sit above it | The name button's own hover, ending at the first badge or button | A row that half-answers the pointer reads as broken; a badge click did nothing |
|
||||
| 2026-09-25 | Badge tones (F29) | `Badge tone` neutral / accent / warn at 6%; light `--warn` #b45309 → #a14a07; edged label spans lint-refused | Keep edged status pills (a private Badge in Users, Settings "Granted", the suggestions count) | Edged = pressable at a glance; amber-700 was 4.58:1 as text and fell under 4.5 on its own tint |
|
||||
| 2026-09-26 | Page band built (B4) | Controls only in the band (intros scroll); the fold shows the same row in place; one `h1` per page (`PageHeading` steps the hidden one aside); `SectionLabel` headings; Skip to content | A popover of the folded filters; the page title kept visible | A folded row is still the same controls, nothing duplicated; the rail and the tab title already name the page |
|
||||
| 2026-09-26 | Fade built (B9) | `fade-below` on sticky toolbars and table headers; the scroller mask where the band sits on the backdrop | A painted band over the backdrop | The backdrop is the colour under the band, so fading the content into it is the same edge |
|
||||
| 2026-09-26 | Viewport units | `dvh` everywhere (the Plex player stage with S3f) | `vh` / `h-screen` | `100vh` hides the last rows under a tablet browser's toolbar |
|
||||
| 2026-09-26 | Video actions built (C1) | One list per video (menu, right-click, long-press, "…"); the card keeps watched / save / add to playlist on its picture on hover; download, details, copy, hide, reset in the menu | The six-button row under the card and the hover ⓘ | The menu is the one door every device has; the card loses a line of height and the touch card its row of 28px buttons |
|
||||
| 2026-09-26 | Plex actions built (C1) | One list per Plex title (play/open, watched, add to playlist, media info, copy title, pin); the poster keeps watched / add / "…" on its picture on hover; seasons get open / watched / add / copy | The hover ⓘ, the pin-only menu, an episode's add button at rest, a collection's "Remove" X on the card | The same door as a video's; the collection grid goes to two columns on a phone so the touch "…" leaves the title its line |
|
||||
| 2026-09-26 | Playlist actions built (C1) | A playlist row's "…" (play, pin, copy title / link, move, order, remove), Delete + an undo toast; one list per playlist (rename, pin, YouTube open / push / revert, delete) from its rail row and its header's "…" | The row's X column and its confirm; the header's pencil, YouTube link, push, revert and trash buttons | Remove is reversible, so it needs no confirm; the header keeps Play all; a narrow table drops "#" and Channel so the title keeps a width |
|
||||
| 2026-09-26 | Channel + Plex list actions (C1) | A channel row's "…" (open, pin, YouTube, copy, full history, hide + undo, unsubscribe); a Plex playlist's title / show / season "…", Delete + undo; its header's "…" (rename, delete); an action toast lasts ≥ 8 s | The channel row's full-history, hide and unsubscribe buttons; the Plex list's red X on every row, show and season; the header's pencil and trash | Hiding and removing are reversible, so they need no confirm and the undo needs time; unsubscribe and deleting a playlist keep theirs |
|
||||
| 2026-09-26 | Download / notification / Scheduler actions (C1) | A download row's "…" (play, share, save, edit, details, re-download, manage sharing, copies, cancel / delete / remove); a library row's Share stays in sight, ahead of save; a notification row's "…" and hover dismiss; the inbox band's and the Scheduler strip's "…" | The download row's five rare buttons and its cancel / delete / remove; the source link's copy button; an activity notice's Find / Undo links; the band's Clear everything; the strip's Purge discovery | The destructive rows keep their confirm, and Clear everything gains one; a row link keeps the browser's own menu; Find in feed drops the tag / search / age filters that could hide its video |
|
||||
| 2026-09-26 | Channel menu: Tags + Priority (C1) | Tags A–Z as in-place checks; Priority as fixed steps 10 · 5 · 3 · 2 · 1 · 0, the current one ticked; a pick re-sorts at once and the page + focus follow the row; one drawn `MenuCheck` for every checkable row that cannot be an `<input>` | Relative steps (+1 / +5); straight to the top; the stepper only; seven hand-drawn check marks (three box sizes, a tinted box, a × for "on", a trailing tick) | Bubbling a channel up is one pick; fixed levels keep the scale the reader already uses; the list moving where the reader can see it is the feedback |
|
||||
| 2026-09-26 | Reorder without a drag (WCAG 2.5.7) | Row menus: Top / Up / Down / Bottom (YouTube + Plex playlists); saved views: four buttons with the place beside the selected view (owner: A); side panels in edit mode: an Up / Down pair per group (owner: A), the name wrapping there | A "…" on every saved-view row; a "…" on every panel group; drag only | A visible, one-click path where the list is edited; the edge moves shown off so the buttons never shift |
|
||||
| 2026-09-26 | Titles in click surfaces | The surface stays a button, flagged `selectable`: its text selects, and the click that ends a selection does nothing; every plain `Pressable` pinned with its reason | Rebuilding each card and row so the title sits outside the button; exceptions for master-detail lists | Chrome selects inside a button once it is `user-select: text` (measured), so one flag on the primitive keeps the keyboard, focus and list walking every surface already has |
|
||||
| 2026-09-26 | Right-click on a selection | The browser's own menu opens on selected words; beside them the object's menu | A "Copy selection" row in the app menu | The reader selected the words to do something with them; the native menu copies, searches and translates, and changes nothing else |
|
||||
| 2026-09-26 | Keyboard ring on a flush row | Inside the row (a table row by its tag, a log line, a show header in its clipping card, with the card's corners) | The ring outside, as on a card | Outside it ran under the table head and past the table, and a clipping card cut it away entirely (invisible focus) |
|
||||
| 2026-09-26 | Fade at rest | The sticky header's fade appears with the first 1rem of scroll (a scroll-driven animation; no fade where unsupported) | A fade that is always drawn | At rest it dimmed the first row and hid its ring's top edge; nothing is under the header yet |
|
||||
| 2026-09-26 | Narrow playlist table | "#" and Channel hide by the table's own width (under 42rem), not the window's | Hiding below the `sm` window width | Beside the playlist rail a desktop window left the table 432 px and the title 0 px |
|
||||
| 2026-09-26 | Background menu built | Five list pages (feed, Plex grid, channels, playlist, notifications); the toolbar's sort / view / filters and the band's actions; no destructive row | Every page with a toolbar; the "…" menus copied whole, delete included | The tab-only pages have nothing a menu would add; a blank-area right-click must not be one click from delete |
|
||||
| 2026-09-26 | Media card anatomy (C) | One frame; the meta line pinned to its bottom; the length a badge on the picture; the Plex card on the same primitive as the video card | A: the year as an overline over the title; B: the year inline after it; a fixed two-line title slot | A one-line title left a hole the eye reads as a missing line; C puts nothing under a variable-height title |
|
||||
| 2026-09-26 | One Play | `MediaPlayGlyph` (the media button variant) and only where the click plays | The Plex card's hand-painted accent circle; a playable dot on every card | One Play for every card; a dot that only a colour and a tooltip explain says nothing |
|
||||
| 2026-09-26 | Filtering values on the info page | A value that filters (year, certificate, studio, director) is an edged chip; a fact is flat text | Everything flat, the clickable ones only hover-tinted | The S3d rule: edged = pressable, so the reader sees which values do something |
|
||||
| 2026-09-26 | Shelf density | On any strip: one title line over one meta line | The grid's two lines and full meta on a strip | The tallest card set the strip's height and left holes in the cards with less to say |
|
||||
| 2026-09-26 | Compact list row (B) | The title leads; a faint "…" at the end; the toggles float over the date on hover | A leading 128px action column | That column sat empty at rest, a hole at the start of every row |
|
||||
| 2026-09-26 | Media-info layout (B) | A main column (collections first, crew, cast) beside a facts sidebar; an empty column is not drawn | Every section full width, stacked | The collection is the most used; the keywords' column left a void beside a short details box |
|
||||
| 2026-09-26 | Mixed Home shelf (A) | Plex titles as wide art beside videos | Posters, with the videos not stretched | Stretching a video card to a poster's height left an ~80px gap |
|
||||
| 2026-09-27 | Home hero | A card (hover row + Play on the picture, the whole menu, a touch "…"; owner: A); three quarters wide and centred on a wide window; one fact line; the summary at the measure | The hero's pin row alone; full width; an 18rem picture column | The one answer stands in focus, and it offers what the same card offers anywhere |
|
||||
| 2026-09-27 | Home shelf tiles | A channel / playlist tile: Play, ↗ open, "…" with Open; a show acts on its next episode, ⓘ opens it; the shelf slot 232px | The tile's pin row alone; 200px | A tile without its object's actions read as broken; at 200 the Play touched the hover row |
|
||||
| 2026-09-27 | Synopsis preview (C) | A ~0.9 s mouse rest opens a passive panel beside the card | The synopsis as the picture's `title` tooltip; an ⓘ to hover; none | One long tooltip line fired anywhere on the art and clashed with the cut title's own tooltip |
|
||||
| 2026-09-27 | Reading measure | Prose capped at ~65 characters a line (`measure`) | The container's full width | At 110–130 characters a line the eye loses the next line's start |
|
||||
| 2026-09-24 | Chip family | One `Chip` (filter / token / tag / suggest / ghost) + `DismissibleChip`; nothing under 24; the lint rule "no raw `<button>` outside ui/" | Five chip geometries in five files | The applied-filter row, the tag cells, the year picker and the saved-view editor each drew their own chip; a one-letter tag was 21px wide |
|
||||
| 2026-09-24 | Button families | `Button` / `IconButton` / `Pressable` / chips / `DragHandle`; `lg` 40 for a hero CTA; pills and a 48 Play on media (from C5) | Every button hand-painted | 268 hand-rolled buttons in 79 files drifted into as many looks; the ladder only holds if every button stands on it |
|
||||
| 2026-09-27 | C4 as built | `initialFocus` required on every `Modal`; the commit row in `ModalFooter`, portalled under the body from any depth; the fades driven by the body's scroll timeline; `interactive-widget=resizes-content` | An optional prop defaulting to the ✕; a `footer` prop (the issue composer's buttons live three components down); always-on fades | A default is how 21 dialogs came to open on the ✕; lifting the composer's state to reach a prop is the larger change; a resting fade dimmed the first line |
|
||||
| 2026-09-27 | C3 + C5 as built | A tap toggles the bar through `pointerType` from `pointerdown`; a double-tap takes back its first tap's toggle; the card's pair is the video card's `MediaResumeButtons` (Continue / Restart), a sibling over the picture; the YT description is an ⓘ click popover | A delayed single tap (wait out the double-tap window); new "Resume / From the start" words for Plex; the pair inside the picture's button | A delayed tap feels dead; one media card says one pair of words; a button inside a button is invalid and the hover row already sits beside the picture |
|
||||
| 2026-09-27 | C2 as built | The side panel's drawer is `ui/Modal` `drawer-start`; drawer = a coarse pointer or below 768; the pin offered in its header ≥ 1024 | A drawer of its own inside `SidePanel` | Esc, Back, the scrim, the trap and the scroll lock are the dialog's; a second implementation is how two shells drift |
|
||||
| 2026-09-27 | Narrow rows by container | The download row, the playlist table and the Channels tag row stack by a container query; a fact line never wraps | Breakpoints on the window | The iPad zoomed in, split screen and a column beside a rail are narrow while the window is not; the zoomed download row broke "18.9 / MB" apart |
|
||||
| 2026-09-27 | Phones deferred | A phone-only layout (the hamburger nav, a folded feed band) waits for its own epic; E28 stays desktop + tablet | Fix the phone views inside E28 | Principle 2; the first phone attempts made the feed worse, and the epic's review grows with every sprint |
|
||||
| 2026-09-27 | Conformance check (S3g) | A four-step check before a UI change is done: primitive at a named rung, the chapter re-read, the rendered page measured by kind and look, every state seen | A reviewer's eye per sprint | The S1 probe found 69 button looks; after S3 every control kind is drawn by its primitive, and the check keeps a new look from arriving unnoticed |
|
||||
| 2026-09-27 | Segmented rung | `SegmentedToggle` defaults to the toolbar rung (28, `text-xs`); 24 only for a panel's captioned match toggle | The small default | Two toolbars had inherited 10px type from the default beside 28px buttons |
|
||||
| Date | Decision | Chosen | Over | Why |
|
||||
| ---------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 2026-09-24 | Floating button | Back to top: a round 48 glass FAB; "round" = floats over something (media or the page) | A Material 3 rounded square; a labelled pill; the old 64 circle | The common web convention for back to top; round tells it apart from every toolbar button; 64 was oversized |
|
||||
| 2026-09-24 | Device contract | Tablet portrait + landscape and up; phones out | A 1024px floor with a "too small" message | Portrait tablet use is real, and the app already renders at 820 portrait |
|
||||
| 2026-09-24 | Card actions on touch | Fewer visible, the rest in a "…" menu | 44px buttons that no longer fit the card | Six 44px targets need more width than a card has |
|
||||
| 2026-09-24 | Action placement | One menu with several triggers, never gesture-only, a desktop background menu that mirrors the toolbar | Visible buttons for everything | Fewer non-critical buttons on screen, without hiding any function |
|
||||
| 2026-09-24 | Text selection | Text stays selectable; a click that ends a selection does nothing | Click-to-play on titles | The user must be able to copy any text the app shows |
|
||||
| 2026-09-24 | A1 Button shape | r8 + a rung per context; glass is a Button variant | r12; a pill | Nested corners (r8 in r12 in r16); the pill already means "a filter or a choice" |
|
||||
| 2026-09-24 | A2 Icon buttons | Two sizes, r8; a round button only on media | One size; all round | A lone toolbar icon at the row size looks lost; round rows read as tags |
|
||||
| 2026-09-24 | A3 Touch size | 44 for every control primitive | 36 + gaps; Button only | One rule (HIG 44) instead of two sizes nobody remembers |
|
||||
| 2026-09-24 | A4 Input | Bordered, a 55% fg-into-bg edge (≥ 3:1) | Filled; today's look as a primitive | The only option that clears WCAG 1.4.11 without a heavy fill on glass |
|
||||
| 2026-09-24 | A5 Dropdown trigger | Pill in a toolbar, r8 in a form | Pill everywhere; r8 everywhere | Matches the neighbours in each context |
|
||||
| 2026-09-24 | A6 Checkbox, switch, slider | Native, accent-tinted, the row is the target | Drawn primitives | Free keyboard / screen-reader / forced-colours support; only 3 sites |
|
||||
| 2026-09-24 | B1 Density | Compact 32 / 28 / 24 by explicit height | Standard 38 / 32 / 26 | The owner's read of the lab; touch is covered separately (A3), and one toolbar row lines up at 28 |
|
||||
| 2026-09-24 | B2 Scheme set | Midnight, Forest→cyan, Slate→fuchsia, Matrix→lime | Today's six | The accent collided with danger (YouTube), warn (Slate) and success (Forest, Matrix); Midnight and Starship were one scheme twice; 4 of 6 light accents failed 4.5:1 |
|
||||
| 2026-09-24 | B3 State layers | Text colour at 10% / 16% over anything | Material's 8 / 12%; today's opacity + bg-card | Visible in every scheme and mode (≥ 1.2:1); today's row hover was 1.00:1 on a card |
|
||||
| 2026-09-24 | B4 Page chrome | One sticky band + fade; filters fold when short | A floating glass bar; everything scrolls | Tabs and filters always in reach, in one place; a keyboard-up tablet keeps its content |
|
||||
| 2026-09-24 | B5 Surfaces | One class + radius per role | Today's per-site mix | Nested corners and no per-site alpha drift |
|
||||
| 2026-09-24 | B6 /watch | The app's surfaces, native video controls | Flat and minimal | The page strangers see should look like the product; native controls work on any device |
|
||||
| 2026-09-24 | B7 Nav rail | Soft tint + edge bar, optional labels (a user setting), slim | Solid accent (recommended); a Material rail | The owner's combination: calmer than a solid fill, labels for who wants them, no width cost |
|
||||
| 2026-09-24 | B8 Loading | Skeleton by shape, spinner in a control | A spinner everywhere | Nothing jumps when data lands; a long wait doesn't look stuck |
|
||||
| 2026-09-24 | B9 Fade | Toolbars / headers, panel tabs, dialog edges, chip rows | Thumbnails; buttons | The north-star depth where content scrolls under chrome; not on content or small controls |
|
||||
| 2026-09-24 | C1 Action classification | The table (lab C1): state + primary at rest, everyday toggles on hover, "…" on touch, destructive into the menu | Visible buttons per site, as today | One action list per object, nothing gesture-only, fewer buttons without losing a function |
|
||||
| 2026-09-24 | C2 Side panels | A tap opens an overlay drawer; pin is a separate button ≥ 1024 | A tap pins and the content reflows | Below 768 the filters were unreachable; on touch a tap pinned and pushed the grid |
|
||||
| 2026-09-24 | C3 Players on touch | Tap = show the controls; 44px seek hit area; 44 buttons; the YT description behind ⓘ | Today (tap also pauses; 7.5px seek bar) | Measured live: you couldn't reach the seek bar without pausing, and the hover-only description never opened on a tap |
|
||||
| 2026-09-24 | C4 Dialogs | Focus by purpose; a pinned footer | Focus on Close; the footer scrolls | A form starts in its first field, and Save stays on screen with the keyboard up |
|
||||
| 2026-09-24 | C5 Plex player controls | The owner's lean set: no Stop / Restart / ±10 s buttons, Back by position, Next only with a next item, Resume + From the start on the card, touch double-tap ±10 s | Every control in one row (it ran off-screen below 1024); Back by a press counter; ±10 s keyboard-only | The row fits a portrait tablet; the decision to restart is made on the card; a tablet keeps the 10 s skip |
|
||||
| 2026-09-24 | Glass tuner | Retired; the schemes are the only colour choice | Keep it with ranges clamped to the rules | Free tuning is the drift the constitution exists to stop (owner) |
|
||||
| 2026-09-24 | Matrix toned down | Moss #86a05a at Midnight's lightness, near-neutral greys; every white-on-accent part → `--accent-fg` | The B2 lime #a3e635 | The owner found the lime neon and tiring; it was the loudest accent (L 0.85, C 0.21), and fixed white parts sat on it at 1.5:1 |
|
||||
| 2026-09-24 | Field primitives | `Input` / `Textarea` / `InnerInput` + `fieldClass`, `SelectTrigger`, the native controls; the lint rule "no raw `<input>`" | `inputCls` and ~40 hand-styled boxes | Six edges, four radii and two focus looks for one control; the ≥ 3:1 edge held nowhere |
|
||||
| 2026-09-25 | Surface roles | `SURFACE` roles in `ui/surfaces.ts`; the lint rule "no bare glass class"; menus p-1 with r8 rows | Three tiers paired ad hoc with five radii and three menu paddings | ~140 sites drifted; nested corners only hold if the radius comes with the role |
|
||||
| 2026-09-25 | Static labels | Flat tonal `Badge`, no edge; a filtering tag is an edged chip | A glass pill with an edge, like the chips | Edged = pressable at a glance; a static tag that looks like a chip gets clicked and does nothing |
|
||||
| 2026-09-25 | Header search | A 40 glass pill with the field edge (`lg`); what sits inside it is round | 44, r16, a glass box with a faint edge | The one rounded box among pills, and its edge under 3:1; the hero search is the YouTube/Gmail pill |
|
||||
| 2026-09-25 | Switch row (F12) | The whole settings row toggles (`SwitchRow`; a hand-laid row points a label at the switch) | Only the 36×24 switch toggles | The iOS / Material settings row; the text was a dead click beside the control it names |
|
||||
| 2026-09-25 | Edges and hovers (F14) | The ≥ 3:1 edge on the field family (fields, the form trigger); labelled pills keep the faint edge and the accent-edge hover | The ≥ 3:1 edge and a state-layer hover on every control | 1.4.11 asks an edge only where nothing else identifies the control; a heavy edge on every chip and tab weighs the whole UI down |
|
||||
| 2026-09-25 | Rating and live (F16) | Own tokens: `--rating` (theme-scoped gold), `--live`/`--live-fg`, `--rating-media`, `--media-chrome`; palette classes lint-refused | Map them to `--warn` / `--danger` | A gold star and a red LIVE are what the eye expects; as warn/danger they would read as a status. The old amber score was 2:1 on light |
|
||||
| 2026-09-25 | Fjord trial (B2 exception) | A fifth scheme, temporary: Midnight's neutrals, Playnite's steel accent (#337599 lifted to #5aa1cb) and steel rules; exit = fold into Midnight or remove | Retune Midnight straight away; borrow only the qualities | The owner wants to live with the calmer palette app-wide before it replaces the default; it breaks "own hue family" on purpose, and only until that call |
|
||||
| 2026-09-25 | Whole-card row (owner) | A card row's B3 layer is the card's `::before` under its content; the open button's hit layer spans the card, the actions sit above it | The name button's own hover, ending at the first badge or button | A row that half-answers the pointer reads as broken; a badge click did nothing |
|
||||
| 2026-09-25 | Badge tones (F29) | `Badge tone` neutral / accent / warn at 6%; light `--warn` #b45309 → #a14a07; edged label spans lint-refused | Keep edged status pills (a private Badge in Users, Settings "Granted", the suggestions count) | Edged = pressable at a glance; amber-700 was 4.58:1 as text and fell under 4.5 on its own tint |
|
||||
| 2026-09-26 | Page band built (B4) | Controls only in the band (intros scroll); the fold shows the same row in place; one `h1` per page (`PageHeading` steps the hidden one aside); `SectionLabel` headings; Skip to content | A popover of the folded filters; the page title kept visible | A folded row is still the same controls, nothing duplicated; the rail and the tab title already name the page |
|
||||
| 2026-09-26 | Fade built (B9) | `fade-below` on sticky toolbars and table headers; the scroller mask where the band sits on the backdrop | A painted band over the backdrop | The backdrop is the colour under the band, so fading the content into it is the same edge |
|
||||
| 2026-09-26 | Viewport units | `dvh` everywhere (the Plex player stage with S3f) | `vh` / `h-screen` | `100vh` hides the last rows under a tablet browser's toolbar |
|
||||
| 2026-09-26 | Video actions built (C1) | One list per video (menu, right-click, long-press, "…"); the card keeps watched / save / add to playlist on its picture on hover; download, details, copy, hide, reset in the menu | The six-button row under the card and the hover ⓘ | The menu is the one door every device has; the card loses a line of height and the touch card its row of 28px buttons |
|
||||
| 2026-09-26 | Plex actions built (C1) | One list per Plex title (play/open, watched, add to playlist, media info, copy title, pin); the poster keeps watched / add / "…" on its picture on hover; seasons get open / watched / add / copy | The hover ⓘ, the pin-only menu, an episode's add button at rest, a collection's "Remove" X on the card | The same door as a video's; the collection grid goes to two columns on a phone so the touch "…" leaves the title its line |
|
||||
| 2026-09-26 | Playlist actions built (C1) | A playlist row's "…" (play, pin, copy title / link, move, order, remove), Delete + an undo toast; one list per playlist (rename, pin, YouTube open / push / revert, delete) from its rail row and its header's "…" | The row's X column and its confirm; the header's pencil, YouTube link, push, revert and trash buttons | Remove is reversible, so it needs no confirm; the header keeps Play all; a narrow table drops "#" and Channel so the title keeps a width |
|
||||
| 2026-09-26 | Channel + Plex list actions (C1) | A channel row's "…" (open, pin, YouTube, copy, full history, hide + undo, unsubscribe); a Plex playlist's title / show / season "…", Delete + undo; its header's "…" (rename, delete); an action toast lasts ≥ 8 s | The channel row's full-history, hide and unsubscribe buttons; the Plex list's red X on every row, show and season; the header's pencil and trash | Hiding and removing are reversible, so they need no confirm and the undo needs time; unsubscribe and deleting a playlist keep theirs |
|
||||
| 2026-09-26 | Download / notification / Scheduler actions (C1) | A download row's "…" (play, share, save, edit, details, re-download, manage sharing, copies, cancel / delete / remove); a library row's Share stays in sight, ahead of save; a notification row's "…" and hover dismiss; the inbox band's and the Scheduler strip's "…" | The download row's five rare buttons and its cancel / delete / remove; the source link's copy button; an activity notice's Find / Undo links; the band's Clear everything; the strip's Purge discovery | The destructive rows keep their confirm, and Clear everything gains one; a row link keeps the browser's own menu; Find in feed drops the tag / search / age filters that could hide its video |
|
||||
| 2026-09-26 | Channel menu: Tags + Priority (C1) | Tags A–Z as in-place checks; Priority as fixed steps 10 · 5 · 3 · 2 · 1 · 0, the current one ticked; a pick re-sorts at once and the page + focus follow the row; one drawn `MenuCheck` for every checkable row that cannot be an `<input>` | Relative steps (+1 / +5); straight to the top; the stepper only; seven hand-drawn check marks (three box sizes, a tinted box, a × for "on", a trailing tick) | Bubbling a channel up is one pick; fixed levels keep the scale the reader already uses; the list moving where the reader can see it is the feedback |
|
||||
| 2026-09-26 | Reorder without a drag (WCAG 2.5.7) | Row menus: Top / Up / Down / Bottom (YouTube + Plex playlists); saved views: four buttons with the place beside the selected view (owner: A); side panels in edit mode: an Up / Down pair per group (owner: A), the name wrapping there | A "…" on every saved-view row; a "…" on every panel group; drag only | A visible, one-click path where the list is edited; the edge moves shown off so the buttons never shift |
|
||||
| 2026-09-26 | Titles in click surfaces | The surface stays a button, flagged `selectable`: its text selects, and the click that ends a selection does nothing; every plain `Pressable` pinned with its reason | Rebuilding each card and row so the title sits outside the button; exceptions for master-detail lists | Chrome selects inside a button once it is `user-select: text` (measured), so one flag on the primitive keeps the keyboard, focus and list walking every surface already has |
|
||||
| 2026-09-26 | Right-click on a selection | The browser's own menu opens on selected words; beside them the object's menu | A "Copy selection" row in the app menu | The reader selected the words to do something with them; the native menu copies, searches and translates, and changes nothing else |
|
||||
| 2026-09-26 | Keyboard ring on a flush row | Inside the row (a table row by its tag, a log line, a show header in its clipping card, with the card's corners) | The ring outside, as on a card | Outside it ran under the table head and past the table, and a clipping card cut it away entirely (invisible focus) |
|
||||
| 2026-09-26 | Fade at rest | The sticky header's fade appears with the first 1rem of scroll (a scroll-driven animation; no fade where unsupported) | A fade that is always drawn | At rest it dimmed the first row and hid its ring's top edge; nothing is under the header yet |
|
||||
| 2026-09-26 | Narrow playlist table | "#" and Channel hide by the table's own width (under 42rem), not the window's | Hiding below the `sm` window width | Beside the playlist rail a desktop window left the table 432 px and the title 0 px |
|
||||
| 2026-09-26 | Background menu built | Five list pages (feed, Plex grid, channels, playlist, notifications); the toolbar's sort / view / filters and the band's actions; no destructive row | Every page with a toolbar; the "…" menus copied whole, delete included | The tab-only pages have nothing a menu would add; a blank-area right-click must not be one click from delete |
|
||||
| 2026-09-26 | Media card anatomy (C) | One frame; the meta line pinned to its bottom; the length a badge on the picture; the Plex card on the same primitive as the video card | A: the year as an overline over the title; B: the year inline after it; a fixed two-line title slot | A one-line title left a hole the eye reads as a missing line; C puts nothing under a variable-height title |
|
||||
| 2026-09-26 | One Play | `MediaPlayGlyph` (the media button variant) and only where the click plays | The Plex card's hand-painted accent circle; a playable dot on every card | One Play for every card; a dot that only a colour and a tooltip explain says nothing |
|
||||
| 2026-09-26 | Filtering values on the info page | A value that filters (year, certificate, studio, director) is an edged chip; a fact is flat text | Everything flat, the clickable ones only hover-tinted | The S3d rule: edged = pressable, so the reader sees which values do something |
|
||||
| 2026-09-26 | Shelf density | On any strip: one title line over one meta line | The grid's two lines and full meta on a strip | The tallest card set the strip's height and left holes in the cards with less to say |
|
||||
| 2026-09-26 | Compact list row (B) | The title leads; a faint "…" at the end; the toggles float over the date on hover | A leading 128px action column | That column sat empty at rest, a hole at the start of every row |
|
||||
| 2026-09-26 | Media-info layout (B) | A main column (collections first, crew, cast) beside a facts sidebar; an empty column is not drawn | Every section full width, stacked | The collection is the most used; the keywords' column left a void beside a short details box |
|
||||
| 2026-09-26 | Mixed Home shelf (A) | Plex titles as wide art beside videos | Posters, with the videos not stretched | Stretching a video card to a poster's height left an ~80px gap |
|
||||
| 2026-09-27 | Home hero | A card (hover row + Play on the picture, the whole menu, a touch "…"; owner: A); three quarters wide and centred on a wide window; one fact line; the summary at the measure | The hero's pin row alone; full width; an 18rem picture column | The one answer stands in focus, and it offers what the same card offers anywhere |
|
||||
| 2026-09-27 | Home shelf tiles | A channel / playlist tile: Play, ↗ open, "…" with Open; a show acts on its next episode, ⓘ opens it; the shelf slot 232px | The tile's pin row alone; 200px | A tile without its object's actions read as broken; at 200 the Play touched the hover row |
|
||||
| 2026-09-27 | Synopsis preview (C) | A ~0.9 s mouse rest opens a passive panel beside the card | The synopsis as the picture's `title` tooltip; an ⓘ to hover; none | One long tooltip line fired anywhere on the art and clashed with the cut title's own tooltip |
|
||||
| 2026-09-27 | Reading measure | Prose capped at ~65 characters a line (`measure`) | The container's full width | At 110–130 characters a line the eye loses the next line's start |
|
||||
| 2026-09-24 | Chip family | One `Chip` (filter / token / tag / suggest / ghost) + `DismissibleChip`; nothing under 24; the lint rule "no raw `<button>` outside ui/" | Five chip geometries in five files | The applied-filter row, the tag cells, the year picker and the saved-view editor each drew their own chip; a one-letter tag was 21px wide |
|
||||
| 2026-09-24 | Button families | `Button` / `IconButton` / `Pressable` / chips / `DragHandle`; `lg` 40 for a hero CTA; pills and a 48 Play on media (from C5) | Every button hand-painted | 268 hand-rolled buttons in 79 files drifted into as many looks; the ladder only holds if every button stands on it |
|
||||
| 2026-09-27 | C4 as built | `initialFocus` required on every `Modal`; the commit row in `ModalFooter`, portalled under the body from any depth; the fades driven by the body's scroll timeline; `interactive-widget=resizes-content` | An optional prop defaulting to the ✕; a `footer` prop (the issue composer's buttons live three components down); always-on fades | A default is how 21 dialogs came to open on the ✕; lifting the composer's state to reach a prop is the larger change; a resting fade dimmed the first line |
|
||||
| 2026-09-27 | C3 + C5 as built | A tap toggles the bar through `pointerType` from `pointerdown`; a double-tap takes back its first tap's toggle; the card's pair is the video card's `MediaResumeButtons` (Continue / Restart), a sibling over the picture; the YT description is an ⓘ click popover | A delayed single tap (wait out the double-tap window); new "Resume / From the start" words for Plex; the pair inside the picture's button | A delayed tap feels dead; one media card says one pair of words; a button inside a button is invalid and the hover row already sits beside the picture |
|
||||
| 2026-09-27 | C2 as built | The side panel's drawer is `ui/Modal` `drawer-start`; drawer = a coarse pointer or below 768; the pin offered in its header ≥ 1024 | A drawer of its own inside `SidePanel` | Esc, Back, the scrim, the trap and the scroll lock are the dialog's; a second implementation is how two shells drift |
|
||||
| 2026-09-27 | Narrow rows by container | The download row, the playlist table and the Channels tag row stack by a container query; a fact line never wraps | Breakpoints on the window | The iPad zoomed in, split screen and a column beside a rail are narrow while the window is not; the zoomed download row broke "18.9 / MB" apart |
|
||||
| 2026-09-27 | Phones deferred | A phone-only layout (the hamburger nav, a folded feed band) waits for its own epic; E28 stays desktop + tablet | Fix the phone views inside E28 | Principle 2; the first phone attempts made the feed worse, and the epic's review grows with every sprint |
|
||||
| 2026-09-27 | Conformance check (S3g) | A four-step check before a UI change is done: primitive at a named rung, the chapter re-read, the rendered page measured by kind and look, every state seen | A reviewer's eye per sprint | The S1 probe found 69 button looks; after S3 every control kind is drawn by its primitive, and the check keeps a new look from arriving unnoticed |
|
||||
| 2026-09-27 | Segmented rung | `SegmentedToggle` defaults to the toolbar rung (28, `text-xs`); 24 only for a panel's captioned match toggle | The small default | Two toolbars had inherited 10px type from the default beside 28px buttons |
|
||||
| 2026-09-27 | Copy rule (S3g) | One name per feature (the rail may shorten it); four verbs for four meanings; sentence case with the app's places capitalised; "…" when a command asks first; one-sentence messages without a stop; curly quotes; dates relative in lists with the exact minute on hover, three absolute forms; units from Intl, short | Word-for-word names everywhere; the verbs as they were; all lower case; no "…"; always a stop; always absolute dates; long or compact units | The copy survey found 7 EN and 6 HU double names, 22 Delete against 27 Remove, five date shapes and three spellings of a minute; the owner picked every recommended option |
|
||||
|
||||
@@ -82,6 +82,37 @@ const focusSafeDisabled = {
|
||||
};
|
||||
},
|
||||
};
|
||||
// E28 S3g (docs/DESIGN.md "Copy"): the app shows a date in three forms plus the relative one, all
|
||||
// built in lib/format.ts in the UI language. A bare `toLocaleString()` formats in the BROWSER's locale
|
||||
// and as numbers with seconds; seven call sites had drifted into five shapes. Outside lib/format.ts
|
||||
// a `toLocale*String` call or an `Intl.DateTimeFormat` / `Intl.RelativeTimeFormat` is refused (a
|
||||
// number goes through formatNumber, which the same rule covers).
|
||||
const dateFormat = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
messages: {
|
||||
date: "Format dates and numbers through lib/format.ts (formatDate, formatDateTime, formatDateTimeSec, relativeTime, formatNumber), in the UI language.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const path = context.filename.split("\\").join("/");
|
||||
// lib/format.ts is where the forms live; a test computes its expectations; the Plex player's
|
||||
// on-screen clock (lib/plexPlayerPrefs.ts) is a clock face with its own 12/24h switch, not a date.
|
||||
if (/\/src\/lib\/(format|plexPlayerPrefs)\.ts$|\.test\.tsx?$/.test(path)) return {};
|
||||
return {
|
||||
"CallExpression > MemberExpression.callee[property.name=/^toLocale(Date|Time)?String$/]"(
|
||||
node,
|
||||
) {
|
||||
context.report({ node, messageId: "date" });
|
||||
},
|
||||
"MemberExpression[object.name='Intl'][property.name=/^(DateTimeFormat|RelativeTimeFormat)$/]"(
|
||||
node,
|
||||
) {
|
||||
context.report({ node, messageId: "date" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
// E28 S3b: one primitive per button family (docs/DESIGN.md). 268 hand-rolled buttons in 79 files had
|
||||
// drifted into as many looks, heights and hover styles; they were swept onto ui/Button, IconButton,
|
||||
// Pressable, Chip and DragHandle, and this keeps the next one out. The raw element lives only in the
|
||||
@@ -351,6 +382,7 @@ const localRules = {
|
||||
"jsx-comment-text": jsxCommentText,
|
||||
"focus-safe-disabled": focusSafeDisabled,
|
||||
"button-primitive": buttonPrimitive,
|
||||
"date-format": dateFormat,
|
||||
"input-primitive": inputPrimitive,
|
||||
"surface-role": surfaceRole,
|
||||
"literal-a11y-text": literalA11yText,
|
||||
@@ -445,6 +477,7 @@ export default tseslint.config(
|
||||
"local/jsx-comment-text": "error",
|
||||
"local/focus-safe-disabled": "error",
|
||||
"local/button-primitive": "error",
|
||||
"local/date-format": "error",
|
||||
"local/input-primitive": "error",
|
||||
"local/surface-role": "error",
|
||||
"local/literal-a11y-text": "error",
|
||||
|
||||
@@ -154,7 +154,9 @@ function MembershipPanel({
|
||||
{/* A React portal still propagates events up the REACT tree, so a click inside this panel
|
||||
would otherwise reach the feed card that hosts the trigger and start playback. */}
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<div className="text-xs text-muted px-1.5 pb-1.5">{t("playlists.addToPlaylist")}</div>
|
||||
<div className="text-xs text-muted px-1.5 pb-1.5">
|
||||
{t("playlists.addToPlaylistTitle")}
|
||||
</div>
|
||||
<div
|
||||
ref={fade.ref}
|
||||
style={fade.style}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Copy, Radio, Search } from "lucide-react";
|
||||
|
||||
import { api, type LogFacet, type LogQuery, type ServerLogRow } from "../lib/api";
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import { formatDateTime } from "../lib/format";
|
||||
import { formatDateTimeSec } from "../lib/format";
|
||||
import { nextNavId, notifyNav, rememberNavPosition } from "../lib/history";
|
||||
import { LEVEL_GROUPS, type LevelGroup } from "../lib/log";
|
||||
import {
|
||||
@@ -406,7 +406,7 @@ export default function AdminLogs() {
|
||||
level={row.level}
|
||||
event={row.event ?? "—"}
|
||||
msg={row.msg}
|
||||
title={formatDateTime(row.ts)}
|
||||
title={formatDateTimeSec(row.ts)}
|
||||
open={open === row.id}
|
||||
onToggle={() => setOpen(open === row.id ? null : row.id)}
|
||||
>
|
||||
@@ -475,7 +475,7 @@ function RowDetail({
|
||||
<div className="flex flex-col gap-2">
|
||||
<dl className="grid grid-cols-[max-content_minmax(0,1fr)] gap-x-3 gap-y-0.5 font-sans text-xs">
|
||||
<dt>{t("adminLogs.fields.when")}</dt>
|
||||
<dd className="tabular-nums">{formatDateTime(row.ts)}</dd>
|
||||
<dd className="tabular-nums">{formatDateTimeSec(row.ts)}</dd>
|
||||
<dt>{t("adminLogs.fields.source")}</dt>
|
||||
<dd className="font-mono">
|
||||
{row.source} · {row.logger}
|
||||
|
||||
@@ -33,7 +33,7 @@ import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { api, DEFAULT_TAG_MODES, type FeedFilters, type Me, type ChannelDetail } from "../lib/api";
|
||||
import type { FeedView } from "../lib/feedView";
|
||||
import { channelYouTubeUrl, formatViews } from "../lib/format";
|
||||
import { channelYouTubeUrl, formatDate, formatViews } from "../lib/format";
|
||||
import { ChannelTagsControl } from "./ChannelTags";
|
||||
import { useChannelTagActions } from "../lib/useChannelTagActions";
|
||||
import { Button } from "./ui/Button";
|
||||
@@ -285,12 +285,7 @@ export default function ChannelPage({
|
||||
|
||||
const name = ch?.title ?? initialName ?? channelId;
|
||||
const ytUrl = channelYouTubeUrl(channelId, ch?.handle);
|
||||
const joined = ch?.published_at
|
||||
? new Date(ch.published_at).toLocaleDateString(i18n.language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
})
|
||||
: null;
|
||||
const joined = ch?.published_at ? formatDate(ch.published_at, i18n.language) : null;
|
||||
// Handle + stats on one compact meta line under the name (saves the separate stats row).
|
||||
const metaParts = [
|
||||
ch?.handle ? `@${ch.handle.replace(/^@/, "")}` : null,
|
||||
|
||||
@@ -8,12 +8,12 @@ import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import { relativeTime } from "../lib/format";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { partnerPub, POLL_MS, refreshPartnerPub, useKeyState } from "../lib/messaging";
|
||||
import * as e2ee from "../lib/e2ee";
|
||||
import KeyGate from "./KeyGate";
|
||||
import { Textarea } from "./ui/Input";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
|
||||
// The message list + composer for one conversation, shared by the full Messages page and the
|
||||
// floating dock window. Handles client-side decryption and encrypt-on-send; the surrounding
|
||||
@@ -194,9 +194,10 @@ export default function ChatThread({
|
||||
}`}
|
||||
>
|
||||
{text}
|
||||
<div className={`text-3xs mt-1 ${mine ? "text-accent-fg/70" : "text-muted"}`}>
|
||||
{relativeTime(m.created_at)}
|
||||
</div>
|
||||
<RelativeTime
|
||||
at={m.created_at}
|
||||
className={`block text-3xs mt-1 ${mine ? "text-accent-fg/70" : "text-muted"}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
selectDateChip,
|
||||
type DatePreset,
|
||||
} from "../lib/dateFilter";
|
||||
import { formatDate } from "../lib/format";
|
||||
import { Button } from "./ui/Button";
|
||||
import { SelectTrigger } from "./ui/SelectTrigger";
|
||||
import { Input } from "./ui/Input";
|
||||
@@ -57,10 +58,7 @@ export default function DateFilterSelect({
|
||||
const key = deriveDateKey(filters, customOpen, presets);
|
||||
|
||||
const fmt = (s: string) => {
|
||||
const d = parseDay(s);
|
||||
return d
|
||||
? new Intl.DateTimeFormat(i18n.language, { month: "short", day: "numeric" }).format(d)
|
||||
: s;
|
||||
return parseDay(s) ? formatDate(s, i18n.language) : s;
|
||||
};
|
||||
const rangeSummary = () => {
|
||||
const { dateFrom: f, dateTo: to } = filters;
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useStepCount,
|
||||
useStepPrompt,
|
||||
} from "../lib/debugRecorder";
|
||||
import { formatDuration } from "../lib/format";
|
||||
import { formatDateTimeSec, formatDuration } from "../lib/format";
|
||||
import { useLayer } from "../lib/layerStack";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
@@ -372,7 +372,7 @@ const chipTone = (s: DebugSession): string => {
|
||||
|
||||
/** The last sessions, newest first — reopening one opens its composer, with what was written. */
|
||||
function SessionHistory() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const q = useQuery({ queryKey: qk.debugSessions(), queryFn: api.debugSessions });
|
||||
const items = q.data?.items.filter((s) => s.ended_at) ?? [];
|
||||
if (q.isError && !q.data)
|
||||
@@ -394,9 +394,7 @@ function SessionHistory() {
|
||||
key={s.id}
|
||||
className="flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2 text-sm"
|
||||
>
|
||||
<span className="tabular-nums">
|
||||
{new Date(s.started_at).toLocaleString(i18n.language)}
|
||||
</span>
|
||||
<span className="tabular-nums">{formatDateTimeSec(s.started_at)}</span>
|
||||
<span className="min-w-0 flex-1 truncate text-muted">
|
||||
{s.draft?.title || t(`settings.diagnostics.levels.${s.level}`)}
|
||||
</span>
|
||||
|
||||
@@ -122,9 +122,7 @@ describe("DiagnosticsSettings", () => {
|
||||
fireEvent.click(levelButton("settings.diagnostics.levels.DEBUG"));
|
||||
fireEvent.click(screen.getByTestId("diagnostics-consent-confirm"));
|
||||
await waitFor(() => expect(screen.getByTestId("diagnostics-status")).toBeTruthy());
|
||||
expect(screen.getByTestId("diagnostics-status").textContent).toContain(
|
||||
"settings.diagnostics.hoursMinutes:24h0m",
|
||||
);
|
||||
expect(screen.getByTestId("diagnostics-status").textContent).toContain("24 hr 0 min");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SegmentedToggle from "./ui/SegmentedToggle";
|
||||
import { Section, SettingRow } from "./ui/form";
|
||||
import MyLogViewer from "./MyLogViewer";
|
||||
import { RecorderSettings } from "./DebugRecorder";
|
||||
import { formatUnit } from "../lib/format";
|
||||
|
||||
const LEVELS: readonly DiagnosticsLevel[] = ["INFO", "DEBUG", "TRACE"];
|
||||
|
||||
@@ -171,8 +172,8 @@ export default function DiagnosticsSettings() {
|
||||
level: levelName(current),
|
||||
time:
|
||||
remaining.h > 0
|
||||
? t("settings.diagnostics.hoursMinutes", remaining)
|
||||
: t("settings.diagnostics.minutes", remaining),
|
||||
? `${formatUnit(remaining.h, "hour")} ${formatUnit(remaining.m, "minute")}`
|
||||
: formatUnit(remaining.m, "minute"),
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ExternalLink, FileText, Plus, Server, X } from "lucide-react";
|
||||
|
||||
import { api, type DebugScreenshot, type DebugSession, type ServerLogRow } from "../lib/api";
|
||||
import { closeComposer } from "../lib/debugRecorder";
|
||||
import { formatBytes } from "../lib/format";
|
||||
import { formatBytes, formatDateTimeSec, formatUnit } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import {
|
||||
@@ -303,8 +303,8 @@ function ComposerBody({
|
||||
<div ref={root} className="flex flex-col gap-4">
|
||||
<p className="text-sm text-muted">
|
||||
{t("recorder.composer.meta", {
|
||||
start: started.toLocaleString(i18n.language),
|
||||
minutes,
|
||||
start: formatDateTimeSec(started.toISOString()),
|
||||
minutes: formatUnit(minutes, "minute"),
|
||||
level: t(`settings.diagnostics.levels.${session.level}`),
|
||||
})}
|
||||
</p>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import { relativeTime } from "../lib/format";
|
||||
import { partnerPub, POLL_MS, SYSTEM_ID, useKeyState } from "../lib/messaging";
|
||||
import { useHistorySubview } from "../lib/history";
|
||||
import { openChat } from "../lib/chatDock";
|
||||
@@ -22,6 +21,7 @@ import { Pressable } from "./ui/Pressable";
|
||||
import { Input } from "./ui/Input";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { SectionLabel } from "./ui/Headings";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
|
||||
type View =
|
||||
| "list"
|
||||
@@ -177,9 +177,10 @@ function ConversationList({
|
||||
>
|
||||
{c.partner.name}
|
||||
</span>
|
||||
<span className="ml-auto text-2xs text-muted shrink-0">
|
||||
{relativeTime(c.last_message.created_at)}
|
||||
</span>
|
||||
<RelativeTime
|
||||
at={c.last_message.created_at}
|
||||
className="ml-auto text-2xs text-muted shrink-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
type VideoHiddenMeta,
|
||||
type VideoWatchedMeta,
|
||||
} from "../lib/notifications";
|
||||
import { channelYouTubeUrl, relativeFromMs, relativeTime } from "../lib/format";
|
||||
import { channelYouTubeUrl } from "../lib/format";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { LEVEL_STYLE } from "./ui/toastLevels";
|
||||
@@ -57,6 +57,7 @@ import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { menuGroups } from "./ui/menuGroups";
|
||||
import { HOVER_REVEAL } from "./ui/reveal";
|
||||
import { useConfirm } from "./confirmContext";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
|
||||
// The single notification center. "System" = durable, server-backed notifications (inbox
|
||||
// phase 1). "Activity" = the client-side transient events (action confirmations, errors, with
|
||||
@@ -545,7 +546,7 @@ function NotificationRow({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-medium text-sm">{title}</span>
|
||||
<span className="text-2xs text-muted shrink-0">{relativeTime(n.created_at)}</span>
|
||||
<RelativeTime at={n.created_at} className="text-2xs text-muted shrink-0" />
|
||||
</div>
|
||||
{body && <div className="text-sm text-muted mt-0.5">{body}</div>}
|
||||
{videos.length > 0 && (
|
||||
@@ -634,7 +635,7 @@ function ClientActivityRow({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-2xs text-muted">{relativeFromMs(n.ts)}</span>
|
||||
<RelativeTime at={n.ts} className="text-2xs text-muted" />
|
||||
{needsAction && (
|
||||
<span className="text-3xs uppercase tracking-wide font-semibold text-accent">
|
||||
{t("notifications.actionNeeded")}
|
||||
|
||||
@@ -637,7 +637,7 @@ describe("Playlists · the header", () => {
|
||||
|
||||
it("sums what is known, and says nothing about a runtime nothing sums to", async () => {
|
||||
mount({ 1: [vid("A", 0), untimed("B")] });
|
||||
expect(await meta()).toBe("playlists.itemCount · 50m");
|
||||
expect(await meta()).toBe("playlists.itemCount · 50 min");
|
||||
cleanup();
|
||||
|
||||
mount({ 1: [untimed("C")] });
|
||||
|
||||
@@ -14,7 +14,8 @@ vi.mock("react-i18next", async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, o?: { n?: number }) => (o?.n != null ? `${key}:${o.n}` : key),
|
||||
t: (key: string, o?: { n?: number; d?: string }) =>
|
||||
o?.d != null ? `${key}:${o.d}` : o?.n != null ? `${key}:${o.n}` : key,
|
||||
i18n: { language: "en" },
|
||||
}),
|
||||
};
|
||||
@@ -223,11 +224,11 @@ describe("a double-tap on a side third skips 10 s (C5)", () => {
|
||||
pressPicture("touch", 800);
|
||||
expect(bandHidden()).toBe(false); // …which the second takes back
|
||||
await waitFor(() => expect(lastStart(session)).toEqual(["A", 1510]));
|
||||
expect(screen.getByText("plex.player.tapForward:10")).toBeTruthy();
|
||||
expect(screen.getByText("plex.player.tapForward:10 sec")).toBeTruthy();
|
||||
|
||||
pressPicture("touch", 800);
|
||||
await waitFor(() => expect(lastStart(session)).toEqual(["A", 1520]));
|
||||
expect(screen.getByText("plex.player.tapForward:20")).toBeTruthy();
|
||||
expect(screen.getByText("plex.player.tapForward:20 sec")).toBeTruthy();
|
||||
|
||||
pressPicture("touch", 50); // the other side, a fresh first tap: a toggle, no skip
|
||||
expect(lastStart(session)).toEqual(["A", 1520]);
|
||||
@@ -249,7 +250,7 @@ describe("a double-tap on a side third skips 10 s (C5)", () => {
|
||||
pressPicture("touch", 100);
|
||||
pressPicture("touch", 100);
|
||||
await waitFor(() => expect(lastStart(session)).toEqual(["A", 1490]));
|
||||
expect(screen.getByText("plex.player.tapBack:10")).toBeTruthy();
|
||||
expect(screen.getByText("plex.player.tapBack:10 sec")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { Slider } from "./ui/NativeControls";
|
||||
import { formatUnit } from "../lib/format";
|
||||
|
||||
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
|
||||
const PlexInfo = lazy(() => import("./PlexInfo"));
|
||||
@@ -865,7 +866,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
seekTo(playhead(absRef, loadStartRef) + zone * TAP_SKIP_S);
|
||||
flashOsd(
|
||||
t(zone < 0 ? "plex.player.tapBack" : "plex.player.tapForward", {
|
||||
n: skips * TAP_SKIP_S,
|
||||
d: formatUnit(skips * TAP_SKIP_S, "second"),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -77,7 +77,7 @@ async function pickFromMenu(open: () => void, label: string) {
|
||||
describe("PlexPlaylistView · the header's meta line", () => {
|
||||
it("sums the known runtimes and counts an unknown one as nothing", async () => {
|
||||
expect(await meta([movie("1", 5400), movie("2"), movie("3", 1800)])).toBe(
|
||||
"plex.browse.items · 2h 0m",
|
||||
"plex.browse.items · 2 hr 0 min",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { Button } from "./ui/Button";
|
||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import { useConfirm } from "./confirmContext";
|
||||
import { formatNumber, relativeTime } from "../lib/format";
|
||||
import { formatDateTime, formatNumber, formatUnit, relativeTime } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import Tooltip from "./ui/Tooltip";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
@@ -48,11 +48,12 @@ function secsUntil(iso: string | null): number | null {
|
||||
|
||||
function fmtCountdown(s: number): string {
|
||||
if (s <= 0) return i18n.t("time.eta.now");
|
||||
if (s < 60) return `${s}s`;
|
||||
// The countdown speaks the UI language's units (DESIGN Copy), like every other duration.
|
||||
if (s < 60) return formatUnit(s, "second");
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ${s % 60}s`;
|
||||
if (m < 60) return `${formatUnit(m, "minute")} ${formatUnit(s % 60, "second")}`;
|
||||
const h = Math.floor(m / 60);
|
||||
return `${h}h ${m % 60}m`;
|
||||
return `${formatUnit(h, "hour")} ${formatUnit(m % 60, "minute")}`;
|
||||
}
|
||||
|
||||
const DOT_CLASS: Record<string, string> = {
|
||||
@@ -243,7 +244,10 @@ function JobRow({
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-2xs text-muted truncate">
|
||||
<div
|
||||
className="text-2xs text-muted truncate"
|
||||
title={(!job.running && formatDateTime(job.last_finished)) || undefined}
|
||||
>
|
||||
{job.running
|
||||
? t("scheduler.runningNow")
|
||||
: job.last_finished
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { withActiveAccount } from "../lib/activeAccount";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { formatDateTime } from "../lib/format";
|
||||
import { formatDate, formatDateTime } from "../lib/format";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Activity, Bell, Check, Loader2, Monitor, RefreshCw, Trash2, User } from "lucide-react";
|
||||
@@ -519,7 +519,7 @@ function SignInMethods({ me }: { me: Me }) {
|
||||
// and does a one-time "Plex is master" import. Admin-only (rides the server admin token) and only
|
||||
// shown when the Plex module is enabled. Two-way push/pull arrive in later phases.
|
||||
function PlexWatchSync() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const link = useQuery({ queryKey: qk.plexWatchLink(), queryFn: () => api.plexWatchLink() });
|
||||
const enabled = link.data?.sync_enabled ?? false;
|
||||
@@ -562,9 +562,7 @@ function PlexWatchSync() {
|
||||
// Reflect the toggle's target immediately (optimistic) so the switch responds on click rather than
|
||||
// only flipping once the multi-second import returns — otherwise it reads as "nothing happened".
|
||||
const shown = toggle.isPending ? Boolean(toggle.variables) : enabled;
|
||||
const last = link.data?.last_watch_sync_at
|
||||
? new Date(link.data.last_watch_sync_at).toLocaleString(i18n.language)
|
||||
: null;
|
||||
const last = link.data?.last_watch_sync_at ? formatDateTime(link.data.last_watch_sync_at) : null;
|
||||
|
||||
return (
|
||||
<Section title={t("settings.plexSync.title")}>
|
||||
@@ -670,9 +668,7 @@ export function DownloadAuth() {
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{hosts.map((h) => {
|
||||
const when = h.updated_at
|
||||
? new Date(h.updated_at).toLocaleDateString(i18n.language)
|
||||
: "";
|
||||
const when = h.updated_at ? formatDate(h.updated_at, i18n.language) : "";
|
||||
const open = openHost === h.host_key;
|
||||
return (
|
||||
<div key={h.host_key} className="py-2" data-testid={`cookie-host-${h.host_key}`}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { History, Pause, Play, RefreshCw } from "lucide-react";
|
||||
import { api, type AdminQuotaRow, type Me } from "../lib/api";
|
||||
import { formatEta, formatNumber, quotaActionLabel } from "../lib/format";
|
||||
import { formatEta, formatNumber, formatUnit, quotaActionLabel } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||
import { useWizard } from "./wizardContext";
|
||||
@@ -259,7 +259,7 @@ function SystemStats() {
|
||||
className="flex items-center gap-1.5"
|
||||
options={RANGES.map((d) => ({
|
||||
value: String(d),
|
||||
label: t("stats.rangeDays", { count: d }),
|
||||
label: t("stats.rangeDays", { d: formatUnit(d, "day") }),
|
||||
}))}
|
||||
value={String(days)}
|
||||
onChange={(v) => setDays(Number(v))}
|
||||
@@ -286,7 +286,7 @@ function SystemStats() {
|
||||
<div className={`${SURFACE.card} p-3 mb-4`}>
|
||||
<div className="text-xs text-muted mb-2">
|
||||
{t("stats.dailyTotal", {
|
||||
count: data.range_days,
|
||||
d: formatUnit(data.range_days, "day"),
|
||||
units: formatNumber(grandTotal),
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -143,13 +143,13 @@ describe("the card on a shelf", () => {
|
||||
const title = titleOf(card).closest(".font-medium")!;
|
||||
expect(title.className).toContain("truncate");
|
||||
expect(title.className).not.toContain("line-clamp-2");
|
||||
expect(card.querySelector("span[title]")).toBeNull();
|
||||
expect(card.querySelector("time[title]")).toBeNull();
|
||||
expect(screen.getByRole("link", { name: "LEMMiNO" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("off a shelf keeps two title lines and the age", () => {
|
||||
const card = renderSmall(false);
|
||||
expect(titleOf(card).closest(".font-medium")!.className).toContain("line-clamp-2");
|
||||
expect(card.querySelector("span[title]")).not.toBeNull();
|
||||
expect(card.querySelector("time[title]")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "./ui/mediaCardStyles";
|
||||
import { MediaCard, MediaProgress } from "./ui/MediaCard";
|
||||
import { MediaResumeButtons } from "./MediaResumeButtons";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
|
||||
/** The "…" that opens the video's action list, and whether its menu is the open one. */
|
||||
type More = { props: Record<string, unknown>; open: boolean };
|
||||
@@ -700,17 +701,7 @@ function VideoCard({
|
||||
{/* The small card keeps the relative time and moves the date into its tooltip: at a
|
||||
shelf's width the full pair wrapped to a third meta line, and the tallest card sets
|
||||
the whole shelf's height (every card fills its slot). */}
|
||||
{spec.dense ? (
|
||||
<span
|
||||
title={
|
||||
video.published_at ? formatDate(video.published_at, i18n.language) : undefined
|
||||
}
|
||||
>
|
||||
{relativeTime(video.published_at)}
|
||||
</span>
|
||||
) : (
|
||||
published
|
||||
)}
|
||||
{spec.dense ? <RelativeTime at={video.published_at} /> : published}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import { relativeTime, formatDate } from "../lib/format";
|
||||
import i18n from "../i18n";
|
||||
import type { AuditRow } from "../lib/api";
|
||||
import type { Column } from "./ui/DataTable";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
|
||||
function actionLabel(t: TFunction, action: string): string {
|
||||
// Canonical key → localized label; falls back to the raw key for any unmapped action.
|
||||
@@ -44,13 +43,7 @@ export function auditColumns(
|
||||
sortValue: (r) => r.created_at ?? "",
|
||||
cardPrimary: true,
|
||||
render: (r) => (
|
||||
<span
|
||||
className="text-muted tabular-nums"
|
||||
title={r.created_at ? formatDate(r.created_at, i18n.language) : ""}
|
||||
>
|
||||
{/* `|| "—"` not a null check: relativeTime is empty for a missing OR unreadable date. */}
|
||||
{relativeTime(r.created_at) || "—"}
|
||||
</span>
|
||||
<RelativeTime at={r.created_at} seconds empty="—" className="text-muted tabular-nums" />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import type { TFunction } from "i18next";
|
||||
import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api";
|
||||
import { formatCountOrDash, formatTotalHours, relativeTime } from "../lib/format";
|
||||
import { formatCountOrDash, formatTotalHours } from "../lib/format";
|
||||
import type { Column } from "./ui/DataTable";
|
||||
import { PriorityStepper } from "./ui/PriorityStepper";
|
||||
import ChannelLink from "./ChannelLink";
|
||||
@@ -9,6 +9,7 @@ import AboutCell from "./AboutCell";
|
||||
import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags";
|
||||
import { StoredCell } from "./channelCells";
|
||||
import { isFullySynced } from "./channelSync";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
|
||||
/** The shared "subscribers" DataTable column (right-aligned, sortable, em-dash for null) — identical
|
||||
* in the Channels manager and the ChannelDiscovery table, which show subscriber counts on different
|
||||
@@ -148,7 +149,7 @@ export function buildChannelColumns(deps: ChannelColumnDeps): Column<ManagedChan
|
||||
width: "6rem",
|
||||
sortable: true,
|
||||
sortValue: (c) => (c.last_video_at ? new Date(c.last_video_at).getTime() : 0),
|
||||
render: (c) => <span className="text-muted">{relativeTime(c.last_video_at) || "—"}</span>,
|
||||
render: (c) => <RelativeTime at={c.last_video_at} empty="—" className="text-muted" />,
|
||||
},
|
||||
{
|
||||
key: "length",
|
||||
|
||||
@@ -111,7 +111,7 @@ function NewCollectionPanel({
|
||||
return (
|
||||
<div className={`${SURFACE.panel} mb-6 p-4`}>
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-semibold">{t("plex.collection.newCollection")}</h3>
|
||||
<h3 className="text-sm font-semibold">{t("plex.collection.newCollectionTitle")}</h3>
|
||||
<IconButton size="md" onClick={onClose} label={t("common.close")}>
|
||||
<X />
|
||||
</IconButton>
|
||||
|
||||
@@ -85,7 +85,7 @@ describe("PlexPosterCard a11y", () => {
|
||||
);
|
||||
expect(screen.getByText("2023")).toBeTruthy();
|
||||
expect(screen.getByText("7.5")).toBeTruthy(); // rating.toFixed(1)
|
||||
expect(screen.getByText("1h 53m")).toBeTruthy();
|
||||
expect(screen.getByText("1 hr 53 min")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: movie().title }).getAttribute("title")).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ domain-free.
|
||||
|
||||
B5 glass roles; a bare glass class is a lint error), `Badge` (a static label), `LinkButton` / `IconLink` (a
|
||||
link wearing the button face) over `buttonStyles`, `Skeleton` (B8 first loads, through `LoadingState shape`), `NavRail` (B7), `form`, `DraftSaveBar`, `Combobox`, `FilterSelect`, `SegmentedToggle`, `SortControl`, `ColumnSortControl`, `PriorityStepper`, `UndoToolbar` |
|
||||
| data display | `DataTable`, `tableHeader` + `tableAlign`, `Pager` + `pagerContent`, `VirtualGrid` + `gridColumns` (the column/width maths, incl. the ultrawide `maxCols` cap), `readAhead`, `Carousel`, `MediaCard` + `mediaCardStyles` (the one media card, docs/DESIGN.md "Media card") + `MediaPreview` / `useMediaPreview` (its mouse-rest preview), `NavGrid` (a plain grid, list or table body the keyboard walks — `DataTable`, `VirtualGrid` and `Carousel` take `keyboardNav`), `Avatar`, `CountBadge`, `ItemCount`, `FilterChipGroup` |
|
||||
| data display | `DataTable`, `tableHeader` + `tableAlign`, `Pager` + `pagerContent`, `VirtualGrid` + `gridColumns` (the column/width maths, incl. the ultrawide `maxCols` cap), `readAhead`, `Carousel`, `MediaCard` + `mediaCardStyles` (the one media card, docs/DESIGN.md "Media card") + `MediaPreview` / `useMediaPreview` (its mouse-rest preview), `NavGrid` (a plain grid, list or table body the keyboard walks — `DataTable`, `VirtualGrid` and `Carousel` take `keyboardNav`), `Avatar`, `CountBadge`, `ItemCount`, `FilterChipGroup`, `RelativeTime` ("3d ago" with the exact minute on hover, docs/DESIGN.md "Copy") |
|
||||
| page & panel structure | `PageShell` + `pageToolbarSlot` + `pageHeading`, `PageScroller`, `Headings` (`PageHeading`, `SectionLabel`), `SkipLink` + `landmarks`, `FilterFold` (B4), `PanelGroup`, `PanelGroups`, `Tabs`, `BackToTop` |
|
||||
| state & messaging | `QueryState`, `Banner`, `InlineNotice` |
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// docs/DESIGN.md "Copy": a relative time in a list carries the exact moment on hover.
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { formatDateTime, formatDateTimeSec, relativeTime } from "../../lib/format";
|
||||
import { RelativeTime } from "./RelativeTime";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const THREE_DAYS_AGO = new Date(Date.now() - 3 * 86400_000).toISOString();
|
||||
|
||||
describe("RelativeTime", () => {
|
||||
it("says how long ago, and names the exact minute on hover", () => {
|
||||
const { container } = render(<RelativeTime at={THREE_DAYS_AGO} />);
|
||||
const el = container.querySelector("time")!;
|
||||
expect(el.textContent).toBe(relativeTime(THREE_DAYS_AGO));
|
||||
expect(el.getAttribute("title")).toBe(formatDateTime(THREE_DAYS_AGO));
|
||||
expect(el.getAttribute("dateTime")).toBe(THREE_DAYS_AGO);
|
||||
});
|
||||
|
||||
it("takes epoch millis, and the log form with seconds", () => {
|
||||
const ms = Date.parse(THREE_DAYS_AGO);
|
||||
const { container } = render(<RelativeTime at={ms} seconds />);
|
||||
expect(container.querySelector("time")!.getAttribute("title")).toBe(
|
||||
formatDateTimeSec(THREE_DAYS_AGO),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the placeholder for a missing or unreadable moment, never a bare title", () => {
|
||||
const { container } = render(
|
||||
<>
|
||||
<RelativeTime at={null} empty="—" />
|
||||
<RelativeTime at="not a date" />
|
||||
</>,
|
||||
);
|
||||
expect(container.textContent).toBe("—");
|
||||
expect(container.querySelector("time")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { formatDateTime, formatDateTimeSec, relativeFromMs } from "../../lib/format";
|
||||
|
||||
// docs/DESIGN.md "Copy": a relative time ("3d ago") says how recent something is in a list, and the
|
||||
// exact moment is one hover away — so the pair travels together here instead of every list
|
||||
// remembering (or forgetting) its own `title`. `seconds` is the log form (the audit). An unreadable
|
||||
// or missing moment renders `empty` ("—" in a table cell) or nothing.
|
||||
export function RelativeTime({
|
||||
at,
|
||||
seconds = false,
|
||||
empty = "",
|
||||
className,
|
||||
}: {
|
||||
/** An ISO timestamp, or epoch millis (a client-side notification). */
|
||||
at: string | number | null | undefined;
|
||||
seconds?: boolean;
|
||||
empty?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const ms = typeof at === "number" ? at : at ? new Date(at).getTime() : NaN;
|
||||
const text = Number.isFinite(ms) ? relativeFromMs(ms) : "";
|
||||
if (!text) return empty ? <span className={className}>{empty}</span> : null;
|
||||
const iso = new Date(ms).toISOString();
|
||||
return (
|
||||
<time
|
||||
dateTime={iso}
|
||||
title={seconds ? formatDateTimeSec(iso) : formatDateTime(iso)}
|
||||
className={className}
|
||||
>
|
||||
{text}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// docs/DESIGN.md "Copy" (E28 S3g): the parts of the copy rule a machine can read, held over every
|
||||
// value in both languages. The rest (one name per feature, one verb per meaning, "…" when a command
|
||||
// asks first) needs a reader and is checked by the DESIGN conformance step.
|
||||
|
||||
const files = import.meta.glob("./locales/*/*.json", { eager: true, import: "default" }) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
function values(): [string, string][] {
|
||||
const out: [string, string][] = [];
|
||||
const walk = (o: unknown, path: string) => {
|
||||
if (typeof o === "string") out.push([path, o]);
|
||||
else if (o && typeof o === "object")
|
||||
for (const [k, v] of Object.entries(o)) walk(v, path ? `${path}.${k}` : k);
|
||||
};
|
||||
for (const [file, json] of Object.entries(files))
|
||||
walk(json, file.replace("./locales/", "").replace(".json", ":"));
|
||||
return out;
|
||||
}
|
||||
|
||||
const offenders = (test: (v: string) => boolean) =>
|
||||
values()
|
||||
.filter(([, v]) => test(v))
|
||||
.map(([k, v]) => `${k} = ${v}`);
|
||||
|
||||
describe("the copy rule", () => {
|
||||
it("quotes a name with curly quotes, never straight ones", () => {
|
||||
expect(offenders((v) => v.includes('"'))).toEqual([]);
|
||||
});
|
||||
|
||||
it("writes an ellipsis as one character", () => {
|
||||
expect(offenders((v) => v.includes("..."))).toEqual([]);
|
||||
});
|
||||
|
||||
it("ends no message with a tick", () => {
|
||||
expect(offenders((v) => v.includes("✓"))).toEqual([]);
|
||||
});
|
||||
|
||||
it("spells no unit as a bare letter after a number ('90m', '24h', '{{count}}d', '10 s')", () => {
|
||||
expect(offenders((v) => /(\d|\}\})\s?(m|h|d|s)(?![\p{L}\d/])/u.test(v))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -11,5 +11,5 @@
|
||||
"fixes": "Fixes",
|
||||
"olderVersions_one": "{{count}} older version",
|
||||
"olderVersions_other": "{{count}} older versions",
|
||||
"notesError": "The release notes could not be loaded."
|
||||
"notesError": "Couldn't load the release notes"
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"showRequest": "Only this request's lines",
|
||||
"showJob": "Only this job's lines",
|
||||
"empty": "No lines match the filters.",
|
||||
"stale": "The last refresh failed; what you see may be out of date.",
|
||||
"olderFailed": "Couldn't load the older lines.",
|
||||
"countsFailed": "The filter counts didn't load.",
|
||||
"stale": "Couldn't refresh — what you see may be out of date",
|
||||
"olderFailed": "Couldn't load the older lines",
|
||||
"countsFailed": "Couldn't load the filter counts",
|
||||
"count_one": "{{count}} line loaded · newest first",
|
||||
"count_other": "{{count}} lines loaded · newest first",
|
||||
"older": "Load older",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"clearTitle": "Clear the audit log?",
|
||||
"clearConfirm": "Permanently delete all audit entries? This can't be undone. (One entry recording that you cleared it is kept.)",
|
||||
"cleared": "Cleared {{count}} audit entries",
|
||||
"clearFailed": "Couldn't clear the audit log.",
|
||||
"clearFailed": "Couldn't clear the audit log",
|
||||
"truncated": "Showing the {{returned}} most recent of {{total}} entries.",
|
||||
"cols": {
|
||||
"when": "When",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"job_run": "Scheduler job run",
|
||||
"audit_clear": "Audit log cleared",
|
||||
"download_credential_set": "Download cookies set",
|
||||
"download_credential_delete": "Download cookies removed",
|
||||
"download_credential_delete": "Download cookies deleted",
|
||||
"invite_delete": "Whitelist entry deleted",
|
||||
"invite_send": "Invitation sent",
|
||||
"issue_create": "Bug report sent"
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"fullHistoryProgress": "Fetching history… {{stored}} / {{total}}",
|
||||
"fullHistoryLoadingHint": "Fetching the full back-catalogue — videos appear as they're found; a very large channel's tail is finished in the background.",
|
||||
"fullHistoryQueued": "Fetching this channel's full history — new videos will appear as they're found.",
|
||||
"fullHistoryFailed": "Couldn't start the full-history fetch."
|
||||
"fullHistoryFailed": "Couldn't start the full-history fetch"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"actionsLabel": "Catalog actions",
|
||||
"syncSubscriptions": "Read subscriptions from YouTube",
|
||||
"syncSubscriptions": "Sync subscriptions from YouTube",
|
||||
"syncSubscriptionsHint": "Re-import your subscription list from YouTube — adds channels you've newly followed and drops ones you've unfollowed. The videos themselves keep syncing automatically in the background; this does not re-fetch them.",
|
||||
"backfillEverything": "Backfill everything",
|
||||
"backfillEverythingHint": "Request full back-catalog backfill for every channel you're subscribed to. Older videos and search become complete as the shared daily quota allows — this can take a while.",
|
||||
@@ -31,8 +31,8 @@
|
||||
"needWriteHint": "Enable playlist editing in Settings to subscribe on YouTube.",
|
||||
"subscribedTitle": "Subscribed on YouTube",
|
||||
"subscribedBody": "You're now following {{name}} — its new uploads will start arriving in your Feed.",
|
||||
"subscribeFailed": "Subscribe failed",
|
||||
"confirmSubscribe": "Subscribe to \"{{name}}\" on YouTube? This changes your real YouTube account and spends a little API quota.",
|
||||
"subscribeFailed": "Couldn't subscribe",
|
||||
"confirmSubscribe": "Subscribe to “{{name}}” on YouTube? This changes your real YouTube account and spends a little API quota.",
|
||||
"cols": {
|
||||
"totalVideos": "Videos",
|
||||
"inPlaylists": "In playlists",
|
||||
@@ -54,8 +54,8 @@
|
||||
"noneYet": "No tags yet — create one below.",
|
||||
"create": "Create tag",
|
||||
"createPlaceholder": "New tag…",
|
||||
"createFailed": "Could not create the tag.",
|
||||
"updateFailed": "Could not update the tag.",
|
||||
"createFailed": "Couldn't create the tag",
|
||||
"updateFailed": "Couldn't update the tag",
|
||||
"noTag": "No tag",
|
||||
"moreCount_one": "{{count}} more tag",
|
||||
"moreCount_other": "{{count}} more tags",
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
"row": {
|
||||
"openOnYouTube": "Open on YouTube",
|
||||
"editTags": "Edit tags",
|
||||
"editTags": "Edit tags…",
|
||||
"filterFeedByTag": "Filter the Feed by “{{name}}”",
|
||||
"priorityHint": "Your ranking for this channel. Sort the Feed by “Channel priority” to bring higher-priority channels to the top.",
|
||||
"fullHint": "Full history fetched.",
|
||||
@@ -126,20 +126,20 @@
|
||||
},
|
||||
"notify": {
|
||||
"synced": "Synced {{count}} subscriptions",
|
||||
"syncFailed": "Subscription sync failed",
|
||||
"syncFailed": "Couldn't sync subscriptions",
|
||||
"unsubscribed": "Unsubscribed from {{name}} on YouTube",
|
||||
"unsubscribeFailed": "Unsubscribe failed",
|
||||
"unsubscribeFailed": "Couldn't unsubscribe",
|
||||
"fullHistoryRequested": "Full history requested for {{count}} channels",
|
||||
"fullHistoryFailed": "Couldn't request full history",
|
||||
"needYouTube": "Connect your YouTube account to do this.",
|
||||
"needYouTube": "Connect your YouTube account to do this",
|
||||
"connect": "Connect",
|
||||
"filteredByTag": "Feed filtered by tag: {{name}}",
|
||||
"resetDone": "{{name}} reset — re-fetching now",
|
||||
"resetFailed": "Couldn't reset this channel",
|
||||
"hidden": "“{{name}}” hidden from your Feed"
|
||||
},
|
||||
"confirmUnsubscribe": "Unsubscribe from \"{{name}}\" on YouTube? This changes your real YouTube account. To just remove it from your Feed, hide it instead.",
|
||||
"confirmReset": "Re-fetch \"{{name}}\" from scratch? This re-runs its backfill (recent + full history) and spends some API quota.",
|
||||
"confirmUnsubscribe": "Unsubscribe from “{{name}}” on YouTube? This changes your real YouTube account. To just remove it from your Feed, hide it instead.",
|
||||
"confirmReset": "Re-fetch “{{name}}” from scratch? This re-runs its backfill (recent + full history) and spends some API quota.",
|
||||
"searchPlaceholder": "Search channels…",
|
||||
"blocked": {
|
||||
"title": "Blocked channels",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfService": "Terms of Service",
|
||||
"privacyPolicy": "Privacy policy",
|
||||
"termsOfService": "Terms of service",
|
||||
"sourceCode": "Source code",
|
||||
"close": "Close",
|
||||
"cancel": "Cancel",
|
||||
@@ -10,7 +10,7 @@
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"loading": "Loading…",
|
||||
"somethingWrong": "Something went wrong.",
|
||||
"somethingWrong": "Something went wrong",
|
||||
"itemCount_one": "{{formattedCount}} item",
|
||||
"itemCount_other": "{{formattedCount}} items",
|
||||
"accessRequestsTitle": "Access requests",
|
||||
@@ -20,7 +20,7 @@
|
||||
"demoSharedNotice": "You're in the shared demo account. Everything you do here — playlists, hidden videos, settings — is communal and may be changed by other demo visitors at the same time.",
|
||||
"backToTop": "Back to top",
|
||||
"retry": "Retry",
|
||||
"loadError": "Couldn't load this — check your connection and try again.",
|
||||
"loadError": "Couldn't load this — check your connection and try again",
|
||||
"seeAll": "See all",
|
||||
"scrollLeft": "Scroll left",
|
||||
"scrollRight": "Scroll right",
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"search_source": {
|
||||
"label": "Live search source",
|
||||
"hint": "Where live YouTube search results come from. \"Website\" reads YouTube's own search endpoint and spends no API quota (recommended). \"Official API\" uses search.list (100 units/page) — switch to it if the website source ever breaks or is blocked."
|
||||
"hint": "Where live YouTube search results come from. “Website” reads YouTube's own search endpoint and spends no API quota (recommended). “Official API” uses search.list (100 units/page) — switch to it if the website source ever breaks or is blocked."
|
||||
},
|
||||
"backfill_recent_max_videos": {
|
||||
"label": "Recent backfill — max videos",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"download_upload_max_bytes": {
|
||||
"label": "Upload size cap per file (bytes)",
|
||||
"hint": "Largest single file a user may upload to the download center. At least 1, so the cap is always on; the user's total storage is still bounded by their quota. Default 500 MiB."
|
||||
"hint": "Largest single file a user may upload to Downloads. At least 1, so the cap is always on; the user's total storage is still bounded by their quota. Default 500 MiB."
|
||||
},
|
||||
"download_retention_days": {
|
||||
"label": "Retention (days)",
|
||||
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"download_layout": {
|
||||
"label": "On-disk layout",
|
||||
"hint": "\"Plex\" = a Channel/Season YYYY/… tree with .nfo + poster (Plex-indexable); \"Flat\" = a single folder."
|
||||
"hint": "“Plex” = a Channel/Season YYYY/… tree with .nfo + poster (Plex-indexable); “Flat” = a single folder."
|
||||
},
|
||||
"download_worker_concurrency": {
|
||||
"label": "Worker concurrency",
|
||||
@@ -166,7 +166,7 @@
|
||||
},
|
||||
"log_retention_days": {
|
||||
"label": "Log retention (days)",
|
||||
"hint": "How long stored log events are kept before the log cleanup job deletes them. They include account-linked request history, so there is no \"keep forever\". Default 7."
|
||||
"hint": "How long stored log events are kept before the log cleanup job deletes them. They include account-linked request history, so there is no “keep forever”. Default 7."
|
||||
},
|
||||
"plex_enabled": {
|
||||
"label": "Enable Plex",
|
||||
@@ -292,22 +292,22 @@
|
||||
"testEmail": "Send test email",
|
||||
"testEmailHint": "Send a test email to your own address using the settings above, to confirm they work.",
|
||||
"testSent": "Test email sent to {{to}}",
|
||||
"testFailed": "Test email failed — check the settings",
|
||||
"testFailed": "Couldn't send the test email — check the settings",
|
||||
"plexTest": "Test connection",
|
||||
"plexTestHint": "Verify the Plex server URL + token and load the library list. Save any edits first.",
|
||||
"testDirty": "Save your changes before testing them.",
|
||||
"plexTestOk": "Connected to {{name}}",
|
||||
"plexTestFailed": "Plex connection failed — check the URL and token",
|
||||
"plexTestFailed": "Couldn't connect to Plex — check the URL and token",
|
||||
"plexConnected": "Connected to {{name}} {{version}}",
|
||||
"plexMediaOk": "Media mount OK — a sample file was found on disk.",
|
||||
"plexMediaMissing": "Media not readable at {{path}} — check that the Plex media is bind-mounted into the container and that the path map is correct. Playback will fail until this is fixed.",
|
||||
"plexPickLibraries": "Libraries to expose:",
|
||||
"plexPickHint": "Unchecking all is the same as exposing every library. Remember to Save.",
|
||||
"plexMovies": "Movies",
|
||||
"plexShows": "TV Shows",
|
||||
"plexShows": "TV shows",
|
||||
"emailOff": "Email sending is not configured — Siftlode silently skips every message: verification links, password resets and access-request notices. Fill in the fields above, then send a test.",
|
||||
"issuesTest": "Check the connection",
|
||||
"issuesTestHint": "Checks that the repository answers, is private and takes issues. Save your changes first.",
|
||||
"issuesTestOk": "Works: {{repo}} (private)",
|
||||
"issuesTestFailed": "The issue tracker check failed"
|
||||
"issuesTestFailed": "Couldn't check the issue tracker"
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
"submit": "Add to downloads",
|
||||
"added": "Added to downloads",
|
||||
"view": "View",
|
||||
"manage": "Manage formats"
|
||||
"manage": "Manage formats…"
|
||||
},
|
||||
"page": {
|
||||
"addUrl": "Paste a video link (YouTube, Facebook, …) or a YouTube video id…",
|
||||
"add": "Add",
|
||||
"paste": "Paste link from clipboard",
|
||||
"upload": "Upload",
|
||||
"manage": "Manage formats"
|
||||
"manage": "Manage formats…"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Upload a file",
|
||||
@@ -33,7 +33,7 @@
|
||||
"uploading": "Uploading… {{pct}}%",
|
||||
"uploadProgress": "Upload progress",
|
||||
"added": "File uploaded",
|
||||
"failed": "The upload failed."
|
||||
"failed": "Couldn't upload the file"
|
||||
},
|
||||
"tabs": {
|
||||
"queue": "Queue",
|
||||
@@ -65,15 +65,15 @@
|
||||
"pause": "Pause",
|
||||
"resume": "Resume",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Remove",
|
||||
"delete": "Delete",
|
||||
"retry": "Retry",
|
||||
"download": "Save to my device",
|
||||
"play": "Play",
|
||||
"rename": "Rename",
|
||||
"editDetails": "Edit details",
|
||||
"editDetails": "Edit details…",
|
||||
"share": "Share (copy link)",
|
||||
"shareManage": "Manage sharing",
|
||||
"edit": "Edit / trim",
|
||||
"shareManage": "Manage sharing…",
|
||||
"edit": "Edit / trim…",
|
||||
"redownload": "Re-download (rebuild file)",
|
||||
"removeShared": "Remove from my list"
|
||||
},
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Your storage",
|
||||
"items": "{{used}} / {{max}} items",
|
||||
"items": "{{used}} / {{max}} downloads",
|
||||
"unlimited": "Unlimited"
|
||||
},
|
||||
"source": {
|
||||
@@ -108,7 +108,7 @@
|
||||
"share": {
|
||||
"title": "Share download",
|
||||
"done": "Shared with {{email}}",
|
||||
"failed": "Couldn’t share.",
|
||||
"failed": "Couldn’t share",
|
||||
"userSection": "Share with a user",
|
||||
"userHint": "They’ll find it under “Shared with me”.",
|
||||
"pickPlaceholder": "Search a user…",
|
||||
@@ -127,7 +127,7 @@
|
||||
"days": "{{n}} days",
|
||||
"password": "Password (optional)",
|
||||
"copy": "Copy link",
|
||||
"copyFailed": "Couldn’t copy.",
|
||||
"copyFailed": "Couldn’t copy",
|
||||
"revoke": "Revoke",
|
||||
"revokeConfirm": "This link will stop working immediately.",
|
||||
"views": "{{n}} views",
|
||||
@@ -138,15 +138,15 @@
|
||||
"changePassword": "Change password",
|
||||
"newPassword": "New password",
|
||||
"savePassword": "Save",
|
||||
"removePassword": "Remove",
|
||||
"removePassword": "Delete",
|
||||
"removePasswordConfirm": "This makes the link public — anyone with the URL can watch it without a password."
|
||||
},
|
||||
"confirm": {
|
||||
"cancelTitle": "Cancel download?",
|
||||
"cancelBody": "This stops the download and removes it from your queue.",
|
||||
"cancelAction": "Stop download",
|
||||
"deleteTitle": "Remove download?",
|
||||
"deleteBody": "This removes it from your list. The file may stay cached for other users until it expires.",
|
||||
"deleteTitle": "Delete download?",
|
||||
"deleteBody": "This deletes it from your downloads. The file may stay cached for other users until it expires.",
|
||||
"removeSharedTitle": "Remove from your list?",
|
||||
"removeSharedBody": "This removes it from your shared list only — it won't delete the owner's file.",
|
||||
"redownloadTitle": "Re-download this file?",
|
||||
@@ -198,28 +198,28 @@
|
||||
"errors": {
|
||||
"quota": {
|
||||
"max_bytes": "You've used your download storage quota ({{size}} GB). Free up space first.",
|
||||
"max_jobs": "You've reached your download limit ({{limit}} items). Remove some first.",
|
||||
"generic": "Download quota exceeded."
|
||||
"max_jobs": "You've reached your download limit ({{limit}} downloads). Delete some first.",
|
||||
"generic": "Download quota exceeded"
|
||||
},
|
||||
"edit": {
|
||||
"empty_edit": "Choose a trim range or crop area first.",
|
||||
"source_not_ready": "The source download isn't ready to edit.",
|
||||
"generic": "That edit couldn't be created."
|
||||
"empty_edit": "Choose a trim range or crop area first",
|
||||
"source_not_ready": "The source download isn't ready to edit",
|
||||
"generic": "Couldn't create that edit"
|
||||
},
|
||||
"upload": {
|
||||
"tooLarge": "That file is too large (max {{size}}).",
|
||||
"empty": "That file is empty."
|
||||
"tooLarge": "That file is too large (max {{size}})",
|
||||
"empty": "That file is empty"
|
||||
},
|
||||
"urlBlocked": {
|
||||
"scheme": "Only http:// and https:// links can be downloaded.",
|
||||
"no_host": "That link doesn't contain a website address.",
|
||||
"scheme": "Only http:// and https:// links can be downloaded",
|
||||
"no_host": "That link doesn't contain a website address",
|
||||
"unresolvable": "That address couldn't be looked up. Check the link and try again.",
|
||||
"private": "That address is on the server's own network, so it can't be downloaded from.",
|
||||
"generic": "That address can't be downloaded from.",
|
||||
"malformed": "That link isn't a valid web address."
|
||||
"private": "That address is on the server's own network, so it can't be downloaded from",
|
||||
"generic": "That address can't be downloaded from",
|
||||
"malformed": "That link isn't a valid web address"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Download failed",
|
||||
"title": "Couldn't download",
|
||||
"detailsLink": "details",
|
||||
"detailsToggle": "Technical details",
|
||||
"cat": {
|
||||
@@ -248,7 +248,7 @@
|
||||
"body": "This video is DRM-protected and cannot be downloaded."
|
||||
},
|
||||
"postprocess": {
|
||||
"short": "Processing failed",
|
||||
"short": "Couldn't process the video",
|
||||
"body": "The video downloaded but a processing step (merge or convert) failed. A retry often fixes this."
|
||||
},
|
||||
"source_missing": {
|
||||
@@ -260,8 +260,8 @@
|
||||
"body": "A network error interrupted the download. A retry usually fixes this."
|
||||
},
|
||||
"unknown": {
|
||||
"short": "Download failed",
|
||||
"body": "Something went wrong while downloading this item. The technical details below may help."
|
||||
"short": "Couldn't download",
|
||||
"body": "Something went wrong with this download. The technical details below may help."
|
||||
},
|
||||
"quota_exceeded": {
|
||||
"short": "Storage full",
|
||||
@@ -300,9 +300,9 @@
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
"empty": "Paste your cookies first.",
|
||||
"format": "That doesn't look like a cookies.txt file (Netscape format).",
|
||||
"generic": "Those cookies couldn't be saved."
|
||||
"empty": "Paste your cookies first",
|
||||
"format": "That doesn't look like a cookies.txt file (Netscape format)",
|
||||
"generic": "Couldn't save those cookies"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"fast": {
|
||||
"label": "Fast cut",
|
||||
"hint": "Instant, but cuts snap to keyframes (±1–2s)."
|
||||
"hint": "Instant, but cuts snap to keyframes (±1–2 sec)."
|
||||
},
|
||||
"nameLabel": "Clip name (optional)",
|
||||
"willReencode": "Will re-encode",
|
||||
@@ -21,7 +21,7 @@
|
||||
"createN": "Create {{count}} clips",
|
||||
"created_one": "{{count}} clip added to your downloads",
|
||||
"created_other": "{{count}} clips added to your downloads",
|
||||
"failed": "Couldn’t create the clip.",
|
||||
"failed": "Couldn’t create the clip",
|
||||
"partlyCreated": "Only {{done}} of {{total}} clips were added — the rest are still selected, so creating again adds just those.",
|
||||
"splitHere": "Split here",
|
||||
"output": {
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
{
|
||||
"title": "Couldn't complete that",
|
||||
"generic": "The server couldn't carry out that action. Please try again.",
|
||||
"demo": "This isn't available in the demo account.",
|
||||
"demo": "This isn't available in the demo account",
|
||||
"validation": "Some of the values you entered weren't valid. Please check them and try again.",
|
||||
"tooLarge": "That was too large for the server to accept.",
|
||||
"tooLarge": "That was too large for the server to accept",
|
||||
"server": "Server error",
|
||||
"syncCooldown": "You've synced a few times just now — give it a few minutes. The YouTube budget is shared with everyone on this instance, and background syncing keeps running either way.",
|
||||
"notFound": {
|
||||
"generic": "This item doesn't exist any more.",
|
||||
"view": "This view doesn't exist any more.",
|
||||
"tag": "This tag doesn't exist any more.",
|
||||
"pin": "This isn't on your comfort shelf any more.",
|
||||
"playlist": "This playlist doesn't exist any more.",
|
||||
"move_target": "The playlist you're moving it to doesn't exist any more.",
|
||||
"video": "This video isn't in the library any more.",
|
||||
"invite": "This invite doesn't exist any more.",
|
||||
"user": "This user doesn't exist any more.",
|
||||
"channel": "This channel isn't in the library any more.",
|
||||
"subscription": "You're not subscribed to this channel any more.",
|
||||
"recipient": "There's no user with that email address.",
|
||||
"job": "This job doesn't exist any more.",
|
||||
"download": "This download doesn't exist any more.",
|
||||
"debug_session": "This debug session doesn't exist any more.",
|
||||
"screenshot": "This screenshot doesn't exist any more.",
|
||||
"link": "This share link doesn't exist any more.",
|
||||
"notification": "This notification doesn't exist any more.",
|
||||
"account": "That account no longer exists.",
|
||||
"collection": "This collection doesn't exist any more.",
|
||||
"config_key": "There's no such setting.",
|
||||
"file": "The file is no longer available.",
|
||||
"filmstrip": "There's no filmstrip for this.",
|
||||
"host": "This host isn't on the list any more.",
|
||||
"image": "There's no image for this.",
|
||||
"item": "This item doesn't exist any more.",
|
||||
"library": "This library doesn't exist any more.",
|
||||
"plex_item": "This Plex item isn't in the library any more.",
|
||||
"poster": "There's no poster for this.",
|
||||
"profile": "This download profile doesn't exist any more.",
|
||||
"season": "This season isn't in the library any more.",
|
||||
"show": "This show isn't in the library any more."
|
||||
"generic": "This item doesn't exist any more",
|
||||
"view": "This view doesn't exist any more",
|
||||
"tag": "This tag doesn't exist any more",
|
||||
"pin": "This isn't on your comfort shelf any more",
|
||||
"playlist": "This playlist doesn't exist any more",
|
||||
"move_target": "The playlist you're moving it to doesn't exist any more",
|
||||
"video": "This video isn't in the library any more",
|
||||
"invite": "This invite doesn't exist any more",
|
||||
"user": "This user doesn't exist any more",
|
||||
"channel": "This channel isn't in the library any more",
|
||||
"subscription": "You're not subscribed to this channel any more",
|
||||
"recipient": "There's no user with that email address",
|
||||
"job": "This job doesn't exist any more",
|
||||
"download": "This download doesn't exist any more",
|
||||
"debug_session": "This debug session doesn't exist any more",
|
||||
"screenshot": "This screenshot doesn't exist any more",
|
||||
"link": "This share link doesn't exist any more",
|
||||
"notification": "This notification doesn't exist any more",
|
||||
"account": "That account no longer exists",
|
||||
"collection": "This collection doesn't exist any more",
|
||||
"config_key": "There's no such setting",
|
||||
"file": "The file is no longer available",
|
||||
"filmstrip": "There's no filmstrip for this",
|
||||
"host": "This host isn't on the list any more",
|
||||
"image": "There's no image for this",
|
||||
"item": "This item doesn't exist any more",
|
||||
"library": "This library doesn't exist any more",
|
||||
"plex_item": "This Plex item isn't in the library any more",
|
||||
"poster": "There's no poster for this",
|
||||
"profile": "This download profile doesn't exist any more",
|
||||
"season": "This season isn't in the library any more",
|
||||
"show": "This show isn't in the library any more"
|
||||
},
|
||||
"admin": {
|
||||
"not_allowed": "Their address isn't approved any more. Add it to the whitelist first.",
|
||||
"not_applicable": "This account has nothing left to confirm.",
|
||||
"not_approved": "Only an approved address can be invited.",
|
||||
"not_applicable": "This account has nothing left to confirm",
|
||||
"not_approved": "Only an approved address can be invited",
|
||||
"email_off": "No mail was sent: this instance has no working SMTP setup. Check Configuration → Email / SMTP.",
|
||||
"send_failed": "The mail couldn't be sent: the mail server was unreachable, or it refused the login or the message. Check Configuration → Email / SMTP, then try again.",
|
||||
"demo": "The demo account is shared, so it can't get a set-password link."
|
||||
"demo": "The demo account is shared, so it can't get a set-password link"
|
||||
},
|
||||
"reason": {
|
||||
"unreachable": "The server couldn't be reached. Try again.",
|
||||
"forbidden": "You don't have permission to do that.",
|
||||
"rateLimited": "Too many requests at once — wait a moment and try again.",
|
||||
"forbidden": "You don't have permission to do that",
|
||||
"rateLimited": "Too many requests at once — wait a moment and try again",
|
||||
"unexpected": "Something went wrong in the app. Try again."
|
||||
},
|
||||
"writeFailed": {
|
||||
@@ -70,12 +70,12 @@
|
||||
"delete": "Couldn't delete the download",
|
||||
"redownload": "Couldn't start the download again"
|
||||
},
|
||||
"removeShared": "Couldn't remove the shared item",
|
||||
"removeShared": "Couldn't remove the shared download",
|
||||
"downloadDetails": "Couldn't save the download's details",
|
||||
"enqueueDownload": "Couldn't queue the download",
|
||||
"downloadProfileSave": "Couldn't save the download profile",
|
||||
"downloadProfileDelete": "Couldn't delete the download profile",
|
||||
"downloadCookiesRemove": "Couldn't remove those cookies",
|
||||
"downloadCookiesRemove": "Couldn't delete those cookies",
|
||||
"notifRead": "Couldn't mark the notification as read",
|
||||
"notifReadAll": "Couldn't mark the notifications as read",
|
||||
"notifDismiss": "Couldn't dismiss the notification",
|
||||
@@ -106,8 +106,8 @@
|
||||
"body": "Couldn't reach the server — it may be restarting. This notice will clear once the connection is back."
|
||||
},
|
||||
"boundary": {
|
||||
"title": "Something went wrong.",
|
||||
"subtitle": "The app hit an unexpected error.",
|
||||
"title": "Something went wrong",
|
||||
"subtitle": "The app hit an unexpected error",
|
||||
"reload": "Reload",
|
||||
"notifTitle": "Something broke",
|
||||
"notifMessage": "Unexpected error"
|
||||
@@ -118,111 +118,111 @@
|
||||
"unavailable": "YouTube didn't accept that just now. Please try again."
|
||||
},
|
||||
"refused": {
|
||||
"account_no_access": "That account no longer has access.",
|
||||
"account_signed_out": "That account was signed out elsewhere — sign in again.",
|
||||
"account_suspended": "That account is suspended.",
|
||||
"address_has_account": "That address already has an account — delete the account instead.",
|
||||
"admin_first": "Create the admin account first.",
|
||||
"admin_only": "Only an admin can do that.",
|
||||
"already_set_up": "This instance is already set up.",
|
||||
"already_subscribed": "You're already subscribed to this channel.",
|
||||
"channel_blocked": "You've blocked this channel.",
|
||||
"collection_fields_required": "A title and an item are both needed.",
|
||||
"collection_pending_sync": "The collection changed in Plex, but refreshing the local copy failed — it catches up on the next sync.",
|
||||
"collection_read_only": "This collection is read-only.",
|
||||
"collection_smart": "Smart and auto-list collections can't be made editable.",
|
||||
"demo_activate": "The demo account can't be activated.",
|
||||
"demo_delete": "The demo account can't be deleted.",
|
||||
"demo_role": "The demo account's role can't be changed.",
|
||||
"demo_suspend": "The demo account can't be suspended.",
|
||||
"download_not_ready": "This download isn't ready.",
|
||||
"edit_unfinished": "Only a finished download can be edited.",
|
||||
"filters_not_object": "The view's filters are malformed.",
|
||||
"account_no_access": "That account no longer has access",
|
||||
"account_signed_out": "That account was signed out elsewhere — sign in again",
|
||||
"account_suspended": "That account is suspended",
|
||||
"address_has_account": "That address already has an account — delete the account instead",
|
||||
"admin_first": "Create the admin account first",
|
||||
"admin_only": "Only an admin can do that",
|
||||
"already_set_up": "This instance is already set up",
|
||||
"already_subscribed": "You're already subscribed to this channel",
|
||||
"channel_blocked": "You've blocked this channel",
|
||||
"collection_fields_required": "A title and an item are both needed",
|
||||
"collection_pending_sync": "The collection changed in Plex, but refreshing the local copy failed — it catches up on the next sync",
|
||||
"collection_read_only": "This collection is read-only",
|
||||
"collection_smart": "Smart and auto-list collections can't be made editable",
|
||||
"demo_activate": "The demo account can't be activated",
|
||||
"demo_delete": "The demo account can't be deleted",
|
||||
"demo_role": "The demo account's role can't be changed",
|
||||
"demo_suspend": "The demo account can't be suspended",
|
||||
"download_not_ready": "This download isn't ready",
|
||||
"edit_unfinished": "Only a finished download can be edited",
|
||||
"filters_not_object": "The view's filters are malformed",
|
||||
"google_no_userinfo": "Google didn't send back the account details. Try signing in again.",
|
||||
"image_fetch_failed": "The image couldn't be loaded.",
|
||||
"image_host": "Images from that address aren't loaded.",
|
||||
"invalid_cursor": "The list position is out of date — reload the page.",
|
||||
"invalid_email": "Enter a valid email address.",
|
||||
"invalid_interval": "The interval must be between {{min}} and {{max}} minutes.",
|
||||
"invalid_link": "That doesn't look like a valid video link.",
|
||||
"invalid_role": "Unknown role.",
|
||||
"invalid_status": "Unknown status.",
|
||||
"invite_decided": "This request has already been decided.",
|
||||
"job_running": "This job is already running.",
|
||||
"last_active_admin": "This is the only active admin — make someone else admin first.",
|
||||
"last_admin": "This is the only admin — make someone else admin first.",
|
||||
"link_gone": "This link is no longer available.",
|
||||
"link_required": "A video link or id is required.",
|
||||
"media_missing": "The media file isn't on disk.",
|
||||
"message_empty": "The message is empty.",
|
||||
"message_rate": "Too many messages — slow down a moment.",
|
||||
"message_self": "You can't message yourself.",
|
||||
"message_too_long": "The message is too long.",
|
||||
"messaging_set_up": "Secure messaging is already set up.",
|
||||
"name_required": "A name is required.",
|
||||
"no_encryption_key": "Set TOKEN_ENCRYPTION_KEY to store secrets in the database.",
|
||||
"no_messaging": "That member hasn't set up messaging yet.",
|
||||
"no_profile": "There's no download profile to use.",
|
||||
"no_subscription_id": "There's no YouTube subscription on record yet — sync your subscriptions first.",
|
||||
"not_authenticated": "You're not signed in.",
|
||||
"not_ready": "Not available yet.",
|
||||
"not_redownloadable": "This item can't be downloaded again.",
|
||||
"not_your_account": "That's not an account you've signed into here.",
|
||||
"only_admin_delete": "You're the only admin — make someone else admin before deleting your account.",
|
||||
"own_delete": "Delete your own account in Settings → Account.",
|
||||
"own_role": "You can't change your own role.",
|
||||
"own_suspend": "You can't suspend your own account.",
|
||||
"plex_disabled": "The Plex module is turned off.",
|
||||
"plex_not_configured": "Plex isn't set up — add the server address and token in Configuration → Plex.",
|
||||
"image_fetch_failed": "Couldn't load the image",
|
||||
"image_host": "Images from that address aren't loaded",
|
||||
"invalid_cursor": "The list position is out of date — reload the page",
|
||||
"invalid_email": "Enter a valid email address",
|
||||
"invalid_interval": "The interval must be between {{min}} and {{max}} minutes",
|
||||
"invalid_link": "That doesn't look like a valid video link",
|
||||
"invalid_role": "Unknown role",
|
||||
"invalid_status": "Unknown status",
|
||||
"invite_decided": "This request has already been decided",
|
||||
"job_running": "This job is already running",
|
||||
"last_active_admin": "This is the only active admin — make someone else admin first",
|
||||
"last_admin": "This is the only admin — make someone else admin first",
|
||||
"link_gone": "This link is no longer available",
|
||||
"link_required": "A video link or id is required",
|
||||
"media_missing": "The media file isn't on disk",
|
||||
"message_empty": "The message is empty",
|
||||
"message_rate": "Too many messages — slow down a moment",
|
||||
"message_self": "You can't message yourself",
|
||||
"message_too_long": "The message is too long",
|
||||
"messaging_set_up": "Secure messaging is already set up",
|
||||
"name_required": "A name is required",
|
||||
"no_encryption_key": "Set TOKEN_ENCRYPTION_KEY to store secrets in the database",
|
||||
"no_messaging": "That member hasn't set up messaging yet",
|
||||
"no_profile": "There's no download profile to use",
|
||||
"no_subscription_id": "There's no YouTube subscription on record yet — sync your subscriptions first",
|
||||
"not_authenticated": "You're not signed in",
|
||||
"not_ready": "Not available yet",
|
||||
"not_redownloadable": "This download can't be downloaded again",
|
||||
"not_your_account": "That's not an account you've signed into here",
|
||||
"only_admin_delete": "You're the only admin — make someone else admin before deleting your account",
|
||||
"own_delete": "Delete your own account in Settings → Account",
|
||||
"own_role": "You can't change your own role",
|
||||
"own_suspend": "You can't suspend your own account",
|
||||
"plex_disabled": "The Plex module is turned off",
|
||||
"plex_not_configured": "Plex isn't set up — add the server address and token in Configuration → Plex",
|
||||
"plex_unreachable": "The Plex server didn't answer. Check the address and the token, then try again.",
|
||||
"plex_write_failed": "Plex didn't accept the change. Try again.",
|
||||
"profile_name_required": "A profile name is required.",
|
||||
"quota_reserve": "Today's shared YouTube budget is down to its reserve — try again later.",
|
||||
"profile_name_required": "A profile name is required",
|
||||
"quota_reserve": "Today's shared YouTube budget is down to its reserve — try again later",
|
||||
"quota_spent": "Today's shared YouTube budget is spent. Try again tomorrow.",
|
||||
"recipient_required": "A recipient email is required.",
|
||||
"registration_closed": "Registration is currently closed.",
|
||||
"recipient_required": "A recipient email is required",
|
||||
"registration_closed": "Registration is currently closed",
|
||||
"search_daily_limit": "You've reached today's YouTube search limit ({{limit}}). Try again tomorrow.",
|
||||
"search_term_required": "Enter something to search for.",
|
||||
"session_gone": "The playback session ended.",
|
||||
"setting_not_a_choice": "Pick one of: {{choices}}.",
|
||||
"setting_not_a_number": "Enter a whole number.",
|
||||
"setting_too_high": "The value can be at most {{max}}.",
|
||||
"setting_too_low": "The value must be at least {{min}}.",
|
||||
"setup_token": "The setup token is missing or wrong.",
|
||||
"share_own": "That's already your download.",
|
||||
"share_password_required": "This link needs a password.",
|
||||
"share_unfinished": "Only a finished download can be shared.",
|
||||
"smtp_failed": "The email couldn't be sent — check the SMTP settings.",
|
||||
"smtp_off": "SMTP isn't configured.",
|
||||
"spec_not_object": "The profile's settings are malformed.",
|
||||
"still_downloading": "This download is still in progress.",
|
||||
"tag_name_taken": "You already have a tag named “{{name}}”.",
|
||||
"title_required": "A title is required.",
|
||||
"search_term_required": "Enter something to search for",
|
||||
"session_gone": "The playback session ended",
|
||||
"setting_not_a_choice": "Pick one of: {{choices}}",
|
||||
"setting_not_a_number": "Enter a whole number",
|
||||
"setting_too_high": "The value can be at most {{max}}",
|
||||
"setting_too_low": "The value must be at least {{min}}",
|
||||
"setup_token": "The setup token is missing or wrong",
|
||||
"share_own": "That's already your download",
|
||||
"share_password_required": "This link needs a password",
|
||||
"share_unfinished": "Only a finished download can be shared",
|
||||
"smtp_failed": "The email couldn't be sent — check the SMTP settings",
|
||||
"smtp_off": "SMTP isn't configured",
|
||||
"spec_not_object": "The profile's settings are malformed",
|
||||
"still_downloading": "This download is still in progress",
|
||||
"tag_name_taken": "You already have a tag named “{{name}}”",
|
||||
"title_required": "A title is required",
|
||||
"too_many_attempts": "Too many attempts. Try again shortly.",
|
||||
"unknown_setting": "Unknown setting: {{key}}",
|
||||
"value_missing": "A value is required.",
|
||||
"video_gone": "This video is no longer available.",
|
||||
"view_name_taken": "You already have a view named “{{name}}”.",
|
||||
"watch_sync_off": "Turn on Plex watch-sync first.",
|
||||
"weak_password": "The password must be at least {{min}} characters.",
|
||||
"wrong_password": "The current password is incorrect.",
|
||||
"wrong_share_password": "Wrong password.",
|
||||
"youtube_editing_off": "Turn on YouTube editing in Settings first.",
|
||||
"youtube_read_scope": "Connect your YouTube account (read access) first.",
|
||||
"value_missing": "A value is required",
|
||||
"video_gone": "This video is no longer available",
|
||||
"view_name_taken": "You already have a view named “{{name}}”",
|
||||
"watch_sync_off": "Turn on Plex watch-sync first",
|
||||
"weak_password": "The password must be at least {{min}} characters",
|
||||
"wrong_password": "The current password is incorrect",
|
||||
"wrong_share_password": "Wrong password",
|
||||
"youtube_editing_off": "Turn on YouTube editing in Settings first",
|
||||
"youtube_read_scope": "Connect your YouTube account (read access) first",
|
||||
"debug_session_conflict": "A recording just started in another tab. Reload the page.",
|
||||
"issue_already_sent": "This report was already sent; its draft can no longer change.",
|
||||
"issue_title_required": "Give the report a title before you send it.",
|
||||
"screenshot_already_sent": "The report was already sent; this session takes no more images.",
|
||||
"screenshot_too_many": "A session can have at most {{limit}} screenshots.",
|
||||
"screenshot_empty": "The image is empty.",
|
||||
"screenshot_not_an_image": "This is not a PNG, JPEG or WebP image.",
|
||||
"issue_already_sent": "This report was already sent; its draft can no longer change",
|
||||
"issue_title_required": "Give the report a title before you send it",
|
||||
"screenshot_already_sent": "The report was already sent; this session takes no more images",
|
||||
"screenshot_too_many": "A session can have at most {{limit}} screenshots",
|
||||
"screenshot_empty": "The image is empty",
|
||||
"screenshot_not_an_image": "This is not a PNG, JPEG or WebP image",
|
||||
"issues_not_configured": "No issue tracker is set up. Download the report as a zip instead.",
|
||||
"issues_unreachable": "The issue tracker isn't answering. Try again later.",
|
||||
"issues_bad_token": "The issue tracker refused the token, or it lacks the permission.",
|
||||
"issues_repo_not_found": "The configured issue repository was not found.",
|
||||
"issues_bad_token": "The issue tracker refused the token, or it lacks the permission",
|
||||
"issues_repo_not_found": "The configured issue repository was not found",
|
||||
"issues_public_repo": "The issue repository is public. Because of the log, reports go only to a private one.",
|
||||
"issues_disabled": "The issue repository has its issue tracker switched off.",
|
||||
"issues_rejected": "The issue tracker refused the request.",
|
||||
"issues_disabled": "The issue repository has its issue tracker switched off",
|
||||
"issues_rejected": "The issue tracker refused the request",
|
||||
"issues_rate_limited": "You can send at most {{limit}} reports an hour. Try again later.",
|
||||
"debug_session_rate_limited": "You can start at most {{limit}} new recordings an hour. Try again later.",
|
||||
"export_rate_limited": "Too many downloads in a row. Wait a minute.",
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
"maxAge_other": "Last {{count}} days"
|
||||
},
|
||||
"loading": "Loading Feed…",
|
||||
"loadError": "Couldn't load the Feed.",
|
||||
"loadError": "Couldn't load the Feed",
|
||||
"emptyTitle": "Your Feed is empty",
|
||||
"emptyBody": "Connect your YouTube account to import your subscriptions and build your Feed.",
|
||||
"setUp": "Set up my Feed",
|
||||
"browseShared": "Or just browse the shared library",
|
||||
"browseShared": "Or just browse the shared Library",
|
||||
"demoEmptyTitle": "Nothing here in the demo",
|
||||
"demoEmptyBody": "The demo account has no subscriptions of its own — but the whole shared library is yours to explore, filter and sort.",
|
||||
"browseLibrary": "Browse the library",
|
||||
"demoEmptyBody": "The demo account has no subscriptions of its own — but the whole shared Library is yours to explore, filter and sort.",
|
||||
"browseLibrary": "Browse the Library",
|
||||
"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.",
|
||||
"channelScopeHint": "This channel isn't in your Feed — it may be hidden. Its videos are in the shared Library.",
|
||||
"viewInLibrary": "View in Library",
|
||||
"sortLabel": "Sort",
|
||||
"viewLabel": "View",
|
||||
@@ -70,7 +70,7 @@
|
||||
"back": "Back to Feed",
|
||||
"searching": "Searching YouTube…",
|
||||
"noResults": "No YouTube videos found.",
|
||||
"error": "YouTube search failed.",
|
||||
"error": "Couldn't search YouTube",
|
||||
"loadMore": "Load more (uses quota)",
|
||||
"loadMoreFree": "Load more",
|
||||
"show": "Show",
|
||||
@@ -78,6 +78,6 @@
|
||||
"clearNow": "Clear now",
|
||||
"cleared_one": "Cleared {{count}} result",
|
||||
"cleared_other": "Cleared {{count}} results",
|
||||
"showHint": "How many results to fetch at a time — Load more pulls another batch of this size."
|
||||
"showHint": "How many results to fetch at a time — “Load more” pulls another batch of this size."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"searchYoutubeTip": "Search live on YouTube for this term (uses shared API quota). You can also press Enter.",
|
||||
"go": "Go",
|
||||
"goTip": "Search (or press Enter)",
|
||||
"channelManager": "Channel manager",
|
||||
"channelManager": "Channels",
|
||||
"usageStats": "Usage & stats",
|
||||
"scheduler": "Scheduler",
|
||||
"configuration": "Configuration",
|
||||
@@ -15,7 +15,7 @@
|
||||
"my": "Mine",
|
||||
"myTip": "Only videos from your own subscriptions",
|
||||
"all": "Library",
|
||||
"allTip": "Every video in the shared catalog"
|
||||
"allTip": "Every video in the shared Library"
|
||||
},
|
||||
"account": {
|
||||
"admin": "Admin",
|
||||
@@ -51,14 +51,14 @@
|
||||
"resume": "Resume sync"
|
||||
},
|
||||
"banner": {
|
||||
"updated": "Updated to v{{version}} — see what's changed.",
|
||||
"updated": "Updated to v{{version}} — see what's changed",
|
||||
"releaseNotes": "Release notes",
|
||||
"dismiss": "Dismiss"
|
||||
},
|
||||
"verify": {
|
||||
"confirmed": "Email address confirmed.",
|
||||
"confirmedNamed": "Email address confirmed — {{email}}.",
|
||||
"invalid": "That verification link is no longer valid — it has expired or was already used.",
|
||||
"confirmed": "Email address confirmed",
|
||||
"confirmedNamed": "Email address confirmed — {{email}}",
|
||||
"invalid": "That verification link is no longer valid — it has expired or was already used",
|
||||
"unreachable": "Couldn't reach the server to confirm that link, so it hasn't been used up. Try opening it again in a moment.",
|
||||
"resetSignedOut": "Password-reset links open in a signed-out window. Sign out here, or open the link in a private window, and it will pick up where it left off.",
|
||||
"reset": "The password this account registered with was cleared for security; set a new one before signing in."
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navLabel": "Home",
|
||||
"title": "What should I watch?",
|
||||
"error": "Could not load your suggestions.",
|
||||
"actionFailed": "Could not save that.",
|
||||
"error": "Couldn't load your suggestions",
|
||||
"actionFailed": "Couldn't save that",
|
||||
"budget": {
|
||||
"ask": "How much time do you have?",
|
||||
"short": "10 minutes",
|
||||
@@ -38,7 +38,7 @@
|
||||
"moveLeft": "Left",
|
||||
"moveRight": "Right",
|
||||
"moveLast": "To the end",
|
||||
"moveFailed": "Could not reorder the shelf.",
|
||||
"moveFailed": "Couldn't reorder the shelf",
|
||||
"moveSaving": "Saving the new order…",
|
||||
"openPlaylist": "Open the playlist"
|
||||
},
|
||||
@@ -46,9 +46,9 @@
|
||||
"menu": "Comfort shelf",
|
||||
"add": "Pin to comfort shelf",
|
||||
"remove": "Remove from shelf",
|
||||
"added": "{{title}} is on your comfort shelf.",
|
||||
"removed": "{{title}} is off your comfort shelf.",
|
||||
"addFailed": "Could not pin it to the shelf.",
|
||||
"removeFailed": "Could not remove it from the shelf."
|
||||
"added": "{{title}} is on your comfort shelf",
|
||||
"removed": "{{title}} is off your comfort shelf",
|
||||
"addFailed": "Couldn't pin it to the shelf",
|
||||
"removeFailed": "Couldn't remove it from the shelf"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
"markRead": "Mark read",
|
||||
"dismiss": "Dismiss",
|
||||
"rowMenu": "Notification actions",
|
||||
"menu": "Notification center actions",
|
||||
"menu": "Notifications actions",
|
||||
"sectionSystem": "System",
|
||||
"sectionActivity": "Activity",
|
||||
"andMore": "and {{count}} more",
|
||||
"openScheduler": "Open Scheduler",
|
||||
"openDownloads": "Open Downloads",
|
||||
"revertFailed": "Couldn't undo that — please try again.",
|
||||
"revertFailed": "Couldn't undo that — please try again",
|
||||
"downloadDone": {
|
||||
"title": "Download ready"
|
||||
},
|
||||
@@ -28,11 +28,11 @@
|
||||
},
|
||||
"jobDone": {
|
||||
"titleOk": "{{job}} finished",
|
||||
"titleError": "{{job}} failed"
|
||||
"titleError": "Couldn't run {{job}}"
|
||||
},
|
||||
"emailUndelivered": {
|
||||
"title": "Email is not going out",
|
||||
"notConfigured": "SMTP is not configured, so nothing this instance sends is being delivered — verification links, password resets and access requests alike. One of them was “{{subject}}”. Set it up under Configuration → Email / SMTP.",
|
||||
"sendFailed": "Sending failed, so messages are not being delivered. One of them was “{{subject}}”. Check Configuration → Email / SMTP and send a test; the server log has the reason."
|
||||
"sendFailed": "Couldn't send email, so messages are not being delivered. One of them was “{{subject}}”. Check Configuration → Email / SMTP and send a test; the server log has the reason."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"minimize": "Minimize",
|
||||
"close": "Close",
|
||||
"conversations": "Conversations",
|
||||
"new": "New message",
|
||||
"new": "New message…",
|
||||
"newTitle": "New message",
|
||||
"empty": "No conversations yet. Start one with the New message button.",
|
||||
"searchPeople": "Search people…",
|
||||
@@ -27,9 +27,9 @@
|
||||
"unlock": "Unlock",
|
||||
"passphrase": "Passphrase",
|
||||
"passphraseConfirm": "Confirm passphrase",
|
||||
"passTooShort": "Use at least 6 characters.",
|
||||
"passMismatch": "The passphrases don't match.",
|
||||
"wrongPass": "Wrong passphrase.",
|
||||
"passTooShort": "Use at least 6 characters",
|
||||
"passMismatch": "The passphrases don't match",
|
||||
"wrongPass": "Wrong passphrase",
|
||||
"setupFailed": "Couldn't set up secure messaging. Please try again.",
|
||||
"forgotPass": "Forgot your passphrase?",
|
||||
"resetTitle": "Reset secure messaging",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"channels": "Channels",
|
||||
"playlists": "Playlists",
|
||||
"plex": "Plex",
|
||||
"notifications": "Alerts",
|
||||
"notifications": "Notifications",
|
||||
"messages": "Messages",
|
||||
"downloads": "Downloads",
|
||||
"stats": "Stats",
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
"findInFeed": "Find in Feed",
|
||||
"unhide": "Unhide",
|
||||
"unwatch": "Unwatch",
|
||||
"openInManager": "Channel manager",
|
||||
"openInManager": "Open in Channels",
|
||||
"openOnYouTube": "Open on YouTube"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"close": "Close",
|
||||
"consent": "Google will show a <0>\"Google hasn't verified this app\"</0> screen — that's expected, because Siftlode hasn't gone through Google's full review. It's safe to continue: click <1>Advanced</1> → <2>Go to Siftlode</2>. You can revoke access anytime from your <3>Google Account</3>.",
|
||||
"consent": "Google will show a <0>“Google hasn't verified this app”</0> screen — that's expected, because Siftlode hasn't gone through Google's full review. It's safe to continue: click <1>Advanced</1> → <2>Go to Siftlode</2>. You can revoke access anytime from your <3>Google Account</3>.",
|
||||
"read": {
|
||||
"title": "Connect your YouTube subscriptions",
|
||||
"body": "You're signed in. To build your Feed, Siftlode needs <0>read-only</0> access to the channels you're subscribed to on YouTube. It never posts, deletes, or changes anything with this.",
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"write": {
|
||||
"title": "You're connected",
|
||||
"importFailed": "Your YouTube account is connected, but importing subscriptions failed — you can retry from Settings → Sync. ",
|
||||
"importFailed": "Your YouTube account is connected, but we couldn't import your subscriptions — you can retry from Settings → Sync. ",
|
||||
"feedReady": "Your Feed is ready{{channels}}. Optionally, let Siftlode ",
|
||||
"feedReadyChannels": " ({{count}} channels)",
|
||||
"rationale": "<0>unsubscribe</0> from channels on YouTube for you — this needs an extra write permission. Skip it to stay read-only; you can always enable it later in Settings.",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"delete": "Delete",
|
||||
"rename": "Rename",
|
||||
"confirmDelete": "Delete the playlist “{{name}}”? This can't be undone.",
|
||||
"addToPlaylist": "Add to playlist",
|
||||
"addToPlaylist": "Add to playlist…",
|
||||
"addToPlaylistTitle": "Add to playlist",
|
||||
"exportToYoutube": "Export to YouTube",
|
||||
"pushToYoutube": "Sync to YouTube",
|
||||
"unsynced": "Unsynced",
|
||||
@@ -29,8 +30,8 @@
|
||||
"revertTitle": "Discard local changes?",
|
||||
"revertMsg": "This reloads the playlist from YouTube and discards your unsynced local changes (order, additions, removals, rename). Continue?",
|
||||
"revertConfirm": "Discard & reload",
|
||||
"revertDone": "“{{name}}” reloaded from YouTube ✓",
|
||||
"revertFailed": "Couldn't reload from YouTube.",
|
||||
"revertDone": "“{{name}}” synced from YouTube",
|
||||
"revertFailed": "Couldn't sync from YouTube",
|
||||
"pushTitle": "Sync to YouTube",
|
||||
"pushConfirm": "Sync",
|
||||
"pushPlanCreate": "Create a new YouTube playlist with {{count}} videos? (~{{units}} quota units; {{left}} left today.)",
|
||||
@@ -50,19 +51,19 @@
|
||||
"mergeQuota": " (~{{units}} quota units; {{left}} left today.)",
|
||||
"mergeQuotaFree": " (~0 quota units.)",
|
||||
"pushNoQuota": "Not enough YouTube quota left today ({{left}}) for this sync (~{{units}} needed). Try again tomorrow.",
|
||||
"pushUpToDate": "“{{name}}” is already in sync with YouTube.",
|
||||
"pushDone": "“{{name}}” synced to YouTube ✓",
|
||||
"pushPartial": "Synced, but {{count}} item(s) were skipped.",
|
||||
"pushFailed": "Couldn't sync to YouTube.",
|
||||
"pushUpToDate": "“{{name}}” is already in sync with YouTube",
|
||||
"pushDone": "“{{name}}” synced to YouTube",
|
||||
"pushPartial": "Synced, but {{count}} video(s) were skipped",
|
||||
"pushFailed": "Couldn't sync to YouTube",
|
||||
"deleteHereOnly": "Delete here only",
|
||||
"deleteYtFailed": "Couldn't delete on YouTube; removed here only.",
|
||||
"deleteYtFailed": "Couldn't delete on YouTube; deleted here only",
|
||||
"sortLabel": "Sort",
|
||||
"dirAsc": "Ascending",
|
||||
"dirDesc": "Descending",
|
||||
"groupByChannel": "Group by channel",
|
||||
"railSortCustom": "Custom order",
|
||||
"railSortName": "Name",
|
||||
"railSortCount": "Item count",
|
||||
"railSortCount": "Video count",
|
||||
"railSortDuration": "Total length",
|
||||
"railSortRecent": "Recently changed",
|
||||
"dirtyFirst": "Unsynced first",
|
||||
@@ -72,27 +73,27 @@
|
||||
"rowMenu": "Video actions",
|
||||
"moveTo": "Move to another list",
|
||||
"moveToRecent": "Move to “{{name}}”",
|
||||
"moveNoTargets": "There is no other playlist to move it to yet.",
|
||||
"movedToast": "“{{title}}” moved to “{{name}}” ✓",
|
||||
"moveFailed": "Couldn't move the video.",
|
||||
"moveNoTargets": "There is no other playlist to move it to yet",
|
||||
"movedToast": "“{{title}}” moved to “{{name}}”",
|
||||
"moveFailed": "Couldn't move the video",
|
||||
"removedToast": "“{{title}}” removed from the playlist",
|
||||
"undoRemoveFailed": "Couldn't put the video back.",
|
||||
"removeFailed": "Couldn't remove the video.",
|
||||
"undoRemoveFailed": "Couldn't put the video back",
|
||||
"removeFailed": "Couldn't remove the video",
|
||||
"errors": {
|
||||
"name_required": "Give the playlist a name.",
|
||||
"youtube_read_scope": "Connect YouTube (read access) to sync your playlists.",
|
||||
"not_on_youtube": "This playlist isn't on YouTube.",
|
||||
"watch_later_not_pushable": "The Watch later list can't be sent to YouTube.",
|
||||
"watch_later_undeletable": "The Watch later list can't be deleted.",
|
||||
"youtube_editing_off": "Enable YouTube editing in Settings first.",
|
||||
"quota_exceeded": "Not enough YouTube quota left today for this sync."
|
||||
"name_required": "Give the playlist a name",
|
||||
"youtube_read_scope": "Connect YouTube (read access) to sync your playlists",
|
||||
"not_on_youtube": "This playlist isn't on YouTube",
|
||||
"watch_later_not_pushable": "The Watch later list can't be sent to YouTube",
|
||||
"watch_later_undeletable": "The Watch later list can't be deleted",
|
||||
"youtube_editing_off": "Enable YouTube editing in Settings first",
|
||||
"quota_exceeded": "Not enough YouTube quota left today for this sync"
|
||||
},
|
||||
"moveErrors": {
|
||||
"not_in_playlist": "That video isn't in this playlist any more — it changed somewhere else.",
|
||||
"same_playlist": "That video is already in this playlist."
|
||||
"not_in_playlist": "That video isn't in this playlist any more — it changed somewhere else",
|
||||
"same_playlist": "That video is already in this playlist"
|
||||
},
|
||||
"railMenu": "Playlist actions",
|
||||
"builtinLocked": "The built-in {{name}} list can’t be renamed or deleted.",
|
||||
"builtinLocked": "The built-in “{{name}}” list can’t be renamed or deleted.",
|
||||
"removeItem": "Remove",
|
||||
"removeItemOf": "Remove “{{title}}” from the playlist",
|
||||
"dragHandleOf": "Reorder “{{title}}”",
|
||||
@@ -102,10 +103,10 @@
|
||||
"saveOrder": "Save this order",
|
||||
"filteredCount": "{{count}} shown",
|
||||
"noneMatchFilter": "No videos match the filter.",
|
||||
"loadFailed": "Couldn't load this playlist.",
|
||||
"reorderFailed": "Couldn't save the new order.",
|
||||
"renameFailed": "Couldn't rename the playlist.",
|
||||
"deleteFailed": "Couldn't delete the playlist.",
|
||||
"loadFailed": "Couldn't load this playlist",
|
||||
"reorderFailed": "Couldn't save the new order",
|
||||
"renameFailed": "Couldn't rename the playlist",
|
||||
"deleteFailed": "Couldn't delete the playlist",
|
||||
"confirmDeleteLinked": "Delete the playlist “{{name}}”? It is synced with YouTube — choose whether to delete it there as well. This can't be undone.",
|
||||
"dragBlockedBuiltin": "This list's order is fixed — it can't be reordered by hand.",
|
||||
"deleteBoth": "Delete here and on YouTube",
|
||||
@@ -114,7 +115,7 @@
|
||||
"viewDiffers": "Temporary view ({{view}}) — the playlist's saved order is unchanged. Save it to keep this order.",
|
||||
"viewMatches": "This view ({{view}}) already matches the playlist's saved order, so there is nothing to save.",
|
||||
"viewFiltered": "Filtered view — {{shown}} of {{total}} shown. The order can't be saved while a filter is on.",
|
||||
"addFailed": "Couldn't add it to the playlist.",
|
||||
"addFailed": "Couldn't add it to the playlist",
|
||||
"limits": {
|
||||
"hint": "Missing a list?",
|
||||
"why": "Why?",
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"playAll": "Play all",
|
||||
"shuffle": "Shuffle",
|
||||
"rename": "Rename",
|
||||
"addItem": "Add title",
|
||||
"addItem": "Add title…",
|
||||
"delete": "Delete",
|
||||
"enableEdit": "Enable editing",
|
||||
"removeItem": "Remove from collection",
|
||||
"removeConfirm": "Remove \"{{title}}\" from this collection? This affects all users on Plex.",
|
||||
"removeConfirm": "Remove “{{title}}” from this collection? This affects all users on Plex.",
|
||||
"nameMaybeTaken": "A collection with this name may already exist in another library — you can still create it here.",
|
||||
"readOnly": "Read-only",
|
||||
"empty": "This collection has no items.",
|
||||
@@ -20,16 +20,17 @@
|
||||
"addNoResults": "No matches.",
|
||||
"add": "Add",
|
||||
"added": "Added",
|
||||
"deleteConfirm": "Delete the collection \"{{title}}\"? This removes it from Plex too.",
|
||||
"editFailed": "Collection edit failed",
|
||||
"playFailed": "Couldn't build the play queue — try again.",
|
||||
"playEmpty": "Nothing to play in this collection yet.",
|
||||
"newCollection": "New collection",
|
||||
"deleteConfirm": "Delete the collection “{{title}}”? This removes it from Plex too.",
|
||||
"editFailed": "Couldn't edit the collection",
|
||||
"playFailed": "Couldn't build the play queue — try again",
|
||||
"playEmpty": "Nothing to play in this collection yet",
|
||||
"newCollection": "New collection…",
|
||||
"newCollectionTitle": "New collection",
|
||||
"namePlaceholder": "Collection name",
|
||||
"nameTaken": "A collection with this name already exists.",
|
||||
"nameTaken": "A collection with this name already exists",
|
||||
"newHint": "Pick the first title to add — a collection needs at least one.",
|
||||
"createWith": "Create",
|
||||
"deleteFailed": "Couldn't delete the collection."
|
||||
"deleteFailed": "Couldn't delete the collection"
|
||||
},
|
||||
"searchPlaceholder": "Search Plex…",
|
||||
"searchField": {
|
||||
@@ -78,13 +79,13 @@
|
||||
"duration": "Length",
|
||||
"durationUnit": "min",
|
||||
"durationOpt": {
|
||||
"short": "Under 90m",
|
||||
"mid": "90–120m",
|
||||
"long": "Over 2h"
|
||||
"short": "Under 90 min",
|
||||
"mid": "90–120 min",
|
||||
"long": "Over 2 hr"
|
||||
},
|
||||
"added": "Added to Plex",
|
||||
"addedOpt": {
|
||||
"24h": "24h",
|
||||
"24h": "24 hours",
|
||||
"1w": "1 week",
|
||||
"1m": "1 month",
|
||||
"6m": "6 months",
|
||||
@@ -128,7 +129,7 @@
|
||||
"markNextWatched": "Mark S{{season}}·E{{episode}} watched",
|
||||
"markNextUnwatched": "Mark S{{season}}·E{{episode}} unwatched",
|
||||
"nextUp": "Next: S{{season}}·E{{episode}}",
|
||||
"watchedFailed": "Couldn't update the watched state — please try again.",
|
||||
"watchedFailed": "Couldn't update the watched state — please try again",
|
||||
"seasons": "{{count}} seasons",
|
||||
"unified": {
|
||||
"titles": "Titles",
|
||||
@@ -157,9 +158,9 @@
|
||||
"none": "No editable collections yet. Create one above.",
|
||||
"count": "{{count}} items",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Delete the collection \"{{title}}\" from Plex? This affects all users.",
|
||||
"deleteConfirm": "Delete the collection “{{title}}” from Plex? This affects all users.",
|
||||
"note": "Changes are written to Plex and shared across all clients and users.",
|
||||
"others": "Manage {{count}} existing collections…",
|
||||
"others": "Manage {{count}} existing collections",
|
||||
"makeEditable": "Manage"
|
||||
},
|
||||
"player": {
|
||||
@@ -174,8 +175,8 @@
|
||||
"restart": "Restart from beginning",
|
||||
"prev": "Previous episode",
|
||||
"next": "Next episode",
|
||||
"tapBack": "−{{n}} s",
|
||||
"tapForward": "+{{n}} s",
|
||||
"tapBack": "−{{d}}",
|
||||
"tapForward": "+{{d}}",
|
||||
"mute": "Mute (m)",
|
||||
"volume": "Volume",
|
||||
"fullscreen": "Fullscreen (f)",
|
||||
@@ -222,9 +223,9 @@
|
||||
"subStyle": "Subtitles",
|
||||
"subPreparing": "Preparing subtitles: {{label}}…",
|
||||
"subUnavailable": {
|
||||
"bitmap": "This subtitle is image-based and can't be shown as text.",
|
||||
"missing": "This subtitle track isn't available.",
|
||||
"failed": "This subtitle couldn't be loaded."
|
||||
"bitmap": "This subtitle is image-based and can't be shown as text",
|
||||
"missing": "This subtitle track isn't available",
|
||||
"failed": "Couldn't load this subtitle"
|
||||
},
|
||||
"subSize": "Text size",
|
||||
"subPos": "Position",
|
||||
@@ -274,7 +275,7 @@
|
||||
"audience": "Audience",
|
||||
"keywords": "Keywords",
|
||||
"crew": "Crew",
|
||||
"filterValue": "Filter by \"{{name}}\"",
|
||||
"filterValue": "Filter by “{{name}}”",
|
||||
"facts": {
|
||||
"title": "Details",
|
||||
"release": "Released",
|
||||
@@ -294,23 +295,23 @@
|
||||
"transcode_sw": "Transcoded in software (heaviest)"
|
||||
},
|
||||
"playlistAdd": {
|
||||
"manage": "Add to playlist",
|
||||
"manage": "Add to playlist…",
|
||||
"title": "Add to playlist — {{title}}",
|
||||
"newPlaceholder": "New playlist name…",
|
||||
"create": "Create",
|
||||
"none": "No playlists yet. Create one above.",
|
||||
"count": "{{count}} items",
|
||||
"groupProgress": "{{in}} / {{size}}",
|
||||
"addFailed": "Couldn't add it to the playlist.",
|
||||
"removeFailed": "Couldn't remove it from the playlist.",
|
||||
"createFailed": "Couldn't create the playlist."
|
||||
"addFailed": "Couldn't add it to the playlist",
|
||||
"removeFailed": "Couldn't remove it from the playlist",
|
||||
"createFailed": "Couldn't create the playlist"
|
||||
},
|
||||
"playlist": {
|
||||
"section": "Playlists",
|
||||
"pickerPlaceholder": "Find or create a playlist…",
|
||||
"createNamed": "Create \"{{name}}\"",
|
||||
"createNamed": "Create “{{name}}”",
|
||||
"none": "No playlists yet.",
|
||||
"deleteConfirm": "Delete the playlist \"{{title}}\"?",
|
||||
"deleteConfirm": "Delete the playlist “{{title}}”?",
|
||||
"rename": "Rename",
|
||||
"playAll": "Play all",
|
||||
"delete": "Delete playlist",
|
||||
@@ -326,10 +327,10 @@
|
||||
"addShow": "Add whole show to a playlist",
|
||||
"addSeason": "Add season to a playlist",
|
||||
"addEpisode": "Add episode to a playlist",
|
||||
"removeFailed": "Couldn't remove it from the playlist.",
|
||||
"renameFailed": "Couldn't rename the playlist.",
|
||||
"deleteFailed": "Couldn't delete the playlist.",
|
||||
"reorderFailed": "Couldn't save the new order.",
|
||||
"removeFailed": "Couldn't remove it from the playlist",
|
||||
"renameFailed": "Couldn't rename the playlist",
|
||||
"deleteFailed": "Couldn't delete the playlist",
|
||||
"reorderFailed": "Couldn't save the new order",
|
||||
"reorderItem": "Move “{{title}}”",
|
||||
"rowMenu": "Item actions",
|
||||
"menu": "Playlist actions",
|
||||
@@ -337,7 +338,7 @@
|
||||
"playSeason": "Play the season",
|
||||
"removeFromPlaylist": "Remove from the playlist",
|
||||
"removedToast": "“{{title}}” removed from the playlist",
|
||||
"undoRemoveFailed": "Couldn't put it back in the playlist."
|
||||
"undoRemoveFailed": "Couldn't put it back in the playlist"
|
||||
},
|
||||
"people": {
|
||||
"count_one": "{{count}} title",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"subscriptions_pull": "Read subscriptions",
|
||||
"subscriptions_pull": "Sync subscriptions",
|
||||
"subscriptions_resync": "Auto subscription resync",
|
||||
"playlists_pull": "Read playlists",
|
||||
"playlists_pull": "Sync playlists",
|
||||
"playlists_push": "Push playlists",
|
||||
"playlists_delete": "Delete playlist",
|
||||
"videos_backfill_recent": "Recent backfill",
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"shotFailed": "Couldn't save the screenshot",
|
||||
"stopFailed": "Couldn't stop recording",
|
||||
"stopped": "Recording stopped",
|
||||
"expired_one": "The recording reached its {{count}}-minute limit and stopped.",
|
||||
"expired_other": "The recording reached its {{count}}-minute limit and stopped."
|
||||
"expired_one": "The recording reached its {{count}}-minute limit and stopped",
|
||||
"expired_other": "The recording reached its {{count}}-minute limit and stopped"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Debug session",
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"composer": {
|
||||
"title": "Bug report",
|
||||
"meta": "{{start}} · {{minutes}} min · {{level}}",
|
||||
"meta": "{{start}} · {{minutes}} · {{level}}",
|
||||
"fieldTitle": "Title",
|
||||
"titlePlaceholder": "What went wrong?",
|
||||
"description": "Description",
|
||||
@@ -61,7 +61,7 @@
|
||||
"stepPlaceholder": "What did you do?",
|
||||
"withShot": "image",
|
||||
"addStep": "Add a step",
|
||||
"removeStep": "Remove step {{n}}",
|
||||
"removeStep": "Delete step {{n}}",
|
||||
"environment": "Environment — automatic",
|
||||
"attachments": "Attachments — what is sent, each one removable",
|
||||
"clientLog": "Browser log",
|
||||
@@ -75,7 +75,7 @@
|
||||
"reviewDone": "Done",
|
||||
"keepRow": "Keep line: {{event}} {{msg}}",
|
||||
"screenshot": "Screenshot {{n}}",
|
||||
"removeShot": "Remove screenshot {{n}}",
|
||||
"removeShot": "Delete screenshot {{n}}",
|
||||
"drop": "Paste an image with Ctrl+V, drop it here, or",
|
||||
"pick": "choose a file",
|
||||
"close": "Close",
|
||||
@@ -85,7 +85,7 @@
|
||||
"saveFailed": "Couldn't save the draft",
|
||||
"loadFailed": "Couldn't load the session",
|
||||
"truncated": "The logs carry the first {{kept}} of {{total}} lines.",
|
||||
"sent": "Sent as an issue.",
|
||||
"sent": "Sent as an issue",
|
||||
"openIssue": "Open the issue",
|
||||
"env": {
|
||||
"version": "Version",
|
||||
@@ -99,7 +99,7 @@
|
||||
"sentOk": "Issue created",
|
||||
"sendFailed": "Couldn't send the report",
|
||||
"target": "To: {{repo}} · private",
|
||||
"partial": "The issue was created, but not every attachment went: {{files}}.",
|
||||
"partial": "The issue was created, but not every attachment went: {{files}}",
|
||||
"retry": "Send the missing ones",
|
||||
"edit": "Edit",
|
||||
"preview": "Preview",
|
||||
@@ -107,8 +107,8 @@
|
||||
"previewTitle": "Preview of the report",
|
||||
"previewLocal": "Without an issue tracker, this is the zip's issue.md.",
|
||||
"shotsFailed": "Couldn't load the screenshots",
|
||||
"removeShotFailed": "Couldn't remove the screenshot",
|
||||
"partialImages": "The issue was created, but its text does not show the screenshots yet.",
|
||||
"removeShotFailed": "Couldn't delete the screenshot",
|
||||
"partialImages": "The issue was created, but its text does not show the screenshots yet",
|
||||
"retryImages": "Show them in the issue",
|
||||
"sendingNow": "The report is being sent…",
|
||||
"exportFailed": "Couldn't build the zip"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"quotaRemaining": "{{formatted}} left",
|
||||
"quotaNote": "Shared YouTube Data API budget; resets at midnight US Pacific. Backfill yields to leave headroom for interactive use.",
|
||||
"minutes": "min",
|
||||
"range": "Must be between {{min}} and {{max}}.",
|
||||
"range": "Must be between {{min}} and {{max}}",
|
||||
"editInterval": "Click to change how often this runs",
|
||||
"intervalFailed": "Couldn't change the interval",
|
||||
"runNow": "Run now",
|
||||
@@ -85,8 +85,8 @@
|
||||
"backfill": "Backfill (recent + full history)",
|
||||
"autotag": "Auto-tagging",
|
||||
"shorts": "Shorts classification",
|
||||
"subscriptions": "Subscription resync",
|
||||
"playlist_sync": "YouTube playlist sync",
|
||||
"subscriptions": "Subscription sync from YouTube",
|
||||
"playlist_sync": "Playlist sync from YouTube",
|
||||
"maintenance": "Maintenance + validation",
|
||||
"demo_reset": "Demo account reset",
|
||||
"explore_cleanup": "Explored-channel cleanup",
|
||||
@@ -111,7 +111,7 @@
|
||||
"liveRefreshHint": "Live/upcoming videos (and just-ended streams without a duration yet) re-checked until they settle."
|
||||
},
|
||||
"purgeDiscovery": "Purge discovery",
|
||||
"purgeDiscoveryHint": "Remove all un-kept discovery content now (search results nobody watched/saved, and explored-but-unsubscribed channels) — ignoring the grace period.",
|
||||
"purgeDiscoveryHint": "Delete all un-kept discovery content now (search results nobody watched/saved, and explored-but-unsubscribed channels) — ignoring the grace period.",
|
||||
"purgeConfirmTitle": "Purge discovery content?",
|
||||
"purgedDiscovery": "Removed {{count}} discovery items"
|
||||
"purgedDiscovery": "Deleted {{count}} discovery items"
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"sound": "Sound on alerts",
|
||||
"sound": "Sound on notifications",
|
||||
"soundHint": "Play a short tone when a notification needs your attention (e.g. errors, prompts).",
|
||||
"autoDismiss": "Auto-dismiss toasts",
|
||||
"autoDismissHint": "When off, pop-up toasts stay until you close them. When on, they fade after the set time; one with a button (Undo) stays at least 8 seconds.",
|
||||
@@ -65,7 +65,7 @@
|
||||
"writeEnableHint": "Lets Siftlode unsubscribe from channels on YouTube for you. Optional — skip it to stay read-only.",
|
||||
"granted": "Granted",
|
||||
"enable": "Enable",
|
||||
"enableHint": "Redirects to Google to grant the permission. Google shows an 'unverified app' notice — that's expected; click Advanced → Go to Siftlode.",
|
||||
"enableHint": "Redirects to Google to grant the permission. Google shows an “unverified app” notice — that's expected; click Advanced → Go to Siftlode.",
|
||||
"walkMeThrough": "Walk me through setup",
|
||||
"dangerZone": "Danger zone",
|
||||
"deleteHint": "Permanently delete your account and all your data — subscriptions, tags, watch history, playlists and settings. This can't be undone.",
|
||||
@@ -73,7 +73,7 @@
|
||||
"deleteTitle": "Delete your account?",
|
||||
"deleteConfirm": "This permanently erases your account and all your data (subscriptions, tags, watch/saved/hidden state, playlists, settings). It can't be undone.",
|
||||
"deleteConfirmButton": "Delete everything",
|
||||
"demoNotice": "This is the shared demo account. It has no YouTube connection — you can browse the whole shared library, filter and sort it, build playlists and try things out, but nothing touches a real YouTube account.",
|
||||
"demoNotice": "This is the shared demo account. It has no YouTube connection — you can browse the whole shared Library, filter and sort it, build playlists and try things out, but nothing touches a real YouTube account.",
|
||||
"signInMethods": "Sign-in methods",
|
||||
"googleLink": {
|
||||
"title": "Google account",
|
||||
@@ -81,13 +81,13 @@
|
||||
"connectedHint": "Your Google account is linked. You can sign in with Google and grant YouTube access below.",
|
||||
"connectHint": "Link a Google account to sign in with Google and to enable YouTube access for your Feed.",
|
||||
"connect": "Connect Google",
|
||||
"linked": "Google account linked.",
|
||||
"conflict": "That Google account is already linked to a different Siftlode account.",
|
||||
"linked": "Google account linked",
|
||||
"conflict": "That Google account is already linked to a different Siftlode account",
|
||||
"mismatch": "That's a different Google account than the one already linked here. Sign in with the linked one.",
|
||||
"error": "Couldn't link the Google account. Please try again.",
|
||||
"signedInAs": "Signed in with Google as"
|
||||
},
|
||||
"googleCancelled": "Google sign-in was cancelled.",
|
||||
"googleCancelled": "Google sign-in was cancelled",
|
||||
"password": {
|
||||
"title": "Password",
|
||||
"setHint": "A password is set. You can sign in with your email and password.",
|
||||
@@ -98,8 +98,8 @@
|
||||
"new": "New password (min. 10 characters)",
|
||||
"save": "Save password",
|
||||
"saving": "Saving…",
|
||||
"saved": "Password updated for {{email}}.",
|
||||
"failed": "Couldn't update the password."
|
||||
"saved": "Password updated for {{email}}",
|
||||
"failed": "Couldn't update the password"
|
||||
},
|
||||
"sessions": {
|
||||
"title": "Active sessions",
|
||||
@@ -107,7 +107,7 @@
|
||||
"action": "Log out other sessions",
|
||||
"confirmTitle": "Log out other sessions?",
|
||||
"confirmBody": "This signs you out of every other browser and device. You'll stay signed in here.",
|
||||
"done": "Signed out of all other sessions.",
|
||||
"done": "Signed out of all other sessions",
|
||||
"failed": "Couldn't sign out other sessions. Please try again."
|
||||
}
|
||||
},
|
||||
@@ -116,21 +116,21 @@
|
||||
"intro": "Some sites (e.g. a private Facebook group you belong to) only let logged-in members view a video. Paste your browser cookies for a site here and your downloads from it will use your login. Cookies are encrypted, kept private to your account, and only ever sent to the matching site.",
|
||||
"configured": "Set · updated {{when}}",
|
||||
"notSet": "Not set",
|
||||
"set": "Set cookies",
|
||||
"update": "Update",
|
||||
"remove": "Remove",
|
||||
"set": "Set cookies…",
|
||||
"update": "Update…",
|
||||
"remove": "Delete",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"saving": "Saving…",
|
||||
"placeholder": "Paste the contents of your cookies.txt (Netscape format) here",
|
||||
"help": "Export a cookies.txt for this site with a browser extension, then paste it here. It's write-only — we never show it back.",
|
||||
"saved": "Cookies saved.",
|
||||
"removed": "Cookies removed.",
|
||||
"failed": "Couldn't save those cookies.",
|
||||
"removeConfirmTitle": "Remove cookies?",
|
||||
"saved": "Cookies saved",
|
||||
"removed": "Cookies deleted",
|
||||
"failed": "Couldn't save those cookies",
|
||||
"removeConfirmTitle": "Delete cookies?",
|
||||
"removeConfirmBody": "Downloads from this site will stop using your login.",
|
||||
"removeConfirmAction": "Remove",
|
||||
"loadError": "Couldn't load your download authentication settings."
|
||||
"removeConfirmAction": "Delete",
|
||||
"loadError": "Couldn't load your download authentication settings"
|
||||
},
|
||||
"demo": {
|
||||
"title": "Demo access",
|
||||
@@ -160,14 +160,14 @@
|
||||
"add": "Add",
|
||||
"decided": "{{count}} decided",
|
||||
"approved": "Approved {{email}} — they can sign in now",
|
||||
"approveFailed": "Approve failed",
|
||||
"denyFailed": "Deny failed",
|
||||
"approveFailed": "Couldn't approve the request",
|
||||
"denyFailed": "Couldn't deny the request",
|
||||
"addedToWhitelist": "{{email}} added to the whitelist",
|
||||
"inviteHint": "Send them an invitation. Adding an address never mails anyone — this does.",
|
||||
"invitedAt": "Invited {{time}}. Sending again will mail them a second time.",
|
||||
"inviteLabel": "Send an invitation to {{email}}",
|
||||
"invited": "Invitation sent to {{email}}.",
|
||||
"inviteFailed": "Couldn't send that invitation.",
|
||||
"invited": "Invitation sent to {{email}}",
|
||||
"inviteFailed": "Couldn't send that invitation",
|
||||
"inviteTitle": "Send invitation?",
|
||||
"inviteConfirm": "Email {{email}} to say their address is whitelisted and Siftlode is open for them.",
|
||||
"inviteConfirmAgain": "You already invited {{email}} on {{time}}. Sending now writes to them a second time.",
|
||||
@@ -188,11 +188,11 @@
|
||||
"deleteTitle": "Delete whitelist entry?",
|
||||
"deleteConfirm": "Forget {{email}}? Siftlode drops the record, so anyone at that address would have to ask again.",
|
||||
"deleteHasAccount": "Someone has registered on this address — manage it under Users & roles, where deleting the account removes this entry too.",
|
||||
"deleted": "Whitelist entry removed.",
|
||||
"deleteFailed": "Could not remove the entry.",
|
||||
"emailOff": "Email sending is not configured, so nobody gets a verification link and no request reaches you by email. Watch this list and your inbox instead, and fix it under Configuration → Email / SMTP.",
|
||||
"deletedStillAllowed": "Entry removed, but this address is still allow-listed by the server's configuration, so it keeps access. Remove it from ALLOWED_EMAILS / ADMIN_EMAILS to change that.",
|
||||
"deleteConfirmStillAllowed": "Forget {{email}}? Careful: this address is also allow-listed in the server's configuration (ALLOWED_EMAILS / ADMIN_EMAILS), so removing this row will NOT take its access away — and an ADMIN_EMAILS address is made an admin again on its next Google sign-in."
|
||||
"deleted": "Whitelist entry deleted",
|
||||
"deleteFailed": "Couldn't delete the entry",
|
||||
"emailOff": "Email sending is not configured, so nobody gets a verification link and no request reaches you by email. Watch this list and your Notifications instead, and fix it under Configuration → Email / SMTP.",
|
||||
"deletedStillAllowed": "Entry deleted, but this address is still allow-listed by the server's configuration, so it keeps access. Remove it from ALLOWED_EMAILS / ADMIN_EMAILS to change that.",
|
||||
"deleteConfirmStillAllowed": "Forget {{email}}? Careful: this address is also allow-listed in the server's configuration (ALLOWED_EMAILS / ADMIN_EMAILS), so deleting this row will NOT take its access away — and an ADMIN_EMAILS address is made an admin again on its next Google sign-in."
|
||||
},
|
||||
"diagnostics": {
|
||||
"title": "Logging level",
|
||||
@@ -215,15 +215,13 @@
|
||||
"server_one": "The Siftlode server receives this too and keeps it for {{count}} day, so the problem can be tracked down.",
|
||||
"server_other": "The Siftlode server receives this too and keeps it for {{count}} days, so the problem can be tracked down.",
|
||||
"never": "Passwords, tokens, email addresses and the content of your messages are never included.",
|
||||
"lapse": "After 24 hours it goes back to Basic by itself.",
|
||||
"lapse": "After 24 hours it goes back to “Basic” by itself.",
|
||||
"policy": "Privacy policy",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Turn on for 24 hours"
|
||||
},
|
||||
"status": "{{level}} logging · back to Basic in {{time}}",
|
||||
"hoursMinutes": "{{h}} h {{m}} min",
|
||||
"minutes": "{{m}} min",
|
||||
"revert": "Back to Basic now",
|
||||
"status": "{{level}} logging · back to “Basic” in {{time}}",
|
||||
"revert": "Back to “Basic” now",
|
||||
"demo": "The demo account is shared by several people, so detailed logging can't be turned on here.",
|
||||
"failed": "Couldn't change the logging level"
|
||||
},
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
"desc": "This is the first account and an administrator. You'll sign in with this email and password once setup is done.",
|
||||
"email": "Email address",
|
||||
"password": "Password (min. {{n}} characters)",
|
||||
"invalidEmail": "Enter a valid email address.",
|
||||
"shortPassword": "Password must be at least {{n}} characters."
|
||||
"invalidEmail": "Enter a valid email address",
|
||||
"shortPassword": "Password must be at least {{n}} characters"
|
||||
},
|
||||
"google": {
|
||||
"title": "Sign in with Google (optional)",
|
||||
"desc": "Paste your Google OAuth client ID and secret to enable Google sign-in and YouTube access. Leave both empty to skip — email+password works without it.",
|
||||
"clientId": "Google client ID",
|
||||
"clientSecret": "Google client secret",
|
||||
"bothRequired": "Enter both the client ID and secret, or leave both empty to skip.",
|
||||
"bothRequired": "Enter both the client ID and secret, or leave both empty to skip",
|
||||
"skipHint": "Leave empty to set this up later from the admin Configuration page."
|
||||
},
|
||||
"smtp": {
|
||||
@@ -29,8 +29,8 @@
|
||||
"password": "SMTP password",
|
||||
"testTo": "Send a test email to…",
|
||||
"test": "Send test",
|
||||
"testOk": "Test email sent — check the inbox.",
|
||||
"testFail": "Couldn't send — check the settings.",
|
||||
"testOk": "Test email sent — check the inbox",
|
||||
"testFail": "Couldn't send — check the settings",
|
||||
"skipHint": "Leave empty to set this up later from the admin Configuration page."
|
||||
},
|
||||
"finish": {
|
||||
@@ -46,5 +46,5 @@
|
||||
"title": "Setup link needed",
|
||||
"body": "Open the setup wizard using the link printed in the container logs at first start (it looks like …/setup?token=…). It's required so only the operator can configure this instance."
|
||||
},
|
||||
"genericError": "Something went wrong — please try again."
|
||||
"genericError": "Something went wrong — please try again"
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"year": "Year"
|
||||
},
|
||||
"year": {
|
||||
"more": "More…",
|
||||
"more": "More",
|
||||
"moreLabel": "All years",
|
||||
"none": "No years yet",
|
||||
"sortBy": "Sort",
|
||||
@@ -54,11 +54,11 @@
|
||||
"content": {
|
||||
"normal": "Normal",
|
||||
"shorts": "Shorts",
|
||||
"live": "Live / Upcoming"
|
||||
"live": "Live / upcoming"
|
||||
},
|
||||
"datePresetLabel": "Upload date",
|
||||
"datePreset": {
|
||||
"24h": "24h",
|
||||
"24h": "24 hours",
|
||||
"1week": "1 week",
|
||||
"2weeks": "2 weeks",
|
||||
"1month": "1 month",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"system": "System"
|
||||
},
|
||||
"title": "API quota usage",
|
||||
"rangeDays": "{{count}}d",
|
||||
"rangeDays": "{{d}}",
|
||||
"rangeLabel": "Time range",
|
||||
"introBefore": "YouTube Data API units attributed by who triggered the spend. ",
|
||||
"introSystem": "System",
|
||||
"introAfter": " is shared background work (scheduled backfill, enrichment, resync) that isn't tied to one person.",
|
||||
"loading": "Loading…",
|
||||
"noData": "No data.",
|
||||
"dailyTotal": "Daily total ({{count}}d · {{units}} units)",
|
||||
"dailyTotal": "Daily total ({{d}} · {{units}} units)",
|
||||
"noUsageInRange": "No usage in this range.",
|
||||
"dailyTooltip": "{{day}}: {{units}} units",
|
||||
"system": "System (background)",
|
||||
@@ -36,15 +36,15 @@
|
||||
"last7d": "Last 7 days",
|
||||
"last30d": "Last 30 days",
|
||||
"allTime": "All time",
|
||||
"byAction": "By action (30d)",
|
||||
"byAction": "By action (30 days)",
|
||||
"noUsage": "No usage yet.",
|
||||
"actions": "Actions",
|
||||
"syncSubscriptions": "Sync subscriptions",
|
||||
"syncSubscriptions": "Sync subscriptions from YouTube",
|
||||
"syncSubscriptionsHint": "Re-import your YouTube subscriptions and pull in any newly-followed channels.",
|
||||
"backfillEverything": "Backfill everything",
|
||||
"backfillEverythingHint": "Request full back-catalog backfill for every channel you're subscribed to. Older videos + complete search fill in as the shared daily quota allows.",
|
||||
"synced": "Synced {{count}} subscriptions",
|
||||
"syncFailed": "Sync failed",
|
||||
"syncFailed": "Couldn't sync subscriptions",
|
||||
"fullHistoryRequested": "Full history requested for {{count}} channels",
|
||||
"fullHistoryFailed": "Couldn't request full history",
|
||||
"admin": "Admin",
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"empty": "No tags yet — add one below.",
|
||||
"newPlaceholder": "New tag name…",
|
||||
"add": "Add",
|
||||
"renameFailed": "Couldn't rename the tag.",
|
||||
"deleteFailed": "Couldn't delete the tag."
|
||||
"renameFailed": "Couldn't rename the tag",
|
||||
"deleteFailed": "Couldn't delete the tag"
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
"inactive": "waiting for activation",
|
||||
"activate": "Activate",
|
||||
"activateHint": "Let this account in. Their address is already approved — this only finishes it.",
|
||||
"activated": "{{email}} can sign in now.",
|
||||
"activateFailed": "Couldn't activate that account.",
|
||||
"activated": "{{email}} can sign in now",
|
||||
"activateFailed": "Couldn't activate that account",
|
||||
"resend": "Send the confirmation link again",
|
||||
"resendHint": "Send the confirmation link again, to the address on the account.",
|
||||
"resent": "Confirmation link sent to {{email}}.",
|
||||
"resendFailed": "Couldn't send the confirmation link."
|
||||
"resent": "Confirmation link sent to {{email}}",
|
||||
"resendFailed": "Couldn't send the confirmation link"
|
||||
},
|
||||
"roles": {
|
||||
"title": "Users & roles",
|
||||
@@ -42,7 +42,7 @@
|
||||
"demoteTitle": "Remove admin?",
|
||||
"promoteConfirm": "Give {{email}} full admin access (settings, scheduler, user management)?",
|
||||
"demoteConfirm": "Remove admin access from {{email}}? They'll become a regular user.",
|
||||
"failed": "Couldn't change the role."
|
||||
"failed": "Couldn't change the role"
|
||||
},
|
||||
"suspend": {
|
||||
"action": "Suspend",
|
||||
@@ -55,7 +55,7 @@
|
||||
"undone": "{{email}} unsuspended",
|
||||
"demoLocked": "The demo account can't be suspended.",
|
||||
"selfLocked": "You can't suspend your own account.",
|
||||
"failed": "Couldn't change the suspension."
|
||||
"failed": "Couldn't change the suspension"
|
||||
},
|
||||
"delete": {
|
||||
"action": "Delete",
|
||||
@@ -63,7 +63,7 @@
|
||||
"title": "Delete account?",
|
||||
"confirm": "Permanently delete {{email}} and all their data — subscriptions, tags, watch history, playlists and settings? This can't be undone.",
|
||||
"done": "{{email}} deleted",
|
||||
"failed": "Couldn't delete the account."
|
||||
"failed": "Couldn't delete the account"
|
||||
},
|
||||
"account": {
|
||||
"access": "Access",
|
||||
@@ -75,7 +75,7 @@
|
||||
"approvalWaitingOnYou": "waiting on you",
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
"copyFailed": "Couldn't copy — select the link and copy it by hand.",
|
||||
"copyFailed": "Couldn't copy — select the link and copy it by hand",
|
||||
"email": "Email",
|
||||
"emailConfirmed": "confirmed",
|
||||
"emailUnconfirmed": "not confirmed",
|
||||
@@ -97,7 +97,7 @@
|
||||
"passwordSet": "set",
|
||||
"recovery": "Recovery",
|
||||
"resend": "Send a confirmation link",
|
||||
"resendSent": "Confirmation link sent.",
|
||||
"resendSent": "Confirmation link sent",
|
||||
"setPassword": "Create a set-password link",
|
||||
"setPasswordAgain": "Create another link",
|
||||
"linkFailed": "Couldn't create the link. Please try again."
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Delete the saved view “{{name}}”?",
|
||||
"reorder": "Drag to reorder",
|
||||
"actionFailed": "Couldn't update your saved views — please try again.",
|
||||
"reorderFailed": "Couldn't save the new order — please try again.",
|
||||
"actionFailed": "Couldn't update your saved views — please try again",
|
||||
"reorderFailed": "Couldn't save the new order — please try again",
|
||||
"saved": "Saved views",
|
||||
"manage": "Manage views…",
|
||||
"manageTitle": "Personal views",
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
},
|
||||
"preview": {
|
||||
"feed": "The Feed — filter by channel, language, topic, length and date.",
|
||||
"channels": "Channel manager",
|
||||
"channels": "Channels",
|
||||
"playlists": "Playlists",
|
||||
"player": "In-app player",
|
||||
"downloads": "Download Center",
|
||||
"downloads": "Downloads",
|
||||
"search": "Search results",
|
||||
"plex": "Your Plex library"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
"body": "A built-in player picks each video up where you left off, with keyboard and scroll controls."
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Download Center",
|
||||
"title": "Downloads",
|
||||
"body": "Save videos to your server in a Plex-friendly layout, trim or join them, then share a link."
|
||||
},
|
||||
"plex": {
|
||||
@@ -61,8 +61,8 @@
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"newPassword": "New password (min {{min}} characters)",
|
||||
"invalidEmail": "Enter a valid email address.",
|
||||
"weakPassword": "Password must be at least {{min}} characters.",
|
||||
"invalidEmail": "Enter a valid email address",
|
||||
"weakPassword": "Password must be at least {{min}} characters",
|
||||
"genericError": "Something went wrong. Please try again.",
|
||||
"working": "Please wait…",
|
||||
"signinButton": "Sign in",
|
||||
@@ -84,17 +84,17 @@
|
||||
"demoDenied": "That email isn't enabled for the demo. Ask the admin to add it.",
|
||||
"registerDone": "Almost there — check your email for a confirmation link, and your spam folder if it isn't there in a minute. After you confirm it, an admin approves your account before you can sign in.",
|
||||
"forgotDone": "If that email has an account, we've sent a reset link. Check your inbox, and your spam folder if it hasn't arrived in a minute.",
|
||||
"resetDone": "Your password has been set — you can sign in now.",
|
||||
"resetDone": "Your password has been set — you can sign in now",
|
||||
"deadLink": {
|
||||
"title": "This link is no longer valid",
|
||||
"body": "A password reset link lasts one hour and works once. This one has expired, has already been used, or belongs to an account that no longer exists — so there is nothing to set a password on. Ask for a fresh one and we'll email it.",
|
||||
"short": "This reset link is no longer valid.",
|
||||
"short": "This reset link is no longer valid",
|
||||
"cta": "Request a new link"
|
||||
},
|
||||
"verifyOk": "Email confirmed. Once an admin approves your account, you can sign in.",
|
||||
"verifyOkReset": "Email confirmed. The password this account registered with had been cleared for security — choose a new one to sign in.",
|
||||
"verifyOkResetCta": "Set a new password",
|
||||
"verifyInvalid": "That verification link is invalid or has expired.",
|
||||
"verifyInvalid": "That verification link is invalid or has expired",
|
||||
"verifyUnreachable": "Couldn't reach the server to confirm that link, so it hasn't been used up. Try opening it again in a moment.",
|
||||
"resendCta": "Send a new link",
|
||||
"resendDone": "If that email has an unconfirmed account, we've sent a new confirmation link. Check your inbox, and your spam folder if it hasn't arrived in a minute.",
|
||||
@@ -116,26 +116,26 @@
|
||||
"email_unverified": {
|
||||
"title": "Confirm your email address",
|
||||
"body": "Your password is correct — we just can't let you in until the address on this account is confirmed. Open the link we emailed you, or ask for a new one.",
|
||||
"short": "Confirm your email address first — check your inbox."
|
||||
"short": "Confirm your email address first — check your inbox"
|
||||
},
|
||||
"pending_approval": {
|
||||
"title": "Waiting for approval",
|
||||
"body": "Your password is correct and your address is confirmed. An admin still has to let you in — we'll email you as soon as they decide.",
|
||||
"short": "Your account is waiting for admin approval."
|
||||
"short": "Your account is waiting for admin approval"
|
||||
},
|
||||
"suspended": {
|
||||
"title": "Account suspended",
|
||||
"body": "This account has been suspended, so it can't sign in by any method.",
|
||||
"short": "This account has been suspended."
|
||||
"short": "This account has been suspended"
|
||||
},
|
||||
"invalid": {
|
||||
"short": "Invalid email or password."
|
||||
"short": "Invalid email or password"
|
||||
},
|
||||
"rate_limited": {
|
||||
"short": "Too many attempts. Try again shortly."
|
||||
}
|
||||
},
|
||||
"accessDenied": "That account isn't approved for this instance yet — register below or ask the admin for access.",
|
||||
"accessDenied": "That account isn't approved for this instance yet — register below or ask the admin for access",
|
||||
"suspended": "This account has been suspended and can't sign in. If you think this is a mistake, contact the operator (check your email for details).",
|
||||
"deleted": "Your account and all its data were permanently deleted. We've emailed you a confirmation."
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"fixes": "Javítások",
|
||||
"olderVersions_one": "{{count}} korábbi verzió",
|
||||
"olderVersions_other": "{{count}} korábbi verzió",
|
||||
"notesError": "A verziójegyzéket nem sikerült betölteni."
|
||||
"notesError": "Nem sikerült betölteni a verziójegyzéket"
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"showRequest": "Csak ennek a kérésnek a sorai",
|
||||
"showJob": "Csak ennek a jobnak a sorai",
|
||||
"empty": "Nincs a szűrésnek megfelelő sor.",
|
||||
"stale": "Az utolsó frissítés nem sikerült, amit látsz, elavult lehet.",
|
||||
"olderFailed": "Nem sikerült betölteni a régebbi sorokat.",
|
||||
"countsFailed": "A szűrők számai nem töltődtek be.",
|
||||
"stale": "Nem sikerült frissíteni — amit látsz, elavult lehet",
|
||||
"olderFailed": "Nem sikerült betölteni a régebbi sorokat",
|
||||
"countsFailed": "Nem sikerült betölteni a szűrők számait",
|
||||
"count_one": "{{count}} sor betöltve · a legújabb felül",
|
||||
"count_other": "{{count}} sor betöltve · a legújabb felül",
|
||||
"older": "Régebbiek betöltése",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"clearTitle": "Üríted a naplót?",
|
||||
"clearConfirm": "Véglegesen törlöd az összes naplóbejegyzést? Ez nem vonható vissza. (Egy bejegyzés arról, hogy te ürítetted, megmarad.)",
|
||||
"cleared": "{{count}} naplóbejegyzés törölve",
|
||||
"clearFailed": "A naplót nem sikerült üríteni.",
|
||||
"clearFailed": "Nem sikerült üríteni a naplót",
|
||||
"truncated": "A legutóbbi {{returned}} bejegyzés látszik a(z) {{total}}-ból.",
|
||||
"cols": {
|
||||
"when": "Mikor",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"job_run": "Ütemező job lefutott",
|
||||
"audit_clear": "Napló ürítve",
|
||||
"download_credential_set": "Letöltési cookie-k beállítva",
|
||||
"download_credential_delete": "Letöltési cookie-k eltávolítva",
|
||||
"download_credential_delete": "Letöltési cookie-k törölve",
|
||||
"invite_delete": "Whitelista-bejegyzés törölve",
|
||||
"invite_send": "Meghívó elküldve",
|
||||
"issue_create": "Hibajegy elküldve"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"menu": {
|
||||
"label": "Videó",
|
||||
"unmarkWatched": "Megnézés visszavonása",
|
||||
"unsave": "Törlés a mentettek közül",
|
||||
"unsave": "Eltávolítás a mentettek közül",
|
||||
"more": "További műveletek",
|
||||
"openChannel": "Csatorna megnyitása"
|
||||
}
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"fullHistoryProgress": "Előzmény letöltése… {{stored}} / {{total}}",
|
||||
"fullHistoryLoadingHint": "A teljes előzmény letöltése folyamatban — a videók feltűnnek, ahogy előkerülnek; egy nagyon nagy csatorna maradékát a háttérben fejezi be.",
|
||||
"fullHistoryQueued": "A csatorna teljes előzményének letöltése elindult — az új videók feltűnnek, ahogy előkerülnek.",
|
||||
"fullHistoryFailed": "Nem sikerült elindítani a teljes előzmény letöltését."
|
||||
"fullHistoryFailed": "Nem sikerült elindítani a teljes előzmény letöltését"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"actionsLabel": "Katalógus-műveletek",
|
||||
"syncSubscriptions": "Feliratkozások beolvasása a YouTube-ról",
|
||||
"syncSubscriptions": "Feliratkozások szinkronizálása a YouTube-ról",
|
||||
"syncSubscriptionsHint": "Újraimportálja a feliratkozási listádat a YouTube-ról — hozzáadja az újonnan követett csatornákat, és eltávolítja azokat, amelyekről leiratkoztál. Maguk a videók a háttérben automatikusan tovább szinkronizálódnak; ez nem tölti le őket újra.",
|
||||
"backfillEverything": "Minden letöltése",
|
||||
"backfillEverythingHint": "Teljes archívum letöltését kéri minden csatornához, amelyre feliratkoztál. A régebbi videók és a keresés a megosztott napi kvóta függvényében válik teljessé — ez eltarthat egy ideig.",
|
||||
@@ -31,7 +31,7 @@
|
||||
"needWriteHint": "Engedélyezd a lejátszási listák szerkesztését a Beállításokban a feliratkozáshoz.",
|
||||
"subscribedTitle": "Feliratkozva a YouTube-on",
|
||||
"subscribedBody": "Mostantól követed: {{name}} — az új feltöltései megjelennek a Feededben.",
|
||||
"subscribeFailed": "A feliratkozás sikertelen",
|
||||
"subscribeFailed": "Nem sikerült feliratkozni",
|
||||
"confirmSubscribe": "Feliratkozol erre a YouTube-on: „{{name}}”? Ez módosítja a valódi YouTube-fiókodat és kevés API-kvótát használ.",
|
||||
"cols": {
|
||||
"totalVideos": "Videók",
|
||||
@@ -54,15 +54,15 @@
|
||||
"noneYet": "Még nincs címke — hozz létre egyet lent.",
|
||||
"create": "Címke létrehozása",
|
||||
"createPlaceholder": "Új címke…",
|
||||
"createFailed": "A címke létrehozása nem sikerült.",
|
||||
"updateFailed": "A címke frissítése nem sikerült.",
|
||||
"createFailed": "Nem sikerült létrehozni a címkét",
|
||||
"updateFailed": "Nem sikerült frissíteni a címkét",
|
||||
"noTag": "Nincs címke",
|
||||
"moreCount_one": "még {{count}} címke",
|
||||
"moreCount_other": "még {{count}} címke",
|
||||
"suggestedHint": "A címkézési mintád alapján javasolt ({{pct}}% egyezés) — kattints a hozzáadáshoz",
|
||||
"acceptSuggestion": "{{name}} javasolt címke hozzáadása",
|
||||
"dismissSuggestion": "Javaslat elvetése",
|
||||
"suggestionsHint": "A címkézetlen csatornáidhoz javasolt címkék, abból tanulva, hogyan címkézted a hasonlókat. Kattints egy szellem-chipre a hozzáadáshoz, vagy ×-re az elvetéshez.",
|
||||
"dismissSuggestion": "Javaslat elrejtése",
|
||||
"suggestionsHint": "A címkézetlen csatornáidhoz javasolt címkék, abból tanulva, hogyan címkézted a hasonlókat. Kattints egy szellem-chipre a hozzáadáshoz, vagy ×-re az elrejtéshez.",
|
||||
"suggestionsCount_one": "{{count}} javaslat",
|
||||
"suggestionsCount_other": "{{count}} javaslat",
|
||||
"acceptAllHigh_one": "{{count}} biztos elfogadása",
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
"row": {
|
||||
"openOnYouTube": "Megnyitás a YouTube-on",
|
||||
"editTags": "Címkék szerkesztése",
|
||||
"editTags": "Címkék szerkesztése…",
|
||||
"filterFeedByTag": "Feed szűrése: „{{name}}”",
|
||||
"priorityHint": "A te rangsorod ehhez a csatornához. Rendezd a Feedet „Csatorna prioritás” szerint, hogy a magasabb prioritású csatornák felülre kerüljenek.",
|
||||
"fullHint": "Teljes előzmény letöltve.",
|
||||
@@ -126,12 +126,12 @@
|
||||
},
|
||||
"notify": {
|
||||
"synced": "{{count}} feliratkozás szinkronizálva",
|
||||
"syncFailed": "A feliratkozások szinkronizálása sikertelen",
|
||||
"syncFailed": "Nem sikerült szinkronizálni a feliratkozásokat",
|
||||
"unsubscribed": "Leiratkozva a(z) {{name}} csatornáról a YouTube-on",
|
||||
"unsubscribeFailed": "A leiratkozás sikertelen",
|
||||
"unsubscribeFailed": "Nem sikerült leiratkozni",
|
||||
"fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához",
|
||||
"fullHistoryFailed": "Nem sikerült teljes előzményt kérni",
|
||||
"needYouTube": "Ehhez csatlakoztasd a YouTube-fiókod.",
|
||||
"needYouTube": "Ehhez csatlakoztasd a YouTube-fiókod",
|
||||
"connect": "Csatlakoztatás",
|
||||
"filteredByTag": "Feed szűrve címkére: {{name}}",
|
||||
"resetDone": "{{name}} resetelve — újraletöltés folyamatban",
|
||||
@@ -143,12 +143,12 @@
|
||||
"searchPlaceholder": "Csatornák keresése…",
|
||||
"blocked": {
|
||||
"title": "Tiltott csatornák",
|
||||
"hint": "A videóik el vannak rejtve a Feededből, a Library-ből és a YouTube-keresésből.",
|
||||
"hint": "A videóik el vannak rejtve a Feededből, a Könyvtárból és a YouTube-keresésből.",
|
||||
"empty": "Nincs tiltott csatorna. Egy csatorna oldaláról tilthatsz le egyet, hogy minden videóját elrejtsd leiratkozás nélkül."
|
||||
},
|
||||
"hidden": {
|
||||
"title": "Elrejtett csatornák",
|
||||
"hint": "A videóik kimaradnak a személyes Feededből, de a megosztott Library-ben továbbra is megjelennek.",
|
||||
"hint": "A videóik kimaradnak a személyes Feededből, de a megosztott Könyvtárban továbbra is megjelennek.",
|
||||
"empty": "Nincs elrejtett csatorna. Egy feliratkozást a sorában (a szem ikonnal) rejthetsz el, hogy kimaradjon a személyes Feededből leiratkozás nélkül.",
|
||||
"unhide": "Felfedés"
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"undo": "Visszavonás",
|
||||
"redo": "Újra",
|
||||
"loading": "Betöltés…",
|
||||
"somethingWrong": "Hiba történt.",
|
||||
"somethingWrong": "Hiba történt",
|
||||
"itemCount_one": "{{formattedCount}} elem",
|
||||
"itemCount_other": "{{formattedCount}} elem",
|
||||
"accessRequestsTitle": "Hozzáférési kérések",
|
||||
@@ -20,7 +20,7 @@
|
||||
"demoSharedNotice": "Egy közös demo fiókban vagy. Minden, amit itt csinálsz — lejátszási listák, elrejtett videók, beállítások — közös, és más demo látogatók egyszerre módosíthatják.",
|
||||
"backToTop": "Vissza a tetejére",
|
||||
"retry": "Újra",
|
||||
"loadError": "Nem sikerült betölteni — ellenőrizd a kapcsolatot, és próbáld újra.",
|
||||
"loadError": "Nem sikerült betölteni — ellenőrizd a kapcsolatot, és próbáld újra",
|
||||
"seeAll": "Összes",
|
||||
"scrollLeft": "Görgetés balra",
|
||||
"scrollRight": "Görgetés jobbra",
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"download_upload_max_bytes": {
|
||||
"label": "Feltöltési méretkorlát fájlonként (bájt)",
|
||||
"hint": "A letöltési központba feltölthető legnagyobb egyedi fájl. Legalább 1, így a korlát mindig él; a felhasználó teljes tárhelyét továbbra is a kvótája korlátozza. Alapérték 500 MiB."
|
||||
"hint": "A Letöltésekbe feltölthető legnagyobb egyedi fájl. Legalább 1, így a korlát mindig él; a felhasználó teljes tárhelyét továbbra is a kvótája korlátozza. Alapérték 500 MiB."
|
||||
},
|
||||
"download_retention_days": {
|
||||
"label": "Megőrzés (nap)",
|
||||
@@ -292,12 +292,12 @@
|
||||
"testEmail": "Teszt-email küldése",
|
||||
"testEmailHint": "Teszt-email küldése a saját címedre a fenti beállításokkal, hogy ellenőrizd, működnek-e.",
|
||||
"testSent": "Teszt-email elküldve ide: {{to}}",
|
||||
"testFailed": "A teszt-email sikertelen — ellenőrizd a beállításokat",
|
||||
"testFailed": "Nem sikerült elküldeni a teszt-emailt — ellenőrizd a beállításokat",
|
||||
"plexTest": "Kapcsolat tesztelése",
|
||||
"plexTestHint": "Ellenőrzi a Plex-szerver URL-t és tokent, és betölti a könyvtárlistát. Előbb mentsd a módosításokat.",
|
||||
"testDirty": "Mentsd a változásokat a tesztelés előtt.",
|
||||
"plexTestOk": "Kapcsolódva: {{name}}",
|
||||
"plexTestFailed": "A Plex-kapcsolat sikertelen — ellenőrizd az URL-t és a tokent",
|
||||
"plexTestFailed": "Nem sikerült csatlakozni a Plexhez — ellenőrizd az URL-t és a tokent",
|
||||
"plexConnected": "Kapcsolódva: {{name}} {{version}}",
|
||||
"plexMediaOk": "A media-mount rendben — egy mintafájl megtalálható a lemezen.",
|
||||
"plexMediaMissing": "A média nem olvasható itt: {{path}} — ellenőrizd, hogy a Plex média be van-e mountolva a konténerbe, és hogy helyes-e az útvonal-leképezés. A lejátszás addig nem fog működni, amíg ez nincs javítva.",
|
||||
@@ -309,5 +309,5 @@
|
||||
"issuesTest": "Kapcsolat ellenőrzése",
|
||||
"issuesTestHint": "Megnézi, hogy a tárhely elérhető, privát, és fogad hibajegyet. Előbb mentsd a módosításokat.",
|
||||
"issuesTestOk": "Rendben: {{repo}} (privát)",
|
||||
"issuesTestFailed": "A hibajegy-kezelő ellenőrzése nem sikerült"
|
||||
"issuesTestFailed": "Nem sikerült ellenőrizni a hibajegy-kezelőt"
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
"submit": "Hozzáadás a letöltésekhez",
|
||||
"added": "Hozzáadva a letöltésekhez",
|
||||
"view": "Megnyitás",
|
||||
"manage": "Formátumok kezelése"
|
||||
"manage": "Formátumok kezelése…"
|
||||
},
|
||||
"page": {
|
||||
"addUrl": "Illessz be egy videólinket (YouTube, Facebook, …) vagy YouTube-videóazonosítót…",
|
||||
"add": "Hozzáad",
|
||||
"paste": "Link beillesztése a vágólapról",
|
||||
"upload": "Feltöltés",
|
||||
"manage": "Formátumok kezelése"
|
||||
"manage": "Formátumok kezelése…"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Fájl feltöltése",
|
||||
@@ -33,7 +33,7 @@
|
||||
"uploading": "Feltöltés… {{pct}}%",
|
||||
"uploadProgress": "Feltöltés folyamata",
|
||||
"added": "Fájl feltöltve",
|
||||
"failed": "A feltöltés nem sikerült."
|
||||
"failed": "Nem sikerült feltölteni a fájlt"
|
||||
},
|
||||
"tabs": {
|
||||
"queue": "Sor",
|
||||
@@ -65,15 +65,15 @@
|
||||
"pause": "Szünet",
|
||||
"resume": "Folytatás",
|
||||
"cancel": "Megszakítás",
|
||||
"delete": "Eltávolítás",
|
||||
"delete": "Törlés",
|
||||
"retry": "Újra",
|
||||
"download": "Mentés a gépemre",
|
||||
"play": "Lejátszás",
|
||||
"rename": "Átnevezés",
|
||||
"editDetails": "Adatok szerkesztése",
|
||||
"editDetails": "Adatok szerkesztése…",
|
||||
"share": "Megosztás (link másolása)",
|
||||
"shareManage": "Megosztás kezelése",
|
||||
"edit": "Szerkesztés / vágás",
|
||||
"shareManage": "Megosztás kezelése…",
|
||||
"edit": "Szerkesztés / vágás…",
|
||||
"redownload": "Újraletöltés (fájl újraépítése)",
|
||||
"removeShared": "Eltávolítás a listámból"
|
||||
},
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Tárhelyed",
|
||||
"items": "{{used}} / {{max}} elem",
|
||||
"items": "{{used}} / {{max}} letöltés",
|
||||
"unlimited": "Korlátlan"
|
||||
},
|
||||
"source": {
|
||||
@@ -108,9 +108,9 @@
|
||||
"share": {
|
||||
"title": "Letöltés megosztása",
|
||||
"done": "Megosztva vele: {{email}}",
|
||||
"failed": "A megosztás nem sikerült.",
|
||||
"failed": "Nem sikerült megosztani",
|
||||
"userSection": "Megosztás felhasználóval",
|
||||
"userHint": "A „Velem megosztott” fülön találja meg.",
|
||||
"userHint": "A címzett a „Velem megosztva” fülön találja meg.",
|
||||
"pickPlaceholder": "Keress egy felhasználót…",
|
||||
"noUsers": "Nincs más felhasználó.",
|
||||
"linkSection": "Megosztás linkkel",
|
||||
@@ -127,7 +127,7 @@
|
||||
"days": "{{n}} nap",
|
||||
"password": "Jelszó (opcionális)",
|
||||
"copy": "Link másolása",
|
||||
"copyFailed": "Nem sikerült a másolás.",
|
||||
"copyFailed": "Nem sikerült másolni",
|
||||
"revoke": "Visszavonás",
|
||||
"revokeConfirm": "A link azonnal használhatatlanná válik.",
|
||||
"views": "{{n}} megtekintés",
|
||||
@@ -145,8 +145,8 @@
|
||||
"cancelTitle": "Megszakítod a letöltést?",
|
||||
"cancelBody": "Ez leállítja a letöltést és eltávolítja a sorból.",
|
||||
"cancelAction": "Letöltés leállítása",
|
||||
"deleteTitle": "Eltávolítod a letöltést?",
|
||||
"deleteBody": "Ez eltávolítja a listádról. A fájl a lejáratáig még a gyorsítótárban maradhat mások számára.",
|
||||
"deleteTitle": "Törlöd a letöltést?",
|
||||
"deleteBody": "Ez törli a letöltéseid közül. A fájl a lejáratáig még a gyorsítótárban maradhat mások számára.",
|
||||
"removeSharedTitle": "Eltávolítod a listádból?",
|
||||
"removeSharedBody": "Csak a te megosztott listádból tűnik el — a tulajdonos fájlját nem törli.",
|
||||
"redownloadTitle": "Újraletöltöd ezt a fájlt?",
|
||||
@@ -198,28 +198,28 @@
|
||||
"errors": {
|
||||
"quota": {
|
||||
"max_bytes": "Elfogyott a letöltési tárhelykereted ({{size}} GB). Előbb szabadíts fel helyet.",
|
||||
"max_jobs": "Elérted a letöltési korlátodat ({{limit}} elem). Előbb távolíts el néhányat.",
|
||||
"generic": "Túllépted a letöltési kvótát."
|
||||
"max_jobs": "Elérted a letöltési korlátodat ({{limit}} letöltés). Előbb törölj néhányat.",
|
||||
"generic": "Túllépted a letöltési kvótát"
|
||||
},
|
||||
"edit": {
|
||||
"empty_edit": "Előbb válassz vágási tartományt vagy körbevágási területet.",
|
||||
"source_not_ready": "A forrásletöltés még nem szerkeszthető.",
|
||||
"generic": "Ezt a vágást nem sikerült létrehozni."
|
||||
"empty_edit": "Előbb válassz vágási tartományt vagy körbevágási területet",
|
||||
"source_not_ready": "A forrásletöltés még nem szerkeszthető",
|
||||
"generic": "Nem sikerült létrehozni a vágást"
|
||||
},
|
||||
"upload": {
|
||||
"tooLarge": "Ez a fájl túl nagy (legfeljebb {{size}}).",
|
||||
"empty": "Ez a fájl üres."
|
||||
"tooLarge": "Ez a fájl túl nagy (legfeljebb {{size}})",
|
||||
"empty": "Ez a fájl üres"
|
||||
},
|
||||
"urlBlocked": {
|
||||
"scheme": "Csak http:// és https:// hivatkozás tölthető le.",
|
||||
"no_host": "Ez a hivatkozás nem tartalmaz webcímet.",
|
||||
"scheme": "Csak http:// és https:// hivatkozás tölthető le",
|
||||
"no_host": "Ez a hivatkozás nem tartalmaz webcímet",
|
||||
"unresolvable": "Ezt a címet nem sikerült feloldani. Ellenőrizd a hivatkozást, és próbáld újra.",
|
||||
"private": "Ez a cím a szerver saját hálózatán van, ezért nem tölthető le róla.",
|
||||
"generic": "Erről a címről nem lehet letölteni.",
|
||||
"malformed": "Ez a hivatkozás nem érvényes webcím."
|
||||
"private": "Ez a cím a szerver saját hálózatán van, ezért nem tölthető le róla",
|
||||
"generic": "Erről a címről nem lehet letölteni",
|
||||
"malformed": "Ez a hivatkozás nem érvényes webcím"
|
||||
},
|
||||
"fail": {
|
||||
"title": "A letöltés nem sikerült",
|
||||
"title": "Nem sikerült letölteni",
|
||||
"detailsLink": "részletek",
|
||||
"detailsToggle": "Technikai részletek",
|
||||
"cat": {
|
||||
@@ -248,7 +248,7 @@
|
||||
"body": "Ez a videó DRM-védett, ezért nem tölthető le."
|
||||
},
|
||||
"postprocess": {
|
||||
"short": "A feldolgozás megszakadt",
|
||||
"short": "Nem sikerült feldolgozni a videót",
|
||||
"body": "A videó letöltődött, de egy feldolgozási lépés (összefűzés vagy konvertálás) meghiúsult. Újrapróbálásra gyakran megjavul."
|
||||
},
|
||||
"source_missing": {
|
||||
@@ -260,8 +260,8 @@
|
||||
"body": "Hálózati hiba szakította meg a letöltést. Újrapróbálásra általában megjavul."
|
||||
},
|
||||
"unknown": {
|
||||
"short": "A letöltés nem sikerült",
|
||||
"body": "Valami hiba történt az elem letöltése közben. Az alábbi technikai részletek segíthetnek."
|
||||
"short": "Nem sikerült letölteni",
|
||||
"body": "Valami hiba történt ennél a letöltésnél. Az alábbi technikai részletek segíthetnek."
|
||||
},
|
||||
"quota_exceeded": {
|
||||
"short": "Betelt a tárhely",
|
||||
@@ -300,9 +300,9 @@
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
"empty": "Előbb illeszd be a cookie-kat.",
|
||||
"format": "Ez nem tűnik cookies.txt fájlnak (Netscape formátum).",
|
||||
"generic": "A cookie-kat nem sikerült menteni."
|
||||
"empty": "Előbb illeszd be a cookie-kat",
|
||||
"format": "Ez nem tűnik cookies.txt fájlnak (Netscape formátum)",
|
||||
"generic": "Nem sikerült menteni a cookie-kat"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"createN": "{{count}} klip létrehozása",
|
||||
"created_one": "{{count}} klip a letöltésekhez adva",
|
||||
"created_other": "{{count}} klip a letöltésekhez adva",
|
||||
"failed": "A klip létrehozása nem sikerült.",
|
||||
"failed": "Nem sikerült létrehozni a klipet",
|
||||
"partlyCreated": "{{total}} klipből csak {{done}} került a letöltésekhez — a többi kijelölve maradt, így az újbóli létrehozás csak azokat adja hozzá.",
|
||||
"splitHere": "Vágás itt",
|
||||
"output": {
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
{
|
||||
"title": "Nem sikerült",
|
||||
"generic": "A szerver nem tudta végrehajtani a műveletet. Próbáld újra.",
|
||||
"demo": "Ez a bemutató fiókban nem érhető el.",
|
||||
"demo": "Ez a bemutató fiókban nem érhető el",
|
||||
"validation": "Néhány megadott érték érvénytelen volt. Ellenőrizd, majd próbáld újra.",
|
||||
"tooLarge": "Ez túl nagy, a szerver nem fogadta el.",
|
||||
"tooLarge": "Ez túl nagy, a szerver nem fogadta el",
|
||||
"server": "Szerverhiba",
|
||||
"syncCooldown": "Az imént többször is szinkronizáltál — várj pár percet. A YouTube-keret közös az instance összes felhasználójával, a háttérszinkron pedig így is fut tovább.",
|
||||
"notFound": {
|
||||
"generic": "Ez az elem már nem létezik.",
|
||||
"view": "Ez a nézet már nem létezik.",
|
||||
"tag": "Ez a címke már nem létezik.",
|
||||
"pin": "Ez már nincs a komfort polcodon.",
|
||||
"playlist": "Ez a lejátszási lista már nem létezik.",
|
||||
"move_target": "A lejátszási lista, ahová áthelyeznéd, már nem létezik.",
|
||||
"video": "Ez a videó már nincs a könyvtárban.",
|
||||
"invite": "Ez a meghívó már nem létezik.",
|
||||
"user": "Ez a felhasználó már nem létezik.",
|
||||
"channel": "Ez a csatorna már nincs a könyvtárban.",
|
||||
"subscription": "Már nem vagy feliratkozva erre a csatornára.",
|
||||
"recipient": "Nincs felhasználó ezzel az e-mail címmel.",
|
||||
"job": "Ez a feladat már nem létezik.",
|
||||
"download": "Ez a letöltés már nem létezik.",
|
||||
"debug_session": "Ez a hibakereső munkamenet már nem létezik.",
|
||||
"screenshot": "Ez a képernyőkép már nem létezik.",
|
||||
"link": "Ez a megosztási link már nem létezik.",
|
||||
"notification": "Ez az értesítés már nem létezik.",
|
||||
"account": "Ez a fiók már nem létezik.",
|
||||
"collection": "Ez a gyűjtemény már nem létezik.",
|
||||
"config_key": "Nincs ilyen beállítás.",
|
||||
"file": "A fájl már nem érhető el.",
|
||||
"filmstrip": "Ehhez nincs képsor.",
|
||||
"host": "Ez a kiszolgáló már nincs a listán.",
|
||||
"image": "Ehhez nincs kép.",
|
||||
"item": "Ez az elem már nem létezik.",
|
||||
"library": "Ez a könyvtár már nem létezik.",
|
||||
"plex_item": "Ez a Plex-elem már nincs a könyvtárban.",
|
||||
"poster": "Ehhez nincs poszter.",
|
||||
"profile": "Ez a letöltési profil már nem létezik.",
|
||||
"season": "Ez az évad már nincs a könyvtárban.",
|
||||
"show": "Ez a sorozat már nincs a könyvtárban."
|
||||
"generic": "Ez az elem már nem létezik",
|
||||
"view": "Ez a nézet már nem létezik",
|
||||
"tag": "Ez a címke már nem létezik",
|
||||
"pin": "Ez már nincs a komfort polcodon",
|
||||
"playlist": "Ez a lejátszási lista már nem létezik",
|
||||
"move_target": "A lejátszási lista, ahová áthelyeznéd, már nem létezik",
|
||||
"video": "Ez a videó már nincs a könyvtárban",
|
||||
"invite": "Ez a meghívó már nem létezik",
|
||||
"user": "Ez a felhasználó már nem létezik",
|
||||
"channel": "Ez a csatorna már nincs a könyvtárban",
|
||||
"subscription": "Már nem vagy feliratkozva erre a csatornára",
|
||||
"recipient": "Nincs felhasználó ezzel az e-mail címmel",
|
||||
"job": "Ez a feladat már nem létezik",
|
||||
"download": "Ez a letöltés már nem létezik",
|
||||
"debug_session": "Ez a hibakereső munkamenet már nem létezik",
|
||||
"screenshot": "Ez a képernyőkép már nem létezik",
|
||||
"link": "Ez a megosztási link már nem létezik",
|
||||
"notification": "Ez az értesítés már nem létezik",
|
||||
"account": "Ez a fiók már nem létezik",
|
||||
"collection": "Ez a gyűjtemény már nem létezik",
|
||||
"config_key": "Nincs ilyen beállítás",
|
||||
"file": "A fájl már nem érhető el",
|
||||
"filmstrip": "Ehhez nincs képsor",
|
||||
"host": "Ez a kiszolgáló már nincs a listán",
|
||||
"image": "Ehhez nincs kép",
|
||||
"item": "Ez az elem már nem létezik",
|
||||
"library": "Ez a könyvtár már nem létezik",
|
||||
"plex_item": "Ez a Plex-elem már nincs a könyvtárban",
|
||||
"poster": "Ehhez nincs poszter",
|
||||
"profile": "Ez a letöltési profil már nem létezik",
|
||||
"season": "Ez az évad már nincs a könyvtárban",
|
||||
"show": "Ez a sorozat már nincs a könyvtárban"
|
||||
},
|
||||
"admin": {
|
||||
"not_allowed": "A címe már nincs jóváhagyva. Előbb vedd fel a fehérlistára.",
|
||||
"not_applicable": "Ezen a fiókon már nincs mit megerősíteni.",
|
||||
"not_approved": "Csak jóváhagyott címre küldhető meghívó.",
|
||||
"not_applicable": "Ezen a fiókon már nincs mit megerősíteni",
|
||||
"not_approved": "Csak jóváhagyott címre küldhető meghívó",
|
||||
"email_off": "Nem ment ki levél: ezen a példányon nincs működő SMTP. Nézd meg a Konfiguráció → E-mail / SMTP fület.",
|
||||
"send_failed": "Nem sikerült elküldeni a levelet: a levelezőszerver nem volt elérhető, vagy elutasította a belépést vagy a levelet. Nézd meg a Konfiguráció → E-mail / SMTP fület, majd próbáld újra.",
|
||||
"demo": "A bemutató fiók közös, ezért nem kaphat jelszóbeállító linket."
|
||||
"demo": "A bemutató fiók közös, ezért nem kaphat jelszóbeállító linket"
|
||||
},
|
||||
"reason": {
|
||||
"unreachable": "A szerver nem volt elérhető. Próbáld újra.",
|
||||
"forbidden": "Ehhez nincs jogosultságod.",
|
||||
"rateLimited": "Túl sok kérés egyszerre — várj egy kicsit, és próbáld újra.",
|
||||
"forbidden": "Ehhez nincs jogosultságod",
|
||||
"rateLimited": "Túl sok kérés egyszerre — várj egy kicsit, és próbáld újra",
|
||||
"unexpected": "Váratlan hiba történt az alkalmazásban. Próbáld újra."
|
||||
},
|
||||
"writeFailed": {
|
||||
@@ -70,15 +70,15 @@
|
||||
"delete": "Nem sikerült törölni a letöltést",
|
||||
"redownload": "Nem sikerült újraindítani a letöltést"
|
||||
},
|
||||
"removeShared": "Nem sikerült eltávolítani a megosztott elemet",
|
||||
"removeShared": "Nem sikerült eltávolítani a megosztott letöltést",
|
||||
"downloadDetails": "Nem sikerült menteni a letöltés adatait",
|
||||
"enqueueDownload": "Nem sikerült sorba állítani a letöltést",
|
||||
"downloadProfileSave": "Nem sikerült menteni a letöltési profilt",
|
||||
"downloadProfileDelete": "Nem sikerült törölni a letöltési profilt",
|
||||
"downloadCookiesRemove": "Nem sikerült eltávolítani ezeket a sütiket",
|
||||
"downloadCookiesRemove": "Nem sikerült törölni ezeket a cookie-kat",
|
||||
"notifRead": "Nem sikerült olvasottnak jelölni az értesítést",
|
||||
"notifReadAll": "Nem sikerült olvasottnak jelölni az értesítéseket",
|
||||
"notifDismiss": "Nem sikerült elvetni az értesítést",
|
||||
"notifDismiss": "Nem sikerült elrejteni az értesítést",
|
||||
"notifClear": "Nem sikerült törölni az értesítéseket",
|
||||
"createPlaylist": "Nem sikerült létrehozni a lejátszási listát",
|
||||
"schedulerRun": "Nem sikerült elindítani a feladatot",
|
||||
@@ -106,8 +106,8 @@
|
||||
"body": "Nem sikerült elérni a szervert — lehet, hogy épp újraindul. Ez az üzenet eltűnik, amint helyreáll a kapcsolat."
|
||||
},
|
||||
"boundary": {
|
||||
"title": "Hiba történt.",
|
||||
"subtitle": "Az alkalmazás váratlan hibába ütközött.",
|
||||
"title": "Hiba történt",
|
||||
"subtitle": "Az alkalmazás váratlan hibába ütközött",
|
||||
"reload": "Újratöltés",
|
||||
"notifTitle": "Valami elromlott",
|
||||
"notifMessage": "Váratlan hiba"
|
||||
@@ -118,111 +118,111 @@
|
||||
"unavailable": "A YouTube most nem fogadta el. Próbáld újra."
|
||||
},
|
||||
"refused": {
|
||||
"account_no_access": "Ennek a fióknak már nincs hozzáférése.",
|
||||
"account_signed_out": "Ezt a fiókot máshol kijelentkeztették — lépj be újra.",
|
||||
"account_suspended": "Ez a fiók fel van függesztve.",
|
||||
"address_has_account": "Ehhez a címhez már tartozik fiók — azt töröld helyette.",
|
||||
"admin_first": "Előbb hozd létre az admin fiókot.",
|
||||
"admin_only": "Ezt csak admin teheti meg.",
|
||||
"already_set_up": "Ez a példány már be van állítva.",
|
||||
"already_subscribed": "Erre a csatornára már feliratkoztál.",
|
||||
"channel_blocked": "Ezt a csatornát letiltottad.",
|
||||
"collection_fields_required": "A címet és egy elemet is meg kell adni.",
|
||||
"collection_pending_sync": "A gyűjtemény a Plexben megváltozott, de a helyi másolat frissítése nem sikerült — a következő szinkron pótolja.",
|
||||
"collection_read_only": "Ez a gyűjtemény csak olvasható.",
|
||||
"collection_smart": "Az okos és az automatikus listás gyűjtemények nem tehetők szerkeszthetővé.",
|
||||
"demo_activate": "A bemutató fiók nem aktiválható.",
|
||||
"demo_delete": "A bemutató fiók nem törölhető.",
|
||||
"demo_role": "A bemutató fiók szerepköre nem módosítható.",
|
||||
"demo_suspend": "A bemutató fiók nem függeszthető fel.",
|
||||
"download_not_ready": "Ez a letöltés még nincs kész.",
|
||||
"edit_unfinished": "Csak befejezett letöltés szerkeszthető.",
|
||||
"filters_not_object": "A nézet szűrői hibás formátumúak.",
|
||||
"account_no_access": "Ennek a fióknak már nincs hozzáférése",
|
||||
"account_signed_out": "Ezt a fiókot máshol kijelentkeztették — lépj be újra",
|
||||
"account_suspended": "Ez a fiók fel van függesztve",
|
||||
"address_has_account": "Ehhez a címhez már tartozik fiók — azt töröld helyette",
|
||||
"admin_first": "Előbb hozd létre az admin fiókot",
|
||||
"admin_only": "Ezt csak admin teheti meg",
|
||||
"already_set_up": "Ez a példány már be van állítva",
|
||||
"already_subscribed": "Erre a csatornára már feliratkoztál",
|
||||
"channel_blocked": "Ezt a csatornát letiltottad",
|
||||
"collection_fields_required": "A címet és egy elemet is meg kell adni",
|
||||
"collection_pending_sync": "A gyűjtemény a Plexben megváltozott, de a helyi másolat frissítése nem sikerült — a következő szinkron pótolja",
|
||||
"collection_read_only": "Ez a gyűjtemény csak olvasható",
|
||||
"collection_smart": "Az okos és az automatikus listás gyűjtemények nem tehetők szerkeszthetővé",
|
||||
"demo_activate": "A bemutató fiók nem aktiválható",
|
||||
"demo_delete": "A bemutató fiók nem törölhető",
|
||||
"demo_role": "A bemutató fiók szerepköre nem módosítható",
|
||||
"demo_suspend": "A bemutató fiók nem függeszthető fel",
|
||||
"download_not_ready": "Ez a letöltés még nincs kész",
|
||||
"edit_unfinished": "Csak befejezett letöltés szerkeszthető",
|
||||
"filters_not_object": "A nézet szűrői hibás formátumúak",
|
||||
"google_no_userinfo": "A Google nem küldte vissza a fiók adatait. Próbálj újra belépni.",
|
||||
"image_fetch_failed": "A képet nem sikerült betölteni.",
|
||||
"image_host": "Erről a címről nem töltünk be képet.",
|
||||
"invalid_cursor": "A lista pozíciója elavult — töltsd újra az oldalt.",
|
||||
"invalid_email": "Adj meg egy érvényes e-mail címet.",
|
||||
"invalid_interval": "Az intervallumnak {{min}} és {{max}} perc között kell lennie.",
|
||||
"invalid_link": "Ez nem tűnik érvényes videólinknek.",
|
||||
"invalid_role": "Ismeretlen szerepkör.",
|
||||
"invalid_status": "Ismeretlen állapot.",
|
||||
"invite_decided": "Erről a kérésről már döntöttek.",
|
||||
"job_running": "Ez a feladat már fut.",
|
||||
"last_active_admin": "Ez az egyetlen aktív admin — előbb tegyél meg valaki mást adminnak.",
|
||||
"last_admin": "Ez az egyetlen admin — előbb tegyél meg valaki mást adminnak.",
|
||||
"link_gone": "Ez a link már nem érhető el.",
|
||||
"link_required": "Adj meg egy videólinket vagy -azonosítót.",
|
||||
"media_missing": "A médiafájl nincs a lemezen.",
|
||||
"message_empty": "Az üzenet üres.",
|
||||
"message_rate": "Túl sok üzenet — várj egy kicsit.",
|
||||
"message_self": "Magadnak nem küldhetsz üzenetet.",
|
||||
"message_too_long": "Az üzenet túl hosszú.",
|
||||
"messaging_set_up": "A biztonságos üzenetküldés már be van állítva.",
|
||||
"name_required": "Adj meg egy nevet.",
|
||||
"no_encryption_key": "Titkos értékek tárolásához állítsd be a TOKEN_ENCRYPTION_KEY-t.",
|
||||
"no_messaging": "Ez a tag még nem állította be az üzenetküldést.",
|
||||
"no_profile": "Nincs használható letöltési profil.",
|
||||
"no_subscription_id": "Még nincs rögzített YouTube-feliratkozás — előbb szinkronizáld a feliratkozásaidat.",
|
||||
"not_authenticated": "Nem vagy bejelentkezve.",
|
||||
"not_ready": "Még nem érhető el.",
|
||||
"not_redownloadable": "Ez az elem nem tölthető le újra.",
|
||||
"not_your_account": "Ebbe a fiókba itt nem léptél be.",
|
||||
"only_admin_delete": "Te vagy az egyetlen admin — a fiókod törlése előtt tegyél meg valaki mást adminnak.",
|
||||
"own_delete": "A saját fiókodat a Beállítások → Fiók alatt törölheted.",
|
||||
"own_role": "A saját szerepkörödet nem módosíthatod.",
|
||||
"own_suspend": "A saját fiókodat nem függesztheted fel.",
|
||||
"plex_disabled": "A Plex modul ki van kapcsolva.",
|
||||
"plex_not_configured": "A Plex nincs beállítva — add meg a szerver címét és a tokent a Konfiguráció → Plex alatt.",
|
||||
"image_fetch_failed": "Nem sikerült betölteni a képet",
|
||||
"image_host": "Erről a címről nem töltünk be képet",
|
||||
"invalid_cursor": "A lista pozíciója elavult — töltsd újra az oldalt",
|
||||
"invalid_email": "Adj meg egy érvényes e-mail címet",
|
||||
"invalid_interval": "Az intervallumnak {{min}} és {{max}} perc között kell lennie",
|
||||
"invalid_link": "Ez nem tűnik érvényes videólinknek",
|
||||
"invalid_role": "Ismeretlen szerepkör",
|
||||
"invalid_status": "Ismeretlen állapot",
|
||||
"invite_decided": "Erről a kérésről már döntöttek",
|
||||
"job_running": "Ez a feladat már fut",
|
||||
"last_active_admin": "Ez az egyetlen aktív admin — előbb tegyél meg valaki mást adminnak",
|
||||
"last_admin": "Ez az egyetlen admin — előbb tegyél meg valaki mást adminnak",
|
||||
"link_gone": "Ez a link már nem érhető el",
|
||||
"link_required": "Adj meg egy videólinket vagy -azonosítót",
|
||||
"media_missing": "A médiafájl nincs a lemezen",
|
||||
"message_empty": "Az üzenet üres",
|
||||
"message_rate": "Túl sok üzenet — várj egy kicsit",
|
||||
"message_self": "Magadnak nem küldhetsz üzenetet",
|
||||
"message_too_long": "Az üzenet túl hosszú",
|
||||
"messaging_set_up": "A biztonságos üzenetküldés már be van állítva",
|
||||
"name_required": "Adj meg egy nevet",
|
||||
"no_encryption_key": "Titkos értékek tárolásához állítsd be a TOKEN_ENCRYPTION_KEY-t",
|
||||
"no_messaging": "Ez a tag még nem állította be az üzenetküldést",
|
||||
"no_profile": "Nincs használható letöltési profil",
|
||||
"no_subscription_id": "Még nincs rögzített YouTube-feliratkozás — előbb szinkronizáld a feliratkozásaidat",
|
||||
"not_authenticated": "Nem vagy bejelentkezve",
|
||||
"not_ready": "Még nem érhető el",
|
||||
"not_redownloadable": "Ez a letöltés nem tölthető le újra",
|
||||
"not_your_account": "Ebbe a fiókba itt nem léptél be",
|
||||
"only_admin_delete": "Te vagy az egyetlen admin — a fiókod törlése előtt tegyél meg valaki mást adminnak",
|
||||
"own_delete": "A saját fiókodat a Beállítások → Fiók alatt törölheted",
|
||||
"own_role": "A saját szerepkörödet nem módosíthatod",
|
||||
"own_suspend": "A saját fiókodat nem függesztheted fel",
|
||||
"plex_disabled": "A Plex modul ki van kapcsolva",
|
||||
"plex_not_configured": "A Plex nincs beállítva — add meg a szerver címét és a tokent a Konfiguráció → Plex alatt",
|
||||
"plex_unreachable": "A Plex-szerver nem válaszolt. Ellenőrizd a címet és a tokent, majd próbáld újra.",
|
||||
"plex_write_failed": "A Plex nem fogadta el a módosítást. Próbáld újra.",
|
||||
"profile_name_required": "Adj meg egy profilnevet.",
|
||||
"quota_reserve": "A mai közös YouTube-keret a tartalékánál tart — próbáld később.",
|
||||
"profile_name_required": "Adj meg egy profilnevet",
|
||||
"quota_reserve": "A mai közös YouTube-keret a tartalékánál tart — próbáld később",
|
||||
"quota_spent": "A mai közös YouTube-keret elfogyott. Próbáld holnap.",
|
||||
"recipient_required": "Adj meg egy címzett e-mail címet.",
|
||||
"registration_closed": "A regisztráció jelenleg zárva van.",
|
||||
"recipient_required": "Adj meg egy címzett e-mail címet",
|
||||
"registration_closed": "A regisztráció jelenleg zárva van",
|
||||
"search_daily_limit": "Elérted a mai YouTube-keresési korlátot ({{limit}}). Próbáld holnap.",
|
||||
"search_term_required": "Írj be egy keresőkifejezést.",
|
||||
"session_gone": "A lejátszási munkamenet véget ért.",
|
||||
"setting_not_a_choice": "Válassz ezek közül: {{choices}}.",
|
||||
"setting_not_a_number": "Adj meg egy egész számot.",
|
||||
"setting_too_high": "Az érték nem lehet nagyobb, mint {{max}}.",
|
||||
"setting_too_low": "Az érték nem lehet kisebb, mint {{min}}.",
|
||||
"setup_token": "A beállítási token hiányzik vagy hibás.",
|
||||
"share_own": "Ez már a te letöltésed.",
|
||||
"share_password_required": "Ehhez a linkhez jelszó kell.",
|
||||
"share_unfinished": "Csak befejezett letöltés osztható meg.",
|
||||
"smtp_failed": "Nem sikerült elküldeni a levelet — ellenőrizd az SMTP-beállításokat.",
|
||||
"smtp_off": "Az SMTP nincs beállítva.",
|
||||
"spec_not_object": "A profil beállításai hibás formátumúak.",
|
||||
"still_downloading": "Ez a letöltés még folyamatban van.",
|
||||
"tag_name_taken": "Már van „{{name}}” nevű címkéd.",
|
||||
"title_required": "Adj meg egy címet.",
|
||||
"search_term_required": "Írj be egy keresőkifejezést",
|
||||
"session_gone": "A lejátszási munkamenet véget ért",
|
||||
"setting_not_a_choice": "Válassz ezek közül: {{choices}}",
|
||||
"setting_not_a_number": "Adj meg egy egész számot",
|
||||
"setting_too_high": "Az érték nem lehet nagyobb, mint {{max}}",
|
||||
"setting_too_low": "Az érték nem lehet kisebb, mint {{min}}",
|
||||
"setup_token": "A beállítási token hiányzik vagy hibás",
|
||||
"share_own": "Ez már a te letöltésed",
|
||||
"share_password_required": "Ehhez a linkhez jelszó kell",
|
||||
"share_unfinished": "Csak befejezett letöltés osztható meg",
|
||||
"smtp_failed": "Nem sikerült elküldeni a levelet — ellenőrizd az SMTP-beállításokat",
|
||||
"smtp_off": "Az SMTP nincs beállítva",
|
||||
"spec_not_object": "A profil beállításai hibás formátumúak",
|
||||
"still_downloading": "Ez a letöltés még folyamatban van",
|
||||
"tag_name_taken": "Már van „{{name}}” nevű címkéd",
|
||||
"title_required": "Adj meg egy címet",
|
||||
"too_many_attempts": "Túl sok próbálkozás. Próbáld kicsit később.",
|
||||
"unknown_setting": "Ismeretlen beállítás: {{key}}",
|
||||
"value_missing": "Adj meg egy értéket.",
|
||||
"video_gone": "Ez a videó már nem érhető el.",
|
||||
"view_name_taken": "Már van „{{name}}” nevű nézeted.",
|
||||
"watch_sync_off": "Előbb kapcsold be a Plex megtekintés-szinkront.",
|
||||
"weak_password": "A jelszónak legalább {{min}} karakterből kell állnia.",
|
||||
"wrong_password": "A jelenlegi jelszó helytelen.",
|
||||
"wrong_share_password": "Hibás jelszó.",
|
||||
"youtube_editing_off": "Előbb kapcsold be a YouTube-szerkesztést a Beállításokban.",
|
||||
"youtube_read_scope": "Előbb kapcsold össze a YouTube-fiókodat (olvasási hozzáférés).",
|
||||
"value_missing": "Adj meg egy értéket",
|
||||
"video_gone": "Ez a videó már nem érhető el",
|
||||
"view_name_taken": "Már van „{{name}}” nevű nézeted",
|
||||
"watch_sync_off": "Előbb kapcsold be a Plex megtekintés-szinkront",
|
||||
"weak_password": "A jelszónak legalább {{min}} karakterből kell állnia",
|
||||
"wrong_password": "A jelenlegi jelszó helytelen",
|
||||
"wrong_share_password": "Hibás jelszó",
|
||||
"youtube_editing_off": "Előbb kapcsold be a YouTube-szerkesztést a Beállításokban",
|
||||
"youtube_read_scope": "Előbb kapcsold össze a YouTube-fiókodat (olvasási hozzáférés)",
|
||||
"debug_session_conflict": "Egy másik lapon épp most indult rögzítés. Frissítsd az oldalt.",
|
||||
"issue_already_sent": "Ezt a hibajegyet már elküldted, a vázlata nem változtatható.",
|
||||
"issue_title_required": "Adj címet a hibajegynek, mielőtt elküldöd.",
|
||||
"screenshot_already_sent": "A hibajegy már elment, ehhez a munkamenethez nem adhatsz több képet.",
|
||||
"screenshot_too_many": "Egy munkamenethez legfeljebb {{limit}} képernyőkép tartozhat.",
|
||||
"screenshot_empty": "A kép üres.",
|
||||
"screenshot_not_an_image": "Ez nem PNG, JPEG vagy WebP kép.",
|
||||
"issue_already_sent": "Ezt a hibajegyet már elküldted, a vázlata nem változtatható",
|
||||
"issue_title_required": "Adj címet a hibajegynek, mielőtt elküldöd",
|
||||
"screenshot_already_sent": "A hibajegy már elment, ehhez a munkamenethez nem adhatsz több képet",
|
||||
"screenshot_too_many": "Egy munkamenethez legfeljebb {{limit}} képernyőkép tartozhat",
|
||||
"screenshot_empty": "A kép üres",
|
||||
"screenshot_not_an_image": "Ez nem PNG, JPEG vagy WebP kép",
|
||||
"issues_not_configured": "Nincs beállítva hibajegy-kezelő. Töltsd le a hibajegyet zipként.",
|
||||
"issues_unreachable": "A hibajegy-kezelő nem válaszol. Próbáld újra később.",
|
||||
"issues_bad_token": "A hibajegy-kezelő nem fogadta el a tokent, vagy hiányzik a jogosultsága.",
|
||||
"issues_repo_not_found": "A beállított hibajegy-tárhely nem található.",
|
||||
"issues_bad_token": "A hibajegy-kezelő nem fogadta el a tokent, vagy hiányzik a jogosultsága",
|
||||
"issues_repo_not_found": "A beállított hibajegy-tárhely nem található",
|
||||
"issues_public_repo": "A hibajegy-tárhely nyilvános. A napló miatt csak privát tárhelyre küldünk.",
|
||||
"issues_disabled": "A hibajegy-tárhelyen ki van kapcsolva a hibajegy-kezelés.",
|
||||
"issues_rejected": "A hibajegy-kezelő visszautasította a kérést.",
|
||||
"issues_disabled": "A hibajegy-tárhelyen ki van kapcsolva a hibajegy-kezelés",
|
||||
"issues_rejected": "A hibajegy-kezelő visszautasította a kérést",
|
||||
"issues_rate_limited": "Óránként legfeljebb {{limit}} hibajegyet küldhetsz. Próbáld újra később.",
|
||||
"debug_session_rate_limited": "Óránként legfeljebb {{limit}} új rögzítést indíthatsz. Próbáld újra később.",
|
||||
"export_rate_limited": "Túl sok letöltés egymás után. Várj egy percet.",
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
"maxAge_other": "Utolsó {{count}} nap"
|
||||
},
|
||||
"loading": "Feed betöltése…",
|
||||
"loadError": "Nem sikerült betölteni a Feedet.",
|
||||
"loadError": "Nem sikerült betölteni a Feedet",
|
||||
"emptyTitle": "A Feeded üres",
|
||||
"emptyBody": "Kösd össze a YouTube-fiókodat, hogy importáld a feliratkozásaidat, és felépítsd a Feededet.",
|
||||
"setUp": "Feed beállítása",
|
||||
"browseShared": "Vagy csak böngészd a közös könyvtárat",
|
||||
"browseShared": "Vagy csak böngészd a közös Könyvtárat",
|
||||
"demoEmptyTitle": "Itt a demóban nincs semmi",
|
||||
"demoEmptyBody": "A demo fióknak nincsenek saját feliratkozásai — de a teljes közös könyvtár a tiéd: böngészd, szűrd és rendezd kedvedre.",
|
||||
"demoEmptyBody": "A demo fióknak nincsenek saját feliratkozásai — de a teljes közös Könyvtár a tiéd: böngészd, szűrd és rendezd kedvedre.",
|
||||
"browseLibrary": "Könyvtár böngészése",
|
||||
"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.",
|
||||
"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",
|
||||
"sortLabel": "Rendezés",
|
||||
"viewLabel": "Nézet",
|
||||
@@ -70,7 +70,7 @@
|
||||
"back": "Vissza a Feedhez",
|
||||
"searching": "Keresés a YouTube-on…",
|
||||
"noResults": "Nincs YouTube-találat.",
|
||||
"error": "A YouTube-keresés nem sikerült.",
|
||||
"error": "Nem sikerült keresni a YouTube-on",
|
||||
"loadMore": "Továbbiak betöltése (kvótát fogyaszt)",
|
||||
"loadMoreFree": "Továbbiak betöltése",
|
||||
"show": "Mutat",
|
||||
@@ -78,6 +78,6 @@
|
||||
"clearNow": "Törlés most",
|
||||
"cleared_one": "{{count}} találat törölve",
|
||||
"cleared_other": "{{count}} találat törölve",
|
||||
"showHint": "Hány találatot szerezzen egyszerre — a Több betöltése ennyivel tölt tovább."
|
||||
"showHint": "Hány találatot szerezzen egyszerre — a „Továbbiak betöltése” ennyivel tölt tovább."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"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)",
|
||||
"channelManager": "Csatornakezelő",
|
||||
"channelManager": "Csatornák",
|
||||
"usageStats": "Használat és statisztika",
|
||||
"scheduler": "Ütemező",
|
||||
"configuration": "Konfiguráció",
|
||||
@@ -15,7 +15,7 @@
|
||||
"my": "Sajátom",
|
||||
"myTip": "Csak a saját feliratkozásaid videói",
|
||||
"all": "Könyvtár",
|
||||
"allTip": "A közös könyvtár összes videója"
|
||||
"allTip": "A közös Könyvtár összes videója"
|
||||
},
|
||||
"account": {
|
||||
"admin": "Adminisztrátor",
|
||||
@@ -51,14 +51,14 @@
|
||||
"resume": "Szinkron folytatása"
|
||||
},
|
||||
"banner": {
|
||||
"updated": "Frissítve a(z) v{{version}} verzióra — nézd meg, mi változott.",
|
||||
"updated": "Frissítve a(z) v{{version}} verzióra — nézd meg, mi változott",
|
||||
"releaseNotes": "Verziójegyzék",
|
||||
"dismiss": "Elvetés"
|
||||
"dismiss": "Elrejtés"
|
||||
},
|
||||
"verify": {
|
||||
"confirmed": "Megerősítettük az e-mail-címedet.",
|
||||
"confirmedNamed": "Megerősítettük az e-mail-címedet — {{email}}.",
|
||||
"invalid": "Ez a megerősítő link már nem érvényes — lejárt, vagy már felhasználták.",
|
||||
"confirmed": "Megerősítettük az e-mail-címedet",
|
||||
"confirmedNamed": "Megerősítettük az e-mail-címedet — {{email}}",
|
||||
"invalid": "Ez a megerősítő link már nem érvényes — lejárt, vagy már felhasználták",
|
||||
"unreachable": "Nem sikerült elérni a szervert a link megerősítéséhez, így az nem is használódott el. Próbáld megnyitni újra egy pillanat múlva.",
|
||||
"resetSignedOut": "A jelszó-visszaállító linkek kijelentkezett ablakban nyílnak meg. Jelentkezz ki itt, vagy nyisd meg a linket privát ablakban, és onnan folytatódik.",
|
||||
"reset": "A fiók regisztrációkori jelszavát biztonsági okból töröltük; belépés előtt állíts be újat."
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navLabel": "Kezdőlap",
|
||||
"title": "Mit nézzek most?",
|
||||
"error": "Nem sikerült betölteni az ajánlatokat.",
|
||||
"actionFailed": "Nem sikerült elmenteni.",
|
||||
"error": "Nem sikerült betölteni az ajánlatokat",
|
||||
"actionFailed": "Nem sikerült elmenteni",
|
||||
"budget": {
|
||||
"ask": "Mennyi időd van?",
|
||||
"short": "10 perc",
|
||||
@@ -38,17 +38,17 @@
|
||||
"moveLeft": "Balra",
|
||||
"moveRight": "Jobbra",
|
||||
"moveLast": "A polc végére",
|
||||
"moveFailed": "Nem sikerült átrendezni a polcot.",
|
||||
"moveFailed": "Nem sikerült átrendezni a polcot",
|
||||
"moveSaving": "Az új sorrend mentése…",
|
||||
"openPlaylist": "Lejátszási lista megnyitása"
|
||||
},
|
||||
"pin": {
|
||||
"menu": "Komfort polc",
|
||||
"add": "Komfort polcra",
|
||||
"remove": "Levétel a polcról",
|
||||
"added": "{{title}} felkerült a komfort polcra.",
|
||||
"removed": "{{title}} lekerült a komfort polcról.",
|
||||
"addFailed": "Nem sikerült a polcra tűzni.",
|
||||
"removeFailed": "Nem sikerült levenni a polcról."
|
||||
"remove": "Eltávolítás a polcról",
|
||||
"added": "{{title}} felkerült a komfort polcra",
|
||||
"removed": "{{title}} lekerült a komfort polcról",
|
||||
"addFailed": "Nem sikerült a polcra tűzni",
|
||||
"removeFailed": "Nem sikerült eltávolítani a polcról"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
"clearConfirmTitle": "Törlöd az összes értesítést?",
|
||||
"clearConfirmBody": "Minden itteni értesítés eltűnik a listából, olvasott és olvasatlan is. Nem hozhatók vissza.",
|
||||
"markRead": "Olvasottnak jelöl",
|
||||
"dismiss": "Elvetés",
|
||||
"dismiss": "Elrejtés",
|
||||
"rowMenu": "Értesítés műveletei",
|
||||
"menu": "Értesítésközpont műveletei",
|
||||
"menu": "Értesítések műveletei",
|
||||
"sectionSystem": "Rendszer",
|
||||
"sectionActivity": "Tevékenység",
|
||||
"andMore": "és még {{count}}",
|
||||
"openScheduler": "Ütemező megnyitása",
|
||||
"openDownloads": "Letöltések megnyitása",
|
||||
"revertFailed": "Nem sikerült visszavonni — próbáld újra.",
|
||||
"revertFailed": "Nem sikerült visszavonni — próbáld újra",
|
||||
"downloadDone": {
|
||||
"title": "Letöltés kész"
|
||||
},
|
||||
@@ -28,11 +28,11 @@
|
||||
},
|
||||
"jobDone": {
|
||||
"titleOk": "{{job}} kész",
|
||||
"titleError": "{{job}} hibázott"
|
||||
"titleError": "Nem sikerült lefuttatni: {{job}}"
|
||||
},
|
||||
"emailUndelivered": {
|
||||
"title": "Nem megy ki e-mail",
|
||||
"notConfigured": "Az SMTP nincs beállítva, így semmi nem kézbesíthető, amit ez a példány küldene — megerősítő linkek, jelszó-visszaállítások és hozzáférés-kérelmek egyaránt. Az egyik ilyen: „{{subject}}”. Állítsd be a Konfiguráció → E-mail / SMTP fülön.",
|
||||
"sendFailed": "A küldés nem sikerült, így az üzenetek nem kézbesíthetők. Az egyik ilyen: „{{subject}}”. Nézd meg a Konfiguráció → E-mail / SMTP fület, és küldj tesztet; az okot a szerver naplója tartalmazza."
|
||||
"sendFailed": "Nem sikerült e-mailt küldeni, így az üzenetek nem kézbesíthetők. Az egyik ilyen: „{{subject}}”. Nézd meg a Konfiguráció → E-mail / SMTP fület, és küldj tesztet; az okot a szerver naplója tartalmazza."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"audio": "Hangsáv váltása",
|
||||
"subtitle": "Felirat váltása",
|
||||
"help": "Súgó megjelenítése",
|
||||
"home": "Ugrás a kezdőlapra (mit nézzek?)",
|
||||
"home": "Ugrás: Kezdőlap (mit nézzek?)",
|
||||
"info": "Média infó",
|
||||
"stats": "Statisztika (nerdöknek)",
|
||||
"back": "Leállítás és vissza",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"minimize": "Összecsukás",
|
||||
"close": "Bezárás",
|
||||
"conversations": "Beszélgetések",
|
||||
"new": "Új üzenet",
|
||||
"new": "Új üzenet…",
|
||||
"newTitle": "Új üzenet",
|
||||
"empty": "Még nincs beszélgetés. Indíts egyet az Új üzenet gombbal.",
|
||||
"searchPeople": "Emberek keresése…",
|
||||
@@ -27,15 +27,15 @@
|
||||
"unlock": "Feloldás",
|
||||
"passphrase": "Jelszó",
|
||||
"passphraseConfirm": "Jelszó megerősítése",
|
||||
"passTooShort": "Legalább 6 karakter legyen.",
|
||||
"passMismatch": "A jelszavak nem egyeznek.",
|
||||
"wrongPass": "Hibás jelszó.",
|
||||
"setupFailed": "A biztonságos üzenetküldés beállítása nem sikerült. Próbáld újra.",
|
||||
"passTooShort": "Legalább 6 karakter legyen",
|
||||
"passMismatch": "A jelszavak nem egyeznek",
|
||||
"wrongPass": "Hibás jelszó",
|
||||
"setupFailed": "Nem sikerült beállítani a biztonságos üzenetküldést. Próbáld újra.",
|
||||
"forgotPass": "Elfelejtetted a jelszavad?",
|
||||
"resetTitle": "Biztonságos üzenetküldés visszaállítása",
|
||||
"resetWarn": "Ez véglegesen törli a teljes üzenet-előzményedet — minden beszélgetést, nálad és a partnereidnél is —, mert az többé nem visszafejthető. Utána új jelszót állíthatsz be és tiszta lappal indulhatsz. Ez nem visszavonható.",
|
||||
"resetCurrentPass": "Jelenlegi fiók-jelszó",
|
||||
"resetNeedPass": "Add meg a jelenlegi fiók-jelszavad a megerősítéshez.",
|
||||
"resetButton": "Visszaállítás és előzmény törlése",
|
||||
"resetFailed": "A biztonságos üzenetküldés visszaállítása nem sikerült. Próbáld újra."
|
||||
"resetFailed": "Nem sikerült visszaállítani a biztonságos üzenetküldést. Próbáld újra."
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
"channels": "Csatornák",
|
||||
"playlists": "Listák",
|
||||
"plex": "Plex",
|
||||
"notifications": "Értesítés",
|
||||
"notifications": "Értesítések",
|
||||
"messages": "Üzenetek",
|
||||
"downloads": "Letöltések",
|
||||
"stats": "Statisztika",
|
||||
"scheduler": "Ütemező",
|
||||
"config": "Konfig",
|
||||
"users": "Fiókok",
|
||||
"users": "Felhasználók",
|
||||
"audit": "Audit",
|
||||
"logs": "Napló",
|
||||
"settings": "Beállítások"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"actionNeeded": "Teendő",
|
||||
"dismiss": "Elvetés",
|
||||
"dismiss": "Elrejtés",
|
||||
"findInFeed": "Keresés a Feedben",
|
||||
"unhide": "Megjelenítés",
|
||||
"unwatch": "Megtekintés visszavonása",
|
||||
"openInManager": "Csatornakezelő",
|
||||
"openInManager": "Megnyitás a Csatornákban",
|
||||
"openOnYouTube": "Megnyitás a YouTube-on"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"write": {
|
||||
"title": "Csatlakoztatva",
|
||||
"importFailed": "A YouTube-fiókod csatlakoztatva, de a feliratkozások importálása sikertelen volt — újrapróbálhatod a Beállítások → Szinkronizálás menüben. ",
|
||||
"importFailed": "A YouTube-fiókod csatlakoztatva, de nem sikerült importálni a feliratkozásaidat — újrapróbálhatod a Beállítások → Szinkronizálás menüben. ",
|
||||
"feedReady": "A Feeded készen áll{{channels}}. Választhatóan engedélyezheted, hogy a Siftlode ",
|
||||
"feedReadyChannels": " ({{count}} csatorna)",
|
||||
"rationale": "<0>leiratkozzon</0> helyetted a YouTube-csatornákról — ehhez egy plusz írási engedély szükséges. Hagyd ki, ha csak olvasható módban szeretnél maradni; ezt később bármikor engedélyezheted a Beállításokban.",
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"manageGroup": "Kezelés",
|
||||
"listGroup": "Listáid",
|
||||
"watchLater": "Megnézem később",
|
||||
"syncYoutube": "Szinkron YouTube-ról",
|
||||
"syncYoutube": "Szinkronizálás a YouTube-ról",
|
||||
"syncedToast": "{{count}} lista szinkronizálva a YouTube-ról",
|
||||
"syncFailed": "Nem sikerült a YouTube-szinkron",
|
||||
"syncFailed": "Nem sikerült szinkronizálni a YouTube-ról",
|
||||
"noneYet": "Még nincs lejátszási lista",
|
||||
"newPlaylist": "Új lista…",
|
||||
"searchPlaceholder": "Listák keresése…",
|
||||
@@ -19,20 +19,21 @@
|
||||
"delete": "Törlés",
|
||||
"rename": "Átnevezés",
|
||||
"confirmDelete": "Törlöd a(z) „{{name}}” listát? Nem vonható vissza.",
|
||||
"addToPlaylist": "Hozzáadás listához",
|
||||
"addToPlaylist": "Hozzáadás listához…",
|
||||
"addToPlaylistTitle": "Hozzáadás listához",
|
||||
"exportToYoutube": "Exportálás YouTube-ra",
|
||||
"pushToYoutube": "Szinkron YouTube-ra",
|
||||
"pushToYoutube": "Szinkronizálás a YouTube-ra",
|
||||
"unsynced": "Nincs szinkronizálva",
|
||||
"unsyncedHint": "Vannak helyi módosításaid, amelyek még nincsenek a YouTube-ra szinkronizálva.",
|
||||
"openOnYoutube": "Megnyitás YouTube-on",
|
||||
"revert": "Visszaállítás YouTube-ról",
|
||||
"revertTitle": "Eldobod a helyi módosításokat?",
|
||||
"revertMsg": "Ez újratölti a listát a YouTube-ról, és eldobja a nem szinkronizált helyi módosításaidat (sorrend, hozzáadás, eltávolítás, átnevezés). Folytatod?",
|
||||
"revertConfirm": "Eldobás és újratöltés",
|
||||
"revertDone": "„{{name}}” újratöltve a YouTube-ról ✓",
|
||||
"revertFailed": "Nem sikerült újratölteni a YouTube-ról.",
|
||||
"pushTitle": "Szinkron YouTube-ra",
|
||||
"pushConfirm": "Szinkron",
|
||||
"revertTitle": "Elveted a helyi módosításokat?",
|
||||
"revertMsg": "Ez újratölti a listát a YouTube-ról, és elveti a nem szinkronizált helyi módosításaidat (sorrend, hozzáadás, eltávolítás, átnevezés). Folytatod?",
|
||||
"revertConfirm": "Elvetés és újratöltés",
|
||||
"revertDone": "„{{name}}” szinkronizálva a YouTube-ról",
|
||||
"revertFailed": "Nem sikerült szinkronizálni a YouTube-ról",
|
||||
"pushTitle": "Szinkronizálás a YouTube-ra",
|
||||
"pushConfirm": "Szinkronizálás",
|
||||
"pushPlanCreate": "Létrehozol egy új YouTube-listát {{count}} videóval? (~{{units}} kvótaegység; ma még {{left}} maradt.)",
|
||||
"mergeAddLocal_one": "{{count}} videó bekerül a Siftlode-listádba a YouTube-ról.",
|
||||
"mergeAddLocal_other": "{{count}} videó bekerül a Siftlode-listádba a YouTube-ról.",
|
||||
@@ -50,19 +51,19 @@
|
||||
"mergeQuota": " (~{{units}} kvótaegység; ma még {{left}} maradt.)",
|
||||
"mergeQuotaFree": " (~0 kvótaegység.)",
|
||||
"pushNoQuota": "Nincs elég YouTube-kvóta mára ({{left}}) ehhez a szinkronhoz (~{{units}} kell). Próbáld újra holnap.",
|
||||
"pushUpToDate": "A(z) „{{name}}” már szinkronban van a YouTube-bal.",
|
||||
"pushDone": "„{{name}}” szinkronizálva a YouTube-ra ✓",
|
||||
"pushPartial": "Szinkronizálva, de {{count}} elem kimaradt.",
|
||||
"pushFailed": "Nem sikerült a YouTube-ra szinkronizálás.",
|
||||
"pushUpToDate": "A(z) „{{name}}” már szinkronban van a YouTube-bal",
|
||||
"pushDone": "„{{name}}” szinkronizálva a YouTube-ra",
|
||||
"pushPartial": "Szinkronizálva, de {{count}} videó kimaradt",
|
||||
"pushFailed": "Nem sikerült szinkronizálni a YouTube-ra",
|
||||
"deleteHereOnly": "Törlés csak itt",
|
||||
"deleteYtFailed": "Nem sikerült törölni a YouTube-on; csak itt lett eltávolítva.",
|
||||
"deleteYtFailed": "Nem sikerült törölni a YouTube-on; csak itt törölve",
|
||||
"sortLabel": "Rendezés",
|
||||
"dirAsc": "Növekvő",
|
||||
"dirDesc": "Csökkenő",
|
||||
"groupByChannel": "Csoportosítás csatorna szerint",
|
||||
"railSortCustom": "Egyéni sorrend",
|
||||
"railSortName": "Név",
|
||||
"railSortCount": "Elemszám",
|
||||
"railSortCount": "Videók száma",
|
||||
"railSortDuration": "Összhossz",
|
||||
"railSortRecent": "Utoljára módosított",
|
||||
"dirtyFirst": "Nem szinkronizáltak elöl",
|
||||
@@ -72,24 +73,24 @@
|
||||
"rowMenu": "Videó műveletek",
|
||||
"moveTo": "Áthelyezés másik listába",
|
||||
"moveToRecent": "Áthelyezés ide: „{{name}}”",
|
||||
"moveNoTargets": "Még nincs másik lejátszási lista, ahová áthelyezhetnéd.",
|
||||
"movedToast": "„{{title}}” áthelyezve ide: „{{name}}” ✓",
|
||||
"moveFailed": "Nem sikerült áthelyezni a videót.",
|
||||
"moveNoTargets": "Még nincs másik lejátszási lista, ahová áthelyezhetnéd",
|
||||
"movedToast": "„{{title}}” áthelyezve ide: „{{name}}”",
|
||||
"moveFailed": "Nem sikerült áthelyezni a videót",
|
||||
"removedToast": "„{{title}}” eltávolítva a listáról",
|
||||
"undoRemoveFailed": "Nem sikerült visszatenni a videót.",
|
||||
"removeFailed": "Nem sikerült eltávolítani a videót.",
|
||||
"undoRemoveFailed": "Nem sikerült visszatenni a videót",
|
||||
"removeFailed": "Nem sikerült eltávolítani a videót",
|
||||
"errors": {
|
||||
"name_required": "Adj nevet a lejátszási listának.",
|
||||
"youtube_read_scope": "A listák szinkronizálásához kapcsold össze a YouTube-ot (olvasási hozzáférés).",
|
||||
"not_on_youtube": "Ez a lejátszási lista nincs fent a YouTube-on.",
|
||||
"watch_later_not_pushable": "A „Megnézem később” listát nem lehet a YouTube-ra küldeni.",
|
||||
"watch_later_undeletable": "A „Megnézem később” lista nem törölhető.",
|
||||
"youtube_editing_off": "Előbb engedélyezd a YouTube-szerkesztést a Beállításokban.",
|
||||
"quota_exceeded": "Mára nem maradt elég YouTube-keret ehhez a szinkronhoz."
|
||||
"name_required": "Adj nevet a lejátszási listának",
|
||||
"youtube_read_scope": "A listák szinkronizálásához kapcsold össze a YouTube-ot (olvasási hozzáférés)",
|
||||
"not_on_youtube": "Ez a lejátszási lista nincs fent a YouTube-on",
|
||||
"watch_later_not_pushable": "A „Megnézem később” listát nem lehet a YouTube-ra küldeni",
|
||||
"watch_later_undeletable": "A „Megnézem később” lista nem törölhető",
|
||||
"youtube_editing_off": "Előbb engedélyezd a YouTube-szerkesztést a Beállításokban",
|
||||
"quota_exceeded": "Mára nem maradt elég YouTube-keret ehhez a szinkronhoz"
|
||||
},
|
||||
"moveErrors": {
|
||||
"not_in_playlist": "Ez a videó már nincs ebben a listában — időközben máshol megváltozott.",
|
||||
"same_playlist": "Ez a videó már ebben a listában van."
|
||||
"not_in_playlist": "Ez a videó már nincs ebben a listában — időközben máshol megváltozott",
|
||||
"same_playlist": "Ez a videó már ebben a listában van"
|
||||
},
|
||||
"railMenu": "Lejátszási lista műveletei",
|
||||
"builtinLocked": "A beépített „{{name}}” lista nem nevezhető át és nem törölhető.",
|
||||
@@ -102,10 +103,10 @@
|
||||
"saveOrder": "Sorrend mentése",
|
||||
"filteredCount": "{{count}} látszik",
|
||||
"noneMatchFilter": "Egy videó sem felel meg a szűrőnek.",
|
||||
"loadFailed": "Nem sikerült betölteni ezt a listát.",
|
||||
"reorderFailed": "Nem sikerült elmenteni az új sorrendet.",
|
||||
"renameFailed": "Nem sikerült átnevezni a listát.",
|
||||
"deleteFailed": "Nem sikerült törölni a listát.",
|
||||
"loadFailed": "Nem sikerült betölteni ezt a listát",
|
||||
"reorderFailed": "Nem sikerült elmenteni az új sorrendet",
|
||||
"renameFailed": "Nem sikerült átnevezni a listát",
|
||||
"deleteFailed": "Nem sikerült törölni a listát",
|
||||
"confirmDeleteLinked": "Törlöd a(z) „{{name}}” listát? A YouTube-bal szinkronban van — válaszd ki, hogy ott is törlődjön-e. Nem vonható vissza.",
|
||||
"dragBlockedBuiltin": "Ennek a listának kötött a sorrendje — kézzel nem rendezhető át.",
|
||||
"deleteBoth": "Törlés itt és a YouTube-on",
|
||||
@@ -114,7 +115,7 @@
|
||||
"viewDiffers": "Ideiglenes nézet ({{view}}) — a lista mentett sorrendje változatlan. Mentsd, ha ez maradjon.",
|
||||
"viewMatches": "Ez a nézet ({{view}}) megegyezik a lista mentett sorrendjével, így nincs mit menteni.",
|
||||
"viewFiltered": "Szűrt nézet — {{total}} elemből {{shown}} látszik. Szűrés közben a sorrend nem menthető.",
|
||||
"addFailed": "Nem sikerült hozzáadni a lejátszási listához.",
|
||||
"addFailed": "Nem sikerült hozzáadni a lejátszási listához",
|
||||
"limits": {
|
||||
"hint": "Hiányzik egy lista?",
|
||||
"why": "Miért?",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"playAll": "Összes lejátszása",
|
||||
"shuffle": "Keverés",
|
||||
"rename": "Átnevezés",
|
||||
"addItem": "Cím hozzáadása",
|
||||
"addItem": "Cím hozzáadása…",
|
||||
"delete": "Törlés",
|
||||
"enableEdit": "Szerkesztés engedélyezése",
|
||||
"removeItem": "Eltávolítás a kollekcióból",
|
||||
@@ -21,15 +21,16 @@
|
||||
"add": "Hozzáad",
|
||||
"added": "Hozzáadva",
|
||||
"deleteConfirm": "Törlöd a(z) „{{title}}” kollekciót? Ez a Plexből is eltávolítja.",
|
||||
"editFailed": "A kollekció szerkesztése sikertelen",
|
||||
"playFailed": "Nem sikerült összeállítani a lejátszási sort — próbáld újra.",
|
||||
"playEmpty": "Ebben a kollekcióban még nincs mit lejátszani.",
|
||||
"newCollection": "Új kollekció",
|
||||
"editFailed": "Nem sikerült szerkeszteni a kollekciót",
|
||||
"playFailed": "Nem sikerült összeállítani a lejátszási sort — próbáld újra",
|
||||
"playEmpty": "Ebben a kollekcióban még nincs mit lejátszani",
|
||||
"newCollection": "Új kollekció…",
|
||||
"newCollectionTitle": "Új kollekció",
|
||||
"namePlaceholder": "Kollekció neve",
|
||||
"nameTaken": "Már létezik ilyen nevű kollekció.",
|
||||
"nameTaken": "Már létezik ilyen nevű kollekció",
|
||||
"newHint": "Válaszd ki az első címet — egy kollekcióhoz legalább egy elem kell.",
|
||||
"createWith": "Létrehozás",
|
||||
"deleteFailed": "Nem sikerült törölni a gyűjteményt."
|
||||
"deleteFailed": "Nem sikerült törölni a gyűjteményt"
|
||||
},
|
||||
"searchPlaceholder": "Keresés a Plexben…",
|
||||
"searchField": {
|
||||
@@ -78,9 +79,9 @@
|
||||
"duration": "Hossz",
|
||||
"durationUnit": "perc",
|
||||
"durationOpt": {
|
||||
"short": "90 perc alatt",
|
||||
"mid": "90–120 perc",
|
||||
"long": "2 óra felett"
|
||||
"short": "90 p alatt",
|
||||
"mid": "90–120 p",
|
||||
"long": "2 ó felett"
|
||||
},
|
||||
"added": "Plexhez adva",
|
||||
"addedOpt": {
|
||||
@@ -128,7 +129,7 @@
|
||||
"markNextWatched": "S{{season}}·E{{episode}} megnézettnek jelöl",
|
||||
"markNextUnwatched": "S{{season}}·E{{episode}} nem-nézettnek jelöl",
|
||||
"nextUp": "Következik: S{{season}}·E{{episode}}",
|
||||
"watchedFailed": "Nem sikerült frissíteni a megnézett állapotot — próbáld újra.",
|
||||
"watchedFailed": "Nem sikerült frissíteni a megnézett állapotot — próbáld újra",
|
||||
"seasons": "{{count}} évad",
|
||||
"unified": {
|
||||
"titles": "Címek",
|
||||
@@ -159,7 +160,7 @@
|
||||
"delete": "Törlés",
|
||||
"deleteConfirm": "Törlöd a(z) „{{title}}” kollekciót a Plexből? Ez minden felhasználót érint.",
|
||||
"note": "A változtatások a Plexbe íródnak, és minden kliensen/felhasználónál megjelennek.",
|
||||
"others": "{{count}} meglévő kollekció kezelése…",
|
||||
"others": "{{count}} meglévő kollekció kezelése",
|
||||
"makeEditable": "Kezelem"
|
||||
},
|
||||
"player": {
|
||||
@@ -174,8 +175,8 @@
|
||||
"restart": "Újra az elejéről",
|
||||
"prev": "Előző rész",
|
||||
"next": "Következő rész",
|
||||
"tapBack": "−{{n}} mp",
|
||||
"tapForward": "+{{n}} mp",
|
||||
"tapBack": "−{{d}}",
|
||||
"tapForward": "+{{d}}",
|
||||
"mute": "Némítás (m)",
|
||||
"volume": "Hangerő",
|
||||
"fullscreen": "Teljes képernyő (f)",
|
||||
@@ -186,7 +187,7 @@
|
||||
"subOff": "Ki",
|
||||
"errNotFound": "Ez a fájl nem játszható le — a média nem érhető el a szerveren (hiányzik a Plex media-mount, vagy hibás az útvonal-leképezés). Kérd meg az adminisztrátort, hogy ellenőrizze a Plex beállításait.",
|
||||
"errTranscode": "Ennek a fájlnak a formátuma transzkódolást igényel, ami még nem támogatott.",
|
||||
"errGeneric": "A lejátszást nem sikerült elindítani. Próbáld újra.",
|
||||
"errGeneric": "Nem sikerült elindítani a lejátszást. Próbáld újra.",
|
||||
"help": "Billentyűparancsok",
|
||||
"infoBtn": "Média infó",
|
||||
"more": "Továbbiak",
|
||||
@@ -222,9 +223,9 @@
|
||||
"subStyle": "Feliratok",
|
||||
"subPreparing": "Felirat előkészítése: {{label}}…",
|
||||
"subUnavailable": {
|
||||
"bitmap": "Ez a felirat képalapú, szövegként nem jeleníthető meg.",
|
||||
"missing": "Ez a feliratsáv nem érhető el.",
|
||||
"failed": "Ezt a feliratot nem sikerült betölteni."
|
||||
"bitmap": "Ez a felirat képalapú, szövegként nem jeleníthető meg",
|
||||
"missing": "Ez a feliratsáv nem érhető el",
|
||||
"failed": "Nem sikerült betölteni ezt a feliratot"
|
||||
},
|
||||
"subSize": "Szövegméret",
|
||||
"subPos": "Pozíció",
|
||||
@@ -294,16 +295,16 @@
|
||||
"transcode_sw": "Szoftveresen átkódolva (legnehezebb)"
|
||||
},
|
||||
"playlistAdd": {
|
||||
"manage": "Listához adás",
|
||||
"manage": "Listához adás…",
|
||||
"title": "Listához adás — {{title}}",
|
||||
"newPlaceholder": "Új lista neve…",
|
||||
"create": "Létrehozás",
|
||||
"none": "Még nincs lista. Hozz létre egyet fent.",
|
||||
"count": "{{count}} elem",
|
||||
"groupProgress": "{{in}} / {{size}}",
|
||||
"addFailed": "Nem sikerült hozzáadni a lejátszási listához.",
|
||||
"removeFailed": "Nem sikerült eltávolítani a lejátszási listáról.",
|
||||
"createFailed": "Nem sikerült létrehozni a lejátszási listát."
|
||||
"addFailed": "Nem sikerült hozzáadni a lejátszási listához",
|
||||
"removeFailed": "Nem sikerült eltávolítani a lejátszási listáról",
|
||||
"createFailed": "Nem sikerült létrehozni a lejátszási listát"
|
||||
},
|
||||
"playlist": {
|
||||
"section": "Lejátszási listák",
|
||||
@@ -326,10 +327,10 @@
|
||||
"addShow": "Egész sorozat listához adása",
|
||||
"addSeason": "Évad listához adása",
|
||||
"addEpisode": "Epizód listához adása",
|
||||
"removeFailed": "Nem sikerült eltávolítani a lejátszási listáról.",
|
||||
"renameFailed": "Nem sikerült átnevezni a lejátszási listát.",
|
||||
"deleteFailed": "Nem sikerült törölni a lejátszási listát.",
|
||||
"reorderFailed": "Nem sikerült menteni az új sorrendet.",
|
||||
"removeFailed": "Nem sikerült eltávolítani a lejátszási listáról",
|
||||
"renameFailed": "Nem sikerült átnevezni a lejátszási listát",
|
||||
"deleteFailed": "Nem sikerült törölni a lejátszási listát",
|
||||
"reorderFailed": "Nem sikerült menteni az új sorrendet",
|
||||
"reorderItem": "„{{title}}” áthelyezése",
|
||||
"rowMenu": "Elem műveletei",
|
||||
"menu": "Lista műveletei",
|
||||
@@ -337,7 +338,7 @@
|
||||
"playSeason": "Évad lejátszása",
|
||||
"removeFromPlaylist": "Eltávolítás a listáról",
|
||||
"removedToast": "„{{title}}” eltávolítva a listáról",
|
||||
"undoRemoveFailed": "Nem sikerült visszatenni a listára."
|
||||
"undoRemoveFailed": "Nem sikerült visszatenni a listára"
|
||||
},
|
||||
"people": {
|
||||
"count_one": "{{count}} cím",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"subscriptions_pull": "Feliratkozások beolvasása",
|
||||
"subscriptions_pull": "Feliratkozások szinkronizálása",
|
||||
"subscriptions_resync": "Automatikus feliratkozás-újraszinkron",
|
||||
"playlists_pull": "Lejátszási listák beolvasása",
|
||||
"playlists_pull": "Lejátszási listák szinkronizálása",
|
||||
"playlists_push": "Lejátszási listák feltöltése",
|
||||
"playlists_delete": "Lejátszási lista törlése",
|
||||
"videos_backfill_recent": "Friss feltöltések betöltése",
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"shotFailed": "Nem sikerült elmenteni a képernyőképet",
|
||||
"stopFailed": "Nem sikerült leállítani a rögzítést",
|
||||
"stopped": "Rögzítés leállítva",
|
||||
"expired_one": "A rögzítés elérte a {{count}} perces határt, és leállt.",
|
||||
"expired_other": "A rögzítés elérte a {{count}} perces határt, és leállt."
|
||||
"expired_one": "A rögzítés elérte a {{count}} perces határt, és leállt",
|
||||
"expired_other": "A rögzítés elérte a {{count}} perces határt, és leállt"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Hibakereső munkamenet",
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"composer": {
|
||||
"title": "Hibajegy",
|
||||
"meta": "{{start}} · {{minutes}} perc · {{level}}",
|
||||
"meta": "{{start}} · {{minutes}} · {{level}}",
|
||||
"fieldTitle": "Cím",
|
||||
"titlePlaceholder": "Mi romlott el?",
|
||||
"description": "Leírás",
|
||||
@@ -75,7 +75,7 @@
|
||||
"reviewDone": "Kész",
|
||||
"keepRow": "Sor megtartása: {{event}} {{msg}}",
|
||||
"screenshot": "{{n}}. képernyőkép",
|
||||
"removeShot": "{{n}}. képernyőkép kivétele",
|
||||
"removeShot": "{{n}}. képernyőkép törlése",
|
||||
"drop": "Kép beillesztése: Ctrl+V, vagy húzd ide, vagy",
|
||||
"pick": "válassz fájlt",
|
||||
"close": "Bezárás",
|
||||
@@ -85,7 +85,7 @@
|
||||
"saveFailed": "Nem sikerült menteni a vázlatot",
|
||||
"loadFailed": "Nem sikerült betölteni a munkamenetet",
|
||||
"truncated": "A napló az első {{kept}} sort tartalmazza, összesen {{total}} volt.",
|
||||
"sent": "Elküldve hibajegyként.",
|
||||
"sent": "Elküldve hibajegyként",
|
||||
"openIssue": "Hibajegy megnyitása",
|
||||
"env": {
|
||||
"version": "Verzió",
|
||||
@@ -99,7 +99,7 @@
|
||||
"sentOk": "Hibajegy létrehozva",
|
||||
"sendFailed": "Nem sikerült elküldeni a hibajegyet",
|
||||
"target": "Cél: {{repo}} · privát",
|
||||
"partial": "A hibajegy létrejött, de nem ment el minden csatolmány: {{files}}.",
|
||||
"partial": "A hibajegy létrejött, de nem ment el minden csatolmány: {{files}}",
|
||||
"retry": "Hiányzók küldése",
|
||||
"edit": "Szerkesztés",
|
||||
"preview": "Előnézet",
|
||||
@@ -108,7 +108,7 @@
|
||||
"previewLocal": "Hibajegy-kezelő nélkül ez a zip issue.md fájlja.",
|
||||
"shotsFailed": "Nem sikerült betölteni a képernyőképeket",
|
||||
"removeShotFailed": "Nem sikerült törölni a képernyőképet",
|
||||
"partialImages": "A hibajegy létrejött, de a képernyőképek még nem látszanak a szövegében.",
|
||||
"partialImages": "A hibajegy létrejött, de a képernyőképek még nem látszanak a szövegében",
|
||||
"retryImages": "Megjelenítés a hibajegyben",
|
||||
"sendingNow": "A hibajegy küldése folyamatban…",
|
||||
"exportFailed": "Nem sikerült elkészíteni a zipet"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"quotaRemaining": "{{formatted}} maradt",
|
||||
"quotaNote": "Közös YouTube Data API-keret; éjfélkor (US Pacific) nullázódik. A backfill hátrébb sorol, hogy maradjon hely az interaktív használatnak.",
|
||||
"minutes": "perc",
|
||||
"range": "{{min}} és {{max}} között lehet.",
|
||||
"range": "{{min}} és {{max}} között lehet",
|
||||
"editInterval": "Kattints a gyakoriság módosításához",
|
||||
"intervalFailed": "Nem sikerült módosítani a gyakoriságot",
|
||||
"runNow": "Futtatás most",
|
||||
@@ -85,8 +85,8 @@
|
||||
"backfill": "Backfill (friss + teljes előzmény)",
|
||||
"autotag": "Automatikus címkézés",
|
||||
"shorts": "Shorts-besorolás",
|
||||
"subscriptions": "Feliratkozások újraszinkronja",
|
||||
"playlist_sync": "YouTube lejátszási listák szinkronja",
|
||||
"subscriptions": "Feliratkozások szinkronizálása a YouTube-ról",
|
||||
"playlist_sync": "Lejátszási listák szinkronizálása a YouTube-ról",
|
||||
"maintenance": "Karbantartás + ellenőrzés",
|
||||
"demo_reset": "Demo fiók visszaállítása",
|
||||
"explore_cleanup": "Felfedezett csatornák takarítása",
|
||||
@@ -111,7 +111,7 @@
|
||||
"liveRefreshHint": "Élő/közelgő videók (és a frissen véget ért, hossz nélküli adások), amíg le nem ülepednek."
|
||||
},
|
||||
"purgeDiscovery": "Felfedezés ürítése",
|
||||
"purgeDiscoveryHint": "Most eltávolítja az összes meg nem tartott felfedezés-tartalmat (senki által meg nem nézett/mentett keresési találatok és felfedezett, de nem követett csatornák) — a türelmi időt figyelmen kívül hagyva.",
|
||||
"purgeDiscoveryHint": "Most törli az összes meg nem tartott felfedezés-tartalmat (senki által meg nem nézett/mentett keresési találatok és felfedezett, de nem követett csatornák) — a türelmi időt figyelmen kívül hagyva.",
|
||||
"purgeConfirmTitle": "Felfedezés-tartalom törlése?",
|
||||
"purgedDiscovery": "{{count}} felfedezés-elem eltávolítva"
|
||||
"purgedDiscovery": "{{count}} felfedezés-elem törölve"
|
||||
}
|
||||
|
||||
@@ -36,17 +36,17 @@
|
||||
"railLabels": "Feliratok az oldalsó menüben",
|
||||
"railLabelsHint": "Rövid név minden ikon alatt a bal oldali menüben. A menü ugyanolyan keskeny marad, csak magasabb lesz.",
|
||||
"startup": "Indulás",
|
||||
"startOnHome": "Indítás a kezdőlapon",
|
||||
"startOnHome": "Indítás a Kezdőlapon",
|
||||
"startOnHomeHint": "Minden induláskor a „Mit nézzek most?” oldal nyílik meg. Kikapcsolva ott folytatod, ahol abbahagytad.",
|
||||
"textSize": "Betűméret"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Értesítések",
|
||||
"sound": "Hang riasztásoknál",
|
||||
"sound": "Hang értesítéseknél",
|
||||
"soundHint": "Rövid hangjelzés, ha egy értesítés a figyelmedet igényli (pl. hibák, kérdések).",
|
||||
"autoDismiss": "Buborékok automatikus eltüntetése",
|
||||
"autoDismiss": "Buborékok automatikus elrejtése",
|
||||
"autoDismissHint": "Kikapcsolva a felugró buborékok addig maradnak, amíg be nem zárod őket. Bekapcsolva a beállított idő után elhalványulnak; a gombos (Visszavonás) legalább 8 másodpercig marad.",
|
||||
"dismissAfter": "Eltüntetés ennyi után",
|
||||
"dismissAfter": "Elrejtés ennyi után",
|
||||
"sendTest": "Tesztértesítés küldése",
|
||||
"testTitle": "Tesztértesítés",
|
||||
"testMessage": "Így néz ki egy értesítés."
|
||||
@@ -73,7 +73,7 @@
|
||||
"deleteTitle": "Törlöd a fiókodat?",
|
||||
"deleteConfirm": "Ez véglegesen törli a fiókodat és minden adatodat (feliratkozások, címkék, megnézett/mentett/elrejtett állapot, lejátszási listák, beállítások). Nem vonható vissza.",
|
||||
"deleteConfirmButton": "Minden törlése",
|
||||
"demoNotice": "Ez a közös demo fiók. Nincs YouTube-kapcsolata — böngészheted a teljes közös könyvtárat, szűrheted és rendezheted, listákat építhetsz és kipróbálhatsz dolgokat, de semmi nem nyúl valódi YouTube-fiókhoz.",
|
||||
"demoNotice": "Ez a közös demo fiók. Nincs YouTube-kapcsolata — böngészheted a teljes közös Könyvtárat, szűrheted és rendezheted, listákat építhetsz és kipróbálhatsz dolgokat, de semmi nem nyúl valódi YouTube-fiókhoz.",
|
||||
"signInMethods": "Bejelentkezési módok",
|
||||
"googleLink": {
|
||||
"title": "Google-fiók",
|
||||
@@ -81,13 +81,13 @@
|
||||
"connectedHint": "A Google-fiókod össze van kapcsolva. Bejelentkezhetsz Google-lel, és alább engedélyezheted a YouTube-hozzáférést.",
|
||||
"connectHint": "Kapcsolj össze egy Google-fiókot, hogy Google-lel jelentkezhess be, és engedélyezhesd a YouTube-hozzáférést a Feededhez.",
|
||||
"connect": "Google összekapcsolása",
|
||||
"linked": "Google-fiók összekapcsolva.",
|
||||
"conflict": "Ez a Google-fiók már egy másik Siftlode-fiókhoz van kapcsolva.",
|
||||
"linked": "Google-fiók összekapcsolva",
|
||||
"conflict": "Ez a Google-fiók már egy másik Siftlode-fiókhoz van kapcsolva",
|
||||
"mismatch": "Ez nem az a Google-fiók, amely ide már össze van kapcsolva. Az összekapcsolttal jelentkezz be.",
|
||||
"error": "Nem sikerült összekapcsolni a Google-fiókot. Próbáld újra.",
|
||||
"signedInAs": "Google-belépés ezzel a címmel:"
|
||||
},
|
||||
"googleCancelled": "A Google-belépés megszakadt.",
|
||||
"googleCancelled": "A Google-belépés megszakadt",
|
||||
"password": {
|
||||
"title": "Jelszó",
|
||||
"setHint": "Van beállított jelszó. Bejelentkezhetsz e-mail-címmel és jelszóval.",
|
||||
@@ -98,8 +98,8 @@
|
||||
"new": "Új jelszó (min. 10 karakter)",
|
||||
"save": "Jelszó mentése",
|
||||
"saving": "Mentés…",
|
||||
"saved": "Jelszó frissítve ehhez: {{email}}.",
|
||||
"failed": "Nem sikerült frissíteni a jelszót."
|
||||
"saved": "Jelszó frissítve ehhez: {{email}}",
|
||||
"failed": "Nem sikerült frissíteni a jelszót"
|
||||
},
|
||||
"sessions": {
|
||||
"title": "Aktív munkamenetek",
|
||||
@@ -107,7 +107,7 @@
|
||||
"action": "Kijelentkezés máshonnan",
|
||||
"confirmTitle": "Kijelentkezés a többi munkamenetből?",
|
||||
"confirmBody": "Ez kijelentkeztet minden más böngészőből és eszközről. Itt bejelentkezve maradsz.",
|
||||
"done": "Kijelentkeztetve minden más munkamenetből.",
|
||||
"done": "Kijelentkeztetve minden más munkamenetből",
|
||||
"failed": "Nem sikerült kijelentkeztetni a többi munkamenetet. Próbáld újra."
|
||||
}
|
||||
},
|
||||
@@ -116,21 +116,21 @@
|
||||
"intro": "Egyes oldalak (pl. egy zárt Facebook-csoport, aminek tagja vagy) csak bejelentkezett tagoknak mutatják a videót. Illeszd be ide a böngésződ cookie-jait egy oldalhoz, és az onnan induló letöltések a te bejelentkezésedet használják. A cookie-k titkosítva, csak a te fiókodhoz kötve tárolódnak, és kizárólag az adott oldalnak küldjük el őket.",
|
||||
"configured": "Beállítva · frissítve: {{when}}",
|
||||
"notSet": "Nincs beállítva",
|
||||
"set": "Cookie-k beállítása",
|
||||
"update": "Frissítés",
|
||||
"remove": "Eltávolítás",
|
||||
"set": "Cookie-k beállítása…",
|
||||
"update": "Frissítés…",
|
||||
"remove": "Törlés",
|
||||
"save": "Mentés",
|
||||
"cancel": "Mégse",
|
||||
"saving": "Mentés…",
|
||||
"placeholder": "Illeszd be ide a cookies.txt tartalmát (Netscape formátum)",
|
||||
"help": "Exportálj cookies.txt-t ehhez az oldalhoz egy böngészőbővítménnyel, majd illeszd be ide. Csak írható — sosem mutatjuk vissza.",
|
||||
"saved": "Cookie-k mentve.",
|
||||
"removed": "Cookie-k eltávolítva.",
|
||||
"failed": "A cookie-kat nem sikerült menteni.",
|
||||
"removeConfirmTitle": "Cookie-k eltávolítása?",
|
||||
"saved": "Cookie-k mentve",
|
||||
"removed": "Cookie-k törölve",
|
||||
"failed": "Nem sikerült menteni a cookie-kat",
|
||||
"removeConfirmTitle": "Törlöd a cookie-kat?",
|
||||
"removeConfirmBody": "Az erről az oldalról induló letöltések nem használják tovább a bejelentkezésedet.",
|
||||
"removeConfirmAction": "Eltávolítás",
|
||||
"loadError": "Nem sikerült betölteni a letöltési hitelesítési beállításokat."
|
||||
"removeConfirmAction": "Törlés",
|
||||
"loadError": "Nem sikerült betölteni a letöltési hitelesítési beállításokat"
|
||||
},
|
||||
"demo": {
|
||||
"title": "Demo hozzáférés",
|
||||
@@ -160,14 +160,14 @@
|
||||
"add": "Hozzáadás",
|
||||
"decided": "{{count}} eldöntve",
|
||||
"approved": "Jóváhagyva: {{email}} — most már be tud jelentkezni",
|
||||
"approveFailed": "A jóváhagyás sikertelen",
|
||||
"denyFailed": "Az elutasítás sikertelen",
|
||||
"approveFailed": "Nem sikerült jóváhagyni a kérelmet",
|
||||
"denyFailed": "Nem sikerült elutasítani a kérelmet",
|
||||
"addedToWhitelist": "{{email}} hozzáadva a fehérlistához",
|
||||
"inviteHint": "Küldj neki meghívót. A cím hozzáadása soha nem küld levelet — ez igen.",
|
||||
"invitedAt": "Meghívva: {{time}}. Az újraküldés másodszor is levelet küld neki.",
|
||||
"inviteLabel": "Meghívó küldése ide: {{email}}",
|
||||
"invited": "Meghívó elküldve ide: {{email}}.",
|
||||
"inviteFailed": "Nem sikerült elküldeni a meghívót.",
|
||||
"invited": "Meghívó elküldve ide: {{email}}",
|
||||
"inviteFailed": "Nem sikerült elküldeni a meghívót",
|
||||
"inviteTitle": "Küldesz meghívót?",
|
||||
"inviteConfirm": "Levél megy a(z) {{email}} címre arról, hogy a címe engedélyezve van, és nyitva áll előtte a Siftlode.",
|
||||
"inviteConfirmAgain": "Ezt a címet már meghívtad ekkor: {{time}} ({{email}}). Ha most küldesz, másodszor is írsz neki.",
|
||||
@@ -188,9 +188,9 @@
|
||||
"deleteTitle": "Törlöd a whitelista-bejegyzést?",
|
||||
"deleteConfirm": "Elfelejted {{email}} címet? A Siftlode eldobja a bejegyzést, így az adott címről újra kérelmezni kell.",
|
||||
"deleteHasAccount": "Erre a címre már regisztrált valaki — a Felhasználók és szerepek fülön kezelhető, ahol a fiók törlése ezt a bejegyzést is eltávolítja.",
|
||||
"deleted": "Whitelista-bejegyzés eltávolítva.",
|
||||
"deleteFailed": "A bejegyzést nem sikerült eltávolítani.",
|
||||
"emailOff": "Az e-mail-küldés nincs beállítva, így senki nem kap megerősítő linket, és e-mailben egy kérelem sem jut el hozzád. Ezt a listát és az értesítéseidet figyeld helyette, a javítás pedig a Konfiguráció → E-mail / SMTP fülön van.",
|
||||
"deleted": "Whitelista-bejegyzés törölve",
|
||||
"deleteFailed": "Nem sikerült törölni a bejegyzést",
|
||||
"emailOff": "Az e-mail-küldés nincs beállítva, így senki nem kap megerősítő linket, és e-mailben egy kérelem sem jut el hozzád. Ezt a listát és az Értesítéseket figyeld helyette, a javítás pedig a Konfiguráció → E-mail / SMTP fülön van.",
|
||||
"deletedStillAllowed": "A bejegyzés törölve, de ezt a címet a szerver konfigurációja továbbra is engedélyezi, tehát megmarad a hozzáférése. Vedd ki az ALLOWED_EMAILS / ADMIN_EMAILS listából, ha ezen változtatnál.",
|
||||
"deleteConfirmStillAllowed": "Elfelejted {{email}} címet? Figyelem: ezt a címet a szerver konfigurációja is engedélyezi (ALLOWED_EMAILS / ADMIN_EMAILS), így a bejegyzés törlése NEM veszi el a hozzáférését — sőt egy ADMIN_EMAILS cím a következő Google-belépéskor újra adminná válik."
|
||||
},
|
||||
@@ -215,14 +215,12 @@
|
||||
"server_one": "Ezt a Siftlode szervere is megkapja, és {{count}} napig őrzi, hogy a hibát ki lehessen nyomozni.",
|
||||
"server_other": "Ezt a Siftlode szervere is megkapja, és {{count}} napig őrzi, hogy a hibát ki lehessen nyomozni.",
|
||||
"never": "Jelszó, token, e-mail-cím és az üzeneteid tartalma soha nem kerül bele.",
|
||||
"lapse": "24 óra múlva magától visszaáll Alapra.",
|
||||
"lapse": "24 óra múlva magától visszaáll „Alap” szintre.",
|
||||
"policy": "Adatvédelmi irányelvek",
|
||||
"cancel": "Mégse",
|
||||
"confirm": "Bekapcsolom 24 órára"
|
||||
},
|
||||
"status": "{{level}} naplózás · visszaáll Alapra {{time}} múlva",
|
||||
"hoursMinutes": "{{h}} ó {{m}} p",
|
||||
"minutes": "{{m}} p",
|
||||
"status": "{{level}} naplózás · {{time}} múlva visszaáll „Alap” szintre",
|
||||
"revert": "Visszaállítás most",
|
||||
"demo": "A demo fiókot többen használják, ezért itt nem kapcsolható be részletes naplózás.",
|
||||
"failed": "Nem sikerült módosítani a naplózási szintet"
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
"desc": "Ez az első fiók, egyben adminisztrátor. A telepítés után ezzel az e-maillel és jelszóval jelentkezel be.",
|
||||
"email": "E-mail-cím",
|
||||
"password": "Jelszó (min. {{n}} karakter)",
|
||||
"invalidEmail": "Adj meg érvényes e-mail-címet.",
|
||||
"shortPassword": "A jelszó legalább {{n}} karakter legyen."
|
||||
"invalidEmail": "Adj meg érvényes e-mail-címet",
|
||||
"shortPassword": "A jelszó legalább {{n}} karakter legyen"
|
||||
},
|
||||
"google": {
|
||||
"title": "Google bejelentkezés (opcionális)",
|
||||
"desc": "Illeszd be a Google OAuth kliens-azonosítót és -titkot a Google-bejelentkezés és a YouTube-hozzáférés engedélyezéséhez. Hagyd üresen mindkettőt a kihagyáshoz — e-mail+jelszó enélkül is működik.",
|
||||
"clientId": "Google kliens-azonosító",
|
||||
"clientSecret": "Google kliens-titok",
|
||||
"bothRequired": "Add meg a kliens-azonosítót és a -titkot is, vagy hagyd üresen mindkettőt a kihagyáshoz.",
|
||||
"skipHint": "Hagyd üresen, ha később, az admin Configuration oldalon állítanád be."
|
||||
"bothRequired": "Add meg a kliens-azonosítót és a -titkot is, vagy hagyd üresen mindkettőt a kihagyáshoz",
|
||||
"skipHint": "Hagyd üresen, ha később, az admin Konfiguráció oldalon állítanád be."
|
||||
},
|
||||
"smtp": {
|
||||
"title": "E-mail / SMTP (opcionális)",
|
||||
@@ -29,9 +29,9 @@
|
||||
"password": "SMTP-jelszó",
|
||||
"testTo": "Teszt-e-mail ide…",
|
||||
"test": "Teszt küldése",
|
||||
"testOk": "Teszt-e-mail elküldve — nézd meg a postafiókot.",
|
||||
"testFail": "Nem sikerült elküldeni — ellenőrizd a beállításokat.",
|
||||
"skipHint": "Hagyd üresen, ha később, az admin Configuration oldalon állítanád be."
|
||||
"testOk": "Teszt-e-mail elküldve — nézd meg a postafiókot",
|
||||
"testFail": "Nem sikerült elküldeni — ellenőrizd a beállításokat",
|
||||
"skipHint": "Hagyd üresen, ha később, az admin Konfiguráció oldalon állítanád be."
|
||||
},
|
||||
"finish": {
|
||||
"title": "Minden kész!",
|
||||
@@ -46,5 +46,5 @@
|
||||
"title": "Setup-link szükséges",
|
||||
"body": "Nyisd meg a varázslót a konténer logjaiba az első indításkor kiírt linkkel (így néz ki: …/setup?token=…). Ez azért kell, hogy csak az üzemeltető tudja beállítani a példányt."
|
||||
},
|
||||
"genericError": "Valami hiba történt — próbáld újra."
|
||||
"genericError": "Valami hiba történt — próbáld újra"
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"year": "Év"
|
||||
},
|
||||
"year": {
|
||||
"more": "Több…",
|
||||
"more": "Több",
|
||||
"moreLabel": "Összes év",
|
||||
"none": "Még nincs év",
|
||||
"sortBy": "Rendezés",
|
||||
@@ -54,7 +54,7 @@
|
||||
"content": {
|
||||
"normal": "Normál",
|
||||
"shorts": "Shorts",
|
||||
"live": "Élő / Hamarosan"
|
||||
"live": "Élő / hamarosan"
|
||||
},
|
||||
"datePresetLabel": "Feltöltés dátuma",
|
||||
"datePreset": {
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"system": "Rendszer"
|
||||
},
|
||||
"title": "API-kvóta használat",
|
||||
"rangeDays": "{{count}} nap",
|
||||
"rangeDays": "{{d}}",
|
||||
"rangeLabel": "Időtartomány",
|
||||
"introBefore": "A YouTube Data API-egységek aszerint, hogy ki váltotta ki a felhasználást. A ",
|
||||
"introSystem": "Rendszer",
|
||||
"introAfter": " a megosztott háttérmunka (ütemezett letöltés, gazdagítás, újraszinkronizálás), amely nem köthető egy személyhez.",
|
||||
"loading": "Betöltés…",
|
||||
"noData": "Nincs adat.",
|
||||
"dailyTotal": "Napi összesen ({{count}} nap · {{units}} egység)",
|
||||
"dailyTotal": "Napi összesen ({{d}} · {{units}} egység)",
|
||||
"noUsageInRange": "Nincs használat ebben az időszakban.",
|
||||
"dailyTooltip": "{{day}}: {{units}} egység",
|
||||
"system": "Rendszer (háttér)",
|
||||
@@ -39,12 +39,12 @@
|
||||
"byAction": "Művelet szerint (30 nap)",
|
||||
"noUsage": "Még nincs használat.",
|
||||
"actions": "Műveletek",
|
||||
"syncSubscriptions": "Feliratkozások szinkronizálása",
|
||||
"syncSubscriptions": "Feliratkozások szinkronizálása a YouTube-ról",
|
||||
"syncSubscriptionsHint": "Újraimportálja a YouTube-feliratkozásaidat és behúzza az újonnan követett csatornákat.",
|
||||
"backfillEverything": "Minden letöltése",
|
||||
"backfillEverythingHint": "Teljes archívum letöltésének kérése minden csatornához, amelyre fel vagy iratkozva. A régebbi videók és a teljes keresés annyira töltődnek fel, amennyit a megosztott napi kvóta enged.",
|
||||
"synced": "{{count}} feliratkozás szinkronizálva",
|
||||
"syncFailed": "A szinkronizálás sikertelen",
|
||||
"syncFailed": "Nem sikerült szinkronizálni a feliratkozásokat",
|
||||
"fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához",
|
||||
"fullHistoryFailed": "Nem sikerült teljes előzményt kérni",
|
||||
"admin": "Adminisztrátor",
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"empty": "Még nincs címke — adj hozzá lentebb.",
|
||||
"newPlaceholder": "Új címke neve…",
|
||||
"add": "Hozzáad",
|
||||
"renameFailed": "Nem sikerült átnevezni a címkét.",
|
||||
"deleteFailed": "Nem sikerült törölni a címkét."
|
||||
"renameFailed": "Nem sikerült átnevezni a címkét",
|
||||
"deleteFailed": "Nem sikerült törölni a címkét"
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
"inactive": "aktiválásra vár",
|
||||
"activate": "Aktiválás",
|
||||
"activateHint": "Beengedi ezt a fiókot. A címe már jóvá van hagyva — ez csak befejezi.",
|
||||
"activated": "{{email}} mostantól be tud lépni.",
|
||||
"activateFailed": "Nem sikerült aktiválni a fiókot.",
|
||||
"activated": "{{email}} mostantól be tud lépni",
|
||||
"activateFailed": "Nem sikerült aktiválni a fiókot",
|
||||
"resend": "Megerősítő link újraküldése",
|
||||
"resendHint": "Újraküldi a megerősítő linket, a fiókon szereplő címre.",
|
||||
"resent": "Megerősítő link elküldve ide: {{email}}.",
|
||||
"resendFailed": "Nem sikerült elküldeni a megerősítő linket."
|
||||
"resent": "Megerősítő link elküldve ide: {{email}}",
|
||||
"resendFailed": "Nem sikerült elküldeni a megerősítő linket"
|
||||
},
|
||||
"roles": {
|
||||
"title": "Felhasználók és szerepek",
|
||||
@@ -42,7 +42,7 @@
|
||||
"demoteTitle": "Elveszed az admin jogot?",
|
||||
"promoteConfirm": "Teljes admin hozzáférést adsz neki: {{email}} (beállítások, ütemező, felhasználókezelés)?",
|
||||
"demoteConfirm": "Elveszed az admin jogot tőle: {{email}}? Sima felhasználó lesz.",
|
||||
"failed": "Nem sikerült módosítani a szerepkört."
|
||||
"failed": "Nem sikerült módosítani a szerepkört"
|
||||
},
|
||||
"suspend": {
|
||||
"action": "Felfüggeszt",
|
||||
@@ -55,7 +55,7 @@
|
||||
"undone": "{{email}} felfüggesztése feloldva",
|
||||
"demoLocked": "A demo fiók nem függeszthető fel.",
|
||||
"selfLocked": "A saját fiókodat nem függesztheted fel.",
|
||||
"failed": "Nem sikerült módosítani a felfüggesztést."
|
||||
"failed": "Nem sikerült módosítani a felfüggesztést"
|
||||
},
|
||||
"delete": {
|
||||
"action": "Törlés",
|
||||
@@ -63,7 +63,7 @@
|
||||
"title": "Törlöd a fiókot?",
|
||||
"confirm": "Véglegesen törlöd: {{email}} és minden adatát — feliratkozások, címkék, megtekintési előzmény, lejátszási listák és beállítások? Ez nem visszavonható.",
|
||||
"done": "{{email}} törölve",
|
||||
"failed": "Nem sikerült törölni a fiókot."
|
||||
"failed": "Nem sikerült törölni a fiókot"
|
||||
},
|
||||
"account": {
|
||||
"access": "Hozzáférés",
|
||||
@@ -75,7 +75,7 @@
|
||||
"approvalWaitingOnYou": "rád vár",
|
||||
"copied": "Másolva",
|
||||
"copy": "Másolás",
|
||||
"copyFailed": "Nem sikerült másolni — jelöld ki a linket, és másold kézzel.",
|
||||
"copyFailed": "Nem sikerült másolni — jelöld ki a linket, és másold kézzel",
|
||||
"email": "E-mail",
|
||||
"emailConfirmed": "megerősítve",
|
||||
"emailUnconfirmed": "nincs megerősítve",
|
||||
@@ -97,7 +97,7 @@
|
||||
"passwordSet": "be van állítva",
|
||||
"recovery": "Helyreállítás",
|
||||
"resend": "Megerősítő link küldése",
|
||||
"resendSent": "Megerősítő link elküldve.",
|
||||
"resendSent": "Megerősítő link elküldve",
|
||||
"setPassword": "Jelszóbeállító link készítése",
|
||||
"setPasswordAgain": "Új link készítése",
|
||||
"linkFailed": "Nem sikerült létrehozni a linket. Próbáld újra."
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"delete": "Törlés",
|
||||
"deleteConfirm": "Törlöd a(z) „{{name}}” mentett nézetet?",
|
||||
"reorder": "Húzd az átrendezéshez",
|
||||
"actionFailed": "Nem sikerült frissíteni a mentett nézeteket — próbáld újra.",
|
||||
"reorderFailed": "Nem sikerült menteni az új sorrendet — próbáld újra.",
|
||||
"actionFailed": "Nem sikerült frissíteni a mentett nézeteket — próbáld újra",
|
||||
"reorderFailed": "Nem sikerült menteni az új sorrendet — próbáld újra",
|
||||
"saved": "Mentett nézetek",
|
||||
"manage": "Nézetek kezelése…",
|
||||
"manageTitle": "Személyes nézetek",
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
},
|
||||
"preview": {
|
||||
"feed": "A Feed — szűrés csatorna, nyelv, téma, hossz és dátum szerint.",
|
||||
"channels": "Csatorna-kezelő",
|
||||
"channels": "Csatornák",
|
||||
"playlists": "Lejátszási listák",
|
||||
"player": "Beépített lejátszó",
|
||||
"downloads": "Letöltésközpont",
|
||||
"downloads": "Letöltések",
|
||||
"search": "Keresési találatok",
|
||||
"plex": "A Plex-könyvtárad"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
"body": "A beépített lejátszó onnan folytatja a videót, ahol abbahagytad — billentyű- és görgetés-vezérléssel."
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Letöltésközpont",
|
||||
"title": "Letöltések",
|
||||
"body": "Ments videókat a szerveredre Plex-barát elrendezésben, vágd vagy fűzd össze őket, majd oszd meg egy linkkel."
|
||||
},
|
||||
"plex": {
|
||||
@@ -61,8 +61,8 @@
|
||||
"email": "E-mail",
|
||||
"password": "Jelszó",
|
||||
"newPassword": "Új jelszó (min. {{min}} karakter)",
|
||||
"invalidEmail": "Adj meg egy érvényes e-mail-címet.",
|
||||
"weakPassword": "A jelszónak legalább {{min}} karakteresnek kell lennie.",
|
||||
"invalidEmail": "Adj meg egy érvényes e-mail-címet",
|
||||
"weakPassword": "A jelszónak legalább {{min}} karakteresnek kell lennie",
|
||||
"genericError": "Valami hiba történt. Próbáld újra.",
|
||||
"working": "Egy pillanat…",
|
||||
"signinButton": "Belépés",
|
||||
@@ -84,17 +84,17 @@
|
||||
"demoDenied": "Ez az e-mail nincs engedélyezve a demóhoz. Kérd az admint, hogy adja hozzá.",
|
||||
"registerDone": "Majdnem kész — nézd meg a postafiókod a megerősítő linkért, és a Spam mappát is, ha egy percen belül nem jön meg. Miután megerősítetted, egy admin jóváhagyja a fiókodat, mielőtt beléphetnél.",
|
||||
"forgotDone": "Ha ehhez az e-mailhez tartozik fiók, elküldtük a visszaállító linket. Nézd meg a postafiókod, és a Spam mappát is, ha egy percen belül nem érkezik meg.",
|
||||
"resetDone": "A jelszavad be van állítva — most már beléphetsz.",
|
||||
"resetDone": "A jelszavad be van állítva — most már beléphetsz",
|
||||
"deadLink": {
|
||||
"title": "Ez a link már nem érvényes",
|
||||
"body": "A jelszó-visszaállító link egy óráig él, és egyszer használható. Ez lejárt, már felhasználták, vagy olyan fiókhoz tartozik, ami már nem létezik — nincs mire jelszót állítani. Kérj újat, és elküldjük.",
|
||||
"short": "Ez a visszaállító link már nem érvényes.",
|
||||
"short": "Ez a visszaállító link már nem érvényes",
|
||||
"cta": "Új link kérése"
|
||||
},
|
||||
"verifyOk": "E-mail megerősítve. Amint egy admin jóváhagyja a fiókodat, beléphetsz.",
|
||||
"verifyOkReset": "E-mail megerősítve. A fiók regisztrációkori jelszavát biztonsági okból korábban töröltük — válassz újat a belépéshez.",
|
||||
"verifyOkResetCta": "Új jelszó beállítása",
|
||||
"verifyInvalid": "Ez a megerősítő link érvénytelen vagy lejárt.",
|
||||
"verifyInvalid": "Ez a megerősítő link érvénytelen vagy lejárt",
|
||||
"verifyUnreachable": "Nem sikerült elérni a szervert a link megerősítéséhez, így az nem is használódott el. Próbáld megnyitni újra egy pillanat múlva.",
|
||||
"resendCta": "Új link küldése",
|
||||
"resendDone": "Ha ehhez az e-mailhez tartozik megerősítetlen fiók, elküldtünk egy új megerősítő linket. Nézd meg a postafiókod, és a Spam mappát is, ha egy percen belül nem érkezik meg.",
|
||||
@@ -116,26 +116,26 @@
|
||||
"email_unverified": {
|
||||
"title": "Erősítsd meg az e-mail-címed",
|
||||
"body": "A jelszavad jó — csak addig nem engedhetünk be, amíg a fiókon lévő cím nincs megerősítve. Nyisd meg a kiküldött linket, vagy kérj újat.",
|
||||
"short": "Előbb erősítsd meg az e-mail-címed — nézd meg a postafiókod."
|
||||
"short": "Előbb erősítsd meg az e-mail-címed — nézd meg a postafiókod"
|
||||
},
|
||||
"pending_approval": {
|
||||
"title": "Jóváhagyásra vár",
|
||||
"body": "A jelszavad jó, a címed megerősítve. Már csak arra várunk, hogy egy admin beengedjen — szólunk e-mailben, amint döntött.",
|
||||
"short": "A fiókod admin jóváhagyásra vár."
|
||||
"short": "A fiókod admin jóváhagyásra vár"
|
||||
},
|
||||
"suspended": {
|
||||
"title": "Felfüggesztett fiók",
|
||||
"body": "Ez a fiók fel van függesztve, így semmilyen módon nem tud belépni.",
|
||||
"short": "Ez a fiók fel van függesztve."
|
||||
"short": "Ez a fiók fel van függesztve"
|
||||
},
|
||||
"invalid": {
|
||||
"short": "Hibás e-mail-cím vagy jelszó."
|
||||
"short": "Hibás e-mail-cím vagy jelszó"
|
||||
},
|
||||
"rate_limited": {
|
||||
"short": "Túl sok próbálkozás. Próbáld újra kicsit később."
|
||||
}
|
||||
},
|
||||
"accessDenied": "Ez a fiók még nincs jóváhagyva ehhez a példányhoz — regisztrálj lent, vagy kérj hozzáférést az admintól.",
|
||||
"accessDenied": "Ez a fiók még nincs jóváhagyva ehhez a példányhoz — regisztrálj lent, vagy kérj hozzáférést az admintól",
|
||||
"suspended": "Ez a fiók fel van függesztve, nem tud belépni. Ha tévedésnek gondolod, keresd az üzemeltetőt (a részletek az e-mailedben).",
|
||||
"deleted": "A fiókod és minden adata véglegesen törölve lett. A megerősítést e-mailben is elküldtük."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { eslint } from "./lintProbe.testkit";
|
||||
|
||||
// `local/date-format` (eslint.config.js): docs/DESIGN.md "Copy" — a date is shown in the forms
|
||||
// lib/format.ts builds, in the UI language. This pins that the rule fires on the ways a date used to
|
||||
// be formatted by hand, and where it stands down.
|
||||
const RULE = "local/date-format";
|
||||
|
||||
async function hits(source: string, filePath = "src/components/__probe.tsx"): Promise<number> {
|
||||
const [result] = await eslint.lintText(source, { filePath });
|
||||
return (result?.messages ?? []).filter((m) => m.ruleId === RULE).length;
|
||||
}
|
||||
|
||||
describe("a hand-formatted date", () => {
|
||||
it("is refused as toLocale*String or an Intl date formatter", async () => {
|
||||
expect(await hits("export const a = (d: Date) => d.toLocaleString('en');\n")).toBe(1);
|
||||
expect(await hits("export const b = (d: Date) => d.toLocaleDateString();\n")).toBe(1);
|
||||
expect(await hits("export const c = (d: Date) => d.toLocaleTimeString();\n")).toBe(1);
|
||||
expect(
|
||||
await hits("export const e = new Intl.DateTimeFormat('en', { month: 'short' });\n"),
|
||||
).toBe(1);
|
||||
expect(await hits("export const f = new Intl.RelativeTimeFormat('en');\n")).toBe(1);
|
||||
});
|
||||
|
||||
it("is left alone in lib/format.ts, in a test and for a number format", async () => {
|
||||
const src = "export const g = (d: Date) => d.toLocaleString('en');\n";
|
||||
expect(await hits(src, "src/lib/format.ts")).toBe(0);
|
||||
expect(await hits(src, "src/lib/format.test.ts")).toBe(0);
|
||||
expect(await hits("export const h = new Intl.NumberFormat('en');\n")).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe("failCopyKeys", () => {
|
||||
).toBe("Sign-in required");
|
||||
// A category this client has no copy for (a newer backend): the generic sentence, never a key.
|
||||
expect(i18n.t(failCopyKeys({ error_code: "rate_limited", error_detail: null }, "short"))).toBe(
|
||||
"Download failed",
|
||||
"Couldn't download",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
RELATIVE_UNITS,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
formatDateTimeSec,
|
||||
formatNumber,
|
||||
formatRuntime,
|
||||
formatTotalHours,
|
||||
@@ -161,7 +162,7 @@ describe("formatNumber", () => {
|
||||
|
||||
it("carries the UI language into the channel total-hours label", () => {
|
||||
i18nStub.language = "hu";
|
||||
expect(formatTotalHours(12345 * 3600)).toBe(`${new Intl.NumberFormat("hu").format(12345)} h`);
|
||||
expect(formatTotalHours(12345 * 3600)).toBe(`${new Intl.NumberFormat("hu").format(12345)} ó`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,26 +171,32 @@ describe("formatRuntime", () => {
|
||||
[null, ""],
|
||||
[undefined, ""],
|
||||
[0, ""], // nothing to show — the call sites suppress their separator on the empty string
|
||||
[1, "1s"],
|
||||
[45, "45s"], // one Short: the regression case
|
||||
[45.7, "45s"], // truncates, so the seconds tier cannot overstate either
|
||||
[59, "59s"], // last second before the minute tier
|
||||
[60, "1m"],
|
||||
[90, "1m"], // floors, never rounds up — a total must not overstate
|
||||
[90.9, "1m"],
|
||||
[3599, "59m"],
|
||||
[3600, "1h 0m"], // the zero minutes stay: "1h" alone reads as an approximation
|
||||
[5025, "1h 23m"],
|
||||
[360000, "100h 0m"], // a large aggregate keeps counting hours rather than rolling to days
|
||||
[1, "1 sec"],
|
||||
[45, "45 sec"], // one Short: the regression case
|
||||
[45.7, "45 sec"], // truncates, so the seconds tier cannot overstate either
|
||||
[59, "59 sec"], // last second before the minute tier
|
||||
[60, "1 min"],
|
||||
[90, "1 min"], // floors, never rounds up — a total must not overstate
|
||||
[90.9, "1 min"],
|
||||
[3599, "59 min"],
|
||||
[3600, "1 hr 0 min"], // the zero minutes stay: "1 hr" alone reads as an approximation
|
||||
[5025, "1 hr 23 min"],
|
||||
[360000, "100 hr 0 min"], // a large aggregate keeps counting hours rather than rolling to days
|
||||
];
|
||||
|
||||
it.each(CASES)("formats %o as %o", (sec, expected) => {
|
||||
expect(formatRuntime(sec)).toBe(expected);
|
||||
});
|
||||
|
||||
it("speaks the UI language's units (DESIGN Copy: Intl short), not English letters", () => {
|
||||
i18nStub.language = "hu";
|
||||
expect(formatRuntime(5025)).toBe("1 ó 23 p");
|
||||
i18nStub.language = "en";
|
||||
});
|
||||
|
||||
it("never reports a non-zero duration as zero", () => {
|
||||
for (let sec = 1; sec < 3600; sec++) {
|
||||
expect(formatRuntime(sec), `${sec}s`).not.toBe("0m");
|
||||
expect(formatRuntime(sec), `${sec}s`).not.toBe("0 min");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -212,12 +219,20 @@ describe("formatDateTime / formatDate", () => {
|
||||
const asEnglish = formatDateTime(iso);
|
||||
i18nStub.language = "hu";
|
||||
const asHungarian = formatDateTime(iso);
|
||||
expect(asEnglish).toBe(new Date(iso).toLocaleString("en"));
|
||||
expect(asHungarian).toBe(new Date(iso).toLocaleString("hu"));
|
||||
expect(asEnglish).not.toBe(asHungarian);
|
||||
expect(formatDate(iso, "en")).not.toBe(formatDate(iso, "hu"));
|
||||
});
|
||||
|
||||
it("names the month and stops at the minute; seconds only in the log form (DESIGN Copy)", () => {
|
||||
process.env.TZ = "UTC";
|
||||
const iso = "2026-09-17T08:30:14Z";
|
||||
expect(formatDateTime(iso)).toBe("Sep 17, 2026, 08:30 AM");
|
||||
expect(formatDateTimeSec(iso)).toBe("Sep 17, 2026, 08:30:14 AM");
|
||||
i18nStub.language = "hu";
|
||||
expect(formatDateTime(iso)).toBe("2026. szept. 17. 08:30");
|
||||
expect(formatDate(iso, "hu")).toBe("2026. szept. 17.");
|
||||
});
|
||||
|
||||
it("shows a date-only value as that calendar day in every time zone", () => {
|
||||
process.env.TZ = "America/New_York";
|
||||
expect(formatDate("2026-09-17", "en")).toBe("Sep 17, 2026");
|
||||
|
||||
+41
-15
@@ -96,19 +96,34 @@ export function formatDuration(sec: number | null): string {
|
||||
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
|
||||
}
|
||||
|
||||
/** Compact human runtime ("1h 30m", "45m", "45s") from seconds. Formats BOTH a single item's
|
||||
* length (Plex movies/episodes) and an aggregate total (playlist playtime, on the YouTube and the
|
||||
* Plex side alike) — it is the one renderer for a playlist total, so a total never appears in two
|
||||
* shapes on one screen. Empty string for a missing/zero duration (nothing to show).
|
||||
// A duration's unit in the UI language, in the browser's own short form (docs/DESIGN.md "Copy", E28
|
||||
// S3g): "90 min", "2 hr" / "90 p", "2 ó". Hand-typed "90m" / "2h" were English in both languages and
|
||||
// came in three spellings. One formatter per language × unit, built lazily (the grids format one per card).
|
||||
export type DurationUnit = "second" | "minute" | "hour" | "day";
|
||||
const unitFormatters = new Map<string, Intl.NumberFormat>();
|
||||
export function formatUnit(value: number, unit: DurationUnit): string {
|
||||
const key = `${i18n.language}|${unit}`;
|
||||
let f = unitFormatters.get(key);
|
||||
if (!f) {
|
||||
f = new Intl.NumberFormat(i18n.language, { style: "unit", unit, unitDisplay: "short" });
|
||||
unitFormatters.set(key, f);
|
||||
}
|
||||
return f.format(value);
|
||||
}
|
||||
|
||||
/** Compact human runtime ("1 hr 30 min", "45 min", "45 sec"; HU "1 ó 30 p") from seconds. Formats
|
||||
* BOTH a single item's length (Plex movies/episodes) and an aggregate total (playlist playtime, on
|
||||
* the YouTube and the Plex side alike) — it is the one renderer for a playlist total, so a total
|
||||
* never appears in two shapes on one screen. Empty string for a missing/zero duration.
|
||||
* The sub-minute tier is load-bearing, not decoration: an aggregate is the only input that can land
|
||||
* between 1 and 59 seconds (a playlist of Shorts), and flooring that to a flat "0m" made a
|
||||
* between 1 and 59 seconds (a playlist of Shorts), and flooring that to a flat "0 min" made a
|
||||
* non-empty playlist report no runtime at all. Keep it, or that case silently regresses. */
|
||||
export function formatRuntime(sec?: number | null): string {
|
||||
if (!sec) return "";
|
||||
if (sec < 60) return `${Math.floor(sec)}s`;
|
||||
if (sec < 60) return formatUnit(Math.floor(sec), "second");
|
||||
const h = Math.floor(sec / 3600);
|
||||
const m = Math.floor((sec % 3600) / 60);
|
||||
return h ? `${h}h ${m}m` : `${m}m`;
|
||||
return h ? `${formatUnit(h, "hour")} ${formatUnit(m, "minute")}` : formatUnit(m, "minute");
|
||||
}
|
||||
|
||||
// Human label for a canonical quota-action key (see backend app.quota.QuotaAction). Resolved
|
||||
@@ -128,11 +143,11 @@ export function formatEta(seconds: number): string {
|
||||
return i18n.t("time.eta.days", { count: Math.round(hours / 24) });
|
||||
}
|
||||
|
||||
/** Compact whole-channel total length ("12 h", "45 m"), hours-scale so H:MM:SS isn't huge. */
|
||||
/** Compact whole-channel total length ("12 hr", "45 min"), hours-scale so H:MM:SS isn't huge. */
|
||||
export function formatTotalHours(sec: number): string {
|
||||
if (!sec) return "—";
|
||||
const h = Math.round(sec / 3600);
|
||||
return h >= 1 ? `${formatNumber(h)} h` : `${Math.max(1, Math.round(sec / 60))} m`;
|
||||
return h >= 1 ? formatUnit(h, "hour") : formatUnit(Math.max(1, Math.round(sec / 60)), "minute");
|
||||
}
|
||||
|
||||
// One grouped-number formatter per language, built lazily — the same reason as the relative-time
|
||||
@@ -169,10 +184,23 @@ export function formatCountOrDash(n: number | null | undefined): string {
|
||||
return n != null ? formatViews(n) : "—";
|
||||
}
|
||||
|
||||
// Absolute date AND time in the UI language (e.g. "16 Sept 2026, 08:30:14"). A bare `toLocaleString()`
|
||||
// formats in the browser's locale instead, so an English UI showed a Hungarian-shaped date. Empty for null.
|
||||
// The app's date forms (docs/DESIGN.md "Copy", E28 S3g), all in the UI language — never a bare
|
||||
// `toLocaleString()`, which formats in the BROWSER's locale (an English UI showed a Hungarian-shaped
|
||||
// date) and, with no options, as numbers with seconds ("9/27/2026, 8:30:14 AM"):
|
||||
// relativeTime "3d ago" / "3 napja" — recency in a list; the exact form on hover
|
||||
// formatDate "Sep 27, 2026" / "2026. szept. 27."
|
||||
// formatDateTime "Sep 27, 2026, 08:30 AM" / "2026. szept. 27. 08:30"
|
||||
// formatDateTimeSec the same with seconds — only the server log, the audit and the recorder
|
||||
// lint (`no-restricted-syntax`) keeps date formatting in this file. Empty for null.
|
||||
const DAY: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric" };
|
||||
const MINUTE: Intl.DateTimeFormatOptions = { ...DAY, hour: "2-digit", minute: "2-digit" };
|
||||
|
||||
export function formatDateTime(iso: string | null | undefined): string {
|
||||
return iso ? new Date(iso).toLocaleString(i18n.language) : "";
|
||||
return iso ? new Date(iso).toLocaleString(i18n.language, MINUTE) : "";
|
||||
}
|
||||
|
||||
export function formatDateTimeSec(iso: string | null | undefined): string {
|
||||
return iso ? new Date(iso).toLocaleString(i18n.language, { ...MINUTE, second: "2-digit" }) : "";
|
||||
}
|
||||
|
||||
// Absolute day-precision date (e.g. "12 Jan 2017"), shown alongside a relative time. Empty for null.
|
||||
@@ -183,9 +211,7 @@ export function formatDate(iso: string | null | undefined, lang: string): string
|
||||
if (!iso) return "";
|
||||
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(iso);
|
||||
return new Date(iso).toLocaleDateString(lang, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
...DAY,
|
||||
...(dateOnly ? { timeZone: "UTC" } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user