Files
Psychotoxical-psysonic/src/locales/nb/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
12 KiB
TypeScript

export const orbit = {
triggerLabel: 'Orbit',
triggerTooltip: 'Start eller bli med i en delt lytteøkt',
launchCreate: 'Opprett en økt',
launchJoin: 'Bli med i en økt',
launchHelp: 'Hvordan fungerer dette?',
launchHelpSoon: 'Kommer snart',
joinModalTitle: 'Bli med i en Orbit-økt',
joinModalSub: 'Lim inn invitasjonslenken verten sendte deg.',
joinModalLinkLabel: 'Invitasjonslenke',
joinModalLinkPlaceholder: 'psysonic2-…',
joinModalLinkHelper: 'Fungerer med enhver gyldig Orbit-lenke. Må peke til serveren du er innlogget på.',
joinModalPasteTooltip: 'Lim inn fra utklippstavlen',
joinModalSubmit: 'Bli med',
joinModalBusy: 'Blir med…',
joinErrEmpty: 'Lim inn en invitasjonslenke.',
joinErrInvalid: 'Dette ser ikke ut som en Orbit-invitasjonslenke.',
closeAria: 'Lukk',
heroTitlePrefix: 'Lytt sammen med',
heroTitleBrand: 'Orbit',
heroSub: 'Start en økt — andre brukere på {{server}} lytter med og kan foreslå spor.',
fallbackServer: 'denne serveren',
tipLan: 'Du er for øyeblikket koblet til via en hjemmenettverksadresse. Gjester utenfor nettverket kan ikke bli med — bytt til en offentlig serveradresse i innstillingene før du starter.',
tipRemote: 'For at gjester utenfor hjemmenettverket skal kunne bli med, må serveradressen være offentlig tilgjengelig. Hvis serveren din bare er intern, bytt før du starter.',
labelName: 'Øktnavn',
namePlaceholder: 'Velvet Orbit',
reshuffleTooltip: 'Nytt forslag',
reshuffleAria: 'Foreslå et nytt navn',
helperName: 'Slik vises økten for gjestene — behold den eller skriv din egen.',
labelMax: 'Maks gjester',
helperMax: 'Du telles ikke — dette begrenser bare innkommende gjester.',
labelLink: 'Invitasjonslenke',
tooltipCopied: 'Kopiert',
tooltipCopy: 'Kopier',
ariaCopyLink: 'Kopier invitasjonslenke',
helperLink: 'Send denne lenken til gjestene dine. De kan lime den inn hvor som helst i Psysonic med Ctrl+V.',
labelClearQueue: 'Tøm min kø først',
helperClearQueue: 'Start økten med en tom kø. Av: eksisterende spor beholdes og deles med gjester.',
btnCancel: 'Avbryt',
btnStarting: 'Starter…',
btnStart: 'Start Orbit',
btnCopyAndStart: 'Kopier lenke & start',
errNameRequired: 'Gi økten din et navn.',
errStartFailed: 'Kunne ikke starte.',
hostLabel: 'Vert: {{name}}',
participantsTooltip: 'Deltakere',
settingsTooltip: 'Øktinnstillinger',
shareTooltip: 'Del invitasjonslenke',
shuffleLabel: 'Kø stokkes på nytt om',
catchUpLabel: 'ta igjen',
catchUpTooltip: 'Hopp til vertens nåværende posisjon',
hostAway: 'Vert offline',
hostOnline: 'Vert online',
endTooltip: 'Avslutt økt',
leaveTooltip: 'Forlat økt',
helpTooltip: 'Hvordan Orbit fungerer',
diag: {
openTooltip: 'Diagnose — kopierbar hendelseslogg',
title: 'Orbit-diagnose',
role: 'Rolle',
hostTrack: 'Vertens spor-ID',
hostPos: 'Vertens posisjon',
guestTrack: 'Min spor-ID',
guestPos: 'Min posisjon',
drift: 'Avvik',
stateAge: 'Tilstandens alder',
eventLog: 'Hendelseslogg ({{count}})',
copyLabel: 'Kopier',
copyTooltip: 'Kopier loggen til utklippstavlen',
clearLabel: 'Tøm',
clearTooltip: 'Tøm bufferen',
empty: 'Ingen hendelser ennå — de dukker opp når Orbit synkroniserer.',
hint: 'Åpne dette før du reproduserer problemet, klikk Kopier og lim inn i feilrapporten.',
copied: '{{count}} linjer kopiert',
copyFailed: 'Kunne ikke kopiere til utklippstavlen',
cleared: 'Buffer tømt',
},
helpTitle: 'Hvordan Orbit fungerer',
helpIntro: 'Orbit gjør Psysonic til et delt lytterom. En vert velger musikken; gjester lytter synkronisert og kan foreslå spor.',
helpHostOnly: '(vert)',
helpSec1Title: 'Hva er Orbit?',
helpSec1Body: 'En "lytte sammen"-modus — verten driver avspillingen, gjester blir med. Alt går via spillelister på din egen Navidrome-server; ingen ekstern infrastruktur.',
helpSec2Title: 'Vert og gjester',
helpSec2Body: 'Verten kontrollerer avspillingen (spill / pause / hopp / søk) og bestemmer når økten avsluttes. Gjester følger vertens avspilling, kan foreslå spor og forlate eller bli med igjen når som helst. Bare verten kan utvise eller permanent utestenge en deltaker.',
helpSec2WarnHead: 'Viktig: separate kontoer.',
helpSec2WarnBody: 'Hver deltaker trenger sin egen Navidrome-konto. Hvis vert og gjest logger inn med samme bruker kolliderer innsendingene, og verten ser aldri gjestens forslag.',
helpSec3Title: 'Starte og invitere',
helpSec3Body: 'Klikk på "Orbit"-knappen → Opprett en økt → startmodalen viser en klar-til-kopiering invitasjonslenke og lar deg eventuelt tømme gjeldende kø. Del lenken som du vil. Mens en økt kjører, kopierer delknappen i øktlinjen øverst lenken når som helst.',
helpSec4Title: 'Bli med',
helpSec4Body: 'Lim inn invitasjonslenken hvor som helst i Psysonic med Ctrl+V / Cmd+V — bli-med-spørsmålet dukker opp automatisk. Alternativt: "Orbit" → Bli med i en økt → lim inn i feltet. Hvis lenken peker til en server du har konto på, bytter Psysonic for deg. Ved flere kontoer på den serveren spør en liten velger hvilken som skal brukes.',
helpSec5Title: 'Foreslå spor',
helpSec5Body: 'I enhver sporliste: dobbeltklikk en rad, eller høyreklikk → "Legg til i Orbit-økt". Et enkelt klikk viser et hint i stedet for å dumpe hele albumet i den delte køen — det er bevisst. Eksplisitte bulk-handlinger som "Spill alt" eller "Spill album" ber først om bekreftelse og legger deretter til (erstatter aldri).',
helpSec6Title: 'Den delte køen',
helpSec6Body: 'Køen viser vertens kommende spor — synlig for alle. Innkommende bulk-tillegg legges alltid til bakerst, så gjesteforslag går ikke tapt. Auto-stokking omorganiserer køen periodisk (konfigurerbar: 1 / 5 / 10 / 15 / 30 min). Hvis avspillingen din avviker fra verten, bringer "Ta igjen"-knappen i øktlinjen deg tilbake til vertens liveposisjon.',
helpSec7Title: 'Godkjenninger',
helpSec7Body: 'Som standard trenger gjesteforslag manuell godkjenning — de vises i en "Godkjenninger"-stripe øverst i køpanelet med godta / avvis-knapper. Auto-godkjenning kan slås på i øktinnstillingene slik at alt lander direkte.',
helpSec8Title: 'Deltakere og innstillinger',
helpSec8Body: 'Åpne innstillingsikonet i øktlinjen for stokkingsfrekvens, auto-godkjenning og "Stokk nå"-snarveien. Klikk på deltakerantallet for å se hvem som er koblet til — utvis (kan bli med igjen via lenken) eller utesteng (utelåst for økten). Gjester ser også deltakerlisten, men bare-lesing.',
helpSec9Title: 'Avslutte økten',
helpSec9Body: 'Vert X → bekreftelsesdialog → økten lukkes for alle og serverspillelistene ryddes opp automatisk. Gjest X → gjesten forlater, økten fortsetter. Hvis verten er stille i 5 minutter, forlater gjesten automatisk med en melding; korte gjentilkoblinger innenfor tiden er usynlige.',
participantsInviteLabel: 'Invitasjonslenke',
participantsCountLabel: '{{count}} i økten',
participantsHost: 'vert',
participantsEmpty: 'Ingen gjester ennå',
participantsKickTooltip: 'Fjern fra økten',
participantsKickAria: 'Fjern {{user}}',
participantsRemoveTooltip: 'Fjern fra økten',
participantsRemoveAria: 'Fjern {{user}} fra denne økten',
participantsBanTooltip: 'Uteste permanent',
participantsBanAria: 'Uteste {{user}} permanent',
participantsMuteTooltip: 'Blokker forslag',
participantsUnmuteTooltip: 'Tillat forslag',
participantsMuteAria: 'Blokker forslag fra {{user}}',
participantsUnmuteAria: 'Tillat forslag fra {{user}} igjen',
confirmRemoveTitle: 'Fjerne fra økten?',
confirmRemoveBody: '{{user}} fjernes fra økten, men kan bli med igjen via invitasjonslenken din.',
confirmRemoveConfirm: 'Fjern',
confirmBanTitle: 'Uteste permanent?',
confirmBanBody: '{{user}} fjernes og hindres i å bli med i denne økten igjen. Kan ikke angres i løpet av økten.',
confirmBanConfirm: 'Uteste',
confirmCancel: 'Avbryt',
confirmJoinTitle: 'Bli med i Orbit-økt?',
confirmJoinBody: '{{host}} inviterte deg til "{{name}}". Bli med i økten?',
confirmJoinConfirm: 'Bli med',
confirmLeaveTitle: 'Forlate økten?',
confirmLeaveBody: 'Forlate "{{name}}"? Du kan bli med igjen når som helst via {{host}}s invitasjonslenke.',
confirmLeaveConfirm: 'Forlat',
confirmEndTitle: 'Avslutte økten for alle?',
confirmEndBody: '"{{name}}" lukkes for alle deltakere. Øktens spillelister ryddes opp automatisk.',
confirmEndConfirm: 'Avslutt økt',
invalidLinkTitle: 'Invitasjonslenken er ikke lenger gyldig',
invalidLinkBody: 'Denne Orbit-økten finnes ikke lenger eller er allerede avsluttet. Be verten om en ny invitasjonslenke.',
settingsTitle: 'Øktinnstillinger',
settingAutoApprove: 'Godkjenn forslag automatisk',
settingAutoApproveHint: 'Gjesteforslag lander umiddelbart i køen din. Av: de blir værende i øktlisten for senere gjennomgang.',
settingAutoShuffle: 'Automatisk stokking',
settingAutoShuffleHint: 'Periodisk Fisher-Yates-stokking av kommende kø. Av: rekkefølgen endres bare når du omorganiserer den.',
settingShuffleInterval: 'Stokk på nytt hver',
settingShuffleIntervalHint: 'Hvor ofte kommende kø stokkes på nytt mens økten kjører.',
suggestBlockedMuted: 'Verten har blokkert deg for denne økten — forslag er av.',
settingShuffleIntervalValue_one: '{{count}} min',
settingShuffleIntervalValue_other: '{{count}} min',
settingShuffleNow: 'Stokk nå',
toastSuggested: '{{user}} foreslo "{{title}}"',
toastSuggestedMany: '{{count}} nye forslag i køen',
toastSuggestLost: 'Forslaget ditt nådde kanskje ikke verten',
toastShuffled: 'Kø stokket',
toastJoined: 'Ble med i økt',
toastLoginFirst: 'Logg inn før du blir med i en økt',
toastSwitchServer: 'Bytt til {{url}} først, og lim inn igjen',
toastNoAccountForServer: 'Du har ikke tilgang til {{url}}. Be verten om en invitasjon.',
toastSwitchFailed: 'Kunne ikke bytte til {{url}}',
accountPickerTitle: 'Hvilken konto?',
accountPickerSub: 'Du har mer enn én konto for {{url}}. Velg den du vil bli med i økten med.',
toastJoinFail: 'Kunne ikke bli med i økten',
joinErrNotFound: 'Økt ikke funnet',
joinErrEnded: 'Økten er avsluttet',
joinErrFull: 'Økten er full',
joinErrKicked: 'Du kan ikke bli med i denne økten igjen',
joinErrNoUser: 'Ingen aktiv server',
joinErrServerError: 'Kunne ikke bli med',
ctxAddToSession: 'Legg til i Orbit-økt',
ctxSuggestedToast: 'Foreslått til økten',
ctxSuggestFailed: 'Kunne ikke foreslå — ikke blitt med',
ctxAddToSessionHost: 'Legg til i Orbit-økt',
ctxAddedHostToast: 'Lagt til i økten',
ctxAddHostFailed: 'Kunne ikke legge til i økten',
bulkConfirmTitle: 'Legg alt til i Orbit-køen?',
bulkConfirmBody: 'Dette vil slippe {{count}} spor i den delte køen på én gang. Det er mye for gjestene dine. Fortsette?',
bulkConfirmYes: 'Legg til alle',
bulkConfirmNo: 'Avbryt',
guestLive: 'Live',
guestPlaying: 'Spiller nå',
guestPaused: 'Pauset',
guestLoading: 'Laster…',
guestSuggestions: 'Forslag',
guestUpNext: 'Neste',
guestUpNextEmpty: 'Ingenting i kø. Åpne et spors kontekstmeny og velg "Legg til i Orbit-økt" for å foreslå ett.',
guestPendingTitle: 'Venter på vert',
guestPendingHint: 'Foreslått — dukker opp i køen når verten tar det.',
approvalTitle: 'Venter på godkjenning',
approvalFrom: 'Foreslått av {{user}}',
approvalAccept: 'Godta',
approvalDecline: 'Avvis',
guestUpNextMore: '+ {{count}} flere i vertens kø',
guestSubmitter: 'av {{user}}',
queueAddedByHost: 'Lagt til av verten',
queueAddedByYou: 'Lagt til av deg',
queueAddedByUser: 'Lagt til av {{user}}',
guestEmpty: 'Ingen forslag ennå. Åpne et spors kontekstmeny og velg "Legg til i Orbit-økt".',
guestFooter: 'Verten kontrollerer avspillingen — du kan ikke endre listen.',
exitKickedTitle: 'Du ble utestengt fra økten',
exitRemovedTitle: 'Du ble fjernet fra økten',
exitEndedTitle: 'Verten avsluttet økten',
exitHostTimeoutTitle: 'Vert ikke tilgjengelig',
exitHostTimeoutBody: '{{host}} har ikke sendt en oppdatering på en stund, så "{{name}}" ble lukket på din side. Du kan bli med igjen når som helst med invitasjonslenken.',
exitKickedBody: '{{host}} utestengte deg fra "{{name}}". Du kan ikke bli med igjen.',
exitRemovedBody: '{{host}} fjernet deg fra "{{name}}". Du kan bli med igjen når som helst via invitasjonslenken.',
exitEndedBody: '"{{name}}" er avsluttet. Håper du hadde det gøy.',
exitOk: 'OK',
};