- 언어 설정 실적용: 변경 시 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
124 lines
3.4 KiB
Swift
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)
|
|
}
|
|
}
|