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:
link2026
2026-05-30 10:28:24 +08:00
parent 910ca99f21
commit d2c77d5c51
84 changed files with 15643 additions and 699 deletions

View File

@@ -14,8 +14,13 @@ struct ArchiveListView: View {
@Query(sort: \Symptom.startedAt, order: .reverse)
private var symptoms: [Symptom]
@Query(sort: \HealthExport.createdAt, order: .reverse)
private var exports: [HealthExport]
@State private var filter: TimelineKind? = nil
@State private var endingSymptom: Symptom?
@State private var showExportSheet = false
@State private var showExportList = false
@MainActor
private var allEntries: [TimelineEntry] {
@@ -35,6 +40,15 @@ struct ArchiveListView: View {
private var totalCount: Int { allEntries.count }
var body: some View {
NavigationStack {
content
.navigationDestination(isPresented: $showExportList) {
HealthExportListView()
}
}
}
private var content: some View {
VStack(alignment: .leading, spacing: 0) {
header
.padding(.horizontal, 20)
@@ -71,6 +85,9 @@ struct ArchiveListView: View {
.sheet(item: $endingSymptom) { sym in
SymptomEndSheet(symptom: sym)
}
.fullScreenCover(isPresented: $showExportSheet) {
HealthExportSheet()
}
}
@ViewBuilder
@@ -93,17 +110,44 @@ struct ArchiveListView: View {
Text("记录")
.font(.tjTitle(26))
.foregroundStyle(Tj.Palette.text)
Text(totalCount == 0 ? "" : "\(totalCount)")
Text(totalCount == 0 ? "" : String(appLoc: "\(totalCount)"))
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
Menu {
Button {
showExportSheet = true
} label: {
Label("生成新导出", systemImage: "doc.text.below.ecg")
}
if !exports.isEmpty {
Button {
showExportList = true
} label: {
Label("我的导出 · \(exports.count)", systemImage: "clock.arrow.circlepath")
}
}
} label: {
HStack(spacing: 6) {
Image(systemName: "doc.text.below.ecg")
.font(.system(size: 12, weight: .semibold))
Text("导出")
.font(.system(size: 13, weight: .semibold))
Image(systemName: "chevron.down")
.font(.system(size: 9, weight: .semibold))
}
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(Capsule().fill(Tj.Palette.ink))
}
}
}
private var filterChips: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
chip(label: "全部", selected: filter == nil) { filter = nil }
chip(label: String(appLoc: "全部"), selected: filter == nil) { filter = nil }
ForEach(TimelineKind.allCases) { kind in
chip(label: kind.label, selected: filter == kind) {
filter = filter == kind ? nil : kind
@@ -152,9 +196,9 @@ struct ArchiveListView: View {
private var emptyState: some View {
VStack(spacing: 14) {
Spacer()
TjPlaceholder(label: "还没有任何记录\n点底部 + 号开始")
TjPlaceholder(label: String(appLoc: "还没有任何记录\n点底部 + 号开始"))
.frame(width: 240, height: 140)
Text(filter == nil ? "记录会按时间归类显示" : "这个类别下没有记录")
Text(filter == nil ? String(appLoc: "记录会按时间归类显示") : String(appLoc: "这个类别下没有记录"))
.font(.system(size: 13))
.foregroundStyle(Tj.Palette.text3)
Spacer()
@@ -166,6 +210,8 @@ struct ArchiveListView: View {
#Preview {
ArchiveListView()
.modelContainer(for: [
Indicator.self, Report.self, DiaryEntry.self, Symptom.self, Asset.self
Indicator.self, Report.self, DiaryEntry.self, Symptom.self, Asset.self,
HealthExport.self, ChatTurn.self, UserProfile.self,
MetricReminder.self, CustomMonitorMetric.self
], inMemory: true)
}