Files
hapi/ios/Hapi/Features/Pairing/ManualEntryView.swift
T
weishu 6066f20c20 feat(ios): zh-CN localization + catalog dedupe (A-M5)
Part 1 — Simplified Chinese localization of the iOS app layer:
- ios/Hapi/Resources/Localizable.xcstrings: hand-authored String Catalog
  (395 keys, zh-Hans; en implicit as source). Terminology mined from
  web/src/lib/locales/zh-CN.ts (会话/新建会话/权限模式/允许/拒绝/工作树/
  机器/语音输入/用量/草稿夹 …).
- Mechanical edits only: merged multi-part string concatenations into
  single LocalizedStringKey literals, converted ternary/plain-String user
  copy to String(localized:), switched helper params (detailRow,
  DashboardCard, optionPicker, notice) to LocalizedStringKey.
- LocalizedNoticeMapper (app layer): display-point translation of the 27
  known HapiKit-emitted strings (ChatInteractor notices, dictation errors,
  window-sync warnings, files fallbacks, worktree-name validation) with
  verbatim passthrough for server-originated text; package stays
  language-free.
- Deliberately untranslated (web parity / non-copy): event rows
  (EventPresentation — web renders presentation.ts verbatim), catalog
  option labels (Default/Auto/Sonnet/permission modes — web shows them
  verbatim), tool names, code-like titles (grep(pattern:), MCP:, Skill,
  Task), unit suffixes (B/KB/MB, m/h/d/w), decorative separators.
- Language row wired: AppLanguage gains .system (follow system, new
  default); explicit picks write the AppleLanguages override, Follow
  system removes it; footer notes a relaunch applies it (no supported
  in-place SwiftUI locale swap).
- project.pbxproj: zh-Hans added to knownRegions.

Part 2 — #39 catalog dedupe (HapiProtocol):
- NewSessionCatalogs.claudeModels/claudeEfforts now derive from
  ClaudeModels/ClaudeEfforts (single source shared with ModelCatalog);
  codexReasoningEfforts stays own data (web CODEX_REASONING_EFFORT_OPTIONS
  minus max); effortLabel delegates to ModelCatalog.capitalizedFirst.
- New CatalogTests lock test for the derived option lists.

Gate: bash ios/scripts/linux-test.sh — 459 tests green.
2026-08-18 16:04:03 +08:00

95 lines
3.5 KiB
Swift

import HapiProtocol
import SwiftUI
/// Manual pairing path for hubs without `--relay` (nothing to scan): type or
/// paste the hub URL and the access token the hub prints at startup, then
/// continue into the shared confirm step.
struct ManualEntryView: View {
@Environment(\.dismiss) private var dismiss
@State private var hubUrl = ""
@State private var accessToken = ""
@State private var pending: PendingPairing?
private var canContinue: Bool {
!hubUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& !accessToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
var body: some View {
NavigationStack {
Form {
Section {
TextField("http://192.168.1.20:3006", text: $hubUrl)
.keyboardType(.URL)
.textContentType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
} header: {
Text("Hub URL")
} footer: {
Text("The address the hub prints at startup — a LAN address like http://192.168.1.20:3006, or the public tunnel URL when running with --relay.")
}
Section {
TextField("Access token", text: $accessToken)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.monospaced()
} header: {
Text("Access token")
} footer: {
Text("Printed by the hub at startup, and shown in the web app under Settings → Companion Pairing. Pasting a full pairing link into either field also works.")
}
Section {
Button("Continue") {
continueToConfirm()
}
.frame(maxWidth: .infinity)
.disabled(!canContinue && parsePastedLink() == nil)
}
}
.navigationTitle("Enter Hub Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
}
.navigationDestination(item: $pending) { pending in
PairingConfirmView(pending: pending)
}
}
}
/// Convenience: a whole pairing link pasted into either field carries
/// both values at once.
private func parsePastedLink() -> BindLink? {
BindLink.parse(hubUrl) ?? BindLink.parse(accessToken)
}
private func continueToConfirm() {
if let link = parsePastedLink() {
pending = PendingPairing(
hubUrl: link.hubUrl,
accessToken: link.accessToken,
source: .manual
)
return
}
var address = hubUrl.trimmingCharacters(in: .whitespacesAndNewlines)
// Typing the scheme on a phone is annoying; local hubs serve plain
// HTTP, and the confirm step shows the resulting URL before pairing.
if !address.isEmpty, !address.contains("://") {
address = "http://\(address)"
}
pending = PendingPairing(
hubUrl: address,
accessToken: accessToken.trimmingCharacters(in: .whitespacesAndNewlines),
source: .manual
)
}
}