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

@@ -171,7 +171,7 @@ struct IndicatorQuickSheet: View {
private var monitorGridSection: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
sectionLabel("长期监测(进趋势)")
sectionLabel(String(appLoc: "长期监测(进趋势)"))
Spacer()
if !hiddenSet.isEmpty {
hiddenCountChip
@@ -329,7 +329,7 @@ struct IndicatorQuickSheet: View {
private var labPresetSection: some View {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("化验项快捷(不进趋势)")
sectionLabel(String(appLoc: "化验项快捷(不进趋势)"))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(labPresets) { p in
@@ -345,14 +345,14 @@ struct IndicatorQuickSheet: View {
private var bpFieldSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
sectionLabel("收缩 / 舒张")
sectionLabel(String(appLoc: "收缩 / 舒张"))
Spacer()
bpRangeHint
}
HStack(spacing: 12) {
bpField(label: "收缩压", value: $systolic, placeholder: "120")
bpField(label: String(appLoc: "收缩压"), value: $systolic, placeholder: "120")
Text("/").font(.system(size: 22, weight: .light)).foregroundStyle(Tj.Palette.text3)
bpField(label: "舒张压", value: $diastolic, placeholder: "80")
bpField(label: String(appLoc: "舒张压"), value: $diastolic, placeholder: "80")
Text("mmHg").foregroundStyle(Tj.Palette.text3)
}
bpStatusChips
@@ -396,10 +396,10 @@ struct IndicatorQuickSheet: View {
private var bpStatusChips: some View {
HStack(spacing: 8) {
if let s = computedBPStatus(.systolic) {
statusBadge("收缩 " + s.label, color: s.color)
statusBadge(String(appLoc: "收缩 ") + s.label, color: s.color)
}
if let s = computedBPStatus(.diastolic) {
statusBadge("舒张 " + s.label, color: s.color)
statusBadge(String(appLoc: "舒张 ") + s.label, color: s.color)
}
Spacer()
}
@@ -407,7 +407,7 @@ struct IndicatorQuickSheet: View {
private var nameSection: some View {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("指标名")
sectionLabel(String(appLoc: "指标名"))
TextField("例如:血红蛋白", text: $name)
.textInputAutocapitalization(.never)
.padding(.horizontal, 14)
@@ -427,7 +427,7 @@ struct IndicatorQuickSheet: View {
private var valueRow: some View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("数值")
sectionLabel(String(appLoc: "数值"))
TextField(monitorFieldPlaceholder, text: $value)
.keyboardType(.decimalPad)
.font(.system(size: 18, weight: .semibold, design: .monospaced))
@@ -437,7 +437,7 @@ struct IndicatorQuickSheet: View {
.overlay(fieldBorder)
}
VStack(alignment: .leading, spacing: 8) {
sectionLabel("单位")
sectionLabel(String(appLoc: "单位"))
TextField("mmol/L", text: $unit)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
@@ -455,7 +455,7 @@ struct IndicatorQuickSheet: View {
private var rangeSection: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
sectionLabel("参考范围")
sectionLabel(String(appLoc: "参考范围"))
Spacer()
if let m = selectedMonitor, m != .bloodPressure {
monitorRangeHint(m)
@@ -486,11 +486,11 @@ struct IndicatorQuickSheet: View {
private var statusSection: some View {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("状态")
sectionLabel(String(appLoc: "状态"))
HStack(spacing: 8) {
statusChip(.normal, label: "正常", color: Tj.Palette.leaf)
statusChip(.high, label: "偏高 ↑", color: Tj.Palette.brick)
statusChip(.low, label: "偏低 ↓", color: Tj.Palette.amber)
statusChip(.normal, label: String(appLoc: "正常"), color: Tj.Palette.leaf)
statusChip(.high, label: String(appLoc: "偏高 ↑"), color: Tj.Palette.brick)
statusChip(.low, label: String(appLoc: "偏低 ↓"), color: Tj.Palette.amber)
}
}
}
@@ -498,7 +498,7 @@ struct IndicatorQuickSheet: View {
private var autoStatusHint: some View {
let auto = computedSingleStatus
return HStack(spacing: 8) {
sectionLabel("状态(按数值自动判)")
sectionLabel(String(appLoc: "状态(按数值自动判)"))
if let s = auto {
statusBadge(s.label, color: s.color)
} else {
@@ -511,7 +511,7 @@ struct IndicatorQuickSheet: View {
private var timeSection: some View {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("测量时间")
sectionLabel(String(appLoc: "测量时间"))
DatePicker("", selection: $capturedAt, in: ...Date.now)
.datePickerStyle(.compact)
.labelsHidden()
@@ -520,7 +520,7 @@ struct IndicatorQuickSheet: View {
private var noteSection: some View {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("备注(可选)")
sectionLabel(String(appLoc: "备注(可选)"))
TextField("例如:空腹采血", text: $note, axis: .vertical)
.lineLimit(1...3)
.padding(.horizontal, 14)
@@ -535,7 +535,7 @@ struct IndicatorQuickSheet: View {
private var reminderSection: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
sectionLabel("周期提醒")
sectionLabel(String(appLoc: "周期提醒"))
Spacer()
Toggle("", isOn: $reminderEnabled)
.labelsHidden()
@@ -570,13 +570,13 @@ struct IndicatorQuickSheet: View {
}
weekdayPickerRow
HStack(spacing: 8) {
quickFreqChip("每天") {
quickFreqChip(String(appLoc: "每天")) {
reminderWeekdays = Set(1...7)
}
quickFreqChip("工作日") {
quickFreqChip(String(appLoc: "工作日")) {
reminderWeekdays = Set([2, 3, 4, 5, 6])
}
quickFreqChip("周末") {
quickFreqChip(String(appLoc: "周末")) {
reminderWeekdays = Set([1, 7])
}
}
@@ -600,15 +600,23 @@ struct IndicatorQuickSheet: View {
}
private var reminderFrequencyLabel: String {
if reminderWeekdays.count == 7 { return "每天" }
if reminderWeekdays.isEmpty { return "未选" }
let names = ["", "", "", "", "", "", ""]
if reminderWeekdays.count == 7 { return String(appLoc: "每天") }
if reminderWeekdays.isEmpty { return String(appLoc: "未选") }
let names = [
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""),
]
let sorted = reminderWeekdays.sorted()
return "每周 " + sorted.map { names[$0 - 1] }.joined()
return String(appLoc: "每周 ") + sorted.map { names[$0 - 1] }.joined()
}
private var weekdayPickerRow: some View {
let names = ["", "", "", "", "", "", ""]
let names = [
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""),
]
let weekdayValues = [2, 3, 4, 5, 6, 7, 1] // (Apple Calendar )
return HStack(spacing: 6) {
ForEach(Array(weekdayValues.enumerated()), id: \.offset) { idx, w in
@@ -1074,9 +1082,9 @@ struct IndicatorQuickSheet: View {
private extension IndicatorStatus {
var label: String {
switch self {
case .normal: return "正常"
case .high: return "偏高 ↑"
case .low: return "偏低 ↓"
case .normal: return String(appLoc: "正常")
case .high: return String(appLoc: "偏高 ↑")
case .low: return String(appLoc: "偏低 ↓")
}
}