mycode/myApp/HaruDanim/Haru_DanimWatchWidgets/ComplicationSupport.swift
songyc macbook a6c040cd6a feat(i18n): 전체 UI 다국어 지원 (한국어·영어·일본어)
- 언어 설정 실적용: 변경 시 AppleLanguages 오버라이드 저장, 재시작 안내 footer
- 자동 추출되지 않던 일반 문자열(탭 이름, 픽커 라벨, 상태 뱃지, 포매터,
  삼항 문구, 심볼 카테고리 등)을 String(localized:)로 전환
- 위젯·워치 앱·컴플리케이션 타깃에 Localizable.xcstrings 신설
  (중복 리소스 방지 pbxproj 예외 추가), xcstringstool sync로 키 추출
- 4개 카탈로그 전체 키(고유 430개)에 en/ja 번역 주입
- 영어의 긴 횟수 단위("times")가 잘리지 않도록 모음 탭 셀 축소 허용
- 검증: -AppleLanguages "(en)"/"(ja)"로 모음 탭·설정 화면 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-10 15:03:24 +09:00

124 lines
3.4 KiB
Swift

//
// ComplicationSupport.swift
// Haru_DanimWatchWidgetsExtension
//
// (CLAUDE.md §9.2)
// - App Group defaults
// - /
//
import AppIntents
import Foundation
import SwiftUI
import WidgetKit
// MARK: -
nonisolated enum ComplicationStore {
static let defaults: UserDefaults = UserDefaults(suiteName: "group.com.yechan.HaruDanim") ?? .standard
static func snapshot() -> WatchSnapshot? {
guard let data = defaults.data(forKey: WatchSync.cachedSnapshotKey) else { return nil }
return WatchSnapshot.decode(data)
}
}
// MARK: -
enum WatchSpanOption: String, AppEnum {
case day, week, month
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "기간")
static let caseDisplayRepresentations: [WatchSpanOption: DisplayRepresentation] = [
.day: "하루",
.week: "주간",
.month: "월간",
]
var label: String {
switch self {
case .day: return String(localized: "하루")
case .week: return String(localized: "주간")
case .month: return String(localized: "월간")
}
}
}
// MARK: - / ( )
struct WatchGoalEntity: AppEntity {
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "목표")
static let defaultQuery = WatchGoalEntityQuery()
let id: UUID
let title: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(title)")
}
}
struct WatchGoalEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [WatchGoalEntity] {
(ComplicationStore.snapshot()?.goals ?? [])
.filter { identifiers.contains($0.id) }
.map { WatchGoalEntity(id: $0.id, title: $0.title) }
}
func suggestedEntities() async throws -> [WatchGoalEntity] {
(ComplicationStore.snapshot()?.goals ?? [])
.map { WatchGoalEntity(id: $0.id, title: $0.title) }
}
}
struct WatchQuestEntity: AppEntity {
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "다짐")
static let defaultQuery = WatchQuestEntityQuery()
let id: UUID
let title: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(title)")
}
}
struct WatchQuestEntityQuery: EntityQuery {
private func all() -> [WatchQuestEntity] {
(ComplicationStore.snapshot()?.goals ?? []).flatMap { goal in
goal.quests.map { WatchQuestEntity(id: $0.id, title: "\(goal.title) · \($0.name)") }
}
}
func entities(for identifiers: [UUID]) async throws -> [WatchQuestEntity] {
all().filter { identifiers.contains($0.id) }
}
func suggestedEntities() async throws -> [WatchQuestEntity] {
all()
}
}
// MARK: -
struct ComplicationLockedView: View {
var body: some View {
VStack(spacing: 2) {
Image(systemName: "crown.fill")
Text("프리미엄")
.font(.system(size: 10))
}
}
}
struct ComplicationEmptyView: View {
let message: String
var body: some View {
Text(message)
.font(.system(size: 10))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}