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

@@ -3,16 +3,31 @@ import SwiftUI
struct CalendarMonthGrid: View {
let monthAnchor: Date
let data: CalendarData
let selectedDate: Date?
let onTapDay: (Date) -> Void
init(monthAnchor: Date,
data: CalendarData,
selectedDate: Date? = nil,
onTapDay: @escaping (Date) -> Void) {
self.monthAnchor = monthAnchor
self.data = data
self.selectedDate = selectedDate
self.onTapDay = onTapDay
}
private let calendar: Calendar = {
var c = Calendar(identifier: .gregorian)
c.firstWeekday = 2 //
c.locale = Locale(identifier: "zh_CN")
c.locale = Locale.current
return c
}()
private let weekdayLabels = ["", "", "", "", "", "", ""]
private let weekdayLabels = [
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: "")
]
private let columns = Array(repeating: GridItem(.flexible(), spacing: 4), count: 7)
private var days: [DayCell] {
@@ -64,6 +79,9 @@ struct CalendarMonthGrid: View {
ranges: data.ranges(touching: cell.date, calendar: calendar),
marks: data.marks(for: cell.date, calendar: calendar),
isToday: calendar.isDateInToday(cell.date),
isSelected: selectedDate.map {
calendar.isDate(cell.date, inSameDayAs: $0)
} ?? false,
calendar: calendar
)
.onTapGesture { onTapDay(cell.date) }
@@ -84,6 +102,7 @@ private struct DayCellView: View {
let ranges: [SymptomRange]
let marks: DayMarks
let isToday: Bool
let isSelected: Bool
let calendar: Calendar
private var dayNumber: Int {
@@ -92,14 +111,20 @@ private struct DayCellView: View {
var body: some View {
ZStack(alignment: .top) {
// :
// :selected > today
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(isToday ? Tj.Palette.sand2 : Color.clear)
.fill(backgroundFill)
//
if isSelected {
RoundedRectangle(cornerRadius: 6, style: .continuous)
.strokeBorder(Tj.Palette.brick, lineWidth: 1.5)
}
VStack(spacing: 2) {
Text("\(dayNumber)")
.font(.system(size: 13,
weight: isToday ? .bold : .regular,
weight: (isToday || isSelected) ? .bold : .regular,
design: .default))
.foregroundStyle(textColor)
.padding(.top, 4)
@@ -145,10 +170,17 @@ private struct DayCellView: View {
private var textColor: Color {
if !cell.inCurrentMonth { return Tj.Palette.text3.opacity(0.5) }
if isSelected { return Tj.Palette.brick }
if isToday { return Tj.Palette.ink }
return Tj.Palette.text
}
private var backgroundFill: Color {
if isSelected { return Tj.Palette.brickSoft.opacity(0.5) }
if isToday { return Tj.Palette.sand2 }
return .clear
}
private func symptomBar(_ range: SymptomRange) -> some View {
let pos = range.position(cell.date, calendar: calendar)
let leadingRadius: CGFloat = (pos == .start || pos == .single) ? 3 : 0