feat(l10n): localize CSV header row per language

- CSV 열 이름도 언어별 현지화 (파일명 현지화 b17a199의 후속, 사용자 결정) — 스프레드시트에서 열 이름은 곧 UI. 값의 rawValue(time/count, inProgress 등)는 데이터라 언어 무관 유지
- 기존 키 최대 재사용(이름/시각/수량/메모/방향/종료일/꼬리표/즐겨찾기/시작/종료 + '추적 방식' — ko 헤더만 '추적방식'→'추적 방식'으로 공백 하나 추가됨), 신규 10키(행동id/만든날/초/내용/시작일/상태/달성기준%/대상/주기/목표량) en/ja 완비 — 카탈로그 761키 missing/stale 0
- 검증(시뮬레이터 실헤더): en "id,Name,Tracking type,Tags,Favorite,Created" 등 5파일, ja "id,名前,計測方式,タグ,お気に入り,作成日" 등 5파일, ko 회귀 정상. Debug·Store 빌드 성공

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-16 10:28:53 +09:00
parent b17a199b53
commit bdc771455b
3 changed files with 195 additions and 19 deletions

View File

@ -225,7 +225,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
- `ExportSnapshot`: SwiftData 비의존 값 타입 (title/periodLabel/filterNote/hero/sections). 섹션 종류: timetable / records / bars / lines / table
- `ExportBuilder.history(...)` / `.stats(...)`: 화면과 동일한 집계 규칙으로 스냅숏 생성. `trimHours: false`면 기록이 없어도 24시간 전체 그리드(일기용)
- `injectingCalendarEvents(_:)`: 하루 타임테이블에 캘린더 블록 주입. `replacingTimetableSections(with:)`: 일기 필터용 — 타임테이블 섹션만 교체하고 나머지는 유지
- **내보내기 파일명은 언어별 현지화** — ko "하루다님-기록-…" / en "HaruDanim-History-…" / ja "HaruDanim-記録-…" (수신 환경이 한글을 지원하지 않을 수 있어 접두는 en/ja에서 라틴 통일). 리포트 PNG·일기 PDF/PNG·CSV 5종 모두 적용, 패턴은 카탈로그 포맷 키("하루다님-%@.png" 등). CSV 헤더는 데이터로 보고 한국어 유지
- **내보내기 파일명은 언어별 현지화** — ko "하루다님-기록-…" / en "HaruDanim-History-…" / ja "HaruDanim-記録-…" (수신 환경이 한글을 지원하지 않을 수 있어 접두는 en/ja에서 라틴 통일). 리포트 PNG·일기 PDF/PNG·CSV 5종 모두 적용, 패턴은 카탈로그 포맷 키("하루다님-%@.png" 등). CSV 헤더(열 이름)도 현지화 — 값의 rawValue(time/count, inProgress 등)는 데이터라 언어 무관 유지
- `ExportPosterView`: 고정폭 430pt, ImageRenderer scale 3 → 1290px PNG. 라이트/다크는 `.environment(\.colorScheme)` 강제. `ExportSectionView`/`ExportHeroRow`는 일기 요약 화면과 공용 — **화면과 내보내기 결과가 항상 일치**
- 일기 인쇄용: `DiaryPrintSummaryView`(768pt) + `DiaryPrintNotePageView`(768×1024, 라이트 고정)

View File

@ -2783,6 +2783,22 @@
}
}
},
"내용": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Title"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "内容"
}
}
}
},
"내용을 입력하세요": {
"localizations": {
"en": {
@ -3519,6 +3535,22 @@
}
}
},
"달성기준%": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Achieve threshold %"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "達成基準%"
}
}
}
},
"달성률 기간": {
"localizations": {
"en": {
@ -3616,6 +3648,22 @@
}
}
},
"대상": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Target"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "対象"
}
}
}
},
"대상 없음": {
"localizations": {
"en": {
@ -3968,6 +4016,22 @@
}
}
},
"만든날": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Created"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "作成日"
}
}
}
},
"매달": {
"localizations": {
"en": {
@ -4759,6 +4823,22 @@
}
}
},
"목표량": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Target amount"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標量"
}
}
}
},
"목표를 지금 종료할까요?": {
"localizations": {
"en": {
@ -5485,6 +5565,22 @@
}
}
},
"상태": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Status"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "状態"
}
}
}
},
"상품 정보를 불러오는 중...": {
"localizations": {
"en": {
@ -6415,6 +6511,22 @@
}
}
},
"시작일": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Start date"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "開始日"
}
}
}
},
"시작일 (과거 가능)": {
"localizations": {
"en": {
@ -8259,12 +8371,6 @@
},
"일기-%@~%@": {
"localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "일기-%1$@~%2$@"
}
},
"en": {
"stringUnit": {
"state": "translated",
@ -8276,6 +8382,12 @@
"state": "translated",
"value": "日記-%@~%@"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "일기-%1$@~%2$@"
}
}
}
},
@ -8983,6 +9095,22 @@
}
}
},
"주기": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Cycle"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "周期"
}
}
}
},
"주기마다 목표량 이상을 달성하는 것이 목표예요.": {
"localizations": {
"en": {
@ -9367,6 +9495,22 @@
}
}
},
"초": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Seconds"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "秒"
}
}
}
},
"총 측정 시간": {
"localizations": {
"en": {
@ -10058,12 +10202,6 @@
},
"통계-%@-%@": {
"localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "통계-%1$@-%2$@"
}
},
"en": {
"stringUnit": {
"state": "translated",
@ -10075,6 +10213,12 @@
"state": "translated",
"value": "統計-%@-%@"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "통계-%1$@-%2$@"
}
}
}
},
@ -11563,6 +11707,22 @@
}
}
},
"행동id": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "actionId"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "アクションID"
}
}
}
},
"행동·목표·다짐을 무제한으로 만들 수 있어요.": {
"localizations": {
"en": {

View File

@ -93,8 +93,12 @@ struct DataExportView: View {
isRendering = true
Task { @MainActor in
var urls: [URL?] = []
// ( )
urls.append(write(String(localized: "행동"), header: ["id", "이름", "추적방식", "꼬리표", "즐겨찾기", "만든날"],
// ·( ) ,
// UI. rawValue(time/count ) .
urls.append(write(String(localized: "행동"), header: [
"id", String(localized: "이름"), String(localized: "추적 방식"),
String(localized: "꼬리표"), String(localized: "즐겨찾기"), String(localized: "만든날"),
],
rows: actions.map { action in
[action.uuid.uuidString, action.name,
action.trackingType == .time ? "time" : "count",
@ -102,7 +106,10 @@ struct DataExportView: View {
action.isFavorite ? "true" : "false",
iso(action.createdAt)]
}))
urls.append(write(String(localized: "시간기록", comment: "CSV 파일명용 (공백 없이)"), header: ["행동id", "행동", "시작", "종료", "", "메모"],
urls.append(write(String(localized: "시간기록", comment: "CSV 파일명용 (공백 없이)"), header: [
String(localized: "행동id"), String(localized: "행동"), String(localized: "시작"),
String(localized: "종료"), String(localized: ""), String(localized: "메모"),
],
rows: sessions.compactMap { session in
guard let action = session.action else { return nil }
return [action.uuid.uuidString, action.name,
@ -111,19 +118,28 @@ struct DataExportView: View {
String(Int(session.duration())),
session.note]
}))
urls.append(write(String(localized: "횟수기록", comment: "CSV 파일명용 (공백 없이)"), header: ["행동id", "행동", "시각", "수량", "메모"],
urls.append(write(String(localized: "횟수기록", comment: "CSV 파일명용 (공백 없이)"), header: [
String(localized: "행동id"), String(localized: "행동"), String(localized: "시각"),
String(localized: "수량"), String(localized: "메모"),
],
rows: entries.compactMap { entry in
guard let action = entry.action else { return nil }
return [action.uuid.uuidString, action.name,
iso(entry.timestamp), String(entry.amount), entry.note]
}))
urls.append(write(String(localized: "목표"), header: ["id", "내용", "시작일", "종료일", "상태", "달성기준%"],
urls.append(write(String(localized: "목표"), header: [
"id", String(localized: "내용"), String(localized: "시작일"),
String(localized: "종료일"), String(localized: "상태"), String(localized: "달성기준%"),
],
rows: goals.map { goal in
[goal.uuid.uuidString, goal.title, iso(goal.startDate),
goal.endDate.map(iso) ?? "", goal.statusRaw,
String(goal.achieveThresholdPercent)]
}))
urls.append(write(String(localized: "다짐"), header: ["id", "목표", "대상", "주기", "목표량", "방향"],
urls.append(write(String(localized: "다짐"), header: [
"id", String(localized: "목표"), String(localized: "대상"),
String(localized: "주기"), String(localized: "목표량"), String(localized: "방향"),
],
rows: goals.flatMap(\.sortedQuests).map { quest in
[quest.uuid.uuidString, quest.goal?.title ?? "",
quest.targetName, quest.scheduleLabel,