mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
78a177b1bc
* 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
202 lines
16 KiB
TypeScript
202 lines
16 KiB
TypeScript
export const orbit = {
|
|
triggerLabel: 'Orbit',
|
|
triggerTooltip: '共有リスニングセッションを開始するか参加します',
|
|
launchCreate: 'セッションを作成',
|
|
launchJoin: 'セッションに参加',
|
|
launchHelp: '仕組みを見る',
|
|
launchHelpSoon: '近日対応',
|
|
joinModalTitle: 'Orbit セッションに参加',
|
|
joinModalSub: 'ホストから送られた招待リンクを貼り付けてください。',
|
|
joinModalLinkLabel: '招待リンク',
|
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
|
joinModalLinkHelper: '有効な Orbit リンクなら使用できます。現在サインインしているサーバーを指している必要があります。',
|
|
joinModalPasteTooltip: 'クリップボードから貼り付け',
|
|
joinModalSubmit: '参加',
|
|
joinModalBusy: '参加中…',
|
|
joinErrEmpty: '招待リンクを貼り付けてください。',
|
|
joinErrInvalid: 'Orbit 招待リンクではないようです。',
|
|
closeAria: '閉じる',
|
|
heroTitlePrefix: '一緒に聴く',
|
|
heroTitleBrand: 'Orbit',
|
|
heroSub: 'セッションを開始すると、{{server}}上の他のユーザーが一緒に聴き、トラックを提案できます。',
|
|
fallbackServer: 'このサーバー',
|
|
tipLan: '現在、ホームネットワーク用アドレスで接続しています。ネットワーク外のゲストは参加できません。開始前に設定で公開サーバーアドレスへ切り替えてください。',
|
|
tipRemote: '自宅ネットワーク外のゲストが参加するには、サーバーアドレスが公開到達可能である必要があります。内部専用サーバーの場合は開始前に切り替えてください。',
|
|
labelName: 'セッション名',
|
|
namePlaceholder: 'Velvet Orbit',
|
|
reshuffleTooltip: '新しい提案',
|
|
reshuffleAria: '新しい名前を提案',
|
|
helperName: 'ゲストに表示されるセッション名です。そのままでも、自分で入力してもかまいません。',
|
|
labelMax: '最大ゲスト数',
|
|
helperMax: 'ホスト自身は含めず、参加するゲスト数だけを制限します。',
|
|
labelLink: '招待リンク',
|
|
tooltipCopied: 'コピーしました',
|
|
tooltipCopy: 'コピー',
|
|
ariaCopyLink: '招待リンクをコピー',
|
|
helperLink: 'このリンクをゲストへ送ってください。ゲストは Psysonic のどこでも Ctrl+V で貼り付けられます。',
|
|
labelClearQueue: '先に自分のキューをクリア',
|
|
helperClearQueue: '空の新しいキューでセッションを開始します。オフの場合、すでにキューにあるトラックが残り、ゲストにも共有されます。',
|
|
btnCancel: 'キャンセル',
|
|
btnStarting: '開始中…',
|
|
btnStart: 'Orbit を開始',
|
|
btnCopyAndStart: 'リンクをコピーして開始',
|
|
errNameRequired: 'セッション名を入力してください。',
|
|
errStartFailed: '開始できませんでした。',
|
|
hostLabel: 'ホスト: {{name}}',
|
|
participantsTooltip: '参加者',
|
|
settingsTooltip: 'セッション設定',
|
|
shareTooltip: '招待リンクを共有',
|
|
shuffleLabel: 'キュー再シャッフルまで',
|
|
catchUpLabel: '追いつく',
|
|
catchUpTooltip: 'ホストの現在位置へジャンプ',
|
|
hostAway: 'ホストはオフライン',
|
|
hostOnline: 'ホストはオンライン',
|
|
endTooltip: 'セッションを終了',
|
|
leaveTooltip: 'セッションを退出',
|
|
helpTooltip: 'Orbit の仕組み',
|
|
diag: {
|
|
openTooltip: '診断: コピー可能なイベントログ',
|
|
title: 'Orbit 診断',
|
|
role: '役割',
|
|
hostTrack: 'ホストのトラック ID',
|
|
hostPos: 'ホスト位置',
|
|
guestTrack: '自分のトラック ID',
|
|
guestPos: '自分の位置',
|
|
drift: 'ずれ',
|
|
stateAge: '状態の経過時間',
|
|
eventLog: 'イベントログ ({{count}})',
|
|
copyLabel: 'コピー',
|
|
copyTooltip: 'ログをクリップボードへコピー',
|
|
clearLabel: 'クリア',
|
|
clearTooltip: 'バッファを消去',
|
|
empty: 'まだイベントはありません。Orbit が同期するとイベントが表示されます。',
|
|
hint: '問題を再現する前にこれを開き、コピーをクリックしてバグ報告へ貼り付けてください。',
|
|
copied: '{{count}} 行のイベントをコピーしました',
|
|
copyFailed: 'クリップボードにコピーできませんでした',
|
|
cleared: 'バッファをクリアしました',
|
|
},
|
|
helpTitle: 'Orbit の仕組み',
|
|
helpIntro: 'Orbit は Psysonic を共有リスニングルームにします。ホストが音楽を選び、ゲストは同期して聴きながらトラックを提案できます。',
|
|
helpHostOnly: '(ホスト)',
|
|
helpSec1Title: 'Orbit とは?',
|
|
helpSec1Body: '一緒に聴くためのモードです。ホストが再生を操作し、ゲストがそこへ同期します。すべて自分の Navidrome サーバー上のプレイリストを通じて動作し、外部インフラは使いません。',
|
|
helpSec2Title: 'ホストとゲスト',
|
|
helpSec2Body: 'ホストは再生操作 (再生 / 一時停止 / スキップ / シーク) とセッション終了を制御します。ゲストはホストの再生に追従し、トラックを提案し、いつでも退出または再参加できます。参加者を強制退出またはBANできるのはホストだけです。',
|
|
helpSec2WarnHead: '重要: 別々のアカウントが必要です。',
|
|
helpSec2WarnBody: '各参加者には自分専用の Navidrome アカウントが必要です。ホストとゲストが同じユーザーでサインインすると投稿が衝突し、ホストはゲストの提案を見られません。',
|
|
helpSec3Title: '開始と招待',
|
|
helpSec3Body: '"Orbit" ボタン → セッションを作成、と進むと、開始モーダルにコピー可能な招待リンクが表示されます。現在のキューをクリアするかも選べます。好きな方法でそのリンクを共有してください。セッション中は上部のセッションバーにある共有ボタンでいつでもリンクを再コピーできます。',
|
|
helpSec4Title: '参加',
|
|
helpSec4Body: 'Psysonic のどこでも Ctrl+V / Cmd+V で招待リンクを貼り付けると、参加確認が自動で表示されます。または "Orbit" → セッションに参加、と進んで入力欄へ貼り付けます。リンク先サーバーのアカウントを持っていれば、Psysonic が切り替えます。そのサーバーに複数アカウントがある場合は、使用するアカウントを選ぶメニューが表示されます。',
|
|
helpSec5Title: 'トラック提案',
|
|
helpSec5Body: '任意の曲リストで、行をダブルクリックするか、右クリック → "Orbit セッションに追加" を選びます。シングルクリックではアルバム全体を共有キューへ入れず、ヒントだけを表示します。これは仕様です。"すべて再生" や "アルバムを再生" のような明示的な一括操作は先に確認し、その後に追加します (置き換えはしません)。',
|
|
helpSec6Title: '共有キュー',
|
|
helpSec6Body: 'キューにはホストの次のトラックが表示され、全員に見えます。一括追加は常に末尾へ追加されるため、ゲストの提案は失われません。自動シャッフルは定期的にキューを並べ替えます (設定可能: 1 / 5 / 10 / 15 / 30 分)。再生がホストからずれた場合、セッションバーの "追いつく" ボタンでホストの現在位置へ戻れます。',
|
|
helpSec7Title: '承認',
|
|
helpSec7Body: '既定では、ゲストの提案は手動承認が必要です。キューパネル上部の "承認待ち" ストリップに表示され、承認 / 却下できます。セッション設定で自動承認をオンにすると、すべてすぐキューへ入ります。',
|
|
helpSec8Title: '参加者と設定',
|
|
helpSec8Body: 'セッションバーの設定アイコンから、シャッフル間隔、自動承認、"今すぐシャッフル" ショートカットを開けます。参加者数をクリックすると接続中の人を確認できます。強制退出 (リンクから再参加可能) またはBAN (セッション中は再参加不可) が可能です。ゲストも参加者一覧を見られますが、読み取り専用です。',
|
|
helpSec9Title: 'セッション終了',
|
|
helpSec9Body: 'ホストが X を押して確認すると、セッションは全員に対して閉じられ、サーバー上のプレイリストは自動で片付けられます。ゲストが X を押すと、そのゲストだけ退出し、セッションは続きます。ホストが 5 分間応答しない場合、ゲストは通知付きで自動退出します。その範囲内の短い再接続は表示されません。',
|
|
participantsInviteLabel: '招待リンク',
|
|
participantsCountLabel: 'セッション内 {{count}} 人',
|
|
participantsHost: 'ホスト',
|
|
participantsEmpty: 'まだゲストはいません',
|
|
participantsKickTooltip: 'セッションから削除',
|
|
participantsKickAria: '{{user}} を削除',
|
|
participantsRemoveTooltip: 'セッションから削除',
|
|
participantsRemoveAria: '{{user}} をこのセッションから削除',
|
|
participantsBanTooltip: 'BAN',
|
|
participantsBanAria: '{{user}} をBAN',
|
|
participantsMuteTooltip: '提案をミュート',
|
|
participantsUnmuteTooltip: '提案を許可',
|
|
participantsMuteAria: '{{user}} からの提案をミュート',
|
|
participantsUnmuteAria: '{{user}} からの提案を許可',
|
|
confirmRemoveTitle: 'セッションから削除しますか?',
|
|
confirmRemoveBody: '{{user}} はセッションから外されますが、招待リンクから再参加できます。',
|
|
confirmRemoveConfirm: '削除',
|
|
confirmBanTitle: 'BANしますか?',
|
|
confirmBanBody: '{{user}} はこのセッションから削除され、再参加できなくなります。セッションが続く間は取り消せません。',
|
|
confirmBanConfirm: 'BAN',
|
|
confirmCancel: 'キャンセル',
|
|
confirmJoinTitle: 'Orbit セッションに参加しますか?',
|
|
confirmJoinBody: '{{host}} が "{{name}}" に招待しています。参加しますか?',
|
|
confirmJoinConfirm: '参加',
|
|
confirmLeaveTitle: 'セッションを退出しますか?',
|
|
confirmLeaveBody: '"{{name}}" から退出しますか?{{host}} の招待リンクからいつでも再参加できます。',
|
|
confirmLeaveConfirm: '退出',
|
|
confirmEndTitle: '全員のセッションを終了しますか?',
|
|
confirmEndBody: '"{{name}}" はすべての参加者に対して閉じられます。セッションのプレイリストは自動で片付けられます。',
|
|
confirmEndConfirm: 'セッションを終了',
|
|
invalidLinkTitle: '招待リンクはもう有効ではありません',
|
|
invalidLinkBody: 'この Orbit セッションは存在しないか、すでに終了しています。ホストに新しい招待リンクを依頼してください。',
|
|
settingsTitle: 'セッション設定',
|
|
settingAutoApprove: '提案を自動承認',
|
|
settingAutoApproveHint: 'ゲストの提案が即座にキューへ入ります。オフの場合、後で確認できるようセッション一覧に残ります。',
|
|
settingAutoShuffle: '自動再シャッフル',
|
|
settingAutoShuffleHint: '次のキューを定期的に Fisher-Yates シャッフルします。オフの場合、手動で並べ替えたときだけ順序が変わります。',
|
|
settingShuffleInterval: '再シャッフル間隔',
|
|
settingShuffleIntervalHint: 'セッション中、次のキューをどの頻度で再シャッフルするかです。',
|
|
suggestBlockedMuted: 'ホストがこのセッションであなたからの提案をミュートしました。提案はオフです。',
|
|
settingShuffleIntervalValue_one: '{{count}} 分',
|
|
settingShuffleIntervalValue_other: '{{count}} 分',
|
|
settingShuffleNow: '今すぐシャッフル',
|
|
toastSuggested: '{{user}} が "{{title}}" を提案しました',
|
|
toastSuggestedMany: 'キューに {{count}} 件の新しい提案',
|
|
toastSuggestLost: 'あなたの提案がホストに届かなかった可能性があります',
|
|
toastShuffled: 'キューをシャッフルしました',
|
|
toastJoined: 'セッションに参加しました',
|
|
toastLoginFirst: 'セッション参加前にログインしてください',
|
|
toastSwitchServer: '先に {{url}} へ切り替えてから、もう一度貼り付けてください',
|
|
toastNoAccountForServer: '{{url}} へのアクセス権がありません。ホストに招待を依頼してください。',
|
|
toastSwitchFailed: '{{url}} へ切り替えられませんでした',
|
|
accountPickerTitle: 'どのアカウントを使いますか?',
|
|
accountPickerSub: '{{url}} に複数のアカウントがあります。セッションに参加するアカウントを選んでください。',
|
|
toastJoinFail: 'セッションに参加できませんでした',
|
|
joinErrNotFound: 'セッションが見つかりません',
|
|
joinErrEnded: 'セッションは終了しました',
|
|
joinErrFull: 'セッションは満員です',
|
|
joinErrKicked: 'このセッションには再参加できません',
|
|
joinErrNoUser: '有効なサーバーがありません',
|
|
joinErrServerError: '参加できませんでした',
|
|
ctxAddToSession: 'Orbit セッションに追加',
|
|
ctxSuggestedToast: 'セッションへ提案しました',
|
|
ctxSuggestFailed: '提案できませんでした。セッションに参加していません',
|
|
ctxAddToSessionHost: 'Orbit セッションに追加',
|
|
ctxAddedHostToast: 'セッションへ追加しました',
|
|
ctxAddHostFailed: 'セッションへ追加できませんでした',
|
|
bulkConfirmTitle: '選択した項目をすべて Orbit キューへ追加しますか?',
|
|
bulkConfirmBody: '共有キューへ {{count}} 曲を一度に追加します。ゲストにはかなり多い量です。続行しますか?',
|
|
bulkConfirmYes: 'すべて追加',
|
|
bulkConfirmNo: 'キャンセル',
|
|
guestLive: 'ライブ',
|
|
guestPlaying: '再生中',
|
|
guestPaused: '一時停止',
|
|
guestLoading: '読み込み中…',
|
|
guestSuggestions: '提案',
|
|
guestUpNext: '次に再生',
|
|
guestUpNextEmpty: 'キューは空です。トラックのコンテキストメニューから "Orbit セッションに追加" を選んで提案できます。',
|
|
guestPendingTitle: 'ホスト待ち',
|
|
guestPendingHint: '提案済みです。ホストが取り込むとキューに表示されます。',
|
|
approvalTitle: '承認待ち',
|
|
approvalFrom: '{{user}} からの提案',
|
|
approvalAccept: '承認',
|
|
approvalDecline: '却下',
|
|
guestUpNextMore: '+ ホストのキューにさらに {{count}} 件',
|
|
guestSubmitter: '{{user}} より',
|
|
queueAddedByHost: 'ホストが追加',
|
|
queueAddedByYou: 'あなたが追加',
|
|
queueAddedByUser: '{{user}} が追加',
|
|
guestEmpty: '提案はまだありません。トラックのコンテキストメニューから "Orbit セッションに追加" を選んでください。',
|
|
guestFooter: '再生はホストが制御しています。ゲストはリストを変更できません。',
|
|
exitKickedTitle: 'セッションからBANされました',
|
|
exitRemovedTitle: 'セッションから削除されました',
|
|
exitEndedTitle: 'ホストがセッションを終了しました',
|
|
exitHostTimeoutTitle: 'ホストが応答していません',
|
|
exitHostTimeoutBody: '{{host}} からしばらく更新が送られてこないため、この端末で "{{name}}" を閉じました。招待リンクからいつでも再参加できます。',
|
|
exitKickedBody: '{{host}} が "{{name}}" であなたをBANしました。再参加はできません。',
|
|
exitRemovedBody: '{{host}} が "{{name}}" からあなたを削除しました。招待リンクからいつでも再参加できます。',
|
|
exitEndedBody: '"{{name}}" は終了しました。',
|
|
exitOk: 'OK',
|
|
};
|