From bdc771455b9c127a9e533da5652393b03e456be1 Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Thu, 16 Jul 2026 10:28:53 +0900 Subject: [PATCH] feat(l10n): localize CSV header row per language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7 --- myApp/HaruDanim/CLAUDE.md | 2 +- myApp/HaruDanim/IOS/Localizable.xcstrings | 184 ++++++++++++++++-- .../HaruDanim/IOS/Views/DataExportView.swift | 28 ++- 3 files changed, 195 insertions(+), 19 deletions(-) diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index b2f3392..19d0500 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -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, 라이트 고정) diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index cade239..7ce4326 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -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": { diff --git a/myApp/HaruDanim/IOS/Views/DataExportView.swift b/myApp/HaruDanim/IOS/Views/DataExportView.swift index dd89ffa..e0cf2a4 100644 --- a/myApp/HaruDanim/IOS/Views/DataExportView.swift +++ b/myApp/HaruDanim/IOS/Views/DataExportView.swift @@ -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,