feat(l10n): localize export file names per language
- 내보내기 파일명을 언어별로 현지화 — 수신 환경(PC·클라우드·메일)이 한글 파일명을 지원하지 않을 수 있음 (사용자 결정). ko는 기존 그대로("하루다님-기록-…"), en/ja는 라틴 접두 "HaruDanim-" + 그 언어의 종류 단어 (History/記録, Diary/日記, Stats/統計, Actions/アクション 등)
- 대상 전부: 기록·통계 리포트 PNG(ExportSnapshot.fileName + 슬러그 3종), 일기 PDF·스티치 PNG(슬러그 3종 포함), CSV 5종(행동·목표·다짐은 기존 키 재사용, 시간기록·횟수기록 신규 키). CSV 헤더는 데이터로 보고 한국어 유지
- 카탈로그 신규 11키 en/ja 완비(751키 missing/stale 0), CLAUDE.md §7에 규칙 기재
- 검증(시뮬레이터 실파일명): en CSV 5종 "HaruDanim-Actions/TimeRecords/CountRecords/Goals/Quest.csv", en 리포트 "HaruDanim-History-2026-07-16.png", iPad en 일기 "HaruDanim-Diary-2026-07-15~2026-07-16.pdf"(기간 슬러그), ko 회귀 "하루다님-행동.csv" 등 5종 그대로. 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
4e07a90a1b
commit
b17a199b53
@ -225,6 +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 헤더는 데이터로 보고 한국어 유지
|
||||
- `ExportPosterView`: 고정폭 430pt, ImageRenderer scale 3 → 1290px PNG. 라이트/다크는 `.environment(\.colorScheme)` 강제. `ExportSectionView`/`ExportHeroRow`는 일기 요약 화면과 공용 — **화면과 내보내기 결과가 항상 일치**
|
||||
- 일기 인쇄용: `DiaryPrintSummaryView`(768pt) + `DiaryPrintNotePageView`(768×1024, 라이트 고정)
|
||||
|
||||
|
||||
@ -2105,6 +2105,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"기록-%@": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "History-%@"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "記録-%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"기록-주간-%@": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "History-Weekly-%@"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "記録-週間-%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"기록·통계 탭": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -6254,6 +6286,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"시간기록": {
|
||||
"comment": "CSV 파일명용 (공백 없이)",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "TimeRecords"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "時間記録"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"시간을 측정 중이면 다이나믹 아일랜드와 잠금화면에 진행 시간이 표시돼요. 여러 개를 동시에 측정하면 설정의 '대표 시간 기준'(먼저/나중에 시작한 행동)에 따라 하나가 대표로 보이고 나머지는 +N으로 표시돼요.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -8192,6 +8241,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"일기-%@": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Diary-%@"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "日記-%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"일기-%@~%@": {
|
||||
"localizations": {
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "일기-%1$@~%2$@"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Diary-%@~%@"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "日記-%@~%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"일기는 프리미엄 기능이에요": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -9969,6 +10056,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"통계-%@-%@": {
|
||||
"localizations": {
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "통계-%1$@-%2$@"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Stats-%@-%@"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "統計-%@-%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"특정 기간": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -10786,6 +10895,70 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"하루다님-%@.csv": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-%@.csv"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-%@.csv"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"하루다님-%@.pdf": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-%@.pdf"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-%@.pdf"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"하루다님-%@.png": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-%@.png"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-%@.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"하루다님-일기-%@.png": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-Diary-%@.png"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "HaruDanim-日記-%@.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"하루의 기록·통계와 함께 애플 펜슬로 쓰는\n나만의 다이어리를 만들 수 있어요.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -11982,6 +12155,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"횟수기록": {
|
||||
"comment": "CSV 파일명용 (공백 없이)",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "CountRecords"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "回数記録"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "1.0"
|
||||
|
||||
@ -93,7 +93,8 @@ struct DataExportView: View {
|
||||
isRendering = true
|
||||
Task { @MainActor in
|
||||
var urls: [URL?] = []
|
||||
urls.append(write("행동", header: ["id", "이름", "추적방식", "꼬리표", "즐겨찾기", "만든날"],
|
||||
// 파일명은 언어별 현지화 (수신 환경이 한글을 지원하지 않을 수 있음 — 헤더는 데이터라 유지)
|
||||
urls.append(write(String(localized: "행동"), header: ["id", "이름", "추적방식", "꼬리표", "즐겨찾기", "만든날"],
|
||||
rows: actions.map { action in
|
||||
[action.uuid.uuidString, action.name,
|
||||
action.trackingType == .time ? "time" : "count",
|
||||
@ -101,7 +102,7 @@ struct DataExportView: View {
|
||||
action.isFavorite ? "true" : "false",
|
||||
iso(action.createdAt)]
|
||||
}))
|
||||
urls.append(write("시간기록", header: ["행동id", "행동", "시작", "종료", "초", "메모"],
|
||||
urls.append(write(String(localized: "시간기록", comment: "CSV 파일명용 (공백 없이)"), header: ["행동id", "행동", "시작", "종료", "초", "메모"],
|
||||
rows: sessions.compactMap { session in
|
||||
guard let action = session.action else { return nil }
|
||||
return [action.uuid.uuidString, action.name,
|
||||
@ -110,19 +111,19 @@ struct DataExportView: View {
|
||||
String(Int(session.duration())),
|
||||
session.note]
|
||||
}))
|
||||
urls.append(write("횟수기록", header: ["행동id", "행동", "시각", "수량", "메모"],
|
||||
urls.append(write(String(localized: "횟수기록", comment: "CSV 파일명용 (공백 없이)"), header: ["행동id", "행동", "시각", "수량", "메모"],
|
||||
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("목표", header: ["id", "내용", "시작일", "종료일", "상태", "달성기준%"],
|
||||
urls.append(write(String(localized: "목표"), header: ["id", "내용", "시작일", "종료일", "상태", "달성기준%"],
|
||||
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("다짐", header: ["id", "목표", "대상", "주기", "목표량", "방향"],
|
||||
urls.append(write(String(localized: "다짐"), header: ["id", "목표", "대상", "주기", "목표량", "방향"],
|
||||
rows: goals.flatMap(\.sortedQuests).map { quest in
|
||||
[quest.uuid.uuidString, quest.goal?.title ?? "",
|
||||
quest.targetName, quest.scheduleLabel,
|
||||
@ -162,7 +163,7 @@ struct DataExportView: View {
|
||||
// 엑셀이 UTF-8 한글을 바로 인식하도록 BOM을 붙인다
|
||||
let content = "\u{FEFF}" + lines.joined(separator: "\n")
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("하루다님-\(name).csv")
|
||||
.appendingPathComponent(String(localized: "하루다님-\(name).csv"))
|
||||
do {
|
||||
try content.write(to: url, atomically: true, encoding: .utf8)
|
||||
return url
|
||||
|
||||
@ -220,9 +220,10 @@ struct DiaryExportSheet: View {
|
||||
|
||||
private var fileSlug: String {
|
||||
let entries = targetEntries
|
||||
guard let first = entries.first else { return "일기" }
|
||||
if entries.count == 1 { return "일기-\(slugDate(first.dayKey))" }
|
||||
return "일기-\(slugDate(first.dayKey))~\(slugDate(entries.last!.dayKey))"
|
||||
// 파일명은 언어별 현지화 (ExportSnapshot.fileName과 같은 이유)
|
||||
guard let first = entries.first else { return String(localized: "일기") }
|
||||
if entries.count == 1 { return String(localized: "일기-\(slugDate(first.dayKey))") }
|
||||
return String(localized: "일기-\(slugDate(first.dayKey))~\(slugDate(entries.last!.dayKey))")
|
||||
}
|
||||
|
||||
/// 렌더 대상 날짜(논리적 하루)에 겹치는 기록만 그때그때 조회 — 전량 @Query 로드 방지
|
||||
@ -306,7 +307,7 @@ struct DiaryExportSheet: View {
|
||||
@MainActor
|
||||
private func renderPDF(_ entries: [DiaryEntry]) -> URL? {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("하루다님-\(fileSlug).pdf")
|
||||
.appendingPathComponent(String(localized: "하루다님-\(fileSlug).pdf"))
|
||||
let bounds = CGRect(origin: .zero, size: DiaryPageMetrics.size)
|
||||
let renderer = UIGraphicsPDFRenderer(bounds: bounds)
|
||||
do {
|
||||
@ -353,7 +354,7 @@ struct DiaryExportSheet: View {
|
||||
}
|
||||
guard let data = stitched.pngData() else { return nil }
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("하루다님-일기-\(slugDate(entry.dayKey)).png")
|
||||
.appendingPathComponent(String(localized: "하루다님-일기-\(slugDate(entry.dayKey)).png"))
|
||||
try? data.write(to: url, options: .atomic)
|
||||
return url
|
||||
}
|
||||
|
||||
@ -32,7 +32,9 @@ struct ExportSnapshot {
|
||||
/// 파일명용 (예: "기록-2026-07-13")
|
||||
var fileSlug: String
|
||||
|
||||
var fileName: String { "하루다님-\(fileSlug).png" }
|
||||
// 파일명은 언어별로 현지화 — 수신 환경(PC·클라우드)이 한글을 지원하지 않을 수 있어
|
||||
// en/ja는 라틴 접두 "HaruDanim-"과 그 언어의 종류 단어를 쓴다 (fileSlug도 현지화됨)
|
||||
var fileName: String { String(localized: "하루다님-\(fileSlug).png") }
|
||||
|
||||
/// 하루짜리 타임테이블 섹션에 캘린더 일정 블록을 끼워 넣는다 (일기 화면·내보내기 공용)
|
||||
func injectingCalendarEvents(_ events: [ExportTimetableData.Block]) -> ExportSnapshot {
|
||||
@ -272,7 +274,7 @@ enum ExportBuilder {
|
||||
filterNote: note,
|
||||
hero: recordHero(segments: segments, counts: counts),
|
||||
sections: sections,
|
||||
fileSlug: "기록-주간-\(slugDate(keys.first ?? dayKey))"
|
||||
fileSlug: String(localized: "기록-주간-\(slugDate(keys.first ?? dayKey))")
|
||||
)
|
||||
}
|
||||
|
||||
@ -326,7 +328,7 @@ enum ExportBuilder {
|
||||
filterNote: note,
|
||||
hero: recordHero(segments: segments, counts: counts),
|
||||
sections: sections,
|
||||
fileSlug: "기록-\(slugDate(dayKey))"
|
||||
fileSlug: String(localized: "기록-\(slugDate(dayKey))")
|
||||
)
|
||||
}
|
||||
|
||||
@ -761,7 +763,7 @@ enum ExportBuilder {
|
||||
filterNote: note,
|
||||
hero: hero,
|
||||
sections: sections,
|
||||
fileSlug: "통계-\(span.rawValue)-\(slugDate(anchorDayKey))"
|
||||
fileSlug: String(localized: "통계-\(span.rawValue)-\(slugDate(anchorDayKey))")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user