Files
kangkang/康康/Features/Trends/CalendarYearGrid.swift
link2026 d2c77d5c51 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>
2026-05-30 10:28:24 +08:00

112 lines
3.7 KiB
Swift

import SwiftUI
struct CalendarYearGrid: View {
let year: Int
let data: CalendarData
let onTapMonth: (Date) -> Void
private let calendar: Calendar = {
var c = Calendar(identifier: .gregorian)
c.firstWeekday = 2
c.locale = Locale.current
return c
}()
private var monthAnchors: [Date] {
(1...12).compactMap { m in
var comps = DateComponents()
comps.year = year; comps.month = m; comps.day = 1
return calendar.date(from: comps)
}
}
private let columns = Array(repeating: GridItem(.flexible(), spacing: 14), count: 3)
var body: some View {
LazyVGrid(columns: columns, spacing: 18) {
ForEach(monthAnchors, id: \.self) { anchor in
Button {
onTapMonth(anchor)
} label: {
MiniMonth(anchor: anchor, data: data, calendar: calendar)
}
.buttonStyle(.plain)
}
}
}
}
private struct MiniMonth: View {
let anchor: Date
let data: CalendarData
let calendar: Calendar
private var monthLabel: String {
anchor.formatted(.dateTime.month())
}
private var days: [Date] {
guard let interval = calendar.dateInterval(of: .month, for: anchor) else { return [] }
let count = calendar.dateComponents([.day], from: interval.start, to: interval.end).day ?? 30
return (0..<count).compactMap { calendar.date(byAdding: .day, value: $0, to: interval.start) }
}
private var leadingPadding: Int {
guard let first = days.first else { return 0 }
return (calendar.component(.weekday, from: first) - calendar.firstWeekday + 7) % 7
}
private let microColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 7)
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(monthLabel)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
LazyVGrid(columns: microColumns, spacing: 2) {
ForEach(0..<leadingPadding, id: \.self) { _ in
Color.clear.frame(height: 8)
}
ForEach(days, id: \.self) { d in
dot(for: d)
}
}
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
}
private func dot(for date: Date) -> some View {
let marks = data.marks(for: date, calendar: calendar)
let ranges = data.ranges(touching: date, calendar: calendar)
let color: Color = {
if marks.abnormalCount > 0 { return Tj.Palette.brick }
if let topSeverity = ranges.map(\.severity).max() {
switch topSeverity {
case 1, 2: return Tj.Palette.leaf
case 3: return Tj.Palette.amber
default: return Tj.Palette.brick
}
}
if marks.hasAnyEvent { return Tj.Palette.text3.opacity(0.6) }
return Tj.Palette.lineSoft
}()
let isToday = calendar.isDateInToday(date)
return RoundedRectangle(cornerRadius: 2, style: .continuous)
.fill(color)
.frame(height: 8)
.overlay(
RoundedRectangle(cornerRadius: 2, style: .continuous)
.strokeBorder(Tj.Palette.ink, lineWidth: isToday ? 1 : 0)
)
}
}