Files
hapi/ios/Packages/HapiKit/Sources/HapiProtocol/Catalog/NewSessionCatalogs.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

55 lines
2.4 KiB
Swift

import Foundation
/// A `(wire value, display label)` pair for the create-form option pickers.
public struct NewSessionOption: Equatable, Hashable, Sendable {
public let value: String
public let label: String
public init(value: String, label: String) {
self.value = value
self.label = label
}
}
/// Static option catalogs for the new-session form. The claude rows are
/// **derived** from `ClaudeModels` / `ClaudeEfforts` (issue #39: one source
/// of truth shared with `ModelCatalog`'s composer builders) rather than
/// duplicating the preset/label tables. Only the codex reasoning list is its
/// own data — a port of the web `CODEX_REASONING_EFFORT_OPTIONS`
/// (`web/src/components/NewSession/types.ts`; the web drops `max` for codex
/// — `EffortField.tsx`). Android keeps a feature-local copy
/// (`NewSessionCatalogs.kt`); here the catalogs live in one package so the
/// pure form logic and the app UI share one source.
public enum NewSessionCatalogs {
/// `'auto'` sentinel row uses the web's "Default" label; preset rows come
/// from `ClaudeModels` in picker order.
public static let claudeModels: [NewSessionOption] =
[NewSessionOption(value: "auto", label: "Default")]
+ ClaudeModels.presets.map {
NewSessionOption(value: $0, label: ClaudeModels.label(for: $0) ?? $0)
}
/// `'auto'` sentinel row, then the `ClaudeEfforts` levels in ascending
/// order.
public static let claudeEfforts: [NewSessionOption] =
[NewSessionOption(value: "auto", label: "Auto")]
+ ClaudeEfforts.levels.map {
NewSessionOption(value: $0, label: ClaudeEfforts.label(for: $0) ?? effortLabel($0))
}
/// Static codex fallback when the model row advertises no efforts
/// (the web drops `max` for codex — `EffortField.tsx`).
public static let codexReasoningEfforts: [NewSessionOption] = [
NewSessionOption(value: "default", label: "Default"),
NewSessionOption(value: "low", label: "Low"),
NewSessionOption(value: "medium", label: "Medium"),
NewSessionOption(value: "high", label: "High"),
NewSessionOption(value: "xhigh", label: "XHigh"),
]
/// Capitalized label for a server-advertised effort id (`high` → `High`).
public static func effortLabel(_ value: String) -> String {
ModelCatalog.capitalizedFirst(value)
}
}