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:
parent
b17a199b53
commit
bdc771455b
@ -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, 라이트 고정)
|
||||
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user