diff --git a/AGENTS.md b/AGENTS.md index 2aa94ace..63cdf8c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,9 @@ approaches. Don't re-litigate a recorded decision without new facts. doc. - `design-conventions.md` — TALLY tokens, appearance system, type scaling, platform splits. Color is state, never decoration. +- `i18n.md` — String Catalogs (zh-Hant, ja), the A/B/C string tiers (TALLY + chrome stays English), glossary, `./Tools/build.sh strings`. Wrap new + user-visible prose in `String(localized:)`. ## Known limits (v1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3b92bb3..3c549768 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,20 @@ opinionated (see `DESIGN.md` and `docs/agents/`), and agreeing on the shape up front avoids work that can't be merged. Feature PRs without a linked issue may be closed. +## Language support + +Translation PRs are welcome without prior discussion — a new language, or +corrections to an existing one (currently Traditional Chinese and Japanese). +It's recommended that you be a native speaker or a learner of the language, +though this isn't a hard requirement. + +Read [`docs/agents/i18n.md`](docs/agents/i18n.md) first. In short: strings +live in the String Catalogs (`Multiplex/Localizable.xcstrings`, +`MultiplexWidgets/Localizable.xcstrings`); the glossary and string tiers +there apply (the TALLY micro chrome deliberately stays English); a new +locale should also mirror `fastlane/metadata/en-US` for the App Store +listing. `./Tools/build.sh strings` syncs the catalogs after code changes. + ## Before you send a PR - Edit `project.yml`, never the `.xcodeproj`; run `xcodegen generate`. diff --git a/Multiplex/AppIntents/OpenHostIntents.swift b/Multiplex/AppIntents/OpenHostIntents.swift index 02fcce08..ca21f317 100644 --- a/Multiplex/AppIntents/OpenHostIntents.swift +++ b/Multiplex/AppIntents/OpenHostIntents.swift @@ -396,11 +396,11 @@ enum ShortcutSetupScriptOptions { var choices = [ Choice( value: ExternalSetupScriptSelection.rememberedToken, - title: "New Session Default" + title: String(localized: "New Session Default") ), Choice( value: ExternalSetupScriptSelection.noneToken, - title: "None" + title: String(localized: "None") ), ] var seen = Set() @@ -408,7 +408,7 @@ enum ShortcutSetupScriptOptions { let name = script.displayName.trimmingCharacters(in: .whitespacesAndNewlines) choices.append(Choice( value: script.id.uuidString, - title: name.isEmpty ? "Script" : name + title: name.isEmpty ? String(localized: "Script") : name )) } return choices diff --git a/Multiplex/Design/TallyEditorControls.swift b/Multiplex/Design/TallyEditorControls.swift index 12722a2a..d8092a4a 100644 --- a/Multiplex/Design/TallyEditorControls.swift +++ b/Multiplex/Design/TallyEditorControls.swift @@ -90,7 +90,7 @@ final class TallyEditorSwitch: UIControl { private func render(animated: Bool = false) { track.setOn(isOn, animated: animated) caption.setInk(isOn ? UIKitChassis.signal : UIKitChassis.signal2) - accessibilityValue = isOn ? "On" : "Off" + accessibilityValue = isOn ? String(localized: "On") : String(localized: "Off") if isOn { accessibilityTraits.insert(.selected) } else { @@ -238,7 +238,7 @@ enum TallyEditorRowActions { ) -> Trio { let up = TallyEditorRowActionButton( systemImage: "arrow.up", - accessibilityLabel: "Move command up", + accessibilityLabel: String(localized: "Move command up"), enabled: index > 0, scale: scale, action: { move(-1) } @@ -246,7 +246,7 @@ enum TallyEditorRowActions { up.accessibilityIdentifier = "\(identifierPrefix).moveUp.\(rowID.uuidString)" let down = TallyEditorRowActionButton( systemImage: "arrow.down", - accessibilityLabel: "Move command down", + accessibilityLabel: String(localized: "Move command down"), enabled: index < count - 1, scale: scale, action: { move(1) } @@ -254,7 +254,7 @@ enum TallyEditorRowActions { down.accessibilityIdentifier = "\(identifierPrefix).moveDown.\(rowID.uuidString)" let trash = TallyEditorRowActionButton( systemImage: "trash", - accessibilityLabel: "Delete command", + accessibilityLabel: String(localized: "Delete command"), enabled: true, scale: scale, action: delete @@ -334,7 +334,9 @@ enum TallyEditorFooter { addState == .upgrade ? "ADD COMMAND · PRO" : "ADD COMMAND", systemImage: "plus", prominent: addState == .upgrade, - accessibilityLabel: addState == .upgrade ? "Add command with Multiplex Pro" : "Add command", + accessibilityLabel: addState == .upgrade + ? String(localized: "Add command with Multiplex Pro") + : String(localized: "Add command"), action: add ) addChip.accessibilityIdentifier = "\(identifierPrefix).add" @@ -344,14 +346,14 @@ enum TallyEditorFooter { spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) let cancelChip = UIKitChassisChip( "CANCEL", - accessibilityLabel: "Cancel", + accessibilityLabel: String(localized: "Cancel"), action: cancel ) cancelChip.accessibilityIdentifier = "\(identifierPrefix).cancel" let doneChip = UIKitChassisChip( "DONE", prominent: true, - accessibilityLabel: "Done", + accessibilityLabel: String(localized: "Done"), action: done ) doneChip.accessibilityIdentifier = "\(identifierPrefix).done" diff --git a/Multiplex/InfoPlist.xcstrings b/Multiplex/InfoPlist.xcstrings new file mode 100644 index 00000000..7ceff10d --- /dev/null +++ b/Multiplex/InfoPlist.xcstrings @@ -0,0 +1,121 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "NSCameraUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex uses the camera when you take a photo to send to your remote SSH session, and when you scan the code that binds a machine to the app." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex は、リモートの SSH セッションへ送る写真を撮るときと、マシンをこの App にバインドするコードをスキャンするときにカメラを使用します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 會在你拍照傳送到遠端 SSH 工作階段,以及掃描綁定機器的條碼時使用相機。" + } + } + } + }, + "NSFaceIDUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex unlocks with Face ID when the optional App Lock setting is turned on." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オプションの「App ロック」設定をオンにすると、Multiplex は Face ID でロックを解除します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟選用的「App 鎖定」設定後,Multiplex 會使用 Face ID 解鎖。" + } + } + } + }, + "NSLocalNetworkUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex connects to SSH hosts on your local network, and finds machines offering to bind themselves to the app." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex はローカルネットワーク上の SSH ホストに接続し、この App へのバインドを申し出ているマシンを見つけます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 會連接區域網路上的 SSH 主機,並尋找正在提供綁定的機器。" + } + } + } + }, + "NSMicrophoneUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex listens only while you hold a dictation going from the terminal key bar." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex がマイクを使うのは、ターミナルのキーバーから音声入力を押し続けている間だけです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "只有在你從終端機按鍵列按住聽寫時,Multiplex 才會使用麥克風。" + } + } + } + }, + "NSSpeechRecognitionUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex transcribes your dictation so the words can be typed into the terminal session you are attached to. Recognition stays on device wherever your language supports it." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex は音声入力を文字に起こし、接続中のターミナルセッションに入力します。お使いの言語が対応していれば、認識はデバイス上で行われます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 會轉寫你的聽寫內容,好把文字輸入到你目前連接的終端機工作階段。只要你的語言支援,辨識就會在裝置上完成。" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Multiplex/Localizable.xcstrings b/Multiplex/Localizable.xcstrings new file mode 100644 index 00000000..124f5ddc --- /dev/null +++ b/Multiplex/Localizable.xcstrings @@ -0,0 +1,15804 @@ +{ + "sourceLanguage" : "en", + "strings" : { + " — the remote's own pane menu." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " — リモート自身のペインメニューです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " — 遠端自己的窗格選單。" + } + } + } + }, + " after the name scrolls to that line." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " が名前の後にあると、その行までスクロールします。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 會捲動到該行。" + } + } + } + }, + " delete-word, and your own chords or one-line text macros from CUSTOM SETUP, on every device." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " で単語削除、そしてCUSTOM SETUPで作った自分のコードや1行のテキストマクロが、すべてのデバイスで使えます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 刪除單字,以及你自己在 CUSTOM SETUP 建立的組合鍵或單行文字巨集,在每部裝置上都能用。" + } + } + } + }, + " in the press block right-clicks the remote for you — herdr's pane menu opens under your finger." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " は長押しブロックからリモートを右クリックします — herdrのペインメニューが指の下に開きます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 在長按區塊中會替你對遠端執行右鍵點擊 — herdr 的窗格選單會在你手指下開啟。" + } + } + } + }, + " Install it there (%@), " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " そこにインストールする(%@)か、" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 請在該主機上安裝(%@)," + } + } + } + }, + " Install it there, " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " そこにインストールするか、" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 請在該主機上安裝," + } + } + } + }, + " key about half a second: taps stop summoning the keyboard. Hold again — or ⋯ → Unlock Keyboard — to release." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " キーを約0.5秒長押しすると、タップしてもキーボードが出なくなります。もう一度長押しする — または ⋯ → Unlock Keyboard — で解除できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 鍵約半秒:輕點就不會再叫出鍵盤。再按住一次 — 或用 ⋯ → Unlock Keyboard — 即可解除。" + } + } + } + }, + " newline, a double " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " で改行、2回連続の " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 換行、連按兩次 " + } + } + } + }, + " opens a message box: write with autocorrect and your keyboard's own dictation, attach photos or files, then " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " はメッセージボックスを開きます。自動修正やキーボードの音声入力で書き、写真やファイルを添付して、" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 會開啟訊息框:用自動修正和鍵盤自己的聽寫撰寫、附加照片或檔案,然後 " + } + } + } + }, + " opens it in the File Viewer. A " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " はファイルビューアで開きます。名前の後の " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 會在檔案檢視器中開啟。名稱後面的 " + } + } + } + }, + " sends it all as one message. The rail keeps driving the pane while you write; hold " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " ですべてを1つのメッセージとして送信します。書いている間もキーバーはペインを操作し続けます。長押しすると " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 會把全部內容當成一則訊息送出。你在寫的時候,按鍵列仍可操作窗格;按住 " + } + } + } + }, + " to type without submitting. Locking the keyboard closes it." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " で送信せずに入力できます。キーボードをロックすると閉じます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 就能輸入而不送出。鎖定鍵盤會關閉它。" + } + } + } + }, + ", %lld times" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、%lld回" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",%lld 次" + } + } + } + }, + ", closes the panel" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、パネルを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",會關閉面板" + } + } + } + }, + ", expanded" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、展開済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",已展開" + } + } + } + }, + ", or dock the page beside this tab as a " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、またはページをこのタブの隣にドックできます — " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",或把頁面停靠在這個標籤頁旁成為 " + } + } + } + }, + ", or open the branch's diff from its ± counts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、または ± の数からブランチの差分を開けます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",或從 ± 計數開啟該分支的差異" + } + } + } + }, + ", panel stays open" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、パネルは開いたまま" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",面板會保持開啟" + } + } + } + }, + ", then Enter" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、その後Enter" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",然後按 Enter" + } + } + } + }, + ", titled %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、タイトルは%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",標題為 %@" + } + } + } + }, + ", vendored" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "、ベンダー同梱" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",已內嵌" + } + } + } + }, + "; a hold opens KEY COMMANDS — " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "を固定、長押しでKEY COMMANDS — " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : ",長按開啟 KEY COMMANDS — " + } + } + } + }, + ". Attach" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "。アタッチ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "。連接" + } + } + } + }, + ". On Vision Pro, links glow under your eye; pinch for the same sheet." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "。Vision Proでは、視線を向けるとリンクが光ります。ピンチで同じシートが開きます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "。在 Vision Pro 上,你注視時連結會發亮;捏合可開啟同一張表單。" + } + } + } + }, + "\"ALLOW PASTE\" EVERY TIME?" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "毎回「ペーストを許可」が表示される" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "老是跳出「允許貼上」?" + } + } + } + }, + "%@" : { + "shouldTranslate" : false + }, + "%@ %lld, %@, current %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ %2$lld, %3$@, current %4$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ %lld、%@、現在の%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ %lld,%@,目前的%@" + } + } + } + }, + "%@ active" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@アクティブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 使用中" + } + } + } + }, + "%@ connected" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@接続済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已連線" + } + } + } + }, + "%@ didn't answer — check the port, and any firewall between this device and the host." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@から応答がありません — ポートと、このデバイスとホストの間のファイアウォールを確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 沒有回應 — 請檢查連接埠,以及這部裝置與主機之間的防火牆。" + } + } + } + }, + "%@ dropped the connection — the port may not speak SSH." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@が接続を切断しました — そのポートはSSHではない可能性があります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 中斷了連線 — 該連接埠可能不是 SSH。" + } + } + } + }, + "%@ finished" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@が完了しました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已完成" + } + } + } + }, + "%@ has a question" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@から質問があります" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 有問題要問" + } + } + } + }, + "%@ identified itself with a %@ key, which isn't among the keys Multiplex recorded for it. Nothing was sent. If the server's host keys changed, forget the recorded keys in Host Settings." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ identified itself with a %2$@ key, which isn't among the keys Multiplex recorded for it. Nothing was sent. If the server's host keys changed, forget the recorded keys in Host Settings." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は%@鍵で自身を証明しましたが、Multiplexが記録している鍵に含まれていません。何も送信していません。サーバのホスト鍵が変わった場合は、ホスト設定で記録済みの鍵を削除してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 以 %@ 金鑰表明身分,但那不在 Multiplex 為它記錄的金鑰之中。沒有送出任何資料。如果伺服器的主機金鑰變更了,請在主機設定中移除已記錄的金鑰。" + } + } + } + }, + "%@ identified itself with a %@ key, which isn't recorded for this host. Nothing was sent. If its host keys changed, forget the recorded keys under Host key and check again." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ identified itself with a %2$@ key, which isn't recorded for this host. Nothing was sent. If its host keys changed, forget the recorded keys under Host key and check again." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は%@鍵で自身を証明しましたが、このホストには記録がありません。何も送信していません。ホスト鍵が変わった場合は、「ホスト鍵」で記録済みの鍵を削除してもう一度確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 以 %@ 金鑰表明身分,但這部主機沒有記錄該金鑰。沒有送出任何資料。如果主機金鑰變更了,請在「主機金鑰」中移除已記錄的金鑰後再檢查一次。" + } + } + } + }, + "%@ in session %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ in session %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のセッション%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 的工作階段 %@" + } + } + } + }, + "%@ is %@ — the viewer opens %@ up to %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ is %2$@ — the viewer opens %3$@ up to %4$@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は%@です — ビューアが開ける%@の上限は%@です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 為 %@ — 檢視器可開啟的%@上限為 %@。" + } + } + } + }, + "%@ IS A FOLDER" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@はフォルダです" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 是資料夾" + } + } + } + }, + "%@ is also running %@ under %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ is also running %2$@ under %3$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は%@も%@で実行しています" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 另外還有 %@ 在 %@ 上" + } + } + } + }, + "%@ is asking to bind" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@がバインドを要求しています" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 要求綁定" + } + } + } + }, + "%@ is disabled and is not being connected" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は無効なので接続しません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已停用,不會連線" + } + } + } + }, + "%@ is disabled. Enable" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は無効です。有効にする" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已停用。啟用" + } + } + } + }, + "%@ is disabled. Enable it on the deck to open sessions on it." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は無効です。Deckで有効にすると、セッションを開けます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已停用。請在 Deck 上啟用它,才能在上面開啟工作階段。" + } + } + } + }, + "%@ is over 64 MB" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は64 MBを超えています" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 超過 64 MB" + } + } + } + }, + "%@ is running %@ as well as %@. Showing both %@.\n\nNew sessions still start on %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ is running %2$@ as well as %3$@. Showing both %4$@.\n\nNew sessions still start on %5$@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@は%@と%@の両方を実行しています。どちらの%@も表示します。\n\n新しいセッションは引き続き%@で作成されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 同時執行 %@ 和 %@。兩者的%@都會顯示。\n\n新的工作階段仍會在 %@ 上建立。" + } + } + } + }, + "%@ isn’t installed on %@, so there’s nothing to create the session with." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ isn’t installed on %2$@, so there’s nothing to create the session with." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@が%@にインストールされていないため、セッションを作成できません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 沒有安裝在 %@ 上,因此無法用它建立工作階段。" + } + } + } + }, + "%@ license text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のライセンス本文" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 授權條款內容" + } + } + } + }, + "%@ linking" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@接続中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 連線中" + } + } + } + }, + "%@ links stay here" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@リンクはここから出ません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 連結不會離開這裡" + } + } + } + }, + "%@ models, one per line" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のモデル、1行に1つ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 模型,每行一個" + } + } + } + }, + "%@ needs its SSH key passphrase" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@にはSSH鍵のパスフレーズが必要です" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 需要 SSH 金鑰密語" + } + } + } + }, + "%@ needs its SSH key passphrase. Unlock" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@にはSSH鍵のパスフレーズが必要です。ロック解除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 需要 SSH 金鑰密語。解鎖" + } + } + } + }, + "%@ of %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ of %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ / %@" + } + } + } + }, + "%@ on %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ on %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@(%@)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 於 %@" + } + } + } + }, + "%@ presented a different host key than the one recorded for this host — %@, got %@. Nothing was sent. If you rebuilt the server, forget the recorded key under Host key and check again." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ presented a different host key than the one recorded for this host — %2$@, got %3$@. Nothing was sent. If you rebuilt the server, forget the recorded key under Host key and check again." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@が提示したホスト鍵はこのホストの記録と異なります — 記録は%@、受信は%@です。何も送信していません。サーバを再構築した場合は、「ホスト鍵」で記録済みの鍵を削除してもう一度確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 提供的主機金鑰與這部主機記錄的不同 — 記錄為 %@,收到 %@。沒有送出任何資料。如果你重建了伺服器,請在「主機金鑰」中移除已記錄的金鑰後再檢查一次。" + } + } + } + }, + "%@ presented a different SSH host key than the one Multiplex recorded. Either the server was rebuilt, or something is impersonating it. Nothing was sent. Recorded %@, got %@. If you rebuilt it, forget the recorded key in Host Settings." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ presented a different SSH host key than the one Multiplex recorded. Either the server was rebuilt, or something is impersonating it. Nothing was sent. Recorded %2$@, got %3$@. If you rebuilt it, forget the recorded key in Host Settings." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@が提示したSSHホスト鍵はMultiplexが記録したものと異なります。サーバが再構築されたか、何かが成りすましています。何も送信していません。記録は%@、受信は%@です。再構築した場合は、ホスト設定で記録済みの鍵を削除してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 提供的 SSH 主機金鑰與 Multiplex 記錄的不同。可能是伺服器重建了,也可能有東西正在冒充它。沒有送出任何資料。記錄為 %@,收到 %@。如果是你重建的,請在主機設定中移除已記錄的金鑰。" + } + } + } + }, + "%@ rang the bell" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@がベルを鳴らしました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 發出了響鈴" + } + } + } + }, + "%@ refused the connection on port %@ — is an SSH server listening there?" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ refused the connection on port %2$lld — is an SSH server listening there?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@がポート%2$lldでの接続を拒否しました — SSHサーバーが待ち受けているか確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ 拒絕了連接埠 %2$lld 上的連線 — 請確保有 SSH 伺服器正在監聽。" + } + } + } + }, + "%@ rejected the key — check the user name, and that the key's public half is in ~/.ssh/authorized_keys on the host." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@が鍵を拒否しました — ユーザ名と、鍵の公開部分がホストの ~/.ssh/authorized_keysにあるかを確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 拒絕了金鑰 — 請檢查使用者名稱,以及金鑰的公開部分是否在主機的 ~/.ssh/authorized_keys 中。" + } + } + } + }, + "%@ rejected the sign-in — check the user name and password." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@がサインインを拒否しました — ユーザ名とパスワードを確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 拒絕了登入 — 請檢查使用者名稱和密碼。" + } + } + } + }, + "%@ standby" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@待機" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 待命" + } + } + } + }, + "%@ tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@タブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 標籤頁" + } + } + } + }, + "%@ tab, active" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@タブ、アクティブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 標籤頁,使用中" + } + } + } + }, + "%@ theme" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@テーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 主題" + } + } + } + }, + "%@ unreachable" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@接続不可" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 無法連線" + } + } + } + }, + "%@ Unreachable" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続できません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 無法連線" + } + } + } + }, + "%@ unreachable. Reconnect" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続できません。再接続" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 無法連線。重新連線" + } + } + } + }, + "%@ wants permission" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@が許可を求めています" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 要求權限" + } + } + } + }, + "%@ wasn't found on the host — the deck can't list sessions there. Plain shells still work." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@がホストで見つかりません — Deckはそこのセッションを一覧できません。通常のシェルは使えます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 在主機上找不到 — Deck 無法列出上面的工作階段。一般的 Shell 仍可使用。" + } + } + } + }, + "%@, %@, attached" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@, %2$@, attached" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、%@、アタッチ済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,%@,已連接" + } + } + } + }, + "%@, %@, press twice to confirm" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@, %2$@, press twice to confirm" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、%@、2回押して確定" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,%@,按兩次以確認" + } + } + } + }, + "%@, edit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、編集" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,編輯" + } + } + } + }, + "%@, editing" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、編集中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,編輯中" + } + } + } + }, + "%@, less" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、減らす" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,減少" + } + } + } + }, + "%@, line %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@, line %2$lld" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@、%2$lld行目" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@,第 %2$lld 行" + } + } + } + }, + "%@, more" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、増やす" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,增加" + } + } + } + }, + "%@, press again to close" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、もう一度押すと閉じます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,再按一次可關閉" + } + } + } + }, + "%@, upload failed, %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@, upload failed, %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、アップロード失敗、%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,上傳失敗,%@" + } + } + } + }, + "%@, uploading %lld percent" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@, uploading %2$lld percent" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@、アップロード中%lldパーセント" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@,上傳中,百分之 %lld" + } + } + } + }, + "%@: the Mac's keychain is locked, so Claude Code shows signed out" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@: Macのキーチェーンがロックされているため、Claude Codeはサインアウト表示になります" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@:Mac 的鑰匙圈已鎖定,因此 Claude Code 顯示為已登出" + } + } + } + }, + "%lld additions, %lld deletions" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld additions, %2$lld deletions" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld行追加、%lld行削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增 %lld 行,刪除 %lld 行" + } + } + } + }, + "%lld KEY COMMANDS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld件のキーコマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 個按鍵指令" + } + } + } + }, + "%lld sessions" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldセッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 個工作階段" + } + } + } + }, + "%lld tabs" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldタブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 個標籤頁" + } + } + } + }, + "%lld windows and %lld panes" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld windows and %2$lld panes" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldウインドウ、%lldペイン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 個視窗和 %lld 個窗格" + } + } + } + }, + "%lld windows, %lld panes. %@%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld windows, %2$lld panes. %3$@%4$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldウインドウ、%lldペイン。%@%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 個視窗,%lld 個窗格。%@%@" + } + } + } + }, + "+ New Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 新規セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 新增工作階段" + } + } + } + }, + "✳ AGENT HELPERS · PRO" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "✳ エージェントヘルパー · PRO" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "✳ AGENT 輔助按鈕 · PRO" + } + } + } + }, + "1 session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 個工作階段" + } + } + } + }, + "A drag scrolls the remote screen itself — the app speaks wheel, or arrow keys in full-screen apps. There is no separate local scrollback to fall out of." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグはリモート画面そのものをスクロールします — Appはホイールを、フルスクリーンアプリでは矢印キーを送ります。ずれてしまう別個のローカルスクロールバックはありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "拖曳會直接捲動遠端畫面 — App 送出滾輪事件,在全螢幕程式中則送方向鍵。這裡沒有另一份本機捲動記錄會脫節。" + } + } + } + }, + "A fresh %@ session, its own tile on the deck. Choose a session to add a tab to its focused workspace instead." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しい%@セッションを作成し、Deckに独自のタイルを持ちます。セッションを選ぶと、代わりにそのフォーカス中のワークスペースにタブを追加します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "全新的 %@ 工作階段,在 Deck 上有自己的方塊。也可以選擇某個工作階段,改為在它的焦點工作區加一個標籤頁。" + } + } + } + }, + "A host shows no tmux or herdr" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストにtmuxもherdrも表示されない" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機上看不到 tmux 或 herdr" + } + } + } + }, + "A machine on another network — a VPS, a box behind a firewall — can't announce itself here. Scan the QR its terminal printed, or have it hand you the code." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "別のネットワークにあるマシン — VPSやファイアウォールの内側のマシン — はここに自身を通知できません。ターミナルに表示されたQRコードをスキャンするか、コードを教えてもらってください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在其他網路上的機器 — VPS、防火牆後方的主機 — 無法在這裡廣播自己。掃描它終端機印出的 QR 碼,或請它提供代碼。" + } + } + } + }, + "A mouse's secondary click runs the same chain as a long press. On herdr it adds " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マウスの副ボタンクリックは長押しと同じ動作をします。herdrではさらに " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滑鼠的次要點按會執行和長按相同的流程。在 herdr 上還會加上 " + } + } + } + }, + "A newline that doesn't send the line." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "行を送信せずに改行します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "換行,但不送出這一行。" + } + } + } + }, + "A path on %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@上のパス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 上的路徑" + } + } + } + }, + "A public address" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パブリックアドレス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開位址" + } + } + } + }, + "a session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段" + } + } + } + }, + "A tap latches " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップで " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "輕點鎖定 " + } + } + } + }, + "About" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "情報" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關於" + } + } + } + }, + "Acquiring signal" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "信号を取得中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在取得訊號" + } + } + } + }, + "Actions for %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@の操作" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 的動作" + } + } + } + }, + "Add command" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドを追加" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "加入指令" + } + } + } + }, + "Add command with Multiplex Pro" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proでコマンドを追加" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 Multiplex Pro 加入指令" + } + } + } + }, + "Add directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディレクトリを追加" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "加入目錄" + } + } + } + }, + "Add host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストを追加" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "加入主機" + } + } + } + }, + "Add Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストを追加" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "加入主機" + } + } + } + }, + "Add script" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリプトを追加" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "加入指令碼" + } + } + } + }, + "Added to the fleet — it's on the deck now." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追加しました — Deckに表示されています。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已加入 — 現在就在 Deck 上。" + } + } + } + }, + "Address" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アドレス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "位址" + } + } + } + }, + "Adds a tab to “%@”'s focused workspace — no new session is created." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」のフォーカス中のワークスペースにタブを追加します — 新しいセッションは作成されません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在「%@」的焦點工作區加入一個標籤頁 — 不會建立新的工作階段。" + } + } + } + }, + "Agent" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェント" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent" + } + } + } + }, + "Agent alerts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェント通知" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 提醒" + } + } + } + }, + "Agent Alerts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェント通知" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 提醒" + } + } + } + }, + "Agent default" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントのデフォルト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 預設" + } + } + } + }, + "Agent Default" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントのデフォルト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 預設" + } + } + } + }, + "Agent Helpers" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントヘルパー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 輔助按鈕" + } + } + } + }, + "AGENT HELPERS + ALERTS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントヘルパーと通知" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 輔助按鈕與提醒" + } + } + } + }, + "Agent helpers Pro" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントヘルパー Pro" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 輔助按鈕 Pro" + } + } + } + }, + "AGENT IS BUSY" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントは処理中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 忙碌中" + } + } + } + }, + "Agent launch models" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントの起動モデル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 啟動模型" + } + } + } + }, + "agent needs your input" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントが入力を待っています" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 需要你的輸入" + } + } + } + }, + "agent running" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェント実行中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 執行中" + } + } + } + }, + "Agents" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェント" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent" + } + } + } + }, + "Also Show %@ Sessions" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@セッションも表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "同時顯示 %@ 工作階段" + } + } + } + }, + "An absolute, home-relative, or working-directory-relative path on the host." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト上の絶対パス、またはホームディレクトリや作業ディレクトリからの相対パス。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機上的絕對路徑,或相對於家目錄、工作目錄的路徑。" + } + } + } + }, + "Another announcement is using this name. If you started only one mpx bind, one of these isn't your machine — scan its QR instead." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "別のアナウンスが同じ名前を使っています。mpx bindを1回しか実行していない場合、どちらかはあなたのマシンではありません — 代わりにQRコードをスキャンしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "另一則廣播也用了這個名稱。如果你只執行了一次 mpx bind,其中一台就不是你的機器 — 請改為掃描它的 QR 碼。" + } + } + } + }, + "ANOTHER CLIENT RESIZES THIS SESSION" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "別のクライアントがこのセッションをリサイズする" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "其他用戶端調整這個工作階段的大小" + } + } + } + }, + "ANSI · Bright" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANSI · ブライト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANSI · 亮色" + } + } + } + }, + "ANSI · Normal" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANSI · ノーマル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANSI · 一般" + } + } + } + }, + "ANSWER THE AGENT FIRST" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "先にエージェントに返答" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "先回覆 Agent" + } + } + } + }, + "App lock" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appロック" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "App 鎖定" + } + } + } + }, + "Appearance" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外観" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "外觀" + } + } + } + }, + "Arrow down" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "下矢印" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向下鍵" + } + } + } + }, + "Arrow left" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "左矢印" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向左鍵" + } + } + } + }, + "Arrow right" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "右矢印" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向右鍵" + } + } + } + }, + "Arrow up" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上矢印" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向上鍵" + } + } + } + }, + "Asking to bind" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バインドを要求中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在要求綁定" + } + } + } + }, + "Attach a file" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルを添付" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "附加檔案" + } + } + } + }, + "Attach as New Tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規タブでアタッチ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以新標籤頁連接" + } + } + } + }, + "Attach in New Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規ウインドウでアタッチ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在新視窗中連接" + } + } + } + }, + "auto submit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動送信" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動送出" + } + } + } + }, + "Auto Submit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動送信" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動送出" + } + } + } + }, + "Awaiting signal" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "信号を待機中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "等待訊號" + } + } + } + }, + "Back" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "戻る" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "返回" + } + } + } + }, + "Back 15 seconds" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "15秒戻す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "倒退 15 秒" + } + } + } + }, + "Back to live" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライブに戻る" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "回到即時畫面" + } + } + } + }, + "Back to rendered markdown" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レンダリングされたMarkdownに戻る" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "回到 Markdown 排版檢視" + } + } + } + }, + "Backend" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バックエンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "後端" + } + } + } + }, + "Backends" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バックエンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "後端" + } + } + } + }, + "Background" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "背景" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "背景" + } + } + } + }, + "Background keep-alive is off: leaving the app suspends it, this host's sessions drop, and its tabs reattach when you return." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バックグラウンドのキープアライブがオフです:Appを離れると一時停止し、このホストのセッションは切断され、戻るとタブが再アタッチされます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "背景保持連線已關閉:離開 App 會讓它暫停,這台主機的工作階段會中斷,回來時它的標籤頁會重新連接。" + } + } + } + }, + "Bell" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "提示音" + } + } + } + }, + "Binary files differ." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バイナリファイルが異なります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "二進位檔案內容不同。" + } + } + } + }, + "Binding never sends a private key: this device makes its own key and the machine adds the public half to authorized_keys. mpx unbind removes it." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バインドで秘密鍵が送信されることはありません:このデバイスが自分で鍵を作成し、マシンは公開鍵をauthorized_keysに追加します。mpx unbindで削除できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "綁定絕不會傳送私鑰:這台裝置會自行產生金鑰,機器只把公鑰加進 authorized_keys。mpx unbind 可以移除它。" + } + } + } + }, + "Black" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブラック" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "黑色" + } + } + } + }, + "Blue" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブルー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "藍色" + } + } + } + }, + "Both are shown, each tile marked with the backend it came from. That %@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "両方が表示され、各タイルにはどのバックエンド由来かが示されます。これにより%@。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "兩者都會顯示,每個方塊都標示它來自哪個後端。這%@。" + } + } + } + }, + "Bound, but the first connection failed: %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バインドしましたが、最初の接続に失敗しました:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已綁定,但第一次連線失敗:%@" + } + } + } + }, + "Bright Black" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトブラック" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮黑色" + } + } + } + }, + "Bright Blue" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトブルー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮藍色" + } + } + } + }, + "Bright Cyan" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトシアン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮青色" + } + } + } + }, + "Bright Green" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトグリーン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮綠色" + } + } + } + }, + "Bright Magenta" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトマゼンタ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮洋紅色" + } + } + } + }, + "Bright Red" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトレッド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮紅色" + } + } + } + }, + "Bright White" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトホワイト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮白色" + } + } + } + }, + "Bright Yellow" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブライトイエロー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "亮黃色" + } + } + } + }, + "Build and edit terminal palettes while the Tally chassis stays consistent." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tallyのシャーシは一貫したまま、ターミナルパレットを作成・編集できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立並編輯終端機調色盤,Tally 外框維持一致。" + } + } + } + }, + "Built-in commands" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "組み込みコマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "內建指令" + } + } + } + }, + "BUILT-IN PALETTE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "内蔵パレット" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "內建調色盤" + } + } + } + }, + "Built-in themes" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "組み込みテーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "內建主題" + } + } + } + }, + "Built-ins and custom commands each live in Bar or More; custom Bar labels keep 9 characters." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "組み込みコマンドもカスタムコマンドもBarかMoreに配置します。カスタムのBarラベルは9文字までです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "內建與自訂指令都可放在 Bar 或 More;自訂 Bar 標籤限 9 個字元。" + } + } + } + }, + "Buy once. Use it on iPad and Vision Pro." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "一度購入すれば、iPadとVision Proの両方で使えます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "一次購買,iPad 與 Vision Pro 都能使用。" + } + } + } + }, + "CAN'T DIFF %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@の差分を表示できません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法比對 %@ 的差異" + } + } + } + }, + "Can't Open %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を開けません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法開啟 %@" + } + } + } + }, + "Can't open link" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リンクを開けません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法開啟連結" + } + } + } + }, + "CAN'T READ %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を読み込めません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法讀取 %@" + } + } + } + }, + "CAN'T SHOW — %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示できません — %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法顯示 — %@" + } + } + } + }, + "Can't show this image. %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この画像を表示できません。%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法顯示這張圖片。%@" + } + } + } + }, + "Cancel" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キャンセル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } + } + } + }, + "Cancel address edit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アドレスの編集をキャンセル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消編輯網址" + } + } + } + }, + "Cancel finding message" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージの検索をキャンセル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消尋找訊息" + } + } + } + }, + "Cancelled" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キャンセル済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已取消" + } + } + } + }, + "Check the connection to %@ and try again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@への接続を確認して、もう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "請檢查與 %@ 的連線後再試一次。" + } + } + } + }, + "Checked" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チェック済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已勾選" + } + } + } + }, + "Checked on every connection. Forget these only if you rebuilt the server." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続のたびに確認されます。サーバーを再構築した場合にのみ削除してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每次連線都會檢查。只有在你重建伺服器後才需要忘記這些金鑰。" + } + } + } + }, + "Checking the connection…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続を確認中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在檢查連線…" + } + } + } + }, + "Checking your App Store purchases…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeの購入項目を確認中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在檢查你的 App Store 購買項目…" + } + } + } + }, + "Checks this Apple ID for a Multiplex Pro purchase" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このApple IDにMultiplex Proの購入があるか確認します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢查此 Apple ID 是否購買過 Multiplex Pro" + } + } + } + }, + "Choose a backend" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バックエンドを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇後端" + } + } + } + }, + "Choose a model" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モデルを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇模型" + } + } + } + }, + "Choose a session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇工作階段" + } + } + } + }, + "Choose a session from the deck to attach it here." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckからセッションを選ぶと、ここにアタッチできます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從 Deck 選擇一個工作階段,即可在此連接。" + } + } + } + }, + "Choose a setup script" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セットアップスクリプトを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇設定指令碼" + } + } + } + }, + "Choose a terminal palette. Press and hold a theme to duplicate it as a custom starting point." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルパレットを選びます。テーマを長押しすると複製され、カスタムの出発点になります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇終端機調色盤。長按主題可複製一份,作為自訂主題的起點。" + } + } + } + }, + "Choose a working directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作業ディレクトリを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇工作目錄" + } + } + } + }, + "Choose Claude Code, Codex, Pi, or Grok Build" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code、Codex、Pi、Grok Buildから選択します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇 Claude Code、Codex、Pi 或 Grok Build" + } + } + } + }, + "Choose Focused Pane to inherit the session's own directory." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Focused Paneを選ぶと、セッション自身のディレクトリを引き継ぎます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇 Focused Pane 可沿用工作階段本身的目錄。" + } + } + } + }, + "Choose Home to use the login shell's default." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Homeを選ぶと、ログインシェルのデフォルトを使います。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇 Home 可使用登入 Shell 的預設值。" + } + } + } + }, + "Choose where the agent opens" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントを開く場所を選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇 Agent 開啟的位置" + } + } + } + }, + "Choose Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウインドウを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇視窗" + } + } + } + }, + "Chords send what a hardware keyboard would; text rows type one line, then Enter when SUBMIT is on." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コードはハードウェアキーボードと同じ入力を送ります。テキスト行は1行を入力し、SUBMITがオンならそのあとにEnterを送ります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "組合鍵會送出硬體鍵盤的按鍵;文字列會輸入一行,並在 SUBMIT 開啟時按下 Enter。" + } + } + } + }, + "Claude Code" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code" + } + } + } + }, + "Claude Code on %@ is waiting at its sign-in screen, but the stored login is probably intact: on a Mac, Claude Code keeps its credentials in the login keychain, and an SSH or tmux session never unlocks it — no GUI login happened. Unlock it once in any shell on %@, then restart Claude Code:" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Claude Code on %1$@ is waiting at its sign-in screen, but the stored login is probably intact: on a Mac, Claude Code keeps its credentials in the login keychain, and an SSH or tmux session never unlocks it — no GUI login happened. Unlock it once in any shell on %2$@, then restart Claude Code:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のClaude Codeはサインイン画面で待機していますが、保存されたログインはおそらく無事です。MacではClaude Codeは認証情報をログインキーチェーンに保存しますが、SSHやtmuxのセッションではそれが解除されません — GUIログインが行われていないためです。%@上のいずれかのシェルで一度解除してから、Claude Codeを再起動してください:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 上的 Claude Code 停在登入畫面,但已儲存的登入資訊很可能仍然完好:在 Mac 上,Claude Code 會把憑證存在登入鑰匙圈中,而 SSH 或 tmux 工作階段永遠不會解鎖它 — 因為沒有經過 GUI 登入。請在 %@ 上的任一 Shell 中解鎖一次,然後重新啟動 Claude Code:" + } + } + } + }, + "Claude Code shows signed out" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Codeがサインアウト表示になる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code 顯示為已登出" + } + } + } + }, + "Claude Code shows signed out in tmux" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "tmuxでClaude Codeがサインアウト表示になる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code 在 tmux 中顯示為已登出" + } + } + } + }, + "Clear" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "消去" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除" + } + } + } + }, + "Clear Browsing Data" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閲覧データを消去" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除瀏覽資料" + } + } + } + }, + "Clear Browsing Data…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閲覧データを消去…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除瀏覽資料…" + } + } + } + }, + "cleared — nothing applied" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリア済み — 何も適用されません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已清除 — 不會套用任何內容" + } + } + } + }, + "Clears cookies, caches, and site storage for every viewport page — dev-server logins included. This page reloads signed out." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべてのビューポートページのCookie、キャッシュ、サイトストレージを消去します — 開発サーバーのログインも含みます。このページはサインアウトした状態で再読み込みされます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除所有檢視區頁面的 cookie、快取與網站儲存空間 — 包含開發伺服器的登入。此頁面會以登出狀態重新載入。" + } + } + } + }, + "CLIPBOARD" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリップボード" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "剪貼板" + } + } + } + }, + "Close" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉" + } + } + } + }, + "Close file viewer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルビューアを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉檔案檢視器" + } + } + } + }, + "Close message box" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージボックスを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉訊息框" + } + } + } + }, + "Close Pane" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉窗格" + } + } + } + }, + "Close Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉工作階段" + } + } + } + }, + "Close tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タブを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉標籤頁" + } + } + } + }, + "Close Tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タブを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉標籤頁" + } + } + } + }, + "Close the message box" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージボックスを閉じます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉訊息框" + } + } + } + }, + "Close the panel on press" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "押すとパネルを閉じます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按下可關閉面板" + } + } + } + }, + "Close viewport" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビューポートを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉檢視區" + } + } + } + }, + "Close Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウインドウを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉視窗" + } + } + } + }, + "Close Workspace" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペースを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉工作區" + } + } + } + }, + "Codex" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codex" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codex" + } + } + } + }, + "Collapse message" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを折りたたむ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "收合訊息" + } + } + } + }, + "Collapsed" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "折りたたみ済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已收合" + } + } + } + }, + "Collapses connection details" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続の詳細を折りたたみます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "收合連線詳細資訊" + } + } + } + }, + "Command" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指令" + } + } + } + }, + "Command content" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドの内容" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指令內容" + } + } + } + }, + "Commands" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指令" + } + } + } + }, + "Components" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コンポーネント" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "元件" + } + } + } + }, + "Configured models for %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に設定されたモデル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已設定的模型" + } + } + } + }, + "Confirm %@ to require it when Multiplex opens" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@で確認して、Multiplexを開くときに要求します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認 %@ 以要求開啟 Multiplex 時驗證" + } + } + } + }, + "Confirm forgetting recorded host keys" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "記録済みのホスト鍵の削除を確認" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認忘記已記錄的主機金鑰" + } + } + } + }, + "conflicted" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コンフリクト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "衝突" + } + } + } + }, + "Connect on the deck" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckで接続します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Deck 上連線" + } + } + } + }, + "Connect Once" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今回のみ接続" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "僅這次連線" + } + } + } + }, + "Connect with mosh" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "moshで接続" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 mosh 連線" + } + } + } + }, + "Connected to %@ as %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Connected to %1$@ as %2$@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に%@として接続しました。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已連線到 %@,登入身分為 %@。" + } + } + } + }, + "Connecting to %@…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在連線到 %@…" + } + } + } + }, + "Connection stats" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続統計" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連線統計" + } + } + } + }, + "Connection Stats" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続統計" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連線統計" + } + } + } + }, + "CONNECTION STATS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続統計" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連線統計" + } + } + } + }, + "Connection stats are turned off in Settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続統計は設定でオフになっています。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連線統計已在設定中關閉。" + } + } + } + }, + "Connection stats for %@: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Connection stats for %1$@: %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@の接続統計:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 的連線統計:%@" + } + } + } + }, + "Connection Stats…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続統計…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連線統計…" + } + } + } + }, + "Connects over mosh" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "moshで接続します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "透過 mosh 連線" + } + } + } + }, + "Control" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Control" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Control" + } + } + } + }, + "Control %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Control %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Control %@" + } + } + } + }, + "Copy" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } + } + } + }, + "Copy %@ license text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のライセンス文をコピー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製 %@ 的授權文字" + } + } + } + }, + "Copy a line, run it in a terminal on the machine you want to add, then leave mpx bind running. Nothing here runs on this device." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "いずれかの行をコピーし、追加したいマシンのターミナルで実行して、mpx bindを実行したままにします。ここのコマンドはこのデバイスでは実行されません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製其中一行,在你要加入的機器上以終端機執行,然後讓 mpx bind 保持執行。這裡的指令都不會在本裝置上執行。" + } + } + } + }, + "Copy Address" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アドレスをコピー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製位址" + } + } + } + }, + "Copy command" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドをコピー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製指令" + } + } + } + }, + "Copy Command for Handoff" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "引き継ぎ用コマンドをコピー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製交接指令" + } + } + } + }, + "Copy license text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンス文をコピー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製授權文字" + } + } + } + }, + "Copy Mode" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピーモード" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製模式" + } + } + } + }, + "Copy selection" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択範囲をコピー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製選取內容" + } + } + } + }, + "Couldn't connect to %@: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Couldn't connect to %1$@: %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続できませんでした:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連線至 %@:%@" + } + } + } + }, + "Couldn't create %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を作成できませんでした" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法建立 %@" + } + } + } + }, + "Couldn't Create Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションを作成できません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法建立工作階段" + } + } + } + }, + "Couldn't Create Tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タブを作成できません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法建立標籤頁" + } + } + } + }, + "Couldn't create the session on %@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@でセッションを作成できませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法在 %@ 上建立工作階段。" + } + } + } + }, + "Couldn't find “%@” — check the address for typos." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」が見つかりません — アドレスに誤りがないか確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "找不到「%@」— 請檢查位址是否有拼字錯誤。" + } + } + } + }, + "Couldn't open a new %@ in session %@ on %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Couldn't open a new %1$@ in session %2$@ on %3$@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%3$@のセッション%2$@で新しい%1$@を開けませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法在 %3$@ 的工作階段 %2$@ 中開啟新的 %1$@。" + } + } + } + }, + "Couldn't open a new tab in session %@ on %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Couldn't open a new tab in session %1$@ on %2$@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@のセッション%1$@で新しいタブを開けませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法在 %2$@ 的工作階段 %1$@ 中開啟新標籤頁。" + } + } + } + }, + "Couldn't reach %@ (%@)." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Couldn't reach %1$@ (%2$@)." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続できませんでした(%@)。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連上 %@(%@)。" + } + } + } + }, + "Couldn't reach %@ over mosh. %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Couldn't reach %1$@ over mosh. %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "moshで%@に接続できませんでした。%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法透過 mosh 連上 %@。%@" + } + } + } + }, + "Couldn't reach %@ to start mosh (%@)." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Couldn't reach %1$@ to start mosh (%2$@)." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "moshを開始するため%@に接続できませんでした(%@)。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連上 %@ 以啟動 mosh(%@)。" + } + } + } + }, + "Couldn't reach %@. %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Couldn't reach %1$@. %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続できませんでした。%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連上 %@。%@" + } + } + } + }, + "Couldn't reach %@. No response." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続できませんでした。応答がありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連上 %@。沒有回應。" + } + } + } + }, + "Couldn't reach the machine — %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マシンに接続できませんでした — %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連上這台機器 — %@" + } + } + } + }, + "Couldn't resolve %@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を解決できませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法解析 %@。" + } + } + } + }, + "Couldn’t seal the key with that passphrase — nothing was enrolled." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのパスフレーズで鍵を封印できませんでした — 何も登録されていません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法用該密語封裝金鑰 — 未註冊任何內容。" + } + } + } + }, + "Couldn’t seal the key with that passphrase — nothing was saved." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのパスフレーズで鍵を封印できませんでした — 何も保存されていません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法用該密語封裝金鑰 — 未儲存任何內容。" + } + } + } + }, + "Create & Attach" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作成してアタッチ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立並連接" + } + } + } + }, + "Create and attach" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作成してアタッチ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立並連接" + } + } + } + }, + "Creates" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作成" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立" + } + } + } + }, + "Creates the %@ session, then attaches to its login shell. %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Creates the %1$@ session, then attaches to its login shell. %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@セッションを作成し、そのログインシェルにアタッチします。%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立 %@ 工作階段,然後連接其登入 Shell。%@" + } + } + } + }, + "Creating, duplicating, and editing custom themes requires Multiplex Pro. Existing themes remain selectable and deletable." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムテーマの作成、複製、編集にはMultiplex Proが必要です。既存のテーマは引き続き選択および削除できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立、複製與編輯自訂主題需要 Multiplex Pro。既有主題仍可選用與刪除。" + } + } + } + }, + "Credentials" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "認証情報" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "憑證" + } + } + } + }, + "Current theme" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のテーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前主題" + } + } + } + }, + "Cursor" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カーソル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "游標" + } + } + } + }, + "Custom" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタム" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂" + } + } + } + }, + "Custom command" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムコマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂指令" + } + } + } + }, + "Custom command %@, %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Custom command %1$@, %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムコマンド%@、%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂指令 %@,%@" + } + } + } + }, + "CUSTOM PALETTE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムパレット" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂調色盤" + } + } + } + }, + "Custom setup" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタム設定" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂設定" + } + } + } + }, + "Custom Themes" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムテーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂主題" + } + } + } + }, + "CUSTOM THEMES" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムテーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂主題" + } + } + } + }, + "Customize Commands…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドをカスタマイズ…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂指令…" + } + } + } + }, + "Cyan" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シアン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "青色" + } + } + } + }, + "dark" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダーク" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "深色" + } + } + } + }, + "Dark shares this terminal theme with Glass." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダークは、このターミナルテーマをGlassと共有します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "深色與 Glass 共用這個終端機主題。" + } + } + } + }, + "Deck" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck" + } + } + } + }, + "Delete" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "刪除" + } + } + } + }, + "Delete command" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドを削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "刪除指令" + } + } + } + }, + "Delete directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディレクトリを削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "刪除目錄" + } + } + } + }, + "Delete script" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリプトを削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "刪除指令碼" + } + } + } + }, + "Delete Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションを削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "刪除工作階段" + } + } + } + }, + "Delete Session…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションを削除…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "刪除工作階段…" + } + } + } + }, + "deleted" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已刪除" + } + } + } + }, + "Detach" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デタッチ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "中斷連接" + } + } + } + }, + "Detach or close the session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションをデタッチまたは終了" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "中斷連接或關閉工作階段" + } + } + } + }, + "Detached" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デタッチ済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已中斷連接" + } + } + } + }, + "Detected in: " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出場所:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "偵測到於:" + } + } + } + }, + "device passcode" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスのパスコード" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "裝置密碼" + } + } + } + }, + "DICTATE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "音声入力" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "聽寫" + } + } + } + }, + "Directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディレクトリ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "目錄" + } + } + } + }, + "Disable Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストを無効にする" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "停用主機" + } + } + } + }, + "Disabled" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無効" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已停用" + } + } + } + }, + "Dismiss %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉 %@" + } + } + } + }, + "Dismiss machine" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マシンを閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉機器" + } + } + } + }, + "Don't Offer %@ Here" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ここでは%@を表示しない" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不要在這裡提供 %@" + } + } + } + }, + "Done" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } + } + } + }, + "Done selecting" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択を完了" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成選取" + } + } + } + }, + "DOUBLE TAP" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダブルタップ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點兩下" + } + } + } + }, + "Drag to reorder within this window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグしてこのウインドウ内で並べ替え" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "拖曳以在此視窗內重新排序" + } + } + } + }, + "Draws terminal text on the GPU instead of CoreGraphics. Takes effect for newly opened terminal windows." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルのテキストをCoreGraphicsではなくGPUで描画します。新しく開いたターミナルウインドウから有効になります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 GPU 取代 CoreGraphics 繪製終端機文字。對新開啟的終端機視窗生效。" + } + } + } + }, + "Drop to upload" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドロップしてアップロード" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "拖放以上傳" + } + } + } + }, + "Duplicate" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } + } + } + }, + "EDGE SWIPE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エッジスワイプ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "邊緣滑動" + } + } + } + }, + "Edit Host…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストを編集…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "編輯主機…" + } + } + } + }, + "Edit private key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "秘密鍵を編集" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "編輯私鑰" + } + } + } + }, + "Edit…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "編集…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "編輯…" + } + } + } + }, + "Edits the address" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アドレスを編集" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "編輯位址" + } + } + } + }, + "Enable Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストを有効にする" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟用主機" + } + } + } + }, + "Ended, %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "終了しました、%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已結束,%@" + } + } + } + }, + "Enroll machine" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マシンを登録" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "註冊這部機器" + } + } + } + }, + "Enrolling this device’s key…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスの鍵を登録中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在註冊此裝置的金鑰…" + } + } + } + }, + "Enter a password first." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "先にパスワードを入力してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "請先輸入密碼。" + } + } + } + }, + "Enter a remote file path and an optional positive line number." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートのファイルパスと、必要に応じて正の行番号を入力してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入遠端檔案路徑,並可選擇填入正整數行號。" + } + } + } + }, + "Enter the document's password." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "書類のパスワードを入力してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入文件的密碼。" + } + } + } + }, + "Escape" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + } + } + }, + "Every machine bound from this pane is added on this backend — binding proves who a machine is, not what it runs. The deck monitors herdr (herdr.dev) sessions and their agents through the herdr CLI, one tile per session. Change it later in Host Settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このペインからバインドしたマシンは、すべてこのバックエンドで追加されます — バインドが証明するのはマシンの身元であって、動作しているものではありません。Deckはherdr CLIを通じてherdr(herdr.dev)のセッションとそのエージェントを監視し、セッションごとに1つのタイルを表示します。後からHost Settingsで変更できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從這個窗格綁定的每台機器都會加在這個後端上 — 綁定只證明機器的身分,不代表它執行什麼。Deck 會透過 herdr CLI 監看 herdr(herdr.dev)的工作階段與其中的 Agent,每個工作階段一個方塊。之後可在 Host Settings 中變更。" + } + } + } + }, + "Every machine bound from this pane is added on this backend — binding proves who a machine is, not what it runs. tmux is the remote server the deck monitors: sessions, windows, and agent panes. Change it later in Host Settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このペインからバインドしたマシンは、すべてこのバックエンドで追加されます — バインドが証明するのはマシンの身元であって、動作しているものではありません。tmuxはDeckが監視するリモートのサーバで、セッション、ウインドウ、エージェントのペインを対象とします。後からHost Settingsで変更できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從這個窗格綁定的每台機器都會加在這個後端上 — 綁定只證明機器的身分,不代表它執行什麼。tmux 是 Deck 監看的遠端伺服器:工作階段、視窗與 Agent 窗格。之後可在 Host Settings 中變更。" + } + } + } + }, + "Every machine bound from this pane is added showing both — binding proves who a machine is, not what it runs. Each tile is marked with the backend it came from, and that %@. Change it later in Host Settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このペインからバインドしたマシンは、両方を表示する形で追加されます — バインドが証明するのはマシンの身元であって、動作しているものではありません。各タイルには由来するバックエンドが表示され、%@。後からHost Settingsで変更できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從這個窗格綁定的每台機器都會同時顯示兩者 — 綁定只證明機器的身分,不代表它執行什麼。每個方塊都會標示它來自哪個後端,而且 %@。之後可在 Host Settings 中變更。" + } + } + } + }, + "Every tmux session, its own window in space." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべてのtmuxセッションに、空間上の専用ウインドウを。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每個 tmux 工作階段,都有自己的空間視窗。" + } + } + } + }, + "Everything Multiplex %@ changed, and the license notices for the third-party code shipped with it." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex %@での変更点と、同梱されたサードパーティコードのライセンス表示。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex %@ 的所有變更,以及隨附第三方程式碼的授權聲明。" + } + } + } + }, + "Expand message" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを展開" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開訊息" + } + } + } + }, + "Expanded" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已展開" + } + } + } + }, + "expanded folder" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開されたフォルダ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已展開的檔案夾" + } + } + } + }, + "Expands connection details" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続の詳細を展開します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開連線詳細資料" + } + } + } + }, + "Expected host key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "想定されるホスト鍵" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "預期的主機金鑰" + } + } + } + }, + "Expected key (optional)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "想定される鍵(任意)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "預期的金鑰(選填)" + } + } + } + }, + "File Path" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルパス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案路徑" + } + } + } + }, + "File upload requires tmux or herdr over SSH" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルのアップロードにはSSH経由のtmuxまたはherdrが必要です" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案上傳需要透過 SSH 的 tmux 或 herdr" + } + } + } + }, + "File Viewer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルビューア" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案檢視器" + } + } + } + }, + "Files on %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@上のファイル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 上的檔案" + } + } + } + }, + "Files…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイル…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案…" + } + } + } + }, + "First prompt" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初のプロンプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一個提示" + } + } + } + }, + "First Prompt" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初のプロンプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一個提示" + } + } + } + }, + "Focused Pane" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フォーカス中のペイン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "聚焦的窗格" + } + } + } + }, + "folder" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フォルダ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案夾" + } + } + } + }, + "For paste" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "貼り付け用" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "供貼上使用" + } + } + } + }, + "Forget recorded host keys" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "記録したホスト鍵を削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "忘記已記錄的主機金鑰" + } + } + } + }, + "Forward" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "進む" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "前進" + } + } + } + }, + "Forward 15 seconds" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "15秒進む" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "快轉 15 秒" + } + } + } + }, + "Free daily command taps return tomorrow" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無料版の1日のコマンド実行回数は明日回復します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "免費版每日的指令點擊次數明天恢復" + } + } + } + }, + "Free keeps %lld commands; Multiplex Pro raises the set to %lld." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Free keeps %1$lld commands; Multiplex Pro raises the set to %2$lld." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無料版ではコマンドは%lld個まで、Multiplex Proなら%lld個に増えます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "免費版保留 %lld 個指令;Multiplex Pro 可提升至 %lld 個。" + } + } + } + }, + "Free tier" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無料プラン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "免費版" + } + } + } + }, + "Frequently asked questions" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "よくある質問" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "常見問題" + } + } + } + }, + "From a scanned or pasted bind code." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スキャンまたは貼り付けたバインドコードから。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "來自掃描或貼上的綁定碼。" + } + } + } + }, + "From the screen's left edge, swipe right to step back to the deck." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面の左端から右にスワイプするとDeckに戻ります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從螢幕左緣往右滑,即可退回 Deck。" + } + } + } + }, + "Full notes" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リリースノート全文" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "完整版本說明" + } + } + } + }, + "Gap between sends" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信の間隔" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳送間隔" + } + } + } + }, + "gave up after fallbacks" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フォールバック後に中止" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "嘗試備援後放棄" + } + } + } + }, + "Glass shares this dark terminal theme with Dark." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ガラスはこのダークなターミナルテーマをダークと共有します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "玻璃與深色共用這個深色終端機主題。" + } + } + } + }, + "Go" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "前往" + } + } + } + }, + "Green" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グリーン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "綠色" + } + } + } + }, + "Grok Build" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grok Build" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grok Build" + } + } + } + }, + "grok-build, or a full model id" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "grok-build、または完全なモデルID" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "grok-build,或完整的模型 ID" + } + } + } + }, + "Grow the hold-CTRL set of saved chords and text macros from %lld to %lld, synced to every device." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Grow the hold-CTRL set of saved chords and text macros from %1$lld to %2$lld, synced to every device." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CTRL長押しで使う保存済みのコードとテキストマクロを%lld個から%lld個に増やし、すべてのデバイスに同期します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "把按住 CTRL 的已存組合鍵與文字巨集從 %lld 組增加到 %lld 組,並同步到所有裝置。" + } + } + } + }, + "Guide" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ガイド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指南" + } + } + } + }, + "Heard on your network. Check the address and fingerprint against the terminal, then type its PIN." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク上で検出しました。ターミナルに表示されたアドレスと指紋を確認してから、PINを入力してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在你的網路上發現。請比對終端機上的位址與指紋,然後輸入它的 PIN。" + } + } + } + }, + "herdr %@ is older than this app speaks — update herdr on the host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdr %@はこのAppが対応するバージョンより古すぎます — ホストでherdrを更新してください" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdr %@ 比這個 App 支援的版本更舊 — 請在主機上更新 herdr" + } + } + } + }, + "herdr answered status but not the session list" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdrはステータスを返しましたが、セッション一覧を返しませんでした" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdr 回應了狀態,卻沒有回應工作階段列表" + } + } + } + }, + "HERDR PANES" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdrペイン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdr 窗格" + } + } + } + }, + "Hide %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を隠す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏 %@" + } + } + } + }, + "Hide %@ helpers" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のヘルパーチップを隠す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏 %@ 輔助按鈕" + } + } + } + }, + "Hide Key passphrase" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "鍵のパスフレーズを隠す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏金鑰密語" + } + } + } + }, + "Hide the file tree" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルツリーを隠す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏檔案樹" + } + } + } + }, + "HISTORY UNAVAILABLE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "履歴を取得できません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法取得歷史" + } + } + } + }, + "Hold a pane border until it wakes, then drag it where you want." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインの境界を長押しして反応したら、好きな位置までドラッグします。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按住窗格邊界直到它有反應,再拖到你想要的位置。" + } + } + } + }, + "HOLD CTRL" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CTRLを長押し" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按住 CTRL" + } + } + } + }, + "Hold for %lld-cell steps, repeating until release." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長押しすると%lldセル単位で、離すまで繰り返し移動します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按住可每次移動 %lld 格,放開前持續重複。" + } + } + } + }, + "Hold the " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーボードの " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按住 " + } + } + } + }, + "Holds this host's sessions and probing open for the extra seconds iOS grants after you leave the app" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appを離れた後にiOSが与える数秒間、このホストのセッションと監視を維持します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在你離開 App 後,利用 iOS 額外給的幾秒鐘,維持這部主機的工作階段與偵測" + } + } + } + }, + "Home" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホーム" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主目錄" + } + } + } + }, + "Homebrew and /usr/local installs are already on the probe's PATH." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Homebrewと/usr/localへのインストールはすでにプローブのPATHに含まれています。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Homebrew 與 /usr/local 的安裝已在偵測用的 PATH 上。" + } + } + } + }, + "Homebrew, ~/.local/bin, and ~/.cargo/bin installs are already on the probe's PATH." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Homebrew、~/.local/bin、~/.cargo/binへのインストールはすでにプローブのPATHに含まれています。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Homebrew、~/.local/bin 與 ~/.cargo/bin 的安裝已在偵測用的 PATH 上。" + } + } + } + }, + "Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機" + } + } + } + }, + "Host Default" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストのデフォルト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機預設" + } + } + } + }, + "Host identity" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストの識別情報" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機識別" + } + } + } + }, + "Host key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト鍵" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機金鑰" + } + } + } + }, + "Host options for %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のホストオプション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 的主機選項" + } + } + } + }, + "Host Settings" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト設定" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機設定" + } + } + } + }, + "host:port or https://…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "host:portまたはhttps://…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "host:port 或 https://…" + } + } + } + }, + "HW keyboard" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハードウェアキーボード" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "硬體鍵盤" + } + } + } + }, + "Image, opens on its own screen" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画像、専用の画面で開きます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖片,會在自己的畫面開啟" + } + } + } + }, + "Image, pinch to zoom" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画像、ピンチで拡大縮小" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖片,可捏合縮放" + } + } + } + }, + "images" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画像" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖片" + } + } + } + }, + "In %@'s home" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のホーム内" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 %@ 的主目錄" + } + } + } + }, + "Initial prompt" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初のプロンプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "初始提示" + } + } + } + }, + "Initial prompt (optional)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初のプロンプト(任意)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "初始提示(選填)" + } + } + } + }, + "Install %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@をインストール" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "安裝 %@" + } + } + } + }, + "Install guide" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インストールガイド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "安裝指南" + } + } + } + }, + "INTERNET — OPENS FROM THIS DEVICE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インターネット — このデバイスから開きます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "網際網路 — 從這部裝置開啟" + } + } + } + }, + "iOS asks before Multiplex may read another app's copy. To stop the prompt: Settings → Multiplex → Paste from Other Apps → Allow." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "他のAppがコピーした内容をMultiplexが読み取る前に、iOSが確認します。確認を止めるには:設定 → Multiplex → 他のAppからペースト → 許可。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "iOS 會在 Multiplex 讀取其他 App 複製的內容前先詢問。若要停止詢問:設定 → Multiplex → 從其他 App 貼上 → 允許。" + } + } + } + }, + "It resolves against the pane's current directory." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインの現在のディレクトリを基準に解決されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "它會相對於窗格目前的目錄解析。" + } + } + } + }, + "Keep alive in background" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バックグラウンドでキープアライブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在背景保持連線" + } + } + } + }, + "Keep terminals alive through headset sleep, network roaming and IP changes." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ヘッドセットのスリープ、ネットワークの切り替え、IPの変更をまたいでもターミナルを維持します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在頭戴裝置休眠、網路漫遊與 IP 變動時,終端機都保持連線。" + } + } + } + }, + "Keep your work box, homelab and servers on one live fleet wall." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "仕事用マシン、ホームラボ、サーバーを1枚のライブなフリートウォールにまとめます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "把你的工作機、家用實驗室與伺服器放在同一面即時的機群牆上。" + } + } + } + }, + "Key Commands" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーコマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按鍵指令" + } + } + } + }, + "Key passphrase" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "鍵のパスフレーズ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "金鑰密語" + } + } + } + }, + "KEYBOARD" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーボード" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鍵盤" + } + } + } + }, + "Keychain locked" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーチェーンがロックされています" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鑰匙圈已鎖定" + } + } + } + }, + "Keys" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按鍵" + } + } + } + }, + "Kills “%@” on %@ and everything running in it, then closes the tab." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Kills “%1$@” on %2$@ and everything running in it, then closes the tab." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」(%@上)とその中で実行中のすべてを終了し、タブを閉じます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終止「%@」(位於 %@)及其中執行的所有內容,然後關閉標籤頁。" + } + } + } + }, + "Kills “%@” on %@ and everything running in it." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Kills “%1$@” on %2$@ and everything running in it." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@の「%1$@」と、その中で実行中のすべてを終了します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終止 %2$@ 上的「%1$@」以及其中執行的所有項目。" + } + } + } + }, + "LAN — DIALLED FROM THIS DEVICE, NETWORK PERMITTING" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LAN — このデバイスから接続、ネットワークが許す場合" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "區域網路 — 由本裝置撥接,視網路而定" + } + } + } + }, + "Language" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "言語" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "語言" + } + } + } + }, + "Language, opens system settings" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "言語、システム設定を開きます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "語言,開啟系統設定" + } + } + } + }, + "Larger text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "大きな文字" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "放大文字" + } + } + } + }, + "Larger Text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "大きな文字" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "放大文字" + } + } + } + }, + "Launch" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "起動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟動" + } + } + } + }, + "Launch %@ on %@?" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Launch %1$@ on %2$@?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@で%1$@を起動しますか?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要在 %2$@ 上啟動 %1$@ 嗎?" + } + } + } + }, + "Launches as %@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@として起動します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 %@ 啟動。" + } + } + } + }, + "Launches the agent inside this existing session instead of creating one. Leave empty for a fresh session." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規に作成せず、この既存のセッション内でエージェントを起動します。空欄にすると新しいセッションを使います。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在這個既有的工作階段中啟動 Agent,而不是新增一個。留空則使用全新的工作階段。" + } + } + } + }, + "Launches the agent with --model set to this value. Configure choices in Multiplex's Host Settings; leave empty for the agent's default model." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "--modelにこの値を設定してエージェントを起動します。選択肢はMultiplexのホスト設定で構成できます。空欄にするとエージェントのデフォルトモデルを使います。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟動 Agent 時將 --model 設為這個值。可在 Multiplex 的主機設定中設定選項;留空則使用 Agent 的預設模型。" + } + } + } + }, + "Leaves Multiplex" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexから離れます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "會離開 Multiplex" + } + } + } + }, + "Leaving the app holds this host's sessions and probing open for the extra time iOS grants — tens of seconds, not minutes. Agent alerts still reach you inside that window; after it the app suspends as usual and tabs reattach when you come back." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appを離れても、iOSが許可する追加の時間だけこのホストのセッションとプローブを維持します — 数分ではなく数十秒です。その間はエージェント通知も届きます。それ以降は通常どおりAppが中断され、戻るとタブが再アタッチします。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "離開 App 後,這台主機的工作階段與偵測會在 iOS 允許的額外時間內保持開啟 — 是數十秒,而不是數分鐘。在這段時間內你仍會收到 Agent 提醒;時間過後 App 會照常暫停,回來時標籤頁會重新連接。" + } + } + } + }, + "Letter or digit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "英字または数字" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "字母或數字" + } + } + } + }, + "License copied" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンスをコピーしました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已複製授權" + } + } + } + }, + "light" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "淺色" + } + } + } + }, + "Line Number" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "行番号" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "行號" + } + } + } + }, + "LINKS & PATHS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リンクとパス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連結與路徑" + } + } + } + }, + "live" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "即時" + } + } + } + }, + "Live preview" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライブプレビュー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "即時預覽" + } + } + } + }, + "Lives on %@, not this device" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスではなく%@にあります" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "位於 %@,而非本裝置" + } + } + } + }, + "Loading image" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画像を読み込み中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在載入圖片" + } + } + } + }, + "Loading the App Store price…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeの価格を読み込み中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在載入 App Store 價格…" + } + } + } + }, + "Local Network Access Is Off" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローカルネットワークアクセスがオフです" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "區域網路存取已關閉" + } + } + } + }, + "localhost in a remote pane is the host's own loopback — an address this device can't dial. VIA aims the viewport at the address that already reaches the host; the server must listen beyond loopback (vite --host) to answer." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートペインのlocalhostはホスト自身のループバックであり、このデバイスからは接続できないアドレスです。VIAはビューポートを、すでにホストに到達できるアドレスへ向けます。応答させるには、サーバがループバック以外でも待ち受ける必要があります(vite --host)。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "遠端窗格中的 localhost 是主機自己的迴路位址 — 這台裝置連不到。VIA 會把檢視區指向已經連得到主機的位址;伺服器也必須監聽迴路位址以外的位址(vite --host)才能回應。" + } + } + } + }, + "LOCK THE KEYBOARD" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーボードをロック" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鎖定鍵盤" + } + } + } + }, + "locked" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロック中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已鎖定" + } + } + } + }, + "Locks the app behind %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@でAppをロックします" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 %@ 鎖定 App" + } + } + } + }, + "login shell" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログインシェル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "登入 Shell" + } + } + } + }, + "LONG PRESS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長押し" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "長按" + } + } + } + }, + "Long press and drag to reorder within this host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長押ししてドラッグすると、このホスト内で並べ替えられます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "長按並拖移可在此主機內重新排序" + } + } + } + }, + "Machines running mpx bind on this network appear here on their own. Confirm each one with the 6-digit PIN its terminal printed." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このネットワークでmpx bindを実行しているマシンは自動的にここに表示されます。それぞれのターミナルに表示された6桁のPINで確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在這個網路上執行 mpx bind 的機器會自動出現在這裡。用它的終端機印出的 6 位數 PIN 逐一確認。" + } + } + } + }, + "Magenta" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マゼンタ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "洋紅" + } + } + } + }, + "Merge" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "結合" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "合併" + } + } + } + }, + "Merge All Windows" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべてのウインドウを結合" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "合併所有視窗" + } + } + } + }, + "Merge another window into this one" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "別のウインドウをこのウインドウに結合します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將另一個視窗合併到這個視窗" + } + } + } + }, + "Merge Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウインドウを結合" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "合併視窗" + } + } + } + }, + "Message" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息" + } + } + } + }, + "Message %@…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@にメッセージ…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳訊給 %@…" + } + } + } + }, + "MESSAGE BOX" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージボックス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息框" + } + } + } + }, + "Message history for %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のメッセージ履歴" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 的訊息記錄" + } + } + } + }, + "Message this pane…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このペインにメッセージ…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳訊給這個窗格…" + } + } + } + }, + "MESSAGE TOO SHORT TO FIND" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージが短すぎて検索できません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息太短,無法尋找" + } + } + } + }, + "Metal renderer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metalレンダラー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metal renderer" + } + } + } + }, + "Model" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モデル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "模型" + } + } + } + }, + "Model (optional)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モデル(オプション)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "模型(選填)" + } + } + } + }, + "model id per line" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1行に1つのモデルID" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每行一個模型 ID" + } + } + } + }, + "modified" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "変更済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已修改" + } + } + } + }, + "Monitoring" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "監控中" + } + } + } + }, + "More %@ commands" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他の%@コマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更多 %@ 指令" + } + } + } + }, + "MOSH LINK — SRTT FROM EVERY DATAGRAM · LOSS FROM SEQUENCE GAPS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moshリンク — SRTTはすべてのデータグラムから · 損失はシーケンスの欠落から" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mosh 連線 — SRTT 取自每個資料包 · 遺失率取自序號缺口" + } + } + } + }, + "MOSH SAMPLES EVERY DATAGRAM · SSH HAS NO KEEPALIVE — RTT IS THE DECK PROBE · ECHO MEASURED WHILE YOU TYPE · SESSION-ONLY, NOTHING LEAVES THIS DEVICE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moshはすべてのデータグラムをサンプリング · SSHにはキープアライブがありません — RTTはDeckのプローブによるもの · エコーは入力中に測定 · セッション内のみで、このデバイスから出ません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mosh 取樣每個資料包 · SSH 沒有保持連線 — RTT 來自 Deck 偵測 · 回應在你打字時測量 · 僅限本次工作階段,不會離開本裝置" + } + } + } + }, + "mosh session lost sync" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "moshセッションの同期が失われました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "mosh 工作階段失去同步" + } + } + } + }, + "Mosh transport" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moshトランスポート" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mosh 傳輸" + } + } + } + }, + "Mosh Transport" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moshトランスポート" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mosh 傳輸" + } + } + } + }, + "MOSH TRANSPORT" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moshトランスポート" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mosh 傳輸" + } + } + } + }, + "mosh-server didn't start on %@: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "mosh-server didn't start on %1$@: %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@でmosh-serverが起動しませんでした:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "mosh-server 未在 %@ 上啟動:%@" + } + } + } + }, + "mosh-server started on %@ but no UDP reply arrived. Check firewalls/NAT for the mosh port range (default 60000-61000)." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@でmosh-serverは起動しましたが、UDPの応答が届きませんでした。moshのポート範囲(デフォルト60000-61000)についてファイアウォール/NATを確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "mosh-server 已在 %@ 上啟動,但沒有收到 UDP 回應。請檢查防火牆/NAT 是否開放 mosh 的埠範圍(預設 60000-61000)。" + } + } + } + }, + "mosh-server wasn't found — mosh attaches will fail. Install it on the host or set its path below." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "mosh-serverが見つかりません — moshでのアタッチは失敗します。ホストにインストールするか、下でパスを指定してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "找不到 mosh-server — mosh 連線會失敗。請在主機上安裝,或在下方設定它的路徑。" + } + } + } + }, + "Move command down" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドを下に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將指令下移" + } + } + } + }, + "Move command up" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドを上に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將指令上移" + } + } + } + }, + "Move directory down" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディレクトリを下に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將目錄下移" + } + } + } + }, + "Move directory up" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディレクトリを上に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將目錄上移" + } + } + } + }, + "Move Down" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "下に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "下移" + } + } + } + }, + "Move script down" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリプトを下に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將指令碼下移" + } + } + } + }, + "Move script up" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリプトを上に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將指令碼上移" + } + } + } + }, + "Move to New Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規ウインドウへ移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移至新視窗" + } + } + } + }, + "Move Up" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上に移動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "上移" + } + } + } + }, + "Multiplex can’t reach SSH hosts on your local network. Turn on Local Network access in Settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexはローカルネットワーク上のSSHホストに接続できません。「設定」でローカルネットワークのアクセスをオンにしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 無法連到你區域網路上的 SSH 主機。請在「設定」中開啟區域網路存取權。" + } + } + } + }, + "Multiplex follows the language you pick for it in Settings. English, Traditional Chinese and Japanese are available; the terminal chrome stays English." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexは「設定」で選んだ言語に従います。英語、繁体字中国語、日本語が利用できます。ターミナルの表示は英語のままです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 會使用你在「設定」中為它選擇的語言。提供英文、繁體中文與日文;終端機介面維持英文。" + } + } + } + }, + "Multiplex Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexホスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 主機" + } + } + } + }, + "Multiplex is locked. Unlock with %@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexはロックされています。%@でロックを解除してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 已鎖定。使用 %@ 解鎖。" + } + } + } + }, + "Multiplex opens web and mail links only. A %@: link from a remote pane would act on this device, so it is shown rather than followed — copy it if you want it elsewhere." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexはウェブとメールのリンクのみを開きます。リモートのペインからの%@: リンクはこのデバイス上で動作してしまうため、開かずに表示します — 別の場所で使う場合はコピーしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 只會開啟網頁與郵件連結。來自遠端窗格的 %@: 連結會作用在本裝置上,因此只顯示而不開啟 — 若要在別處使用,請複製它。" + } + } + } + }, + "Multiplex Pro is not available for purchase right now." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proは現在購入できません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Pro 目前無法購買。" + } + } + } + }, + "Multiplex Pro is not available from the App Store right now." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proは現在App Storeから入手できません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Pro 目前無法從 App Store 取得。" + } + } + } + }, + "Multiplex Pro is not available in this App Store storefront." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このApp StoreストアフロントではMultiplex Proを提供していません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個 App Store 商店未提供 Multiplex Pro。" + } + } + } + }, + "Multiplex Pro is unlocked" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proのロックが解除されています" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Pro 已解鎖" + } + } + } + }, + "Multiplex Pro is unlocked. This purchase is available on your devices with the same Apple ID." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proのロックが解除されています。この購入項目は、同じApple IDのデバイスでご利用いただけます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Pro 已解鎖。這項購買項目可在使用相同 Apple ID 的裝置上使用。" + } + } + } + }, + "Multiplex Pro was restored." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proを復元しました。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已回復 Multiplex Pro。" + } + } + } + }, + "Multiplex, %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex、%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex,%@" + } + } + } + }, + "Name" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名前" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "名稱" + } + } + } + }, + "needs you" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "対応が必要" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要你" + } + } + } + }, + "New Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增工作階段" + } + } + } + }, + "New Session can type a chosen script into the fresh shell before anything launches. Add one to make it available." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「新規セッション」では、何かが起動する前に、選んだスクリプトを新しいシェルに入力できます。追加すると使えるようになります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "「新增工作階段」可以在任何東西啟動前,把選定的指令碼輸入到全新的 Shell 中。加入一個即可使用。" + } + } + } + }, + "New Session Default" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規セッションのデフォルト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增工作階段預設值" + } + } + } + }, + "New session defaults" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規セッションのデフォルト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新工作階段預設值" + } + } + } + }, + "New Session offers these by name; the chosen one is typed into the fresh shell before the launch command." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「新規セッション」ではこれらを名前で選べます。選んだものは起動コマンドの前に新しいシェルに入力されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "「新增工作階段」會依名稱提供這些項目;選定的那一個會在啟動指令之前輸入到全新的 Shell 中。" + } + } + } + }, + "New session on %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@で新規セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 %@ 上新增工作階段" + } + } + } + }, + "New session tmux conf" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規セッションのtmux設定" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新工作階段 tmux 設定" + } + } + } + }, + "New sessions run on" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規セッションの実行先" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新工作階段執行於" + } + } + } + }, + "New sessions start in the host's home directory. Add paths to make them available in New Session." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規セッションはホストのホームディレクトリで開始します。パスを追加すると、新規セッションで選べるようになります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新工作階段會從主機的家目錄開始。加入路徑後,就能在「新增工作階段」中選用。" + } + } + } + }, + "New Tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規タブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增標籤頁" + } + } + } + }, + "New Tab (Focused Workspace)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規タブ(フォーカス中のワークスペース)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增標籤頁(聚焦的工作區)" + } + } + } + }, + "New Tab in Workspace" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペースに新規タブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在工作區中新增標籤頁" + } + } + } + }, + "New tab: another session or the file viewer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規タブ:別のセッションまたはファイルビューア" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新標籤頁:另一個工作階段或檔案檢視器" + } + } + } + }, + "New tab: another session, a tab in this herdr workspace, or the file viewer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規タブ:別のセッション、このherdrワークスペース内のタブ、またはファイルビューア" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新標籤頁:另一個工作階段、此 herdr 工作區中的標籤頁,或檔案檢視器" + } + } + } + }, + "New theme" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規テーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增主題" + } + } + } + }, + "New themes begin with the active palette. Use the row menu to edit, duplicate, or delete one." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規テーマは現在のパレットから始まります。行のメニューで編集、複製、削除ができます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新主題以目前使用的調色盤為起點。使用列表選單可編輯、複製或刪除主題。" + } + } + } + }, + "New Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規ウインドウ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增視窗" + } + } + } + }, + "New Workspace" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規ワークスペース" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增工作區" + } + } + } + }, + "Next Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "次のウインドウ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一個視窗" + } + } + } + }, + "No %@ on host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストに%@がありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機上沒有 %@" + } + } + } + }, + "No answer from %@ after %lld seconds — check the address and port, and that the host is reachable from this network." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No answer from %1$@ after %2$lld seconds — check the address and port, and that the host is reachable from this network." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@から%lld秒待っても応答がありません — アドレスとポート、そしてこのネットワークからホストに到達できるかを確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 在 %lld 秒後仍未回應 — 請檢查位址與埠號,以及此網路是否連得到該主機。" + } + } + } + }, + "No answer from mpx bind — is it still running?" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "mpx bindから応答がありません — 実行中であることを確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "mpx bind 沒有回應 — 請確保它正在執行。" + } + } + } + }, + "NO COMMANDS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドなし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有指令" + } + } + } + }, + "NO COMMANDS · CUSTOM SETUP" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コマンドなし · カスタムセットアップ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有指令 · 自訂設定" + } + } + } + }, + "No configured host matches this shortcut. Pick a host in the widget or shortcut settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このショートカットに一致するホストが設定されていません。ウィジェットまたはショートカットの設定でホストを選んでください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有已設定的主機符合此捷徑。請在小工具或捷徑設定中選擇主機。" + } + } + } + }, + "NO CUSTOM COMMANDS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムコマンドなし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有自訂指令" + } + } + } + }, + "No custom themes" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタムテーマがありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有自訂主題" + } + } + } + }, + "No hosts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストがありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有主機" + } + } + } + }, + "No hosts yet." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "まだホストがありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "還沒有主機。" + } + } + } + }, + "No machine has answered yet." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "まだどのマシンからも応答がありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "還沒有機器回應。" + } + } + } + }, + "NO MESSAGES YET" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "まだメッセージはありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚無訊息" + } + } + } + }, + "No Multiplex Pro purchase was found for this Apple ID." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このApple IDではMultiplex Proの購入が見つかりませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "找不到此 Apple ID 的 Multiplex Pro 購買項目。" + } + } + } + }, + "No Multiplex Pro purchase was found for this Apple ID. TestFlight builds use Apple's test store, so a purchase made in the App Store version doesn't appear here. Pro can be unlocked again inside TestFlight free of charge." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このApple IDではMultiplex Proの購入が見つかりませんでした。TestFlightビルドはAppleのテスト用ストアを使うため、App Store版での購入はここには表示されません。TestFlight内ではProを無料で再度アンロックできます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "找不到此 Apple ID 的 Multiplex Pro 購買項目。TestFlight 版本使用 Apple 的測試商店,因此在 App Store 版本中的購買不會出現在這裡。你可以在 TestFlight 中免費再次解鎖 Pro。" + } + } + } + }, + "no output" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力なし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有輸出" + } + } + } + }, + "No route to %@ — check this device's network connection." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@への経路がありません — このデバイスのネットワーク接続を確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連到 %@ — 請檢查這台裝置的網路連線。" + } + } + } + }, + "No saved credentials for %@. Edit the host and sign in again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@の保存された認証情報がありません。ホストを編集して再度サインインしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有 %@ 的已儲存憑證。請編輯主機並重新登入。" + } + } + } + }, + "NO SESSION FILE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションファイルなし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有工作階段檔案" + } + } + } + }, + "No Signal" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "信号なし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無訊號" + } + } + } + }, + "No terminal selected" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルが選択されていません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未選取終端機" + } + } + } + }, + "No textual hunks." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキストの差分ハンクがありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有文字差異區塊。" + } + } + } + }, + "No tmux on host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストにtmuxがありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機上沒有 tmux" + } + } + } + }, + "NO WORKING DIRECTORY" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作業ディレクトリなし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有工作目錄" + } + } + } + }, + "None" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "なし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無" + } + } + } + }, + "Not a host key. Paste a SHA256: fingerprint, or a public key line from the machine's /etc/ssh." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト鍵ではありません。SHA256: フィンガープリント、またはマシンの/etc/sshにある公開鍵の行を貼り付けてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這不是主機金鑰。請貼上 SHA256: 指紋,或機器 /etc/ssh 中的公鑰行。" + } + } + } + }, + "NOT A PATH MULTIPLEX CAN READ" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexが読み取れるパスではありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 無法讀取的路徑" + } + } + } + }, + "Not a usable address" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用できないアドレスです" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不是可用的位址" + } + } + } + }, + "Not a usable path" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用できないパスです" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不是可用的路徑" + } + } + } + }, + "NOT AN ADDRESS MULTIPLEX CAN READ" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexが読み取れるアドレスではありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 無法讀取的位址" + } + } + } + }, + "NOT AN IMAGE THIS VIEWER CAN DRAW" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このビューアが描画できない画像です" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個檢視器無法繪製的圖片" + } + } + } + }, + "not attached" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未アタッチ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未連接" + } + } + } + }, + "Not connected" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未接続" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未連線" + } + } + } + }, + "Not connected to %@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に接続していません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未連線到 %@。" + } + } + } + }, + "NOT IN THE VISIBLE TRANSCRIPT" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示中のトランスクリプトにありません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不在可見的對話記錄中" + } + } + } + }, + "Not Now" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今はしない" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在不要" + } + } + } + }, + "Not selected" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未選取" + } + } + } + }, + "Not text — Multiplex won't render it as code." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキストではありません — Multiplexはコードとして表示しません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不是文字 — Multiplex 不會以程式碼呈現。" + } + } + } + }, + "Nothing extra runs. A setup script is typed into the fresh shell before the launch." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追加で実行されるものはありません。セットアップスクリプトは起動前に新しいシェルへ入力されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不會執行額外的內容。設定指令碼會在啟動前輸入到全新的 Shell 中。" + } + } + } + }, + "Nothing opens by itself. A press raises a sheet showing where it really points — " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動で開くものはありません。押すと実際の指し先を示すシートが表示されます — " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不會自行開啟任何東西。按下後會出現面板,顯示它實際指向的位置 — " + } + } + } + }, + "Off" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オフ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉" + } + } + } + }, + "Off keeps the host in the fleet without connecting to it" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オフにすると、ホストは接続せずにフリートに残ります" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉後主機仍留在機群中,但不會連線" + } + } + } + }, + "Off parks the host on the deck without dialling it: no probing, no tiles, and widgets or Shortcuts report it as disabled. Terminal windows already open keep running, and Signal check below still connects on demand." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オフにすると、ホストは接続せずにDeckに置かれます:プローブもタイルもなく、ウィジェットやショートカットでは無効と表示されます。すでに開いているターミナルウインドウは動き続け、下のSignalチェックは必要に応じて接続します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉會讓主機停在 Deck 上而不撥號:不探測、沒有方塊,小工具或捷徑會回報為已停用。已開啟的終端機視窗會繼續執行,下方的 Signal 檢查仍會在需要時連線。" + } + } + } + }, + "OK" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "好" + } + } + } + }, + "On" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟" + } + } + } + }, + "on %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@上" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 %@ 上" + } + } + } + }, + "On a Mac host, Claude Code keeps its credentials in the login keychain, and an SSH or tmux session never unlocks it — no GUI login happened — so Claude Code starts as if you were signed out even though your login is intact. Unlock the keychain once inside the tmux session, then restart Claude Code:" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Macホストでは、Claude Codeは認証情報をログインキーチェーンに保存しますが、SSHやtmuxセッションではロック解除されません — GUIログインが行われないためです — そのため、ログインが有効なままでもClaude Codeはサインアウト状態で起動します。tmuxセッション内で一度キーチェーンをロック解除してから、Claude Codeを再起動してください:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Mac 主機上,Claude Code 會把憑證存在登入鑰匙圈,而 SSH 或 tmux 工作階段永遠不會解鎖它 — 因為沒有發生 GUI 登入 — 所以即使你的登入仍然有效,Claude Code 啟動時仍像是已登出。請在 tmux 工作階段中解鎖鑰匙圈一次,然後重新啟動 Claude Code:" + } + } + } + }, + "On the machine" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マシン側" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在機器上" + } + } + } + }, + "On this device's network" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスのネットワーク上" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在這台裝置的網路上" + } + } + } + }, + "One key on the rail opens the whole shortcut panel: copy mode, splits, windows, workspaces, and the confirmed closes." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーバーの1つのキーでショートカットパネル全体を開けます:コピーモード、分割、ウインドウ、ワークスペース、確認付きのクローズ。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按鍵列上的一個按鍵就能開啟整個捷徑面板:複製模式、分割、視窗、工作區,以及需要確認的關閉動作。" + } + } + } + }, + "One line, typed as-is" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1行、そのまま入力" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "一行,原樣輸入" + } + } + } + }, + "One model id per line, in picker order. New Session, the Open Agent shortcut, and the host widget offer them as choices, passed as --model; nothing is applied unless chosen at launch." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1行に1つのモデルIDを、ピッカーの順序で記述します。新規セッション、「エージェントを開く」ショートカット、ホストウィジェットがこれらを選択肢として提示し、--modelとして渡します。起動時に選ばない限り何も適用されません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每行一個模型 ID,依選單順序排列。「新增工作階段」、「開啟 Agent」捷徑與主機小工具會提供這些選項,並以 --model 傳入;除非在啟動時選擇,否則不會套用任何項目。" + } + } + } + }, + "One option per line, like a .tmux.conf (mouse on, history-limit 50000). Each line is applied when Multiplex creates a session with tmux set-option -t that session. Attaching never applies anything, and session-scoped options do not change sessions made on the host. Hosts start with mouse on and focus-events on; clear the field to apply nothing. Focus events and other server-scoped options still reach the whole tmux server." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1行に1つのオプションを、.tmux.confのように記述します(mouse on、history-limit 50000)。各行はMultiplexがセッションを作成するときに、tmux set-option -tでそのセッションに適用されます。アタッチ時には何も適用されず、セッション単位のオプションはホスト側で作成されたセッションを変更しません。ホストの初期値はmouse onとfocus-events onです。フィールドを空にすると何も適用されません。focus eventsなどサーバー単位のオプションはtmuxサーバー全体に影響します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每行一個選項,就像 .tmux.conf(mouse on、history-limit 50000)。每一行都會在 Multiplex 建立工作階段時,以 tmux set-option -t 套用到該工作階段。連接時永遠不會套用任何內容,工作階段範圍的選項也不會改變在主機上建立的工作階段。主機預設帶有 mouse on 與 focus-events on;清空欄位則不套用任何內容。focus events 與其他伺服器範圍的選項仍會影響整個 tmux 伺服器。" + } + } + } + }, + "Open" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟" + } + } + } + }, + "Open %@?" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を開きますか?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要開啟 %@ 嗎?" + } + } + } + }, + "Open ${path} on ${host}" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "${host} で ${path} を開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 ${host} 上開啟 ${path}" + } + } + } + }, + "Open a shell on ${host}" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "${host} でシェルを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 ${host} 上開啟 Shell" + } + } + } + }, + "Open Agent" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟 Agent" + } + } + } + }, + "Open File" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟檔案" + } + } + } + }, + "Open file on %@?" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@でファイルを開きますか?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要在 %@ 上開啟檔案嗎?" + } + } + } + }, + "Open In" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開く場所" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟位置" + } + } + } + }, + "Open in New Tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規タブで開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在新標籤頁中開啟" + } + } + } + }, + "Open in the system browser" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "システムのブラウザで開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在系統瀏覽器中開啟" + } + } + } + }, + "Open link" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リンクを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟連結" + } + } + } + }, + "Open Settings" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定を開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟設定" + } + } + } + }, + "Open Shell" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シェルを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟 Shell" + } + } + } + }, + "Open shell on %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@でシェルを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 %@ 上開啟 Shell" + } + } + } + }, + "Open source licenses" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オープンソースライセンス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開放原始碼授權" + } + } + } + }, + "Open Source Licenses" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オープンソースライセンス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開放原始碼授權" + } + } + } + }, + "Open the fleet board behind every rail chip: round-trips, typing echo, mosh loss and roams, reconnects, and data volume per host." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "各キーバーのチップの背後にあるフリートボードを開きます:往復、入力エコー、moshのロスとローミング、再接続、ホストごとのデータ量。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟每個按鍵列標籤背後的機群面板:往返、輸入回應、mosh 遺失與漫遊、重新連線,以及每台主機的資料量。" + } + } + } + }, + "Open the message box" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージボックスを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟訊息框" + } + } + } + }, + "Open the remote pane menu" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートペインのメニューを開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟遠端窗格選單" + } + } + } + }, + "Open the working tree's diff" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作業ツリーの差分を開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟工作樹的差異" + } + } + } + }, + "Open this file on its own screen" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このファイルを独立した画面で開く" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在獨立畫面中開啟這個檔案" + } + } + } + }, + "Opens a remote file read-only in Multiplex, optionally at a line number." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートファイルをMultiplexで読み取り専用として開きます。行番号の指定も可能です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Multiplex 中以唯讀方式開啟遠端檔案,可指定行號。" + } + } + } + }, + "Opens connection stats" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続統計を開きます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟連線統計" + } + } + } + }, + "Opens the host's most recent tmux session in Multiplex, creating one if none exists." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストの最新のtmuxセッションをMultiplexで開きます。存在しない場合は新規作成します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Multiplex 中開啟主機最近的 tmux 工作階段,若沒有則建立一個。" + } + } + } + }, + "Opens the new tab's shell in “%@”. %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Opens the new tab's shell in “%1$@”. %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規タブのシェルを「%@」で開きます。%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在「%@」中開啟新標籤頁的 Shell。%@" + } + } + } + }, + "Opens the SSH key passphrase prompt" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH鍵のパスフレーズ入力を開きます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟 SSH 金鑰密語提示" + } + } + } + }, + "Optional" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オプション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選填" + } + } + } + }, + "Optional initial prompt" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "初期プロンプト(省略可)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選填的初始提示" + } + } + } + }, + "Optional model" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モデル(省略可)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選填的模型" + } + } + } + }, + "Optional model for %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のモデル(省略可)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 的選填模型" + } + } + } + }, + "Optional, for every machine bound from this pane: its SSH key is generated sealed with this passphrase, which is then saved in the host's settings so connecting keeps working. Clear it in Host Settings to be asked when connecting instead. Empty means the key is stored unlocked, exactly as before." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "省略可。このペインからバインドするすべてのマシンに適用されます:SSH鍵はこのパスフレーズで保護して生成され、パスフレーズはホスト設定に保存されるため接続はそのまま使えます。ホスト設定で消すと、接続時に入力を求められます。空欄の場合、鍵は従来どおりロックなしで保存されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選填,適用於從此面板綁定的每台機器:它的 SSH 金鑰會以此密語封裝產生,密語隨後儲存在主機設定中,讓連線持續可用。在主機設定中清除它,改為在連線時詢問。留空表示金鑰以未鎖定的方式儲存,與以往完全相同。" + } + } + } + }, + "Options" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オプション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選項" + } + } + } + }, + "opus, sonnet, or a full model id" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "opus、sonnet、または完全なモデルID" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "opus、sonnet,或完整的模型 ID" + } + } + } + }, + "or change this host’s backend in Host Settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "または、ホスト設定でこのホストのバックエンドを変更してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "或在主機設定中更改這台主機的後端。" + } + } + } + }, + "Or, macOS or Linux" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "または、macOSかLinux" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "或者,macOS 或 Linux" + } + } + } + }, + "Over SSH that uses OSC 52, so the code lands on your local terminal's clipboard and Universal Clipboard carries it here." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OSC 52を使うSSH経由なので、コードはローカルターミナルのクリップボードに入り、ユニバーサルクリップボードでここに届きます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "透過使用 OSC 52 的 SSH,綁定碼會進到你本機終端機的剪貼板,再由「通用剪貼板」帶到這裡。" + } + } + } + }, + "PAN TO SCROLL" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグしてスクロール" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "拖曳以捲動" + } + } + } + }, + "PANE MENU" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインメニュー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "窗格選單" + } + } + } + }, + "Panes" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペイン" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "窗格" + } + } + } + }, + "Passphrase" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスフレーズ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "密語" + } + } + } + }, + "Passphrase Required" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスフレーズが必要です" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要密語" + } + } + } + }, + "Password" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスワード" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "密碼" + } + } + } + }, + "Password-protected. Unlocking opens it on this screen only — the password is not kept." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスワードで保護されています。ロックを解除してもこの画面で開くだけです — パスワードは保存されません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "受密碼保護。解鎖只會在此畫面開啟 — 密碼不會被保留。" + } + } + } + }, + "Paste" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペースト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "貼上" + } + } + } + }, + "Paste a private key first." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "先に秘密鍵をペーストしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "請先貼上私鑰。" + } + } + } + }, + "Paste bind code" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バインドコードをペースト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "貼上綁定碼" + } + } + } + }, + "Paste needs the code on this device's clipboard, and mpx never takes a clipboard unless you ask it to. Run it with --copy instead:" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペーストにはこのデバイスのクリップボードにコードが必要ですが、mpxは指示しない限りクリップボードを取得しません。代わりに --copyを付けて実行してください:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "貼上需要綁定碼位於這台裝置的剪貼板上,而 mpx 除非你要求,否則不會動剪貼板。請改用 --copy 執行:" + } + } + } + }, + "Pause" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "一時停止" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "暫停" + } + } + } + }, + "Payment is charged to your Apple ID. No subscription." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お支払いはApple IDに請求されます。サブスクリプションではありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "款項會向你的 Apple ID 收取。非訂閱制。" + } + } + } + }, + "PDFs" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + } + } + }, + "Photo Library…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "写真ライブラリ…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "照片圖庫…" + } + } + } + }, + "Pi" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pi" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pi" + } + } + } + }, + "Pick a file from the tree%@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ツリーからファイルを選んでください%@。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從樹狀結構中挑選檔案%@。" + } + } + } + }, + "PIN from %@’s terminal" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のターミナルに表示されたPIN" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "來自 %@ 終端機的 PIN" + } + } + } + }, + "Place each built-in in Bar or More. Custom content may span many lines; turn Submit off to leave it ready to edit." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "各ビルトインをBarまたはMoreに配置します。カスタム内容は複数行にできます。Submitをオフにすると、編集できる状態のまま残ります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "把每個內建項目放進 Bar 或 More。自訂內容可以跨多行;關掉 Submit 就會停在可編輯的狀態。" + } + } + } + }, + "Play" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再生" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "播放" + } + } + } + }, + "Playback position" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再生位置" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "播放位置" + } + } + } + }, + "Pointer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポインタ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指標" + } + } + } + }, + "Port" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポート" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連接埠" + } + } + } + }, + "Posts a banner when Claude Code, Codex, or Grok Build finishes a turn, asks a question, or wants permission in a session you are not typing in. Multiplex must remain open. Requires Pro." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "入力していないセッションでClaude Code、Codex、Grok Buildがターンを終えた、質問した、許可を求めたときにバナーを表示します。Multiplexを開いたままにする必要があります。Proが必要です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "當 Claude Code、Codex 或 Grok Build 在你沒有在輸入的工作階段中完成回合、提出問題或請求權限時顯示橫幅。Multiplex 必須保持開啟。需要 Pro。" + } + } + } + }, + "PRESS A LINK" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リンクを押す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按下連結" + } + } + } + }, + "PRESS A PATH" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスを押す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按下路徑" + } + } + } + }, + "press again to close" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "もう一度押すと閉じます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "再按一次以關閉" + } + } + } + }, + "Preview of the %@ terminal theme" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ターミナルテーマのプレビュー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 終端機主題的預覽" + } + } + } + }, + "Previous Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "前のウインドウ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一個視窗" + } + } + } + }, + "Privacy policy" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライバシーポリシー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱私權政策" + } + } + } + }, + "Private key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "秘密鍵" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "私鑰" + } + } + } + }, + "Pro adds unlimited hosts, mosh, unlimited agent-helper commands, alerts, connection stats, custom themes, and a %lld-command Key Commands set (free keeps %lld). SSH terminals, agent detection, and the wall's live state stay free." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Pro adds unlimited hosts, mosh, unlimited agent-helper commands, alerts, connection stats, custom themes, and a %1$lld-command Key Commands set (free keeps %2$lld). SSH terminals, agent detection, and the wall's live state stay free." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proでは、ホスト数無制限、mosh、エージェントヘルパーコマンド無制限、通知、接続統計、カスタムテーマ、そして%lld個のキーコマンドセットが使えます(無料版は%lld個)。SSHターミナル、エージェント検出、ウォールのライブ状態は無料のままです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pro 加上無限主機、mosh、無限 Agent 輔助指令、提醒、連線統計、自訂主題,以及 %lld 組按鍵指令(免費版為 %lld 組)。SSH 終端機、Agent 偵測與監控牆的即時狀態維持免費。" + } + } + } + }, + "Pro details" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proの詳細" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pro 詳細資訊" + } + } + } + }, + "Pro unlocked" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proのロックを解除しました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已解鎖 Pro" + } + } + } + }, + "Prompt" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロンプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "提示" + } + } + } + }, + "provider/model-id per line" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1行に1つのprovider/model-id" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每行一個 provider/model-id" + } + } + } + }, + "Proving the PIN…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PINを検証中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在驗證 PIN…" + } + } + } + }, + "Purchase complete." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "購入が完了しました。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "購買完成。" + } + } + } + }, + "Purchase pending approval. Pro unlocks automatically if approved. If it was declined, tap Restore Purchases to check again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "購入は承認待ちです。承認されるとProは自動的にロック解除されます。拒否された場合は「購入を復元」をタップしてもう一度確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "購買正在等待批准。若獲批准,Pro 會自動解鎖。若遭拒絕,請點一下「回復購買項目」再檢查一次。" + } + } + } + }, + "Purchases are not allowed for this Apple ID on this device." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスではこのApple IDでの購入が許可されていません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這台裝置上的這個 Apple ID 不允許購買。" + } + } + } + }, + "Purchases the non-consumable Multiplex Pro unlock" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "非消耗型のMultiplex Pro解除を購入します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "購買非消耗性的 Multiplex Pro 解鎖項目" + } + } + } + }, + "Reach: %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続経路:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連線途徑:%@" + } + } + } + }, + "Reading session file" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションファイルを読み込み中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在讀取工作階段檔案" + } + } + } + }, + "READING SESSION FILE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションファイルを読み込み中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在讀取工作階段檔案" + } + } + } + }, + "Reattaching to %@…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に再アタッチ中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在重新連接 %@…" + } + } + } + }, + "Reconnect" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再接続" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新連線" + } + } + } + }, + "Reconnecting to %@…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に再接続中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在重新連線至 %@…" + } + } + } + }, + "Red" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "赤" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "紅" + } + } + } + }, + "REFRESH dials again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "REFRESHでもう一度接続します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "REFRESH 會再撥接一次。" + } + } + } + }, + "Refresh file viewer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルビューアを再読み込み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新整理檔案檢視器" + } + } + } + }, + "Relative to the pane's directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインのディレクトリからの相対パス" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "相對於窗格的目錄" + } + } + } + }, + "Reload" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再読み込み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新載入" + } + } + } + }, + "Remember launch choice" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "起動の選択を記憶" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "記住啟動選擇" + } + } + } + }, + "REMEMBER saves only the launch choice." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "REMEMBERは起動の選択のみを保存します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "REMEMBER 只會儲存啟動選擇。" + } + } + } + }, + "REMEMBER saves the launch and setup-script choices." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "REMEMBERは起動とセットアップスクリプトの選択を保存します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "REMEMBER 會儲存啟動與設定指令碼的選擇。" + } + } + } + }, + "REMOTE LOOPBACK → REWRITES TO %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートループバック → %@に書き換え" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "遠端回送 → 改寫為 %@" + } + } + } + }, + "Remove" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除" + } + } + } + }, + "Remove attachment" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "添付を削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除附件" + } + } + } + }, + "Remove Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストを削除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除主機" + } + } + } + }, + "Remove Host…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストを削除…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除主機…" + } + } + } + }, + "Removes “%@” and its saved secret from this device and your synced devices. tmux sessions on the host keep running." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」と保存されたシークレットを、このデバイスと同期中のデバイスから削除します。ホスト上のtmuxセッションは動作し続けます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從這台裝置與你同步的裝置移除「%@」及其儲存的金鑰。主機上的 tmux 工作階段會繼續執行。" + } + } + } + }, + "Rename" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名称変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新命名" + } + } + } + }, + "Rename only — contents unchanged." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名称変更のみ — 内容は変わりません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "僅重新命名 — 內容不變。" + } + } + } + }, + "Rename Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウインドウの名称変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新命名視窗" + } + } + } + }, + "Rename Workspace" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペースの名称変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新命名工作區" + } + } + } + }, + "renamed" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名称変更済み" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已重新命名" + } + } + } + }, + "Repeat count" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リピート回数" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重複次數" + } + } + } + }, + "Repeat guard: ×%lld at most, %lld–%lld ms between sends, whole burst within %lld s. Values clamp." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Repeat guard: ×%1$lld at most, %2$lld–%3$lld ms between sends, whole burst within %4$lld s. Values clamp." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リピートガード:最大 ×%lld、送信間隔%lld–%lld ms、バースト全体は%lld秒以内。値は自動的に制限されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重複保護:最多 ×%lld、每次傳送間隔 %lld–%lld 毫秒、整批在 %lld 秒內完成。數值會自動限制。" + } + } + } + }, + "Require %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を要求" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要 %@" + } + } + } + }, + "Require %@ when Multiplex opens or returns from the background. The deck and every terminal stay covered until you authenticate; connections and the wall keep running. This device only." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexを開いたとき、またはバックグラウンドから復帰したときに%@を要求します。認証するまでDeckとすべてのターミナルは覆われたままですが、接続とウォールは動作し続けます。このデバイスのみ。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Multiplex 開啟或從背景返回時需要 %@。在你完成驗證前,Deck 與每個終端機都會保持遮蔽;連線與監控牆持續運作。僅限這台裝置。" + } + } + } + }, + "Required" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "必須" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "必填" + } + } + } + }, + "Requires Multiplex Pro" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proが必要です" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要 Multiplex Pro" + } + } + } + }, + "Reset %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@をリセット" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重設 %@" + } + } + } + }, + "Resize Down" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "下にサイズ変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向下調整大小" + } + } + } + }, + "Resize Left" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "左にサイズ変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向左調整大小" + } + } + } + }, + "Resize Pane" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインサイズを変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "調整窗格大小" + } + } + } + }, + "RESIZE PANES" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインサイズを変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "調整窗格大小" + } + } + } + }, + "Resize Right" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "右にサイズ変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向右調整大小" + } + } + } + }, + "Resize Up" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上にサイズ変更" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "向上調整大小" + } + } + } + }, + "RESIZED MID-JUMP — TRY AGAIN" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ジャンプ中にサイズが変わりました — もう一度お試しください" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "跳至途中尺寸改變 — 請再試一次" + } + } + } + }, + "Restore Purchases" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "購入を復元" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "回復購買項目" + } + } + } + }, + "Restoring Purchases…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "購入を復元中…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在回復購買項目…" + } + } + } + }, + "Retries the upload" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アップロードを再試行します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重試上傳" + } + } + } + }, + "Retry" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再試行" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重試" + } + } + } + }, + "Return" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Return" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Return" + } + } + } + }, + "RIGHT CLICK" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "右クリック" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "右鍵點按" + } + } + } + }, + "roughly doubles what this host fetches on every deck refresh" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckを更新するたびに、このホストが取得するデータ量がおよそ倍になります" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每次 Deck 重新整理時,這台主機抓取的資料量大約會加倍" + } + } + } + }, + "Round-trips, echo latency, loss, and volume — numbers the transports already compute, measured passively and kept in memory for this session only. Nothing is stored or synced. Off hides the rail chips and the stats board and stops measuring. The live chip is free; the board requires Pro." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "往復、エコーレイテンシ、損失、転送量 — トランスポートがすでに計算している数値を受動的に測定し、このセッションの間だけメモリに保持します。保存も同期もしません。オフにするとバーのチップと統計ボードを非表示にし、測定を停止します。ライブチップは無料で、ボードにはProが必要です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "往返、回應延遲、遺失率與流量 — 這些數字傳輸層本來就會算,只被動測量,而且僅在記憶體中保留這個工作階段。不會儲存也不會同步。關閉後會隱藏按鍵列上的統計顯示與統計面板,並停止測量。即時顯示是免費的;統計面板需要 Pro。" + } + } + } + }, + "Run" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "実行" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "執行" + } + } + } + }, + "Run the mpx CLI on the machine you're adding and it offers itself — no address, user, or key to type here. Can't install it? Switch to MANUAL." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追加したいマシンでmpx CLIを実行すると、そのマシンが自ら名乗り出ます — ここでアドレスやユーザ、鍵を入力する必要はありません。インストールできない場合はMANUALに切り替えてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在你要新增的機器上執行 mpx CLI,它就會主動報到 — 這裡不用輸入位址、使用者或金鑰。無法安裝?改用 MANUAL。" + } + } + } + }, + "running" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "実行中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "執行中" + } + } + } + }, + "Runs before the agent. Leave empty for the setup script remembered in Multiplex." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントの前に実行します。空欄にするとMultiplexに記憶されたセットアップスクリプトを使います。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Agent 之前執行。留空則使用 Multiplex 記住的設定指令碼。" + } + } + } + }, + "Runs first" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初に実行" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "先執行" + } + } + } + }, + "Runs on" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "実行環境" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "執行於" + } + } + } + }, + "Same press, its own sheet: " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "同じ押し方で、専用のシートが開きます:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "同樣的按法,有專屬的面板:" + } + } + } + }, + "Save" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "儲存" + } + } + } + }, + "Save & Connect" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存して接続" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "儲存並連線" + } + } + } + }, + "Save theme" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テーマを保存" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "儲存主題" + } + } + } + }, + "Script" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指令碼" + } + } + } + }, + "Script commands" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリプトのコマンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指令碼命令" + } + } + } + }, + "Script name" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリプト名" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "指令碼名稱" + } + } + } + }, + "Scroll the terminal back to this message" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルをこのメッセージまでスクロールして戻す" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將終端機捲回這則訊息" + } + } + } + }, + "SEARCH FAILED" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検索に失敗しました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜尋失敗" + } + } + } + }, + "Searching transcript" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トランスクリプトを検索中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在搜尋對話記錄" + } + } + } + }, + "Select all in pane" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペイン内をすべて選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選取窗格內全部" + } + } + } + }, + "Select source text to copy" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピーするソーステキストを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選取原始碼文字以複製" + } + } + } + }, + "Select text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキストを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選取文字" + } + } + } + }, + "Selected" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已選取" + } + } + } + }, + "Selections apply to every terminal immediately. %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択はすべてのターミナルに即座に適用されます。%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇會立即套用到所有終端機。%@" + } + } + } + }, + "Send" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳送" + } + } + } + }, + "Send a file to this session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このセッションにファイルを送信" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳送檔案到這個工作階段" + } + } + } + }, + "Send File…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルを送信…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳送檔案…" + } + } + } + }, + "Send, waiting for uploads" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信(アップロード待ち)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳送,等待上傳完成" + } + } + } + }, + "Sent as %@'s launch argument — it starts working on it immediately. Leave empty to open %@ without a prompt." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sent as %1$@'s launch argument — it starts working on it immediately. Leave empty to open %2$@ without a prompt." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@の起動引数として送信され、すぐに作業を開始します。空欄にするとプロンプトなしで%@を開きます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "會作為 %@ 的啟動參數送出 — 它會立刻開始處理。留空則直接開啟 %@,不帶提示。" + } + } + } + }, + "Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段" + } + } + } + }, + "session %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッション%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段 %@" + } + } + } + }, + "Session identity" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションの識別情報" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段識別" + } + } + } + }, + "SESSION NOT FOUND" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションが見つかりません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "找不到工作階段" + } + } + } + }, + "Session setup scripts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションのセットアップスクリプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段設定指令碼" + } + } + } + }, + "Sessions run under herdr" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションはherdrで実行されます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段在 herdr 下執行" + } + } + } + }, + "Sessions, live tiles, and attach all come from a %@ server on each host, and %@ doesn't have %@ yet. You can still use a plain shell — the SHELL chip on the host's rail opens one. For the full deck, install %@ on the host:" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sessions, live tiles, and attach all come from a %1$@ server on each host, and %2$@ doesn't have %3$@ yet. You can still use a plain shell — the SHELL chip on the host's rail opens one. For the full deck, install %4$@ on the host:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッション、ライブタイル、アタッチはすべて各ホストの%@サーバーから提供されますが、%@にはまだ%@がありません。通常のシェルは使えます — ホストのバーにあるSHELLチップで開けます。Deckをすべて使うには、ホストに%@をインストールしてください:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段、即時方塊與連接功能都來自每台主機上的 %@ 伺服器,而 %@ 還沒有 %@。你仍然可以使用一般的 Shell — 主機按鍵列上的 SHELL 會開啟一個。若要完整的 Deck,請在主機上安裝 %@:" + } + } + } + }, + "Settings" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + } + } + }, + "Setup script" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セットアップスクリプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定指令碼" + } + } + } + }, + "Setup Script" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セットアップスクリプト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定指令碼" + } + } + } + }, + "Shared across Claude Code, Codex, Pi, and Grok Build" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code、Codex、Pi、Grok Buildで共有" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Claude Code、Codex、Pi 與 Grok Build 之間共用" + } + } + } + }, + "Shared keeps one editable command synchronized across Claude Code, Codex, Pi, and Grok Build." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「共有」にすると、編集できる1つのコマンドがClaude Code、Codex、Pi、Grok Buildの間で同期されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "共用會讓同一個可編輯的指令在 Claude Code、Codex、Pi 與 Grok Build 之間保持同步。" + } + } + } + }, + "Shell" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shell" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shell" + } + } + } + }, + "Show %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示 %@" + } + } + } + }, + "Show %@ helpers" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@のヘルパーチップを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示 %@ 輔助按鈕" + } + } + } + }, + "Show connection stats" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続統計を表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示連線統計" + } + } + } + }, + "Show diff" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "差分を表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示差異" + } + } + } + }, + "Show herdr shortcuts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdrのショートカットを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示 herdr 快速鍵" + } + } + } + }, + "Show in Bar" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バーに表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示在按鍵列" + } + } + } + }, + "Show Key passphrase" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "鍵のパスフレーズを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示金鑰密語" + } + } + } + }, + "Show only changed files" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "変更されたファイルのみ表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "只顯示變更過的檔案" + } + } + } + }, + "Show or hide keyboard" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーボードを表示/非表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示或隱藏鍵盤" + } + } + } + }, + "Show source" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ソースを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示原始碼" + } + } + } + }, + "Show the file tree" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルツリーを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示檔案樹狀結構" + } + } + } + }, + "Show the parent directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "親ディレクトリを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示上層目錄" + } + } + } + }, + "Show the whole tree" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ツリー全体を表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示完整樹狀結構" + } + } + } + }, + "Show Them" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示する" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示" + } + } + } + }, + "Show tmux shortcuts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "tmuxのショートカットを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示 tmux 快速鍵" + } + } + } + }, + "Showing the first %@." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初の%@を表示しています。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示前 %@。" + } + } + } + }, + "Shown in Settings and anywhere this palette is selected." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定と、このパレットが選択されているすべての場所に表示されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "會顯示在設定中,以及任何選用這個調色盤的地方。" + } + } + } + }, + "Shown on the deck and in the terminal window's source label." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckとターミナルウインドウのソースラベルに表示されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示在 Deck 以及終端機視窗的來源標籤上。" + } + } + } + }, + "Shows how to unlock the keychain" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーチェーンのロック解除方法を表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "說明如何解鎖鑰匙圈" + } + } + } + }, + "Shows its open tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開いているタブを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示它開啟的標籤頁" + } + } + } + }, + "Shows its open window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開いているウインドウを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示它開啟的視窗" + } + } + } + }, + "Shows placement controls for built-in commands" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "内蔵コマンドの配置コントロールを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示內建指令的位置設定" + } + } + } + }, + "Shows the saved key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存された鍵を表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示已儲存的金鑰" + } + } + } + }, + "Shows them on this host's deck" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このホストのDeckに表示します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在這台主機的 Deck 上顯示它們" + } + } + } + }, + "Shows why the host could not be reached" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストに接続できなかった理由を表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示無法連線到主機的原因" + } + } + } + }, + "Sign in with" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サインイン方法" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "登入方式" + } + } + } + }, + "Signal check" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シグナルチェック" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊號檢查" + } + } + } + }, + "Signs in over SSH with the settings above, then looks for %@ and mosh-server on the host." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上記の設定でSSHにサインインし、ホスト上で%@とmosh-serverを探します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用上面的設定透過 SSH 登入,然後在主機上尋找 %@ 與 mosh-server。" + } + } + } + }, + "Signs in over SSH with the settings above, then looks for %@ on the host." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上記の設定でSSHにサインインし、ホスト上で%@を探します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用上面的設定透過 SSH 登入,然後在主機上尋找 %@。" + } + } + } + }, + "Smaller text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字を小さく" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "縮小文字" + } + } + } + }, + "Smaller Text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字を小さく" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "縮小文字" + } + } + } + }, + "Something outside Multiplex asked to launch %@ on %@ with this first prompt:\n\n%@\n\nRun it only if you started this." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Something outside Multiplex asked to launch %1$@ on %2$@ with this first prompt:\n\n%3$@\n\nRun it only if you started this." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexの外部から、%@を%@で起動するよう要求がありました。最初のプロンプトは次のとおりです:\n\n%@\n\n心当たりがある場合のみ実行してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 以外的來源要求啟動 %@(主機:%@),第一個提示為:\n\n%@\n\n只有在這是你自己發起的情況下才執行。" + } + } + } + }, + "Something outside Multiplex asked to launch %@ on %@.\n\nRun it only if you started this." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Something outside Multiplex asked to launch %1$@ on %2$@.\n\nRun it only if you started this." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexの外部から、%@を%@で起動するよう要求がありました。\n\n心当たりがある場合のみ実行してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 以外的來源要求啟動 %@(主機:%@)。\n\n只有在這是你自己發起的情況下才執行。" + } + } + } + }, + "Something outside Multiplex asked to open %@ on %@. Open it only if you started this." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Something outside Multiplex asked to open %1$@ on %2$@. Open it only if you started this." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexの外部から、%@を%@で開くよう要求がありました。心当たりがある場合のみ開いてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 以外的來源要求開啟 %@(主機:%@)。只有在這是你自己發起的情況下才開啟。" + } + } + } + }, + "Something outside Multiplex asked to view %@ on %@. The file opens read-only. Open it only if you started this." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Something outside Multiplex asked to view %1$@ on %2$@. The file opens read-only. Open it only if you started this." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexの外部から、%@を%@で表示するよう要求がありました。ファイルは読み取り専用で開きます。心当たりがある場合のみ開いてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex 以外的來源要求檢視 %@(主機:%@)。檔案會以唯讀方式開啟。只有在這是你自己發起的情況下才開啟。" + } + } + } + }, + "Somewhere else" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他の場所" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在其他地方" + } + } + } + }, + "sound files" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サウンドファイル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "音訊檔案" + } + } + } + }, + "Source or diff" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ソースまたは差分" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "原始碼或差異" + } + } + } + }, + "Split Left / Right" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "左右に分割" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "左右分割" + } + } + } + }, + "Split Top / Bottom" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上下に分割" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "上下分割" + } + } + } + }, + "SSH carries both the control connection and attached terminals." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSHは制御用の接続とアタッチしたターミナルの両方を運びます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 同時承載控制連線與已連接的終端機。" + } + } + } + }, + "SSH LINK — NO KEEPALIVE · RTT SAMPLED BY THE DECK PROBE · ECHO MEASURED WHILE YOU TYPE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSHリンク — キープアライブなし · RTTはDeckのプローブで取得 · エコーは入力中に測定" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 連線 — 無保持連線 · RTT 由 Deck 探測取樣 · 回應在你打字時測量" + } + } + } + }, + "staged addition" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ステージ済みの追加" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已暫存的新增" + } + } + } + }, + "Standby" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "待機" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "待命" + } + } + } + }, + "Start ${agent} on ${host}" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "${host} で ${agent} を起動" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 ${host} 上啟動 ${agent}" + } + } + } + }, + "Start from the active palette, then tune its surface and ANSI colors." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のパレットから始めて、表面とANSIカラーを調整します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從目前的調色盤開始,再調整它的表面與 ANSI 顏色。" + } + } + } + }, + "Starting directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始ディレクトリ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "起始目錄" + } + } + } + }, + "Starts %@ in the fresh shell. The optional prompt becomes its first message; %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Starts %1$@ in the fresh shell. The optional prompt becomes its first message; %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいシェルで%1$@を起動します。任意のプロンプトが最初のメッセージになります。%2$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在全新的 Shell 中啟動 %1$@。選填的提示會成為它的第一則訊息;%2$@" + } + } + } + }, + "Starts %@ in the new tab. The optional prompt becomes its first message; %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Starts %1$@ in the new tab. The optional prompt becomes its first message; %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいタブで%1$@を起動します。任意のプロンプトが最初のメッセージになります。%2$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在新標籤頁中啟動 %1$@。選填的提示會成為它的第一則訊息;%2$@" + } + } + } + }, + "Starts a CLI agent in a fresh tmux session, with an optional working directory, setup script, and first prompt." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいtmuxセッションでCLIエージェントを起動します。作業ディレクトリ、セットアップスクリプト、最初のプロンプトは任意です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在全新的 tmux 工作階段中啟動 CLI Agent,可選填工作目錄、設定指令碼與第一個提示。" + } + } + } + }, + "Starts in" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始場所" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "起始於" + } + } + } + }, + "Starts in %@. %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Starts in %1$@. %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@から開始します。%2$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從 %1$@ 開始。%2$@" + } + } + } + }, + "Starts in %@. Choose Home to use the login shell's default." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@から開始します。ログインシェルのデフォルトを使うには「ホーム」を選択します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "從 %@ 開始。選擇「家目錄」可使用登入 Shell 的預設值。" + } + } + } + }, + "Starts in, %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始場所、%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "起始於,%@" + } + } + } + }, + "Starts in, Home" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始場所、ホーム" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "起始於,家目錄" + } + } + } + }, + "Starts monitoring this host on the deck again" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このホストをDeckで再び監視します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新在 Deck 上監控這台主機" + } + } + } + }, + "stays open" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開いたまま" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持開啟" + } + } + } + }, + "Stop loading" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "読み込みを停止" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "停止載入" + } + } + } + }, + "Stops “%@” on %@ and everything running in it, and deletes its saved state (herdr keeps its default session on disk, stopped)." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Stops “%1$@” on %2$@ and everything running in it, and deletes its saved state (herdr keeps its default session on disk, stopped)." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@上の「%1$@」とその中で実行中のすべてを停止し、保存された状態を削除します(herdrはデフォルトセッションを停止した状態でディスクに残します)。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "停止 %2$@ 上的「%1$@」以及其中執行的一切,並刪除其已儲存的狀態(herdr 會將預設工作階段保留在磁碟上,處於停止狀態)。" + } + } + } + }, + "Stops “%@” on %@, deletes its saved state when herdr allows it, then closes the tab." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Stops “%1$@” on %2$@, deletes its saved state when herdr allows it, then closes the tab." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@上の「%1$@」を停止し、herdrが許可すれば保存された状態を削除してから、タブを閉じます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "停止 %2$@ 上的「%1$@」,在 herdr 允許時刪除其已儲存的狀態,然後關閉標籤頁。" + } + } + } + }, + "Stored in iCloud Keychain, never in the host record." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "iCloudキーチェーンに保存され、ホストの記録には保存されません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "儲存在 iCloud 鑰匙圈中,絕不會存入主機記錄。" + } + } + } + }, + "Submit with Enter" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enterで送信" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 Enter 送出" + } + } + } + }, + "submits" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信する" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "送出" + } + } + } + }, + "Surface" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表面" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "表面" + } + } + } + }, + "Switch this host to the herdr backend" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このホストをherdrバックエンドに切り替えます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將這台主機切換到 herdr 後端" + } + } + } + }, + "Switch to %@ %lld, %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Switch to %1$@ %2$lld, %3$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$lldに切り替え、%3$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換至 %1$@ %2$lld,%3$@" + } + } + } + }, + "Switch Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウインドウを切り替え" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換視窗" + } + } + } + }, + "Switch Workspace" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペースを切り替え" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換工作區" + } + } + } + }, + "System follows the device. The deck, terminal chrome, and forms switch together; each appearance keeps its own terminal theme below." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「システム」はデバイスに従います。Deck、ターミナルのフレーム、フォームが一緒に切り替わります。各外観は下にそれぞれのターミナルテーマを保持します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "「系統」會跟著裝置。Deck、終端機外框與表單會一起切換;每種外觀在下方各自保有自己的終端機主題。" + } + } + } + }, + "System follows the device. The deck, terminal chrome, and forms switch together. Dark and Glass share the dark terminal theme below." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「システム」はデバイスに従います。Deck、ターミナルのフレーム、フォームが一緒に切り替わります。ダークとグラスは下のダークターミナルテーマを共有します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "「系統」會跟著裝置。Deck、終端機外框與表單會一起切換。深色與玻璃共用下方的深色終端機主題。" + } + } + } + }, + "tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "標籤頁" + } + } + } + }, + "Tab" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "標籤頁" + } + } + } + }, + "Tab in “%@”" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」内のタブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」中的標籤頁" + } + } + } + }, + "Tabs" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タブ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "標籤頁" + } + } + } + }, + "Tap again to forget. The next connection trusts whatever answers, and records that." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "もう一度タップすると削除します。次回の接続は応答したホストをそのまま信頼し、それを記録します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "再點一次即可忘記。下次連線會信任任何回應的一方,並將其記錄下來。" + } + } + } + }, + "Tap or click twice. The remote gets two real clicks first — then the selection block rises at that spot: " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2回タップまたはクリックします。リモートにはまず実際のクリックが2回届き — その後その位置に選択ブロックが現れます:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "輕點或按兩下。遠端會先收到兩次真正的點擊 — 接著選取區塊會在該處浮現:" + } + } + } + }, + "TAP SENDS · DOT = STAYS OPEN · HOLD A CELL TO EDIT · TAP OUTSIDE CLOSES" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップで送信 · ドット = 開いたまま · セルを長押しで編集 · 外側をタップで閉じる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "輕點即送出 · 圓點 = 保持開啟 · 長按格子可編輯 · 點外面即關閉" + } + } + } + }, + "Target host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "対象ホスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "目標主機" + } + } + } + }, + "Terminal" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機" + } + } + } + }, + "Terminal actions" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルの操作" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機動作" + } + } + } + }, + "Terminal renderer" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルレンダラー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機 renderer" + } + } + } + }, + "TERMINAL SURFACE" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルの表面" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機表面" + } + } + } + }, + "TERMINAL TOO SHORT TO JUMP" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルが短すぎてジャンプできません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機太短,無法跳至" + } + } + } + }, + "Terminals attach over UDP and survive roaming or sleep. SSH still signs in, starts mosh-server, and probes the deck." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルはUDPでアタッチし、ローミングやスリープ後も維持されます。SSHは引き続きサインインし、mosh-serverを起動してDeckを探索します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機會透過 UDP 連線,並能在漫遊或睡眠後存活。SSH 仍負責登入、啟動 mosh-server 並探測 Deck。" + } + } + } + }, + "Test connection" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続をテスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "測試連線" + } + } + } + }, + "Text" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字" + } + } + } + }, + "Text Size" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字サイズ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字大小" + } + } + } + }, + "Text size %@; resets to 100 percent" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字サイズ%@、100パーセントにリセット" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字大小 %@;重設為 100%%" + } + } + } + }, + "Text to type" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "入力するテキスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要輸入的文字" + } + } + } + }, + "That bind code is missing its key." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのバインドコードには鍵がありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該綁定碼缺少金鑰。" + } + } + } + }, + "That machine stopped announcing — run mpx bind again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのマシンは通知を停止しました — mpx bindをもう一度実行してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "那台機器已停止廣播 — 請重新執行 mpx bind。" + } + } + } + }, + "That passphrase didn't unlock the private key for “%@”. Try again. Save & Connect replaces the copy in iCloud Keychain." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのパスフレーズでは「%@」の秘密鍵を解除できませんでした。もう一度お試しください。「保存して接続」はiCloudキーチェーン内のコピーを置き換えます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該密語無法解鎖「%@」的私鑰。請再試一次。「儲存並連線」會取代 iCloud 鑰匙圈中的副本。" + } + } + } + }, + "That passphrase didn't unlock the private key. Try again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのパスフレーズでは秘密鍵を解除できませんでした。もう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該密語無法解鎖私鑰。請再試一次。" + } + } + } + }, + "That password didn't unlock it. Try again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのパスワードでは解除できませんでした。もう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該密碼無法解鎖。請再試一次。" + } + } + } + }, + "That PIN didn't match — check the terminal and try again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PINが一致しません — ターミナルを確認してもう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN 碼不符 — 請檢視終端機後再試一次。" + } + } + } + }, + "The App Store could not be reached. Check the internet connection and try again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeに接続できませんでした。インターネット接続を確認してもう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法連線至 App Store。請檢查網路連線後再試一次。" + } + } + } + }, + "The App Store could not complete the request. Check that this device is signed in to the App Store, then try again. (%@ %lld)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The App Store could not complete the request. Check that this device is signed in to the App Store, then try again. (%1$@ %2$lld)" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeがリクエストを完了できませんでした。このデバイスがApp Storeにサインインしていることを確認してから、もう一度お試しください。(%1$@ %2$lld)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Store 無法完成這項請求。請確認此裝置已登入 App Store,然後再試一次。(%1$@ %2$lld)" + } + } + } + }, + "The App Store could not complete the request. Try again in a moment." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeがリクエストを完了できませんでした。しばらくしてからもう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Store 無法完成這項請求。請稍後再試。" + } + } + } + }, + "The App Store purchase sheet could not be presented from this window." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このウインドウからApp Storeの購入シートを表示できませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法從這個視窗顯示 App Store 購買面板。" + } + } + } + }, + "The App Store reported a system error. (%@)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeがシステムエラーを報告しました。(%@)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Store 回報了系統錯誤。(%@)" + } + } + } + }, + "The App Store returned an unknown purchase result." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeが不明な購入結果を返しました。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Store 傳回了未知的購買結果。" + } + } + } + }, + "The App Store transaction could not be verified." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeの取引を検証できませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法驗證這筆 App Store 交易。" + } + } + } + }, + "The backend New Session, widgets, and Shortcuts start on unless they name another" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「新規セッション」、ウィジェット、ショートカットは、他を指定しない限りこのバックエンドで起動します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "除非另有指定,否則「新增工作階段」、小工具與捷徑都會以此後端啟動" + } + } + } + }, + "The bind didn’t complete. Run mpx bind again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バインドが完了しませんでした。mpx bindをもう一度実行してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "綁定未完成。請重新執行 mpx bind。" + } + } + } + }, + "The bind exchange broke off — %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バインドのやり取りが中断しました — %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "綁定交握中斷 — %@" + } + } + } + }, + "The clipboard doesn’t hold a bind code. Copy the multiplex:// line the CLI printed." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリップボードにバインドコードがありません。CLIが出力したmultiplex://の行をコピーしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "剪貼板中沒有綁定碼。請複製 CLI 印出的 multiplex:// 那一行。" + } + } + } + }, + "The command prompts for that Mac account's login password. The unlock holds until macOS locks the keychain again — after a restart, or per the keychain's own lock settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このコマンドはそのMacアカウントのログインパスワードを尋ねます。ロック解除はmacOSが再びキーチェーンをロックするまで — 再起動後、またはキーチェーン自体のロック設定に従って — 続きます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該指令會要求輸入那個 Mac 帳號的登入密碼。解鎖狀態會維持到 macOS 再次鎖定鑰匙圈為止 — 重新開機後,或依鑰匙圈本身的鎖定設定。" + } + } + } + }, + "The command prompts for that Mac account's login password. The unlock holds until macOS locks the keychain again — after a restart, or per the keychain's own lock settings. When the deck detects this state it also points here: the host's rail reads KEYCHAIN LOCKED." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このコマンドはそのMacアカウントのログインパスワードを尋ねます。ロック解除はmacOSが再びキーチェーンをロックするまで — 再起動後、またはキーチェーン自体のロック設定に従って — 続きます。Deckがこの状態を検出した場合もここを案内します:ホストのレールにKEYCHAIN LOCKEDと表示されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該指令會要求輸入那個 Mac 帳號的登入密碼。解鎖狀態會維持到 macOS 再次鎖定鑰匙圈為止 — 重新開機後,或依鑰匙圈本身的鎖定設定。當 Deck 偵測到這個狀態時也會指向這裡:主機的列上會顯示 KEYCHAIN LOCKED。" + } + } + } + }, + "The connection closed before the check finished. Try again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認が終わる前に接続が閉じられました。もう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢查完成前連線就已關閉。請再試一次。" + } + } + } + }, + "The deck is built around a session multiplexer on each host — sessions, live tiles, and attach all come from it. Each host runs one, tmux or herdr, chosen in its settings under Sessions run on. A host without that multiplexer still works as a plain shell (the SHELL chip on its rail), it just has no session tiles. To get the full deck, install the one the host is set to:" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckは各ホスト上のセッションマルチプレクサを中心に構成されています — セッション、ライブタイル、アタッチはすべてそこから来ます。各ホストはtmuxかherdrのどちらか1つを実行し、ホスト設定の「セッションの実行先」で選択します。マルチプレクサのないホストも通常のシェルとしては動作しますが(レールのSHELLチップ)、セッションタイルはありません。Deckをフルに使うには、そのホストに設定されている方をインストールしてください:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck 是圍繞每台主機上的工作階段多工器建立的 — 工作階段、即時方塊與連接全都來自它。每台主機執行一個,tmux 或 herdr,在其設定的「工作階段執行於」中選擇。沒有多工器的主機仍可作為單純的 Shell 使用(列上的 SHELL 按鈕),只是沒有工作階段方塊。若要取得完整的 Deck,請安裝該主機所設定的那一個:" + } + } + } + }, + "The deck monitors a remote tmux server — sessions, windows, and agent panes. herdr (herdr.dev) is the alternative for hosts that run it." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckはリモートのtmuxサーバー — セッション、ウインドウ、エージェントのペイン — を監視します。herdr (herdr.dev) はそれを実行するホスト向けの選択肢です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck 會監控遠端的 tmux 伺服器 — 工作階段、視窗與 Agent 窗格。herdr(herdr.dev)則是給執行它的主機的另一個選擇。" + } + } + } + }, + "The deck monitors herdr (herdr.dev) sessions and their agents through the herdr CLI — one tile per session, its workspaces as the tile's window lines; a tile attaches the full herdr client. The tmux options editor doesn't apply to herdr sessions." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckはherdr CLIを通じてherdr (herdr.dev) のセッションとそのエージェントを監視します — セッションごとに1つのタイル、そのワークスペースがタイルのウインドウ行になります。タイルからはherdrクライアント全体にアタッチします。tmuxオプションエディタはherdrセッションには適用されません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck 會透過 herdr CLI 監控 herdr(herdr.dev)的工作階段與其中的 Agent — 每個工作階段一個方塊,其工作區則作為方塊上的視窗列;點一個方塊會連接完整的 herdr 用戶端。tmux 選項編輯器不適用於 herdr 工作階段。" + } + } + } + }, + "The deck probes this host about every five seconds while it's in front, and its sessions appear as live tiles." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このホストが前面にある間、Deckは約5秒ごとに探索し、そのセッションがライブタイルとして表示されます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "當這台主機在前景時,Deck 大約每五秒探測一次,其工作階段會以即時方塊呈現。" + } + } + } + }, + "The deck re-probes every few seconds — session tiles light up as soon as %@ is on the host. %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The deck re-probes every few seconds — session tiles light up as soon as %1$@ is on the host. %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckは数秒ごとに再探索します — %1$@がホストに入るとすぐにセッションタイルが点灯します。%2$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck 每隔幾秒就會重新探測 — %1$@ 一在主機上就緒,工作階段方塊就會亮起。%2$@" + } + } + } + }, + "The deck re-probes every few seconds and lights the tile as soon as the multiplexer lands — no restart, and nothing to configure on the host." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckは数秒ごとに再探索し、マルチプレクサが入るとすぐにタイルが点灯します — 再起動も、ホスト側の設定も不要です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck 每隔幾秒就會重新探測,多工器一就位就會點亮方塊 — 不需重新啟動,主機上也不用設定任何東西。" + } + } + } + }, + "The deck runs on %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deckは%@で動作します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deck 以 %@ 運作" + } + } + } + }, + "The eight standard colors emitted by terminal programs." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルプログラムが出力する8つの標準カラー。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機程式輸出的八種標準顏色。" + } + } + } + }, + "The field holds nothing Multiplex can open — a target needs a web or mail address. Edit it, or copy the text if it's still useful." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このフィールドにはMultiplexが開けるものがありません — 対象にはWebまたはメールのアドレスが必要です。編集するか、テキストが役に立つならコピーしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個欄位中沒有 Multiplex 能開啟的內容 — 目標需要一個網址或郵件地址。你可以編輯它,或在文字仍有用時複製起來。" + } + } + } + }, + "The field holds nothing the viewer can resolve — a relative path needs a directory in it, and spaces only work behind /, ~/, ./ or $HOME/. Edit it, or copy the text if it's still useful." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このフィールドにはビューアが解決できるものがありません — 相対パスにはディレクトリが必要で、スペースは/、~/、./、$HOME/の後ろでのみ有効です。編集するか、テキストが役に立つならコピーしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個欄位中沒有檢視器能解析的內容 — 相對路徑需要包含目錄,而空格只有接在 /、~/、./ 或 $HOME/ 之後才有效。你可以編輯它,或在文字仍有用時複製起來。" + } + } + } + }, + "The first path is the default. New Session can choose another path or the host's home directory." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初のパスがデフォルトです。「新規セッション」では別のパスやホストのホームディレクトリを選べます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一個路徑是預設值。「新增工作階段」可以選擇其他路徑或主機的家目錄。" + } + } + } + }, + "The free tier stays useful: two hosts, spatial SSH terminals, live agent detection, built-in themes, %lld saved Key Commands and %lld built-in or custom agent-command taps each day." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The free tier stays useful: two hosts, spatial SSH terminals, live agent detection, built-in themes, %1$lld saved Key Commands and %2$lld built-in or custom agent-command taps each day." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無料プランも十分に使えます:ホスト2台、空間SSHターミナル、リアルタイムのエージェント検出、内蔵テーマ、保存できるキーコマンド%1$lld件、内蔵またはカスタムのエージェントコマンドのタップが1日%2$lld回。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "免費方案依然好用:兩台主機、空間 SSH 終端機、即時 Agent 偵測、內建主題、%1$lld 組已儲存的按鍵指令,以及每天 %2$lld 次內建或自訂的 Agent 指令輕點。" + } + } + } + }, + "The high-intensity variants used for bold and bright output." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボールドや明るい出力に使われる高輝度のバリアント。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "用於粗體與明亮輸出的高亮度變體。" + } + } + } + }, + "the host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機" + } + } + } + }, + "The host refused: permission denied." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストが拒否しました:許可がありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機拒絕:權限不足。" + } + } + } + }, + "The host says %@ no longer exists." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストによると%@はもう存在しません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機表示 %@ 已不存在。" + } + } + } + }, + "The host says there is no such file." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストによるとそのようなファイルはありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機表示找不到這個檔案。" + } + } + } + }, + "The host's key is sealed. Set its passphrase in Host Settings, or open a terminal to this host first." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストの鍵は封印されています。ホスト設定でパスフレーズを設定するか、先にこのホストのターミナルを開いてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機金鑰已封存。請在主機設定中設定密語,或先開啟此主機的終端機。" + } + } + } + }, + "The line to reveal after the file opens." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルを開いた後に表示する行です。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案開啟後要顯示的行。" + } + } + } + }, + "The Mac's keychain is locked, so Claude Code shows signed out" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Macのキーチェーンがロックされているため、Claude Codeはサインアウト状態で表示されます" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac 的鑰匙圈已鎖定,因此 Claude Code 顯示為已登出" + } + } + } + }, + "The machine declined the bind." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのマシンはバインドを拒否しました。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該機器拒絕了綁定。" + } + } + } + }, + "The machine refused the bind." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのマシンはバインドを拒否しました。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該機器拒絕了綁定。" + } + } + } + }, + "The machine refused: %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マシンが拒否しました:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "該機器拒絕了:%@" + } + } + } + }, + "The name labels this host on the deck. Address, port, and user form the SSH destination." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名前はDeck上でこのホストを示すラベルです。アドレス、ポート、ユーザがSSHの接続先になります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "名稱是這台主機在 Deck 上的標籤。位址、埠和使用者組成 SSH 目的地。" + } + } + } + }, + "The OpenSSH key is stored in iCloud Keychain. Leave its passphrase blank to enter it when connecting, or save the passphrase there too." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenSSH鍵はiCloudキーチェーンに保存されます。パスフレーズを空欄のままにすると接続時に入力でき、パスフレーズも一緒に保存できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenSSH 金鑰儲存在 iCloud 鑰匙圈中。密語留空即可在連線時輸入,或一併將密語存在那裡。" + } + } + } + }, + "The passphrase didn't unlock the private key for %@. Try again." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスフレーズで%@の秘密鍵を解除できませんでした。もう一度お試しください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "密語無法解鎖 %@ 的私鑰。請再試一次。" + } + } + } + }, + "The pending purchase did not grant an active Multiplex Pro entitlement." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留中の購入では有効なMultiplex Proの権利が付与されませんでした。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "待處理的購買項目未授予有效的 Multiplex Pro 權利。" + } + } + } + }, + "The private key couldn't be read. Paste an OpenSSH ed25519 or RSA key, including its BEGIN/END lines." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "秘密鍵を読み取れませんでした。BEGIN/ENDの行を含むOpenSSHのed25519またはRSA鍵を貼り付けてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法讀取私鑰。請貼上 OpenSSH ed25519 或 RSA 金鑰,並包含 BEGIN/END 行。" + } + } + } + }, + "The private key for “%@” is encrypted. Connect Once keeps the passphrase until Multiplex closes. Save & Connect stores it in iCloud Keychain for your other devices." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」の秘密鍵は暗号化されています。Connect OnceはMultiplexを終了するまでパスフレーズを保持します。Save & ConnectはiCloudキーチェーンに保存し、ほかのデバイスでも使えます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "「%@」的私鑰已加密。「Connect Once」會保留密語直到 Multiplex 關閉;「Save & Connect」會將它存入 iCloud 鑰匙圈,供你的其他裝置使用。" + } + } + } + }, + "The private key for %@ couldn't be read. Paste an OpenSSH ed25519 or RSA key." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@の秘密鍵を読み取れませんでした。OpenSSHのed25519またはRSA鍵を貼り付けてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法讀取 %@ 的私鑰。請貼上 OpenSSH ed25519 或 RSA 金鑰。" + } + } + } + }, + "The private key for %@ is encrypted. Enter its passphrase to connect." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@の秘密鍵は暗号化されています。接続するにはパスフレーズを入力してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 的私鑰已加密。請輸入密語以連線。" + } + } + } + }, + "The purchase is not an active Multiplex Pro entitlement." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この購入は有効なMultiplex Proの権利ではありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此購買項目不是有效的 Multiplex Pro 權利。" + } + } + } + }, + "The same block, right where you pressed — and it reaches links and paths at any mouse mode, when a tap belongs to the remote." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "押した場所にそのままブロックが出ます — タップがリモート側のものになるどのマウスモードでも、リンクやパスに届きます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "同樣的區塊,就在你按住的位置 — 而且無論哪種滑鼠模式,即使點按屬於遠端,也能觸及連結與路徑。" + } + } + } + }, + "The speech-bubble key beside " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "吹き出しキーは " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "對話框圖示按鍵位於 " + } + } + } + }, + "The tab can't reconnect because its host no longer exists in the deck." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このタブのホストはDeckに存在しないため、再接続できません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此標籤頁無法重新連線,因為它的主機已不在 Deck 中。" + } + } + } + }, + "The terminal's canvas, text, and insertion cursor." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ターミナルのキャンバス、テキスト、挿入カーソルです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "終端機的畫布、文字和插入游標。" + } + } + } + }, + "The text looked like a link but has no address Multiplex can open. Copy it if it's still useful." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リンクのように見えますが、Multiplexが開けるアドレスがありません。必要であればコピーしてください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這段文字看起來像連結,但沒有 Multiplex 能開啟的位址。若仍有用,可以複製它。" + } + } + } + }, + "THE TMUX / HRDR KEY" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "tmux/HRDRキー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "tmux / HRDR 前置鍵" + } + } + } + }, + "The viewport renders this address inside Multiplex; OPEN still hands it to the system browser. It came from the host — check the address above before opening." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビューポートはこのアドレスをMultiplex内で表示します。OPENを使うとシステムのブラウザに渡されます。これはホストから来たものです — 開く前に上のアドレスを確認してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢視區會在 Multiplex 內繪製這個位址;OPEN 仍會交給系統瀏覽器開啟。它來自主機——開啟前請先確認上方的位址。" + } + } + } + }, + "The working tree matches HEAD here." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ここでは作業ツリーがHEADと一致しています。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這裡的工作樹與 HEAD 相同。" + } + } + } + }, + "Theme" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主題" + } + } + } + }, + "Theme identity" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テーマの識別情報" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主題識別" + } + } + } + }, + "Then run" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "次に実行" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "接著執行" + } + } + } + }, + "This address came from the host, and a hyperlink's visible text can differ from where it points — check the host above before opening it outside Multiplex. The target is editable when detection caught the wrong text." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このアドレスはホストから来たもので、ハイパーリンクの表示テキストはリンク先と異なる場合があります — Multiplexの外で開く前に上のホストを確認してください。検出が誤っている場合はリンク先を編集できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個位址來自主機,而超連結顯示的文字可能與實際指向不同——在 Multiplex 之外開啟前,請先確認上方的主機。若偵測抓錯文字,目標位址可以編輯。" + } + } + } + }, + "This address lives on %@'s network — the device must share it to load the page." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このアドレスは%@のネットワーク上にあります — ページを読み込むには、デバイスが同じネットワークに接続している必要があります。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個位址位於 %@ 的網路上——裝置必須連上同一個網路才能載入頁面。" + } + } + } + }, + "This copy of the app is not entitled to App Store purchases." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このAppにはApp Storeの購入に対する権利がありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這份 App 沒有 App Store 購買項目的權利。" + } + } + } + }, + "This device can't decode it — a format Core Audio doesn't read (Ogg Vorbis, for one), or not audio at all." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスではデコードできません — Core Audioが読めない形式(Ogg Vorbisなど)か、そもそも音声ではありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這台裝置無法解碼——可能是 Core Audio 不支援的格式(例如 Ogg Vorbis),或根本不是音訊。" + } + } + } + }, + "This device has no network connection" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスはネットワークに接続していません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這台裝置沒有網路連線" + } + } + } + }, + "This host shows both. New sessions start on the one chosen here; its default is set in Host Settings." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このホストは両方を表示します。新しいセッションはここで選んだ方で開始します。デフォルトはホスト設定で指定します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這台主機兩者都有。新工作階段會在這裡選擇的一方啟動;預設值可在主機設定中設定。" + } + } + } + }, + "This host was removed" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このホストは削除されました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這台主機已被移除" + } + } + } + }, + "This is the %@ theme now on screen." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これは現在画面に表示されている%@テーマです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這是目前畫面上的 %@ 主題。" + } + } + } + }, + "This mosh session is already open." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このmoshセッションはすでに開いています。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個 mosh 工作階段已經開啟。" + } + } + } + }, + "This page rides %@'s own address. The server must listen beyond loopback (vite --host, -H 0.0.0.0) for anything to answer." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このページは%@自身のアドレスを使います。応答を得るには、サーバがループバック以外でも待ち受ける必要があります(vite --host、-H 0.0.0.0)。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個頁面使用 %@ 自己的位址。伺服器必須監聽 loopback 以外的位址(vite --host、-H 0.0.0.0)才會有回應。" + } + } + } + }, + "This private key is encrypted. Enter its passphrase above." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この秘密鍵は暗号化されています。上にパスフレーズを入力してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "這個私鑰已加密。請在上方輸入密語。" + } + } + } + }, + "This purchase is available on your devices with the same Apple ID." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この購入は、同じApple IDを使用しているデバイスで利用できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此購買項目可在使用同一個 Apple ID 的裝置上使用。" + } + } + } + }, + "To" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "To" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "To" + } + } + }, + "shouldTranslate" : false + }, + "Toggle Pane Zoom" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペインのズームを切り替え" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換窗格縮放" + } + } + } + }, + "TOO LARGE — %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サイズが大きすぎます — %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案過大 — %@" + } + } + } + }, + "TOUCH & POINTER" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タッチとポインタ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "觸控與指標" + } + } + } + }, + "Transport" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トランスポート" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳輸方式" + } + } + } + }, + "Type %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を入力" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入 %@" + } + } + } + }, + "type only" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "入力のみ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "僅輸入" + } + } + } + }, + "Type the SSH destination yourself. Nothing to install on the machine." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSHの接続先を自分で入力します。マシンにインストールするものはありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自行輸入 SSH 目的地。機器上不需安裝任何東西。" + } + } + } + }, + "Types %@ into the fresh shell first, so the launch inherits what it sets up." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいシェルでまず%@を入力し、その設定を引き継いだ状態で起動します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "先在新的 Shell 中輸入 %@,讓啟動繼承它設定好的環境。" + } + } + } + }, + "UDP port or range" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDPポートまたは範囲" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDP 埠或範圍" + } + } + } + }, + "Unchecked" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オフ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未勾選" + } + } + } + }, + "unknown" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知" + } + } + } + }, + "Unlimited Hosts" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト数無制限" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無限主機" + } + } + } + }, + "UNLIMITED HOSTS" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホスト数無制限" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無限主機" + } + } + } + }, + "Unlock" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロック解除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "解鎖" + } + } + } + }, + "Unlock Multiplex" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplexのロックを解除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "解鎖 Multiplex" + } + } + } + }, + "Unlock Multiplex Pro" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proのロックを解除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "解鎖 Multiplex Pro" + } + } + } + }, + "Unlock Multiplex Pro · %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proのロックを解除 · %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "解鎖 Multiplex Pro · %@" + } + } + } + }, + "Unlock PDF" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDFのロックを解除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "解鎖 PDF" + } + } + } + }, + "Unlock SSH Key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH鍵のロックを解除" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "解鎖 SSH 金鑰" + } + } + } + }, + "Unlocks Multiplex with %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@でMultiplexのロックを解除します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 %@ 解鎖 Multiplex" + } + } + } + }, + "unreadable herdr probe response" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "herdrの応答を読み取れません" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法讀取的 herdr 探測回應" + } + } + } + }, + "untracked" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未追跡" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "未追蹤" + } + } + } + }, + "Upload failed" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アップロードに失敗しました" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "上傳失敗" + } + } + } + }, + "Use Claude Code, Codex, Pi, and Grok Build quick commands without the daily limit, browse a Claude Code session's prompt history and jump its transcript back to any message, and get a banner when an unwatched supported session needs you." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code、Codex、Pi、Grok Buildのクイックコマンドを1日の上限なしで使用でき、Claude Codeセッションのプロンプト履歴を閲覧してトランスクリプトを任意のメッセージまでジャンプでき、見ていない対応セッションがあなたを必要とするときにバナーを受け取れます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "不受每日上限地使用 Claude Code、Codex、Pi 和 Grok Build 快速指令,瀏覽 Claude Code 工作階段的提示歷史並將對話記錄跳至任一則訊息,並在你沒在看的支援工作階段需要你時收到橫幅通知。" + } + } + } + }, + "Use one port or a range from 1 to 65535." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1から65535までの単一のポートまたは範囲を指定してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "請使用 1 到 65535 之間的單一埠或範圍。" + } + } + } + }, + "User" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用者" + } + } + } + }, + "Uses %@'s own default model." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@自身のデフォルトモデルを使用します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 %@ 自己的預設模型。" + } + } + } + }, + "Uses the focused pane's directory — where the session is looking now." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フォーカス中のペインのディレクトリ — セッションが今見ている場所を使用します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用聚焦窗格的目錄——工作階段目前所在的位置。" + } + } + } + }, + "Uses the host's login-shell home directory." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホストのログインシェルのホームディレクトリを使用します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用主機登入 Shell 的家目錄。" + } + } + } + }, + "Version %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本 %@" + } + } + } + }, + "Version %@, " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン%@、" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本 %@," + } + } + } + }, + "View" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢視" + } + } + } + }, + "View file" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルを表示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢視檔案" + } + } + } + }, + "View Multiplex Pro" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplex Proを見る" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢視 Multiplex Pro" + } + } + } + }, + "VIEW opens it read-only in the file viewer, beside this session — nothing runs, nothing is written. The path is editable when detection caught the wrong text." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "VIEWはこのセッションの横にあるファイルビューアで読み取り専用として開きます — 何も実行されず、何も書き込まれません。検出が誤っている場合はパスを編集できます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "VIEW 會在這個工作階段旁的檔案檢視器中以唯讀方式開啟——不會執行任何東西,也不會寫入。若偵測抓錯文字,路徑可以編輯。" + } + } + } + }, + "Viewport" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビューポート" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢視區" + } + } + } + }, + "Volume" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "音量" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "音量" + } + } + } + }, + "Waiting for the App Store…" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storeを待っています…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在等待 App Store…" + } + } + } + }, + "WEB ADDRESSES ONLY" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェブアドレスのみ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "僅限網址" + } + } + } + }, + "What should %@ do?" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@に何をさせますか?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要讓 %@ 做什麼?" + } + } + } + }, + "What to launch" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "起動する対象" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要啟動什麼" + } + } + } + }, + "What’s new" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新機能" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新功能" + } + } + } + }, + "What’s New" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新機能" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新功能" + } + } + } + }, + "Where an existing-session launch opens: a new window on tmux; a new tab in the focused workspace or a new workspace on herdr." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "既存セッションでの起動先:tmuxでは新しいウインドウ、herdrではフォーカス中のワークスペースの新しいタブまたは新しいワークスペースです。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在既有工作階段中啟動時的開啟位置:tmux 為新視窗;herdr 為聚焦工作區中的新標籤頁或新工作區。" + } + } + } + }, + "Where the new session — or the new window, tab, or workspace inside an existing session — starts. Leave empty for the host default." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいセッション — または既存セッション内の新しいウインドウ、タブ、ワークスペース — が開始する場所です。空欄にするとホストのデフォルトを使用します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新工作階段——或既有工作階段中的新視窗、標籤頁或工作區——的起始位置。留空則使用主機預設值。" + } + } + } + }, + "Which backend the session runs on" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セッションを実行するバックエンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作階段執行於哪個後端" + } + } + } + }, + "Which multiplexer the agent runs on. Leave empty for the host default." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エージェントを実行するマルチプレクサです。空欄にするとホストのデフォルトを使用します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent 執行於哪個多工器。留空則使用主機預設值。" + } + } + } + }, + "Which multiplexer's session to open. Leave empty for the host default." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "どのマルチプレクサのセッションを開くかです。空欄にするとホストのデフォルトを使用します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要開啟哪個多工器的工作階段。留空則使用主機預設值。" + } + } + } + }, + "White" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホワイト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "白色" + } + } + } + }, + "Will verify against %@" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@と照合して検証します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將以 %@ 驗證" + } + } + } + }, + "Will verify against %@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Will verify against %1$@ %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ %@と照合して検証します" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將以 %@ %@ 驗證" + } + } + } + }, + "window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウインドウ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "視窗" + } + } + } + }, + "Windows" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウインドウ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "視窗" + } + } + } + }, + "With a hardware keyboard — or while locked — the mic key listens. Words type once they settle, and nothing is ever submitted for you." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハードウェアキーボード使用時も — ロック中も — マイクキーで音声入力できます。確定した語句だけが入力され、代わりに送信されることはありません。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用實體鍵盤時 — 或在鎖定時 — 麥克風鍵一樣會聽寫。文字在辨識確定後才輸入,而且永遠不會替你送出。" + } + } + } + }, + "With nothing here, the first connection trusts what answers and records it. Paste a key you already have and it is checked instead." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ここが空の場合、初回接続では応答したホストを信頼して記録します。すでに持っている鍵を貼り付けると、代わりにそれを照合します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此處留空時,首次連線會信任回應的主機並記錄其金鑰。若貼上你已有的金鑰,則改為驗證它。" + } + } + } + }, + "Working Directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作業ディレクトリ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作目錄" + } + } + } + }, + "Working tree vs HEAD" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作業ツリーとHEADの差分" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作樹與 HEAD 的差異" + } + } + } + }, + "workspace" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペース" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作區" + } + } + } + }, + "workspace %lld" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペース%lld" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作區 %lld" + } + } + } + }, + "Workspaces" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ワークスペース" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作區" + } + } + } + }, + "Yellow" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "イエロー" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "黃色" + } + } + } + }, + "You can still use a plain shell — press SHELL." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通常のシェルも使えます — SHELLを押してください。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你仍然可以用一般 Shell — 按 SHELL。" + } + } + } + }, + "Your themes" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "あなたのテーマ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的主題" + } + } + } + }, + "Zoom %lld percent; resets to fit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ズーム%lldパーセント。リセットで全体表示に戻ります" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "縮放 %lld 百分比;可重設為符合大小" + } + } + } + }, + "Zoom 100 percent; resets to fit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ズーム100パーセント。リセットで全体表示に戻ります" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "縮放 100 百分比;可重設為符合大小" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Multiplex/Models/CustomAgentCommand.swift b/Multiplex/Models/CustomAgentCommand.swift index 64469211..f8bc9404 100644 --- a/Multiplex/Models/CustomAgentCommand.swift +++ b/Multiplex/Models/CustomAgentCommand.swift @@ -63,7 +63,7 @@ struct CustomAgentCommand: Identifiable, Codable, Hashable { /// The actual payload is never truncated. var menuLabel: String { let value = normalizedContent - guard !value.isEmpty else { return "Custom command" } + guard !value.isEmpty else { return String(localized: "Custom command") } let lines = value.split(separator: "\n", omittingEmptySubsequences: false) let firstLine = lines.first.map(String.init) ?? value diff --git a/Multiplex/Models/ExternalAction.swift b/Multiplex/Models/ExternalAction.swift index d1ad4ad8..2f06d1ee 100644 --- a/Multiplex/Models/ExternalAction.swift +++ b/Multiplex/Models/ExternalAction.swift @@ -10,7 +10,7 @@ enum ExternalHostRef: Hashable { /// What a not-found failure calls the host. var displayName: String { switch self { - case .id: "Host" + case .id: String(localized: "Host") case .named(let name): name } } @@ -412,13 +412,14 @@ struct ExternalActionConfirmation: Equatable { static func make(for action: ExternalAction, hostName: String) -> ExternalActionConfirmation { switch action { case .openShell(_, let sessionName, _): - let target = sessionName.map { "session \($0)" } ?? "a session" + let target = sessionName.map { String(localized: "session \($0)") } + ?? String(localized: "a session") return ExternalActionConfirmation( - title: "Open \(hostName)?", - message: """ + title: String(localized: "Open \(hostName)?"), + message: String(localized: """ Something outside Multiplex asked to open \(target) on \ \(hostName). Open it only if you started this. - """, + """), action: action ) case .openAgent(_, let agent, let prompt, _, _, _, _, let target, _): @@ -426,32 +427,40 @@ struct ExternalActionConfirmation: Equatable { // the launch types into that session, not a fresh one. var location = hostName if case .existingSession(let name, _) = target { - location = "\(hostName) in session \(name)" + location = String(localized: "\(hostName) in session \(name)") } - var message = """ - Something outside Multiplex asked to launch \ - \(agent.displayName) on \(location) - """ + let message: String if let prompt, !prompt.isEmpty { - message += " with this first prompt:\n\n\(prompt)" + message = String(localized: """ + Something outside Multiplex asked to launch \ + \(agent.displayName) on \(location) with this first prompt: + + \(prompt) + + Run it only if you started this. + """) } else { - message += "." + message = String(localized: """ + Something outside Multiplex asked to launch \ + \(agent.displayName) on \(location). + + Run it only if you started this. + """) } - message += "\n\nRun it only if you started this." return ExternalActionConfirmation( - title: "Launch \(agent.displayName) on \(hostName)?", + title: String(localized: "Launch \(agent.displayName) on \(hostName)?"), message: message, action: action ) case .openFile(_, let path, let line): - let location = line.map { "\(path), line \($0)" } ?? path + let location = line.map { String(localized: "\(path), line \(String($0))") } ?? path return ExternalActionConfirmation( - title: "Open file on \(hostName)?", - message: """ + title: String(localized: "Open file on \(hostName)?"), + message: String(localized: """ Something outside Multiplex asked to view \(location) on \ \(hostName). The file opens read-only. Open it only if you \ started this. - """, + """), action: action ) } diff --git a/Multiplex/Models/HerdrShortcut.swift b/Multiplex/Models/HerdrShortcut.swift index 5e8d08fe..8fe60ea0 100644 --- a/Multiplex/Models/HerdrShortcut.swift +++ b/Multiplex/Models/HerdrShortcut.swift @@ -1,3 +1,5 @@ +import Foundation + /// Familiar herdr commands exposed by the terminal chrome — the herdr-backend /// sibling of `TmuxShortcut`. herdr's default prefix is Control-B like tmux's, /// and non-destructive rows send the stock prefix binding through SwiftTerm. @@ -17,6 +19,16 @@ enum HerdrShortcut: String, CaseIterable, Identifiable, Sendable { case panes = "Panes" case tabs = "Tabs" case workspaces = "Workspaces" + + /// Section header shown in the panel; the raw value stays English + /// because it also names accessibility identifiers. + var title: String { + switch self { + case .panes: String(localized: "Panes") + case .tabs: String(localized: "Tabs") + case .workspaces: String(localized: "Workspaces") + } + } } /// What a confirmed destructive row closes. The raw value is herdr's own @@ -50,14 +62,14 @@ enum HerdrShortcut: String, CaseIterable, Identifiable, Sendable { var title: String { switch self { - case .splitLeftRight: "Split Left / Right" - case .splitTopBottom: "Split Top / Bottom" - case .closePane: "Close Pane" - case .newTab: "New Tab" - case .closeTab: "Close Tab" - case .newWorkspace: "New Workspace" - case .renameWorkspace: "Rename Workspace" - case .closeWorkspace: "Close Workspace" + case .splitLeftRight: String(localized: "Split Left / Right") + case .splitTopBottom: String(localized: "Split Top / Bottom") + case .closePane: String(localized: "Close Pane") + case .newTab: String(localized: "New Tab") + case .closeTab: String(localized: "Close Tab") + case .newWorkspace: String(localized: "New Workspace") + case .renameWorkspace: String(localized: "Rename Workspace") + case .closeWorkspace: String(localized: "Close Workspace") } } diff --git a/Multiplex/Models/Host.swift b/Multiplex/Models/Host.swift index 8dd48b48..a70df021 100644 --- a/Multiplex/Models/Host.swift +++ b/Multiplex/Models/Host.swift @@ -9,8 +9,8 @@ struct Host: Identifiable, Codable, Hashable { var id: String { rawValue } var label: String { switch self { - case .password: "Password" - case .privateKey: "Private key" + case .password: String(localized: "Password") + case .privateKey: String(localized: "Private key") } } } diff --git a/Multiplex/Models/HostGuide.swift b/Multiplex/Models/HostGuide.swift index 2fc83a94..f4823923 100644 --- a/Multiplex/Models/HostGuide.swift +++ b/Multiplex/Models/HostGuide.swift @@ -28,7 +28,7 @@ enum HostGuide { static let herdrInstall: [Command] = [ Command(label: "Homebrew", command: "brew install herdr"), Command( - label: "Or, macOS or Linux", + label: String(localized: "Or, macOS or Linux"), command: "curl -fsSL https://herdr.dev/install.sh | sh"), ] @@ -51,14 +51,17 @@ enum HostGuide { _ backend: Host.SessionBackend, hostName: String ) -> String { let install = multiplexerInstall(for: backend).first?.command - var message = "\(backend.rawValue) isn’t installed on \(hostName), so there’s " - + "nothing to create the session with." + var message = String(localized: """ + \(backend.rawValue) isn’t installed on \(hostName), so there’s \ + nothing to create the session with. + """) if let install { - message += " Install it there (\(install)), " + message += String(localized: " Install it there (\(install)), ") } else { - message += " Install it there, " + message += String(localized: " Install it there, ") } - return message + "or change this host’s backend in Host Settings." + return message + String( + localized: "or change this host’s backend in Host Settings.") } /// Why a default install needs no further host setup: both probes @@ -68,10 +71,13 @@ enum HostGuide { static func probePathDetail(for backend: Host.SessionBackend) -> String { switch backend { case .tmux: - "Homebrew and /usr/local installs are already on the probe's PATH." + String(localized: + "Homebrew and /usr/local installs are already on the probe's PATH.") case .herdr: - "Homebrew, ~/.local/bin, and ~/.cargo/bin installs are already on " - + "the probe's PATH." + String(localized: """ + Homebrew, ~/.local/bin, and ~/.cargo/bin installs are already on \ + the probe's PATH. + """) } } @@ -82,8 +88,8 @@ enum HostGuide { /// must agree: the deck rail's offer confirmation, which is the press /// that spends it, and Host Settings' Backend detail, which is where it /// can be undone. A stale copy in either one misstates the trade. - static let secondBackendCost = - "roughly doubles what this host fetches on every deck refresh" + static let secondBackendCost = String( + localized: "roughly doubles what this host fetches on every deck refresh") /// The macOS locked-keychain fix (see `KeychainLockCheck`): unlock once /// in any shell on the host, then restart the signed-out agent. @@ -104,12 +110,13 @@ enum HostGuide { label: "Homebrew", command: "brew install multiplex-term/tap/mpx"), Command( - label: "Or, macOS or Linux", + label: String(localized: "Or, macOS or Linux"), command: "curl -fsSL https://multiplexterm.dev/install-mpx-cli | sh"), ] /// The one command the whole flow turns on. - static let mpxBind = Command(label: "Then run", command: "mpx bind") + static let mpxBind = Command( + label: String(localized: "Then run"), command: "mpx bind") /// The clipboard is opt-in in the CLI — a bind payload is /// credential-grade, and over Universal Clipboard it would land on every @@ -117,5 +124,5 @@ enum HostGuide { /// copy, and the app has to say so rather than let someone press a /// button that cannot do anything. static let mpxBindCopy = Command( - label: "For paste", command: "mpx bind --copy") + label: String(localized: "For paste"), command: "mpx bind --copy") } diff --git a/Multiplex/Models/KeyCommand.swift b/Multiplex/Models/KeyCommand.swift index 27186d99..d62b1216 100644 --- a/Multiplex/Models/KeyCommand.swift +++ b/Multiplex/Models/KeyCommand.swift @@ -96,19 +96,21 @@ struct KeyCommand: Identifiable, Codable, Hashable { return parts.joined(separator: " · ") case .cellHint: var parts: [String] = [] - if submits { parts.append("submits") } + if submits { parts.append(String(localized: "submits")) } if isRepeating { parts.append("×\(repeatCount) · \(repeatGapMilliseconds) ms") } - if !closesPanel { parts.append("stays open") } + if !closesPanel { parts.append(String(localized: "stays open")) } return parts.joined(separator: " · ") case .spoken: var text: String switch kind { case .chord(let chord): text = chord.accessibilityName - case .text(let snippet): text = "Type \(snippet.normalizedText)" + case .text(let snippet): text = String(localized: "Type \(snippet.normalizedText)") } - if submits { text += ", then Enter" } - if isRepeating { text += ", \(repeatCount) times" } - text += closesPanel ? ", closes the panel" : ", panel stays open" + if submits { text += String(localized: ", then Enter") } + if isRepeating { text += String(localized: ", \(repeatCount) times") } + text += closesPanel + ? String(localized: ", closes the panel") + : String(localized: ", panel stays open") return text } } diff --git a/Multiplex/Models/SessionScript.swift b/Multiplex/Models/SessionScript.swift index 40a2e094..d088863c 100644 --- a/Multiplex/Models/SessionScript.swift +++ b/Multiplex/Models/SessionScript.swift @@ -49,7 +49,7 @@ struct SessionScript: Identifiable, Hashable { let collapsed = firstLine .split(whereSeparator: \Character.isWhitespace) .joined(separator: " ") - guard !collapsed.isEmpty else { return "Script" } + guard !collapsed.isEmpty else { return String(localized: "Script") } guard collapsed.count > Self.maximumFallbackNameLength else { return collapsed } return String(collapsed.prefix(Self.maximumFallbackNameLength - 1)) + "…" } diff --git a/Multiplex/Models/ShortcutPanelContent.swift b/Multiplex/Models/ShortcutPanelContent.swift index ea5db122..6a2b53bc 100644 --- a/Multiplex/Models/ShortcutPanelContent.swift +++ b/Multiplex/Models/ShortcutPanelContent.swift @@ -1,3 +1,5 @@ +import Foundation + /// Pure presentation records for the shared multiplexer-shortcut panel: one /// UIKit dropdown renders both the tmux and the herdr shortcut sets. Derived /// entirely from `TmuxShortcut`/`HerdrShortcut` so the enums stay the single @@ -94,15 +96,15 @@ struct ShortcutPanelContent: Equatable, Sendable { prefixLabel: "DEFAULT PREFIX ⌃B", sections: TmuxShortcut.Group.allCases.map { group in ShortcutPanelSection( - title: group.rawValue, + title: group.title, accessibilityIdentifier: "tmuxGroup.\(group.rawValue)", items: TmuxShortcut.shortcuts(in: group).map(ShortcutPanelItem.init) ) }, - switchSectionTitle: "Switch Window", + switchSectionTitle: String(localized: "Switch Window"), switchSectionAccessibilityIdentifier: "tmuxWindowSection", switchChoiceAccessibilityPrefix: "tmuxWindow.", - switchNoun: "window" + switchNoun: String(localized: "window") ) /// herdr's default prefix is also Control-B (`herdr --default-config`, @@ -112,15 +114,15 @@ struct ShortcutPanelContent: Equatable, Sendable { prefixLabel: "DEFAULT PREFIX ⌃B", sections: HerdrShortcut.Group.allCases.map { group in ShortcutPanelSection( - title: group.rawValue, + title: group.title, accessibilityIdentifier: "herdrGroup.\(group.rawValue)", items: HerdrShortcut.shortcuts(in: group).map(ShortcutPanelItem.init) ) }, - switchSectionTitle: "Switch Workspace", + switchSectionTitle: String(localized: "Switch Workspace"), switchSectionAccessibilityIdentifier: "herdrWorkspaceSection", switchChoiceAccessibilityPrefix: "herdrWorkspace.", - switchNoun: "workspace" + switchNoun: String(localized: "workspace") ) /// The panel for one terminal tab's backend; nil hides the chrome diff --git a/Multiplex/Models/Talkback.swift b/Multiplex/Models/Talkback.swift index 9623d6fa..df9fefdb 100644 --- a/Multiplex/Models/Talkback.swift +++ b/Multiplex/Models/Talkback.swift @@ -165,6 +165,7 @@ enum TalkbackMessage { /// The field's empty-state copy, in the target's own name. static func placeholder(agentName: String?) -> String { - agentName.map { "Message \($0)…" } ?? "Message this pane…" + agentName.map { String(localized: "Message \($0)…") } + ?? String(localized: "Message this pane…") } } diff --git a/Multiplex/Models/TerminalGuide.swift b/Multiplex/Models/TerminalGuide.swift index 26f210c1..7675a9c2 100644 --- a/Multiplex/Models/TerminalGuide.swift +++ b/Multiplex/Models/TerminalGuide.swift @@ -1,3 +1,5 @@ +import Foundation + enum TerminalGuideBank: CaseIterable, Equatable { case touchPointer case linksPaths @@ -7,11 +9,11 @@ enum TerminalGuideBank: CaseIterable, Equatable { var title: String { switch self { - case .touchPointer: "TOUCH & POINTER" - case .linksPaths: "LINKS & PATHS" - case .keyboard: "KEYBOARD" - case .herdrPanes: "HERDR PANES" - case .clipboard: "CLIPBOARD" + case .touchPointer: String(localized: "TOUCH & POINTER") + case .linksPaths: String(localized: "LINKS & PATHS") + case .keyboard: String(localized: "KEYBOARD") + case .herdrPanes: String(localized: "HERDR PANES") + case .clipboard: String(localized: "CLIPBOARD") } } } @@ -58,13 +60,13 @@ enum TerminalGuide { id: "doubletap", figure: 1, bank: .touchPointer, - title: "DOUBLE TAP", + title: String(localized: "DOUBLE TAP"), tag: nil, body: [ - .text( - "Tap or click twice. The remote gets two real clicks first — " - + "then the selection block rises at that spot: " - ), + .text(String(localized: """ + Tap or click twice. The remote gets two real clicks first — then the \ + selection block rises at that spot:\u{20} + """)), .control("SELECT"), .text(" · "), .control("SELECT ALL"), @@ -77,98 +79,115 @@ enum TerminalGuide { id: "longpress", figure: 2, bank: .touchPointer, - title: "LONG PRESS", + title: String(localized: "LONG PRESS"), tag: nil, body: [ - .text("The same block, right where you pressed — and it reaches links " - + "and paths at any mouse mode, when a tap belongs to the remote."), + .text(String(localized: """ + The same block, right where you pressed — and it reaches links and \ + paths at any mouse mode, when a tap belongs to the remote. + """)), ] ), TerminalGuideEntry( id: "rightclick", figure: 3, bank: .touchPointer, - title: "RIGHT CLICK", - tag: "Pointer", + title: String(localized: "RIGHT CLICK"), + tag: String(localized: "Pointer"), body: [ - .text("A mouse's secondary click runs the same chain as a long press. On herdr it adds "), + .text(String(localized: """ + A mouse's secondary click runs the same chain as a long press. On herdr \ + it adds\u{20} + """)), .control("MENU"), - .text(" — the remote's own pane menu."), + .text(String(localized: " — the remote's own pane menu.")), ] ), TerminalGuideEntry( id: "pan", figure: 4, bank: .touchPointer, - title: "PAN TO SCROLL", + title: String(localized: "PAN TO SCROLL"), tag: nil, body: [ - .text("A drag scrolls the remote screen itself — the app speaks wheel, " - + "or arrow keys in full-screen apps. There is no separate local " - + "scrollback to fall out of."), + .text(String(localized: """ + A drag scrolls the remote screen itself — the app speaks wheel, or \ + arrow keys in full-screen apps. There is no separate local scrollback \ + to fall out of. + """)), ] ), TerminalGuideEntry( id: "edgeswipe", figure: 5, bank: .touchPointer, - title: "EDGE SWIPE", + title: String(localized: "EDGE SWIPE"), tag: "iPhone", body: [ - .text("From the screen's left edge, swipe right to step back to the deck."), + .text(String(localized: """ + From the screen's left edge, swipe right to step back to the deck. + """)), ] ), TerminalGuideEntry( id: "link", figure: 6, bank: .linksPaths, - title: "PRESS A LINK", + title: String(localized: "PRESS A LINK"), tag: nil, body: [ - .text("Nothing opens by itself. A press raises a sheet showing where it " - + "really points — "), + .text(String(localized: """ + Nothing opens by itself. A press raises a sheet showing where it \ + really points —\u{20} + """)), .control("OPEN"), - .text(", or dock the page beside this tab as a "), + .text(String(localized: ", or dock the page beside this tab as a ")), .control("⌗ VIEWPORT"), - .text(". On Vision Pro, links glow under your eye; pinch for the same sheet."), + .text(String(localized: """ + . On Vision Pro, links glow under your eye; pinch for the same sheet. + """)), ] ), TerminalGuideEntry( id: "path", figure: 7, bank: .linksPaths, - title: "PRESS A PATH", + title: String(localized: "PRESS A PATH"), tag: nil, body: [ - .text("Same press, its own sheet: "), + .text(String(localized: "Same press, its own sheet: ")), .control("▤ VIEW"), - .text(" opens it in the File Viewer. A "), + .text(String(localized: " opens it in the File Viewer. A ")), .key(":120"), - .text(" after the name scrolls to that line."), + .text(String(localized: " after the name scrolls to that line.")), ] ), TerminalGuideEntry( id: "kbdlock", figure: 8, bank: .keyboard, - title: "LOCK THE KEYBOARD", + title: String(localized: "LOCK THE KEYBOARD"), tag: "iPhone · iPad", body: [ - .text("Hold the "), + .text(String(localized: "Hold the ")), .key("⌨"), - .text(" key about half a second: taps stop summoning the keyboard. " - + "Hold again — or ⋯ → Unlock Keyboard — to release."), + .text(String(localized: """ + key about half a second: taps stop summoning the keyboard. Hold \ + again — or ⋯ → Unlock Keyboard — to release. + """)), ] ), TerminalGuideEntry( id: "dictate", figure: 9, bank: .keyboard, - title: "DICTATE", + title: String(localized: "DICTATE"), tag: "iPhone · iPad", body: [ - .text("With a hardware keyboard — or while locked — the mic key listens. " - + "Words type once they settle, and nothing is ever submitted for you."), + .text(String(localized: """ + With a hardware keyboard — or while locked — the mic key listens. Words \ + type once they settle, and nothing is ever submitted for you. + """)), ] ), TerminalGuideEntry( @@ -176,90 +195,106 @@ enum TerminalGuide { figure: 10, bank: .keyboard, title: "SHIFT + RETURN", - tag: "HW keyboard", + tag: String(localized: "HW keyboard"), body: [ - .text("A newline that doesn't send the line."), + .text(String(localized: "A newline that doesn't send the line.")), ] ), TerminalGuideEntry( id: "shortcutkey", figure: 11, bank: .keyboard, - title: "THE TMUX / HRDR KEY", + title: String(localized: "THE TMUX / HRDR KEY"), tag: nil, body: [ - .text("One key on the rail opens the whole shortcut panel: copy mode, " - + "splits, windows, workspaces, and the confirmed closes."), + .text(String(localized: """ + One key on the rail opens the whole shortcut panel: copy mode, splits, \ + windows, workspaces, and the confirmed closes. + """)), ] ), TerminalGuideEntry( id: "keycommands", figure: 12, bank: .keyboard, - title: "HOLD CTRL", + title: String(localized: "HOLD CTRL"), tag: nil, body: [ - .text("A tap latches "), + .text(String(localized: "A tap latches ")), .key("CTRL"), - .text("; a hold opens KEY COMMANDS — "), + .text(String(localized: "; a hold opens KEY COMMANDS — ")), .key("⇧⏎"), - .text(" newline, a double "), + .text(String(localized: " newline, a double ")), .key("⌃C"), .text(", "), .key("⌥⌫"), - .text(" delete-word, and your own chords or one-line text macros " - + "from CUSTOM SETUP, on every device."), + .text(String(localized: """ + delete-word, and your own chords or one-line text macros from CUSTOM \ + SETUP, on every device. + """)), ] ), TerminalGuideEntry( id: "talkback", figure: 13, bank: .keyboard, - title: "MESSAGE BOX", + title: String(localized: "MESSAGE BOX"), tag: nil, body: [ - .text("The speech-bubble key beside "), + .text(String(localized: "The speech-bubble key beside ")), .key("RET"), - .text(" opens a message box: write with autocorrect and your keyboard's " - + "own dictation, attach photos or files, then "), + .text(String(localized: """ + opens a message box: write with autocorrect and your keyboard's own \ + dictation, attach photos or files, then\u{20} + """)), .control("↑"), - .text(" sends it all as one message. The rail keeps driving the pane " - + "while you write; hold "), + .text(String(localized: """ + sends it all as one message. The rail keeps driving the pane while you \ + write; hold\u{20} + """)), .control("↑"), - .text(" to type without submitting. Locking the keyboard closes it."), + .text(String(localized: """ + to type without submitting. Locking the keyboard closes it. + """)), ] ), TerminalGuideEntry( id: "resize", figure: 14, bank: .herdrPanes, - title: "RESIZE PANES", + title: String(localized: "RESIZE PANES"), tag: "herdr", body: [ - .text("Hold a pane border until it wakes, then drag it where you want."), + .text(String(localized: """ + Hold a pane border until it wakes, then drag it where you want. + """)), ] ), TerminalGuideEntry( id: "panemenu", figure: 15, bank: .herdrPanes, - title: "PANE MENU", + title: String(localized: "PANE MENU"), tag: "herdr", body: [ .control("MENU"), - .text(" in the press block right-clicks the remote for you — herdr's " - + "pane menu opens under your finger."), + .text(String(localized: """ + in the press block right-clicks the remote for you — herdr's pane menu \ + opens under your finger. + """)), ] ), TerminalGuideEntry( id: "paste", figure: nil, bank: .clipboard, - title: "\"ALLOW PASTE\" EVERY TIME?", + title: String(localized: "\"ALLOW PASTE\" EVERY TIME?"), tag: "iPhone · iPad", body: [ - .text("iOS asks before Multiplex may read another app's copy. To stop " - + "the prompt: Settings → Multiplex → Paste from Other Apps → Allow."), + .text(String(localized: """ + iOS asks before Multiplex may read another app's copy. To stop the \ + prompt: Settings → Multiplex → Paste from Other Apps → Allow. + """)), ] ), ] diff --git a/Multiplex/Models/TerminalRoute.swift b/Multiplex/Models/TerminalRoute.swift index c0a88361..b908a67b 100644 --- a/Multiplex/Models/TerminalRoute.swift +++ b/Multiplex/Models/TerminalRoute.swift @@ -186,8 +186,8 @@ struct TerminalRoute: Codable, Hashable, Identifiable { /// follows them on herdr tabs. var menuTitle: String { switch self { - case .session: "New Session" - case .herdrWorkspaceTab: "New Tab in Workspace" + case .session: String(localized: "New Session") + case .herdrWorkspaceTab: String(localized: "New Tab in Workspace") } } @@ -196,15 +196,17 @@ struct TerminalRoute: Codable, Hashable, Identifiable { offering extra: NewTabTarget? ) -> String { extra == .herdrWorkspaceTab - ? "New tab: another session, a tab in this herdr workspace, " - + "or the file viewer" - : "New tab: another session or the file viewer" + ? String(localized: """ + New tab: another session, a tab in this herdr workspace, \ + or the file viewer + """) + : String(localized: "New tab: another session or the file viewer") } var failureTitle: String { switch self { - case .session: "Couldn't Create Session" - case .herdrWorkspaceTab: "Couldn't Create Tab" + case .session: String(localized: "Couldn't Create Session") + case .herdrWorkspaceTab: String(localized: "Couldn't Create Tab") } } } diff --git a/Multiplex/Models/TerminalTheme.swift b/Multiplex/Models/TerminalTheme.swift index dbf8a10f..4abef6d0 100644 --- a/Multiplex/Models/TerminalTheme.swift +++ b/Multiplex/Models/TerminalTheme.swift @@ -91,9 +91,14 @@ struct TerminalTheme: Codable, Hashable, Identifiable { } static let ansiNames: [String] = [ - "Black", "Red", "Green", "Yellow", "Blue", "Magenta", "Cyan", "White", - "Bright Black", "Bright Red", "Bright Green", "Bright Yellow", - "Bright Blue", "Bright Magenta", "Bright Cyan", "Bright White", + String(localized: "Black"), String(localized: "Red"), + String(localized: "Green"), String(localized: "Yellow"), + String(localized: "Blue"), String(localized: "Magenta"), + String(localized: "Cyan"), String(localized: "White"), + String(localized: "Bright Black"), String(localized: "Bright Red"), + String(localized: "Bright Green"), String(localized: "Bright Yellow"), + String(localized: "Bright Blue"), String(localized: "Bright Magenta"), + String(localized: "Bright Cyan"), String(localized: "Bright White"), ] } diff --git a/Multiplex/Models/TmuxShortcut.swift b/Multiplex/Models/TmuxShortcut.swift index dcff0f79..fefe989e 100644 --- a/Multiplex/Models/TmuxShortcut.swift +++ b/Multiplex/Models/TmuxShortcut.swift @@ -1,3 +1,5 @@ +import Foundation + /// Familiar tmux commands exposed by the terminal chrome. Most use the stock /// binding: Control-B (the default prefix) followed by one key. Destructive /// commands are confirmed by our UI, then run over the SSH control connection @@ -11,6 +13,16 @@ enum TmuxShortcut: String, CaseIterable, Identifiable, Sendable { case panes = "Panes" case resize = "Resize Pane" case windows = "Windows" + + /// Section header shown in the panel; the raw value stays English + /// because it also names accessibility identifiers. + var title: String { + switch self { + case .panes: String(localized: "Panes") + case .resize: String(localized: "Resize Pane") + case .windows: String(localized: "Windows") + } + } } case splitLeftRight @@ -46,21 +58,21 @@ enum TmuxShortcut: String, CaseIterable, Identifiable, Sendable { var title: String { switch self { - case .splitLeftRight: "Split Left / Right" - case .splitTopBottom: "Split Top / Bottom" - case .togglePaneZoom: "Toggle Pane Zoom" - case .copyMode: "Copy Mode" - case .closePane: "Close Pane" - case .resizeLeft: "Resize Left" - case .resizeDown: "Resize Down" - case .resizeUp: "Resize Up" - case .resizeRight: "Resize Right" - case .newWindow: "New Window" - case .chooseWindow: "Choose Window" - case .nextWindow: "Next Window" - case .previousWindow: "Previous Window" - case .renameWindow: "Rename Window" - case .closeWindow: "Close Window" + case .splitLeftRight: String(localized: "Split Left / Right") + case .splitTopBottom: String(localized: "Split Top / Bottom") + case .togglePaneZoom: String(localized: "Toggle Pane Zoom") + case .copyMode: String(localized: "Copy Mode") + case .closePane: String(localized: "Close Pane") + case .resizeLeft: String(localized: "Resize Left") + case .resizeDown: String(localized: "Resize Down") + case .resizeUp: String(localized: "Resize Up") + case .resizeRight: String(localized: "Resize Right") + case .newWindow: String(localized: "New Window") + case .chooseWindow: String(localized: "Choose Window") + case .nextWindow: String(localized: "Next Window") + case .previousWindow: String(localized: "Previous Window") + case .renameWindow: String(localized: "Rename Window") + case .closeWindow: String(localized: "Close Window") } } diff --git a/Multiplex/Services/AppLockStore.swift b/Multiplex/Services/AppLockStore.swift index bd4d9ab6..338e44b1 100644 --- a/Multiplex/Services/AppLockStore.swift +++ b/Multiplex/Services/AppLockStore.swift @@ -133,7 +133,7 @@ final class AppLockStore { guard enabled != isEnabled else { return } if enabled { guard await authenticate( - "Confirm \(Self.methodName) to require it when Multiplex opens" + String(localized: "Confirm \(Self.methodName) to require it when Multiplex opens") ) else { return } } isEnabled = enabled @@ -145,7 +145,7 @@ final class AppLockStore { guard !isAuthenticating else { return } isAuthenticating = true defer { isAuthenticating = false } - if await authenticate("Unlock Multiplex") { + if await authenticate(String(localized: "Unlock Multiplex")) { isLocked = false } } @@ -179,12 +179,12 @@ final class AppLockStore { let context = LAContext() guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: nil - ) else { return "device passcode" } + ) else { return String(localized: "device passcode") } switch context.biometryType { case .faceID: return "Face ID" case .touchID: return "Touch ID" case .opticID: return "Optic ID" - default: return "device passcode" + default: return String(localized: "device passcode") } } diff --git a/Multiplex/Services/AttentionCenter.swift b/Multiplex/Services/AttentionCenter.swift index 5c4cfa7d..9c510e65 100644 --- a/Multiplex/Services/AttentionCenter.swift +++ b/Multiplex/Services/AttentionCenter.swift @@ -312,16 +312,17 @@ final class AttentionCenter { } nonisolated static func copy(for alert: AttentionAlert) -> NotificationCopy { - let agent = alert.agent?.displayName ?? "Session" + let agent = alert.agent?.displayName ?? String(localized: "Session") let title: String = switch alert.event { case .turnEnded: - "\(agent) finished" + String(localized: "\(agent) finished") case .needsInput(.permission): - "\(agent) wants permission" + String(localized: "\(agent) wants permission") case .needsInput(.question): - "\(agent) has a question" + String(localized: "\(agent) has a question") case .bell: - alert.agent.map { "\($0.displayName) rang the bell" } ?? "Bell" + alert.agent.map { String(localized: "\($0.displayName) rang the bell") } + ?? String(localized: "Bell") } let place = "\(alert.sessionName) · \(alert.host.name)" // The dialog's own copy outranks the pane-title task summary, which diff --git a/Multiplex/Services/Bind/BindClient.swift b/Multiplex/Services/Bind/BindClient.swift index 6d150a47..ed6b6983 100644 --- a/Multiplex/Services/Bind/BindClient.swift +++ b/Multiplex/Services/Bind/BindClient.swift @@ -24,10 +24,10 @@ enum BindClient { var errorDescription: String? { switch self { - case .unreachable(let detail): "Couldn't reach the machine — \(detail)" + case .unreachable(let detail): String(localized: "Couldn't reach the machine — \(detail)") case .rejected(let reason): reason - case .wire(let detail): "The bind exchange broke off — \(detail)" - case .timedOut: "No answer from mpx bind — is it still running?" + case .wire(let detail): String(localized: "The bind exchange broke off — \(detail)") + case .timedOut: String(localized: "No answer from mpx bind — is it still running?") } } } @@ -144,10 +144,10 @@ enum BindClient { private static func rejectionMessage(_ raw: String?) -> String { switch raw { - case "wrong pin": "That PIN didn't match — check the terminal and try again." - case "declined on the host": "The machine declined the bind." - case .some(let reason): "The machine refused: \(reason)" - case nil: "The machine refused the bind." + case "wrong pin": String(localized: "That PIN didn't match — check the terminal and try again.") + case "declined on the host": String(localized: "The machine declined the bind.") + case .some(let reason): String(localized: "The machine refused: \(reason)") + case nil: String(localized: "The machine refused the bind.") } } diff --git a/Multiplex/Services/Bind/BindController.swift b/Multiplex/Services/Bind/BindController.swift index c9618ef3..ff9d886e 100644 --- a/Multiplex/Services/Bind/BindController.swift +++ b/Multiplex/Services/Bind/BindController.swift @@ -385,7 +385,7 @@ final class BindController { } else if let sealed = await Self.sealedOffMain(key: raw, passphrase: keyPassphrase) { storedPrivateKey = sealed } else { - fail(id: id, "Couldn’t seal the key with that passphrase — nothing was enrolled.") + fail(id: id, String(localized: "Couldn’t seal the key with that passphrase — nothing was enrolled.")) return } let device = Self.deviceName @@ -402,7 +402,7 @@ final class BindController { ) case .discovered(let announcement): guard let endpoint = discovery.endpoint(for: announcement) else { - fail(id: id, "That machine stopped announcing — run mpx bind again.") + fail(id: id, String(localized: "That machine stopped announcing — run mpx bind again.")) return } completion = try await BindClient.run( @@ -435,7 +435,7 @@ final class BindController { ) } catch { fail(id: id, (error as? LocalizedError)?.errorDescription - ?? "The bind didn’t complete. Run mpx bind again.") + ?? String(localized: "The bind didn’t complete. Run mpx bind again.")) } } @@ -465,7 +465,7 @@ final class BindController { guard let offline = payload.offline, let raw = try? Curve25519.Signing.PrivateKey(rawRepresentation: offline.seed) else { - fail(id: id, "That bind code is missing its key.") + fail(id: id, String(localized: "That bind code is missing its key.")) return } let key = BindSSHKey.make(from: raw) @@ -481,7 +481,7 @@ final class BindController { } else { guard let sealed = await Self.sealedOffMain(key: raw, passphrase: keyPassphrase) else { - fail(id: id, "Couldn’t seal the key with that passphrase — nothing was saved.") + fail(id: id, String(localized: "Couldn’t seal the key with that passphrase — nothing was saved.")) return } privateKey = sealed @@ -571,7 +571,9 @@ final class BindController { // The record and its key are saved either way — the host is on // the wall and can be fixed in Host Settings. Say what happened. log.debug("bind test connect failed: \(message, privacy: .public)") - setStage(id: id, .failed("Bound, but the first connection failed: \(message)")) + setStage(id: id, .failed( + String(localized: "Bound, but the first connection failed: \(message)") + )) } } diff --git a/Multiplex/Services/DictationSession.swift b/Multiplex/Services/DictationSession.swift index 931e0e61..3aa5639a 100644 --- a/Multiplex/Services/DictationSession.swift +++ b/Multiplex/Services/DictationSession.swift @@ -226,11 +226,11 @@ final class DictationSession { private func authorizeAndBegin() async { guard await Self.requestSpeechAuthorization() else { - deliver(.failure("Speech recognition access is off in Settings")) + deliver(.failure(String(localized: "Speech recognition access is off in Settings"))) return } guard await Self.requestMicrophoneAccess() else { - deliver(.failure("Microphone access is off in Settings")) + deliver(.failure(String(localized: "Microphone access is off in Settings"))) return } await begin() @@ -276,7 +276,7 @@ final class DictationSession { } catch { Self.logger.error("dictation-audio-failed \(error.localizedDescription, privacy: .public)") stopAudio() - deliver(.failure("The microphone couldn't be opened")) + deliver(.failure(String(localized: "The microphone couldn't be opened"))) return } @@ -295,7 +295,7 @@ final class DictationSession { guard onFinish != nil, !isListening else { return } if analyzerBox == nil { guard let recognizer = makeRecognizer(), recognizer.isAvailable else { - deliver(.failure("Dictation isn't available for this language")) + deliver(.failure(String(localized: "Dictation isn't available for this language"))) return } self.recognizer = recognizer @@ -307,7 +307,7 @@ final class DictationSession { } catch { Self.logger.error("dictation-audio-failed \(error.localizedDescription, privacy: .public)") stopAudio() - deliver(.failure("The microphone couldn't be opened")) + deliver(.failure(String(localized: "The microphone couldn't be opened"))) return } @@ -449,7 +449,7 @@ final class DictationSession { // and a bar that simply vanishes mid-dictation reads as the // feature quietly breaking. Self.logger.error("dictation-restart-exhausted") - deliver(.failure("Dictation stopped unexpectedly")) + deliver(.failure(String(localized: "Dictation stopped unexpectedly"))) return } scheduleSegment() @@ -570,7 +570,7 @@ final class DictationSession { onDeviceRecognition = recognizer?.supportsOnDeviceRecognition ?? false } guard recognizer?.isAvailable == true else { - deliver(.failure("Dictation isn't available for this language")) + deliver(.failure(String(localized: "Dictation isn't available for this language"))) return } beginSegment() @@ -623,7 +623,7 @@ final class DictationSession { // On STOP this is the expected ending, and everything heard was // finalized on the way out. Otherwise the analyzer gave up on its // own, which the pane says rather than leaving a dead LISTENING bar. - deliver(finishing || error == nil ? .ended : .failure("Dictation stopped unexpectedly")) + deliver(finishing || error == nil ? .ended : .failure(String(localized: "Dictation stopped unexpectedly"))) } // MARK: Audio diff --git a/Multiplex/Services/EntitlementStore.swift b/Multiplex/Services/EntitlementStore.swift index 56ac78e1..07b8f5e9 100644 --- a/Multiplex/Services/EntitlementStore.swift +++ b/Multiplex/Services/EntitlementStore.swift @@ -64,9 +64,9 @@ private enum ProStoreClientError: LocalizedError { var errorDescription: String? { switch self { case .missingProduct: - "Multiplex Pro is not available from the App Store right now." + String(localized: "Multiplex Pro is not available from the App Store right now.") case .missingPurchasePresenter: - "The App Store purchase sheet could not be presented from this window." + String(localized: "The App Store purchase sheet could not be presented from this window.") } } } @@ -453,7 +453,9 @@ final class EntitlementStore { guard let product = try await storeClient.loadProduct(), product.id == Self.proProductID else { Self.logger.error("product load returned no Pro product") - return .failed("Multiplex Pro is not available from the App Store right now.") + return .failed(String( + localized: "Multiplex Pro is not available from the App Store right now." + )) } return .loaded(product) } catch { @@ -519,7 +521,8 @@ final class EntitlementStore { } guard let product = proProduct else { return settlePurchase(withFallback: .failed( - productLoadError ?? "Multiplex Pro is not available from the App Store right now." + productLoadError + ?? String(localized: "Multiplex Pro is not available from the App Store right now.") )) } @@ -530,13 +533,13 @@ final class EntitlementStore { case .success(.unverified): Self.logger.error("purchase returned an unverified transaction") return settlePurchase(withFallback: .failed( - "The App Store transaction could not be verified." + String(localized: "The App Store transaction could not be verified.") )) case .success(.verified(let transaction)): guard transactionGrantsPro(transaction) else { return settlePurchase(withFallback: .failed( - "The purchase is not an active Multiplex Pro entitlement." + String(localized: "The purchase is not an active Multiplex Pro entitlement.") )) } advanceEntitlementAuthority() @@ -567,7 +570,7 @@ final class EntitlementStore { case .unknown: return settlePurchase(withFallback: .failed( - "The App Store returned an unknown purchase result." + String(localized: "The App Store returned an unknown purchase result.") )) } } catch { @@ -821,7 +824,9 @@ final class EntitlementStore { switch (wasAwaitingApproval, commerceState) { case (true, _): commerceState = .failed( - "The pending purchase did not grant an active Multiplex Pro entitlement." + String( + localized: "The pending purchase did not grant an active Multiplex Pro entitlement." + ) ) case (false, .purchased): commerceState = .idle @@ -836,7 +841,9 @@ final class EntitlementStore { ) guard productID == Self.proProductID, purchaseAwaitingApproval else { continue } purchaseAwaitingApproval = false - commerceState = .failed("The App Store transaction could not be verified.") + commerceState = .failed( + String(localized: "The App Store transaction could not be verified.") + ) } } } @@ -879,22 +886,22 @@ final class EntitlementStore { static func storeErrorMessage(_ error: Error) -> String { switch error { case StoreKitError.networkError: - return "The App Store could not be reached. Check the internet " - + "connection and try again." + return String(localized: """ + The App Store could not be reached. Check the internet \ + connection and try again. + """) case StoreKitError.systemError(let underlying): - return "The App Store reported a system error. " - + "(\(underlying.localizedDescription))" + return String(localized: "The App Store reported a system error. (\(underlying.localizedDescription))") case StoreKitError.notAvailableInStorefront: - return "Multiplex Pro is not available in this App Store storefront." + return String(localized: "Multiplex Pro is not available in this App Store storefront.") case StoreKitError.notEntitled: - return "This copy of the app is not entitled to App Store purchases." + return String(localized: "This copy of the app is not entitled to App Store purchases.") case StoreKitError.unknown: - return "The App Store could not complete the request. " - + "Try again in a moment." + return String(localized: "The App Store could not complete the request. Try again in a moment.") case Product.PurchaseError.purchaseNotAllowed: - return "Purchases are not allowed for this Apple ID on this device." + return String(localized: "Purchases are not allowed for this Apple ID on this device.") case Product.PurchaseError.productUnavailable: - return "Multiplex Pro is not available for purchase right now." + return String(localized: "Multiplex Pro is not available for purchase right now.") default: // An error with no authored description renders as the opaque // "operation couldn't be completed (SKInternalErrorDomain error @@ -904,9 +911,10 @@ final class EntitlementStore { let nsError = error as NSError if nsError.userInfo[NSLocalizedDescriptionKey] == nil, (error as? LocalizedError)?.errorDescription == nil { - return "The App Store could not complete the request. Check " - + "that this device is signed in to the App Store, then " - + "try again. (\(nsError.domain) \(nsError.code))" + return String(localized: """ + The App Store could not complete the request. Check that this device is \ + signed in to the App Store, then try again. (\(nsError.domain) \(nsError.code)) + """) } return error.localizedDescription } diff --git a/Multiplex/Services/ExternalActionRouter.swift b/Multiplex/Services/ExternalActionRouter.swift index e05f8eb7..17f050a4 100644 --- a/Multiplex/Services/ExternalActionRouter.swift +++ b/Multiplex/Services/ExternalActionRouter.swift @@ -126,7 +126,10 @@ enum ExternalActionPerformer { guard let host = resolveHost(action.hostRef, in: context.store) else { context.presentFailure(ExternalActionFailure( hostName: action.hostRef.displayName, - message: "No configured host matches this shortcut. Pick a host in the widget or shortcut settings." + message: String(localized: """ + No configured host matches this shortcut. Pick a host in the \ + widget or shortcut settings. + """) )) return } @@ -137,7 +140,10 @@ enum ExternalActionPerformer { guard host.isEnabled else { context.presentFailure(ExternalActionFailure( hostName: host.name, - message: "\(host.name) is disabled. Enable it on the deck to open sessions on it." + message: String(localized: """ + \(host.name) is disabled. Enable it on the deck to open \ + sessions on it. + """) )) return } @@ -351,7 +357,8 @@ enum ExternalActionPerformer { guard let target = TerminalPathTarget.resolveExplicit(path, line: line) else { context.presentFailure(ExternalActionFailure( hostName: host.name, - message: "Enter a remote file path and an optional positive line number." + message: String( + localized: "Enter a remote file path and an optional positive line number.") )) return } @@ -404,15 +411,16 @@ enum ExternalActionPerformer { // The RESOLVED session's backend names the thing that didn't // happen, not the host's primary. switch (backend, placement) { - case (.tmux, _): noun = "window" - case (.herdr, .tab): noun = "tab" - case (.herdr, .workspace): noun = "workspace" + case (.tmux, _): noun = String(localized: "window") + case (.herdr, .tab): noun = String(localized: "tab") + case (.herdr, .workspace): noun = String(localized: "workspace") } let message: String if case .failed(let reason) = model.phase { message = reason } else { - message = "Couldn't open a new \(noun) in session \(session) on \(host.name)." + message = String( + localized: "Couldn't open a new \(noun) in session \(session) on \(host.name).") } context.presentFailure(ExternalActionFailure(hostName: host.name, message: message)) } @@ -434,7 +442,7 @@ enum ExternalActionPerformer { if case .failed(let reason) = model.phase { message = reason } else { - message = "Couldn't create the session on \(host.name)." + message = String(localized: "Couldn't create the session on \(host.name).") } } context.presentFailure(ExternalActionFailure(hostName: host.name, message: message)) @@ -443,7 +451,7 @@ enum ExternalActionPerformer { private static func failureMessage(for phase: HostConnectionModel.Phase, host: Host) -> String { switch phase { case .failed(let reason): reason - default: "Couldn't reach \(host.name). No response." + default: String(localized: "Couldn't reach \(host.name). No response.") } } } diff --git a/Multiplex/Services/FileViewerController.swift b/Multiplex/Services/FileViewerController.swift index 2b87e172..e233f540 100644 --- a/Multiplex/Services/FileViewerController.swift +++ b/Multiplex/Services/FileViewerController.swift @@ -280,10 +280,12 @@ final class FileViewerController: AuxiliaryPaneController { let name = FileTree.name(of: path) do { let stat = try await withConnection { try await $0.statFile(atPath: path) } - if stat.isDirectory { return .failed("\(name.uppercased()) IS A FOLDER") } + if stat.isDirectory { + return .failed(String(localized: "\(name.uppercased()) IS A FOLDER")) + } let size = stat.size ?? 0 guard size <= UInt64(Self.imageByteLimit) else { - return .failed("TOO LARGE — \(Self.formatBytes(size))") + return .failed(String(localized: "TOO LARGE — \(Self.formatBytes(size))")) } let (data, _) = try await withConnection { try await $0.readFile(atPath: path, limit: Self.imageByteLimit) @@ -294,7 +296,7 @@ final class FileViewerController: AuxiliaryPaneController { guard let image = Self.decodeImage( data, maxPixelEdge: Self.inlineImageMaxPixelEdge ) else { - return .failed("NOT AN IMAGE THIS VIEWER CAN DRAW") + return .failed(String(localized: "NOT AN IMAGE THIS VIEWER CAN DRAW")) } return .ready(image) } catch { @@ -723,7 +725,7 @@ final class FileViewerController: AuxiliaryPaneController { content = .failure(title: refusal.title, message: refusal.message) } else { content = .failure( - title: "CAN'T READ \(name.uppercased())", + title: String(localized: "CAN'T READ \(name.uppercased())"), message: failureMessage(error) ) } @@ -763,7 +765,8 @@ final class FileViewerController: AuxiliaryPaneController { break case .pdf: let data = try await readWhole( - path: path, name: name, size: size, limit: Self.mediaByteLimit, noun: "PDFs" + path: path, name: name, size: size, limit: Self.mediaByteLimit, + noun: String(localized: "PDFs") ) // The xref walk of a scanned book is real work — off the main // actor like the highlighter; pages then render lazily on screen. @@ -776,7 +779,8 @@ final class FileViewerController: AuxiliaryPaneController { } case .audio: let data = try await readWhole( - path: path, name: name, size: size, limit: Self.mediaByteLimit, noun: "sound files" + path: path, name: name, size: size, limit: Self.mediaByteLimit, + noun: String(localized: "sound files") ) // A decode failure keeps the AUDIO verdict: the screen says // CAN'T PLAY and why, where BINARY would only say "not text". @@ -785,7 +789,8 @@ final class FileViewerController: AuxiliaryPaneController { ) case .image: let data = try await readWhole( - path: path, name: name, size: size, limit: Self.imageByteLimit, noun: "images" + path: path, name: name, size: size, limit: Self.imageByteLimit, + noun: String(localized: "images") ) // Undecodable "image" bytes are binary content by any honest // reading — same verdict as the NUL sniff below. Decoding goes @@ -833,7 +838,10 @@ final class FileViewerController: AuxiliaryPaneController { guard size <= UInt64(limit) else { throw LoadRefusal( title: "TOO LARGE", - message: "\(name) is \(Self.formatBytes(size)) — the viewer opens \(noun) up to \(Self.formatBytes(UInt64(limit)))." + message: String(localized: """ + \(name) is \(Self.formatBytes(size)) — the viewer opens \(noun) up to \ + \(Self.formatBytes(UInt64(limit))). + """) ) } let (data, _) = try await withConnection { @@ -959,7 +967,7 @@ final class FileViewerController: AuxiliaryPaneController { } catch { guard generation == contentGeneration else { return } content = .failure( - title: "CAN'T DIFF \(emptyLabel.uppercased())", + title: String(localized: "CAN'T DIFF \(emptyLabel.uppercased())"), message: failureMessage(error) ) } @@ -1094,7 +1102,7 @@ final class FileViewerController: AuxiliaryPaneController { case .document(let document) = content { content = .failure( title: "FILE GONE", - message: "The host says \(document.name) no longer exists." + message: String(localized: "The host says \(document.name) no longer exists.") ) } } @@ -1142,10 +1150,13 @@ final class FileViewerController: AuxiliaryPaneController { // The stock copy asks for the passphrase; the viewer has no // prompt to offer, so point at the two places that can // unlock the key instead. - return "The host's key is sealed. Set its passphrase in " - + "Host Settings, or open a terminal to this host first." + return String(localized: """ + The host's key is sealed. Set its passphrase in Host Settings, or open \ + a terminal to this host first. + """) case .notConnected: - return connectionError.userMessage(host: host) + " REFRESH dials again." + return connectionError.userMessage(host: host) + + " " + String(localized: "REFRESH dials again.") case .missingCredentials, .unsupportedKey, .connectFailed, .hostKeyRefused: // One copy source for connection failures, app-wide. return connectionError.userMessage(host: host) @@ -1153,10 +1164,10 @@ final class FileViewerController: AuxiliaryPaneController { } let message = "\(error)" if message.localizedCaseInsensitiveContains("no such file") { - return "The host says there is no such file." + return String(localized: "The host says there is no such file.") } if message.localizedCaseInsensitiveContains("permission denied") { - return "The host refused: permission denied." + return String(localized: "The host refused: permission denied.") } return message } diff --git a/Multiplex/Services/HerdrProbe.swift b/Multiplex/Services/HerdrProbe.swift index 0c9c71cb..c5d3db1a 100644 --- a/Multiplex/Services/HerdrProbe.swift +++ b/Multiplex/Services/HerdrProbe.swift @@ -223,7 +223,7 @@ enum HerdrProbe { static func parseProbe(_ output: String) -> ParsedProbe { var result = ParsedProbe( - state: .failed("unreadable herdr probe response"), + state: .failed(String(localized: "unreadable herdr probe response")), tails: [:], miniatures: [:], paneStatuses: [:], tailTargets: [], sessionNames: [] ) @@ -255,14 +255,16 @@ enum HerdrProbe { if let clientProtocol = status?.client.protocolVersion, clientProtocol < minimumProtocol { result.state = .updateNeeded( - installedVersion: status?.client.version ?? "unknown") + installedVersion: status?.client.version ?? String(localized: "unknown")) return result } let region = sessionsRegion(lines) guard let list = region.list else { if status != nil { - result.state = .failed("herdr answered status but not the session list") + result.state = .failed( + String(localized: "herdr answered status but not the session list") + ) } return result } @@ -634,7 +636,9 @@ enum HerdrProbe { /// names in tmux-land too — duplicates are fine, only emptiness isn't. private static func displayLabel(_ workspace: Workspace) -> String { let label = workspace.label.trimmingCharacters(in: .whitespaces) - return label.isEmpty ? "workspace \(workspace.number)" : label + return label.isEmpty + ? String(localized: "workspace \(workspace.number)") + : label } /// The one pane a session fronts — its focused pane, falling back @@ -1314,7 +1318,9 @@ extension HerdrProbe.State { case .herdrMissing: .tmuxMissing case .noServer: .noServer case .updateNeeded(let version): - .failed("herdr \(version) is older than this app speaks — update herdr on the host") + .failed(String( + localized: "herdr \(version) is older than this app speaks — update herdr on the host" + )) case .sessions(let sessions): .sessions(sessions) case .failed(let message): .failed(message) } diff --git a/Multiplex/Services/HostTest.swift b/Multiplex/Services/HostTest.swift index cacd330c..0c2b5c9a 100644 --- a/Multiplex/Services/HostTest.swift +++ b/Multiplex/Services/HostTest.swift @@ -88,38 +88,44 @@ enum HostTest { switch ssh { case .missingCredentials: return host.authMethod == .password - ? "Enter a password first." - : "Paste a private key first." + ? String(localized: "Enter a password first.") + : String(localized: "Paste a private key first.") case .keyPassphraseRequired: - return "This private key is encrypted. Enter its passphrase above." + return String(localized: "This private key is encrypted. Enter its passphrase above.") case .incorrectKeyPassphrase: - return "That passphrase didn't unlock the private key. Try again." + return String(localized: "That passphrase didn't unlock the private key. Try again.") case .unsupportedKey: - return "The private key couldn't be read. Paste an OpenSSH ed25519 " - + "or RSA key, including its BEGIN/END lines." + return String(localized: """ + The private key couldn't be read. Paste an OpenSSH ed25519 \ + or RSA key, including its BEGIN/END lines. + """) case .connectFailed(let detail): return connectFailureMessage(detail, host: host) case .notConnected: - return "The connection closed before the check finished. Try again." + return String(localized: "The connection closed before the check finished. Try again.") case .hostKeyRefused(let refusal): // Named rather than placed: Host key is the last section in // the form, not the next one down, so "below" would send the // reader looking in the wrong place. switch refusal { case .changed(let expected, let presented): - return "\(host.hostname) presented a different host key than the one " - + "recorded for this host — \(expected.fingerprint), got " - + "\(presented.fingerprint). Nothing was sent. If you rebuilt the " - + "server, forget the recorded key under Host key and check again." + return String(localized: """ + \(host.hostname) presented a different host key than the one \ + recorded for this host — \(expected.fingerprint), got \ + \(presented.fingerprint). Nothing was sent. If you rebuilt the \ + server, forget the recorded key under Host key and check again. + """) case .unrecognizedAlgorithm(let presented, _): - return "\(host.hostname) identified itself with a \(presented.algorithm) " - + "key, which isn't recorded for this host. Nothing was sent. If its " - + "host keys changed, forget the recorded keys under Host key and check again." + return String(localized: """ + \(host.hostname) identified itself with a \(presented.algorithm) \ + key, which isn't recorded for this host. Nothing was sent. If its \ + host keys changed, forget the recorded keys under Host key and check again. + """) } } } if error is DeadlineExceeded { - return "No answer from \(host.hostname) after \(Int(connectDeadline)) seconds — check the address and port, and that the host is reachable from this network." + return String(localized: "No answer from \(host.hostname) after \(Int(connectDeadline)) seconds — check the address and port, and that the host is reachable from this network.") } return connectFailureMessage(String(describing: error), host: host) } @@ -131,28 +137,28 @@ enum HostTest { let lower = detail.lowercased() if lower.contains("authentication") || lower.contains("permission denied") { return host.authMethod == .password - ? "\(host.hostname) rejected the sign-in — check the user name and password." - : "\(host.hostname) rejected the key — check the user name, and that the key's public half is in ~/.ssh/authorized_keys on the host." + ? String(localized: "\(host.hostname) rejected the sign-in — check the user name and password.") + : String(localized: "\(host.hostname) rejected the key — check the user name, and that the key's public half is in ~/.ssh/authorized_keys on the host.") } if lower.contains("connection refused") || lower.contains("econnrefused") { - return "\(host.hostname) refused the connection on port \(host.port) — is an SSH server listening there?" + return String(localized: "\(host.hostname) refused the connection on port \(String(host.port)) — is an SSH server listening there?") } if lower.contains("dnsaerror") || lower.contains("dnsaaaaerror") || lower.contains("nodename nor servname") || lower.contains("name or service not known") { - return "Couldn't find “\(host.hostname)” — check the address for typos." + return String(localized: "Couldn't find “\(host.hostname)” — check the address for typos.") } if lower.contains("timed out") || lower.contains("timeout") { - return "\(host.hostname) didn't answer — check the port, and any firewall between this device and the host." + return String(localized: "\(host.hostname) didn't answer — check the port, and any firewall between this device and the host.") } if lower.contains("network is unreachable") || lower.contains("enetunreach") || lower.contains("no route to host") || lower.contains("ehostunreach") { - return "No route to \(host.hostname) — check this device's network connection." + return String(localized: "No route to \(host.hostname) — check this device's network connection.") } if lower.contains("connection reset") || lower.contains("econnreset") { - return "\(host.hostname) dropped the connection — the port may not speak SSH." + return String(localized: "\(host.hostname) dropped the connection — the port may not speak SSH.") } - return "Couldn't connect to \(host.hostname): \(detail)" + return String(localized: "Couldn't connect to \(host.hostname): \(detail)") } } diff --git a/Multiplex/Services/Mosh/MoshBootstrap.swift b/Multiplex/Services/Mosh/MoshBootstrap.swift index afc02fb7..3eacdaa4 100644 --- a/Multiplex/Services/Mosh/MoshBootstrap.swift +++ b/Multiplex/Services/Mosh/MoshBootstrap.swift @@ -8,11 +8,11 @@ enum MoshBootstrapError: Error { func userMessage(host: Host) -> String { switch self { case .dnsFailure: - "Couldn't resolve \(host.hostname)." + String(localized: "Couldn't resolve \(host.hostname).") case .sshFailed(let detail): - "Couldn't reach \(host.name) to start mosh (\(detail))." + String(localized: "Couldn't reach \(host.name) to start mosh (\(detail)).") case .serverFailed(let detail): - "mosh-server didn't start on \(host.name): \(detail)" + String(localized: "mosh-server didn't start on \(host.name): \(detail)") } } } @@ -97,7 +97,7 @@ enum MoshBootstrap { .filter { !$0.isEmpty && !$0.hasPrefix("[mosh-server") } let tail = lines.suffix(2).joined(separator: " · ") let clipped = tail.count > 160 ? String(tail.prefix(160)) + "…" : tail - return clipped.isEmpty ? "no output" : clipped + return clipped.isEmpty ? String(localized: "no output") : clipped } // MARK: - Resolution @@ -214,6 +214,6 @@ enum MoshBootstrap { } throw MoshBootstrapError.serverFailed(failureDetail(output)) } - throw MoshBootstrapError.serverFailed("gave up after fallbacks") + throw MoshBootstrapError.serverFailed(String(localized: "gave up after fallbacks")) } } diff --git a/Multiplex/Services/Mosh/MoshSession.swift b/Multiplex/Services/Mosh/MoshSession.swift index 827c30af..c8c5d47e 100644 --- a/Multiplex/Services/Mosh/MoshSession.swift +++ b/Multiplex/Services/Mosh/MoshSession.swift @@ -18,9 +18,12 @@ actor MoshSession: TerminalTransport { func userMessage(host: Host) -> String { switch self { case .noResponse: - "mosh-server started on \(host.name) but no UDP reply arrived. Check firewalls/NAT for the mosh port range (default 60000-61000)." + String(localized: """ + mosh-server started on \(host.name) but no UDP reply arrived. \ + Check firewalls/NAT for the mosh port range (default 60000-61000). + """) case .alreadyOpen: - "This mosh session is already open." + String(localized: "This mosh session is already open.") } } } @@ -328,7 +331,7 @@ actor MoshSession: TerminalTransport { return } if engine.isDesynced(now: now) { - finish(reason: "mosh session lost sync", notify: true) + finish(reason: String(localized: "mosh session lost sync"), notify: true) return } updateContact(now: now) diff --git a/Multiplex/Services/SSHConnection.swift b/Multiplex/Services/SSHConnection.swift index d49363fc..4015acc0 100644 --- a/Multiplex/Services/SSHConnection.swift +++ b/Multiplex/Services/SSHConnection.swift @@ -177,34 +177,34 @@ enum SSHConnectionError: Error { func userMessage(host: Host) -> String { switch self { case .missingCredentials: - "No saved credentials for \(host.name). Edit the host and sign in again." + String(localized: "No saved credentials for \(host.name). Edit the host and sign in again.") case .keyPassphraseRequired: - "The private key for \(host.name) is encrypted. Enter its passphrase to connect." + String(localized: "The private key for \(host.name) is encrypted. Enter its passphrase to connect.") case .incorrectKeyPassphrase: - "The passphrase didn't unlock the private key for \(host.name). Try again." + String(localized: "The passphrase didn't unlock the private key for \(host.name). Try again.") case .unsupportedKey: - "The private key for \(host.name) couldn't be read. Paste an OpenSSH ed25519 or RSA key." + String(localized: "The private key for \(host.name) couldn't be read. Paste an OpenSSH ed25519 or RSA key.") case .connectFailed(let detail): - "Couldn't reach \(host.name) (\(detail))." + String(localized: "Couldn't reach \(host.name) (\(detail)).") case .notConnected: - "Not connected to \(host.name)." + String(localized: "Not connected to \(host.name).") case .hostKeyRefused(let refusal): switch refusal { case .changed(let expected, let presented): - """ - \(host.name) presented a different SSH host key than the one \ - Multiplex recorded. Either the server was rebuilt, or something \ - is impersonating it. Nothing was sent. \ - Recorded \(expected.fingerprint), got \(presented.fingerprint). \ - If you rebuilt it, forget the recorded key in Host Settings. - """ + String(localized: """ + \(host.name) presented a different SSH host key than the one \ + Multiplex recorded. Either the server was rebuilt, or something \ + is impersonating it. Nothing was sent. \ + Recorded \(expected.fingerprint), got \(presented.fingerprint). \ + If you rebuilt it, forget the recorded key in Host Settings. + """) case .unrecognizedAlgorithm(let presented, _): - """ - \(host.name) identified itself with a \(presented.algorithm) key, \ - which isn't among the keys Multiplex recorded for it. \ - Nothing was sent. If the server's host keys changed, forget the \ - recorded keys in Host Settings. - """ + String(localized: """ + \(host.name) identified itself with a \(presented.algorithm) key, \ + which isn't among the keys Multiplex recorded for it. \ + Nothing was sent. If the server's host keys changed, forget the \ + recorded keys in Host Settings. + """) } } } @@ -497,7 +497,7 @@ actor SSHConnection { } } guard let file else { - throw DropError(message: "Couldn't create \(upload.preferredName)") + throw DropError(message: String(localized: "Couldn't create \(upload.preferredName)")) } do { let chunkSize = 512 * 1024 diff --git a/Multiplex/Services/TerminalSessionController.swift b/Multiplex/Services/TerminalSessionController.swift index 68df493c..6c9a1190 100644 --- a/Multiplex/Services/TerminalSessionController.swift +++ b/Multiplex/Services/TerminalSessionController.swift @@ -470,7 +470,7 @@ final class TerminalSessionController { else { return } captureKeyPassphraseChallenge(from: error) let message = (error as? SSHConnectionError)?.userMessage(host: host) - ?? "Couldn't reach \(host.name). \(error.localizedDescription)" + ?? String(localized: "Couldn't reach \(host.name). \(error.localizedDescription)") status = .ended(message) self.connection = nil transport = nil @@ -548,7 +548,12 @@ final class TerminalSessionController { let message = (error as? MoshBootstrapError)?.userMessage(host: host) ?? (error as? MoshSession.Failure)?.userMessage(host: host) ?? (error as? SSHConnectionError)?.userMessage(host: host) - ?? "Couldn't reach \(host.name) over mosh. \(error.localizedDescription)" + ?? String( + localized: """ + Couldn't reach \(host.name) over mosh. \ + \(error.localizedDescription) + """ + ) status = .ended(message) moshSession = nil transport = nil @@ -1464,11 +1469,11 @@ final class TerminalSessionController { } guard !Task.isCancelled, agentHistory != nil else { return } guard let result else { - agentHistory = .unavailable("NO WORKING DIRECTORY") + agentHistory = .unavailable(String(localized: "NO WORKING DIRECTORY")) return } guard result.filePath != nil else { - agentHistory = .unavailable("NO SESSION FILE") + agentHistory = .unavailable(String(localized: "NO SESSION FILE")) return } agentHistory = .loaded( @@ -1479,7 +1484,7 @@ final class TerminalSessionController { ) } catch { guard !Task.isCancelled, agentHistory != nil else { return } - agentHistory = .unavailable("HISTORY UNAVAILABLE") + agentHistory = .unavailable(String(localized: "HISTORY UNAVAILABLE")) } } @@ -1547,7 +1552,7 @@ final class TerminalSessionController { guard let prologue = isHerdr ? AgentSessionHistory.parseHerdrJumpPrologue(prologueOutput) : AgentSessionHistory.parseJumpPrologue(prologueOutput) - else { return .failed("SESSION NOT FOUND") } + else { return .failed(String(localized: "SESSION NOT FOUND")) } // The walk's coordinate: tmux aims at the session (its // active pane answers); herdr aims at the prologue's // bake-vetted focused pane. @@ -1557,15 +1562,15 @@ final class TerminalSessionController { switch AgentAttention.classify( title: prologue.paneTitle, tail: prologue.capture ) { - case .busy: return .failed("AGENT IS BUSY") - case .needsYou: return .failed("ANSWER THE AGENT FIRST") + case .busy: return .failed(String(localized: "AGENT IS BUSY")) + case .needsYou: return .failed(String(localized: "ANSWER THE AGENT FIRST")) case .idle: break } let targetNeedles = AgentSessionHistory.needles( for: message, paneColumns: prologue.paneWidth ) guard let targetPrimary = targetNeedles.first else { - return .failed("MESSAGE TOO SHORT TO FIND") + return .failed(String(localized: "MESSAGE TOO SHORT TO FIND")) } let entries = AgentSessionHistory.needleEntries( for: allMessages, paneColumns: prologue.paneWidth @@ -1622,23 +1627,23 @@ final class TerminalSessionController { // the needles were built against), so name it. return .failed( prologue.clientSizeCount > 1 - ? "ANOTHER CLIENT RESIZES THIS SESSION" - : "NOT IN THE VISIBLE TRANSCRIPT" + ? String(localized: "ANOTHER CLIENT RESIZES THIS SESSION") + : String(localized: "NOT IN THE VISIBLE TRANSCRIPT") ) case .short: - return .failed("TERMINAL TOO SHORT TO JUMP") + return .failed(String(localized: "TERMINAL TOO SHORT TO JUMP")) case .resized: return .failed( prologue.clientSizeCount > 1 - ? "ANOTHER CLIENT RESIZES THIS SESSION" - : "RESIZED MID-JUMP — TRY AGAIN" + ? String(localized: "ANOTHER CLIENT RESIZES THIS SESSION") + : String(localized: "RESIZED MID-JUMP — TRY AGAIN") ) case nil: - return .failed("SEARCH FAILED") + return .failed(String(localized: "SEARCH FAILED")) } } } catch { - outcome = .failed("SEARCH FAILED") + outcome = .failed(String(localized: "SEARCH FAILED")) } switch outcome { @@ -1765,7 +1770,8 @@ final class TerminalSessionController { !host.useMosh && route.sessionBackend != nil } - static let uploadUnavailableMessage = "File upload requires tmux or herdr over SSH" + static let uploadUnavailableMessage = + String(localized: "File upload requires tmux or herdr over SSH") func deliverDrop(_ files: [DroppedFile]) { // The jump search owns the pane's input while it pages; the FINDING @@ -1810,7 +1816,7 @@ final class TerminalSessionController { } catch is CancellationError { dropState = nil } catch { - dropState = .failed((error as? DropError)?.message ?? "Upload failed") + dropState = .failed((error as? DropError)?.message ?? String(localized: "Upload failed")) dropClearTask = Task { [weak self] in try? await Task.sleep(for: .seconds(5)) guard !Task.isCancelled else { return } @@ -1845,7 +1851,7 @@ final class TerminalSessionController { ) async throws -> [String] { let uploads = try files.map { file -> SSHUpload in guard file.data.count <= DropText.maxBytes else { - throw DropError(message: "\(file.name) is over 64 MB") + throw DropError(message: String(localized: "\(file.name) is over 64 MB")) } return SSHUpload( data: file.data, @@ -2113,7 +2119,7 @@ final class TerminalSessionController { return } guard status == .live, let connection else { - failTalkbackAttachment(id, "Not connected") + failTalkbackAttachment(id, String(localized: "Not connected")) return } do { @@ -2132,12 +2138,16 @@ final class TerminalSessionController { } } } - guard let path = paths.first else { throw DropError(message: "Upload failed") } + guard let path = paths.first else { + throw DropError(message: String(localized: "Upload failed")) + } talkback.update(id) { $0.state = .ready(path: path) } } catch is CancellationError { - failTalkbackAttachment(id, "Cancelled") + failTalkbackAttachment(id, String(localized: "Cancelled")) } catch { - failTalkbackAttachment(id, (error as? DropError)?.message ?? "Upload failed") + failTalkbackAttachment( + id, (error as? DropError)?.message ?? String(localized: "Upload failed") + ) } } diff --git a/Multiplex/Shared/AgentChoice.swift b/Multiplex/Shared/AgentChoice.swift index 0bee9748..a9e561dc 100644 --- a/Multiplex/Shared/AgentChoice.swift +++ b/Multiplex/Shared/AgentChoice.swift @@ -1,4 +1,5 @@ import AppIntents +import Foundation /// The CLI agent as an intent/widget-configuration parameter. Raw values /// mirror `AgentKind` one-to-one (unit-tested) so the app maps with @@ -42,7 +43,7 @@ enum AgentModelChoices { /// "Agent Default" leads even when the host has nothing configured — /// which is also the honest way to say where configuration lives. static func choices(configured: [String]) -> [Choice] { - var choices = [Choice(value: agentDefaultValue, title: "Agent Default")] + var choices = [Choice(value: agentDefaultValue, title: String(localized: "Agent Default"))] choices += values(configured: configured).map { Choice(value: $0, title: $0) } diff --git a/Multiplex/Shared/SessionTargetChoices.swift b/Multiplex/Shared/SessionTargetChoices.swift index 2031c510..7c4b8bb8 100644 --- a/Multiplex/Shared/SessionTargetChoices.swift +++ b/Multiplex/Shared/SessionTargetChoices.swift @@ -41,7 +41,7 @@ enum SessionTargetChoices { let backends = (backendsRaw ?? []).filter { !$0.isEmpty } guard backends.count > 1 else { return [] } var choices = [ - Choice(value: hostDefaultBackendValue, title: "Host Default"), + Choice(value: hostDefaultBackendValue, title: String(localized: "Host Default")), ] var seen = Set() for backend in backends where seen.insert(backend).inserted { @@ -54,7 +54,7 @@ enum SessionTargetChoices { /// query flash-dismisses the picker) and the published snapshot's /// session names follow, deduped in the snapshot's own order. static func sessionChoices(names: [String]) -> [Choice] { - var choices = [Choice(value: newSessionValue, title: "New Session")] + var choices = [Choice(value: newSessionValue, title: String(localized: "New Session"))] var seen = Set() for raw in names { let name = raw.trimmingCharacters(in: .whitespacesAndNewlines) @@ -74,10 +74,10 @@ enum SessionTargetChoices { static func placementChoices(backendRaw: String?) -> [Choice] { if backendRaw == herdrBackendRaw { return [ - Choice(value: "tab", title: "New Tab (Focused Workspace)"), - Choice(value: "workspace", title: "New Workspace"), + Choice(value: "tab", title: String(localized: "New Tab (Focused Workspace)")), + Choice(value: "workspace", title: String(localized: "New Workspace")), ] } - return [Choice(value: "window", title: "New Window")] + return [Choice(value: "window", title: String(localized: "New Window"))] } } diff --git a/Multiplex/Shared/ShortcutWorkingDirectoryOptions.swift b/Multiplex/Shared/ShortcutWorkingDirectoryOptions.swift index a0e68336..27661a13 100644 --- a/Multiplex/Shared/ShortcutWorkingDirectoryOptions.swift +++ b/Multiplex/Shared/ShortcutWorkingDirectoryOptions.swift @@ -36,11 +36,11 @@ enum ShortcutWorkingDirectoryOptions { /// query flash-dismisses the picker), then the configured dirs, then /// Home titled as such. static func choices(configured: [String]) -> [Choice] { - var choices = [Choice(value: hostDefaultValue, title: "Host Default")] + var choices = [Choice(value: hostDefaultValue, title: String(localized: "Host Default"))] choices += values(configured: configured).dropLast().map { Choice(value: $0, title: $0) } - choices.append(Choice(value: "~", title: "Home")) + choices.append(Choice(value: "~", title: String(localized: "Home"))) return choices } } diff --git a/Multiplex/Views/AppLockVeil.swift b/Multiplex/Views/AppLockVeil.swift index b168c2e1..6faf63ee 100644 --- a/Multiplex/Views/AppLockVeil.swift +++ b/Multiplex/Views/AppLockVeil.swift @@ -190,7 +190,7 @@ final class AppLockView: UIView { lamp.alignment = .center lamp.spacing = Metrics.lampSpacing lamp.isAccessibilityElement = true - lamp.accessibilityLabel = "locked" + lamp.accessibilityLabel = String(localized: "locked") configure( titleLabel, @@ -199,8 +199,9 @@ final class AppLockView: UIView { kerning: 15 * scale * 0.09, color: TallyPalette.signal ) - titleLabel.accessibilityLabel = - "Multiplex is locked. Unlock with \(AppLockStore.methodName)." + titleLabel.accessibilityLabel = String( + localized: "Multiplex is locked. Unlock with \(AppLockStore.methodName)." + ) configure( methodLabel, @@ -256,8 +257,8 @@ final class AppLockView: UIView { effect: .highlight, shape: .rect(cornerRadius: 2) ) - unlockButton.accessibilityLabel = "Unlock" - unlockButton.accessibilityHint = "Unlocks Multiplex with \(AppLockStore.methodName)" + unlockButton.accessibilityLabel = String(localized: "Unlock") + unlockButton.accessibilityHint = String(localized: "Unlocks Multiplex with \(AppLockStore.methodName)") unlockButton.addTarget(self, action: #selector(unlockPressed), for: .touchUpInside) } diff --git a/Multiplex/Views/Deck/AddHostSheet.swift b/Multiplex/Views/Deck/AddHostSheet.swift index 18981d61..3b47fb90 100644 --- a/Multiplex/Views/Deck/AddHostSheet.swift +++ b/Multiplex/Views/Deck/AddHostSheet.swift @@ -408,7 +408,9 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, override func viewDidLoad() { super.viewDidLoad() - title = form.editing == nil ? "Add Host" : "Host Settings" + title = form.editing == nil + ? String(localized: "Add Host") + : String(localized: "Host Settings") view.backgroundColor = GlassPrototype.sheetGround configureNavigation() configureScrollView() @@ -477,11 +479,13 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private var modeDetail: String { switch resolvedMode { case .bind: - return "Run the mpx CLI on the machine you're adding and it offers " - + "itself — no address, user, or key to type here. Can't " - + "install it? Switch to MANUAL." + return String(localized: """ + Run the mpx CLI on the machine you're adding and it offers \ + itself — no address, user, or key to type here. Can't \ + install it? Switch to MANUAL. + """) case .manual: - return "Type the SSH destination yourself. Nothing to install on the machine." + return String(localized: "Type the SSH destination yourself. Nothing to install on the machine.") } } @@ -491,7 +495,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, navigationItem.titleView = UIKitChassisLabel(title ?? "", size: 12) #endif cancellationItem = UIBarButtonItem( - title: "Cancel", + title: String(localized: "Cancel"), style: .plain, target: self, action: #selector(cancelPressed) @@ -500,12 +504,12 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, navigationItem.leftBarButtonItem = cancellationItem let save = UIBarButtonItem( - title: "Save", + title: String(localized: "Save"), style: .plain, target: self, action: #selector(savePressed) ) - save.accessibilityLabel = "Save" + save.accessibilityLabel = String(localized: "Save") saveItem = save } @@ -674,7 +678,9 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, } private func updateNavigationItems() { - cancellationItem.title = resolvedMode == .bind ? "Done" : "Cancel" + cancellationItem.title = resolvedMode == .bind + ? String(localized: "Done") + : String(localized: "Cancel") cancellationItem.accessibilityLabel = cancellationItem.title navigationItem.rightBarButtonItem = resolvedMode == .manual ? saveItem : nil updateSaveAvailability() @@ -731,29 +737,39 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, hostSection = makeHostSection() monitoringSection = makeMonitoringSection() - credentialsSection = AddHostSectionView(title: "Credentials", detail: nil, rows: []) + credentialsSection = AddHostSectionView( + title: String(localized: "Credentials"), detail: nil, rows: [] + ) credentialsSection.accessibilityIdentifier = "addhost.section.credentials" - testSection = AddHostSectionView(title: "Signal check", detail: nil, rows: []) + testSection = AddHostSectionView( + title: String(localized: "Signal check"), detail: nil, rows: [] + ) testSection.accessibilityIdentifier = "addhost.section.signal" - hostKeySection = AddHostSectionView(title: "Host key", detail: nil, rows: []) + hostKeySection = AddHostSectionView( + title: String(localized: "Host key"), detail: nil, rows: [] + ) hostKeySection.accessibilityIdentifier = "addhost.section.hostkey" workingDirectoriesSection = AddHostSectionView( - title: "New session defaults", + title: String(localized: "New session defaults"), detail: nil, rows: [] ) workingDirectoriesSection.accessibilityIdentifier = "addhost.section.directories" tmuxConfSection = makeTmuxConfSection() scriptsSection = AddHostSectionView( - title: "Session setup scripts", + title: String(localized: "Session setup scripts"), detail: nil, rows: [] ) scriptsSection.accessibilityIdentifier = "addhost.section.scripts" agentModelsSection = makeAgentModelsSection() - transportSection = AddHostSectionView(title: "Transport", detail: nil, rows: []) + transportSection = AddHostSectionView( + title: String(localized: "Transport"), detail: nil, rows: [] + ) transportSection.accessibilityIdentifier = "addhost.section.transport" - backendSection = AddHostSectionView(title: "Backend", detail: nil, rows: []) + backendSection = AddHostSectionView( + title: String(localized: "Backend"), detail: nil, rows: [] + ) backendSection.accessibilityIdentifier = "addhost.section.backend" // Host key sits last, after Transport. It stopped being a read-out @@ -785,7 +801,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private func makeHostSection() -> AddHostSectionView { configureTextField(nameField, placeholder: "devbox", identifier: "addhost.name") - nameField.accessibilityLabel = "Name" + nameField.accessibilityLabel = String(localized: "Name") nameField.text = form.name nameField.addTarget(self, action: #selector(identityFieldChanged(_:)), for: .editingChanged) @@ -795,7 +811,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, identifier: "addhost.hostname" ) hostnameField.text = form.hostname - hostnameField.accessibilityLabel = "Address" + hostnameField.accessibilityLabel = String(localized: "Address") hostnameField.keyboardType = .URL hostnameField.autocorrectionType = .no hostnameField.autocapitalizationType = .none @@ -803,13 +819,13 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, configureTextField(portField, placeholder: "22", identifier: "addhost.port") portField.text = form.port - portField.accessibilityLabel = "Port" + portField.accessibilityLabel = String(localized: "Port") portField.keyboardType = .numberPad portField.addTarget(self, action: #selector(identityFieldChanged(_:)), for: .editingChanged) configureTextField(usernameField, placeholder: "root", identifier: "addhost.username") usernameField.text = form.username - usernameField.accessibilityLabel = "User" + usernameField.accessibilityLabel = String(localized: "User") usernameField.keyboardType = .asciiCapable usernameField.autocorrectionType = .no usernameField.autocapitalizationType = .none @@ -817,14 +833,16 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, usernameField.addTarget(self, action: #selector(identityFieldChanged(_:)), for: .editingChanged) let section = AddHostSectionView( - title: "Host identity", - detail: "The name labels this host on the deck. Address, port, and user " - + "form the SSH destination.", + title: String(localized: "Host identity"), + detail: String(localized: """ + The name labels this host on the deck. Address, port, and user \ + form the SSH destination. + """), rows: [ - AddHostFieldRow(label: "Name", inputView: nameField), - AddHostFieldRow(label: "Address", inputView: hostnameField), - AddHostFieldRow(label: "Port", inputView: portField), - AddHostFieldRow(label: "User", inputView: usernameField), + AddHostFieldRow(label: String(localized: "Name"), inputView: nameField), + AddHostFieldRow(label: String(localized: "Address"), inputView: hostnameField), + AddHostFieldRow(label: String(localized: "Port"), inputView: portField), + AddHostFieldRow(label: String(localized: "User"), inputView: usernameField), ] ) section.accessibilityIdentifier = "addhost.section.host" @@ -833,9 +851,11 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private func makeMonitoringSection() -> AddHostSectionView { let control = SettingsBooleanRow( - title: "Connect on the deck", + title: String(localized: "Connect on the deck"), isOn: form.isEnabled, - accessibilityHint: "Off keeps the host in the fleet without connecting to it" + accessibilityHint: String( + localized: "Off keeps the host in the fleet without connecting to it" + ) ) { [weak self] enabled in guard let self else { return } self.form.isEnabled = enabled @@ -843,10 +863,12 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, } monitoringControl = control let keepAlive = SettingsBooleanRow( - title: "Keep alive in background", + title: String(localized: "Keep alive in background"), isOn: form.backgroundKeepAlive, - accessibilityHint: "Holds this host's sessions and probing open for the extra " - + "seconds iOS grants after you leave the app" + accessibilityHint: String(localized: """ + Holds this host's sessions and probing open for the extra \ + seconds iOS grants after you leave the app + """) ) { [weak self] enabled in guard let self else { return } self.form.backgroundKeepAlive = enabled @@ -854,7 +876,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, } backgroundKeepAliveControl = keepAlive let section = AddHostSectionView( - title: "Monitoring", + title: String(localized: "Monitoring"), detail: monitoringDetail, rows: [control, keepAlive] ) @@ -867,28 +889,36 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, /// doing so for a moment after you look away. private var monitoringDetail: String { let connect = form.isEnabled - ? "The deck probes this host about every five seconds while it's in " - + "front, and its sessions appear as live tiles." - : "Off parks the host on the deck without dialling it: no probing, no tiles, " - + "and widgets or Shortcuts report it as disabled. Terminal windows already " - + "open keep running, and Signal check below still connects on demand." + ? String(localized: """ + The deck probes this host about every five seconds while it's in \ + front, and its sessions appear as live tiles. + """) + : String(localized: """ + Off parks the host on the deck without dialling it: no probing, no tiles, \ + and widgets or Shortcuts report it as disabled. Terminal windows already \ + open keep running, and Signal check below still connects on demand. + """) // Sized to the mechanism on purpose. iOS grants a leaving app a short // stretch of extra running time and nothing more; the modes that would // buy minutes are for apps genuinely playing audio or tracking // location, so the promise here stops where the grant does. let keepAlive = form.backgroundKeepAlive - ? "Leaving the app holds this host's sessions and probing open for the extra " - + "time iOS grants — tens of seconds, not minutes. Agent alerts still reach " - + "you inside that window; after it the app suspends as usual and tabs " - + "reattach when you come back." - : "Background keep-alive is off: leaving the app suspends it, this host's " - + "sessions drop, and its tabs reattach when you return." + ? String(localized: """ + Leaving the app holds this host's sessions and probing open for the extra \ + time iOS grants — tens of seconds, not minutes. Agent alerts still reach \ + you inside that window; after it the app suspends as usual and tabs \ + reattach when you come back. + """) + : String(localized: """ + Background keep-alive is off: leaving the app suspends it, this host's \ + sessions drop, and its tabs reattach when you return. + """) return connect + "\n\n" + keepAlive } private func renderCredentials() { let authLabel = addHostLabel( - "Sign in with", + String(localized: "Sign in with"), font: UIKitChassis.uiFont(10, weight: .semibold), color: UIKitChassis.signal2 ) @@ -909,22 +939,24 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, switch form.authMethod { case .password: let secret = AddHostRevealableSecretField( - title: "Password", - prompt: "Required", + title: String(localized: "Password"), + prompt: String(localized: "Required"), text: form.password ) { [weak self] text in self?.updateTestSensitive { $0.password = text } } secret.textField.accessibilityIdentifier = "addhost.password" passwordField = secret - rows.append(AddHostFieldRow(label: "Password", inputView: secret)) + rows.append(AddHostFieldRow(label: String(localized: "Password"), inputView: secret)) credentialsSection.setDetail( - "Stored in iCloud Keychain, never in the host record." + String(localized: "Stored in iCloud Keychain, never in the host record.") ) case .privateKey: if form.privateKeyConcealed { let reveal = makeConcealedPrivateKeyButton() - rows.append(AddHostFieldRow(label: "Private key", inputView: reveal)) + rows.append(AddHostFieldRow( + label: String(localized: "Private key"), inputView: reveal + )) } else { let keyView = AddHostGrowingTextView( text: form.privateKey, @@ -933,7 +965,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, minimumLines: 4, maximumLines: 8 ) - keyView.accessibilityLabel = "Private key" + keyView.accessibilityLabel = String(localized: "Private key") keyView.accessibilityIdentifier = "addhost.privateKey" keyView.autocorrectionType = .no keyView.autocapitalizationType = .none @@ -942,21 +974,25 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, self?.updateTestSensitive { $0.privateKey = text } } privateKeyView = keyView - rows.append(AddHostFieldRow(label: "Private key", inputView: keyView)) + rows.append(AddHostFieldRow( + label: String(localized: "Private key"), inputView: keyView + )) } let secret = AddHostRevealableSecretField( - title: "Passphrase", - prompt: "Optional", + title: String(localized: "Passphrase"), + prompt: String(localized: "Optional"), text: form.passphrase ) { [weak self] text in self?.updateTestSensitive { $0.passphrase = text } } secret.textField.accessibilityIdentifier = "addhost.passphrase" passphraseField = secret - rows.append(AddHostFieldRow(label: "Passphrase", inputView: secret)) + rows.append(AddHostFieldRow(label: String(localized: "Passphrase"), inputView: secret)) credentialsSection.setDetail( - "The OpenSSH key is stored in iCloud Keychain. Leave its passphrase " - + "blank to enter it when connecting, or save the passphrase there too." + String(localized: """ + The OpenSSH key is stored in iCloud Keychain. Leave its passphrase \ + blank to enter it when connecting, or save the passphrase there too. + """) ) } credentialsSection.setRows(rows) @@ -966,8 +1002,8 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, let button = UIButton(type: .custom) button.backgroundColor = .clear button.hoverStyle = UIHoverStyle(effect: .highlight, shape: .rect(cornerRadius: 2)) - button.accessibilityLabel = "Edit private key" - button.accessibilityHint = "Shows the saved key" + button.accessibilityLabel = String(localized: "Edit private key") + button.accessibilityHint = String(localized: "Shows the saved key") button.accessibilityIdentifier = "addhost.privateKeyConcealed" let bullets = addHostLabel( String(repeating: "•", count: 8), @@ -1002,29 +1038,31 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private func makeTmuxConfSection() -> AddHostSectionView { let textView = AddHostGrowingTextView( text: form.newSessionTmuxConf, - placeholder: "cleared — nothing applied", + placeholder: String(localized: "cleared — nothing applied"), font: UIKitChassis.monoFont(12), minimumLines: 2, maximumLines: 6 ) textView.autocorrectionType = .no textView.autocapitalizationType = .none - textView.accessibilityLabel = "Options" + textView.accessibilityLabel = String(localized: "Options") textView.accessibilityIdentifier = "addhost.tmuxConf" textView.onTextChange = { [weak self] text in self?.form.newSessionTmuxConf = text } newSessionTmuxConfView = textView let section = AddHostSectionView( - title: "New session tmux conf", - detail: "One option per line, like a .tmux.conf (mouse on, history-limit " - + "50000). Each line is applied when Multiplex creates a session with " - + "tmux set-option -t that session. Attaching never applies anything, " - + "and session-scoped options do not change sessions made on the host. " - + "Hosts start with mouse on and focus-events on; clear the field to " - + "apply nothing. Focus events and other server-scoped options still " - + "reach the whole tmux server.", - rows: [AddHostFieldRow(label: "Options", inputView: textView)] + title: String(localized: "New session tmux conf"), + detail: String(localized: """ + One option per line, like a .tmux.conf (mouse on, history-limit \ + 50000). Each line is applied when Multiplex creates a session with \ + tmux set-option -t that session. Attaching never applies anything, \ + and session-scoped options do not change sessions made on the host. \ + Hosts start with mouse on and focus-events on; clear the field to \ + apply nothing. Focus events and other server-scoped options still \ + reach the whole tmux server. + """), + rows: [AddHostFieldRow(label: String(localized: "Options"), inputView: textView)] ) section.accessibilityIdentifier = "addhost.section.tmuxConf" return section @@ -1042,7 +1080,9 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, ) textView.autocorrectionType = .no textView.autocapitalizationType = .none - textView.accessibilityLabel = "\(agent.displayName) models, one per line" + textView.accessibilityLabel = String( + localized: "\(agent.displayName) models, one per line" + ) textView.accessibilityIdentifier = "addhost.models.\(agent.rawValue)" textView.onTextChange = { [weak self] text in self?.form.modelText[agent] = text @@ -1051,10 +1091,12 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, rows.append(AddHostFieldRow(label: agent.displayName, inputView: textView)) } let section = AddHostSectionView( - title: "Agent launch models", - detail: "One model id per line, in picker order. New Session, the Open Agent " - + "shortcut, and the host widget offer them as choices, passed as " - + "--model; nothing is applied unless chosen at launch.", + title: String(localized: "Agent launch models"), + detail: String(localized: """ + One model id per line, in picker order. New Session, the Open Agent \ + shortcut, and the host widget offer them as choices, passed as \ + --model; nothing is applied unless chosen at launch. + """), rows: rows ) section.accessibilityIdentifier = "addhost.section.agentModels" @@ -1063,10 +1105,10 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private func modelPlaceholder(for agent: AgentKind) -> String { switch agent { - case .claudeCode: return "opus, sonnet, or a full model id" - case .codex: return "model id per line" - case .pi: return "provider/model-id per line" - case .grok: return "grok-build, or a full model id" + case .claudeCode: return String(localized: "opus, sonnet, or a full model id") + case .codex: return String(localized: "model id per line") + case .pi: return String(localized: "provider/model-id per line") + case .grok: return String(localized: "grok-build, or a full model id") } } @@ -1075,16 +1117,20 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private var testDetail: String { let backend = form.sessionBackend.rawValue return form.useMosh - ? "Signs in over SSH with the settings above, then looks for \(backend) and " - + "mosh-server on the host." - : "Signs in over SSH with the settings above, then looks for \(backend) " - + "on the host." + ? String(localized: """ + Signs in over SSH with the settings above, then looks for \(backend) and \ + mosh-server on the host. + """) + : String(localized: """ + Signs in over SSH with the settings above, then looks for \(backend) \ + on the host. + """) } private func renderTestSection() { let chip = UIKitChassisChip( "TEST CONNECTION", - accessibilityLabel: "Test connection" + accessibilityLabel: String(localized: "Test connection") ) { [weak self] in self?.runTest() } @@ -1168,12 +1214,18 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private var hostKeyDetail: String { if forgetHostKeysArmed { - return "Tap again to forget. The next connection trusts whatever answers, and records that." + return String(localized: """ + Tap again to forget. The next connection trusts whatever answers, and records that. + """) } return recordedHostKeys.isEmpty - ? "With nothing here, the first connection trusts what answers and records it. " - + "Paste a key you already have and it is checked instead." - : "Checked on every connection. Forget these only if you rebuilt the server." + ? String(localized: """ + With nothing here, the first connection trusts what answers and records it. \ + Paste a key you already have and it is checked instead. + """) + : String(localized: """ + Checked on every connection. Forget these only if you rebuilt the server. + """) } private func renderHostKeys() { @@ -1200,8 +1252,8 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, let forget = UIKitChassisChip( forgetHostKeysArmed ? "CONFIRM FORGET" : "FORGET", accessibilityLabel: forgetHostKeysArmed - ? "Confirm forgetting recorded host keys" - : "Forget recorded host keys" + ? String(localized: "Confirm forgetting recorded host keys") + : String(localized: "Forget recorded host keys") ) { [weak self] in self?.forgetHostKeysTapped() } forget.accessibilityIdentifier = "addhost.hostkey.forget" let forgetRow = UIStackView(arrangedSubviews: [forget, addHostFlexibleSpacer()]) @@ -1229,7 +1281,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, placeholder: "SHA256:… or ssh-ed25519 AAAA…", font: UIKitChassis.monoFont(11) ) - expectedHostKeyField.accessibilityLabel = "Expected host key" + expectedHostKeyField.accessibilityLabel = String(localized: "Expected host key") expectedHostKeyField.accessibilityIdentifier = "addhost.hostkey.expected" expectedHostKeyField.autocorrectionType = .no expectedHostKeyField.autocapitalizationType = .none @@ -1248,7 +1300,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, updateExpectedHostKeyStatus() let caption = addHostLabel( - "Expected key (optional)", + String(localized: "Expected key (optional)"), font: UIKitChassis.uiFont(10, weight: .semibold), color: UIKitChassis.signal2 ) @@ -1280,14 +1332,16 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, expectedHostKeyStatus.isHidden = false guard let pin = HostKeyPin(userInput: text) else { expectedHostKeyStatus.textColor = TallyPalette.caution - expectedHostKeyStatus.text = "Not a host key. Paste a SHA256: fingerprint, or a " - + "public key line from the machine's /etc/ssh." + expectedHostKeyStatus.text = String(localized: """ + Not a host key. Paste a SHA256: fingerprint, or a \ + public key line from the machine's /etc/ssh. + """) return } expectedHostKeyStatus.textColor = UIKitChassis.signal2 expectedHostKeyStatus.text = pin.algorithm == HostKeyPin.anyAlgorithm - ? "Will verify against \(pin.fingerprint)" - : "Will verify against \(pin.algorithm) \(pin.fingerprint)" + ? String(localized: "Will verify against \(pin.fingerprint)") + : String(localized: "Will verify against \(pin.algorithm) \(pin.fingerprint)") } private func forgetHostKeysTapped() { @@ -1330,19 +1384,19 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, var warnings: [String] = [] if !report.multiplexerFound { let backend = host.sessionBackend.rawValue - warnings.append( - "\(backend) wasn't found on the host — the deck can't list sessions " - + "there. Plain shells still work." - ) + warnings.append(String(localized: """ + \(backend) wasn't found on the host — the deck can't list sessions \ + there. Plain shells still work. + """)) } if report.moshServerFound == false { - warnings.append( - "mosh-server wasn't found — mosh attaches will fail. Install it " - + "on the host or set its path below." - ) + warnings.append(String(localized: """ + mosh-server wasn't found — mosh attaches will fail. Install it \ + on the host or set its path below. + """)) } self.testState = .passed( - headline: "Connected to \(host.hostname) as \(host.username).", + headline: String(localized: "Connected to \(host.hostname) as \(host.username)."), warnings: warnings ) case .failed(let message): @@ -1367,10 +1421,14 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private var workingDirectoriesDetail: String { form.workingDirectories.isEmpty - ? "New sessions start in the host's home directory. Add paths to make them " - + "available in New Session." - : "The first path is the default. New Session can choose another path or " - + "the host's home directory." + ? String(localized: """ + New sessions start in the host's home directory. Add paths to make them \ + available in New Session. + """) + : String(localized: """ + The first path is the default. New Session can choose another path or \ + the host's home directory. + """) } private func renderWorkingDirectories() { @@ -1381,10 +1439,10 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, configureInlineField( field, text: directory.path, - placeholder: "Directory", + placeholder: String(localized: "Directory"), font: UIKitChassis.monoFont(11) ) - field.accessibilityLabel = "Directory" + field.accessibilityLabel = String(localized: "Directory") field.accessibilityIdentifier = "addhost.directory.\(directory.id.uuidString)" field.autocorrectionType = .no field.autocapitalizationType = .none @@ -1414,7 +1472,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, AddHostInlineWell(contentView: field), AddHostIconButton( systemImage: "arrow.up", - accessibilityLabel: "Move directory up", + accessibilityLabel: String(localized: "Move directory up"), enabled: index > 0 ) { [weak self] in self?.form.moveWorkingDirectory(id: directory.id, offset: -1) @@ -1422,7 +1480,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, }, AddHostIconButton( systemImage: "arrow.down", - accessibilityLabel: "Move directory down", + accessibilityLabel: String(localized: "Move directory down"), enabled: index < form.workingDirectories.count - 1 ) { [weak self] in self?.form.moveWorkingDirectory(id: directory.id, offset: 1) @@ -1430,7 +1488,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, }, AddHostIconButton( systemImage: "trash", - accessibilityLabel: "Delete directory" + accessibilityLabel: String(localized: "Delete directory") ) { [weak self] in self?.form.removeWorkingDirectory(id: directory.id) self?.renderWorkingDirectories() @@ -1448,7 +1506,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, placeholder: "~/projects/app", font: UIKitChassis.monoFont(11) ) - newWorkingDirectoryField.accessibilityLabel = "Add directory" + newWorkingDirectoryField.accessibilityLabel = String(localized: "Add directory") newWorkingDirectoryField.accessibilityIdentifier = "addhost.newDirectory" newWorkingDirectoryField.autocorrectionType = .no newWorkingDirectoryField.autocapitalizationType = .none @@ -1467,7 +1525,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, let add = UIKitChassisChip( "ADD", systemImage: "plus", - accessibilityLabel: "Add directory" + accessibilityLabel: String(localized: "Add directory") ) { [weak self] in self?.addWorkingDirectory() } addDirectoryChip = add updateAddDirectoryAvailability() @@ -1479,7 +1537,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, addRow.alignment = .center addRow.spacing = 8 let addLabel = addHostLabel( - "Add directory", + String(localized: "Add directory"), font: UIKitChassis.uiFont(10, weight: .semibold), color: UIKitChassis.signal2 ) @@ -1509,10 +1567,14 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private var scriptsDetail: String { form.scripts.isEmpty - ? "New Session can type a chosen script into the fresh shell before anything " - + "launches. Add one to make it available." - : "New Session offers these by name; the chosen one is typed into the fresh " - + "shell before the launch command." + ? String(localized: """ + New Session can type a chosen script into the fresh shell before anything \ + launches. Add one to make it available. + """) + : String(localized: """ + New Session offers these by name; the chosen one is typed into the fresh \ + shell before the launch command. + """) } private func renderScripts() { @@ -1524,12 +1586,12 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, configureInlineField( name, text: script.name, - placeholder: "Name", + placeholder: String(localized: "Name"), font: UIKitChassis.uiFont(11, weight: .medium) ) name.autocorrectionType = .no name.autocapitalizationType = .none - name.accessibilityLabel = "Script name" + name.accessibilityLabel = String(localized: "Script name") name.accessibilityIdentifier = "addhost.scriptName.\(script.id.uuidString)" name.addAction(UIAction { [weak self, weak name] _ in guard let self, @@ -1545,7 +1607,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, AddHostInlineWell(contentView: name), AddHostIconButton( systemImage: "arrow.up", - accessibilityLabel: "Move script up", + accessibilityLabel: String(localized: "Move script up"), enabled: index > 0 ) { [weak self] in self?.form.moveScript(id: script.id, offset: -1) @@ -1553,7 +1615,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, }, AddHostIconButton( systemImage: "arrow.down", - accessibilityLabel: "Move script down", + accessibilityLabel: String(localized: "Move script down"), enabled: index < form.scripts.count - 1 ) { [weak self] in self?.form.moveScript(id: script.id, offset: 1) @@ -1561,7 +1623,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, }, AddHostIconButton( systemImage: "trash", - accessibilityLabel: "Delete script" + accessibilityLabel: String(localized: "Delete script") ) { [weak self] in self?.form.scripts.removeAll { $0.id == script.id } self?.renderScripts() @@ -1580,7 +1642,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, ) body.autocorrectionType = .no body.autocapitalizationType = .none - body.accessibilityLabel = "Script commands" + body.accessibilityLabel = String(localized: "Script commands") body.accessibilityIdentifier = "addhost.scriptBody.\(script.id.uuidString)" body.onTextChange = { [weak self] text in guard let self, @@ -1600,7 +1662,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, let add = UIKitChassisChip( "ADD SCRIPT", systemImage: "plus", - accessibilityLabel: "Add script" + accessibilityLabel: String(localized: "Add script") ) { [weak self] in self?.form.scripts.append(AddHostFormState.ScriptRow()) self?.renderScripts() @@ -1619,19 +1681,25 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, let enabled = enabledBackends var detail: String if enabled.contains(.herdr) { - detail = "The deck monitors herdr (herdr.dev) sessions and their " - + "agents through the herdr CLI — one tile per session, its " - + "workspaces as the tile's window lines; a tile attaches " - + "the full herdr client. The tmux options editor doesn't " - + "apply to herdr sessions." + detail = String(localized: """ + The deck monitors herdr (herdr.dev) sessions and their \ + agents through the herdr CLI — one tile per session, its \ + workspaces as the tile's window lines; a tile attaches \ + the full herdr client. The tmux options editor doesn't \ + apply to herdr sessions. + """) } else { - detail = "The deck monitors a remote tmux server — sessions, " - + "windows, and agent panes. herdr (herdr.dev) is the " - + "alternative for hosts that run it." + detail = String(localized: """ + The deck monitors a remote tmux server — sessions, \ + windows, and agent panes. herdr (herdr.dev) is the \ + alternative for hosts that run it. + """) } if enabled.count > 1 { - detail += "\n\nBoth are shown, each tile marked with the backend " - + "it came from. That \(HostGuide.secondBackendCost)." + detail += "\n\n" + String(localized: """ + Both are shown, each tile marked with the backend \ + it came from. That \(HostGuide.secondBackendCost). + """) } return detail } @@ -1647,7 +1715,7 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, } checks.accessibilityIdentifier = "addhost.backendChecks" let checksLabel = addHostLabel( - "Backends", + String(localized: "Backends"), font: UIKitChassis.uiFont(10, weight: .semibold), color: UIKitChassis.signal2 ) @@ -1675,13 +1743,14 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, } bar.accessibilityIdentifier = "addhost.backendBar" let label = addHostLabel( - "New sessions run on", + String(localized: "New sessions run on"), font: UIKitChassis.uiFont(10, weight: .semibold), color: UIKitChassis.signal2 ) - label.accessibilityHint = - "The backend New Session, widgets, and Shortcuts start on " - + "unless they name another" + label.accessibilityHint = String(localized: """ + The backend New Session, widgets, and Shortcuts start on \ + unless they name another + """) stack.addArrangedSubview(label) stack.addArrangedSubview(bar) stack.setCustomSpacing(16, after: checks) @@ -1718,22 +1787,24 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, private var transportDetail: String { if form.useMosh { - return "Terminals attach over UDP and survive roaming or sleep. SSH still " - + "signs in, starts mosh-server, and probes the deck." + return String(localized: """ + Terminals attach over UDP and survive roaming or sleep. SSH still \ + signs in, starts mosh-server, and probes the deck. + """) } - return "SSH carries both the control connection and attached terminals." + return String(localized: "SSH carries both the control connection and attached terminals.") } private func renderTransport() { moshPortsInvalidRow = nil let requiresPro = moshRequiresPro let control = SettingsBooleanRow( - title: "Connect with mosh", + title: String(localized: "Connect with mosh"), isOn: form.useMosh, status: requiresPro ? "PRO" : nil, statusIsProminent: true, optimisticallyUpdates: !requiresPro, - accessibilityHint: requiresPro ? "Requires Multiplex Pro" : nil + accessibilityHint: requiresPro ? String(localized: "Requires Multiplex Pro") : nil ) { [weak self] enabled in guard let self else { return } if enabled && self.moshRequiresPro { @@ -1769,18 +1840,20 @@ final class AddHostViewController: UIViewController, UITextFieldDelegate, identifier: "addhost.moshPorts" ) ports.text = form.moshPorts - ports.accessibilityLabel = "UDP port or range" + ports.accessibilityLabel = String(localized: "UDP port or range") ports.keyboardType = .numbersAndPunctuation ports.autocorrectionType = .no ports.autocapitalizationType = .none ports.addTarget(self, action: #selector(moshFieldChanged(_:)), for: .editingChanged) moshPortsField = ports - rows.append(AddHostFieldRow(label: "UDP port or range", inputView: ports)) + rows.append(AddHostFieldRow( + label: String(localized: "UDP port or range"), inputView: ports + )) let invalid = UIStackView(arrangedSubviews: [ UIKitTallyLamp(caption: "INVALID", color: TallyPalette.caution), addHostLabel( - "Use one port or a range from 1 to 65535.", + String(localized: "Use one port or a range from 1 to 65535."), font: UIKitChassis.uiFont(10), color: UIKitChassis.signal2 ), @@ -2517,8 +2590,8 @@ private final class AddHostChoiceButton: UIButton { accessibilityTraits.insert(.toggleButton) } accessibilityValue = checkable - ? (selected ? "Checked" : "Unchecked") - : (selected ? "Selected" : "Not selected") + ? (selected ? String(localized: "Checked") : String(localized: "Unchecked")) + : (selected ? String(localized: "Selected") : String(localized: "Not selected")) if selected { accessibilityTraits.insert(.selected) } else { @@ -2749,7 +2822,9 @@ final class AddHostRevealableSecretField: UIView, UITextFieldDelegate { ) ) revealButton.setImage(image, for: .normal) - revealButton.accessibilityLabel = revealed ? "Hide \(title)" : "Show \(title)" + revealButton.accessibilityLabel = revealed + ? String(localized: "Hide \(title)") + : String(localized: "Show \(title)") } private static func bullets(_ count: Int) -> String { diff --git a/Multiplex/Views/Deck/AgentPromptSheet.swift b/Multiplex/Views/Deck/AgentPromptSheet.swift index 3ff683a9..d5487d0b 100644 --- a/Multiplex/Views/Deck/AgentPromptSheet.swift +++ b/Multiplex/Views/Deck/AgentPromptSheet.swift @@ -16,26 +16,29 @@ struct AgentPromptFormState { } var directoryLabel: String { - guard let directory, directory != "~" else { return "Home" } + guard let directory, directory != "~" else { return String(localized: "Home") } return directory } var modelDetail: String { let trimmed = model.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { - return "Uses \(request.agent.displayName)'s own default model." + return String(localized: "Uses \(request.agent.displayName)'s own default model.") } - return "Launches as \(request.agent.launchCommand(model: trimmed, initialPrompt: ""))." + let command = request.agent.launchCommand(model: trimmed, initialPrompt: "") + return String(localized: "Launches as \(command).") } var directoryDetail: String { guard !request.host.workingDirs.isEmpty else { - return "Uses the host's login-shell home directory." + return String(localized: "Uses the host's login-shell home directory.") } if let directory, directory != "~" { - return "Starts in \(directory). Choose Home to use the login shell's default." + return String(localized: """ + Starts in \(directory). Choose Home to use the login shell's default. + """) } - return "Uses the host's login-shell home directory." + return String(localized: "Uses the host's login-shell home directory.") } var launchAction: ExternalAction { @@ -57,7 +60,7 @@ struct AgentPromptFormState { /// The sheet's title names an existing-session target — where the /// launch types is part of what the person approves here. var title: String { - var title = "\(request.agent.displayName) on \(request.host.name)" + var title = String(localized: "\(request.agent.displayName) on \(request.host.name)") if case .existingSession(let name, _) = request.target { title += " · \(name)" } @@ -102,7 +105,7 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat form = AgentPromptFormState(request: request) self.submit = submit promptTextView = AgentPromptTextView( - placeholder: "What should \(request.agent.displayName) do?" + placeholder: String(localized: "What should \(request.agent.displayName) do?") ) super.init(nibName: nil, bundle: nil) } @@ -121,23 +124,23 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat navigationItem.titleView = UIKitChassisLabel(title, size: 12) #endif let cancel = UIBarButtonItem( - title: "Cancel", + title: String(localized: "Cancel"), style: .plain, target: self, action: #selector(cancelPressed) ) cancel.tintColor = UIKitChassis.signal - cancel.accessibilityLabel = "Cancel" + cancel.accessibilityLabel = String(localized: "Cancel") navigationItem.leftBarButtonItem = cancel let launch = UIBarButtonItem( - title: "Launch", + title: String(localized: "Launch"), style: .plain, target: self, action: #selector(launchPressed) ) launch.tintColor = UIKitChassis.signal - launch.accessibilityLabel = "Launch" + launch.accessibilityLabel = String(localized: "Launch") navigationItem.rightBarButtonItem = launch configureContent() @@ -216,9 +219,9 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat } private func makePromptSection() -> UIView { - promptTextView.accessibilityLabel = "Prompt" + promptTextView.accessibilityLabel = String(localized: "Prompt") promptTextView.accessibilityHint = - "What should \(form.request.agent.displayName) do?" + String(localized: "What should \(form.request.agent.displayName) do?") promptTextView.accessibilityIdentifier = "agentPrompt.prompt" promptTextView.onTextChange = { [weak self] text in self?.form.prompt = text @@ -244,17 +247,21 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat height.isActive = true promptHeightConstraint = height - let field = makeField(label: "Prompt", input: promptWell) + let field = makeField(label: String(localized: "Prompt"), input: promptWell) + let agentName = form.request.agent.displayName return AgentPromptSectionView( - title: "First prompt", - detail: "Sent as \(form.request.agent.displayName)'s launch argument — it starts working on it immediately. Leave empty to open \(form.request.agent.displayName) without a prompt.", + title: String(localized: "First prompt"), + detail: String(localized: """ + Sent as \(agentName)'s launch argument — it starts working on it \ + immediately. Leave empty to open \(agentName) without a prompt. + """), contentView: field ) } private func makeModelSection() -> UIView { modelField.text = form.model - modelField.placeholder = "Agent default" + modelField.placeholder = String(localized: "Agent default") modelField.font = UIKitChassis.monoFont(12) modelField.textColor = UIKitChassis.signal modelField.tintColor = UIKitChassis.signal @@ -268,7 +275,7 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat modelField.returnKeyType = .done modelField.delegate = self modelField.accessibilityLabel = - "Optional model for \(form.request.agent.displayName)" + String(localized: "Optional model for \(form.request.agent.displayName)") modelField.accessibilityIdentifier = "agentPrompt.model" modelField.addTarget(self, action: #selector(modelChanged), for: .editingChanged) modelField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) @@ -295,9 +302,9 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat row.bottomAnchor.constraint(equalTo: well.bottomAnchor, constant: -9), ]) - let field = makeField(label: "Model", input: well) + let field = makeField(label: String(localized: "Model"), input: well) let section = AgentPromptSectionView( - title: "Model", + title: String(localized: "Model"), detail: form.modelDetail, contentView: field ) @@ -314,7 +321,7 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat let startsIn = UILabel() startsIn.font = UIKitChassis.uiFont(10, weight: .semibold) startsIn.textColor = UIKitChassis.signal2 - startsIn.text = "Starts in" + startsIn.text = String(localized: "Starts in") let home = UILabel() home.font = UIKitChassis.monoFont(10, weight: .medium) @@ -328,24 +335,24 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat row.alignment = .center row.spacing = 12 row.isAccessibilityElement = true - row.accessibilityLabel = "Starts in, Home" + row.accessibilityLabel = String(localized: "Starts in, Home") startsIn.isAccessibilityElement = false home.isAccessibilityElement = false content = row } else { let button = AgentPromptDirectoryButton() button.setDirectory(form.directoryLabel) - button.accessibilityLabel = "Starting directory" + button.accessibilityLabel = String(localized: "Starting directory") button.accessibilityValue = form.directoryLabel button.accessibilityIdentifier = "agentPrompt.directory" button.menu = makeDirectoryMenu() button.showsMenuAsPrimaryAction = true directoryButton = button - content = makeField(label: "Starts in", input: button) + content = makeField(label: String(localized: "Starts in"), input: button) } let section = AgentPromptSectionView( - title: "Directory", + title: String(localized: "Directory"), detail: form.directoryDetail, contentView: content ) @@ -382,7 +389,7 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat button.backgroundColor = GlassPrototype.strataChassis button.hoverStyle = UIHoverStyle(effect: .highlight, shape: .rect(cornerRadius: 2)) button.accessibilityLabel = - "Configured models for \(form.request.agent.displayName)" + String(localized: "Configured models for \(form.request.agent.displayName)") button.accessibilityIdentifier = "agentPrompt.modelMenu" button.showsMenuAsPrimaryAction = true @@ -404,7 +411,7 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat self?.selectModel(candidate) } } - let defaultAction = UIAction(title: "Agent default") { [weak self] _ in + let defaultAction = UIAction(title: String(localized: "Agent default")) { [weak self] _ in self?.selectModel("") } children.append(UIMenu(options: .displayInline, children: [defaultAction])) @@ -418,7 +425,7 @@ final class AgentPromptSheetViewController: UIViewController, UITextFieldDelegat self?.selectDirectory(directory) } } - let home = UIAction(title: "Home") { [weak self] _ in + let home = UIAction(title: String(localized: "Home")) { [weak self] _ in self?.selectDirectory("~") } children.append(UIMenu(options: .displayInline, children: [home])) diff --git a/Multiplex/Views/Deck/BindPane.swift b/Multiplex/Views/Deck/BindPane.swift index b93c6c6a..c2ff5994 100644 --- a/Multiplex/Views/Deck/BindPane.swift +++ b/Multiplex/Views/Deck/BindPane.swift @@ -6,43 +6,67 @@ import VisionKit #endif private enum BindPaneCopy { - static let machineDetail = "Copy a line, run it in a terminal on the machine you " - + "want to add, then leave mpx bind running. Nothing here runs on this device." - static let incomingEmptyDetail = "Machines running mpx bind on this network appear " - + "here on their own. Confirm each one with the 6-digit PIN its terminal printed." - static let incomingDiscoveredDetail = "Heard on your network. Check the address and " - + "fingerprint against the terminal, then type its PIN." - static let incomingPayloadDetail = "From a scanned or pasted bind code." - static let backendTmuxDetail = "Every machine bound from this pane is added on this " - + "backend — binding proves who a machine is, not what it runs. tmux is the remote " - + "server the deck monitors: sessions, windows, and agent panes. Change it later in " - + "Host Settings." - static let backendHerdrDetail = "Every machine bound from this pane is added on this " - + "backend — binding proves who a machine is, not what it runs. The deck monitors " - + "herdr (herdr.dev) sessions and their agents through the herdr CLI, one tile per " - + "session. Change it later in Host Settings." + static let machineDetail = String(localized: """ + Copy a line, run it in a terminal on the machine you \ + want to add, then leave mpx bind running. Nothing here runs on this device. + """) + static let incomingEmptyDetail = String(localized: """ + Machines running mpx bind on this network appear \ + here on their own. Confirm each one with the 6-digit PIN its terminal printed. + """) + static let incomingDiscoveredDetail = String(localized: """ + Heard on your network. Check the address and \ + fingerprint against the terminal, then type its PIN. + """) + static let incomingPayloadDetail = String(localized: "From a scanned or pasted bind code.") + static let backendTmuxDetail = String(localized: """ + Every machine bound from this pane is added on this \ + backend — binding proves who a machine is, not what it runs. tmux is the remote \ + server the deck monitors: sessions, windows, and agent panes. Change it later in \ + Host Settings. + """) + static let backendHerdrDetail = String(localized: """ + Every machine bound from this pane is added on this \ + backend — binding proves who a machine is, not what it runs. The deck monitors \ + herdr (herdr.dev) sessions and their agents through the herdr CLI, one tile per \ + session. Change it later in Host Settings. + """) /// Both checked. The cost sentence is `HostGuide`'s, so this and the /// deck's offer confirmation can't drift on what the escalation costs. - static let backendMixedDetail = "Every machine bound from this pane is added showing " - + "both — binding proves who a machine is, not what it runs. Each tile is marked " - + "with the backend it came from, and that \(HostGuide.secondBackendCost). Change " - + "it later in Host Settings." - static let passphraseDetail = "Optional, for every machine bound from this pane: its " - + "SSH key is generated sealed with this passphrase, which is then saved in the " - + "host's settings so connecting keeps working. Clear it in Host Settings to be " - + "asked when connecting instead. Empty means the key is stored unlocked, " - + "exactly as before." - static let elsewhereDetail = "A machine on another network — a VPS, a box behind a " - + "firewall — can't announce itself here. Scan the QR its terminal printed, or " - + "have it hand you the code." - static let pasteLead = "Paste needs the code on this device's clipboard, and mpx " - + "never takes a clipboard unless you ask it to. Run it with --copy instead:" - static let pastePostscript = "Over SSH that uses OSC 52, so the code lands on your " - + "local terminal's clipboard and Universal Clipboard carries it here." - static let pasteFailure = "The clipboard doesn’t hold a bind code. Copy the " - + "multiplex:// line the CLI printed." - static let footer = "Binding never sends a private key: this device makes its own key " - + "and the machine adds the public half to authorized_keys. mpx unbind removes it." + static let backendMixedDetail = String(localized: """ + Every machine bound from this pane is added showing \ + both — binding proves who a machine is, not what it runs. Each tile is marked \ + with the backend it came from, and that \(HostGuide.secondBackendCost). Change \ + it later in Host Settings. + """) + static let passphraseDetail = String(localized: """ + Optional, for every machine bound from this pane: its \ + SSH key is generated sealed with this passphrase, which is then saved in the \ + host's settings so connecting keeps working. Clear it in Host Settings to be \ + asked when connecting instead. Empty means the key is stored unlocked, \ + exactly as before. + """) + static let elsewhereDetail = String(localized: """ + A machine on another network — a VPS, a box behind a \ + firewall — can't announce itself here. Scan the QR its terminal printed, or \ + have it hand you the code. + """) + static let pasteLead = String(localized: """ + Paste needs the code on this device's clipboard, and mpx \ + never takes a clipboard unless you ask it to. Run it with --copy instead: + """) + static let pastePostscript = String(localized: """ + Over SSH that uses OSC 52, so the code lands on your \ + local terminal's clipboard and Universal Clipboard carries it here. + """) + static let pasteFailure = String(localized: """ + The clipboard doesn’t hold a bind code. Copy the \ + multiplex:// line the CLI printed. + """) + static let footer = String(localized: """ + Binding never sends a private key: this device makes its own key \ + and the machine adds the public half to authorized_keys. mpx unbind removes it. + """) } private extension BindController.Pending { @@ -64,9 +88,9 @@ private extension BindController.Pending { var busyCaption: String { switch stage { - case .binding: "Proving the PIN…" - case .enrolling: "Enrolling this device’s key…" - case .checking: "Checking the connection…" + case .binding: String(localized: "Proving the PIN…") + case .enrolling: String(localized: "Enrolling this device’s key…") + case .checking: String(localized: "Checking the connection…") default: "" } } @@ -289,7 +313,7 @@ final class BindPaneViewController: UIViewController { )) let section = UIKitTallyFormSectionView( - title: "On the machine", + title: String(localized: "On the machine"), detail: BindPaneCopy.machineDetail, contentView: commands ) @@ -311,7 +335,7 @@ final class BindPaneViewController: UIViewController { backendContent.alignment = .fill backendContent.spacing = 8 let section = UIKitTallyFormSectionView( - title: "Backend", + title: String(localized: "Backend"), detail: Self.backendDetail(for: bind.backends), contentView: backendContent ) @@ -340,7 +364,7 @@ final class BindPaneViewController: UIViewController { bind?.backends.setEnabled(enabled) } checks.accessibilityIdentifier = "bind.backendChecks" - backendContent.addArrangedSubview(bindBackendLabel("Backends")) + backendContent.addArrangedSubview(bindBackendLabel(String(localized: "Backends"))) backendContent.addArrangedSubview(checks) guard backends.isMixed else { return } @@ -353,10 +377,11 @@ final class BindPaneViewController: UIViewController { bind?.backends.setPreferred(backend) } bar.accessibilityIdentifier = "bind.backendBar" - let label = bindBackendLabel("New sessions run on") - label.accessibilityHint = - "The backend New Session, widgets, and Shortcuts start on " - + "unless they name another" + let label = bindBackendLabel(String(localized: "New sessions run on")) + label.accessibilityHint = String(localized: """ + The backend New Session, widgets, and Shortcuts start on \ + unless they name another + """) backendContent.addArrangedSubview(label) backendContent.addArrangedSubview(bar) backendContent.setCustomSpacing(16, after: checks) @@ -377,7 +402,7 @@ final class BindPaneViewController: UIViewController { bind?.keyPassphrase = text } let section = UIKitTallyFormSectionView( - title: "Key passphrase", + title: String(localized: "Key passphrase"), detail: BindPaneCopy.passphraseDetail, contentView: passphraseField ) @@ -411,7 +436,7 @@ final class BindPaneViewController: UIViewController { let scan = UIKitChassisChip( "SCAN QR", systemImage: "qrcode.viewfinder", - accessibilityLabel: "Scan QR code", + accessibilityLabel: String(localized: "Scan QR code"), action: { [weak self] in self?.presentScanner() } ) scan.accessibilityIdentifier = "bind.scan" @@ -445,7 +470,7 @@ final class BindPaneViewController: UIViewController { content.spacing = 10 return UIKitTallyFormSectionView( - title: "Somewhere else", + title: String(localized: "Somewhere else"), detail: BindPaneCopy.elsewhereDetail, contentView: content ) @@ -496,8 +521,12 @@ final class BindPaneViewController: UIViewController { .foregroundColor: UIKitChassis.signal3, ] ) + // A translation may not carry the command verbatim; NSNotFound would + // trap inside addAttribute. let range = (BindPaneCopy.footer as NSString).range(of: "mpx unbind") - attributed.addAttribute(.font, value: UIKitChassis.monoFont(10), range: range) + if range.location != NSNotFound { + attributed.addAttribute(.font, value: UIKitChassis.monoFont(10), range: range) + } label.attributedText = attributed let container = UIView() @@ -622,7 +651,7 @@ private final class BindIncomingSectionView: UIView { super.init(frame: .zero) accessibilityIdentifier = "bind.section.incoming" - let title = UIKitChassisLabel("Asking to bind", size: 10) + let title = UIKitChassisLabel(String(localized: "Asking to bind"), size: 10) title.accessibilityTraits.insert(.header) let header = UIView() header.backgroundColor = UIKitChassis.bezel @@ -711,7 +740,7 @@ private final class BindListeningRowView: UIView { let lamp = UIKitTallyLamp(caption: "LISTENING", color: TallyPalette.caution) let message = BindUI.label( - "No machine has answered yet.", + String(localized: "No machine has answered yet."), font: UIKitChassis.uiFont(11), color: UIKitChassis.signal2, lines: 1 @@ -776,7 +805,7 @@ final class BindCandidateRowView: UIView { func apply(_ pending: BindController.Pending) { nameLabel.setText(pending.name) - accessibilityLabel = "\(pending.name) is asking to bind" + accessibilityLabel = String(localized: "\(pending.name) is asking to bind") accessibilityIdentifier = "bind.candidate.\(pending.id)" userLabel.text = "\(pending.user.isEmpty ? "?" : pending.user) @ \(pending.name)" @@ -797,7 +826,7 @@ final class BindCandidateRowView: UIView { pinInput.setCandidate(name: pending.name, id: pending.id) pinInput.setPIN(pending.pin) - dismissChip.accessibilityLabel = "Dismiss \(pending.name)" + dismissChip.accessibilityLabel = String(localized: "Dismiss \(pending.name)") confirmChip.setContent(caption: pending.retryLabel, systemImage: nil) confirmChip.accessibilityLabel = "\(pending.retryLabel.capitalized) \(pending.name)" @@ -876,8 +905,10 @@ final class BindCandidateRowView: UIView { contestedLabel.textColor = TallyPalette.caution contestedLabel.numberOfLines = 0 contestedLabel.isHidden = true - contestedLabel.text = "Another announcement is using this name. If you started " - + "only one mpx bind, one of these isn't your machine — scan its QR instead." + contestedLabel.text = String(localized: """ + Another announcement is using this name. If you started \ + only one mpx bind, one of these isn't your machine — scan its QR instead. + """) contestedLabel.accessibilityIdentifier = "bind.candidate.contested" errorLabel.font = UIKitChassis.uiFont(10) @@ -892,12 +923,12 @@ final class BindCandidateRowView: UIView { dismissChip = UIKitChassisChip( "DISMISS", - accessibilityLabel: "Dismiss machine", + accessibilityLabel: String(localized: "Dismiss machine"), action: { [weak self] in self?.dismiss() } ) confirmChip = UIKitChassisChip( "ENROLL", - accessibilityLabel: "Enroll machine", + accessibilityLabel: String(localized: "Enroll machine"), action: { [weak self] in guard let self, self.canSubmit else { return } self.confirm() @@ -936,7 +967,7 @@ final class BindCandidateRowView: UIView { boundLabel.font = UIKitChassis.uiFont(10) boundLabel.textColor = UIKitChassis.signal2 - boundLabel.text = "Added to the fleet — it's on the deck now." + boundLabel.text = String(localized: "Added to the fleet — it's on the deck now.") boundLabel.numberOfLines = 0 boundLabel.isHidden = true @@ -1059,7 +1090,7 @@ private final class BindPINInputView: UIView, UITextFieldDelegate { required init?(coder: NSCoder) { fatalError("unused") } func setCandidate(name: String, id: String) { - field.accessibilityLabel = "PIN from \(name)’s terminal" + field.accessibilityLabel = String(localized: "PIN from \(name)’s terminal") field.accessibilityIdentifier = "bind.pin.\(id)" } @@ -1125,7 +1156,7 @@ private final class BindRevealableSecretField: UIView, UITextFieldDelegate { field.textColor = UIKitChassis.signal field.tintColor = UIKitChassis.signal field.attributedPlaceholder = NSAttributedString( - string: "Optional", + string: String(localized: "Optional"), attributes: [.foregroundColor: UIKitChassis.signal3] ) field.autocorrectionType = .no @@ -1136,7 +1167,7 @@ private final class BindRevealableSecretField: UIView, UITextFieldDelegate { field.autocapitalizationType = .none field.keyboardType = .asciiCapable field.textContentType = UITextContentType(rawValue: "") - field.accessibilityLabel = "Key passphrase" + field.accessibilityLabel = String(localized: "Key passphrase") field.accessibilityIdentifier = "bind.keyPassphrase" field.delegate = self field.addTarget(self, action: #selector(editingChanged), for: .editingChanged) @@ -1228,8 +1259,8 @@ private final class BindRevealableSecretField: UIView, UITextFieldDelegate { for: .normal ) revealButton.accessibilityLabel = revealed - ? "Hide Key passphrase" - : "Show Key passphrase" + ? String(localized: "Hide Key passphrase") + : String(localized: "Show Key passphrase") } private static func bullets(_ count: Int) -> String { @@ -1256,7 +1287,7 @@ private enum BindUI { @MainActor private final class BindPasteControl: UIPasteControl { - static let spokenLabel = "Paste bind code" + static let spokenLabel = String(localized: "Paste bind code") /// `UIPasteControl` rewrites its accessibility label to the generic /// “Paste” after its target/configuration updates. VoiceOver needs the @@ -1318,16 +1349,16 @@ private final class BindScannerViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Scan Bind Code" + title = String(localized: "Scan Bind Code") navigationItem.largeTitleDisplayMode = .never let cancel = UIBarButtonItem( - title: "Cancel", + title: String(localized: "Cancel"), style: .plain, target: self, action: #selector(cancelPressed) ) cancel.tintColor = UIKitChassis.signal - cancel.accessibilityLabel = "Cancel" + cancel.accessibilityLabel = String(localized: "Cancel") navigationItem.leftBarButtonItem = cancel guard DataScannerViewController.isSupported, @@ -1370,7 +1401,7 @@ private final class BindScannerViewController: UIViewController { private func showUnavailable() { view.backgroundColor = GlassPrototype.clearedChassis let label = UILabel() - label.text = "This device can’t scan. Paste the bind code instead." + label.text = String(localized: "This device can’t scan. Paste the bind code instead.") label.textColor = UIKitChassis.signal2 label.numberOfLines = 0 label.textAlignment = .center diff --git a/Multiplex/Views/Deck/ConnectionStatsView.swift b/Multiplex/Views/Deck/ConnectionStatsView.swift index 1fd4d5fb..2072bae6 100644 --- a/Multiplex/Views/Deck/ConnectionStatsView.swift +++ b/Multiplex/Views/Deck/ConnectionStatsView.swift @@ -49,21 +49,22 @@ final class ConnectionStatsViewController: UIViewController, AppAppearanceFollow override func viewDidLoad() { super.viewDidLoad() - title = "Connection Stats" + title = String(localized: "Connection Stats") view.backgroundColor = GlassPrototype.sheetGround navigationItem.largeTitleDisplayMode = .never #if os(visionOS) - navigationItem.titleView = UIKitChassisLabel("Connection Stats", size: 12) + navigationItem.titleView = UIKitChassisLabel( + String(localized: "Connection Stats"), size: 12) #endif let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done configureContent() @@ -100,9 +101,11 @@ final class ConnectionStatsViewController: UIViewController, AppAppearanceFollow footerLabel.font = UIKitChassis.monoFont(8) footerLabel.textColor = UIKitChassis.signal3 footerLabel.numberOfLines = 0 - footerLabel.text = "MOSH SAMPLES EVERY DATAGRAM · SSH HAS NO KEEPALIVE — " - + "RTT IS THE DECK PROBE · ECHO MEASURED WHILE YOU TYPE · " - + "SESSION-ONLY, NOTHING LEAVES THIS DEVICE" + footerLabel.text = String(localized: """ + MOSH SAMPLES EVERY DATAGRAM · SSH HAS NO KEEPALIVE — \ + RTT IS THE DECK PROBE · ECHO MEASURED WHILE YOU TYPE · \ + SESSION-ONLY, NOTHING LEAVES THIS DEVICE + """) contentStack.axis = .vertical contentStack.alignment = .fill @@ -152,7 +155,8 @@ final class ConnectionStatsViewController: UIViewController, AppAppearanceFollow notice.font = UIKitChassis.uiFont(13) notice.textColor = UIKitChassis.signal2 notice.numberOfLines = 0 - notice.text = "Connection stats are turned off in Settings." + notice.text = String( + localized: "Connection stats are turned off in Settings.") views.append(notice) replaceContent(with: views) return @@ -177,7 +181,7 @@ final class ConnectionStatsViewController: UIViewController, AppAppearanceFollow let empty = UILabel() empty.font = UIKitChassis.uiFont(13) empty.textColor = UIKitChassis.signal2 - empty.text = "No hosts yet." + empty.text = String(localized: "No hosts yet.") views.append(empty) } for row in rows { @@ -363,9 +367,10 @@ private final class ConnectionStatsStripView: UIKitTallyBorderedView { headerRow.accessibilityLabel = "\(host.name), " + (runLabel.text ?? "").lowercased() - + (expanded ? ", expanded" : "") + + (expanded ? String(localized: ", expanded") : "") headerRow.accessibilityHint = expanded - ? "Collapses connection details" : "Expands connection details" + ? String(localized: "Collapses connection details") + : String(localized: "Expands connection details") rebuildCluster( stats: stats, @@ -502,9 +507,12 @@ private final class ConnectionStatsStripView: UIKitTallyBorderedView { // The honesty line: what this host's numbers actually are. let honesty = label(size: 8, ink: UIKitChassis.signal3, lines: 0) honesty.text = mosh != nil - ? "MOSH LINK — SRTT FROM EVERY DATAGRAM · LOSS FROM SEQUENCE GAPS" - : "SSH LINK — NO KEEPALIVE · RTT SAMPLED BY THE DECK PROBE · " - + "ECHO MEASURED WHILE YOU TYPE" + ? String(localized: + "MOSH LINK — SRTT FROM EVERY DATAGRAM · LOSS FROM SEQUENCE GAPS") + : String(localized: """ + SSH LINK — NO KEEPALIVE · RTT SAMPLED BY THE DECK PROBE · \ + ECHO MEASURED WHILE YOU TYPE + """) clusterStack.addArrangedSubview(hairline()) clusterStack.addArrangedSubview(padded(honesty)) } diff --git a/Multiplex/Views/Deck/DeckWindow.swift b/Multiplex/Views/Deck/DeckWindow.swift index 34131f4d..58ddaf48 100644 --- a/Multiplex/Views/Deck/DeckWindow.swift +++ b/Multiplex/Views/Deck/DeckWindow.swift @@ -1015,18 +1015,25 @@ final class DeckWindowViewController: UIViewController { private func presentLocalNetworkAlert() { let alert = UIAlertController( - title: "Local Network Access Is Off", - message: "Multiplex can’t reach SSH hosts on your local network. Turn on Local Network access in Settings.", + title: String(localized: "Local Network Access Is Off"), + message: String(localized: """ + Multiplex can’t reach SSH hosts on your local network. Turn on \ + Local Network access in Settings. + """), preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Open Settings"), style: .default + ) { [weak self] _ in guard let self, let url = URL(string: UIApplication.openSettingsURLString) else { return } self.configuration.openURL(url) self.alertActionEndedPresentation() }) - alert.addAction(UIAlertAction(title: "Not Now", style: .cancel) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Not Now"), style: .cancel + ) { [weak self] _ in self?.alertActionEndedPresentation() }) ownedPresentation = alert diff --git a/Multiplex/Views/Deck/FleetWall.swift b/Multiplex/Views/Deck/FleetWall.swift index c1e28f9a..c8205380 100644 --- a/Multiplex/Views/Deck/FleetWall.swift +++ b/Multiplex/Views/Deck/FleetWall.swift @@ -166,7 +166,7 @@ final class FleetWallContainerViewController: UIViewController { let statsChip = UIKitChassisChip( "STATS", systemImage: "waveform.path.ecg", - accessibilityLabel: "Connection stats" + accessibilityLabel: String(localized: "Connection stats") ) { [weak self] in self?.configuration.openStats(nil) } navigationStatsChip = statsChip observeStatsVisibility() @@ -174,20 +174,20 @@ final class FleetWallContainerViewController: UIViewController { UIKitChassisChip( "HOST", systemImage: "plus", - accessibilityLabel: "Add host", + accessibilityLabel: String(localized: "Add host"), action: configuration.addHost ), statsChip, UIKitChassisChip( "FAQ", systemImage: "questionmark", - accessibilityLabel: "Frequently asked questions", + accessibilityLabel: String(localized: "Frequently asked questions"), action: configuration.openFAQ ), UIKitChassisChip( "SETTINGS", systemImage: "gearshape", - accessibilityLabel: "Settings", + accessibilityLabel: String(localized: "Settings"), action: configuration.openSettings ), ]) @@ -248,7 +248,7 @@ private final class FleetNavigationTitleView: UIView { func setSummary(_ summary: String) { summaryLabel.text = summary - accessibilityLabel = "Multiplex, \(summary)" + accessibilityLabel = String(localized: "Multiplex, \(summary)") } } @@ -716,14 +716,20 @@ final class FleetWallViewController: UIViewController { let name = backend.rawValue let primary = host.sessionBackend.rawValue let alert = UIAlertController( - title: "Also Show \(name) Sessions", - message: "\(host.name) is running \(name) as well as \(primary). " - + "Showing both \(HostGuide.secondBackendCost)." - + "\n\nNew sessions still start on \(primary).", + title: String(localized: "Also Show \(name) Sessions"), + message: String(localized: """ + \(host.name) is running \(name) as well as \(primary). \ + Showing both \(HostGuide.secondBackendCost). + + New sessions still start on \(primary). + """), preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - alert.addAction(UIAlertAction(title: "Show Them", style: .default) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Cancel"), style: .cancel)) + alert.addAction(UIAlertAction( + title: String(localized: "Show Them"), style: .default + ) { [weak self] _ in self?.configuration.store.setSecondaryBackend( true, backend: backend, for: host.id) }) @@ -913,15 +919,16 @@ final class FleetWallViewController: UIViewController { if case .failed(let reason) = model.phase { message = reason } else { - message = "Couldn't create the session on \(host.name)." + message = String( + localized: "Couldn't create the session on \(host.name).") } } let alert = UIAlertController( - title: "Couldn't Create Session", + title: String(localized: "Couldn't Create Session"), message: message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "OK", style: .cancel)) + alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .cancel)) // The sheet dismisses on the press and the mint answers a // round-trip later, so the sheet is normally gone — but a slow // dismissal must not swallow the alert. Present from whatever is @@ -1019,14 +1026,16 @@ final class FleetWallViewController: UIViewController { if case .failed(let reason) = model.phase { message = reason } else { - message = "Couldn't open a new tab in session \(session) on \(host.name)." + message = String(localized: """ + Couldn't open a new tab in session \(session) on \(host.name). + """) } let alert = UIAlertController( - title: "Couldn't Create Tab", + title: String(localized: "Couldn't Create Tab"), message: message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "OK", style: .cancel)) + alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .cancel)) present(alert, animated: true) } @@ -1036,45 +1045,58 @@ final class FleetWallViewController: UIViewController { // The copy carries both truths because the app deliberately // doesn't know which session is the default. let message = host.sessionBackend == .herdr - ? "Stops “\(session.name)” on \(host.name) and everything running in it, " - + "and deletes its saved state (herdr keeps its default session on disk, stopped)." - : "Kills “\(session.name)” on \(host.name) and everything running in it." + ? String(localized: """ + Stops “\(session.name)” on \(host.name) and everything running in it, \ + and deletes its saved state (herdr keeps its default session on disk, stopped). + """) + : String(localized: """ + Kills “\(session.name)” on \(host.name) and everything running in it. + """) let alert = UIAlertController( - title: "Delete Session", + title: String(localized: "Delete Session"), message: message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Delete"), style: .destructive + ) { [weak self] _ in guard let self else { return } let model = self.configuration.hub.model(for: host) Task { await model.killSession(session) } }) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction( + title: String(localized: "Cancel"), style: .cancel)) present(alert, animated: true) } private func confirmRemove(_ host: Host) { let alert = UIAlertController( - title: "Remove Host", - message: "Removes “\(host.name)” and its saved secret from this device and your synced devices. tmux sessions on the host keep running.", + title: String(localized: "Remove Host"), + message: String(localized: """ + Removes “\(host.name)” and its saved secret from this device and \ + your synced devices. tmux sessions on the host keep running. + """), preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "Remove", style: .destructive) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Remove"), style: .destructive + ) { [weak self] _ in guard let self else { return } self.configuration.hub.dropModel(for: host.id) self.configuration.store.remove(host) }) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction( + title: String(localized: "Cancel"), style: .cancel)) present(alert, animated: true) } private func presentUnreachable(host: Host, reason: String) { let alert = UIAlertController( - title: "\(host.name) Unreachable", + title: String(localized: "\(host.name) Unreachable"), message: reason, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "OK", style: .cancel)) + alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .cancel)) present(alert, animated: true) } @@ -1189,22 +1211,22 @@ private final class FleetHeaderView: UIView { addChip = UIKitChassisChip( "HOST", systemImage: "plus", - accessibilityLabel: "Add host" + accessibilityLabel: String(localized: "Add host") ) { [weak self] in self?.actions.addHost() } statsChip = UIKitChassisChip( "STATS", systemImage: "waveform.path.ecg", - accessibilityLabel: "Connection stats" + accessibilityLabel: String(localized: "Connection stats") ) { [weak self] in self?.actions.openStats() } faqChip = UIKitChassisChip( "FAQ", systemImage: "questionmark", - accessibilityLabel: "Frequently asked questions" + accessibilityLabel: String(localized: "Frequently asked questions") ) { [weak self] in self?.actions.openFAQ() } settingsChip = UIKitChassisChip( "SETTINGS", systemImage: "gearshape", - accessibilityLabel: "Settings" + accessibilityLabel: String(localized: "Settings") ) { [weak self] in self?.actions.openSettings() } summaryLabel.font = UIKitChassis.monoFont(11) summaryLabel.textColor = UIKitChassis.signal2 @@ -1855,26 +1877,30 @@ private final class FleetHostSectionView: UIView { private var hostMenu: UIMenu { let moveUp = UIAction( - title: "Move Up", + title: String(localized: "Move Up"), image: UIImage(systemName: "arrow.up"), attributes: configuration.store.canMoveUp(host) ? [] : [.disabled] ) { [weak self] _ in self?.configuration.moveUp() } let moveDown = UIAction( - title: "Move Down", + title: String(localized: "Move Down"), image: UIImage(systemName: "arrow.down"), attributes: configuration.store.canMoveDown(host) ? [] : [.disabled] ) { [weak self] _ in self?.configuration.moveDown() } let enabled = UIAction( - title: host.isEnabled ? "Disable Host" : "Enable Host", + title: host.isEnabled + ? String(localized: "Disable Host") + : String(localized: "Enable Host"), image: UIImage(systemName: host.isEnabled ? "pause.circle" : "play.circle") ) { [weak self] _ in guard let self else { return } self.configuration.setEnabled(!self.host.isEnabled) } - let edit = UIAction(title: "Edit Host…") { [weak self] _ in + let edit = UIAction(title: String(localized: "Edit Host…")) { [weak self] _ in self?.configuration.editHost() } - let remove = UIAction(title: "Remove Host…", attributes: .destructive) { [weak self] _ in + let remove = UIAction( + title: String(localized: "Remove Host…"), attributes: .destructive + ) { [weak self] _ in self?.configuration.removeHost() } var actions: [UIMenuElement] = [enabled] @@ -1883,7 +1909,7 @@ private final class FleetHostSectionView: UIView { // `statsEnabled` is in the identity. if currentSnapshot?.statsEnabled == true { actions.append(UIAction( - title: "Connection Stats…", + title: String(localized: "Connection Stats…"), image: UIImage(systemName: "waveform.path.ecg") ) { [weak self] _ in self?.configuration.showStats() }) } @@ -1934,7 +1960,9 @@ struct FleetBackendOffer: Equatable { } var sessionNoun: String { - sessionCount == 1 ? "1 session" : "\(sessionCount) sessions" + sessionCount == 1 + ? String(localized: "1 session") + : String(localized: "\(sessionCount) sessions") } } @@ -2086,12 +2114,12 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate // host and the rail's accessibility label keeps both. address.isHidden = hidesHostAddress let mosh = FleetBadgeView(caption: "MOSH") - mosh.accessibilityLabel = "Connects over mosh" + mosh.accessibilityLabel = String(localized: "Connects over mosh") mosh.isHidden = !host.useMosh // tmux is the app's premise, so only the deviation is badged — // the same rule the mosh badge follows for SSH. let backend = FleetBadgeView(caption: "HERDR") - backend.accessibilityLabel = "Sessions run under herdr" + backend.accessibilityLabel = String(localized: "Sessions run under herdr") backend.isHidden = host.sessionBackend != .herdr let status = makeStatus( host: host, @@ -2106,7 +2134,7 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate ) let shell = UIKitChassisChip( "SHELL", - accessibilityLabel: "Open shell on \(host.name)", + accessibilityLabel: String(localized: "Open shell on \(host.name)"), action: { [weak self] in self?.openShell() } ) shell.alpha = connected ? 1 : 0 @@ -2119,13 +2147,13 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate if statsEnabled { let chip = self.statsChip ?? UIKitChassisChip( statsCaption ?? "", - accessibilityLabel: "Connection stats", + accessibilityLabel: String(localized: "Connection stats"), action: { [weak self] in self?.showStats() } ) - chip.accessibilityHint = "Opens connection stats" + chip.accessibilityHint = String(localized: "Opens connection stats") chip.accessibilityLabel = statsCaption.map { - "Connection stats for \(host.name): \($0.lowercased())" - } ?? "Connection stats" + String(localized: "Connection stats for \(host.name): \($0.lowercased())") + } ?? String(localized: "Connection stats") self.statsChip = chip chip.isHidden = statsCaption == nil stats = chip @@ -2136,7 +2164,8 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate let menuButton = FleetMenuBadgeButton() menuButton.menu = menu menuButton.showsMenuAsPrimaryAction = true - menuButton.accessibilityLabel = "Host options for \(host.name)" + menuButton.accessibilityLabel = String( + localized: "Host options for \(host.name)") // Neutral chips: an offer is an available ACTION, not live state, so // it never wears tally red (`DESIGN.md` — color is state). let offerChips = offers.map { makeOfferChip($0, host: host) } @@ -2200,8 +2229,8 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate statsChip.isHidden = caption == nil statsChip.setContent(caption: caption ?? "", systemImage: nil) statsChip.accessibilityLabel = caption.map { - "Connection stats for \(hostName): \($0.lowercased())" - } ?? "Connection stats" + String(localized: "Connection stats for \(hostName): \($0.lowercased())") + } ?? String(localized: "Connection stats") } /// `+ HERDR · 3` — press to start monitoring, long-press to stop being @@ -2214,14 +2243,15 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate let name = offer.backend.rawValue let chip = UIKitChassisChip( offer.chipCaption, - accessibilityLabel: - "\(host.name) is also running \(offer.sessionNoun) under \(name)", + accessibilityLabel: String(localized: """ + \(host.name) is also running \(offer.sessionNoun) under \(name) + """), action: { [weak self] in self?.acceptOffer(offer.backend) } ) - chip.accessibilityHint = "Shows them on this host's deck" + chip.accessibilityHint = String(localized: "Shows them on this host's deck") offerMenus[ObjectIdentifier(chip)] = UIMenu(children: [ UIAction( - title: "Don't Offer \(name) Here", + title: String(localized: "Don't Offer \(name) Here"), image: UIImage(systemName: "bell.slash") ) { [weak self] _ in self?.dismissOffer(offer.backend) }, ]) @@ -2257,7 +2287,9 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "DISABLED", dotColor: TallyPalette.signal3, textColor: UIKitChassis.signal3, - accessibilityLabel: "\(host.name) is disabled and is not being connected" + accessibilityLabel: String(localized: """ + \(host.name) is disabled and is not being connected + """) ) } if networkOffline { @@ -2265,7 +2297,8 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "OFFLINE", dotColor: TallyPalette.signal3, textColor: UIKitChassis.signal3, - accessibilityLabel: "This device has no network connection" + accessibilityLabel: String( + localized: "This device has no network connection") ) } guard let phase else { @@ -2273,7 +2306,7 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "STANDBY", dotColor: nil, textColor: UIKitChassis.signal3, - accessibilityLabel: "Standby" + accessibilityLabel: String(localized: "Standby") ) } switch phase { @@ -2283,8 +2316,12 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "KEYCHAIN LOCKED", dotColor: TallyPalette.caution, textColor: TallyPalette.caution, - accessibilityLabel: "\(host.name): the Mac's keychain is locked, so Claude Code shows signed out", - accessibilityHint: "Shows how to unlock the keychain", + accessibilityLabel: String(localized: """ + \(host.name): the Mac's keychain is locked, so Claude Code \ + shows signed out + """), + accessibilityHint: String( + localized: "Shows how to unlock the keychain"), action: { showKeychainGuide(keychainNotice.sessionNames) } ) } @@ -2292,14 +2329,14 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "CONNECTED", dotColor: TallyPalette.ok, textColor: UIKitChassis.signal2, - accessibilityLabel: "\(host.name) connected" + accessibilityLabel: String(localized: "\(host.name) connected") ) case .connecting: return FleetRailStatusView( text: "LINKING", dotColor: UIKitChassis.signal2, textColor: UIKitChassis.signal2, - accessibilityLabel: "\(host.name) linking", + accessibilityLabel: String(localized: "\(host.name) linking"), pulsing: !reduceMotion ) case .failed(let reason): @@ -2308,8 +2345,10 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "NEEDS PASSPHRASE", dotColor: TallyPalette.caution, textColor: TallyPalette.caution, - accessibilityLabel: "\(host.name) needs its SSH key passphrase", - accessibilityHint: "Opens the SSH key passphrase prompt", + accessibilityLabel: String( + localized: "\(host.name) needs its SSH key passphrase"), + accessibilityHint: String( + localized: "Opens the SSH key passphrase prompt"), action: requestPassphrase ) } @@ -2317,8 +2356,9 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "UNREACHABLE", dotColor: TallyPalette.signal3, textColor: UIKitChassis.signal3, - accessibilityLabel: "\(host.name) unreachable", - accessibilityHint: "Shows why the host could not be reached", + accessibilityLabel: String(localized: "\(host.name) unreachable"), + accessibilityHint: String( + localized: "Shows why the host could not be reached"), action: { showUnreachable(reason) } ) case .idle: @@ -2326,7 +2366,7 @@ private final class FleetHostRailView: UIView, UIContextMenuInteractionDelegate text: "STANDBY", dotColor: nil, textColor: UIKitChassis.signal3, - accessibilityLabel: "\(host.name) standby" + accessibilityLabel: String(localized: "\(host.name) standby") ) } } @@ -2906,11 +2946,14 @@ final class FleetSessionTileView: FleetPressView, configuration.newHerdrTab() }) } - children.append(UIAction(title: "Copy Command for Handoff") { _ in + children.append(UIAction(title: String(localized: "Copy Command for Handoff")) { _ in configuration.copyHandoffCommand() }) children.append( - UIAction(title: "Delete Session…", attributes: .destructive) { _ in + UIAction( + title: String(localized: "Delete Session…"), + attributes: .destructive + ) { _ in configuration.delete() }) return UIMenu(children: children) @@ -2925,7 +2968,8 @@ final class FleetSessionTileView: FleetPressView, agentRunning: running, agentNeedsYou: needsYou ) - accessibilityHint = "Long press and drag to reorder within this host" + accessibilityHint = String( + localized: "Long press and drag to reorder within this host") invalidateIntrinsicContentSize() } @@ -3266,11 +3310,13 @@ final class FleetSessionTileView: FleetPressView, private func spineSummary(_ session: TmuxSession) -> String { let activeWindow = session.windows.first(where: \.isActive) - let active = activeWindow.map { "\($0.name) active" } ?? "" + let active = activeWindow.map { String(localized: "\($0.name) active") } ?? "" let title = activeWindow? .displayPaneTitle(serverHost: session.serverHost) - .map { ", titled \($0)" } ?? "" - return "\(session.windowCount) windows, \(session.paneCount) panes. \(active)\(title)" + .map { String(localized: ", titled \($0)") } ?? "" + return String(localized: """ + \(session.windowCount) windows, \(session.paneCount) panes. \(active)\(title) + """) } private func accessibilitySummary( @@ -3285,16 +3331,21 @@ final class FleetSessionTileView: FleetPressView, // lives. Said right after the name it qualifies, and only where it // qualifies anything. if configuration.showsBackendIdentity { - parts.append("on \(configuration.sessionBackend.rawValue)") + parts.append(String( + localized: "on \(configuration.sessionBackend.rawValue)")) } - parts.append(configuration.isLive ? "live" : "not attached") - if agentNeedsYou { parts.append("agent needs your input") } - if agentRunning { parts.append("agent running") } - parts.append("\(session.windowCount) windows and \(session.paneCount) panes") + parts.append(configuration.isLive + ? String(localized: "live") + : String(localized: "not attached")) + if agentNeedsYou { parts.append(String(localized: "agent needs your input")) } + if agentRunning { parts.append(String(localized: "agent running")) } + parts.append(String(localized: """ + \(session.windowCount) windows and \(session.paneCount) panes + """)) return parts.joined(separator: ", ") + (configuration.hasOpenTab ? ". \(configuration.openTabAccessibilityText)" - : ". Attach") + : String(localized: ". Attach")) } } @@ -3367,7 +3418,8 @@ private final class FleetWindowSegmentView: UIView { @MainActor private final class FleetNewSessionTileView: FleetPressView { - private let label = UIKitChassisLabel("+ New Session", size: 11, color: UIKitChassis.signal2) + private let label = UIKitChassisLabel( + String(localized: "+ New Session"), size: 11, color: UIKitChassis.signal2) private let dashLayer = CAShapeLayer() private var heightConstraint: NSLayoutConstraint? @@ -3398,7 +3450,7 @@ private final class FleetNewSessionTileView: FleetPressView { func configure(hostName: String, compact: Bool, action: @escaping () -> Void) { pressAction = action heightConstraint?.constant = compact ? 92 : 138 - accessibilityLabel = "New session on \(hostName)" + accessibilityLabel = String(localized: "New session on \(hostName)") setNeedsLayout() } @@ -3456,21 +3508,25 @@ private final class FleetNoSignalTileView: FleetPressView { let badge: String switch mode { case .unreachable: - caption = "No Signal" + caption = String(localized: "No Signal") ink = UIKitChassis.signal3 badge = "RECONNECT" - accessibilityLabel = "\(host.name) unreachable. Reconnect" + accessibilityLabel = String( + localized: "\(host.name) unreachable. Reconnect") case .passphrase: - caption = "Passphrase Required" + caption = String(localized: "Passphrase Required") ink = TallyPalette.caution badge = "UNLOCK" - accessibilityLabel = "\(host.name) needs its SSH key passphrase. Unlock" + accessibilityLabel = String(localized: """ + \(host.name) needs its SSH key passphrase. Unlock + """) case .disabled: - caption = "Disabled" + caption = String(localized: "Disabled") ink = UIKitChassis.signal3 badge = "ENABLE" - accessibilityLabel = "\(host.name) is disabled. Enable" - accessibilityHint = "Starts monitoring this host on the deck again" + accessibilityLabel = String(localized: "\(host.name) is disabled. Enable") + accessibilityHint = String( + localized: "Starts monitoring this host on the deck again") } let title = UIKitChassisLabel(caption, size: 13, color: ink) screen.addSubview(title) @@ -3521,7 +3577,7 @@ private final class FleetAcquiringTileView: UIKitTallyBorderedView { addSubview(screen) screen.translatesAutoresizingMaskIntoConstraints = false let label = UIKitChassisLabel( - "Acquiring signal", size: 10, color: UIKitChassis.signal3 + String(localized: "Acquiring signal"), size: 10, color: UIKitChassis.signal3 ) let stack = UIStackView(arrangedSubviews: [spinner, label]) stack.axis = .vertical @@ -3546,7 +3602,7 @@ private final class FleetAcquiringTileView: UIKitTallyBorderedView { spinner.color = UIKitChassis.signal2 spinner.startAnimating() isAccessibilityElement = true - accessibilityLabel = "Acquiring signal" + accessibilityLabel = String(localized: "Acquiring signal") } func configure(compact: Bool) { @@ -3567,17 +3623,18 @@ private final class FleetTmuxMissingTileView: UIKitTallyBorderedView { super.init(frame: frame) installChip = UIKitChassisChip( "INSTALL GUIDE", - accessibilityLabel: "Install guide" + accessibilityLabel: String(localized: "Install guide") ) { [weak self] in self?.action?() } // The one-tap side of decision "explicit + detect hint": shown only // when the probe saw herdr installed while tmux is missing. A tap // rewrites the host record; nothing ever flips it automatically. switchChip = UIKitChassisChip( "USE HERDR", - accessibilityLabel: "Switch this host to the herdr backend" + accessibilityLabel: String( + localized: "Switch this host to the herdr backend") ) { [weak self] in self?.switchAction?() } title = UIKitChassisLabel( - "No tmux on host", size: 11, color: UIKitChassis.signal3 + String(localized: "No tmux on host"), size: 11, color: UIKitChassis.signal3 ) let body = UILabel() // A semantic role keeps Dynamic Type and the scene root's @@ -3586,7 +3643,7 @@ private final class FleetTmuxMissingTileView: UIKitTallyBorderedView { body.font = .preferredFont(forTextStyle: .footnote) body.adjustsFontForContentSizeCategory = true body.textColor = UIKitChassis.signal2 - body.text = "You can still use a plain shell — press SHELL." + body.text = String(localized: "You can still use a plain shell — press SHELL.") body.numberOfLines = 0 body.textAlignment = .center let stack = UIStackView(arrangedSubviews: [title, body, installChip, switchChip]) @@ -3613,7 +3670,7 @@ private final class FleetTmuxMissingTileView: UIKitTallyBorderedView { ) { self.action = action switchAction = switchToHerdr - title.setText("No \(backend.rawValue) on host") + title.setText(String(localized: "No \(backend.rawValue) on host")) // The hint only ever means "tmux is dead but herdr is installed" — // the tmux probe is the sole writer — so it alone decides the chip. switchChip.isHidden = !herdrHint @@ -3633,7 +3690,7 @@ private final class FleetAwaitingSignalView: UIView { "ADD HOST", systemImage: "plus", prominent: true, - accessibilityLabel: "Add host" + accessibilityLabel: String(localized: "Add host") ) { [weak self] in self?.action?() } tile.backgroundColor = UIKitChassis.bezel addSubview(tile) @@ -3662,13 +3719,13 @@ private final class FleetAwaitingSignalView: UIView { let screen = UIKitTallyHatchView() let title = UIKitChassisLabel( - "Awaiting signal", size: 13, color: UIKitChassis.signal3 + String(localized: "Awaiting signal"), size: 13, color: UIKitChassis.signal3 ) let body = UILabel() body.font = .preferredFont(forTextStyle: .footnote) body.adjustsFontForContentSizeCategory = true body.textColor = UIKitChassis.signal2 - body.text = "Every tmux session, its own window in space." + body.text = String(localized: "Every tmux session, its own window in space.") body.textAlignment = .center let route = UILabel() route.font = UIKitChassis.monoFont(10) @@ -3690,7 +3747,8 @@ private final class FleetAwaitingSignalView: UIView { ]) content.addArrangedSubview(screen) - let noHosts = UIKitChassisLabel("No hosts", size: 12, color: UIKitChassis.signal3) + let noHosts = UIKitChassisLabel( + String(localized: "No hosts"), size: 12, color: UIKitChassis.signal3) let row = UIStackView(arrangedSubviews: [noHosts, UIView(), addChip]) row.axis = .horizontal row.alignment = .center @@ -3873,7 +3931,7 @@ struct NewSessionFormState { } var commandPreview: String { - guard let agentToLaunch else { return "login shell" } + guard let agentToLaunch else { return String(localized: "login shell") } return agentToLaunch.launchCommand(model: modelToLaunch, initialPrompt: "") } @@ -3924,36 +3982,56 @@ struct NewSessionFormState { var targetDetail: String { guard let tabTargetSession else { - return "A fresh \(backend.rawValue) session, its own tile " - + "on the deck. Choose a session to add a tab to its focused " - + "workspace instead." + return String(localized: """ + A fresh \(backend.rawValue) session, its own tile \ + on the deck. Choose a session to add a tab to its focused \ + workspace instead. + """) } - return "Adds a tab to “\(tabTargetSession)”'s focused workspace — " - + "no new session is created." + return String(localized: """ + Adds a tab to “\(tabTargetSession)”'s focused workspace — \ + no new session is created. + """) } var launchDetail: String { let remembers = host.sessionScripts.isEmpty - ? "REMEMBER saves only the launch choice." - : "REMEMBER saves the launch and setup-script choices." + ? String(localized: "REMEMBER saves only the launch choice.") + : String(localized: "REMEMBER saves the launch and setup-script choices.") if let tabTargetSession { guard let agentToLaunch else { - return "Opens the new tab's shell in “\(tabTargetSession)”. \(remembers)" + return String(localized: """ + Opens the new tab's shell in “\(tabTargetSession)”. \(remembers) + """) } - return "Starts \(agentToLaunch.displayName) in the new tab. The optional prompt becomes its first message; \(remembers)" + return String(localized: """ + Starts \(agentToLaunch.displayName) in the new tab. The optional \ + prompt becomes its first message; \(remembers) + """) } guard let agentToLaunch else { - return "Creates the \(backend.rawValue) session, then attaches " - + "to its login shell. \(remembers)" + return String(localized: """ + Creates the \(backend.rawValue) session, then attaches \ + to its login shell. \(remembers) + """) } - return "Starts \(agentToLaunch.displayName) in the fresh shell. The optional prompt becomes its first message; \(remembers)" + return String(localized: """ + Starts \(agentToLaunch.displayName) in the fresh shell. The optional \ + prompt becomes its first message; \(remembers) + """) } var scriptDetail: String { guard let script else { - return "Nothing extra runs. A setup script is typed into the fresh shell before the launch." + return String(localized: """ + Nothing extra runs. A setup script is typed into the fresh shell \ + before the launch. + """) } - return "Types \(script.displayName) into the fresh shell first, so the launch inherits what it sets up." + return String(localized: """ + Types \(script.displayName) into the fresh shell first, so the launch \ + inherits what it sets up. + """) } /// What an empty directory means, by target: a fresh session spawns at @@ -3961,19 +4039,25 @@ struct NewSessionFormState { /// starts it where the session's focused pane is (the `+ TAB` row's /// behavior — "another one here"). var directoryFallbackTitle: String { - tabTargetSession == nil ? "Home" : "Focused Pane" + tabTargetSession == nil + ? String(localized: "Home") + : String(localized: "Focused Pane") } var directoryDetail: String { let fallback = tabTargetSession == nil - ? "Uses the host's login-shell home directory." - : "Uses the focused pane's directory — where the session is looking now." + ? String(localized: "Uses the host's login-shell home directory.") + : String(localized: """ + Uses the focused pane's directory — where the session is looking now. + """) guard !host.workingDirs.isEmpty else { return fallback } if let directory { let alternative = tabTargetSession == nil - ? "Choose Home to use the login shell's default." - : "Choose Focused Pane to inherit the session's own directory." - return "Starts in \(directory). \(alternative)" + ? String(localized: "Choose Home to use the login shell's default.") + : String(localized: """ + Choose Focused Pane to inherit the session's own directory. + """) + return String(localized: "Starts in \(directory). \(alternative)") } return fallback } @@ -4046,30 +4130,31 @@ final class NewSessionViewController: UIViewController, override func viewDidLoad() { super.viewDidLoad() - title = "New Session" + title = String(localized: "New Session") view.backgroundColor = GlassPrototype.sheetGround navigationItem.largeTitleDisplayMode = .never #if os(visionOS) - navigationItem.titleView = UIKitChassisLabel("New Session", size: 12) + navigationItem.titleView = UIKitChassisLabel( + String(localized: "New Session"), size: 12) #endif let cancel = UIBarButtonItem( - title: "Cancel", + title: String(localized: "Cancel"), style: .plain, target: self, action: #selector(cancelPressed) ) cancel.tintColor = UIKitChassis.signal - cancel.accessibilityLabel = "Cancel" + cancel.accessibilityLabel = String(localized: "Cancel") navigationItem.leftBarButtonItem = cancel let createItem = UIBarButtonItem( - title: "Create & Attach", + title: String(localized: "Create & Attach"), style: .plain, target: self, action: #selector(createPressed) ) createItem.tintColor = UIKitChassis.signal - createItem.accessibilityLabel = "Create and attach" + createItem.accessibilityLabel = String(localized: "Create and attach") navigationItem.rightBarButtonItem = createItem self.createItem = createItem @@ -4184,9 +4269,11 @@ final class NewSessionViewController: UIViewController, bar.accessibilityIdentifier = "newSession.backend" backendChoiceBar = bar return FleetFormSectionView( - title: "Runs on", - detail: "This host shows both. New sessions start on the one " - + "chosen here; its default is set in Host Settings.", + title: String(localized: "Runs on"), + detail: String(localized: """ + This host shows both. New sessions start on the one \ + chosen here; its default is set in Host Settings. + """), rows: [bar] ) } @@ -4208,7 +4295,8 @@ final class NewSessionViewController: UIViewController, row.axis = .horizontal row.alignment = .center row.spacing = 12 - return FleetFormSectionView(title: "Target host", rows: [row]) + return FleetFormSectionView( + title: String(localized: "Target host"), rows: [row]) } /// The herdr-only Creates row: a fresh session (the default), or a tab @@ -4217,11 +4305,11 @@ final class NewSessionViewController: UIViewController, /// a tab could land in. private func makeCreatesSection() -> FleetFormSectionView { let button = FleetMenuFieldButton() - button.accessibilityLabel = "Creates" + button.accessibilityLabel = String(localized: "Creates") button.accessibilityIdentifier = "newSession.creates" createsButton = button return FleetFormSectionView( - title: "Creates", + title: String(localized: "Creates"), detail: form.targetDetail, rows: [button] ) @@ -4231,16 +4319,21 @@ final class NewSessionViewController: UIViewController, configureTextField( nameField, placeholder: form.defaultNameBase, - accessibilityLabel: "Name" + accessibilityLabel: String(localized: "Name") ) nameField.text = form.name nameField.returnKeyType = .next nameField.addTarget(self, action: #selector(nameChanged), for: .editingChanged) nameField.accessibilityIdentifier = "newSession.name" - let row = makeField(label: "Name", input: makeWell(containing: nameField)) + let row = makeField( + label: String(localized: "Name"), + input: makeWell(containing: nameField) + ) return FleetFormSectionView( - title: "Session identity", - detail: "Shown on the deck and in the terminal window's source label.", + title: String(localized: "Session identity"), + detail: String(localized: """ + Shown on the deck and in the terminal window's source label. + """), rows: [row] ) } @@ -4260,8 +4353,8 @@ final class NewSessionViewController: UIViewController, configureTextField( modelField, - placeholder: "Agent default", - accessibilityLabel: "Optional model" + placeholder: String(localized: "Agent default"), + accessibilityLabel: String(localized: "Optional model") ) modelField.returnKeyType = .next modelField.addTarget(self, action: #selector(modelChanged), for: .editingChanged) @@ -4276,8 +4369,11 @@ final class NewSessionViewController: UIViewController, promptView.accessibilityIdentifier = "newSession.initialPrompt" promptView.onTextChange = { [weak self] text in self?.form.initialPrompt = text } let fields = UIStackView(arrangedSubviews: [ - makeField(label: "Model (optional)", input: modelWell), - makeField(label: "Initial prompt (optional)", input: makeWell(containing: promptView)), + makeField(label: String(localized: "Model (optional)"), input: modelWell), + makeField( + label: String(localized: "Initial prompt (optional)"), + input: makeWell(containing: promptView) + ), ]) fields.axis = .vertical fields.alignment = .fill @@ -4294,7 +4390,8 @@ final class NewSessionViewController: UIViewController, commandLabel.lineBreakMode = .byTruncatingHead commandLabel.textAlignment = .right commandLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - let commandCaption = UIKitChassisLabel("Command", size: 7, color: UIKitChassis.signal3) + let commandCaption = UIKitChassisLabel( + String(localized: "Command"), size: 7, color: UIKitChassis.signal3) let commandStack = UIStackView(arrangedSubviews: [commandCaption, commandLabel]) commandStack.axis = .vertical commandStack.alignment = .trailing @@ -4305,7 +4402,7 @@ final class NewSessionViewController: UIViewController, rememberRow.spacing = 12 let section = FleetFormSectionView( - title: "Launch", + title: String(localized: "Launch"), detail: form.launchDetail, rows: [launchChoice, fields, rememberRow] ) @@ -4315,13 +4412,13 @@ final class NewSessionViewController: UIViewController, private func makeScriptSection() -> FleetFormSectionView { let button = FleetMenuFieldButton() - button.accessibilityLabel = "Setup script" + button.accessibilityLabel = String(localized: "Setup script") button.accessibilityIdentifier = "newSession.script" scriptButton = button return FleetFormSectionView( - title: "Setup script", + title: String(localized: "Setup script"), detail: form.scriptDetail, - rows: [makeField(label: "Runs first", input: button)] + rows: [makeField(label: String(localized: "Runs first"), input: button)] ) } @@ -4330,7 +4427,7 @@ final class NewSessionViewController: UIViewController, let starts = UILabel() starts.font = UIKitChassis.uiFont(10, weight: .semibold) starts.textColor = UIKitChassis.signal2 - starts.text = "Starts in" + starts.text = String(localized: "Starts in") let home = UILabel() home.font = UIKitChassis.monoFont(10, weight: .medium) home.textColor = UIKitChassis.signal @@ -4341,22 +4438,23 @@ final class NewSessionViewController: UIViewController, row.alignment = .center row.spacing = 12 row.isAccessibilityElement = true - row.accessibilityLabel = "Starts in, \(form.directoryFallbackTitle)" + row.accessibilityLabel = String( + localized: "Starts in, \(form.directoryFallbackTitle)") directoryHomeRow = row return FleetFormSectionView( - title: "Directory", + title: String(localized: "Directory"), detail: form.directoryDetail, rows: [row] ) } let button = FleetMenuFieldButton() - button.accessibilityLabel = "Starting directory" + button.accessibilityLabel = String(localized: "Starting directory") button.accessibilityIdentifier = "newSession.directory" directoryButton = button return FleetFormSectionView( - title: "Directory", + title: String(localized: "Directory"), detail: form.directoryDetail, - rows: [makeField(label: "Starts in", input: button)] + rows: [makeField(label: String(localized: "Starts in"), input: button)] ) } @@ -4376,11 +4474,11 @@ final class NewSessionViewController: UIViewController, launchSection?.setRow(agentFieldsRow, visible: form.agentToLaunch != nil) } modelField.accessibilityLabel = form.agentToLaunch.map { - "Optional model for \($0.displayName)" - } ?? "Optional model" + String(localized: "Optional model for \($0.displayName)") + } ?? String(localized: "Optional model") promptView.setPlaceholder(form.agentToLaunch.map { - "What should \($0.displayName) do?" - } ?? "Initial prompt") + String(localized: "What should \($0.displayName) do?") + } ?? String(localized: "Initial prompt")) updateCreatesMenu() updateModelMenu() updateScriptMenu() @@ -4397,24 +4495,26 @@ final class NewSessionViewController: UIViewController, scriptSection?.setDetail(form.scriptDetail) directorySection?.setDetail(form.directoryDetail) directoryHomeLabel?.text = form.directoryFallbackTitle.uppercased() - directoryHomeRow?.accessibilityLabel = "Starts in, \(form.directoryFallbackTitle)" + directoryHomeRow?.accessibilityLabel = String( + localized: "Starts in, \(form.directoryFallbackTitle)") createItem?.isEnabled = form.canSubmit } private func updateCreatesMenu() { guard let createsButton else { return } - let value = form.tabTargetSession.map { "Tab in “\($0)”" } ?? "New Session" + let value = form.tabTargetSession.map { String(localized: "Tab in “\($0)”") } + ?? String(localized: "New Session") createsButton.setValue(value) createsButton.accessibilityValue = value var actions: [UIMenuElement] = [ - UIAction(title: "New Session") { [weak self] _ in + UIAction(title: String(localized: "New Session")) { [weak self] _ in self?.form.selectTabTarget(nil) self?.renderForm() }, ] actions.append(UIMenu(options: .displayInline, children: form.tabTargetChoices.map { session in - UIAction(title: "Tab in “\(session)”") { [weak self] _ in + UIAction(title: String(localized: "Tab in “\(session)”")) { [weak self] _ in self?.form.selectTabTarget(session) self?.renderForm() } @@ -4452,7 +4552,8 @@ final class NewSessionViewController: UIViewController, .resolvedColor(with: button.traitCollection).cgColor } button.showsMenuAsPrimaryAction = true - button.accessibilityLabel = "Configured models for \(agent.displayName)" + button.accessibilityLabel = String( + localized: "Configured models for \(agent.displayName)") button.accessibilityIdentifier = "newSession.modelMenu" var children: [UIMenuElement] = configured.map { candidate in UIAction(title: candidate) { [weak self] _ in @@ -4461,7 +4562,7 @@ final class NewSessionViewController: UIViewController, } } children.append(UIMenu(options: .displayInline, children: [ - UIAction(title: "Agent default") { [weak self] _ in + UIAction(title: String(localized: "Agent default")) { [weak self] _ in self?.form.model = "" self?.renderForm() }, @@ -4477,7 +4578,7 @@ final class NewSessionViewController: UIViewController, private func updateScriptMenu() { guard let scriptButton else { return } - scriptButton.setValue(form.script?.displayName ?? "None") + scriptButton.setValue(form.script?.displayName ?? String(localized: "None")) var actions: [UIMenuElement] = form.host.sessionScripts.map { script in UIAction(title: script.displayName) { [weak self] _ in self?.form.script = script @@ -4485,7 +4586,7 @@ final class NewSessionViewController: UIViewController, } } actions.append(UIMenu(options: .displayInline, children: [ - UIAction(title: "None") { [weak self] _ in + UIAction(title: String(localized: "None")) { [weak self] _ in self?.form.script = nil self?.renderForm() }, @@ -4771,7 +4872,7 @@ private final class FleetBackendChoiceBar: UIView { heightAnchor.constraint(equalToConstant: 34), ]) isAccessibilityElement = false - accessibilityLabel = "Which backend the session runs on" + accessibilityLabel = String(localized: "Which backend the session runs on") refresh(animated: false) } @@ -4835,7 +4936,7 @@ private final class FleetLaunchChoiceView: UIView { shell.addAction(UIAction { [weak self] _ in self?.onSelectShell?() }, for: .touchUpInside) agents.showsMenuAsPrimaryAction = true isAccessibilityElement = false - accessibilityLabel = "What to launch" + accessibilityLabel = String(localized: "What to launch") } @available(*, unavailable) @@ -4846,20 +4947,25 @@ private final class FleetLaunchChoiceView: UIView { renderedMode = mode self.selectedAgent = selectedAgent shell.configure( - title: "Shell", + title: String(localized: "Shell"), selected: mode == .shell, showsChevron: false, animationDuration: animatesSelection ? Self.selectionAnimationDuration : nil ) agents.configure( - title: mode == .agents ? selectedAgent.displayName : "Agents", + title: mode == .agents + ? selectedAgent.displayName + : String(localized: "Agents"), selected: mode == .agents, showsChevron: true, animationDuration: animatesSelection ? Self.selectionAnimationDuration : nil ) - agents.accessibilityLabel = "Agents" - agents.accessibilityValue = mode == .agents ? selectedAgent.displayName : "Not selected" - agents.accessibilityHint = "Choose Claude Code, Codex, Pi, or Grok Build" + agents.accessibilityLabel = String(localized: "Agents") + agents.accessibilityValue = mode == .agents + ? selectedAgent.displayName + : String(localized: "Not selected") + agents.accessibilityHint = String( + localized: "Choose Claude Code, Codex, Pi, or Grok Build") agents.menu = UIMenu(children: AgentKind.allCases.map { agent in UIAction(title: agent.displayName, state: agent == selectedAgent ? .on : .off) { [weak self] _ in self?.onSelectAgent?(agent) @@ -4999,7 +5105,7 @@ private final class FleetToggleView: UIView { ]) isAccessibilityElement = true accessibilityTraits = .button - accessibilityLabel = "Remember launch choice" + accessibilityLabel = String(localized: "Remember launch choice") hoverStyle = UIHoverStyle(effect: .highlight, shape: .rect(cornerRadius: 2)) addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(toggle))) render() @@ -5028,7 +5134,7 @@ private final class FleetToggleView: UIView { thumbLeading?.constant = isOn ? 19 : 3 thumb.backgroundColor = isOn ? UIKitChassis.signal : UIKitChassis.signal3 track.tallyBorderColor = isOn ? UIKitChassis.signal2 : UIKitChassis.bezelHi - accessibilityValue = isOn ? "On" : "Off" + accessibilityValue = isOn ? String(localized: "On") : String(localized: "Off") } } @@ -5131,7 +5237,7 @@ private final class FleetPromptTextView: UITextView, UITextViewDelegate { func setPlaceholder(_ value: String) { placeholderLabel.text = value - accessibilityLabel = "Optional initial prompt" + accessibilityLabel = String(localized: "Optional initial prompt") } func setText(_ value: String) { diff --git a/Multiplex/Views/Deck/HostGuideSheets.swift b/Multiplex/Views/Deck/HostGuideSheets.swift index a7fb81ec..b8ae7cd4 100644 --- a/Multiplex/Views/Deck/HostGuideSheets.swift +++ b/Multiplex/Views/Deck/HostGuideSheets.swift @@ -90,7 +90,7 @@ final class UIKitCopyableCommandField: UIKitTallyBorderedView { copyChip = UIKitChassisChip( "COPY", systemImage: "doc.on.doc", - accessibilityLabel: "Copy command", + accessibilityLabel: String(localized: "Copy command"), action: { [weak self] in self?.copyCommand() } ) copyChip.setContentHuggingPriority(.required, for: .horizontal) @@ -162,13 +162,13 @@ class UIKitHostGuideSheetViewController: UIViewController, AppAppearanceFollowin navigationItem.titleView = UIKitChassisLabel(sheetTitle, size: 12) #endif let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done configureContent() @@ -267,7 +267,8 @@ final class TmuxInstallViewController: UIKitHostGuideSheetViewController { init(host: Host) { self.host = host - super.init(title: "Install \(host.sessionBackend.rawValue)") + super.init(title: String( + localized: "Install \(host.sessionBackend.rawValue)")) } @available(*, unavailable) @@ -287,20 +288,23 @@ final class TmuxInstallViewController: UIKitHostGuideSheetViewController { } addSection(UIKitTallyFormSectionView( - title: "The deck runs on \(multiplexer)", - detail: "The deck re-probes every few seconds — " - + "session tiles light up as soon as \(multiplexer) is on " - + "the host. \(probePathDetail)", + title: String(localized: "The deck runs on \(multiplexer)"), + detail: String(localized: """ + The deck re-probes every few seconds — session tiles light up \ + as soon as \(multiplexer) is on the host. \(probePathDetail) + """), contentView: rowStack )) } var intro: String { - "Sessions, live tiles, and attach all come from a \(multiplexer) " - + "server on each host, and \(host.name) doesn't have " - + "\(multiplexer) yet. You can still use a plain shell — the " - + "SHELL chip on the host's rail opens one. For the full deck, " - + "install \(multiplexer) on the host:" + String(localized: """ + Sessions, live tiles, and attach all come from a \(multiplexer) \ + server on each host, and \(host.name) doesn't have \ + \(multiplexer) yet. You can still use a plain shell — the \ + SHELL chip on the host's rail opens one. For the full deck, \ + install \(multiplexer) on the host: + """) } } @@ -315,7 +319,7 @@ final class KeychainUnlockViewController: UIKitHostGuideSheetViewController { init(host: Host, sessionNames: [String]) { self.host = host self.sessionNames = sessionNames - super.init(title: "Keychain locked") + super.init(title: String(localized: "Keychain locked")) } @available(*, unavailable) @@ -331,7 +335,8 @@ final class KeychainUnlockViewController: UIKitHostGuideSheetViewController { let sessions = UILabel() sessions.font = UIKitChassis.monoFont(10) sessions.textColor = UIKitChassis.signal3 - sessions.text = "Detected in: " + sessionNames.joined(separator: " · ") + sessions.text = String(localized: "Detected in: ") + + sessionNames.joined(separator: " · ") sessions.numberOfLines = 0 rowStack.addArrangedSubview(sessions) } @@ -340,21 +345,24 @@ final class KeychainUnlockViewController: UIKitHostGuideSheetViewController { )) addSection(UIKitTallyFormSectionView( - title: "Claude Code shows signed out", - detail: "The command prompts for that Mac account's " - + "login password. The unlock holds until macOS " - + "locks the keychain again — after a restart, " - + "or per the keychain's own lock settings.", + title: String(localized: "Claude Code shows signed out"), + detail: String(localized: """ + The command prompts for that Mac account's login password. \ + The unlock holds until macOS locks the keychain again — after \ + a restart, or per the keychain's own lock settings. + """), contentView: rowStack )) } var intro: String { - "Claude Code on \(host.name) is waiting at its sign-in screen, but " - + "the stored login is probably intact: on a Mac, Claude Code " - + "keeps its credentials in the login keychain, and an SSH or " - + "tmux session never unlocks it — no GUI login happened. " - + "Unlock it once in any shell on \(host.name), then restart " - + "Claude Code:" + String(localized: """ + Claude Code on \(host.name) is waiting at its sign-in screen, but \ + the stored login is probably intact: on a Mac, Claude Code \ + keeps its credentials in the login keychain, and an SSH or \ + tmux session never unlocks it — no GUI login happened. \ + Unlock it once in any shell on \(host.name), then restart \ + Claude Code: + """) } } diff --git a/Multiplex/Views/SSHKeyPassphrasePrompt.swift b/Multiplex/Views/SSHKeyPassphrasePrompt.swift index 1b60fb95..38839d59 100644 --- a/Multiplex/Views/SSHKeyPassphrasePrompt.swift +++ b/Multiplex/Views/SSHKeyPassphrasePrompt.swift @@ -6,11 +6,11 @@ import UIKit /// synchronizable Keychain item. @MainActor final class SSHKeyPassphrasePromptPresenterViewController: UIViewController { - static let titleText = "Unlock SSH Key" - static let fieldPlaceholder = "Key passphrase" - static let connectOnceTitle = "Connect Once" - static let saveAndConnectTitle = "Save & Connect" - static let cancelTitle = "Cancel" + static let titleText = String(localized: "Unlock SSH Key") + static let fieldPlaceholder = String(localized: "Key passphrase") + static let connectOnceTitle = String(localized: "Connect Once") + static let saveAndConnectTitle = String(localized: "Save & Connect") + static let cancelTitle = String(localized: "Cancel") private(set) var currentAlertController: UIAlertController? private(set) var lastPresentedID: UUID? @@ -48,9 +48,9 @@ final class SSHKeyPassphrasePromptPresenterViewController: UIViewController { static func message(for challenge: SSHKeyPassphraseChallenge) -> String { switch challenge.reason { case .required: - return "The private key for “\(challenge.hostName)” is encrypted. Connect Once keeps the passphrase until Multiplex closes. Save & Connect stores it in iCloud Keychain for your other devices." + return String(localized: "The private key for “\(challenge.hostName)” is encrypted. Connect Once keeps the passphrase until Multiplex closes. Save & Connect stores it in iCloud Keychain for your other devices.") case .incorrect: - return "That passphrase didn't unlock the private key for “\(challenge.hostName)”. Try again. Save & Connect replaces the copy in iCloud Keychain." + return String(localized: "That passphrase didn't unlock the private key for “\(challenge.hostName)”. Try again. Save & Connect replaces the copy in iCloud Keychain.") } } diff --git a/Multiplex/Views/Settings/FAQView.swift b/Multiplex/Views/Settings/FAQView.swift index ddccc665..1a12383f 100644 --- a/Multiplex/Views/Settings/FAQView.swift +++ b/Multiplex/Views/Settings/FAQView.swift @@ -28,13 +28,13 @@ final class FAQViewController: UIViewController, AppAppearanceFollowing { navigationItem.titleView = UIKitChassisLabel("FAQ", size: 12) #endif let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done configureContent() @@ -220,34 +220,38 @@ private struct FAQEntry { static let all: [FAQEntry] = [ FAQEntry( id: "host-needs-multiplexer", - question: "A host shows no tmux or herdr", - answer: "The deck is built around a session multiplexer on each " - + "host — sessions, live tiles, and attach all come from it. " - + "Each host runs one, tmux or herdr, chosen in its settings " - + "under Sessions run on. A host without that multiplexer " - + "still works as a plain shell (the SHELL chip on its rail), " - + "it just has no session tiles. To get the full deck, " - + "install the one the host is set to:", + question: String(localized: "A host shows no tmux or herdr"), + answer: String(localized: """ + The deck is built around a session multiplexer on each host — sessions, \ + live tiles, and attach all come from it. Each host runs one, tmux or \ + herdr, chosen in its settings under Sessions run on. A host without that \ + multiplexer still works as a plain shell (the SHELL chip on its rail), it \ + just has no session tiles. To get the full deck, install the one the host \ + is set to: + """), offersMultiplexerInstall: true, - postscript: "The deck re-probes every few seconds and lights the " - + "tile as soon as the multiplexer lands — no restart, and " - + "nothing to configure on the host." + postscript: String(localized: """ + The deck re-probes every few seconds and lights the tile as soon as the \ + multiplexer lands — no restart, and nothing to configure on the host. + """) ), FAQEntry( id: "claude-code-tmux-keychain", - question: "Claude Code shows signed out in tmux", - answer: "On a Mac host, Claude Code keeps its credentials in the " - + "login keychain, and an SSH or tmux session never unlocks it — " - + "no GUI login happened — so Claude Code starts as if you were " - + "signed out even though your login is intact. Unlock the " - + "keychain once inside the tmux session, then restart Claude " - + "Code:", + question: String(localized: "Claude Code shows signed out in tmux"), + answer: String(localized: """ + On a Mac host, Claude Code keeps its credentials in the login keychain, \ + and an SSH or tmux session never unlocks it — no GUI login happened — so \ + Claude Code starts as if you were signed out even though your login is \ + intact. Unlock the keychain once inside the tmux session, then restart \ + Claude Code: + """), commands: [HostGuide.keychainUnlock], - postscript: "The command prompts for that Mac account's login " - + "password. The unlock holds until macOS locks the keychain " - + "again — after a restart, or per the keychain's own lock " - + "settings. When the deck detects this state it also points " - + "here: the host's rail reads KEYCHAIN LOCKED." + postscript: String(localized: """ + The command prompts for that Mac account's login password. The unlock \ + holds until macOS locks the keychain again — after a restart, or per the \ + keychain's own lock settings. When the deck detects this state it also \ + points here: the host's rail reads KEYCHAIN LOCKED. + """) ) ] } diff --git a/Multiplex/Views/Settings/LicensesView.swift b/Multiplex/Views/Settings/LicensesView.swift index d7f67b17..8ed1a856 100644 --- a/Multiplex/Views/Settings/LicensesView.swift +++ b/Multiplex/Views/Settings/LicensesView.swift @@ -72,21 +72,21 @@ final class LicensesViewController: UIViewController, AppAppearanceFollowing { override func viewDidLoad() { super.viewDidLoad() - title = "Open Source Licenses" + title = String(localized: "Open Source Licenses") view.backgroundColor = GlassPrototype.sheetGround navigationItem.largeTitleDisplayMode = .never #if os(visionOS) - navigationItem.titleView = UIKitChassisLabel("Open Source Licenses", size: 12) + navigationItem.titleView = UIKitChassisLabel(String(localized: "Open Source Licenses"), size: 12) #endif if presentingViewController != nil || navigationController?.presentingViewController != nil { let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done } rebuildLayout() @@ -269,7 +269,7 @@ final class LicensesViewController: UIViewController, AppAppearanceFollowing { } private func makeFooterNote() -> UIView { - let title = UIKitChassisLabel("Mosh transport", size: 9) + let title = UIKitChassisLabel(String(localized: "Mosh transport"), size: 9) let detail = licenseLabel( footerNote, font: UIKitChassis.uiFont(10), @@ -327,7 +327,7 @@ final class LicensesViewController: UIViewController, AppAppearanceFollowing { } private func makeRegistryPane() -> UIView { - let title = UIKitChassisLabel("Components", size: 10) + let title = UIKitChassisLabel(String(localized: "Components"), size: 10) let count = licenseLabel( "\(components.count) SHIPPED", font: UIKitChassis.monoFont(8, weight: .medium), @@ -571,9 +571,9 @@ private final class LicenseComponentRow: UIControl { isAccessibilityElement = true accessibilityTraits = .button accessibilityLabel = component.name - accessibilityValue = "Version \(component.version), " + accessibilityValue = String(localized: "Version \(component.version), ") + component.family.displayName - + (component.isVendored ? ", vendored" : "") + + (component.isVendored ? String(localized: ", vendored") : "") addAction(UIAction { _ in action() }, for: .touchUpInside) let name = licenseLabel( @@ -744,7 +744,7 @@ private final class LicenseFilterChip: UIControl { .resolvedColor(with: traitCollection), ] ) - accessibilityValue = active ? "Selected" : "Not selected" + accessibilityValue = active ? String(localized: "Selected") : String(localized: "Not selected") if active { accessibilityTraits.insert(.selected) } else { @@ -766,7 +766,7 @@ private final class LicenseTextPaneView: UIKitTallyBorderedView { private lazy var copyChip = UIKitChassisChip( "COPY", systemImage: "doc.on.doc", - accessibilityLabel: "Copy license text" + accessibilityLabel: String(localized: "Copy license text") ) { [weak self] in self?.copyLicense() } @@ -791,8 +791,8 @@ private final class LicenseTextPaneView: UIKitTallyBorderedView { vendorLabel.isHidden = component.vendorNote == nil vendoredBadge.isHidden = !component.isVendored textView.text = component.licenseText - textView.accessibilityLabel = "\(component.name) license text" - copyChip.accessibilityLabel = "Copy \(component.name) license text" + textView.accessibilityLabel = String(localized: "\(component.name) license text") + copyChip.accessibilityLabel = String(localized: "Copy \(component.name) license text") textView.setContentOffset(.zero, animated: false) } @@ -872,7 +872,7 @@ private final class LicenseTextPaneView: UIKitTallyBorderedView { private func copyLicense() { UIPasteboard.general.string = component.licenseText - UIAccessibility.post(notification: .announcement, argument: "License copied") + UIAccessibility.post(notification: .announcement, argument: String(localized: "License copied")) } } diff --git a/Multiplex/Views/Settings/ProPaywallView.swift b/Multiplex/Views/Settings/ProPaywallView.swift index 2d29b2f5..bcd9deaf 100644 --- a/Multiplex/Views/Settings/ProPaywallView.swift +++ b/Multiplex/Views/Settings/ProPaywallView.swift @@ -85,13 +85,13 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { #endif let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done } @@ -155,17 +155,18 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { let brand = UIKitChassisLabel("Multiplex Pro", size: 18) let promise = makeLabel( - "Buy once. Use it on iPad and Vision Pro.", + String(localized: "Buy once. Use it on iPad and Vision Pro."), style: .title3, weight: .semibold, color: UIKitChassis.signal ) let freeTier = makeLabel( - "The free tier stays useful: two hosts, spatial SSH terminals, " - + "live agent detection, built-in themes, " - + "\(EntitlementStore.freeKeyCommandLimit) saved Key Commands and " - + "\(EntitlementStore.dailySlashChipLimit) built-in or custom " - + "agent-command taps each day.", + String(localized: """ + The free tier stays useful: two hosts, spatial SSH terminals, live agent \ + detection, built-in themes, \(EntitlementStore.freeKeyCommandLimit) saved Key \ + Commands and \(EntitlementStore.dailySlashChipLimit) built-in or custom \ + agent-command taps each day. + """), style: .subheadline, color: UIKitChassis.signal2 ) @@ -180,34 +181,44 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { private func makeFeatures() -> UIView { let stack = UIStackView(arrangedSubviews: [ makeFeature( - "UNLIMITED HOSTS", - "Keep your work box, homelab and servers on one live fleet wall." + String(localized: "UNLIMITED HOSTS"), + String(localized: "Keep your work box, homelab and servers on one live fleet wall.") ), makeFeature( - "MOSH TRANSPORT", - "Keep terminals alive through headset sleep, network roaming and IP changes." + String(localized: "MOSH TRANSPORT"), + String(localized: """ + Keep terminals alive through headset sleep, network roaming and IP changes. + """) ), makeFeature( - "AGENT HELPERS + ALERTS", - "Use Claude Code, Codex, Pi, and Grok Build quick commands without the daily " - + "limit, browse a Claude Code session's prompt history and jump " - + "its transcript back to any message, and get a banner when an " - + "unwatched supported session needs you." + String(localized: "AGENT HELPERS + ALERTS"), + String(localized: """ + Use Claude Code, Codex, Pi, and Grok Build quick commands without the daily \ + limit, browse a Claude Code session's prompt history and jump its transcript \ + back to any message, and get a banner when an unwatched supported session \ + needs you. + """) ), makeFeature( - "CONNECTION STATS", - "Open the fleet board behind every rail chip: round-trips, typing " - + "echo, mosh loss and roams, reconnects, and data volume per host." + String(localized: "CONNECTION STATS"), + String(localized: """ + Open the fleet board behind every rail chip: round-trips, typing echo, mosh \ + loss and roams, reconnects, and data volume per host. + """) ), makeFeature( - "CUSTOM THEMES", - "Build and edit terminal palettes while the Tally chassis stays consistent." + String(localized: "CUSTOM THEMES"), + String(localized: """ + Build and edit terminal palettes while the Tally chassis stays consistent. + """) ), makeFeature( - "\(KeyCommandSet.maximumCount) KEY COMMANDS", - "Grow the hold-CTRL set of saved chords and text macros from " - + "\(EntitlementStore.freeKeyCommandLimit) to \(KeyCommandSet.maximumCount), " - + "synced to every device." + String(localized: "\(KeyCommandSet.maximumCount) KEY COMMANDS"), + String(localized: """ + Grow the hold-CTRL set of saved chords and text macros from \ + \(EntitlementStore.freeKeyCommandLimit) to \(KeyCommandSet.maximumCount), \ + synced to every device. + """) ), ]) stack.axis = .vertical @@ -258,7 +269,7 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { for: .touchUpInside ) purchaseButton.accessibilityHint = - "Purchases the non-consumable Multiplex Pro unlock" + String(localized: "Purchases the non-consumable Multiplex Pro unlock") commerceMessageLabel.font = ProPaywallFont.font(for: .footnote) commerceMessageLabel.adjustsFontForContentSizeCategory = true @@ -275,10 +286,10 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { for: .touchUpInside ) restoreButton.accessibilityHint = - "Checks this Apple ID for a Multiplex Pro purchase" + String(localized: "Checks this Apple ID for a Multiplex Pro purchase") let payment = makeLabel( - "Payment is charged to your Apple ID. No subscription.", + String(localized: "Payment is charged to your Apple ID. No subscription."), style: .caption1, color: UIKitChassis.signal3 ) @@ -313,13 +324,13 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { check.setContentCompressionResistancePriority(.required, for: .horizontal) let title = makeLabel( - "Multiplex Pro is unlocked", + String(localized: "Multiplex Pro is unlocked"), style: .headline, weight: .semibold, color: UIKitChassis.signal ) let detail = makeLabel( - "This purchase is available on your devices with the same Apple ID.", + String(localized: "This purchase is available on your devices with the same Apple ID."), style: .footnote, color: UIKitChassis.signal2 ) @@ -333,8 +344,10 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { row.alignment = .center row.spacing = 10 row.isAccessibilityElement = true - row.accessibilityLabel = "Multiplex Pro is unlocked. " - + "This purchase is available on your devices with the same Apple ID." + row.accessibilityLabel = String(localized: """ + Multiplex Pro is unlocked. This purchase is available on your devices with the same \ + Apple ID. + """) title.isAccessibilityElement = false detail.isAccessibilityElement = false check.isAccessibilityElement = false @@ -384,16 +397,16 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { purchaseButton.isHidden = state.isPro let purchaseLabel = state.productDisplayPrice.map { - "Unlock Multiplex Pro · \($0)" - } ?? "Unlock Multiplex Pro" + String(localized: "Unlock Multiplex Pro · \($0)") + } ?? String(localized: "Unlock Multiplex Pro") purchaseButton.setPurchaseTitle(purchaseLabel) purchaseButton.setPurchasing(state.commerceState == .purchasing) purchaseButton.isEnabled = !state.purchaseIsUnavailable restoreButton.setTitle( state.commerceState == .restoring - ? "Restoring Purchases…" - : "Restore Purchases", + ? String(localized: "Restoring Purchases…") + : String(localized: "Restore Purchases"), for: .normal ) restoreButton.isEnabled = !state.restoreIsUnavailable @@ -412,34 +425,37 @@ final class ProPaywallViewController: UIViewController, AppAppearanceFollowing { if state.isPro { return nil } else if state.productIsLoading { - return "Loading the App Store price…" + return String(localized: "Loading the App Store price…") } else { return state.productLoadError } case .purchasing: - return "Waiting for the App Store…" + return String(localized: "Waiting for the App Store…") case .pending: - return "Purchase pending approval. Pro unlocks automatically if approved. " - + "If it was declined, tap Restore Purchases to check again." + return String(localized: """ + Purchase pending approval. Pro unlocks automatically if approved. If it was \ + declined, tap Restore Purchases to check again. + """) case .purchased: - return "Purchase complete." + return String(localized: "Purchase complete.") case .restoring: - return "Checking your App Store purchases…" + return String(localized: "Checking your App Store purchases…") case .restored: if state.isPro { - return "Multiplex Pro was restored." + return String(localized: "Multiplex Pro was restored.") } // Restore succeeded and genuinely found nothing. On TestFlight // that is the expected answer for a production purchase — the // test store and the App Store never share transactions — so // say why instead of letting the empty result read as a bug. if state.storeEnvironmentIsSandbox { - return "No Multiplex Pro purchase was found for this Apple ID. " - + "TestFlight builds use Apple's test store, so a purchase " - + "made in the App Store version doesn't appear here. Pro " - + "can be unlocked again inside TestFlight free of charge." + return String(localized: """ + No Multiplex Pro purchase was found for this Apple ID. TestFlight builds use \ + Apple's test store, so a purchase made in the App Store version doesn't \ + appear here. Pro can be unlocked again inside TestFlight free of charge. + """) } - return "No Multiplex Pro purchase was found for this Apple ID." + return String(localized: "No Multiplex Pro purchase was found for this Apple ID.") case .failed(let message): return message } diff --git a/Multiplex/Views/Settings/ReleaseNotesView.swift b/Multiplex/Views/Settings/ReleaseNotesView.swift index d899ed39..75294e38 100644 --- a/Multiplex/Views/Settings/ReleaseNotesView.swift +++ b/Multiplex/Views/Settings/ReleaseNotesView.swift @@ -149,7 +149,7 @@ final class WhatsNewViewController: UIViewController, AppAppearanceFollowing { version.font = UIKitChassis.monoFont(11) version.textColor = UIKitChassis.signal2 version.text = ReleaseNotes.version - version.accessibilityLabel = "Version \(ReleaseNotes.version)" + version.accessibilityLabel = String(localized: "Version \(ReleaseNotes.version)") let row = UIStackView(arrangedSubviews: [mark, ReleaseNotesChrome.spacer(), version]) row.axis = .horizontal @@ -228,14 +228,14 @@ final class WhatsNewViewController: UIViewController, AppAppearanceFollowing { private func makeActions() -> UIView { let fullNotes = UIKitChassisChip( "FULL NOTES", - accessibilityLabel: "Full notes" + accessibilityLabel: String(localized: "Full notes") ) { [weak self] in self?.onFullNotes?() } let done = UIKitChassisChip( "DONE", prominent: true, - accessibilityLabel: "Done" + accessibilityLabel: String(localized: "Done") ) { [weak self] in self?.onDone?() } @@ -338,21 +338,21 @@ final class ReleaseLogViewController: UIViewController, AppAppearanceFollowing { override func viewDidLoad() { super.viewDidLoad() - title = "What’s New" + title = String(localized: "What’s New") view.backgroundColor = GlassPrototype.sheetGround navigationItem.largeTitleDisplayMode = .never #if os(visionOS) - navigationItem.titleView = UIKitChassisLabel("What’s New", size: 12) + navigationItem.titleView = UIKitChassisLabel(String(localized: "What’s New"), size: 12) #endif let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done configureContent() diff --git a/Multiplex/Views/Settings/SettingsView.swift b/Multiplex/Views/Settings/SettingsView.swift index 594b3699..bb5ea044 100644 --- a/Multiplex/Views/Settings/SettingsView.swift +++ b/Multiplex/Views/Settings/SettingsView.swift @@ -65,7 +65,7 @@ final class SettingsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Settings" + title = String(localized: "Settings") view.backgroundColor = GlassPrototype.sheetGround configureNavigation() configureContent() @@ -101,16 +101,16 @@ final class SettingsViewController: UIViewController { private func configureNavigation() { navigationItem.largeTitleDisplayMode = .never #if os(visionOS) - navigationItem.titleView = UIKitChassisLabel("Settings", size: 12) + navigationItem.titleView = UIKitChassisLabel(String(localized: "Settings"), size: 12) #endif let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done } @@ -246,6 +246,7 @@ final class SettingsViewController: UIViewController { makeAlertsSection(state), makeAppLockSection(state), makeProSection(state), + makeLanguageSection(), makeAboutSection(), makePrivacyLink(), ] @@ -275,16 +276,18 @@ final class SettingsViewController: UIViewController { appearanceChoiceBar = bar let appearanceDetail: String if GlassPrototype.enabled { - appearanceDetail = "System follows the device. The deck, terminal chrome, " - + "and forms switch together. Dark and Glass share the dark terminal " - + "theme below." + appearanceDetail = String(localized: """ + System follows the device. The deck, terminal chrome, and forms switch \ + together. Dark and Glass share the dark terminal theme below. + """) } else { - appearanceDetail = "System follows the device. The deck, terminal chrome, " - + "and forms switch together; each appearance keeps its own terminal " - + "theme below." + appearanceDetail = String(localized: """ + System follows the device. The deck, terminal chrome, and forms switch \ + together; each appearance keeps its own terminal theme below. + """) } let section = SettingsSectionView( - title: "Appearance", + title: String(localized: "Appearance"), detail: appearanceDetail, rows: [SettingsInsetRow(contentView: bar)] ) @@ -318,7 +321,7 @@ final class SettingsViewController: UIViewController { ) -> UIView { let name = UIKitChassisLabel(theme.name, size: 12) let surface = settingsTrackedLabel( - "TERMINAL SURFACE", + String(localized: "TERMINAL SURFACE"), font: UIKitChassis.monoFont(8, weight: .medium), color: UIKitChassis.signal3, kern: 1 @@ -342,16 +345,18 @@ final class SettingsViewController: UIViewController { let ownership: String switch choice { case .glass: - ownership = "Glass shares this dark terminal theme with Dark." + ownership = String(localized: "Glass shares this dark terminal theme with Dark.") case .dark where GlassPrototype.enabled: - ownership = "Dark shares this terminal theme with Glass." + ownership = String(localized: "Dark shares this terminal theme with Glass.") default: - ownership = "This is the \(resolvedAppearance == .light ? "light" : "dark") " - + "theme now on screen." + let mode = resolvedAppearance == .light + ? String(localized: "light") + : String(localized: "dark") + ownership = String(localized: "This is the \(mode) theme now on screen.") } return SettingsSectionView( - title: "Current theme", - detail: "Selections apply to every terminal immediately. \(ownership)", + title: String(localized: "Current theme"), + detail: String(localized: "Selections apply to every terminal immediately. \(ownership)"), rows: [SettingsInsetRow(contentView: body)] ) } @@ -366,9 +371,11 @@ final class SettingsViewController: UIViewController { ) } return SettingsSectionView( - title: "Built-in themes", - detail: "Choose a terminal palette. Press and hold a theme to duplicate it " - + "as a custom starting point.", + title: String(localized: "Built-in themes"), + detail: String(localized: """ + Choose a terminal palette. Press and hold a theme to duplicate it as a custom \ + starting point. + """), rows: rows ) } @@ -381,12 +388,12 @@ final class SettingsViewController: UIViewController { var rows: [UIView] = [] if state.customThemes.isEmpty { let emptyTitle = UIKitChassisLabel( - "No custom themes", + String(localized: "No custom themes"), size: 10, color: UIKitChassis.signal3 ) let emptyDetail = settingsLabel( - "Start from the active palette, then tune its surface and ANSI colors.", + String(localized: "Start from the active palette, then tune its surface and ANSI colors."), font: UIKitChassis.uiFont(10), color: UIKitChassis.signal2 ) @@ -410,7 +417,7 @@ final class SettingsViewController: UIViewController { let newTheme = UIKitChassisChip( "NEW THEME", systemImage: "plus", - accessibilityLabel: "New theme" + accessibilityLabel: String(localized: "New theme") ) { [weak self] in self?.requestThemeEditor(selectedTheme.asCustom(named: "New Theme")) } @@ -424,26 +431,36 @@ final class SettingsViewController: UIViewController { rows.append(SettingsInsetRow(contentView: actionRow)) let detail = state.canMutateCustomThemes - ? "New themes begin with the active palette. Use the row menu to edit, " - + "duplicate, or delete one." - : "Creating, duplicating, and editing custom themes requires Multiplex Pro. " - + "Existing themes remain selectable and deletable." - return SettingsSectionView(title: "Your themes", detail: detail, rows: rows) + ? String(localized: """ + New themes begin with the active palette. Use the row menu to edit, duplicate, \ + or delete one. + """) + : String(localized: """ + Creating, duplicating, and editing custom themes requires Multiplex Pro. \ + Existing themes remain selectable and deletable. + """) + return SettingsSectionView( + title: String(localized: "Your themes"), + detail: detail, + rows: rows + ) } private func makeRendererSection() -> UIView { // Reads and writes the defaults-backed switch directly: no store // observes it, and the row's optimistic flip is the honest state. let control = SettingsBooleanRow( - title: "Metal renderer", + title: String(localized: "Metal renderer"), isOn: MetalRendererSetting.isEnabled ) { enabled in MetalRendererSetting.setEnabled(enabled) } return SettingsSectionView( - title: "Terminal renderer", - detail: "Draws terminal text on the GPU instead of CoreGraphics. " - + "Takes effect for newly opened terminal windows.", + title: String(localized: "Terminal renderer"), + detail: String(localized: """ + Draws terminal text on the GPU instead of CoreGraphics. Takes effect for newly \ + opened terminal windows. + """), rows: [control] ) } @@ -455,18 +472,19 @@ final class SettingsViewController: UIViewController { // here leaves the device; the detail line carries the stops-measuring // half of the promise. let control = SettingsBooleanRow( - title: "Show connection stats", + title: String(localized: "Show connection stats"), isOn: ConnectionStatsCenter.shared.isCollecting ) { enabled in ConnectionStatsCenter.shared.setCollecting(enabled) } return SettingsSectionView( - title: "Connection stats", - detail: "Round-trips, echo latency, loss, and volume — numbers the " - + "transports already compute, measured passively and kept in " - + "memory for this session only. Nothing is stored or synced. " - + "Off hides the rail chips and the stats board and stops " - + "measuring. The live chip is free; the board requires Pro.", + title: String(localized: "Connection stats"), + detail: String(localized: """ + Round-trips, echo latency, loss, and volume — numbers the transports already \ + compute, measured passively and kept in memory for this session only. Nothing \ + is stored or synced. Off hides the rail chips and the stats board and stops \ + measuring. The live chip is free; the board requires Pro. + """), rows: [control] ) } @@ -474,14 +492,14 @@ final class SettingsViewController: UIViewController { private func makeAlertsSection(_ state: ViewState) -> UIView { let alertsOn = state.canScheduleAgentAlerts && state.alertsEnabled let control = SettingsBooleanRow( - title: "Agent alerts", + title: String(localized: "Agent alerts"), isOn: alertsOn, status: state.canScheduleAgentAlerts ? nil : "PRO", statusIsProminent: true, optimisticallyUpdates: state.canScheduleAgentAlerts, accessibilityHint: state.canScheduleAgentAlerts ? nil - : "Requires Multiplex Pro" + : String(localized: "Requires Multiplex Pro") ) { [weak self] enabled in guard let self else { return } if enabled && !self.entitlements.canScheduleAgentAlerts { @@ -501,16 +519,18 @@ final class SettingsViewController: UIViewController { if !state.canScheduleAgentAlerts { rows.append(SettingsInsetRow(contentView: settingsLeadingView(UIKitChassisChip( "VIEW MULTIPLEX PRO", - accessibilityLabel: "View Multiplex Pro" + accessibilityLabel: String(localized: "View Multiplex Pro") ) { [weak self] in self?.presentPaywall() }))) } return SettingsSectionView( - title: "Agent alerts", - detail: "Posts a banner when Claude Code, Codex, or Grok Build finishes a turn, " - + "asks a question, or wants permission in a session you are not typing in. " - + "Multiplex must remain open. Requires Pro.", + title: String(localized: "Agent alerts"), + detail: String(localized: """ + Posts a banner when Claude Code, Codex, or Grok Build finishes a turn, asks a \ + question, or wants permission in a session you are not typing in. Multiplex \ + must remain open. Requires Pro. + """), rows: rows ) } @@ -518,10 +538,10 @@ final class SettingsViewController: UIViewController { private func makeAppLockSection(_ state: ViewState) -> UIView { let method = AppLockStore.methodName let control = SettingsBooleanRow( - title: "Require \(method)", + title: String(localized: "Require \(method)"), isOn: state.appLockEnabled, optimisticallyUpdates: false, - accessibilityHint: "Locks the app behind \(method)" + accessibilityHint: String(localized: "Locks the app behind \(method)") ) { [weak self] enabled in guard let self else { return } Task { @MainActor in @@ -533,17 +553,24 @@ final class SettingsViewController: UIViewController { } appLockControl = control return SettingsSectionView( - title: "App lock", - detail: "Require \(method) when Multiplex opens or returns from the background. " - + "The deck and every terminal stay covered until you authenticate; " - + "connections and the wall keep running. This device only.", + title: String(localized: "App lock"), + detail: String(localized: """ + Require \(method) when Multiplex opens or returns from the background. The deck \ + and every terminal stay covered until you authenticate; connections and the \ + wall keep running. This device only. + """), rows: [control] ) } private func makeProSection(_ state: ViewState) -> UIView { let entitlementStatus = UIStackView(arrangedSubviews: [ - UIKitChassisLabel(state.isPro ? "Pro unlocked" : "Free tier", size: 11), + UIKitChassisLabel( + state.isPro + ? String(localized: "Pro unlocked") + : String(localized: "Free tier"), + size: 11 + ), settingsFlexibleSpacer(), SettingsBadgeView( state.isPro ? "UNLOCKED" : "FREE", @@ -555,25 +582,27 @@ final class SettingsViewController: UIViewController { entitlementStatus.spacing = 12 var rows: [UIView] = [SettingsInsetRow(contentView: entitlementStatus)] - rows.append(proRow("Unlimited Hosts", state: state)) - rows.append(proRow("Mosh Transport", state: state)) + rows.append(proRow(String(localized: "Unlimited Hosts"), state: state)) + rows.append(proRow(String(localized: "Mosh Transport"), state: state)) rows.append(proRow( - "Agent Helpers", + String(localized: "Agent Helpers"), freeStatus: "\(EntitlementStore.dailySlashChipLimit) / DAY", state: state )) - rows.append(proRow("Agent Alerts", state: state)) - rows.append(proRow("Connection Stats", state: state)) - rows.append(proRow("Custom Themes", state: state)) + rows.append(proRow(String(localized: "Agent Alerts"), state: state)) + rows.append(proRow(String(localized: "Connection Stats"), state: state)) + rows.append(proRow(String(localized: "Custom Themes"), state: state)) rows.append(proRow( - "Key Commands", + String(localized: "Key Commands"), freeStatus: "UP TO \(EntitlementStore.freeKeyCommandLimit)", state: state )) rows.append(SettingsInsetRow(contentView: settingsLeadingView(UIKitChassisChip( state.isPro ? "PRO DETAILS" : "UNLOCK MULTIPLEX PRO", prominent: true, - accessibilityLabel: state.isPro ? "Pro details" : "Unlock Multiplex Pro" + accessibilityLabel: state.isPro + ? String(localized: "Pro details") + : String(localized: "Unlock Multiplex Pro") ) { [weak self] in self?.presentPaywall() }))) @@ -590,10 +619,12 @@ final class SettingsViewController: UIViewController { return SettingsSectionView( title: "Multiplex Pro", - detail: "Pro adds unlimited hosts, mosh, unlimited agent-helper commands, " - + "alerts, connection stats, custom themes, and a \(KeyCommandSet.maximumCount)-command Key Commands set " - + "(free keeps \(EntitlementStore.freeKeyCommandLimit)). SSH terminals, agent " - + "detection, and the wall's live state stay free.", + detail: String(localized: """ + Pro adds unlimited hosts, mosh, unlimited agent-helper commands, alerts, \ + connection stats, custom themes, and a \(KeyCommandSet.maximumCount)-command \ + Key Commands set (free keeps \(EntitlementStore.freeKeyCommandLimit)). SSH \ + terminals, agent detection, and the wall's live state stay free. + """), rows: rows ) } @@ -614,24 +645,50 @@ final class SettingsViewController: UIViewController { return SettingsInsetRow(contentView: row) } + /// Language is chosen per app in Settings.app (iOS/visionOS add the row + /// once the bundle carries a second localization); Multiplex only points + /// there — see docs/agents/i18n.md for why there is no in-app picker. + private func makeLanguageSection() -> UIView { + let language = SettingsNavigationRow( + title: String(localized: "Language"), + accessibilityLabel: String(localized: "Language, opens system settings"), + symbolName: "arrow.up.right" + ) { + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url) + } + language.accessibilityIdentifier = "settings.language" + language.accessibilityTraits = .link + return SettingsSectionView( + title: String(localized: "Language"), + detail: String(localized: """ + Multiplex follows the language you pick for it in Settings. English, \ + Traditional Chinese and Japanese are available; the terminal chrome stays English. + """), + rows: [language] + ) + } + private func makeAboutSection() -> UIView { let whatsNew = SettingsNavigationRow( - title: "What’s New", - accessibilityLabel: "What’s new" + title: String(localized: "What’s New"), + accessibilityLabel: String(localized: "What’s new") ) { [weak self] in self?.showReleaseLog() } whatsNew.accessibilityIdentifier = "settings.whatsNew" let licenses = SettingsNavigationRow( - title: "Open Source Licenses", - accessibilityLabel: "Open source licenses" + title: String(localized: "Open Source Licenses"), + accessibilityLabel: String(localized: "Open source licenses") ) { [weak self] in self?.showLicenses() } return SettingsSectionView( - title: "About", - detail: "Everything Multiplex \(ReleaseNotes.version) changed, and the " - + "license notices for the third-party code shipped with it.", + title: String(localized: "About"), + detail: String(localized: """ + Everything Multiplex \(ReleaseNotes.version) changed, and the license notices \ + for the third-party code shipped with it. + """), rows: [whatsNew, licenses] ) } @@ -641,7 +698,7 @@ final class SettingsViewController: UIViewController { let chip = UIKitChassisChip( "PRIVACY POLICY", systemImage: "arrow.up.right", - accessibilityLabel: "Privacy policy" + accessibilityLabel: String(localized: "Privacy policy") ) { [weak self] in self?.openPrivacy() } @@ -931,7 +988,12 @@ final class SettingsInsetRow: UIView { final class SettingsNavigationRow: UIControl { private let action: () -> Void - init(title: String, accessibilityLabel: String, action: @escaping () -> Void) { + init( + title: String, + accessibilityLabel: String, + symbolName: String = "chevron.right", + action: @escaping () -> Void + ) { self.action = action super.init(frame: .zero) backgroundColor = GlassPrototype.strataChassis @@ -943,7 +1005,7 @@ final class SettingsNavigationRow: UIControl { let titleLabel = UIKitChassisLabel(title, size: 10) let chevron = UIImageView(image: UIImage( - systemName: "chevron.right", + systemName: symbolName, withConfiguration: UIImage.SymbolConfiguration( pointSize: 9 * Theme.typeScale, weight: .semibold @@ -1131,7 +1193,7 @@ private final class SettingsChoiceButton: UIButton { layer.borderColor = (selected ? UIKitChassis.signal2 : UIKitChassis.bezelHi) .resolvedColor(with: traitCollection) .cgColor - accessibilityValue = selected ? "Selected" : "Not selected" + accessibilityValue = selected ? String(localized: "Selected") : String(localized: "Not selected") if selected { accessibilityTraits.insert(.selected) } else { @@ -1239,7 +1301,7 @@ final class SettingsBooleanRow: UIControl { private func refresh(animated: Bool = false) { indicator.setOn(isOn, animated: animated) - accessibilityValue = isOn ? "On" : "Off" + accessibilityValue = isOn ? String(localized: "On") : String(localized: "Off") } } @@ -1395,7 +1457,7 @@ final class UIKitThemePreviewView: UIKitTallyBorderedView { ]) ansiStack.addArrangedSubview(swatch) } - accessibilityLabel = "Preview of the \(theme.name) terminal theme" + accessibilityLabel = String(localized: "Preview of the \(theme.name) terminal theme") } private func configure() { @@ -1476,13 +1538,16 @@ private final class SettingsThemeRowView: UIView { menuProvider = { var actions: [UIMenuElement] = [] if let edit { - actions.append(UIAction(title: "Edit…") { _ in edit() }) + actions.append(UIAction(title: String(localized: "Edit…")) { _ in edit() }) } if let duplicate { - actions.append(UIAction(title: "Duplicate") { _ in duplicate() }) + actions.append(UIAction(title: String(localized: "Duplicate")) { _ in duplicate() }) } if let delete { - actions.append(UIAction(title: "Delete", attributes: .destructive) { _ in + actions.append(UIAction( + title: String(localized: "Delete"), + attributes: .destructive + ) { _ in delete() }) } @@ -1499,8 +1564,10 @@ private final class SettingsThemeRowView: UIView { shape: .rect(cornerRadius: 2) ) selectControl.isAccessibilityElement = true - selectControl.accessibilityLabel = "\(theme.name) theme" - selectControl.accessibilityValue = isSelected ? "Selected" : "Not selected" + selectControl.accessibilityLabel = String(localized: "\(theme.name) theme") + selectControl.accessibilityValue = isSelected + ? String(localized: "Selected") + : String(localized: "Not selected") selectControl.accessibilityTraits = .button if isSelected { selectControl.accessibilityTraits.insert(.selected) } selectControl.addAction(UIAction { _ in select() }, for: .touchUpInside) @@ -1528,7 +1595,7 @@ private final class SettingsThemeRowView: UIView { actionButton.backgroundColor = .clear actionButton.menu = menu actionButton.showsMenuAsPrimaryAction = true - actionButton.accessibilityLabel = "Actions for \(theme.name)" + actionButton.accessibilityLabel = String(localized: "Actions for \(theme.name)") actionButton.hoverStyle = UIHoverStyle( effect: .highlight, shape: .rect(cornerRadius: 2) @@ -1593,7 +1660,9 @@ private final class SettingsThemeRowContentView: UIView { preview = UIKitThemePreviewView(theme: theme, compact: true) let name = UIKitChassisLabel(theme.name, size: 11) let kind = settingsTrackedLabel( - theme.isBuiltIn ? "BUILT-IN PALETTE" : "CUSTOM PALETTE", + theme.isBuiltIn + ? String(localized: "BUILT-IN PALETTE") + : String(localized: "CUSTOM PALETTE"), font: UIKitChassis.monoFont(8, weight: .medium), color: UIKitChassis.signal3, kern: 1 diff --git a/Multiplex/Views/Settings/ThemeEditorView.swift b/Multiplex/Views/Settings/ThemeEditorView.swift index 80570703..0baddb17 100644 --- a/Multiplex/Views/Settings/ThemeEditorView.swift +++ b/Multiplex/Views/Settings/ThemeEditorView.swift @@ -86,13 +86,13 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, private func configureNavigation() { navigationItem.largeTitleDisplayMode = .never saveItem = UIBarButtonItem( - title: "Save", + title: String(localized: "Save"), style: .plain, target: self, action: #selector(savePressed) ) saveItem.tintColor = UIKitChassis.signal - saveItem.accessibilityLabel = "Save theme" + saveItem.accessibilityLabel = String(localized: "Save theme") navigationItem.rightBarButtonItem = saveItem } @@ -103,7 +103,7 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, view.addSubview(livePreviewContainer) livePreviewContainer.translatesAutoresizingMaskIntoConstraints = false - let label = UIKitChassisLabel("Live preview", size: 10) + let label = UIKitChassisLabel(String(localized: "Live preview"), size: 10) livePreviewName.font = UIKitChassis.monoFont(8, weight: .semibold) livePreviewName.textColor = UIKitChassis.signal3 livePreviewName.numberOfLines = 1 @@ -191,13 +191,13 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, contentStack.addArrangedSubview(makeIdentitySection()) contentStack.addArrangedSubview(makeSurfaceSection()) contentStack.addArrangedSubview(makeANSISection( - title: "ANSI · Normal", - detail: "The eight standard colors emitted by terminal programs.", + title: String(localized: "ANSI · Normal"), + detail: String(localized: "The eight standard colors emitted by terminal programs."), range: 0..<8 )) contentStack.addArrangedSubview(makeANSISection( - title: "ANSI · Bright", - detail: "The high-intensity variants used for bold and bright output.", + title: String(localized: "ANSI · Bright"), + detail: String(localized: "The high-intensity variants used for bold and bright output."), range: 8..<16 )) scrollView.addSubview(contentStack) @@ -236,7 +236,7 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, private func makeIdentitySection() -> UIView { let fieldLabel = themeEditorLabel( - "Name", + String(localized: "Name"), font: UIKitChassis.uiFont(10, weight: .semibold), color: UIKitChassis.signal2 ) @@ -250,7 +250,7 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, nameField.clearButtonMode = .whileEditing nameField.returnKeyType = .done nameField.delegate = self - nameField.accessibilityLabel = "Name" + nameField.accessibilityLabel = String(localized: "Name") nameField.addTarget(self, action: #selector(nameChanged), for: .editingChanged) let well = UIKitTallyBorderedView() well.tallyBorderColor = UIKitChassis.bezelHi @@ -267,8 +267,8 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, body.axis = .vertical body.spacing = 7 return SettingsSectionView( - title: "Theme identity", - detail: "Shown in Settings and anywhere this palette is selected.", + title: String(localized: "Theme identity"), + detail: String(localized: "Shown in Settings and anywhere this palette is selected."), rows: [SettingsInsetRow(contentView: body)] ) } @@ -276,27 +276,27 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, private func makeSurfaceSection() -> UIView { let rows = [ makeColorRow( - label: "Background", + label: String(localized: "Background"), value: { [weak self] in self?.draft.background ?? ThemeColor(0) }, initial: initialTheme.background, update: { [weak self] color in self?.draft.background = color } ), makeColorRow( - label: "Text", + label: String(localized: "Text"), value: { [weak self] in self?.draft.foreground ?? ThemeColor(0) }, initial: initialTheme.foreground, update: { [weak self] color in self?.draft.foreground = color } ), makeColorRow( - label: "Cursor", + label: String(localized: "Cursor"), value: { [weak self] in self?.draft.cursor ?? ThemeColor(0) }, initial: initialTheme.cursor, update: { [weak self] color in self?.draft.cursor = color } ), ] return SettingsSectionView( - title: "Surface", - detail: "The terminal's canvas, text, and insertion cursor.", + title: String(localized: "Surface"), + detail: String(localized: "The terminal's canvas, text, and insertion cursor."), rows: rows ) } @@ -348,7 +348,7 @@ final class ThemeEditorViewController: UIViewController, UITextFieldDelegate, } private func refreshDraftRendering() { - let displayName = draft.name.isEmpty ? "Theme" : draft.name + let displayName = draft.name.isEmpty ? String(localized: "Theme") : draft.name title = displayName #if os(visionOS) chassisNavigationTitle.setText(displayName) @@ -470,7 +470,7 @@ final class ThemeEditorColorRow: UIView { effect: .highlight, shape: .rect(cornerRadius: 2) ) - resetButton.accessibilityLabel = "Reset \(label)" + resetButton.accessibilityLabel = String(localized: "Reset \(label)") resetButton.addTarget(self, action: #selector(resetPressed), for: .touchUpInside) let resetBadge = SettingsBadgeView("", systemImage: "arrow.counterclockwise") resetBadge.isAccessibilityElement = false diff --git a/Multiplex/Views/Shell/ExternalActionHost.swift b/Multiplex/Views/Shell/ExternalActionHost.swift index 68c694c4..d3470c0c 100644 --- a/Multiplex/Views/Shell/ExternalActionHost.swift +++ b/Multiplex/Views/Shell/ExternalActionHost.swift @@ -122,11 +122,11 @@ final class ExternalActionUIKitCoordinator: NSObject, acknowledged: @escaping () -> Void ) -> UIAlertController { let alert = UIAlertController( - title: "Can't Open \(failure.hostName)", + title: String(localized: "Can't Open \(failure.hostName)"), message: failure.message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "OK", style: .cancel) { _ in + alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .cancel) { _ in acknowledged() }) return alert @@ -143,10 +143,10 @@ final class ExternalActionUIKitCoordinator: NSObject, message: confirmation.message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + alert.addAction(UIAlertAction(title: String(localized: "Cancel"), style: .cancel) { _ in answered(false) }) - alert.addAction(UIAlertAction(title: "Run", style: .default) { _ in + alert.addAction(UIAlertAction(title: String(localized: "Run"), style: .default) { _ in answered(true) }) return alert diff --git a/Multiplex/Views/Shell/SingleWindowShell.swift b/Multiplex/Views/Shell/SingleWindowShell.swift index 5fbc7152..6590c844 100644 --- a/Multiplex/Views/Shell/SingleWindowShell.swift +++ b/Multiplex/Views/Shell/SingleWindowShell.swift @@ -1169,13 +1169,13 @@ final class SingleWindowShellEmptyTerminalView: UIView { backgroundColor = UIKitChassis.screen let title = UIKitChassisLabel( - "No terminal selected", + String(localized: "No terminal selected"), size: 13, color: UIKitChassis.signal3 ) title.accessibilityIdentifier = "singleWindowShell.emptyTitle" let detail = UILabel() - detail.text = "Choose a session from the deck to attach it here." + detail.text = String(localized: "Choose a session from the deck to attach it here.") detail.font = .preferredFont(forTextStyle: .footnote) detail.adjustsFontForContentSizeCategory = true detail.textColor = UIKitChassis.signal2 diff --git a/Multiplex/Views/Shell/TerminalRouteOpener.swift b/Multiplex/Views/Shell/TerminalRouteOpener.swift index 26aee042..8aa54f70 100644 --- a/Multiplex/Views/Shell/TerminalRouteOpener.swift +++ b/Multiplex/Views/Shell/TerminalRouteOpener.swift @@ -1,3 +1,5 @@ +import Foundation + /// The deck's only terminal-presentation seam. Classic Multiplex opens a new /// scene; the single-window shell adopts the same route as one or more tabs. /// Keeping the destination alongside the closure also lets the tile's @@ -25,15 +27,15 @@ struct TerminalRouteOpener { var duplicateAttachTitle: String { switch destination { - case .window: "Attach in New Window" - case .shell: "Attach as New Tab" + case .window: String(localized: "Attach in New Window") + case .shell: String(localized: "Attach as New Tab") } } var openTabAccessibilityText: String { switch destination { - case .window: "Shows its open window" - case .shell: "Shows its open tab" + case .window: String(localized: "Shows its open window") + case .shell: String(localized: "Shows its open tab") } } } diff --git a/Multiplex/Views/Terminal/AgentHelperStrip.swift b/Multiplex/Views/Terminal/AgentHelperStrip.swift index 5010cd16..2448f134 100644 --- a/Multiplex/Views/Terminal/AgentHelperStrip.swift +++ b/Multiplex/Views/Terminal/AgentHelperStrip.swift @@ -249,14 +249,14 @@ final class AgentHelperStripViewController: UIViewController, } if !customs.isEmpty { groups.append(UIMenu( - title: "Custom", + title: String(localized: "Custom"), options: .displayInline, children: customs )) } let customize = UIAction( - title: "Customize Commands…", + title: String(localized: "Customize Commands…"), image: UIImage(systemName: "slider.horizontal.3") ) { [weak self] _ in self?.perform(.customize) @@ -337,7 +337,9 @@ final class AgentHelperStripViewController: UIViewController, if isCollapsed { let dot = AgentHelperStripDotButton( glyph: configuration.agent.glyph, - accessibilityLabel: "Show \(configuration.agent.displayName) helpers", + accessibilityLabel: String( + localized: "Show \(configuration.agent.displayName) helpers" + ), action: { [weak self] in self?.perform(.expand) } ) dot.accessibilityIdentifier = "agentHelpers.dot" @@ -362,7 +364,9 @@ final class AgentHelperStripViewController: UIViewController, let agent = AgentHelperStripTitleButton( title: configuration.agent.displayName, - accessibilityLabel: "Hide \(configuration.agent.displayName) helpers", + accessibilityLabel: String( + localized: "Hide \(configuration.agent.displayName) helpers" + ), action: { [weak self] in self?.perform(.collapse) } ) agent.accessibilityIdentifier = "agentHelpers.agent" @@ -382,14 +386,14 @@ final class AgentHelperStripViewController: UIViewController, row.addArrangedSubview(makeCommandRail(historyAvailable: available)) } else { let pro = AgentHelperStripButton( - caption: "✳ AGENT HELPERS · PRO", + caption: String(localized: "✳ AGENT HELPERS · PRO"), prominent: true, - accessibilityLabel: "Agent helpers Pro", + accessibilityLabel: String(localized: "Agent helpers Pro"), action: { [weak self] in self?.perform(.openPaywall) } ) pro.accessibilityIdentifier = "agentHelpers.pro" let detail = UILabel() - detail.text = "Free daily command taps return tomorrow" + detail.text = String(localized: "Free daily command taps return tomorrow") detail.font = UIKitChassis.monoFont(8, weight: .medium) detail.textColor = UIKitChassis.signal3 detail.numberOfLines = 1 @@ -449,10 +453,15 @@ final class AgentHelperStripViewController: UIViewController, ]) for command in barCustomCommands { + let submitMode = command.autoSubmit + ? String(localized: "auto submit") + : String(localized: "type only") let button = AgentHelperStripButton( caption: command.barLabel ?? command.menuLabel, color: TallyPalette.customCommand, - accessibilityLabel: "Custom command \(command.menuLabel), \(command.autoSubmit ? "auto submit" : "type only")", + accessibilityLabel: String( + localized: "Custom command \(command.menuLabel), \(submitMode)" + ), action: { [weak self] in self?.perform(.send(command.agentCommand)) } ) button.accessibilityIdentifier = "agentHelpers.custom.\(command.id.uuidString)" @@ -471,7 +480,9 @@ final class AgentHelperStripViewController: UIViewController, let more = AgentHelperStripButton( caption: "MORE", - accessibilityLabel: "More \(configuration.agent.displayName) commands" + accessibilityLabel: String( + localized: "More \(configuration.agent.displayName) commands" + ) ) more.accessibilityIdentifier = "agentHelpers.more" more.menu = makeMoreMenu() @@ -484,7 +495,9 @@ final class AgentHelperStripViewController: UIViewController, if historyAvailable { let history = AgentHelperStripButton( caption: "HIST", - accessibilityLabel: "Message history for \(configuration.agent.displayName)", + accessibilityLabel: String( + localized: "Message history for \(configuration.agent.displayName)" + ), action: { [weak self] in self?.perform(.history) } ) history.accessibilityIdentifier = "agentHelpers.history" diff --git a/Multiplex/Views/Terminal/AgentHistoryPanel.swift b/Multiplex/Views/Terminal/AgentHistoryPanel.swift index 808b5990..b3d3c37c 100644 --- a/Multiplex/Views/Terminal/AgentHistoryPanel.swift +++ b/Multiplex/Views/Terminal/AgentHistoryPanel.swift @@ -150,7 +150,7 @@ final class AgentHistoryPanelViewController: UIViewController { let close = UIKitChassisChip( "CLOSE", - accessibilityLabel: "Close", + accessibilityLabel: String(localized: "Close"), action: { [weak self] in self?.dismiss() } ) close.accessibilityIdentifier = "agentHistory.close" @@ -208,9 +208,9 @@ final class AgentHistoryPanelViewController: UIViewController { // token every other busy indicator in the app uses. spinner.color = UIKitChassis.signal2 spinner.startAnimating() - spinner.accessibilityLabel = "Reading session file" + spinner.accessibilityLabel = String(localized: "Reading session file") let label = UIKitChassisLabel( - "READING SESSION FILE", + String(localized: "READING SESSION FILE"), size: 9, color: UIKitChassis.signal3 ) @@ -236,7 +236,7 @@ final class AgentHistoryPanelViewController: UIViewController { if messages.isEmpty { content = makeStatusRow([ UIKitChassisLabel( - "NO MESSAGES YET", + String(localized: "NO MESSAGES YET"), size: 9, color: UIKitChassis.signal3 ), @@ -478,7 +478,7 @@ private final class AgentHistoryMessageRowView: UIView { if showsJump { let jumpButton = UIKitChassisChip( "JUMP", - accessibilityLabel: "Scroll the terminal back to this message", + accessibilityLabel: String(localized: "Scroll the terminal back to this message"), action: { jump(message) } ) jumpButton.accessibilityIdentifier = "agentHistory.jump.\(message.ordinal)" @@ -521,8 +521,8 @@ private final class AgentHistoryMessageRowView: UIView { $0.removeFromSuperview() } messageControl.accessibilityLabel = expanded - ? "Collapse message" - : "Expand message" + ? String(localized: "Collapse message") + : String(localized: "Expand message") if expanded { let fullText = AgentHistorySelectableTextView(text: message.text) diff --git a/Multiplex/Views/Terminal/CustomAgentCommandPanel.swift b/Multiplex/Views/Terminal/CustomAgentCommandPanel.swift index f7d07b42..40545ce1 100644 --- a/Multiplex/Views/Terminal/CustomAgentCommandPanel.swift +++ b/Multiplex/Views/Terminal/CustomAgentCommandPanel.swift @@ -271,8 +271,10 @@ final class CustomAgentCommandPanelViewController: UIViewController { titleRow.spacing = 12 let description = UILabel() - description.text = "Place each built-in in Bar or More. Custom content may span " - + "many lines; turn Submit off to leave it ready to edit." + description.text = String(localized: """ + Place each built-in in Bar or More. Custom content may span \ + many lines; turn Submit off to leave it ready to edit. + """) description.font = UIKitChassis.uiFont(11) description.textColor = UIKitChassis.signal2 description.numberOfLines = 0 @@ -356,11 +358,17 @@ final class CustomAgentCommandPanelViewController: UIViewController { let legend = TallyEditorLegend.stack([ TallyEditorLegend.row( color: TallyPalette.customCommand, - text: "Built-ins and custom commands each live in Bar or More; custom Bar labels keep 9 characters." + text: String(localized: """ + Built-ins and custom commands each live in Bar or More; \ + custom Bar labels keep 9 characters. + """) ), TallyEditorLegend.row( color: UIKitChassis.signal3, - text: "Shared keeps one editable command synchronized across Claude Code, Codex, Pi, and Grok Build." + text: String(localized: """ + Shared keeps one editable command synchronized across \ + Claude Code, Codex, Pi, and Grok Build. + """) ), ]) let actions = TallyEditorFooter.actions( @@ -456,7 +464,7 @@ final class CustomAgentCommandPanelViewController: UIViewController { if drafts.commands.isEmpty { let empty = UIKitChassisLabel( - "NO CUSTOM COMMANDS", + String(localized: "NO CUSTOM COMMANDS"), size: 10, color: UIKitChassis.signal3 ) @@ -624,8 +632,10 @@ private final class CustomCommandAccordionControl: UIControl { accessibilityIdentifier = "customCommands.builtInAccordion" isAccessibilityElement = true accessibilityTraits = .button - accessibilityLabel = "Built-in commands" - accessibilityHint = "Shows placement controls for built-in commands" + accessibilityLabel = String(localized: "Built-in commands") + accessibilityHint = String( + localized: "Shows placement controls for built-in commands" + ) addTarget(self, action: #selector(pressed), for: .touchUpInside) #if os(visionOS) hoverStyle = UIHoverStyle(effect: .highlight, shape: .rect(cornerRadius: 2)) @@ -671,7 +681,9 @@ private final class CustomCommandAccordionControl: UIControl { } private func updateState() { - accessibilityValue = expanded ? "Expanded" : "Collapsed" + accessibilityValue = expanded + ? String(localized: "Expanded") + : String(localized: "Collapsed") chevron.transform = expanded ? CGAffineTransform(rotationAngle: .pi / 2) : .identity @@ -927,7 +939,7 @@ private final class CustomCommandRowView: UIView, UITextViewDelegate { editor.smartQuotesType = .no editor.smartInsertDeleteType = .no editor.delegate = self - editor.accessibilityLabel = "Command content" + editor.accessibilityLabel = String(localized: "Command content") editor.accessibilityIdentifier = "customCommands.content.\(command.id.uuidString)" editor.layer.borderWidth = 1 editor.layer.borderColor = UIKitChassis.bezelHi @@ -946,19 +958,21 @@ private final class CustomCommandRowView: UIView, UITextViewDelegate { let switches = UIStackView(arrangedSubviews: [ makeSwitch( label: "SUBMIT", - accessibilityLabel: "Auto Submit", + accessibilityLabel: String(localized: "Auto Submit"), value: command.autoSubmit, keyPath: \.autoSubmit ), makeSwitch( label: "BAR", - accessibilityLabel: "Show in Bar", + accessibilityLabel: String(localized: "Show in Bar"), value: command.showInBar, keyPath: \.showInBar ), makeSwitch( label: "SHARED", - accessibilityLabel: "Shared across Claude Code, Codex, Pi, and Grok Build", + accessibilityLabel: String( + localized: "Shared across Claude Code, Codex, Pi, and Grok Build" + ), value: command.shared, keyPath: \.shared ), @@ -1118,7 +1132,7 @@ private final class CustomCommandTextView: UITextView { self.textContainer.lineFragmentPadding = 4 isScrollEnabled = false - placeholderLabel.text = "Command content" + placeholderLabel.text = String(localized: "Command content") placeholderLabel.font = UIKitChassis.monoFont(11) placeholderLabel.textColor = UIKitChassis.signal3 placeholderLabel.isAccessibilityElement = false diff --git a/Multiplex/Views/Terminal/FileAttachMenu.swift b/Multiplex/Views/Terminal/FileAttachMenu.swift index f405579a..4a6cb1fb 100644 --- a/Multiplex/Views/Terminal/FileAttachMenu.swift +++ b/Multiplex/Views/Terminal/FileAttachMenu.swift @@ -113,7 +113,7 @@ class FileAttachPickerPresenterViewController: UIViewController, var actions: [UIMenuElement] = [] #if !os(visionOS) actions.append(UIAction( - title: "Camera…", + title: String(localized: "Camera…"), image: UIImage(systemName: "camera"), identifier: UIAction.Identifier("terminal.attach.camera"), attributes: enabled && FileAttachAvailability.cameraAvailable @@ -123,7 +123,7 @@ class FileAttachPickerPresenterViewController: UIViewController, }) #endif actions.append(UIAction( - title: "Photo Library…", + title: String(localized: "Photo Library…"), image: UIImage(systemName: "photo.on.rectangle"), identifier: UIAction.Identifier("terminal.attach.photos"), attributes: enabled ? [] : .disabled @@ -131,7 +131,7 @@ class FileAttachPickerPresenterViewController: UIViewController, self?.request(.photoLibrary, target: target) }) actions.append(UIAction( - title: "Files…", + title: String(localized: "Files…"), image: UIImage(systemName: "folder"), identifier: UIAction.Identifier("terminal.attach.files"), attributes: enabled ? [] : .disabled @@ -463,7 +463,7 @@ final class FileAttachBadgeButton: UIButton { layer.borderWidth = 1 showsMenuAsPrimaryAction = true hoverStyle = UIHoverStyle(effect: .highlight, shape: .rect(cornerRadius: 2)) - accessibilityLabel = "Send a file to this session" + accessibilityLabel = String(localized: "Send a file to this session") symbolView.image = UIImage( systemName: "paperclip", diff --git a/Multiplex/Views/Terminal/FileViewerPane.swift b/Multiplex/Views/Terminal/FileViewerPane.swift index 1686c9d4..f5075360 100644 --- a/Multiplex/Views/Terminal/FileViewerPane.swift +++ b/Multiplex/Views/Terminal/FileViewerPane.swift @@ -678,7 +678,9 @@ final class FileViewerPaneViewController: UIViewController { additions: diff.additions, deletions: diff.deletions ) - headerCountsLabel.accessibilityLabel = "\(diff.additions) additions, \(diff.deletions) deletions" + headerCountsLabel.accessibilityLabel = String( + localized: "\(diff.additions) additions, \(diff.deletions) deletions" + ) headerCountsLabel.isHidden = false } else { headerCountsLabel.isHidden = true @@ -688,7 +690,7 @@ final class FileViewerPaneViewController: UIViewController { private func headerName(_ state: FileViewerPaneObservedState) -> String { switch state.body { case .document(let document): document.name - case .diff(_, .repo): "Working tree vs HEAD" + case .diff(_, .repo): String(localized: "Working tree vs HEAD") case .diff(_, .file(let path)): FileTree.name(of: path) case .loading(let label): label case .failure: "—" @@ -763,13 +765,16 @@ final class FileViewerPaneViewController: UIViewController { // rail stays uncluttered. The pair shows together (a dimmed twin // keeps the other from jumping around) and dims like REFRESH does. if state.canGoBack || state.canGoForward { - let back = makeChip("◂", accessibility: "Back") { [weak controller] in + let back = makeChip("◂", accessibility: String(localized: "Back")) { [weak controller] in controller?.goBack() } back.accessibilityIdentifier = "fileViewer.back" setChipEnabled(back, state.canGoBack) backChip = back - let forward = makeChip("▸", accessibility: "Forward") { [weak controller] in + let forward = makeChip( + "▸", + accessibility: String(localized: "Forward") + ) { [weak controller] in controller?.goForward() } forward.accessibilityIdentifier = "fileViewer.forward" @@ -789,13 +794,13 @@ final class FileViewerPaneViewController: UIViewController { let source = makeChip( "SOURCE", prominent: sourceMode, - accessibility: "Show source" + accessibility: String(localized: "Show source") ) { [weak controller] in controller?.showSource() } source.accessibilityIdentifier = "fileViewer.source" let diff = makeChip( "DIFF", prominent: !sourceMode, - accessibility: "Show diff" + accessibility: String(localized: "Show diff") ) { [weak self] in guard let self, case .document(let document) = self.observedState?.body @@ -812,7 +817,7 @@ final class FileViewerPaneViewController: UIViewController { mode.alignment = .center mode.spacing = 4 mode.isAccessibilityElement = false - mode.accessibilityLabel = "Source or diff" + mode.accessibilityLabel = String(localized: "Source or diff") railStack.addArrangedSubview(mode) } @@ -821,8 +826,8 @@ final class FileViewerPaneViewController: UIViewController { selectingMarkdownSource ? "DONE" : "SELECT", prominent: selectingMarkdownSource, accessibility: selectingMarkdownSource - ? "Back to rendered markdown" - : "Select source text to copy" + ? String(localized: "Back to rendered markdown") + : String(localized: "Select source text to copy") ) { [weak self] in self?.toggleMarkdownSelection() } select.accessibilityIdentifier = "fileViewer.markdownSelect" selectChip = select @@ -834,20 +839,25 @@ final class FileViewerPaneViewController: UIViewController { railStack.addArrangedSubview(railPathLabel) let host = FileViewerBadgeView(state.hostName.uppercased()) - host.accessibilityLabel = "Files on \(state.hostName)" + host.accessibilityLabel = String(localized: "Files on \(state.hostName)") host.setContentHuggingPriority(.required, for: .horizontal) railStack.addArrangedSubview(host) let treeVisible = isCompactLayout ? drawerOpen : treeDocked let tree = makeChip( treeVisible ? "HIDE" : "TREE", - accessibility: treeVisible ? "Hide the file tree" : "Show the file tree" + accessibility: treeVisible + ? String(localized: "Hide the file tree") + : String(localized: "Show the file tree") ) { [weak self] in self?.toggleTree() } tree.accessibilityIdentifier = "fileViewer.tree" treeChip = tree railStack.addArrangedSubview(tree) - let refresh = makeChip("REFRESH", accessibility: "Refresh file viewer") { [weak controller] in + let refresh = makeChip( + "REFRESH", + accessibility: String(localized: "Refresh file viewer") + ) { [weak controller] in controller?.refresh() } refresh.accessibilityIdentifier = "fileViewer.refresh" @@ -858,7 +868,7 @@ final class FileViewerPaneViewController: UIViewController { let close = makeChip( "CLOSE", prominent: true, - accessibility: "Close file viewer" + accessibility: String(localized: "Close file viewer") ) { [weak self] in self?.closeAction() } close.accessibilityIdentifier = "fileViewer.close" closeChip = close @@ -1063,9 +1073,12 @@ final class FileViewerPaneViewController: UIViewController { private func makeBody(for state: FileViewerPaneObservedState) -> UIView { switch state.body { case .idle: + let gitHint = state.hasGitRoot + ? String(localized: ", or open the branch's diff from its ± counts") + : "" return FileViewerMessageView( caption: "NO FILE ON SCREEN", - detail: "Pick a file from the tree\(state.hasGitRoot ? ", or open the branch's diff from its ± counts" : "").", + detail: String(localized: "Pick a file from the tree\(gitHint)."), captionColor: UIKitChassis.signal3, detailFont: UIFont.preferredFont(forTextStyle: .footnote), maximumWidth: 300 @@ -1119,7 +1132,10 @@ final class FileViewerPaneViewController: UIViewController { return verdictPanel( document, caption: "LOCKED", - message: "Password-protected. Unlocking opens it on this screen only — the password is not kept.", + message: String(localized: """ + Password-protected. Unlocking opens it on this screen only — the \ + password is not kept. + """), action: ("UNLOCK…", { [weak self] in self?.presentPDFUnlock(retrying: false) }) ) } @@ -1135,8 +1151,10 @@ final class FileViewerPaneViewController: UIViewController { return verdictPanel( document, caption: "CAN'T PLAY", - message: "This device can't decode it — a format Core Audio doesn't read " - + "(Ogg Vorbis, for one), or not audio at all." + message: String(localized: """ + This device can't decode it — a format Core Audio doesn't read (Ogg \ + Vorbis, for one), or not audio at all. + """) ) } let view = FileViewerAudioContentView(volumeStore: .shared) @@ -1171,7 +1189,9 @@ final class FileViewerPaneViewController: UIViewController { private func binaryBody(_ document: FileViewerController.Document) -> UIView { verdictPanel( - document, caption: "BINARY", message: "Not text — Multiplex won't render it as code." + document, + caption: "BINARY", + message: String(localized: "Not text — Multiplex won't render it as code.") ) } @@ -1203,21 +1223,27 @@ final class FileViewerPaneViewController: UIViewController { private func presentPDFUnlock(retrying: Bool) { guard presentedViewController == nil else { return } let alert = UIAlertController( - title: "Unlock PDF", + title: String(localized: "Unlock PDF"), message: retrying - ? "That password didn't unlock it. Try again." - : "Enter the document's password.", + ? String(localized: "That password didn't unlock it. Try again.") + : String(localized: "Enter the document's password."), preferredStyle: .alert ) alert.addTextField { field in field.isSecureTextEntry = true - field.placeholder = "Password" + field.placeholder = String(localized: "Password") field.returnKeyType = .go } - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak alert] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Cancel"), + style: .cancel + ) { [weak alert] _ in alert?.textFields?.first?.text = "" }) - alert.addAction(UIAlertAction(title: "Unlock", style: .default) { [weak self, weak alert] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Unlock"), + style: .default + ) { [weak self, weak alert] _ in let password = alert?.textFields?.first?.text ?? "" alert?.textFields?.first?.text = "" guard let self else { return } @@ -1591,7 +1617,9 @@ class FileViewerPanelHostView: UIView { enum FileViewerZoomReadout { static func makeChip(action: @escaping () -> Void) -> UIKitChassisChip { let chip = UIKitChassisChip( - "100% · FIT", accessibilityLabel: "Zoom 100 percent; resets to fit", action: action + "100% · FIT", + accessibilityLabel: String(localized: "Zoom 100 percent; resets to fit"), + action: action ) chip.isHidden = true return chip @@ -1601,7 +1629,7 @@ enum FileViewerZoomReadout { static func show(ratio: CGFloat, on chip: UIKitChassisChip) { let percent = Int((ratio * 100).rounded()) chip.setContent(caption: "\(percent)% · FIT", systemImage: nil) - chip.accessibilityLabel = "Zoom \(percent) percent; resets to fit" + chip.accessibilityLabel = String(localized: "Zoom \(percent) percent; resets to fit") chip.isHidden = abs(ratio - 1) < 0.01 } } @@ -1653,7 +1681,10 @@ final class FileViewerCodeContentView: UIView { override init(frame: CGRect) { let caption = UIKitChassisLabel("TRUNCATED", size: 8, color: TallyPalette.caution) let detail = UILabel() - detail.text = "Showing the first \(FileViewerController.formatBytes(UInt64(FileViewerController.textByteLimit)))." + detail.text = String(localized: """ + Showing the first \ + \(FileViewerController.formatBytes(UInt64(FileViewerController.textByteLimit))). + """) detail.font = UIFont.preferredFont(forTextStyle: .footnote) detail.adjustsFontForContentSizeCategory = true detail.textColor = UIKitChassis.signal3 @@ -1799,7 +1830,7 @@ final class FileViewerDiffContentView: UIView { private(set) var textView = makeFileViewerTextView() private(set) var cleanView = FileViewerMessageView( caption: "NOTHING TO DIFF", - detail: "The working tree matches HEAD here.", + detail: String(localized: "The working tree matches HEAD here."), captionColor: UIKitChassis.signal3, detailFont: UIFont.preferredFont(forTextStyle: .footnote), topInset: 60 @@ -1902,7 +1933,7 @@ final class FileViewerImageContentView: UIView, UIScrollViewDelegate { imageView.contentMode = .scaleAspectFit imageView.isAccessibilityElement = true - imageView.accessibilityLabel = "Image, pinch to zoom" + imageView.accessibilityLabel = String(localized: "Image, pinch to zoom") scrollView.addSubview(imageView) let doubleTap = UITapGestureRecognizer(target: self, action: #selector(doubleTapped)) doubleTap.numberOfTapsRequired = 2 @@ -2174,8 +2205,8 @@ final class FileViewerAudioContentView: FileViewerPanelHostView { init(volumeStore: FileViewerAudioVolumeStore) { self.volumeStore = volumeStore - slider = makeChassisSlider(accessibilityLabel: "Playback position") - volumeSlider = makeChassisSlider(accessibilityLabel: "Volume") + slider = makeChassisSlider(accessibilityLabel: String(localized: "Playback position")) + volumeSlider = makeChassisSlider(accessibilityLabel: String(localized: "Volume")) super.init(panel: panel, outerInset: 20) panel.backgroundColor = UIKitChassis.bezel @@ -2192,7 +2223,7 @@ final class FileViewerAudioContentView: FileViewerPanelHostView { backChip = UIKitChassisChip( "15", systemImage: "gobackward", - accessibilityLabel: "Back 15 seconds", + accessibilityLabel: String(localized: "Back 15 seconds"), action: { [weak self] in self?.clip?.skip(by: -FileViewerAudioClip.skipInterval) self?.refresh() @@ -2202,7 +2233,7 @@ final class FileViewerAudioContentView: FileViewerPanelHostView { "PLAY", systemImage: "play.fill", prominent: true, - accessibilityLabel: "Play", + accessibilityLabel: String(localized: "Play"), action: { [weak self] in self?.clip?.togglePlayback() self?.refresh() @@ -2211,7 +2242,7 @@ final class FileViewerAudioContentView: FileViewerPanelHostView { forwardChip = UIKitChassisChip( "15", systemImage: "goforward", - accessibilityLabel: "Forward 15 seconds", + accessibilityLabel: String(localized: "Forward 15 seconds"), action: { [weak self] in self?.clip?.skip(by: FileViewerAudioClip.skipInterval) self?.refresh() @@ -2320,7 +2351,9 @@ final class FileViewerAudioContentView: FileViewerPanelHostView { caption: playing ? "PAUSE" : "PLAY", systemImage: playing ? "pause.fill" : "play.fill" ) - playChip.accessibilityLabel = playing ? "Pause" : "Play" + playChip.accessibilityLabel = playing + ? String(localized: "Pause") + : String(localized: "Play") setLamp(playing ? .playing : (clip.currentTime > 0 ? .paused : .ready)) let duration = clip.duration let position = duration > 0 ? Float(clip.currentTime / duration) : 0 @@ -2330,7 +2363,9 @@ final class FileViewerAudioContentView: FileViewerPanelHostView { let elapsed = FileViewerAudioClock.label(clip.currentTime) if elapsedLabel.text != elapsed { elapsedLabel.text = elapsed - slider.accessibilityValue = "\(elapsed) of \(FileViewerAudioClock.label(duration))" + slider.accessibilityValue = String( + localized: "\(elapsed) of \(FileViewerAudioClock.label(duration))" + ) } // The clock only moves while the clip plays; a paused panel needs no // wakeups, and the tick that sees playback end stops itself. @@ -2758,7 +2793,7 @@ final class FileViewerMarkdownImageView: UIKitTallyBorderedView { self.openFull = openFull openChip = UIKitChassisChip( "OPEN FILE", - accessibilityLabel: "Open this file on its own screen", + accessibilityLabel: String(localized: "Open this file on its own screen"), action: openFull ) super.init(frame: .zero) @@ -2814,7 +2849,7 @@ final class FileViewerMarkdownImageView: UIKitTallyBorderedView { captionLabel.isHidden = false openChip.isHidden = true isAccessibilityElement = true - accessibilityLabel = "Loading image" + accessibilityLabel = String(localized: "Loading image") case .ready(let image): imageView.image = image imageView.isHidden = false @@ -2822,16 +2857,16 @@ final class FileViewerMarkdownImageView: UIKitTallyBorderedView { openChip.isHidden = true isAccessibilityElement = false imageView.isAccessibilityElement = true - imageView.accessibilityLabel = "Image, opens on its own screen" + imageView.accessibilityLabel = String(localized: "Image, opens on its own screen") imageView.accessibilityTraits = .button case .failed(let reason): imageView.image = nil imageView.isHidden = true - captionLabel.setText("CAN'T SHOW — \(reason)") + captionLabel.setText(String(localized: "CAN'T SHOW — \(reason)")) captionLabel.isHidden = false openChip.isHidden = false isAccessibilityElement = true - accessibilityLabel = "Can't show this image. \(reason)" + accessibilityLabel = String(localized: "Can't show this image. \(reason)") } applyHeight() } diff --git a/Multiplex/Views/Terminal/FileViewerTextView.swift b/Multiplex/Views/Terminal/FileViewerTextView.swift index f19e4ea3..904a5546 100644 --- a/Multiplex/Views/Terminal/FileViewerTextView.swift +++ b/Multiplex/Views/Terminal/FileViewerTextView.swift @@ -190,12 +190,12 @@ struct FileViewerTextContent { builder.line(pieces, ground: fileGround) } if file.isBinary { - builder.line([("Binary files differ.", font, quietInk)]) + builder.line([(String(localized: "Binary files differ."), font, quietInk)]) } else if file.hunks.isEmpty { builder.line([( file.kind == .renamed - ? "Rename only — contents unchanged." - : "No textual hunks.", + ? String(localized: "Rename only — contents unchanged.") + : String(localized: "No textual hunks."), font, quietInk )]) } diff --git a/Multiplex/Views/Terminal/FileViewerTreeColumn.swift b/Multiplex/Views/Terminal/FileViewerTreeColumn.swift index e25358ee..f679b26d 100644 --- a/Multiplex/Views/Terminal/FileViewerTreeColumn.swift +++ b/Multiplex/Views/Terminal/FileViewerTreeColumn.swift @@ -205,7 +205,7 @@ final class FileViewerTreeColumnView: UIView, UITableViewDataSource, }) else { return nil } return UIMenu(children: [ UIAction( - title: "Open in New Tab", + title: String(localized: "Open in New Tab"), image: UIImage(systemName: "plus.square.on.square") ) { [weak self] _ in self?.openInNewTab(path: path) @@ -274,7 +274,7 @@ final class FileViewerTreeColumnView: UIView, UITableViewDataSource, } else { cell.accessibilityCustomActions = [ UIAccessibilityCustomAction( - name: "Open in New Tab", + name: String(localized: "Open in New Tab"), actionHandler: { [weak self] _ in guard let self else { return false } self.openInNewTab(path: row.entry.path) @@ -350,7 +350,7 @@ final class FileViewerTreeColumnView: UIView, UITableViewDataSource, let chip = UIKitChassisChip( "UP", - accessibilityLabel: "Show the parent directory", + accessibilityLabel: String(localized: "Show the parent directory"), action: { [weak self] in self?.onUp() } ) upChip = chip @@ -383,7 +383,7 @@ final class FileViewerTreeColumnView: UIView, UITableViewDataSource, counts.backgroundColor = .clear counts.contentEdgeInsets = .zero counts.contentHorizontalAlignment = .leading - counts.accessibilityLabel = "Open the working tree's diff" + counts.accessibilityLabel = String(localized: "Open the working tree's diff") counts.hoverStyle = UIHoverStyle( effect: .highlight, shape: .rect(cornerRadius: 2) @@ -393,7 +393,7 @@ final class FileViewerTreeColumnView: UIView, UITableViewDataSource, let changed = UIKitChassisChip( "CHANGED", - accessibilityLabel: "Show only changed files", + accessibilityLabel: String(localized: "Show only changed files"), action: { [weak self] in self?.onChangedFilter() } ) changedChip = changed @@ -447,7 +447,8 @@ final class FileViewerTreeColumnView: UIView, UITableViewDataSource, if first || previous?.changedFilter != snapshot.changedFilter { changedChip?.isProminent = snapshot.changedFilter changedChip?.accessibilityLabel = snapshot.changedFilter - ? "Show the whole tree" : "Show only changed files" + ? String(localized: "Show the whole tree") + : String(localized: "Show only changed files") } } @@ -724,7 +725,9 @@ enum FileViewerTreeColumn { static func rowAccessibilityLabel(_ row: FileTree.Row) -> String { var label = row.entry.name if row.entry.isDirectory { - label += row.isExpanded ? ", expanded folder" : ", folder" + label += ", " + (row.isExpanded + ? String(localized: "expanded folder") + : String(localized: "folder")) } if let badge = row.badge { label += ", " + badgeWord(badge) @@ -734,12 +737,12 @@ enum FileViewerTreeColumn { static func badgeWord(_ badge: GitFileStatus.Badge) -> String { switch badge { - case .modified: "modified" - case .added: "staged addition" - case .deleted: "deleted" - case .renamed: "renamed" - case .untracked: "untracked" - case .conflicted: "conflicted" + case .modified: String(localized: "modified") + case .added: String(localized: "staged addition") + case .deleted: String(localized: "deleted") + case .renamed: String(localized: "renamed") + case .untracked: String(localized: "untracked") + case .conflicted: String(localized: "conflicted") } } diff --git a/Multiplex/Views/Terminal/KeyCommandPanel.swift b/Multiplex/Views/Terminal/KeyCommandPanel.swift index b598d87e..39bcb1a3 100644 --- a/Multiplex/Views/Terminal/KeyCommandPanel.swift +++ b/Multiplex/Views/Terminal/KeyCommandPanel.swift @@ -242,7 +242,7 @@ final class KeyCommandPanelViewController: UIViewController { case .commands: contentStack.addArrangedSubview(makeCommandGrid()) contentStack.addArrangedSubview(makeFootnote( - "TAP SENDS · DOT = STAYS OPEN · HOLD A CELL TO EDIT · TAP OUTSIDE CLOSES" + String(localized: "TAP SENDS · DOT = STAYS OPEN · HOLD A CELL TO EDIT · TAP OUTSIDE CLOSES") )) case .setup: contentStack.addArrangedSubview(makeRows()) @@ -290,13 +290,13 @@ final class KeyCommandPanelViewController: UIViewController { private func makeTabPair() -> UIView { let commands = KeyCommandSegmentCell( title: "COMMANDS", - accessibilityLabel: "Commands", + accessibilityLabel: String(localized: "Commands"), selected: selectedTab == .commands ) { [weak self] in self?.selectTab(.commands) } commands.accessibilityIdentifier = "keyCommands.tab.commands" let setup = KeyCommandSegmentCell( title: "CUSTOM SETUP", - accessibilityLabel: "Custom setup", + accessibilityLabel: String(localized: "Custom setup"), selected: selectedTab == .setup ) { [weak self] in self?.selectTab(.setup) } setup.accessibilityIdentifier = "keyCommands.tab.setup" @@ -315,7 +315,7 @@ final class KeyCommandPanelViewController: UIViewController { ) } if cells.isEmpty { - cells.append(makeEmptyLabel("NO COMMANDS · CUSTOM SETUP")) + cells.append(makeEmptyLabel(String(localized: "NO COMMANDS · CUSTOM SETUP"))) } return TallyHairlineGrid.grid(cells, columns: 2) } @@ -364,7 +364,7 @@ final class KeyCommandPanelViewController: UIViewController { stack.addArrangedSubview(row) } if drafts.isEmpty { - stack.addArrangedSubview(makeEmptyLabel("NO COMMANDS")) + stack.addArrangedSubview(makeEmptyLabel(String(localized: "NO COMMANDS"))) } return stack } @@ -384,14 +384,19 @@ final class KeyCommandPanelViewController: UIViewController { var rows = [ TallyEditorLegend.row( color: TallyPalette.customCommand, - text: "Chords send what a hardware keyboard would; text rows type one line, " - + "then Enter when SUBMIT is on." + text: String(localized: """ + Chords send what a hardware keyboard would; text rows type one line, \ + then Enter when SUBMIT is on. + """) ), TallyEditorLegend.row( color: UIKitChassis.signal3, - text: "Repeat guard: ×\(KeyCommandRepeatGuard.maximumCount) at most, " - + "\(KeyCommandRepeatGuard.gapRange.lowerBound)–\(KeyCommandRepeatGuard.gapRange.upperBound) ms " - + "between sends, whole burst within \(KeyCommandRepeatGuard.burstLimitMilliseconds / 1000) s. Values clamp." + text: String(localized: """ + Repeat guard: ×\(KeyCommandRepeatGuard.maximumCount) at most, \ + \(KeyCommandRepeatGuard.gapRange.lowerBound)–\(KeyCommandRepeatGuard.gapRange.upperBound) ms \ + between sends, whole burst within \ + \(KeyCommandRepeatGuard.burstLimitMilliseconds / 1000) s. Values clamp. + """) ), ] // The tier line exists only where a tier applies: Pro (and the @@ -399,8 +404,10 @@ final class KeyCommandPanelViewController: UIViewController { if plan.upgrade != nil, limit < KeyCommandSet.maximumCount { let tier = TallyEditorLegend.row( color: UIKitChassis.signal2, - text: "Free keeps \(limit) commands; Multiplex Pro raises the set to " - + "\(KeyCommandSet.maximumCount)." + text: String(localized: """ + Free keeps \(limit) commands; Multiplex Pro raises the set to \ + \(KeyCommandSet.maximumCount). + """) ) tier.accessibilityIdentifier = "keyCommands.legend.tier" rows.append(tier) @@ -931,8 +938,8 @@ final class KeyCommandRowView: UIView { ]) headerControl.accessibilityIdentifier = "keyCommands.rowHeader.\(command.id.uuidString)" headerControl.accessibilityLabel = expanded - ? "\(command.name), editing" - : "\(command.name), edit" + ? String(localized: "\(command.name), editing") + : String(localized: "\(command.name), edit") headerControl.addTarget(self, action: #selector(headerPressed), for: .touchUpInside) let headerLine = UIStackView(arrangedSubviews: [headerControl, trio.stack]) @@ -1163,11 +1170,13 @@ final class KeyCommandComposerView: UIView, UITextFieldDelegate { // TYPE let keysCell = KeyCommandSegmentCell( - title: "KEYS", accessibilityLabel: "Keys", selected: true, size: 8, inset: 6 + title: "KEYS", accessibilityLabel: String(localized: "Keys"), + selected: true, size: 8, inset: 6 ) { [weak self] in self?.setKind(isText: false) } keysCell.accessibilityIdentifier = "keyCommands.composer.type.keys" let textCell = KeyCommandSegmentCell( - title: "TEXT", accessibilityLabel: "Text", selected: false, size: 8, inset: 6 + title: "TEXT", accessibilityLabel: String(localized: "Text"), + selected: false, size: 8, inset: 6 ) { [weak self] in self?.setKind(isText: true) } textCell.accessibilityIdentifier = "keyCommands.composer.type.text" keysTypeCell = keysCell @@ -1224,7 +1233,7 @@ final class KeyCommandComposerView: UIView, UITextFieldDelegate { return row } characterField.configure(placeholder: "A", width: 30, monoSize: 12, height: KeyCommandMetrics.composerCap) - characterField.accessibilityLabel = "Letter or digit" + characterField.accessibilityLabel = String(localized: "Letter or digit") characterField.accessibilityIdentifier = "keyCommands.composer.character" characterField.delegate = self characterField.addTarget(self, action: #selector(characterEdited), for: .editingChanged) @@ -1268,19 +1277,19 @@ final class KeyCommandComposerView: UIView, UITextFieldDelegate { textBlock.alignment = .fill textBlock.spacing = 10 textField.configure( - placeholder: "One line, typed as-is", + placeholder: String(localized: "One line, typed as-is"), width: 220, monoSize: 11, height: KeyCommandMetrics.composerCap ) - textField.accessibilityLabel = "Text to type" + textField.accessibilityLabel = String(localized: "Text to type") textField.accessibilityIdentifier = "keyCommands.composer.text" textField.delegate = self textField.addTarget(self, action: #selector(textEdited), for: .editingChanged) textField.setContentHuggingPriority(.defaultLow, for: .horizontal) let submit = TallyEditorSwitch( label: "SUBMIT", - accessibilityLabel: "Submit with Enter", + accessibilityLabel: String(localized: "Submit with Enter"), isOn: command.textSnippet?.submits ?? true, identifierPrefix: "keyCommands", scale: KeyCommandMetrics.scale @@ -1299,7 +1308,7 @@ final class KeyCommandComposerView: UIView, UITextFieldDelegate { range: 1...KeyCommandRepeatGuard.maximumCount, step: 1, format: { "×\($0)" }, - accessibilityLabel: "Repeat count", + accessibilityLabel: String(localized: "Repeat count"), changed: { [weak self] value in self?.setRepeatCount(value) } ) count.accessibilityIdentifier = "keyCommands.composer.count" @@ -1315,7 +1324,7 @@ final class KeyCommandComposerView: UIView, UITextFieldDelegate { range: KeyCommandRepeatGuard.gapRange, step: KeyCommandRepeatGuard.gapStep, format: { "\($0) MS" }, - accessibilityLabel: "Gap between sends", + accessibilityLabel: String(localized: "Gap between sends"), changed: { [weak self] value in self?.setRepeatGap(value) } ) gap.accessibilityIdentifier = "keyCommands.composer.gap" @@ -1340,7 +1349,7 @@ final class KeyCommandComposerView: UIView, UITextFieldDelegate { // PANEL let closes = TallyEditorSwitch( label: "CLOSE ON PRESS", - accessibilityLabel: "Close the panel on press", + accessibilityLabel: String(localized: "Close the panel on press"), isOn: command.closesPanel, identifierPrefix: "keyCommands", scale: KeyCommandMetrics.scale @@ -1565,14 +1574,14 @@ final class KeyCommandStepper: UIView { super.init(frame: .zero) minus = TallyEditorRowActionButton( systemImage: "minus", - accessibilityLabel: "\(accessibilityLabel), less", + accessibilityLabel: String(localized: "\(accessibilityLabel), less"), enabled: true, scale: KeyCommandMetrics.scale, action: { [weak self] in self?.adjust(-1) } ) plus = TallyEditorRowActionButton( systemImage: "plus", - accessibilityLabel: "\(accessibilityLabel), more", + accessibilityLabel: String(localized: "\(accessibilityLabel), more"), enabled: true, scale: KeyCommandMetrics.scale, action: { [weak self] in self?.adjust(1) } diff --git a/Multiplex/Views/Terminal/ShortcutPanel.swift b/Multiplex/Views/Terminal/ShortcutPanel.swift index edea66ab..6e840ae0 100644 --- a/Multiplex/Views/Terminal/ShortcutPanel.swift +++ b/Multiplex/Views/Terminal/ShortcutPanel.swift @@ -315,12 +315,12 @@ final class ShortcutPanelViewController: UIViewController { field.clearButtonMode = .whileEditing } let commit = UIAlertAction( - title: "Rename", style: .default + title: String(localized: "Rename"), style: .default ) { [weak self, weak alert] _ in self?.confirmRename(item, to: alert?.textFields?.first?.text ?? "") } alert.addAction(commit) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: String(localized: "Cancel"), style: .cancel)) present(alert, animated: true) } @@ -654,7 +654,9 @@ private final class ShortcutItemButton: ShortcutPressControl { func setArmed(_ armed: Bool) { isArmed = armed - commandLabel.setText(armed ? "press again to close" : item.command) + commandLabel.setText( + armed ? String(localized: "press again to close") : item.command + ) bindingLabel.configure( armed ? "AGAIN" : item.bindingLabel, font: UIKitChassis.monoFont(9, weight: .semibold), @@ -683,10 +685,10 @@ private final class ShortcutItemButton: ShortcutPressControl { isArmed: Bool ) -> String { if isArmed { - return "\(item.title), press again to close" + return String(localized: "\(item.title), press again to close") } if item.requiresDoubleActivation { - return "\(item.title), \(item.command), press twice to confirm" + return String(localized: "\(item.title), \(item.command), press twice to confirm") } return "\(item.title), \(item.command), \(item.bindingLabel)" } @@ -721,8 +723,9 @@ private final class ShortcutCompactItemButton: ShortcutPressControl { minimumHeight(40) accessibilityIdentifier = item.accessibilityIdentifier accessibilityLabel = "\(item.title), \(item.command), \(item.bindingLabel)" - accessibilityHint = - "Hold for \(TmuxShortcut.coarseResizeCells)-cell steps, repeating until release." + accessibilityHint = String( + localized: "Hold for \(TmuxShortcut.coarseResizeCells)-cell steps, repeating until release." + ) let face = UIImageView( image: item.symbolName.flatMap(UIImage.init(systemName:)) @@ -849,8 +852,8 @@ private final class ShortcutChoiceButton: ShortcutPressControl { choice.isActive = active activeLabel.isHidden = !active accessibilityLabel = active - ? "\(noun.capitalized) \(choice.index), \(choice.name), current \(noun)" - : "Switch to \(noun) \(choice.index), \(choice.name)" + ? String(localized: "\(noun.capitalized) \(choice.index), \(choice.name), current \(noun)") + : String(localized: "Switch to \(noun) \(choice.index), \(choice.name)") } } diff --git a/Multiplex/Views/Terminal/TalkbackComposer.swift b/Multiplex/Views/Terminal/TalkbackComposer.swift index f01a9b08..996bc8aa 100644 --- a/Multiplex/Views/Terminal/TalkbackComposer.swift +++ b/Multiplex/Views/Terminal/TalkbackComposer.swift @@ -423,7 +423,7 @@ final class TalkbackComposerViewController: UIViewController, UITextViewDelegate spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) spacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - closeButton.accessibilityLabel = "Close message box" + closeButton.accessibilityLabel = String(localized: "Close message box") closeButton.accessibilityIdentifier = "terminal.talkback.close" closeButton.addTarget(self, action: #selector(closePressed), for: .touchUpInside) @@ -460,13 +460,13 @@ final class TalkbackComposerViewController: UIViewController, UITextViewDelegate line.alignment = .bottom line.spacing = Self.lineSpacing - attachButton.accessibilityLabel = "Attach a file" + attachButton.accessibilityLabel = String(localized: "Attach a file") attachButton.accessibilityIdentifier = "terminal.talkback.attach" attachButton.showsMenuAsPrimaryAction = true textView.delegate = self textView.accessibilityIdentifier = "terminal.talkback.field" - textView.accessibilityLabel = "Message" + textView.accessibilityLabel = String(localized: "Message") textView.font = UIKitChassis.uiFont(15) textView.textColor = UIKitChassis.signal textView.backgroundColor = .clear @@ -504,7 +504,7 @@ final class TalkbackComposerViewController: UIViewController, UITextViewDelegate ), ]) - sendButton.accessibilityLabel = "Send" + sendButton.accessibilityLabel = String(localized: "Send") sendButton.accessibilityIdentifier = "terminal.talkback.send" sendButton.addTarget(self, action: #selector(sendPressed), for: .touchUpInside) let hold = UILongPressGestureRecognizer(target: self, action: #selector(sendHeld(_:))) @@ -642,10 +642,10 @@ final class TalkbackComposerViewController: UIViewController, UITextViewDelegate needsYouLamp = nil } header.accessibilityLabel = [ - "To", presentation.targetLabel, + String(localized: "To"), presentation.targetLabel, presentation.agent?.displayName, - running ? "running" : nil, - needsYou ? "needs you" : nil, + running ? String(localized: "running") : nil, + needsYou ? String(localized: "needs you") : nil, ].compactMap { $0 }.joined(separator: " ") } @@ -702,15 +702,15 @@ final class TalkbackComposerViewController: UIViewController, UITextViewDelegate case .ready: sendButton.style = .prominent sendButton.isEnabled = true - sendButton.accessibilityLabel = "Send" + sendButton.accessibilityLabel = String(localized: "Send") case .waiting: sendButton.style = .waiting sendButton.isEnabled = false - sendButton.accessibilityLabel = "Send, waiting for uploads" + sendButton.accessibilityLabel = String(localized: "Send, waiting for uploads") case .disabled: sendButton.style = .dim sendButton.isEnabled = false - sendButton.accessibilityLabel = "Send" + sendButton.accessibilityLabel = String(localized: "Send") } } @@ -1127,7 +1127,7 @@ final class TalkbackAttachmentView: UIControl { scrim.layer.addSublayer(ring) } - removeBadge.accessibilityLabel = "Remove attachment" + removeBadge.accessibilityLabel = String(localized: "Remove attachment") removeBadge.setImage( UIImage( systemName: "xmark", @@ -1209,13 +1209,14 @@ final class TalkbackAttachmentView: UIControl { ringFill.isHidden = false ringFill.strokeEnd = max(0.02, min(1, fraction)) detailLabel.setText(byteText, ink: UIKitChassis.signal3) - accessibilityLabel = "\(attachment.name), uploading \(Int(fraction * 100)) percent" + let percent = Int(fraction * 100) + accessibilityLabel = String(localized: "\(attachment.name), uploading \(percent) percent") case .ready: detailLabel.setText( isSquare ? byteText : "\(byteText) ✓", ink: UIKitChassis.signal3 ) - accessibilityLabel = "\(attachment.name), \(byteText), attached" + accessibilityLabel = String(localized: "\(attachment.name), \(byteText), attached") case .failed(let reason): // The photo stays visible under a scrim so the amber caption // reads; documents keep their name and say why. @@ -1231,8 +1232,8 @@ final class TalkbackAttachmentView: UIControl { ) } tile.tallyBorderColor = TallyPalette.caution - accessibilityLabel = "\(attachment.name), upload failed, \(reason)" - accessibilityHint = "Retries the upload" + accessibilityLabel = String(localized: "\(attachment.name), upload failed, \(reason)") + accessibilityHint = String(localized: "Retries the upload") } } diff --git a/Multiplex/Views/Terminal/TerminalDrop.swift b/Multiplex/Views/Terminal/TerminalDrop.swift index 7efa979c..a4eb2d25 100644 --- a/Multiplex/Views/Terminal/TerminalDrop.swift +++ b/Multiplex/Views/Terminal/TerminalDrop.swift @@ -151,7 +151,7 @@ final class DropTargetVeilView: UIView { ]) isAccessibilityElement = true - accessibilityLabel = "Drop to upload" + accessibilityLabel = String(localized: "Drop to upload") refreshResolvedLayerColors() } diff --git a/Multiplex/Views/Terminal/TerminalFilePathSheet.swift b/Multiplex/Views/Terminal/TerminalFilePathSheet.swift index 5b19ea60..dde16f96 100644 --- a/Multiplex/Views/Terminal/TerminalFilePathSheet.swift +++ b/Multiplex/Views/Terminal/TerminalFilePathSheet.swift @@ -64,7 +64,7 @@ final class TerminalFilePathSheetViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "View file" + title = String(localized: "View file") view.backgroundColor = GlassPrototype.sheetGround configureNavigation() configureContent() @@ -105,10 +105,12 @@ final class TerminalFilePathSheetViewController: UIViewController { private func configureNavigation() { navigationItem.largeTitleDisplayMode = .never #if os(visionOS) - navigationItem.titleView = UIKitChassisLabel("View file", size: 12) + navigationItem.titleView = UIKitChassisLabel( + String(localized: "View file"), size: 12 + ) #endif let cancel = UIBarButtonItem( - title: "Cancel", + title: String(localized: "Cancel"), style: .plain, target: self, action: #selector(cancelPressed) @@ -171,12 +173,12 @@ final class TerminalFilePathSheetViewController: UIViewController { viewChip = UIKitChassisChip( "▤ VIEW", prominent: true, - accessibilityLabel: "View" + accessibilityLabel: String(localized: "View") ) { [weak self] in self?.viewPressed() } copyChip = UIKitChassisChip( "COPY", systemImage: "doc.on.doc", - accessibilityLabel: "Copy" + accessibilityLabel: String(localized: "Copy") ) { [weak self] in self?.copyPressed() } for chip in [viewChip, copyChip] { chip?.setContentHuggingPriority(.required, for: .horizontal) @@ -249,7 +251,9 @@ final class TerminalFilePathSheetViewController: UIViewController { let target = editedTarget hostNameLabel.text = hostName hostNameLabel.accessibilityLabel = hostName - editor.setNote(target == nil ? "NOT A PATH MULTIPLEX CAN READ" : nil) + editor.setNote( + target == nil ? String(localized: "NOT A PATH MULTIPLEX CAN READ") : nil + ) // Resolution can shed what it reads as prose (a spaced press // arrives with its sentence tail), so when the opened path is not // the field's text, say so — verbatim, mono: a path is screen @@ -285,25 +289,28 @@ final class TerminalFilePathSheetViewController: UIViewController { private func sectionTitle(for target: TerminalPathTarget?) -> String { switch target?.base { - case .absolute: "A path on \(hostName)" - case .home: "In \(hostName)'s home" - case .workingDirectory: "Relative to the pane's directory" - case nil: "Not a usable path" + case .absolute: String(localized: "A path on \(hostName)") + case .home: String(localized: "In \(hostName)'s home") + case .workingDirectory: String(localized: "Relative to the pane's directory") + case nil: String(localized: "Not a usable path") } } private func detail(for target: TerminalPathTarget?) -> String { guard let target else { - return "The field holds nothing the viewer can resolve — a " - + "relative path needs a directory in it, and spaces only " - + "work behind /, ~/, ./ or $HOME/. Edit it, or copy the " - + "text if it's still useful." + return String(localized: """ + The field holds nothing the viewer can resolve — a relative path needs a \ + directory in it, and spaces only work behind /, ~/, ./ or $HOME/. Edit it, \ + or copy the text if it's still useful. + """) } - var text = "VIEW opens it read-only in the file viewer, beside this " - + "session — nothing runs, nothing is written. The path is " - + "editable when detection caught the wrong text." + var text = String(localized: """ + VIEW opens it read-only in the file viewer, beside this session — nothing \ + runs, nothing is written. The path is editable when detection caught the \ + wrong text. + """) if target.base == .workingDirectory { - text += " It resolves against the pane's current directory." + text += " " + String(localized: "It resolves against the pane's current directory.") } return text } diff --git a/Multiplex/Views/Terminal/TerminalGuideViewController.swift b/Multiplex/Views/Terminal/TerminalGuideViewController.swift index 4af05d3d..b4e3a08b 100644 --- a/Multiplex/Views/Terminal/TerminalGuideViewController.swift +++ b/Multiplex/Views/Terminal/TerminalGuideViewController.swift @@ -34,21 +34,21 @@ final class TerminalGuideViewController: UIViewController, AppAppearanceFollowin override func viewDidLoad() { super.viewDidLoad() - title = "Guide" + title = String(localized: "Guide") view.backgroundColor = GlassPrototype.sheetGround navigationItem.largeTitleDisplayMode = .never #if os(visionOS) - navigationItem.titleView = UIKitChassisLabel("Guide", size: 12) + navigationItem.titleView = UIKitChassisLabel(String(localized: "Guide"), size: 12) #endif let done = UIBarButtonItem( - title: "Done", + title: String(localized: "Done"), style: .plain, target: self, action: #selector(donePressed) ) done.tintColor = UIKitChassis.signal - done.accessibilityLabel = "Done" + done.accessibilityLabel = String(localized: "Done") navigationItem.rightBarButtonItem = done configureContent() @@ -519,7 +519,7 @@ private final class TerminalGuideSettingsButton: UIButton { layer.cornerRadius = 2 clipsToBounds = true hoverStyle = UIHoverStyle(effect: .highlight, shape: .rect(cornerRadius: 2)) - accessibilityLabel = "Open Settings" + accessibilityLabel = String(localized: "Open Settings") captionLabel.isUserInteractionEnabled = false captionLabel.isAccessibilityElement = false diff --git a/Multiplex/Views/Terminal/TerminalKeyBar.swift b/Multiplex/Views/Terminal/TerminalKeyBar.swift index cd0f8c81..e9b42ad5 100644 --- a/Multiplex/Views/Terminal/TerminalKeyBar.swift +++ b/Multiplex/Views/Terminal/TerminalKeyBar.swift @@ -322,7 +322,7 @@ final class TerminalCtrlComboView: UIKitTallyBorderedView { ), width: 46, height: faceHeight, - accessibilityLabel: "Control \(letter.uppercased())", + accessibilityLabel: String(localized: "Control \(letter.uppercased())"), accessibilityIdentifier: "terminal.ctrlCombos.\(letter)", action: { send(letter) } ) @@ -374,7 +374,9 @@ final class TerminalCtrlComboViewController: UIViewController { /// The talk key's VoiceOver name on the rail and in the visionOS cluster — /// one wording, flipped in place with the latch. private func talkbackKeyLabel(open: Bool) -> String { - open ? "Close the message box" : "Open the message box" + open + ? String(localized: "Close the message box") + : String(localized: "Open the message box") } #if !os(visionOS) @@ -787,12 +789,15 @@ final class TerminalKeyBar: UIView, UIInputViewAudioFeedback { // Tap CTRL latches (and raises the C / B slab); a hold on the same // key opens Key Commands and never toggles the latch. - var control = caps("CTRL", .ctrl, "Control", identifier: "control", latched: ctrlLatched) + var control = caps( + "CTRL", .ctrl, String(localized: "Control"), + identifier: "control", latched: ctrlLatched + ) control.longPressKey = .keyCommands var groups: [[RailKey]] = [[ - caps("ESC", .esc, "Escape", identifier: "escape"), + caps("ESC", .esc, String(localized: "Escape"), identifier: "escape"), control, - caps("TAB", .tab, "Tab", identifier: "tab"), + caps("TAB", .tab, String(localized: "Tab"), identifier: "tab"), ]] if !specification.symbols.isEmpty { groups.append(specification.symbols.map { symbol in @@ -808,23 +813,24 @@ final class TerminalKeyBar: UIView, UIInputViewAudioFeedback { var right: [RailKey] = [] if specification.pageKeys { right.append(arrowKey( - "arrow.up.to.line", .pageUp, "Page up", identifier: "pageUp" + "arrow.up.to.line", .pageUp, String(localized: "Page up"), identifier: "pageUp" )) right.append(arrowKey( - "arrow.down.to.line", .pageDown, "Page down", identifier: "pageDown" + "arrow.down.to.line", .pageDown, String(localized: "Page down"), + identifier: "pageDown" )) } right.append(contentsOf: [ - arrowKey("arrow.left", .left, "Arrow left", identifier: "left"), - arrowKey("arrow.up", .up, "Arrow up", identifier: "up"), - arrowKey("arrow.down", .down, "Arrow down", identifier: "down"), - arrowKey("arrow.right", .right, "Arrow right", identifier: "right"), + arrowKey("arrow.left", .left, String(localized: "Arrow left"), identifier: "left"), + arrowKey("arrow.up", .up, String(localized: "Arrow up"), identifier: "up"), + arrowKey("arrow.down", .down, String(localized: "Arrow down"), identifier: "down"), + arrowKey("arrow.right", .right, String(localized: "Arrow right"), identifier: "right"), ]) if includesReturn { right.append(RailKey( key: .returnKey, face: .symbol("return", pointSize: 12, weight: .semibold), - accessibility: "Return", + accessibility: String(localized: "Return"), identifier: "return" )) } @@ -845,7 +851,9 @@ final class TerminalKeyBar: UIView, UIInputViewAudioFeedback { pointSize: 13, weight: .semibold ), - accessibility: state.isDictating ? "Stop dictation" : "Dictate", + accessibility: state.isDictating + ? String(localized: "Stop dictation") + : String(localized: "Dictate"), identifier: "dictation", latched: state.isDictating )) @@ -858,8 +866,10 @@ final class TerminalKeyBar: UIView, UIInputViewAudioFeedback { weight: .semibold ), accessibility: state.keyboardLocked - ? "Unlock keyboard" - : "Show or hide keyboard. Hold to lock the keyboard closed", + ? String(localized: "Unlock keyboard") + : String(localized: """ + Show or hide keyboard. Hold to lock the keyboard closed + """), identifier: "keyboard", latched: state.keyboardLocked, longPressKey: state.keyboardLocked ? nil : .lockKeyboard @@ -876,8 +886,8 @@ final class TerminalKeyBar: UIView, UIInputViewAudioFeedback { kerning: 0.7 ), accessibility: backend == .herdr - ? "Show herdr shortcuts" - : "Show tmux shortcuts", + ? String(localized: "Show herdr shortcuts") + : String(localized: "Show tmux shortcuts"), identifier: "tmux" )) } @@ -1821,12 +1831,14 @@ final class TerminalKeyClusterGroupView: UIKitTallyBorderedView { } if role != .trailing { - append(caps("ESC", "Escape", activeMetric, identifier: "escape") { [weak context] in + append(caps( + "ESC", String(localized: "Escape"), activeMetric, identifier: "escape" + ) { [weak context] in context?.sendEscape() }) let control = caps( "CTRL", - "Control", + String(localized: "Control"), activeMetric, identifier: "control", latched: context.ctrlLatched @@ -1841,29 +1853,35 @@ final class TerminalKeyClusterGroupView: UIKitTallyBorderedView { } append(control) ctrlKey = control - append(caps("TAB", "Tab", activeMetric, identifier: "tab") { [weak context] in + append(caps( + "TAB", String(localized: "Tab"), activeMetric, identifier: "tab" + ) { [weak context] in context?.sendTab() }) } if role != .leading { if !minimal { append(arrow( - "arrow.left", "Arrow left", activeMetric, identifier: "left", + "arrow.left", String(localized: "Arrow left"), activeMetric, + identifier: "left", app: EscapeSequences.moveLeftApp, normal: EscapeSequences.moveLeftNormal )) append(arrow( - "arrow.up", "Arrow up", activeMetric, identifier: "up", + "arrow.up", String(localized: "Arrow up"), activeMetric, + identifier: "up", app: EscapeSequences.moveUpApp, normal: EscapeSequences.moveUpNormal )) append(arrow( - "arrow.down", "Arrow down", activeMetric, identifier: "down", + "arrow.down", String(localized: "Arrow down"), activeMetric, + identifier: "down", app: EscapeSequences.moveDownApp, normal: EscapeSequences.moveDownNormal )) append(arrow( - "arrow.right", "Arrow right", activeMetric, identifier: "right", + "arrow.right", String(localized: "Arrow right"), activeMetric, + identifier: "right", app: EscapeSequences.moveRightApp, normal: EscapeSequences.moveRightNormal )) @@ -1872,7 +1890,7 @@ final class TerminalKeyClusterGroupView: UIKitTallyBorderedView { face: .symbol("return", pointSize: 12, weight: .semibold), width: activeMetric.keyWidth, height: 26, - accessibilityLabel: "Return", + accessibilityLabel: String(localized: "Return"), accessibilityIdentifier: "terminal.keyCluster.return", action: { [weak context] in context?.sendReturn() } )) @@ -1893,7 +1911,7 @@ final class TerminalKeyClusterGroupView: UIKitTallyBorderedView { face: .symbol("keyboard", pointSize: 12, weight: .semibold), width: activeMetric.keyWidth, height: 26, - accessibilityLabel: "Show or hide keyboard", + accessibilityLabel: String(localized: "Show or hide keyboard"), accessibilityIdentifier: "terminal.keyCluster.keyboard", action: { [weak context] in context?.toggleKeyboard() } ) diff --git a/Multiplex/Views/Terminal/TerminalLinkSheet.swift b/Multiplex/Views/Terminal/TerminalLinkSheet.swift index aa8a8ff5..fb30e6c9 100644 --- a/Multiplex/Views/Terminal/TerminalLinkSheet.swift +++ b/Multiplex/Views/Terminal/TerminalLinkSheet.swift @@ -162,7 +162,9 @@ final class TerminalLinkSheetViewController: UIViewController { private let reachLabel = UILabel() private let actionStack = UIStackView() private let actionSpacer = UIView() - private let navigationTitleLabel = UIKitChassisLabel("Open link", size: 12) + private let navigationTitleLabel = UIKitChassisLabel( + String(localized: "Open link"), size: 12 + ) private let editor: UIKitTerminalEditableValueBox private var sectionView: UIKitTallyFormSectionView! private var viewportChip: UIKitChassisChip! @@ -238,7 +240,7 @@ final class TerminalLinkSheetViewController: UIViewController { navigationItem.titleView = navigationTitleLabel #endif let cancel = UIBarButtonItem( - title: "Cancel", + title: String(localized: "Cancel"), style: .plain, target: self, action: #selector(cancelPressed) @@ -352,18 +354,18 @@ final class TerminalLinkSheetViewController: UIViewController { viewportChip = UIKitChassisChip( "⌗ VIEWPORT", prominent: true, - accessibilityLabel: "Viewport" + accessibilityLabel: String(localized: "Viewport") ) { [weak self] in self?.viewportPressed() } openChip = UIKitChassisChip( "OPEN", systemImage: "arrow.up.forward.app", prominent: true, - accessibilityLabel: "Open" + accessibilityLabel: String(localized: "Open") ) { [weak self] in self?.openPressed() } copyChip = UIKitChassisChip( "COPY", systemImage: "doc.on.doc", - accessibilityLabel: "Copy" + accessibilityLabel: String(localized: "Copy") ) { [weak self] in self?.copyPressed() } for chip in [viewportChip, openChip, copyChip] { @@ -389,15 +391,19 @@ final class TerminalLinkSheetViewController: UIViewController { let link = editedLink let viewport = editedViewport - title = link?.openableURL == nil ? "Can't open link" : "Open link" + title = link?.openableURL == nil + ? String(localized: "Can't open link") + : String(localized: "Open link") #if os(visionOS) - navigationTitleLabel.setText(title ?? "Open link") + navigationTitleLabel.setText(title ?? String(localized: "Open link")) #endif hostNameLabel.text = link?.host hostNameLabel.accessibilityLabel = link?.host hostStack.isHidden = link?.host == nil - editor.setNote(link == nil ? "NOT AN ADDRESS MULTIPLEX CAN READ" : nil) + editor.setNote( + link == nil ? String(localized: "NOT AN ADDRESS MULTIPLEX CAN READ") : nil + ) reachLabel.text = viewport.map(reachDescription) reachLabel.accessibilityLabel = viewport.map(reachDescription) @@ -421,17 +427,21 @@ final class TerminalLinkSheetViewController: UIViewController { ) -> String { if let viewport, onOpenViewport != nil { return switch viewport.reach { - case .internet: "A public address" - case .lan: "On this device's network" + case .internet: String(localized: "A public address") + case .lan: String(localized: "On this device's network") case .remoteLoopback: - "Lives on \(viewport.viaHostName ?? "the host"), not this device" + String(localized: """ + Lives on \(viewport.viaHostName ?? String(localized: "the host")), \ + not this device + """) } } - guard let link else { return "Not a usable address" } + guard let link else { return String(localized: "Not a usable address") } return switch link.kind { - case .openable: "Leaves Multiplex" - case .blockedScheme(let scheme): "\(scheme.uppercased()) links stay here" - case .malformed: "Not a usable address" + case .openable: String(localized: "Leaves Multiplex") + case .blockedScheme(let scheme): + String(localized: "\(scheme.uppercased()) links stay here") + case .malformed: String(localized: "Not a usable address") } } @@ -442,34 +452,44 @@ final class TerminalLinkSheetViewController: UIViewController { if let viewport, onOpenViewport != nil { return switch viewport.reach { case .internet, .lan: - "The viewport renders this address inside Multiplex; OPEN " - + "still hands it to the system browser. It came from the " - + "host — check the address above before opening." + String(localized: """ + The viewport renders this address inside Multiplex; OPEN still hands it \ + to the system browser. It came from the host — check the address above \ + before opening. + """) case .remoteLoopback: - "localhost in a remote pane is the host's own loopback — an " - + "address this device can't dial. VIA aims the viewport " - + "at the address that already reaches the host; the " - + "server must listen beyond loopback (vite --host) to answer." + String(localized: """ + localhost in a remote pane is the host's own loopback — an address this \ + device can't dial. VIA aims the viewport at the address that already \ + reaches the host; the server must listen beyond loopback (vite --host) \ + to answer. + """) } } guard let link else { - return "The field holds nothing Multiplex can open — a target " - + "needs a web or mail address. Edit it, or copy the text " - + "if it's still useful." + return String(localized: """ + The field holds nothing Multiplex can open — a target needs a web or mail \ + address. Edit it, or copy the text if it's still useful. + """) } return switch link.kind { case .openable: - "This address came from the host, and a hyperlink's visible text " - + "can differ from where it points — check the host above " - + "before opening it outside Multiplex. The target is " - + "editable when detection caught the wrong text." + String(localized: """ + This address came from the host, and a hyperlink's visible text can differ \ + from where it points — check the host above before opening it outside \ + Multiplex. The target is editable when detection caught the wrong text. + """) case .blockedScheme(let scheme): - "Multiplex opens web and mail links only. A \(scheme): link from a " - + "remote pane would act on this device, so it is shown rather " - + "than followed — copy it if you want it elsewhere." + String(localized: """ + Multiplex opens web and mail links only. A \(scheme): link from a remote \ + pane would act on this device, so it is shown rather than followed — copy \ + it if you want it elsewhere. + """) case .malformed: - "The text looked like a link but has no address Multiplex can " - + "open. Copy it if it's still useful." + String(localized: """ + The text looked like a link but has no address Multiplex can open. Copy it \ + if it's still useful. + """) } } @@ -484,13 +504,13 @@ final class TerminalLinkSheetViewController: UIViewController { private func reachDescription(_ viewport: ViewportOffer) -> String { switch viewport.reach { case .internet: - return "INTERNET — OPENS FROM THIS DEVICE" + return String(localized: "INTERNET — OPENS FROM THIS DEVICE") case .lan: - return "LAN — DIALLED FROM THIS DEVICE, NETWORK PERMITTING" + return String(localized: "LAN — DIALLED FROM THIS DEVICE, NETWORK PERMITTING") case .remoteLoopback: var rewritten = viewport.url.host() ?? "" if let port = viewport.url.port { rewritten += ":\(port)" } - return "REMOTE LOOPBACK → REWRITES TO \(rewritten)" + return String(localized: "REMOTE LOOPBACK → REWRITES TO \(rewritten)") } } diff --git a/Multiplex/Views/Terminal/TerminalPaneUIKit.swift b/Multiplex/Views/Terminal/TerminalPaneUIKit.swift index 6d870de6..b85cb297 100644 --- a/Multiplex/Views/Terminal/TerminalPaneUIKit.swift +++ b/Multiplex/Views/Terminal/TerminalPaneUIKit.swift @@ -155,10 +155,10 @@ final class TerminalPaneViewController: UIViewController, UIDropInteractionDeleg sessionName: String?, isResuming: Bool ) -> String { - guard isResuming else { return "Connecting to \(hostName)…" } + guard isResuming else { return String(localized: "Connecting to \(hostName)…") } return sessionName == nil - ? "Reconnecting to \(hostName)…" - : "Reattaching to \(hostName)…" + ? String(localized: "Reconnecting to \(hostName)…") + : String(localized: "Reattaching to \(hostName)…") } // MARK: Drop interaction @@ -627,13 +627,15 @@ final class TerminalPaneViewController: UIViewController, UIDropInteractionDeleg private func makeMissingHostPanel() -> UIView { let title = UILabel() - title.text = "This host was removed" + title.text = String(localized: "This host was removed") title.font = UIKitChassis.monoFont(17, weight: .semibold) title.textColor = UIKitChassis.signal title.textAlignment = .center let detail = UILabel() - detail.text = "The tab can't reconnect because its host no longer exists in the deck." + detail.text = String(localized: """ + The tab can't reconnect because its host no longer exists in the deck. + """) // Body copy, not chrome: a semantic style keeps Dynamic Type (fixed // `uiFont` sizes ignore it, and the two mechanisms never compound). detail.font = .preferredFont(forTextStyle: .subheadline) @@ -646,7 +648,7 @@ final class TerminalPaneViewController: UIViewController, UIDropInteractionDeleg let close = UIKitChassisChip( "CLOSE TAB", prominent: true, - accessibilityLabel: "Close tab", + accessibilityLabel: String(localized: "Close tab"), action: configuration.close ) let panel = TerminalPanePanelView(arrangedSubviews: [title, detail, close]) @@ -725,12 +727,12 @@ final class TerminalPanePanelView: UIKitTallyBorderedView { let reconnectChip = UIKitChassisChip( "RECONNECT", prominent: true, - accessibilityLabel: "Reconnect", + accessibilityLabel: String(localized: "Reconnect"), action: reconnect ) let closeChip = UIKitChassisChip( "CLOSE TAB", - accessibilityLabel: "Close tab", + accessibilityLabel: String(localized: "Close tab"), action: close ) let actions = UIStackView(arrangedSubviews: [reconnectChip, closeChip]) @@ -739,7 +741,8 @@ final class TerminalPanePanelView: UIKitTallyBorderedView { actions.spacing = 12 views.append(actions) let panel = TerminalPanePanelView(arrangedSubviews: views) - panel.accessibilityLabel = reason.map { "Ended, \($0)" } ?? "Detached" + panel.accessibilityLabel = reason.map { String(localized: "Ended, \($0)") } + ?? String(localized: "Detached") return panel } } @@ -781,7 +784,7 @@ final class TerminalContextBarView: UIKitTallyBorderedView { UIKitChassisChip( "DONE", prominent: true, - accessibilityLabel: "Done", + accessibilityLabel: String(localized: "Done"), action: done ), ]) @@ -812,7 +815,7 @@ final class TerminalSelectionActionsOverlay { copyChip = UIKitChassisChip( "COPY", prominent: true, - accessibilityLabel: "Copy selection", + accessibilityLabel: String(localized: "Copy selection"), action: { [weak terminal] in terminal?.copy(nil) done() @@ -823,12 +826,12 @@ final class TerminalSelectionActionsOverlay { copyChip, UIKitChassisChip( "SELECT ALL", - accessibilityLabel: "Select all in pane", + accessibilityLabel: String(localized: "Select all in pane"), action: { [weak terminal] in terminal?.selectAll(nil) } ), UIKitChassisChip( "DONE", - accessibilityLabel: "Done selecting", + accessibilityLabel: String(localized: "Done selecting"), action: done ), ]) @@ -936,24 +939,24 @@ final class TerminalSelectionMenuOverlay { UIKitChassisChip( "SELECT", prominent: true, - accessibilityLabel: "Select text", + accessibilityLabel: String(localized: "Select text"), action: { [weak self] in self?.performSelect() } ), UIKitChassisChip( "SELECT ALL", - accessibilityLabel: "Select all in pane", + accessibilityLabel: String(localized: "Select all in pane"), action: { [weak self] in self?.performSelectAll() } ), UIKitChassisChip( "PASTE", - accessibilityLabel: "Paste", + accessibilityLabel: String(localized: "Paste"), action: { [weak self] in self?.performPaste() } ), ] if remoteMenu != nil { items.append(UIKitChassisChip( "MENU", - accessibilityLabel: "Open the remote pane menu", + accessibilityLabel: String(localized: "Open the remote pane menu"), action: { [weak self] in self?.performRemoteMenu() } )) } @@ -1043,19 +1046,19 @@ extension TerminalContextBarView { pendingLabel.textColor = UIKitChassis.signal3 pendingLabel.lineBreakMode = .byTruncatingHead pendingLabel.numberOfLines = 1 - pendingLabel.accessibilityLabel = "Heard, not typed yet: \(pending)" + pendingLabel.accessibilityLabel = String(localized: "Heard, not typed yet: \(pending)") pendingLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 320).isActive = true items.append(pendingLabel) } items.append(UIKitChassisChip( "CANCEL", - accessibilityLabel: "Cancel dictation", + accessibilityLabel: String(localized: "Cancel dictation"), action: cancel )) items.append(UIKitChassisChip( "STOP", prominent: true, - accessibilityLabel: "Stop dictation", + accessibilityLabel: String(localized: "Stop dictation"), action: stop )) return TerminalContextBarView(items: items) @@ -1084,11 +1087,12 @@ extension TerminalContextBarView { ) -> UIView { let button = UIButton(type: .custom) button.accessibilityIdentifier = "terminalPane.dictation.language" - button.accessibilityLabel = - "Dictation language: \(language.name) \(language.region). Change" + button.accessibilityLabel = String( + localized: "Dictation language: \(language.name) \(language.region). Change" + ) button.showsMenuAsPrimaryAction = true button.menu = UIMenu( - title: "Dictation language", + title: String(localized: "Dictation language"), options: .singleSelection, children: choices.map { choice in UIAction( @@ -1158,7 +1162,7 @@ extension TerminalContextBarView { preview = value action = UIKitChassisChip( "CANCEL", - accessibilityLabel: "Cancel finding message", + accessibilityLabel: String(localized: "Cancel finding message"), action: cancel ) case .jumped(let value, _): @@ -1167,7 +1171,7 @@ extension TerminalContextBarView { action = UIKitChassisChip( "BACK TO LIVE", prominent: true, - accessibilityLabel: "Back to live", + accessibilityLabel: String(localized: "Back to live"), action: backToLive ) } @@ -1252,8 +1256,12 @@ final class TerminalKeyboardLockedView: UIKitTallyBorderedView { ) mic.tintColor = isDictating ? UIKitChassis.chassis : UIKitChassis.signal2 mic.backgroundColor = isDictating ? UIKitChassis.signal2 : .clear - mic.accessibilityLabel = isDictating ? "Stop dictation" : "Dictate" - mic.accessibilityHint = "Types what you say into the session as you speak, never pressing Return" + mic.accessibilityLabel = isDictating + ? String(localized: "Stop dictation") + : String(localized: "Dictate") + mic.accessibilityHint = String(localized: """ + Types what you say into the session as you speak, never pressing Return + """) mic.addAction(UIAction { _ in toggleDictation() }, for: .touchUpInside) mic.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -1286,7 +1294,7 @@ final class TerminalHistoryFindingVeilView: UIView { super.init(frame: frame) backgroundColor = UIKitChassis.screen.withAlphaComponent(0.72) isAccessibilityElement = true - accessibilityLabel = "Searching transcript" + accessibilityLabel = String(localized: "Searching transcript") let progress = UIActivityIndicatorView(style: .medium) progress.color = UIKitChassis.signal2 diff --git a/Multiplex/Views/Terminal/TerminalTabStrip.swift b/Multiplex/Views/Terminal/TerminalTabStrip.swift index e2cf9271..d0346353 100644 --- a/Multiplex/Views/Terminal/TerminalTabStrip.swift +++ b/Multiplex/Views/Terminal/TerminalTabStrip.swift @@ -155,7 +155,7 @@ final class TerminalTabStripView: UIView, UIDropInteractionDelegate { self.split = split self.close = close self.reorder = reorder - accessibilityLabel = "\(items.count) tabs" + accessibilityLabel = String(localized: "\(items.count) tabs") // The window re-renders this strip on every observed change of its // own (a ~5 s host probe is enough), and an Observation registration @@ -202,14 +202,14 @@ final class TerminalTabStripView: UIView, UIDropInteractionDelegate { var actions: [UIMenuElement] = [] if canSplit(id: id) { actions.append(UIAction( - title: "Move to New Window", + title: String(localized: "Move to New Window"), image: UIImage(systemName: "macwindow.badge.plus") ) { [weak self] _ in self?.splitTab(id: id) }) } actions.append(UIAction( - title: "Close Tab", + title: String(localized: "Close Tab"), image: UIImage(systemName: "xmark"), attributes: .destructive ) { [weak self] _ in @@ -685,7 +685,7 @@ final class TerminalTabCell: UIView, isAccessibilityElement = true accessibilityTraits = Self.traits(isActive: item.isActive) accessibilityLabel = Self.accessibilityLabel(for: item) - accessibilityHint = canReorder ? "Drag to reorder within this window" : nil + accessibilityHint = canReorder ? String(localized: "Drag to reorder within this window") : nil accessibilityCustomActions = makeAccessibilityActions() let drag = UIDragInteraction(delegate: self) @@ -760,7 +760,7 @@ final class TerminalTabCell: UIView, if self.canReorder != canReorder { self.canReorder = canReorder tabDragInteraction?.isEnabled = canReorder - accessibilityHint = canReorder ? "Drag to reorder within this window" : nil + accessibilityHint = canReorder ? String(localized: "Drag to reorder within this window") : nil } let nextLabelText = Self.label(for: item) if labelText != nextLabelText { @@ -791,7 +791,9 @@ final class TerminalTabCell: UIView, } private static func accessibilityLabel(for item: TerminalTabStrip.Item) -> String { - "\(item.title) tab\(item.isActive ? ", active" : "")" + item.isActive + ? String(localized: "\(item.title) tab, active") + : String(localized: "\(item.title) tab") } private static func ink(isActive: Bool) -> UIColor { @@ -921,7 +923,7 @@ final class TerminalTabCell: UIView, var actions: [UIAccessibilityCustomAction] = [] if canSplit { actions.append(UIAccessibilityCustomAction( - name: "Move to New Window", + name: String(localized: "Move to New Window"), actionHandler: { [weak self] _ in self?.split() return self != nil @@ -929,7 +931,7 @@ final class TerminalTabCell: UIView, )) } actions.append(UIAccessibilityCustomAction( - name: "Close Tab", + name: String(localized: "Close Tab"), actionHandler: { [weak self] _ in self?.close() return self != nil diff --git a/Multiplex/Views/Terminal/TerminalWindowUIKit.swift b/Multiplex/Views/Terminal/TerminalWindowUIKit.swift index 67481e89..d28e635c 100644 --- a/Multiplex/Views/Terminal/TerminalWindowUIKit.swift +++ b/Multiplex/Views/Terminal/TerminalWindowUIKit.swift @@ -975,19 +975,26 @@ final class TerminalWindowViewController: UIViewController, let host = store.host(id: activeTab.hostID) else { return } let message = activeTab.sessionBackend == .herdr - ? "Stops “\(sessionName)” on \(host.name), deletes its saved state when " - + "herdr allows it, then closes the tab." - : "Kills “\(sessionName)” on \(host.name) and everything running in it, " - + "then closes the tab." + ? String(localized: """ + Stops “\(sessionName)” on \(host.name), deletes its saved state when \ + herdr allows it, then closes the tab. + """) + : String(localized: """ + Kills “\(sessionName)” on \(host.name) and everything running in it, \ + then closes the tab. + """) let alert = UIAlertController( - title: "Close Session", + title: String(localized: "Close Session"), message: message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "Close Session", style: .destructive) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Close Session"), + style: .destructive + ) { [weak self] _ in self?.closeSession(activeTab) }) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: String(localized: "Cancel"), style: .cancel)) present(alert, animated: true) } @@ -1095,10 +1102,11 @@ final class TerminalWindowViewController: UIViewController, guard presentedViewController == nil else { return } let alert = UIAlertController( title: target.failureTitle, - message: reason ?? "Check the connection to \(hostName) and try again.", + message: reason + ?? String(localized: "Check the connection to \(hostName) and try again."), preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "OK", style: .cancel)) + alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .cancel)) present(alert, animated: true) } @@ -1840,7 +1848,7 @@ extension TerminalWindowViewController { } private var windowTitle: String { - guard let activeTab else { return "Terminal" } + guard let activeTab else { return String(localized: "Terminal") } let hostName = store.host(id: activeTab.hostID)?.name let title = tabTitle(for: activeTab) return hostName.map { "\(title) — \($0)" } ?? title @@ -1854,7 +1862,9 @@ extension TerminalWindowViewController { } private var auxiliaryCloseLabel: String { - activeTab?.isFileViewer == true ? "Close file viewer" : "Close viewport" + activeTab?.isFileViewer == true + ? String(localized: "Close file viewer") + : String(localized: "Close viewport") } /// The herdr-only second `+ TAB` entry the active tab offers — the one @@ -2761,7 +2771,7 @@ extension TerminalWindowViewController { presentedActivationID = featureID let sheet = TerminalFilePathSheetViewController( target: target, - hostName: activeTabHost?.name ?? "the host", + hostName: activeTabHost?.name ?? String(localized: "the host"), onView: { [weak self] in self?.openFileViewer(target: $0) }, onCopy: { [weak controller] in controller?.copyConfirmedTarget($0) } ) diff --git a/Multiplex/Views/Terminal/UMDBarUIKit.swift b/Multiplex/Views/Terminal/UMDBarUIKit.swift index 52c5d1de..11751857 100644 --- a/Multiplex/Views/Terminal/UMDBarUIKit.swift +++ b/Multiplex/Views/Terminal/UMDBarUIKit.swift @@ -579,7 +579,7 @@ final class UMDBarViewController: UIViewController, menuButton( caption: "MERGE", identifier: "umd.merge", - accessibilityLabel: "Merge another window into this one", + accessibilityLabel: String(localized: "Merge another window into this one"), menu: makeMergeMenu(titled: false) ) } @@ -590,7 +590,7 @@ final class UMDBarViewController: UIViewController, actionButton( caption: "GUIDE", identifier: "umd.guide", - accessibilityLabel: "Guide", + accessibilityLabel: String(localized: "Guide"), action: .showGuide ) } @@ -601,7 +601,7 @@ final class UMDBarViewController: UIViewController, caption: "DETACH", prominent: true, identifier: "umd.detach", - accessibilityLabel: "Detach", + accessibilityLabel: String(localized: "Detach"), action: .detach ) } @@ -609,7 +609,7 @@ final class UMDBarViewController: UIViewController, caption: "DETACH", prominent: true, identifier: "umd.detach", - accessibilityLabel: "Detach or close the session", + accessibilityLabel: String(localized: "Detach or close the session"), menu: makeDetachMenu() ) } @@ -625,7 +625,7 @@ final class UMDBarViewController: UIViewController, caption: "", systemImage: "ellipsis", identifier: "umd.overflow", - accessibilityLabel: "Terminal actions", + accessibilityLabel: String(localized: "Terminal actions"), menu: menu ) } @@ -638,8 +638,8 @@ final class UMDBarViewController: UIViewController, systemImage: "command", prominent: false, accessibilityLabel: backend == .herdr - ? "Show herdr shortcuts" - : "Show tmux shortcuts" + ? String(localized: "Show herdr shortcuts") + : String(localized: "Show tmux shortcuts") ) button.accessibilityIdentifier = "umd.tmux" button.isEnabled = currentObservedState.status == .live @@ -656,7 +656,7 @@ final class UMDBarViewController: UIViewController, if configuration.controller?.host.useMosh == true { let mosh = UMDStateBadgeView( caption: "MOSH", - accessibilityLabel: "Connects over mosh" + accessibilityLabel: String(localized: "Connects over mosh") ) mosh.accessibilityIdentifier = "umd.status.mosh" views.append(mosh) @@ -689,8 +689,10 @@ final class UMDBarViewController: UIViewController, ) let button = UMDLampButton( lamp: lamp, - accessibilityLabel: "The Mac's keychain is locked, so Claude Code shows signed out", - accessibilityHint: "Shows how to unlock the keychain", + accessibilityLabel: String(localized: """ + The Mac's keychain is locked, so Claude Code shows signed out + """), + accessibilityHint: String(localized: "Shows how to unlock the keychain"), action: { [weak self] in self?.configuration.keychainTip?() } ) button.accessibilityIdentifier = "umd.status.keychain" @@ -749,7 +751,7 @@ final class UMDBarViewController: UIViewController, options: .displayInline, children: [ menuAction( - title: "File Viewer", + title: String(localized: "File Viewer"), identifier: "umd.newTab.fileViewer", action: .openFileViewer ), @@ -774,7 +776,7 @@ final class UMDBarViewController: UIViewController, options: .displayInline, children: [ menuAction( - title: "Merge All Windows", + title: String(localized: "Merge All Windows"), image: UIImage(systemName: "rectangle.stack"), identifier: "umd.mergeAll", action: .mergeAll @@ -782,18 +784,21 @@ final class UMDBarViewController: UIViewController, ] )) } - return UIMenu(title: titled ? "Merge Window" : "", children: children) + return UIMenu( + title: titled ? String(localized: "Merge Window") : "", + children: children + ) } private func makeDetachMenu() -> UIMenu { UIMenu(children: [ menuAction( - title: "Detach", + title: String(localized: "Detach"), identifier: "umd.detach.action", action: .detach ), menuAction( - title: "Close Session", + title: String(localized: "Close Session"), identifier: "umd.closeSession", attributes: .destructive, action: .closeSession @@ -809,7 +814,8 @@ final class UMDBarViewController: UIViewController, if configuration.controller != nil, offersKeyboardLock { children.append(menuAction( title: currentObservedState.keyboardLocked - ? "Unlock Keyboard" : "Lock Keyboard Closed", + ? String(localized: "Unlock Keyboard") + : String(localized: "Lock Keyboard Closed"), image: UIImage( systemName: currentObservedState.keyboardLocked ? "lock.open" : "lock" @@ -821,7 +827,7 @@ final class UMDBarViewController: UIViewController, #endif if configuration.showConnectionStats != nil { children.append(menuAction( - title: "Connection Stats…", + title: String(localized: "Connection Stats…"), image: UIImage(systemName: "waveform.path.ecg"), identifier: "umd.connectionStats", action: .showConnectionStats @@ -831,28 +837,31 @@ final class UMDBarViewController: UIViewController, // The wide rows carry GUIDE as its own chip; it rides the menu // only where the direct actions have been displaced. children.append(menuAction( - title: "Guide", + title: String(localized: "Guide"), image: UIImage(systemName: "questionmark.circle"), identifier: "umd.guide.action", action: .showGuide )) children.append(UIMenu( - title: "Text Size", + title: String(localized: "Text Size"), options: .displayInline, children: [ menuAction( - title: "Smaller Text", + title: String(localized: "Smaller Text"), identifier: "umd.fontDown.action", action: .fontDown ), menuAction( - title: "Larger Text", + title: String(localized: "Larger Text"), identifier: "umd.fontUp.action", action: .fontUp ), ] )) - children.append(UIMenu(title: "New Tab", children: makeNewTabMenu().children)) + children.append(UIMenu( + title: String(localized: "New Tab"), + children: makeNewTabMenu().children + )) if FileAttachAvailability.canOffer(for: configuration.controller) { children.append(makeFileAttachMenu()) } @@ -861,14 +870,14 @@ final class UMDBarViewController: UIViewController, } var closingActions: [UIMenuElement] = [ menuAction( - title: "Detach", + title: String(localized: "Detach"), identifier: "umd.detach.action", action: .detach ), ] if configuration.closeSession != nil { closingActions.append(menuAction( - title: "Close Session", + title: String(localized: "Close Session"), identifier: "umd.closeSession", attributes: .destructive, action: .closeSession @@ -888,7 +897,7 @@ final class UMDBarViewController: UIViewController, var actions: [UIMenuElement] = [] #if !os(visionOS) actions.append(menuAction( - title: "Camera…", + title: String(localized: "Camera…"), image: UIImage(systemName: "camera"), identifier: "umd.attach.camera", attributes: enabled && FileAttachAvailability.cameraAvailable @@ -897,21 +906,21 @@ final class UMDBarViewController: UIViewController, )) #endif actions.append(menuAction( - title: "Photo Library…", + title: String(localized: "Photo Library…"), image: UIImage(systemName: "photo.on.rectangle"), identifier: "umd.attach.photos", attributes: enabled ? [] : .disabled, action: .attach(.photoLibrary) )) actions.append(menuAction( - title: "Files…", + title: String(localized: "Files…"), image: UIImage(systemName: "folder"), identifier: "umd.attach.files", attributes: enabled ? [] : .disabled, action: .attach(.files) )) return UIMenu( - title: "Send File…", + title: String(localized: "Send File…"), image: UIImage(systemName: "paperclip"), children: actions ) diff --git a/Multiplex/Views/Terminal/ViewportPane.swift b/Multiplex/Views/Terminal/ViewportPane.swift index 66c434bc..f39fc3f4 100644 --- a/Multiplex/Views/Terminal/ViewportPane.swift +++ b/Multiplex/Views/Terminal/ViewportPane.swift @@ -54,8 +54,10 @@ struct ViewportOrnamentConfiguration { final class ViewportPaneViewController: UIViewController, UIAdaptivePresentationControllerDelegate { - static let clearBrowsingMessage = "Clears cookies, caches, and site storage for every " - + "viewport page — dev-server logins included. This page reloads signed out." + static let clearBrowsingMessage = String(localized: """ + Clears cookies, caches, and site storage for every viewport page — dev-server \ + logins included. This page reloads signed out. + """) private let controller: ViewportController private var contentSafeArea: UIEdgeInsets @@ -266,13 +268,13 @@ final class ViewportPaneViewController: UIViewController, backChip = chip( "", systemImage: "chevron.left", - accessibility: "Back" + accessibility: String(localized: "Back") ) { [weak controller] in controller?.goBack() } backChip.accessibilityIdentifier = "viewport.back" reloadChip = chip( "", systemImage: "arrow.clockwise", - accessibility: "Reload" + accessibility: String(localized: "Reload") ) { [weak self] in guard let self else { return } if self.state?.isLoading == true { @@ -289,18 +291,25 @@ final class ViewportPaneViewController: UIViewController, urlButton.setContentHuggingPriority(.defaultLow, for: .horizontal) urlButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) urlButton.hoverStyle = UIHoverStyle(effect: .highlight, shape: .rect(cornerRadius: 2)) - urlButton.accessibilityHint = "Edits the address" + urlButton.accessibilityHint = String(localized: "Edits the address") urlButton.addAction(UIAction { [weak self] _ in self?.beginEditingAddress() }, for: .touchUpInside) urlButton.showsMenuAsPrimaryAction = false urlButton.menu = makeAddressMenu() urlButton.accessibilityIdentifier = "viewport.address" reachBadge.setContentHuggingPriority(.required, for: .horizontal) - systemChip = chip("SYSTEM", accessibility: "Open in the system browser") { [weak controller] in + systemChip = chip( + "SYSTEM", + accessibility: String(localized: "Open in the system browser") + ) { [weak controller] in controller?.openInSystemBrowser() } systemChip.accessibilityIdentifier = "viewport.system" - closeChip = chip("CLOSE", prominent: true, accessibility: "Close viewport") { [weak self] in + closeChip = chip( + "CLOSE", + prominent: true, + accessibility: String(localized: "Close viewport") + ) { [weak self] in self?.closeAction() } closeChip.accessibilityIdentifier = "viewport.close" @@ -322,11 +331,13 @@ final class ViewportPaneViewController: UIViewController, caption: "", systemImage: state.isLoading ? "xmark" : "arrow.clockwise" ) - reloadChip.accessibilityLabel = state.isLoading ? "Stop loading" : "Reload" + reloadChip.accessibilityLabel = state.isLoading + ? String(localized: "Stop loading") + : String(localized: "Reload") urlButton.setAttributedTitle(Self.readoutText(state.displayURL), for: .normal) urlButton.accessibilityLabel = state.displayURL.absoluteString reachBadge.setText(state.railTag) - reachBadge.accessibilityLabel = "Reach: \(state.railTag)" + reachBadge.accessibilityLabel = String(localized: "Reach: \(state.railTag)") progressLine.isHidden = !state.isLoading updateProgressWidth() if !showsInWindowRail { ornamentRailDidChange() } @@ -397,11 +408,14 @@ final class ViewportPaneViewController: UIViewController, private func makeAddressMenu() -> UIMenu { UIMenu(children: [ - UIAction(title: "Copy Address", image: UIImage(systemName: "doc.on.doc")) { [weak controller] _ in + UIAction( + title: String(localized: "Copy Address"), + image: UIImage(systemName: "doc.on.doc") + ) { [weak controller] _ in controller?.copyURL() }, UIAction( - title: "Clear Browsing Data…", + title: String(localized: "Clear Browsing Data…"), image: UIImage(systemName: "trash"), attributes: .destructive ) { [weak self] _ in self?.presentClearBrowsingDataConfirmation() }, @@ -505,12 +519,15 @@ final class ViewportPaneViewController: UIViewController, case .internet: nil case .lan: - "This address lives on \(hostName)'s network — " - + "the device must share it to load the page." + String(localized: """ + This address lives on \(hostName)'s network — the device must share it to \ + load the page. + """) case .remoteLoopback: - "This page rides \(hostName)'s own address. " - + "The server must listen beyond loopback (vite --host, " - + "-H 0.0.0.0) for anything to answer." + String(localized: """ + This page rides \(hostName)'s own address. The server must listen beyond \ + loopback (vite --host, -H 0.0.0.0) for anything to answer. + """) } } @@ -518,15 +535,21 @@ final class ViewportPaneViewController: UIViewController, func makeClearBrowsingDataAlert() -> UIAlertController { let alert = UIAlertController( - title: "Clear Browsing Data", + title: String(localized: "Clear Browsing Data"), message: Self.clearBrowsingMessage, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "Clear", style: .destructive) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Clear"), + style: .destructive + ) { [weak self] _ in self?.controller.clearBrowsingData() self?.presentPendingExternalLinkIfPossible() }) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in + alert.addAction(UIAlertAction( + title: String(localized: "Cancel"), + style: .cancel + ) { [weak self] _ in self?.presentPendingExternalLinkIfPossible() }) return alert @@ -609,7 +632,7 @@ final class ViewportBadgeView: UIKitTallyBorderedView { final class ViewportAddressEditorView: UIKitTallyBorderedView, UITextFieldDelegate { private(set) var textField = UITextField() private(set) var rejectedLabel = UIKitChassisLabel( - "WEB ADDRESSES ONLY", + String(localized: "WEB ADDRESSES ONLY"), size: 8, color: TallyPalette.caution ) @@ -629,7 +652,7 @@ final class ViewportAddressEditorView: UIKitTallyBorderedView, UITextFieldDelega let caption = UIKitChassisLabel("ADDRESS", size: 9, color: UIKitChassis.signal3) textField.text = text - textField.placeholder = "host:port or https://…" + textField.placeholder = String(localized: "host:port or https://…") textField.font = UIKitChassis.monoFont(12) textField.textColor = UIKitChassis.signal textField.tintColor = UIKitChassis.signal @@ -649,12 +672,12 @@ final class ViewportAddressEditorView: UIKitTallyBorderedView, UITextFieldDelega goChip = UIKitChassisChip( "GO", prominent: true, - accessibilityLabel: "Go", + accessibilityLabel: String(localized: "Go"), action: submit ) cancelChip = UIKitChassisChip( "CANCEL", - accessibilityLabel: "Cancel address edit", + accessibilityLabel: String(localized: "Cancel address edit"), action: cancel ) let row = UIStackView(arrangedSubviews: [ @@ -736,12 +759,12 @@ final class ViewportFailureOverlayView: UIView { retryChip = UIKitChassisChip( "RETRY", prominent: true, - accessibilityLabel: "Retry", + accessibilityLabel: String(localized: "Retry"), action: retry ) systemChip = UIKitChassisChip( "SYSTEM", - accessibilityLabel: "Open in the system browser", + accessibilityLabel: String(localized: "Open in the system browser"), action: openSystem ) let actions = UIStackView(arrangedSubviews: [retryChip, systemChip]) @@ -959,7 +982,7 @@ final class ViewportUMDViewController: UIViewController { deckChip = chip( configuration.style == .shell ? configuration.deckControlLabel : "DECK", accessibility: configuration.style == .shell - ? configuration.deckControlLabel.capitalized : "Deck", + ? configuration.deckControlLabel.capitalized : String(localized: "Deck"), action: .showDeck ) let title = UIKitChassisLabel( @@ -987,7 +1010,9 @@ final class ViewportUMDViewController: UIViewController { if !configuration.mergeSources.isEmpty { let menu = ViewportMenuButton( caption: "MERGE", - accessibilityLabel: "Merge another window into this one", + accessibilityLabel: String( + localized: "Merge another window into this one" + ), menu: makeMergeMenu() ) mergeButton = menu @@ -1045,11 +1070,19 @@ final class ViewportUMDViewController: UIViewController { var views: [UIView] = [] if state.canGoBack || state.canGoForward { - let back = fileChip("◂", accessibility: "Back", action: fileViewer.goBack) + let back = fileChip( + "◂", + accessibility: String(localized: "Back"), + action: fileViewer.goBack + ) back.accessibilityIdentifier = "fileViewer.back" setChipEnabled(back, state.canGoBack) fileBackChip = back - let forward = fileChip("▸", accessibility: "Forward", action: fileViewer.goForward) + let forward = fileChip( + "▸", + accessibility: String(localized: "Forward"), + action: fileViewer.goForward + ) forward.accessibilityIdentifier = "fileViewer.forward" setChipEnabled(forward, state.canGoForward) fileForwardChip = forward @@ -1064,7 +1097,7 @@ final class ViewportUMDViewController: UIViewController { let source = fileChip( "SOURCE", prominent: state.sourceSelected, - accessibility: "Show source", + accessibility: String(localized: "Show source"), action: fileViewer.showSource ) source.accessibilityIdentifier = "fileViewer.source" @@ -1072,7 +1105,7 @@ final class ViewportUMDViewController: UIViewController { let diff = fileChip( "DIFF", prominent: !state.sourceSelected, - accessibility: "Show diff", + accessibility: String(localized: "Show diff"), action: fileViewer.showDiff ) diff.accessibilityIdentifier = "fileViewer.diff" @@ -1081,7 +1114,7 @@ final class ViewportUMDViewController: UIViewController { segment.axis = .horizontal segment.alignment = .center segment.spacing = 4 - segment.accessibilityLabel = "Source or diff" + segment.accessibilityLabel = String(localized: "Source or diff") views.append(segment) } @@ -1091,7 +1124,8 @@ final class ViewportUMDViewController: UIViewController { caption, prominent: selected, accessibility: selected - ? "Back to rendered markdown" : "Select source text to copy", + ? String(localized: "Back to rendered markdown") + : String(localized: "Select source text to copy"), action: fileViewer.toggleMarkdownSelection ) select.accessibilityIdentifier = "fileViewer.markdownSelect" @@ -1120,7 +1154,7 @@ final class ViewportUMDViewController: UIViewController { views.append(path) let host = FileViewerBadgeView(state.hostName.uppercased()) - host.accessibilityLabel = "Files on \(state.hostName)" + host.accessibilityLabel = String(localized: "Files on \(state.hostName)") host.setContentHuggingPriority(.required, for: .horizontal) fileHostBadge = host views.append(host) @@ -1128,7 +1162,8 @@ final class ViewportUMDViewController: UIViewController { let tree = fileChip( state.treeCaption, accessibility: state.treeCaption == "HIDE" - ? "Hide the file tree" : "Show the file tree", + ? String(localized: "Hide the file tree") + : String(localized: "Show the file tree"), action: fileViewer.toggleTree ) tree.accessibilityIdentifier = "fileViewer.tree" @@ -1137,7 +1172,7 @@ final class ViewportUMDViewController: UIViewController { let refresh = fileChip( "REFRESH", - accessibility: "Refresh file viewer", + accessibility: String(localized: "Refresh file viewer"), action: fileViewer.refresh ) refresh.accessibilityIdentifier = "fileViewer.refresh" @@ -1214,7 +1249,7 @@ final class ViewportUMDViewController: UIViewController { let label = FileViewerTextScale.percentLabel(textScale.scale) let reset = chip( label, - accessibility: "Text size \(label); resets to 100 percent", + accessibility: String(localized: "Text size \(label); resets to 100 percent"), action: .textScaleReset ) reset.accessibilityIdentifier = "viewportUMD.textScaleReset" @@ -1224,7 +1259,7 @@ final class ViewportUMDViewController: UIViewController { let smaller = chip( "A−", - accessibility: "Smaller text", + accessibility: String(localized: "Smaller text"), action: .textScaleStep(-1) ) smaller.accessibilityIdentifier = "viewportUMD.textSmaller" @@ -1234,7 +1269,7 @@ final class ViewportUMDViewController: UIViewController { let larger = chip( "A+", - accessibility: "Larger text", + accessibility: String(localized: "Larger text"), action: .textScaleStep(1) ) larger.accessibilityIdentifier = "viewportUMD.textLarger" @@ -1352,20 +1387,22 @@ final class ViewportSwitchboardViewController: UIViewController { private func render() { guard let viewport = configuration.viewport else { return } - deckChip = chip("DECK", accessibility: "Deck") { [weak self] in + deckChip = chip("DECK", accessibility: String(localized: "Deck")) { [weak self] in self?.configuration.showDeck() } backChip = chip( "", systemImage: "chevron.left", - accessibility: "Back" + accessibility: String(localized: "Back") ) { [weak self] in self?.configuration.viewport?.goBack() } backChip.accessibilityIdentifier = "viewport.back" setChipEnabled(backChip, viewport.key.canGoBack) reloadChip = chip( "", systemImage: viewport.key.isLoading ? "xmark" : "arrow.clockwise", - accessibility: viewport.key.isLoading ? "Stop loading" : "Reload" + accessibility: viewport.key.isLoading + ? String(localized: "Stop loading") + : String(localized: "Reload") ) { [weak self] in self?.configuration.viewport?.reloadOrStop() } reloadChip.accessibilityIdentifier = "viewport.reload" let navigateRow = UIStackView(arrangedSubviews: [ @@ -1383,7 +1420,7 @@ final class ViewportSwitchboardViewController: UIViewController { for: .normal ) addressButton.accessibilityLabel = viewport.key.displayURL.absoluteString - addressButton.accessibilityHint = "Edits the address" + addressButton.accessibilityHint = String(localized: "Edits the address") addressButton.accessibilityIdentifier = "viewport.address" addressButton.hoverStyle = UIHoverStyle( effect: .highlight, @@ -1401,7 +1438,7 @@ final class ViewportSwitchboardViewController: UIViewController { addressButton.widthAnchor.constraint(lessThanOrEqualToConstant: 300), ]) reachBadge = ViewportBadgeView(viewport.key.railTag) - reachBadge.accessibilityLabel = "Reach: \(viewport.key.railTag)" + reachBadge.accessibilityLabel = String(localized: "Reach: \(viewport.key.railTag)") reachBadge.setContentHuggingPriority(.required, for: .horizontal) let locateRow = UIStackView(arrangedSubviews: [addressButton, reachBadge]) configure(row: locateRow, spacing: 10) @@ -1410,7 +1447,10 @@ final class ViewportSwitchboardViewController: UIViewController { progress: viewport.key.isLoading ? viewport.key.progress : nil ) - systemChip = chip("SYSTEM", accessibility: "Open in the system browser") { [weak self] in + systemChip = chip( + "SYSTEM", + accessibility: String(localized: "Open in the system browser") + ) { [weak self] in self?.configuration.viewport?.openInSystemBrowser() } systemChip.accessibilityIdentifier = "viewport.system" @@ -1418,7 +1458,7 @@ final class ViewportSwitchboardViewController: UIViewController { if !configuration.mergeSources.isEmpty { let menu = ViewportMenuButton( caption: "⋯", - accessibilityLabel: "Merge another window into this one", + accessibilityLabel: String(localized: "Merge another window into this one"), menu: mergeMenu() ) mergeButton = menu @@ -1483,11 +1523,14 @@ final class ViewportSwitchboardViewController: UIViewController { private func addressMenu() -> UIMenu { UIMenu(children: [ - UIAction(title: "Copy Address", image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in + UIAction( + title: String(localized: "Copy Address"), + image: UIImage(systemName: "doc.on.doc") + ) { [weak self] _ in self?.configuration.viewport?.copyAddress() }, UIAction( - title: "Clear Browsing Data…", + title: String(localized: "Clear Browsing Data…"), image: UIImage(systemName: "trash"), attributes: .destructive ) { [weak self] _ in @@ -1497,7 +1540,7 @@ final class ViewportSwitchboardViewController: UIViewController { } private func mergeMenu() -> UIMenu { - UIMenu(title: "Merge", children: configuration.mergeSources.map { source in + UIMenu(title: String(localized: "Merge"), children: configuration.mergeSources.map { source in UIAction( title: source.label, image: UIImage(systemName: "macwindow"), diff --git a/MultiplexTests/SettingsUIKitTests.swift b/MultiplexTests/SettingsUIKitTests.swift index 242bc809..47a1c80d 100644 --- a/MultiplexTests/SettingsUIKitTests.swift +++ b/MultiplexTests/SettingsUIKitTests.swift @@ -54,6 +54,7 @@ final class SettingsUIKitTests: XCTestCase { "Agent alerts", "App lock", "Multiplex Pro", + "Language", "About", ]) diff --git a/MultiplexTests/StringCatalogTests.swift b/MultiplexTests/StringCatalogTests.swift new file mode 100644 index 00000000..db1e5b65 --- /dev/null +++ b/MultiplexTests/StringCatalogTests.swift @@ -0,0 +1,78 @@ +import XCTest +@testable import Multiplex + +/// The String Catalogs are the localization: a key that lands in the catalog +/// without a zh-Hant and ja value falls back to English silently at runtime. +/// This test makes that fall-back loud instead. `needs_review` counts as +/// translated (a native reviewer's pass is expected, not required to ship); +/// keys marked `shouldTranslate: false` are the TALLY chrome that stays +/// English by design (see docs/agents/i18n.md). +final class StringCatalogTests: XCTestCase { + private static let locales = ["zh-Hant", "ja"] + + private static let repoRoot = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() // MultiplexTests + .deletingLastPathComponent() // repo + + private struct Unit: Decodable { let state: String; let value: String } + private struct Localization: Decodable { + let stringUnit: Unit? + let variations: [String: [String: [String: Unit]]]? + } + private struct Entry: Decodable { + let extractionState: String? + let shouldTranslate: Bool? + let localizations: [String: Localization]? + } + private struct Catalog: Decodable { + let sourceLanguage: String + let strings: [String: Entry] + } + + private func load(_ relative: String) throws -> Catalog { + let url = Self.repoRoot.appendingPathComponent(relative) + return try JSONDecoder().decode(Catalog.self, from: Data(contentsOf: url)) + } + + private func assertComplete(_ relative: String, file: StaticString = #filePath, line: UInt = #line) throws { + let catalog = try load(relative) + XCTAssertEqual(catalog.sourceLanguage, "en", file: file, line: line) + var missing: [String] = [] + for (key, entry) in catalog.strings { + if entry.shouldTranslate == false { continue } + if entry.extractionState == "stale" { continue } + for locale in Self.locales { + let localization = entry.localizations?[locale] + let value = localization?.stringUnit?.value + ?? localization?.variations?.values.first?.values.first?.values.first?.value + if value?.isEmpty ?? true { missing.append("\(locale): \(key)") } + } + } + XCTAssertTrue( + missing.isEmpty, + "\(relative) has \(missing.count) untranslated keys:\n" + missing.sorted().joined(separator: "\n"), + file: file, line: line + ) + } + + func testAppCatalogIsComplete() throws { + try assertComplete("Multiplex/Localizable.xcstrings") + } + + func testInfoPlistCatalogIsComplete() throws { + try assertComplete("Multiplex/InfoPlist.xcstrings") + } + + func testWidgetCatalogIsComplete() throws { + try assertComplete("MultiplexWidgets/Localizable.xcstrings") + } + + /// The compiled bundle carries both languages, so Settings.app offers the + /// per-app language row (the only language switch Multiplex has). + func testBundleShipsBothLocalizations() { + let shipped = Set(Bundle.main.localizations) + for locale in Self.locales { + XCTAssertTrue(shipped.contains(locale), "bundle lacks \(locale): \(shipped)") + } + } +} diff --git a/MultiplexWidgets/FleetWidget.swift b/MultiplexWidgets/FleetWidget.swift index 323ac033..188c8526 100644 --- a/MultiplexWidgets/FleetWidget.swift +++ b/MultiplexWidgets/FleetWidget.swift @@ -75,7 +75,7 @@ struct FleetWidgetView: View { HStack { WidgetLabel("multiplex · fleet", size: 10) Spacer(minLength: 4) - Text("\(entry.hosts.count) HOSTS · \(sessionCount) SESS") + Text(verbatim: "\(entry.hosts.count) HOSTS · \(sessionCount) SESS") .font(.widgetMono(7.5)) .foregroundStyle(palette.signal3) } @@ -218,9 +218,9 @@ struct FleetWidgetView: View { if let session = tile.session, !session.miniatureLines.isEmpty { MiniatureScreen(lines: session.miniatureLines, fontSize: 7) } else if tile.session != nil { - HatchScreen(caption: "No recent frame") + HatchScreen(caption: String(localized: "No recent frame")) } else { - HatchScreen(caption: "No recent data") + HatchScreen(caption: String(localized: "No recent data")) } } HStack { diff --git a/MultiplexWidgets/HostWidget.swift b/MultiplexWidgets/HostWidget.swift index a8c15606..b9b62c12 100644 --- a/MultiplexWidgets/HostWidget.swift +++ b/MultiplexWidgets/HostWidget.swift @@ -106,7 +106,7 @@ struct HostWidgetView: View { HStack { SeenStamp(date: host.probedAt) Spacer(minLength: 4) - Text("\(host.sessions.count) SESS") + Text(verbatim: "\(host.sessions.count) SESS") .font(.widgetMono(7.5)) .foregroundStyle(palette.signal3) } @@ -224,7 +224,7 @@ struct HostWidgetView: View { HStack(spacing: 4) { SeenStamp(date: host.probedAt) - Text("· \(host.sessions.count) SESS") + Text(verbatim: "· \(host.sessions.count) SESS") .font(.widgetMono(7.5)) .foregroundStyle(palette.signal3) } @@ -246,9 +246,9 @@ struct HostWidgetView: View { if let session, !session.miniatureLines.isEmpty { MiniatureScreen(lines: session.miniatureLines) } else if session != nil { - HatchScreen(caption: "No recent frame") + HatchScreen(caption: String(localized: "No recent frame")) } else { - HatchScreen(caption: "No sessions") + HatchScreen(caption: String(localized: "No sessions")) } } } diff --git a/MultiplexWidgets/Localizable.xcstrings b/MultiplexWidgets/Localizable.xcstrings new file mode 100644 index 00000000..684ea266 --- /dev/null +++ b/MultiplexWidgets/Localizable.xcstrings @@ -0,0 +1,633 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "%@" : { + "shouldTranslate" : false + }, + "Action" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "アクション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "動作" + } + } + } + }, + "Agent" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "エージェント" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Agent" + } + } + } + }, + "Agent Default" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "エージェントのデフォルト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Agent 預設" + } + } + } + }, + "Ask for Prompt" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "毎回プロンプトを尋ねる" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "每次詢問提示" + } + } + } + }, + "Awaiting data" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "データを待機中" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "等待資料" + } + } + } + }, + "Backend" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "バックエンド" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "後端" + } + } + } + }, + "Choose a backend" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "バックエンドを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "選擇後端" + } + } + } + }, + "Choose a model" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "モデルを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "選擇模型" + } + } + } + }, + "Choose a session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "セッションを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "選擇工作階段" + } + } + } + }, + "Choose a working directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "作業ディレクトリを選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "選擇工作目錄" + } + } + } + }, + "Choose where the agent opens" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "エージェントを開く場所を選択" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "選擇 Agent 開啟的位置" + } + } + } + }, + "Claude Code" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Claude Code" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Claude Code" + } + } + } + }, + "Codex" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Codex" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Codex" + } + } + } + }, + "Every host's last-known sessions. Tap a row or tile to attach." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "各ホストの最後に確認されたセッション。行またはタイルをタップしてアタッチします。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "每台主機最後已知的工作階段。點一下列或方塊即可連接。" + } + } + } + }, + "Fleet Wall" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Fleet Wall" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Fleet Wall" + } + } + } + }, + "Grok Build" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Grok Build" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Grok Build" + } + } + } + }, + "Home" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "ホーム" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "主目錄" + } + } + } + }, + "Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "ホスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "主機" + } + } + } + }, + "Host Default" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "ホストのデフォルト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "主機預設" + } + } + } + }, + "Host Monitor" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "ホストモニタ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "主機監視器" + } + } + } + }, + "Host Widget" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "ホストウィジェット" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "主機小工具" + } + } + } + }, + "Last-known sessions for one host. Tap to open a shell or launch an agent." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "1台のホストの最後に確認されたセッション。タップしてシェルを開くか、エージェントを起動します。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "單一主機最後已知的工作階段。點一下即可開啟 Shell 或啟動 Agent。" + } + } + } + }, + "Model" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "モデル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "模型" + } + } + } + }, + "Multiplex Host" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Multiplexホスト" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Multiplex 主機" + } + } + } + }, + "New Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新規セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新增工作階段" + } + } + } + }, + "New Tab (Focused Workspace)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新規タブ(フォーカス中のワークスペース)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新增標籤頁(焦點工作區)" + } + } + } + }, + "New Window" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新規ウインドウ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新增視窗" + } + } + } + }, + "New Workspace" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新規ワークスペース" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "新增工作區" + } + } + } + }, + "No recent data" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "最近のデータなし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "沒有近期資料" + } + } + } + }, + "No recent frame" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "最近のフレームなし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "沒有近期畫面" + } + } + } + }, + "No sessions" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "セッションなし" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "沒有工作階段" + } + } + } + }, + "Open In" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "開く場所" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "開啟於" + } + } + } + }, + "Open Multiplex once" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Multiplexを一度開いてください" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "請先開啟 Multiplex 一次" + } + } + } + }, + "Pi" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Pi" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Pi" + } + } + } + }, + "Pick the host this widget monitors, which session it shows, and what a tap opens." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "このウィジェットが監視するホスト、表示するセッション、タップ時に開く対象を選びます。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "選擇這個小工具要監看的主機、要顯示的工作階段,以及點一下時開啟的內容。" + } + } + } + }, + "Session" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "セッション" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "工作階段" + } + } + } + }, + "Shell" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "シェル" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Shell" + } + } + } + }, + "Tap Opens (Small Widget)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "タップで開く(小サイズウィジェット)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "點一下開啟(小型小工具)" + } + } + } + }, + "Working Directory" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "作業ディレクトリ" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "工作目錄" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/MultiplexWidgets/WidgetTheme.swift b/MultiplexWidgets/WidgetTheme.swift index 50911a95..12ef10aa 100644 --- a/MultiplexWidgets/WidgetTheme.swift +++ b/MultiplexWidgets/WidgetTheme.swift @@ -306,12 +306,12 @@ struct SeenStamp: View { Group { if let date { HStack(spacing: 3) { - Text("SEEN") + Text(verbatim: "SEEN") Text(date, style: .relative) .lineLimit(1) } } else { - Text("NEVER SEEN") + Text(verbatim: "NEVER SEEN") } } .font(.widgetMono(fontSize)) @@ -327,8 +327,8 @@ struct AwaitingDataView: View { var body: some View { VStack(spacing: 8) { - HatchScreen(caption: "Awaiting data") - WidgetLabel("Open Multiplex once", size: 8.5, color: palette.signal2) + HatchScreen(caption: String(localized: "Awaiting data")) + WidgetLabel(String(localized: "Open Multiplex once"), size: 8.5, color: palette.signal2) } } } diff --git a/Tools/build.sh b/Tools/build.sh index 3e31e907..ce4b95b7 100755 --- a/Tools/build.sh +++ b/Tools/build.sh @@ -10,6 +10,8 @@ # ./Tools/build.sh verify [vos|ipad] build + install + drive end-to-end # against the local sshd/tmux harness # ./Tools/build.sh interop round-trip against real mosh-server +# ./Tools/build.sh strings sync the String Catalogs from the +# last visionOS build's .stringsdata # ./Tools/build.sh all gen + lint + build both + test # # Single source of truth for destinations, the shared DerivedData path, and the @@ -78,6 +80,25 @@ lint() { swiftlint lint --strict "$@" } +# String Catalog sync. `xcodebuild` emits per-file .stringsdata (every +# `String(localized:)` / SwiftUI `Text` literal) but only the Xcode IDE folds +# them back into the .xcstrings; this is the same `xcstringstool sync` the IDE +# runs. Run after `build vos`; commit the catalog diff. Keys that vanish from +# source are marked stale (Xcode's behaviour), never silently deleted. +strings() { + local inter="$DERIVED/Build/Intermediates.noindex/Multiplex.build/Debug-xrsimulator" + [ -d "$inter" ] || { echo "run ./Tools/build.sh build vos first" >&2; exit 1; } + local target catalog + for pair in "Multiplex:Multiplex/Localizable.xcstrings" \ + "MultiplexWidgets:MultiplexWidgets/Localizable.xcstrings"; do + target="${pair%%:*}"; catalog="${pair#*:}" + # shellcheck disable=SC2046 + xcrun xcstringstool sync "$catalog" --stringsdata \ + $(find "$inter/$target.build/Objects-normal" -name '*.stringsdata') + echo "synced $catalog" + done +} + # Trailing arguments go straight to xcodebuild, so a caller (CI) can add # settings or flags without a second, drifting invocation of its own. build() { @@ -169,6 +190,7 @@ case "${1:-}" in test) shift; run_tests "$@" ;; verify) verify "${2:-vos}" ;; interop) interop ;; + strings) strings ;; all) gen; lint; build vos; build ipad; run_tests vos ;; *) sed -n '2,18p' "$0"; exit 1 ;; esac diff --git a/docs/agents/e2e-headless.md b/docs/agents/e2e-headless.md index fc1c5e19..e647f8e4 100644 --- a/docs/agents/e2e-headless.md +++ b/docs/agents/e2e-headless.md @@ -154,6 +154,11 @@ Simulator caveat: Xcode 27's DeviceHub always bridges the Mac keyboard as *hardware*, so the software keyboard never auto-shows (Device → Keyboard → Toggle Software Keyboard). +**Language for a capture:** append `-AppleLanguages "(zh-Hant)"` (or `(ja)`) +to `simctl launch … app.multiplexterm.multiplex`; UIKit honours the launch +argument per process, so no `.GlobalPreferences` edit or reboot is needed +(unlike the dictation chip's preferred-language seeding below). + **DEBUG notification hooks** — `xcrun simctl spawn notifyutil -p app.multiplexterm.multiplex.`: diff --git a/docs/agents/i18n.md b/docs/agents/i18n.md new file mode 100644 index 00000000..00eff3ec --- /dev/null +++ b/docs/agents/i18n.md @@ -0,0 +1,118 @@ +# Localization (i18n) + +Multiplex ships English (source), Traditional Chinese (`zh-Hant`, Taiwan +wording) and Japanese (`ja`). Decisions below were settled 2026-08-18; do not +re-litigate without new facts. + +## Mechanics + +- **String Catalogs, English text as the key.** `Multiplex/Localizable.xcstrings` + (app), `MultiplexWidgets/Localizable.xcstrings` (widget extension is its own + bundle), `Multiplex/InfoPlist.xcstrings` (usage descriptions; the plist itself + is generated from `project.yml`). No symbolic keys, no `comment:`/`table:`. +- UIKit sites wrap with `String(localized: "…")` (interpolation stays inside: + `String(localized: "Require \(method)")`). SwiftUI `Text("literal")` and App + Intents `LocalizedStringResource` literals extract by themselves. +- **Integer interpolation is locale-formatted**: `String(localized: "port \(port)")` + renders `2,222`. Ports, line numbers, PIDs, years → interpolate + `\(String(port))` (the key then carries `%@`); counts may keep the grouping. +- Long prose uses a multi-line literal with `\` continuations so SwiftLint's + 120-column line length holds and the runtime string has no newlines: + ```swift + detail: String(localized: """ + System follows the device. The deck, terminal chrome, and forms switch \ + together. Dark and Glass share the dark terminal theme below. + """) + ``` +- **The build fills the catalog, never a hand.** `xcodebuild` emits + `.stringsdata` (`SWIFT_EMIT_LOC_STRINGS`) but only the Xcode IDE folds them + back; `./Tools/build.sh strings` runs the same `xcstringstool sync` after + `build vos`. Keys that vanish from source are marked `stale` (kept for + history until you delete them), new keys arrive empty — translate them. +- `StringCatalogTests` fails the suite when any non-stale key lacks a zh-Hant + or ja value (`needs_review` counts) unless it is `shouldTranslate: false`, + and asserts the bundle ships both `lproj`s. +- `zh-Hant`/`ja` build without `knownRegions` (XcodeGen ignores the option; the + catalog compiler emits every language it holds), so `project.yml` needs + nothing per language. + +## Language switching + +There is **no in-app language picker**. iOS/visionOS add a per-app language +row in Settings.app → Multiplex the moment the bundle carries a second +localization; Multiplex Settings only shows a `Language ↗` row that deep-links +there (`UIApplication.openSettingsURLString`). An `AppleLanguages` override +with a relaunch prompt, and live in-app switching, were both rejected: the +first is a worse copy of the system row, the second needs every UIKit label +site to observe a language change. + +## What is localized — the tiers + +- **A — prose** (always): sheets, section titles + detail text, alerts, + confirmations, errors the user reads, empty states, notification + title/body, paywall, Guide/FAQ, placeholders, accessibility sentences, + usage descriptions. The in-app release-notes card (`ReleaseNotes.swift`) + is the one prose surface that stays English — versioned copy rewritten + every release, not worth a per-release translation pass (decided + 2026-08-18); the App Store release notes are localized instead. +- **B — mid-size labels** (always): menu / context-menu items, section + headers (also ALL-CAPS ones), row and toggle titles, tab titles that are + words, tmux/herdr shortcut labels, chip labels that are words. +- **C — TALLY micro chrome stays English by design**: ALL-CAPS button / + badge / chip labels of ≤ 2 words (`DONE`, `CANCEL`, `SELECT`, `PRO`, `LIVE`, + `NEW THEME`, `TO MAIN`), key-rail key names (`ESC`, `CTRL`, `^C`), glyph + strings, telemetry formats. CJK has no capitals; the compressed-caps / mono + rhythm is the identity (DESIGN.md), and terminal users read `ESC/CTRL` as + glyphs. In SwiftUI use `Text(verbatim:)` for these so they are not extracted; + in the catalog mark any that slip in `shouldTranslate: false`. +- Never localized: proper nouns (Multiplex, Multiplex Pro, Deck, Shell, Agent + as a product noun, tmux, herdr, mosh, SSH, Claude Code / Codex / Grok Build / + Pi, theme names), license texts, anything compared / persisted / matched + (attention-detection patterns, deep-link keys, choice raw values), logs, + shell/tmux command strings. + +## Glossary (zh-Hant · ja) + +| EN | zh-Hant | ja | +|---|---|---| +| host | 主機 | ホスト | +| session (tmux / SSH) | 工作階段 (Apple's term; never 會話) | セッション | +| attach / detach | 連接 / 中斷連接 | アタッチ / デタッチ | +| pane / window (tmux) | 窗格 / 視窗 | ペイン / ウインドウ | +| terminal | 終端機 | ターミナル | +| Deck, Shell, Agent, Pro | unchanged | unchanged (Deck / シェル / エージェント / Pro) | +| copy / duplicate | 複製 (never 拷貝 — Apple's term, not local usage) | コピー / 複製 | +| bind (Bind Host) | 綁定 | バインド | +| settings | 設定 | 設定 | +| keyboard | 鍵盤 | キーボード | +| notification | 通知 | 通知 | +| setup script | 設定指令碼 | セットアップスクリプト | +| working directory | 工作目錄 | 作業ディレクトリ | + +Apple's own Settings glossary wins on ties (設定 not 設置, 視窗 not 窗口, +伺服器 not 服务器). Prose uses full-width `,。` in zh-Hant, and Japanese +`、。`; strings that name keys or commands keep half-width punctuation. +zh-Hant drafts are linted with `zhtw-mcp lint --translationese-domain +technical` (github.com/aspect-build/zhtw-mcp); ja drafts with textlint + +`textlint-rule-preset-jtf-style` + `textlint-rule-preset-ja-spacing` +(defaults: JTF style — **no space between full- and half-width +characters**, full-width `:`, no space around `/` or brackets; the em dash +keeps its surrounding spaces). Export the values one per line, lint, apply +`--fix`, re-import; every drafted row is `needs_review` until a native +reviewer flips it. + +## Layout + +SF / SF Mono carry no CJK glyphs; the system falls back to PingFang TC / +Hiragino Sans, wider and taller than Latin, and Japanese runs ~1.3–1.5× the +English length. Sheets and menus wrap; the tier-C chrome is untouched, so +fixed-width chips never see CJK. Verify with the simulator launched under +`-AppleLanguages (zh-Hant)` / `(ja)` (see e2e-headless.md); screenshots are +proof, README rows are not kept. + +## Store metadata + +`fastlane/metadata/zh-Hant` and `ja` mirror `en-US` per file; +`Tools/metadata_limits.rb` allows CJK by Unicode script range per locale +(the en-US glyph allowlist stays strict). TestFlight what-to-test +(`fastlane/testflight-whats-new.txt`) is English only. diff --git a/fastlane/testflight-whats-new.txt b/fastlane/testflight-whats-new.txt index 455dcc79..f8539d8f 100644 --- a/fastlane/testflight-whats-new.txt +++ b/fastlane/testflight-whats-new.txt @@ -1,8 +1,10 @@ NEW SINCE 1.4.0 • COPY COMMAND FOR HANDOFF — long-press a session tile on the deck to copy the attach line for a terminal on that machine itself (tmux and herdr alike). +• TRADITIONAL CHINESE AND JAPANESE — pick per app in Settings → Multiplex → Language (LANGUAGE in Multiplex Settings takes you there). PLEASE TRY 1. Long-press a session tile, pick COPY COMMAND FOR HANDOFF, and paste it into a terminal on the host machine — it should attach the very session the tile shows. 2. Try it on a herdr session too — the pasted line should land without editing. +3. Switch to Chinese or Japanese; open Add Host, Settings, GUIDE and an alert — report anything clipped or odd. Feedback: screenshot in TestFlight diff --git a/project.yml b/project.yml index 67c1a066..49511bd1 100644 --- a/project.yml +++ b/project.yml @@ -1,6 +1,12 @@ name: Multiplex options: bundleIdPrefix: app.multiplexterm + # Localizations live in String Catalogs (Localizable.xcstrings per bundle, + # InfoPlist.xcstrings for the plist strings). English is the source; the + # catalog is filled by the build (SWIFT_EMIT_LOC_STRINGS below), never by + # hand-adding keys — see docs/agents/i18n.md. + developmentLanguage: en + knownRegions: [en, zh-Hant, ja] createIntermediateGroups: true deploymentTarget: iOS: "17.0" @@ -44,6 +50,10 @@ settings: # declaring it as a target dependency; explicit module builds reject that. SWIFT_ENABLE_EXPLICIT_MODULES: NO _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES: NO + # String Catalogs: the build extracts `String(localized:)` / SwiftUI + # `Text` literals into the target's Localizable.xcstrings. + LOCALIZATION_PREFERS_STRING_CATALOGS: YES + SWIFT_EMIT_LOC_STRINGS: YES schemes: Multiplex: