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

@@ -6,8 +6,8 @@ enum CalendarMode: String, CaseIterable, Identifiable {
var id: String { rawValue }
var label: String {
switch self {
case .month: return ""
case .year: return ""
case .month: return String(appLoc: "")
case .year: return String(appLoc: "")
}
}
}
@@ -31,7 +31,8 @@ struct TrendsView: View {
@State private var mode: CalendarMode = .month
@State private var anchor: Date = .now
@State private var selectedDay: SelectedDay?
/// , inline
@State private var selectedDate: Date = .now
private var profile: UserProfile? { profiles.first }
@@ -44,7 +45,7 @@ struct TrendsView: View {
private let calendar: Calendar = {
var c = Calendar(identifier: .gregorian)
c.firstWeekday = 2
c.locale = Locale(identifier: "zh_CN")
c.locale = Locale.current
return c
}()
@@ -66,6 +67,9 @@ struct TrendsView: View {
anchorBar
calendarBody
legend
if mode == .month {
dayDetailInline
}
seriesSection
}
.padding(.horizontal, 20)
@@ -73,15 +77,31 @@ struct TrendsView: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Tj.Palette.sand.ignoresSafeArea())
.sheet(item: $selectedDay) { sel in
DayDetailSheet(
date: sel.date,
}
/// inline (symptoms / indicators / reports / diaries)
private var dayDetailInline: some View {
VStack(alignment: .leading, spacing: 0) {
DayDetailContent(
date: selectedDate,
indicators: indicators,
reports: reports,
diaries: diaries,
symptoms: symptoms
symptoms: symptoms,
showHeader: true
)
.padding(14)
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
.animation(.snappy(duration: 0.2), value: selectedDate)
}
private var header: some View {
@@ -91,7 +111,10 @@ struct TrendsView: View {
.foregroundStyle(Tj.Palette.text)
Spacer()
Button {
anchor = .now
withAnimation(.snappy(duration: 0.2)) {
anchor = .now
selectedDate = .now
}
} label: {
Text("回到今天")
.font(.system(size: 12))
@@ -164,18 +187,20 @@ struct TrendsView: View {
}
private var anchorTitle: String {
let f = DateFormatter()
f.locale = Locale(identifier: "zh_CN")
f.dateFormat = mode == .month ? "yyyy 年 M 月" : "yyyy 年"
return f.string(from: anchor)
let style: Date.FormatStyle = mode == .month
? .dateTime.year().month()
: .dateTime.year()
return anchor.formatted(style)
}
@ViewBuilder
private var calendarBody: some View {
switch mode {
case .month:
CalendarMonthGrid(monthAnchor: anchor, data: data) { day in
selectedDay = SelectedDay(date: day)
CalendarMonthGrid(monthAnchor: anchor, data: data, selectedDate: selectedDate) { day in
withAnimation(.snappy(duration: 0.2)) {
selectedDate = day
}
}
.padding(14)
.background(
@@ -231,10 +256,10 @@ struct TrendsView: View {
.tracking(0.5)
.foregroundStyle(Tj.Palette.text3)
HStack(spacing: 14) {
legendItem(color: Tj.Palette.brick, label: "指标异常")
legendItem(color: Tj.Palette.amber, label: "症状持续中")
legendItem(color: Tj.Palette.ink2, label: "报告归档")
legendItem(color: Tj.Palette.leaf, label: "正常")
legendItem(color: Tj.Palette.brick, label: String(appLoc: "指标异常"))
legendItem(color: Tj.Palette.amber, label: String(appLoc: "症状持续中"))
legendItem(color: Tj.Palette.ink2, label: String(appLoc: "报告归档"))
legendItem(color: Tj.Palette.leaf, label: String(appLoc: "正常"))
}
}
.padding(.top, 4)
@@ -268,6 +293,14 @@ struct TrendsView: View {
if let next = calendar.date(byAdding: component, value: delta, to: anchor) {
withAnimation(.snappy) {
anchor = next
// selection :() 1
if mode == .month {
if calendar.isDate(next, equalTo: .now, toGranularity: .month) {
selectedDate = .now
} else if let first = calendar.dateInterval(of: .month, for: next)?.start {
selectedDate = first
}
}
}
}
}