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

@@ -10,6 +10,8 @@ struct CaptureReviewForm: View {
let warning: String?
let onSave: (ParsedReport) -> Void
let onCancel: () -> Void
/// assets () nil,banner
var onReanalyze: (() -> Void)? = nil
var body: some View {
ScrollView {
@@ -36,10 +38,22 @@ struct CaptureReviewForm: View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(Tj.Palette.amber)
Text(text)
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text2)
.fixedSize(horizontal: false, vertical: true)
VStack(alignment: .leading, spacing: 8) {
Text(text)
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text2)
.fixedSize(horizontal: false, vertical: true)
if let onReanalyze {
Button {
onReanalyze()
} label: {
Label("重新识别", systemImage: "arrow.clockwise")
.font(.system(size: 12, weight: .semibold))
}
.buttonStyle(.plain)
.foregroundStyle(Tj.Palette.ink)
}
}
Spacer(minLength: 0)
}
.padding(12)
@@ -53,7 +67,7 @@ struct CaptureReviewForm: View {
private var pageThumbnails: some View {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("已保存 \(assets.count) 页(端侧加密)")
sectionLabel(String(appLoc: "已保存 \(assets.count) 页(端侧加密)"))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(Array(assets.enumerated()), id: \.offset) { _, asset in
@@ -78,13 +92,13 @@ struct CaptureReviewForm: View {
private var metaSection: some View {
VStack(alignment: .leading, spacing: 12) {
sectionLabel("基本信息")
sectionLabel(String(appLoc: "基本信息"))
VStack(spacing: 10) {
labeledField("标题") {
labeledField(String(appLoc: "标题")) {
TextField("如:春季年度体检", text: $parsed.title)
.textFieldStyle(.plain)
}
labeledField("类型") {
labeledField(String(appLoc: "类型")) {
Picker("", selection: $parsed.typeRaw) {
ForEach(ReportType.allCases, id: \.rawValue) { t in
Text(t.label).tag(t.rawValue)
@@ -92,18 +106,18 @@ struct CaptureReviewForm: View {
}
.pickerStyle(.segmented)
}
labeledField("报告日期") {
labeledField(String(appLoc: "报告日期")) {
DatePicker("", selection: $parsed.reportDate,
in: ...Date.now,
displayedComponents: .date)
.datePickerStyle(.compact)
.labelsHidden()
.environment(\.locale, Locale(identifier: "zh_CN"))
.environment(\.locale, Locale.current)
}
labeledField("机构(可选)") {
labeledField(String(appLoc: "机构(可选)")) {
TextField("如:协和医院", text: $parsed.institution)
}
labeledField("摘要(可选)") {
labeledField(String(appLoc: "摘要(可选)")) {
TextField("一句话总结", text: $parsed.summary, axis: .vertical)
.lineLimit(1...3)
}
@@ -128,7 +142,7 @@ struct CaptureReviewForm: View {
private var indicatorSection: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
sectionLabel("指标(\(parsed.indicators.count) 项)")
sectionLabel(String(appLoc: "指标(\(parsed.indicators.count) 项)"))
Spacer()
Button {
parsed.indicators.append(