feat: 国际化(i18n) en/ja/ko + App 内语言切换
主体:多语言支持(简体中文源 + 英/日/韩)
- 基础设施:Localizable.xcstrings(String Catalog,sourceLanguage=zh-Hans)
+ pbxproj developmentRegion/knownRegions 注册 en/ja/ko
- 全部硬编码 Locale("zh_CN") → Locale.current;中文 dateFormat → Date.FormatStyle(跟随系统)
- UI 中文字面量统一为 String(appLoc:)(显式绑定所选语言 bundle+locale,即时切换)
Text 字面量走环境 \.locale + Bundle 重定向
- 549 个 catalog key 全部 en/ja/ko 翻译完成(0 未翻译)
- App 内语言切换:我的 → 语言(LanguageManager + 即时生效,无需重启)
- 双用预设(症状/监测指标/慢病)本地化:static→computed 避免缓存
注:本提交为 WIP,一并打包了并行进行的功能模块
(HealthExport 健康导出、Security/Face ID 锁、DiaryAssist 日记 AI 辅助)
及 App 图标、CLAUDE.md、docs/scripts。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,10 @@ enum TjTab: String, Hashable, CaseIterable {
|
||||
case home, records, trend, me
|
||||
var label: String {
|
||||
switch self {
|
||||
case .home: return "主页"
|
||||
case .records: return "记录"
|
||||
case .trend: return "趋势"
|
||||
case .me: return "我的"
|
||||
case .home: return String(appLoc: "主页")
|
||||
case .records: return String(appLoc: "记录")
|
||||
case .trend: return String(appLoc: "趋势")
|
||||
case .me: return String(appLoc: "我的")
|
||||
}
|
||||
}
|
||||
var icon: String {
|
||||
@@ -18,6 +18,15 @@ enum TjTab: String, Hashable, CaseIterable {
|
||||
case .me: return "person.circle"
|
||||
}
|
||||
}
|
||||
/// 屏上从左到右的位置,用于决定页面 push 过渡的方向。
|
||||
var index: Int {
|
||||
switch self {
|
||||
case .home: return 0
|
||||
case .records: return 1
|
||||
case .trend: return 2
|
||||
case .me: return 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ActiveFlow: Identifiable {
|
||||
@@ -27,26 +36,39 @@ enum ActiveFlow: Identifiable {
|
||||
|
||||
struct RootView: View {
|
||||
@State private var tab: TjTab = .home
|
||||
/// 页面 push 过渡的来向:切到右侧 tab 时从 trailing 推入,切到左侧时从 leading 推入。
|
||||
@State private var pushEdge: Edge = .trailing
|
||||
@State private var showRecordSheet = false
|
||||
@State private var activeFlow: ActiveFlow?
|
||||
@State private var showSymptomStart = false
|
||||
@State private var showDiary = false
|
||||
@State private var showIndicator = false
|
||||
@State private var showReminders = false
|
||||
|
||||
/// 统一的 tab 切换入口:按方向设定 pushEdge,再带动画改 tab。
|
||||
/// 所有改 tab 的地方都走这里,保证过渡方向正确。
|
||||
private func select(_ newTab: TjTab) {
|
||||
guard newTab != tab else { return }
|
||||
pushEdge = newTab.index > tab.index ? .trailing : .leading
|
||||
withAnimation(.easeInOut(duration: 0.26)) { tab = newTab }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Group {
|
||||
switch tab {
|
||||
case .home: HomeView(onTapArchive: { tab = .records })
|
||||
case .home: HomeView(onTapArchive: { select(.records) })
|
||||
case .records: ArchiveListView()
|
||||
case .trend: TrendsView()
|
||||
case .me: MeView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.id(tab)
|
||||
.transition(.push(from: pushEdge))
|
||||
|
||||
TabBar(active: tab,
|
||||
onTap: { tab = $0 },
|
||||
onTap: { select($0) },
|
||||
onTapRecord: { showRecordSheet = true })
|
||||
}
|
||||
.background(Tj.Palette.sand.ignoresSafeArea())
|
||||
@@ -60,6 +82,7 @@ struct RootView: View {
|
||||
case .symptom: showSymptomStart = true
|
||||
case .diary: showDiary = true
|
||||
case .indicator: showIndicator = true
|
||||
case .reminder: showReminders = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +96,10 @@ struct RootView: View {
|
||||
.sheet(isPresented: $showIndicator) {
|
||||
IndicatorQuickSheet()
|
||||
}
|
||||
.sheet(isPresented: $showReminders) {
|
||||
// 列表页依赖外层 NavigationStack 提供标题栏;sheet 形态补「完成」按钮。
|
||||
NavigationStack { RemindersListView(presentedAsSheet: true) }
|
||||
}
|
||||
#if os(iOS)
|
||||
.fullScreenCover(item: $activeFlow) { flow in
|
||||
switch flow {
|
||||
@@ -100,6 +127,8 @@ private struct TabBar: View {
|
||||
let onTap: (TjTab) -> Void
|
||||
let onTapRecord: () -> Void
|
||||
|
||||
@Namespace private var indicatorNS
|
||||
|
||||
private let cornerRadius: CGFloat = 22
|
||||
private let slotHeight: CGFloat = 34
|
||||
|
||||
@@ -115,6 +144,7 @@ private struct TabBar: View {
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 6)
|
||||
.background(barBackground)
|
||||
.animation(.spring(response: 0.35, dampingFraction: 0.75), value: active)
|
||||
}
|
||||
|
||||
private var barBackground: some View {
|
||||
@@ -143,6 +173,7 @@ private struct TabBar: View {
|
||||
Capsule()
|
||||
.fill(Tj.Palette.sand2)
|
||||
.frame(width: 44, height: slotHeight - 6)
|
||||
.matchedGeometryEffect(id: "tabIndicator", in: indicatorNS)
|
||||
}
|
||||
Image(systemName: t.icon)
|
||||
.font(.system(size: 18, weight: isActive ? .semibold : .regular))
|
||||
@@ -188,7 +219,7 @@ private struct TabBar: View {
|
||||
.buttonStyle(TabPressStyle())
|
||||
}
|
||||
}
|
||||
|
||||
// 你好
|
||||
private struct TabPressStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
|
||||
Reference in New Issue
Block a user