Files
Psychotoxical-psysonic/src/locales/ro/orbit.ts
T
Psychotoxical 78a177b1bc fix(orbit): outbox lost-update recovery and session hardening (#1159)
* fix(orbit): harden the guest outbox — recover lost suggestions, avoid a duplicate outbox

Two outbox robustness issues on the poll-based, non-atomic playlist
transport:

- Lost-update: a track a guest appends to its outbox can be wiped by a
  concurrent host sweep-clear before the host recorded it, leaving the
  suggestion lost AND stuck on "waiting on host" forever (it only clears
  once it reaches the host's play queue). The playlist read-modify-write
  can't be made atomic, so recover: re-send a pending suggestion the host
  hasn't recorded (absent from state.queue) past a grace window — the host
  dedupes by (user, trackId), so it's idempotent — and give up + toast past
  a 45s window so the row stops hanging. New pendingResend planner module
  with unit tests; suggest + tick share ensureTrackInOutbox.

- Duplicate outbox: a transient getPlaylists failure at join was swallowed
  to [], so the existing-outbox check missed and a second outbox was
  created. Distinguish "lookup failed" from "genuinely absent" and retry
  once before falling back to create.

New i18n key for the give-up toast across all locales.

* fix(orbit): reject non-http(s) schemes in share invites

normalizeShareServerUrl only prepends http:// when the server string doesn't
already start with "http", so an invite carrying a scheme like httpx:// or
https-phish:// slips through unchanged. Reject anything whose parsed protocol
isn't http(s) at parse time, before the known-server match. Add round-trip
tests covering the accepted and rejected schemes.

* fix(orbit): read exit-modal role fresh to avoid a stale keydown closure

The exit modal's Enter/Escape handler binds once per open (deps [isOpen]) and
closed over the role prop, so a role change while the modal was up would act
on a stale value. Read role from the store inside the action instead, and drop
the now-unused reactive subscription.

* docs(changelog): consolidate Orbit fixes into one block, add 1159
2026-06-22 17:32:53 +02:00

202 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const orbit = {
triggerLabel: 'Orbit',
triggerTooltip: 'Pornește sau intră în o sesiune de ascultare distribuită',
launchCreate: 'Crează o sesiune',
launchJoin: 'Intră într-o sesiune',
launchHelp: 'Cum funcționează asta?',
launchHelpSoon: 'În curând',
joinModalTitle: 'Intră într-o sesiune Orbit',
joinModalSub: 'Lipește link-ul de invitație pe care organizatorul ți l-a trimis.',
joinModalLinkLabel: 'Link-ul de invitație',
joinModalLinkPlaceholder: 'psysonic2-…',
joinModalLinkHelper: 'Funcționează cu orice link valid Orbit. Trebuie să arate spre serverul în care ești conectat acum.',
joinModalPasteTooltip: 'Lipește din clipboard',
joinModalSubmit: 'Intră',
joinModalBusy: 'Se intră…',
joinErrEmpty: 'Lipește un link de invitație.',
joinErrInvalid: 'Asta nu arată ca un link de invitație Orbit.',
closeAria: 'Închide',
heroTitlePrefix: 'Ascultă împreună cu',
heroTitleBrand: 'Orbit',
heroSub: 'Pornește o sesiune — alți utilizatori de pe {{server}} vor asculta deodată și pot sugera piese.',
fallbackServer: 'acest server',
tipLan: "Ești conectat acum printr-o adresă de rețea locală. Invitații din afara rețelei tale nu pot intra — schimbă pe o adresă publică de server în setări înainte de a începe.",
tipRemote: 'Pentru ca oaspeții din afara rețelei să poată intra, serverul tău trebuie să fie accesibil public. Dacă serverul tău este doar intern, schimbă înainte de a începe.',
labelName: 'Numele sesiunii',
namePlaceholder: 'Velvet Orbit',
reshuffleTooltip: 'Nouă sugestie',
reshuffleAria: 'Sugerează un nou nume',
helperName: 'Cum apare sesiunea către oaspeții tăi — păstrează-l sau folosește-l pe al tău.',
labelMax: 'Max oaspeți',
helperMax: "Tu nu ești numărat — se ține cont doar de oaspeții care intră.",
labelLink: 'Link-ul de invitație',
tooltipCopied: 'Copiat',
tooltipCopy: 'Copiază',
ariaCopyLink: 'Copiază link-ul de invitație',
helperLink: 'Trimite acest link către invitații tăi. Ei îl pot lipi oriunde în Psysonic cu Ctrl+V.',
labelClearQueue: 'Golește-mi coada mai întâi',
helperClearQueue: 'Începe sesiunea cu o coadă proaspătă goală. Oprit: piesele pe care le ai deja în coadă rămân și sunt distribuite cu oaspeții.',
btnCancel: 'Anulează',
btnStarting: 'Se începe…',
btnStart: 'Începe Orbit',
btnCopyAndStart: 'Copiază acest link & începe',
errNameRequired: 'Dă un nume sesiunii tale.',
errStartFailed: "Nu s-a putut începe.",
hostLabel: 'Gazdă: {{name}}',
participantsTooltip: 'Participanți',
settingsTooltip: 'Setările sesiunii',
shareTooltip: 'Distribuie link-ul de invitație',
shuffleLabel: 'Coada se re-amestecă în',
catchUpLabel: 'ajunge din urmă',
catchUpTooltip: "Sari către poziția curentă a gazdei",
hostAway: 'Gazdă offline',
hostOnline: 'Gazdă online',
endTooltip: 'Termină sesiunea',
leaveTooltip: 'Părăsește sesiunea',
helpTooltip: 'Cum funcționează Orbit',
diag: {
openTooltip: 'Diagnostice — log-uri evenimente copiabile',
title: 'Diagnostice Orbit',
role: 'Rol',
hostTrack: 'Id piesă gazdă',
hostPos: 'Poziție gazdă',
guestTrack: 'Id-ul piesei mele',
guestPos: 'Id-ul poziției mele',
drift: 'Drift',
stateAge: 'Status vârstă',
eventLog: 'Log-uri evenimente ({{count}})',
copyLabel: 'Copiază',
copyTooltip: 'Copiază log-urile în clipboard',
clearLabel: 'Golește',
clearTooltip: 'Șterge buffer-ul',
empty: 'Niciun eveniment deocamdată — evenimentele apar ca sincronizare Orbit.',
hint: 'Deschide asta înainte de a reproduce o problemă, apoi apasă Copiază și lipește în raportul problemei.',
copied: 'S-au copiat {{count}} linii de eveniment',
copyFailed: 'Nu s-a putut copia în clipboard',
cleared: 'Buffer curățat',
},
helpTitle: 'Cum funcționează Orbit',
helpIntro: 'Orbit transformă Psysonic într-o cameră distribuită de ascultare. O gazdă alege muzica; oaspeții ascultă în sincron și pot sugera piese.',
helpHostOnly: '(gazdă)',
helpSec1Title: 'Ce este Orbit?',
helpSec1Body: "Un mod \"ascultă împreună\" — gazda controlează redarea, oaspeții se alătură. Totul rulează prin playlisturile de pe serverul tău Navidrome; nicio infrastructură externă.",
helpSec2Title: 'Gazdă și oaspeți',
helpSec2Body: 'Gazda controlează redarea (redă / pauză / sari / caută) și decide când sesiunea se termină. Oaspeții urmează redarea gazdei, pot sugera piese, și pot ieși și re-intra oricând. Doar gazda poate da afară sau bana permanent un participant.',
helpSec2WarnHead: 'Important: conturi separate.',
helpSec2WarnBody: "Fiecare participant are nevoie de contul său Navidrome. Dacă gazda și oaspetele se conectează cu același utilizator propunerile lor se ciocnesc și gazda nu va vedea propunerile oaspetului.",
helpSec3Title: 'Pornirea și invitarea',
helpSec3Body: 'Apasă pe butonul "Orbit" → Crează o sesiune → modalul de pornire arată un link de invitație gata de copiat și te lasă opțional să golești coada curentă. Împarte acel link oricum dorești. Cât timp o sesiune este în derulare, butonul de distribuire din bara sesiunii de sus copiază link-ul din nou oricând.',
helpSec4Title: 'Intrare',
helpSec4Body: 'Lipește link-ul de invitație oriunde în Psysonic cu Ctrl+V / Cmd+V — prompt-ul de alăturare se deschide automat. Alternativă: "Orbit" → Intră într-o sesiune → lipește în câmpul prezentat. Dacă link-ul arată către un server pe care ai un cont, Psysonic îl schimbă pentru tine. Când ai mai mult de un cont pe acel server, un alegător mic te va întreba pe care să îl folosești.',
helpSec5Title: 'Sugerarea pieselor',
helpSec5Body: 'În orice listă de piese: dublu-clic pe un rând, sau clic-dreapta → "Adaugă la sesiunea Orbit". Un clic singular arată o sugestie în loc de a pune tot albumul în coada distribuită — asta este intenționat. Acțiunile explicite în masă cum ar fi "Redă Tot" sau "Redă Album" întreabă pentru confirmare mai întâi și după adaugă (niciodată schimbă).',
helpSec6Title: 'Coadă distribuită',
helpSec6Body: "Coada afișează piesele următoare ale gazdei — vizibile către toți. Adăugările în masă care sosesc sunt mereu adăugate, deci sugestiile oaspeților nu sunt pierdute. Auto-amestecarea reordonează coada periodic (configrabil: 1 / 5 / 10 / 15 / 30 min). Dacă redarea se Îndepărtează de gazdă, butonul de \"Ajunge din urmă\" în bara sesiunii sare înapoi către poziția live a gazdei.",
helpSec7Title: 'Aprobări',
helpSec7Body: 'Implicit, sugestiile oaspeților au nevoie de aprobare manuală — apar în banda "Aprobări" în vârful panoului cozii cu butoane de acceptă / respinge. Auto-aprobarea poate fi pornită în setările sesiunii pentru ca toate sugestiile să intre din prima.',
helpSec8Title: 'Participanți și setări',
helpSec8Body: 'Deschide iconița de setări din bara sesiunii pentru cadența amestecării, auto-aprobare, și scurtătura "Amestecă acum". Apasă pe numărul de participanți pentru a vedea cine este conectat — dă afară (se poate reconecta cu link-ul) sau ban (blocat din sesiune). Oaspeții pot vedea de asemenea lista de participanți, doar nu o pot modifica.',
helpSec9Title: 'Sfârșirea sesiunii',
helpSec9Body: "Gazdă X → dialog confirmare → sesiunea se închide pentru toată lumea și playlisturile serverului sunt curățate automat. Oaspete X → oaspetele pleacă, sesiunea continuă să ruleze. Dacă gazda nu răspunde pentru 5 minute oaspetele iese automat cu o notificare; reconectările scurte din acel interval sunt invizibile.",
participantsInviteLabel: 'Link-ul de invitație',
participantsCountLabel: '{{count}} în sesiune',
participantsHost: 'gazdă',
participantsEmpty: 'Niciun oaspete deocamdată',
participantsKickTooltip: 'Înlătură din sesiune',
participantsKickAria: 'Înlătură {{user}}',
participantsRemoveTooltip: 'Înlătură din sesiune',
participantsRemoveAria: 'Înlătură {{user}} din această sesiune',
participantsBanTooltip: 'Banează permanent',
participantsBanAria: 'Banează permanent {{user}}',
participantsMuteTooltip: 'Dezactivează sugestiile',
participantsUnmuteTooltip: 'Permite sugestiile',
participantsMuteAria: 'Dezactivează sugestii de la {{user}}',
participantsUnmuteAria: 'Permite sugestii de la {{user}}',
confirmRemoveTitle: 'Înlătură din sesiune?',
confirmRemoveBody: '{{user}} va fi dat afară din sesiune, dar poate reintra prin link-ul tău de invitație.',
confirmRemoveConfirm: 'Înlătură',
confirmBanTitle: 'Banezi permanent?',
confirmBanBody: '{{user}} va fi înlăturat și blocat din reintrarea în această sesiune. Acest lucru nu poate fi anulat pe durata sesiunii.',
confirmBanConfirm: 'Ban',
confirmCancel: 'Anulează',
confirmJoinTitle: 'Te alături sesiunii Orbit?',
confirmJoinBody: '{{host}} te-a invitat în "{{name}}". Intri în sesiune?',
confirmJoinConfirm: 'Intră',
confirmLeaveTitle: 'Părăsești sesiunea?',
confirmLeaveBody: 'Părăsești "{{name}}"? Poți reintra prin link-ul de invitație al {{host}} oricând.',
confirmLeaveConfirm: 'Pleacă',
confirmEndTitle: 'Termini sesiunea pentru toți?',
confirmEndBody: '"{{name}}" va închide pentru toți participanții. Playlisturile sesiunii sunt curățate automat.',
confirmEndConfirm: 'Termină sesiunea',
invalidLinkTitle: 'Link-ul de invitație nu mai este valid',
invalidLinkBody: 'Această sesiune Orbit nu mai există sau s-a terminat deja. Roagă gazda pentru un link de invitație proaspăt.',
settingsTitle: 'Setările sesiunii',
settingAutoApprove: 'Auto-aprobă sugestiile',
settingAutoApproveHint: "Sugestiile oaspeților intră în coadă instant. Oprit: ele rămân în lista sesiunii pentru ca tu să le revizuiești mai târziu.",
settingAutoShuffle: 'Reamestecare automată',
settingAutoShuffleHint: "Amestecare periodică FisherYates a cozii care urmează. Oprit: ordinea se schimbă doar când o rearanjezi.",
settingShuffleInterval: 'Reamestecă la fiecare',
settingShuffleIntervalHint: 'Cât de des se reamestecă coada în timp ce sesiunea rulează.',
suggestBlockedMuted: 'Gazda te-a oprit din a trimite sugestii pentru această sesiune — sugestiile sunt oprite.',
settingShuffleIntervalValue_one: '{{count}} min',
settingShuffleIntervalValue_other: '{{count}} min',
settingShuffleNow: 'Amestecă acum',
toastSuggested: '{{user}} a sugerat "{{title}}"',
toastSuggestedMany: '{{count}} noi sugestii în coadă',
toastSuggestLost: 'Sugestia ta s-ar putea să nu fi ajuns la gazdă',
toastShuffled: 'Coadă amestecată',
toastJoined: 'A intrat în sesiune',
toastLoginFirst: 'Log in before joining a session',
toastSwitchServer: 'Schimbă la {{url}} mai întâi, apoi lipește din nou',
toastNoAccountForServer: "Nu ai acces la {{url}}. Cere o invitație gazdei.",
toastSwitchFailed: "Nu s-a putut schimba la {{url}}",
accountPickerTitle: 'Ce cont?',
accountPickerSub: 'Ai mai mult de un cont pentru {{url}}. Alege unul cu care să intri în sesiune.',
toastJoinFail: "Nu s-a putut alătura sesiunii",
joinErrNotFound: 'Sesiunea nu a fost găsită',
joinErrEnded: 'Sesiunea s-a terminat',
joinErrFull: 'Sesiunea este plină',
joinErrKicked: "Nu poți reintra în această sesiune",
joinErrNoUser: 'Niciun server activ',
joinErrServerError: "Nu s-a putut intra",
ctxAddToSession: 'Adaugă la sesiunea Orbit',
ctxSuggestedToast: 'Sugerat la sesiune',
ctxSuggestFailed: "Nu s-a putut sugera — neintrat",
ctxAddToSessionHost: 'Adaugă la sesiunea Orbit',
ctxAddedHostToast: 'Adăugat la sesiune',
ctxAddHostFailed: "Nu s-a putut adăuga la sesiune",
bulkConfirmTitle: 'Adaugi tot în coada Orbit?',
bulkConfirmBody: "Asta va lăsa {{count}} piese în coada distribuită deodată. Asta este mult pentru toți oaspeții să remarce. Continui?",
bulkConfirmYes: 'Adaugă tot',
bulkConfirmNo: 'Anulează',
guestLive: 'Live',
guestPlaying: 'Se redă acum',
guestPaused: 'Pauză',
guestLoading: 'Se încarcă…',
guestSuggestions: 'Sugestii',
guestUpNext: 'Urmează',
guestUpNextEmpty: 'Nimic în coadă. Deschide meniul de context a unei piese și alege "Adaugă la sesiunea Orbit" pentru a sugera una.',
guestPendingTitle: 'Se așteaptă gazda',
guestPendingHint: 'Sugerat — va apărea în coadă când gazda o va alege.',
approvalTitle: 'Aprobări în așteptare',
approvalFrom: 'Sugerat de{{user}}',
approvalAccept: 'Accept',
approvalDecline: 'Respinge',
guestUpNextMore: '+ {{count}} mai multe în coada gazdei',
guestSubmitter: 'de {{user}}',
queueAddedByHost: 'Adăugat de gazdă',
queueAddedByYou: 'Adăugat de tine',
queueAddedByUser: 'Adăugat de {{user}}',
guestEmpty: 'Nicio sugestie încă. Deschide meniul de context a unei piese și alege "Adaugă la sesiunea Orbit".',
guestFooter: "Gazda controlează redarea — nu poți schimba lista.",
exitKickedTitle: 'Ai fost banat din sesiune',
exitRemovedTitle: 'Ai fost înlăturat din sesiune',
exitEndedTitle: 'Gazda a încheiat sesiunea',
exitHostTimeoutTitle: 'Gazda n-a mai răspuns',
exitHostTimeoutBody: "{{host}} nu a mai trimis un update de ceva timp, așa că \"{{name}}\" a fost închis pe partea ta. Poți reintra oricând cu link-ul de invitație.",
exitKickedBody: '{{host}} te-a banat din "{{name}}". Nu poți reintra',
exitRemovedBody: '{{host}} te-a înlăturat din "{{name}}". Poți reintra oricând cu link-ul de invitație.',
exitEndedBody: '"{{name}}" s-a terminat. Sper că te-ai distrat.',
exitOk: 'OK',
};