diff --git a/myApp/Keging/CLAUDE.md b/myApp/Keging/CLAUDE.md index cd16ff2..9ec5de2 100644 --- a/myApp/Keging/CLAUDE.md +++ b/myApp/Keging/CLAUDE.md @@ -19,8 +19,8 @@ | 번들 ID | `com.yechan.Keging` / 워치 `.watchkitapp` / 위젯 `.KegingWidgets` / App Group `group.com.yechan.Keging` | | 최소 OS | iOS 18.0 / watchOS 10.0 | | 수익 모델 | **완전 무료** — 광고·결제 일절 없음. CloudKit·HealthKit 등 외부 서비스 없음 | -| 언어 | 한국어 하드코딩 (String Catalog 미사용 — 다국어는 필요해지면 도입) | -| 현재 상태 (2026-08-29) | **1.0 전 기능 구현 완료, 시뮬레이터 검증 통과** — Debug·Release·워치 빌드 그린, 화면 QA 10장(라이트/다크/iOS 18.5/워치 46·40mm), 완주·기록 저장 경로 확인. **실기기 테스트 대기(사용자)**: 진동 패턴(포그라운드·백그라운드), 다이나믹 아일랜드, 워치 페어링 동기화(세팅 폰→워치, 기록 워치→폰), 단축어, 워치 손목 내림(확장 런타임)은 시뮬레이터로 검증 불가 | +| 언어 | **한국어(원문)/영어/일본어** — String Catalog(타깃별 `Localizable.xcstrings`, InfoPlist 포함). 설정 → 언어(시스템/한국어/English/日本語)에서 앱 내 전환: `AppleLanguages` 오버라이드, **완전 종료 후 재실행 시 적용**(하루 다님 검증 방식). 워치·Live Activity·단축어 표기는 각자의 시스템 언어를 따름 | +| 현재 상태 (2026-08-29) | **1.0 전 기능 구현 완료(도움말·3개 언어 포함), 시뮬레이터 검증 통과** — Debug·Release·워치 빌드 그린, 화면 QA(라이트/다크/iOS 18.5/워치 46·40mm + en/ja 5화면), 전 카탈로그 missing·stale 0, 완주·기록 저장 경로 확인. **실기기 테스트 대기(사용자)**: 진동 패턴(포그라운드·백그라운드), 다이나믹 아일랜드, 워치 페어링 동기화(세팅 폰→워치, 기록 워치→폰), 단축어, 워치 손목 내림(확장 런타임)은 시뮬레이터로 검증 불가 | ## 2. 빌드·검증 환경 @@ -35,6 +35,7 @@ IOS/ # iOS 앱 타깃 (Keging) TimerSettingsView.swift # 세팅 (수축/이완/횟수 Stepper + 진동 패턴 Picker + 미리 느끼기) AppSettingsView.swift # 설정 (테마 시스템/라이트/다크 — app.theme, 버전) StatsView.swift # 통계 (하루/주간/월간, 달력↔롤링, Swift Charts 꺾은선) + HelpView.swift # 도움말 (첫 화면 ? 버튼 — 5그룹 13주제 전 기능 사용법) WorkoutCoordinator.swift # 엔진↔진동·Live Activity·백그라운드 오디오·화면 꺼짐 방지 배선 Haptics.swift # 진동 재생 (AudioServices 바이브 연타 — 백그라운드에서도 동작) BackgroundAudioKeeper.swift # 운동 중 무음 오디오 루프 (백그라운드 타이머 유지, mixWithOthers) @@ -43,6 +44,8 @@ IOS/ # iOS 앱 타깃 (Keging) KegingIntents.swift # 단축어 "케겔 운동 시작" (openAppWhenRun + 알림/플래그) DebugSeed.swift # DEBUG 런치 인자 (§7) Info.plist # UIBackgroundModes=audio (GENERATE_INFOPLIST_FILE와 병합) + Localizable.xcstrings # ko(원문)/en/ja 문자열 카탈로그 (워치·위젯 폴더에도 각자 있음) + InfoPlist.xcstrings # CFBundleDisplayName 케깅/Keging (워치 폴더에도 있음) Shared/ # 세 타깃 공용 (pbxproj fileSystemSynchronizedGroups에 3타깃 모두 등록) Theme.swift # 하루 다님 팔레트 (§5), Color(hex:)·Color(light:dark:) AppGroup.swift # App Group defaults 단일 인스턴스 + 컨테이너 URL @@ -81,6 +84,23 @@ xcodebuild -project Keging.xcodeproj -scheme "KegingWatch Watch App" \ - 각 타깃은 `GENERATE_INFOPLIST_FILE=YES` + `INFOPLIST_FILE`(추가 키 병합) 조합. 폴더 안 Info.plist는 exception set(`membershipExceptions`)으로 리소스 복사에서 제외돼 있다 — **Info.plist를 옮기거나 새 타깃 폴더에 추가하면 이 제외도 같이 챙길 것** - 위젯 타깃만 `SWIFT_DEFAULT_ACTOR_ISOLATION` 미설정(=nonisolated 기본) — Shared의 데이터 타입(WorkoutConfig·SessionRecord·KegelPhase·HapticPattern·ActivityAttributes)은 **`nonisolated`로 명시**해 타깃 간 격리 차이·Swift 6 경고를 차단해 뒀다. 새 공용 데이터 타입도 동일하게 +### 2.4 다국어(l10n) 루틴 — 하루 다님 방식 계승 + +새 사용자 문구는 SwiftUI 리터럴(`Text("…")` 등) 또는 `String(localized:)`로만 쓴다(변수 String은 추출 안 됨 — a11y 라벨은 `Text(...)` 오버로드, 커스텀 뷰 파라미터는 `Text`/`LocalizedStringKey` 타입으로). 추가 후: + +```zsh +# 1) iOS 빌드 + 워치 스킴 빌드 → 2) 타깃별 stringsdata만 카탈로그에 sync (zsh 배열 ${(f)...} 필수) +DD=~/Library/Developer/Xcode/DerivedData/Keging-<해시> # ⚠️ DD가 2개면 최신(빌드 산출물 있는 쪽) 확인 +app_files=(${(f)"$(find $DD -name '*.stringsdata' -path '*Debug-iphonesimulator/Keging.build/Objects-normal/arm64/*')"}) +xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${app_files[@]}" +# 위젯: KegingWidgetsExtension.build, 워치: 'Debug-watchsimulator/KegingWatch Watch App.build' 경로로 동일하게 +# 3) python으로 en/ja 채움 → 4) sync 재실행(정규화) → 5) 전 카탈로그 missing·stale 0 확인 +``` + +- ⚠️ 카탈로그는 타깃별 — Shared 문자열도 워치·위젯 타깃에서 쓰이면 그 타깃 카탈로그에 각각 추출된다. 세 카탈로그 모두 확인할 것 +- 문구 수정 시 옛 키가 stale로 남음 — sync 후 삭제. 복수형은 en에 plural variations("%lld회" 참고) +- 용어 통일: 수축=Squeeze/締める, 이완=Relax/ゆるめる, 세트=Set/セット, 횟수=Reps/回数. 말투는 ko ~해요체 / ja です·ます체 + ## 3. 도메인·핵심 규칙 ### 3.1 세트와 엔진 (`Shared/KegelEngine.swift`) @@ -115,8 +135,8 @@ xcodebuild -project Keging.xcodeproj -scheme "KegingWatch Watch App" \ ## 4. 화면 구성 (탭 없음 — 애플 기본 앱 느낌) -- **첫 화면**: 중앙 큰 시작 버튼(그린 그라데이션 원) + 좌상단 설정(gear)·우상단 세팅(slider) + 상단 중앙 통계 그랩 힌트 + 버튼 아래 현재 세팅 요약(탭=세팅). 버튼은 이 3개가 전부 -- **세팅/설정**: 시트(Form). 운동 진행: fullScreenCover — 수축 때 원이 오므라들고 이완 때 부풀어 오르는 애니메이션 +- **첫 화면**: 중앙 큰 시작 버튼(그린 그라데이션 원) + 좌상단 설정(gear)·우상단 세팅(slider)·**우하단 도움말(?)** + 상단 중앙 통계 그랩 힌트 + 버튼 아래 현재 세팅 요약(탭=세팅) +- **세팅/설정/도움말**: 시트(Form/List). 설정에 테마·**언어**(시스템/한국어/English/日本語 — 재실행 시 적용) 섹션. 도움말은 5그룹 13주제(기본 사용법·통계·앱 밖에서·애플워치·설정 기타)로 전 기능 사용법 서술. 운동 진행: fullScreenCover — 수축 때 원이 오므라들고 이완 때 부풀어 오르는 애니메이션 - 아이콘 전용 버튼에는 `accessibilityLabel` 필수 (하루 다님 컨벤션 계승) ## 5. 테마·컬러 — 하루 다님 팔레트 계승 (`Shared/Theme.swift`) @@ -146,8 +166,11 @@ xcodebuild -project Keging.xcodeproj -scheme "KegingWatch Watch App" \ | `-openStats` | 시작하자마자 통계 화면 | | `-statsScope day\|week\|month` | 통계 초기 탭 지정 | | `-statsRolling` | 주간·월간을 롤링 모드로 | +| `-openHelp` / `-openSettings` / `-openTimerSettings` | 해당 시트를 열고 시작 (화면 QA용) | | `-autoStart` | 시작하자마자 운동 시작 (아이폰·워치 공통) | | `-quickConfig` | 세팅을 수축1초·이완1초·2회로 (완주 경로 검증) | +| `-resetConfig` | 세팅을 기본값(10초·5초·20회)으로 복원 | +| `-AppleLanguages "(en)"` | (시스템 표준 인자) 그 실행만 언어 강제 — en/ja 렌더 QA용, 워치도 동일 | ## 8. 남은 작업·보류 diff --git a/myApp/Keging/IOS/AppSettingsView.swift b/myApp/Keging/IOS/AppSettingsView.swift index 095984e..047bc4e 100644 --- a/myApp/Keging/IOS/AppSettingsView.swift +++ b/myApp/Keging/IOS/AppSettingsView.swift @@ -9,6 +9,7 @@ import SwiftUI struct AppSettingsView: View { @AppStorage("app.theme", store: AppGroup.defaults) private var themeMode = "system" + @AppStorage("app.language", store: AppGroup.defaults) private var appLanguage = "system" @Environment(\.dismiss) private var dismiss var body: some View { @@ -22,6 +23,18 @@ struct AppSettingsView: View { } .pickerStyle(.segmented) } + Section { + Picker("언어", selection: $appLanguage) { + Text("시스템").tag("system") + Text(verbatim: "한국어").tag("ko") + Text(verbatim: "English").tag("en") + Text(verbatim: "日本語").tag("ja") + } + } header: { + Text("언어") + } footer: { + Text("언어를 바꾸면 앱을 완전히 종료했다가 다시 실행했을 때 적용돼요.") + } Section { LabeledContent("버전", value: versionText) } footer: { @@ -35,6 +48,14 @@ struct AppSettingsView: View { Button("완료") { dismiss() } } } + .onChange(of: appLanguage) { + // 시스템의 앱별 언어 오버라이드 — 다음 실행부터 적용 (하루 다님 검증 방식) + if appLanguage == "system" { + UserDefaults.standard.removeObject(forKey: "AppleLanguages") + } else { + UserDefaults.standard.set([appLanguage], forKey: "AppleLanguages") + } + } } } diff --git a/myApp/Keging/IOS/ContentView.swift b/myApp/Keging/IOS/ContentView.swift index 9969d42..66c1f0f 100644 --- a/myApp/Keging/IOS/ContentView.swift +++ b/myApp/Keging/IOS/ContentView.swift @@ -13,6 +13,7 @@ struct ContentView: View { @State private var showTimerSettings = false @State private var showAppSettings = false + @State private var showHelp = false @State private var showStats = false var body: some View { @@ -27,6 +28,7 @@ struct ContentView: View { } .sheet(isPresented: $showTimerSettings) { TimerSettingsView() } .sheet(isPresented: $showAppSettings) { AppSettingsView() } + .sheet(isPresented: $showHelp) { HelpView() } .fullScreenCover(isPresented: workoutPresented) { WorkoutView() } .onReceive(NotificationCenter.default.publisher(for: .startWorkoutRequested)) { _ in startFromShortcut() @@ -35,6 +37,9 @@ struct ContentView: View { if AppLaunchState.shared.startRequested { startFromShortcut() } #if DEBUG if CommandLine.arguments.contains("-openStats") { showStats = true } + if CommandLine.arguments.contains("-openHelp") { showHelp = true } + if CommandLine.arguments.contains("-openSettings") { showAppSettings = true } + if CommandLine.arguments.contains("-openTimerSettings") { showTimerSettings = true } if CommandLine.arguments.contains("-autoStart") { WorkoutCoordinator.shared.startWorkout() } #endif } @@ -45,7 +50,7 @@ struct ContentView: View { private var mainScreen: some View { VStack(spacing: 0) { HStack(alignment: .top) { - cornerButton(symbol: "gearshape.fill", label: "설정") { showAppSettings = true } + cornerButton(symbol: "gearshape.fill", label: Text("설정")) { showAppSettings = true } Spacer() Button { openStats() @@ -59,9 +64,9 @@ struct ContentView: View { .padding(.top, 10) } .buttonStyle(.plain) - .accessibilityLabel("통계 보기") + .accessibilityLabel(Text("통계 보기")) Spacer() - cornerButton(symbol: "slider.horizontal.3", label: "운동 세팅") { showTimerSettings = true } + cornerButton(symbol: "slider.horizontal.3", label: Text("운동 세팅")) { showTimerSettings = true } } .padding(.horizontal, 20) .padding(.top, 8) @@ -74,7 +79,7 @@ struct ContentView: View { startButtonLabel } .buttonStyle(.plain) - .accessibilityLabel("케겔 운동 시작") + .accessibilityLabel(Text("케겔 운동 시작")) Button { showTimerSettings = true @@ -85,10 +90,17 @@ struct ContentView: View { } .buttonStyle(.plain) .padding(.top, 30) - .accessibilityLabel("운동 세팅 열기 — 현재 \(settings.config.summaryText)") + .accessibilityLabel(Text("운동 세팅 열기 — 현재 \(settings.config.summaryText)")) Spacer() Spacer() + + HStack { + Spacer() + cornerButton(symbol: "questionmark", label: Text("도움말")) { showHelp = true } + } + .padding(.horizontal, 20) + .padding(.bottom, 8) } .contentShape(Rectangle()) .gesture( @@ -134,7 +146,7 @@ struct ContentView: View { Color(light: .white, dark: Color(hex: "#0F2018")) } - private func cornerButton(symbol: String, label: String, action: @escaping () -> Void) -> some View { + private func cornerButton(symbol: String, label: Text, action: @escaping () -> Void) -> some View { Button(action: action) { Image(systemName: symbol) .font(.title3) diff --git a/myApp/Keging/IOS/DebugSeed.swift b/myApp/Keging/IOS/DebugSeed.swift index d8f7264..dfa1580 100644 --- a/myApp/Keging/IOS/DebugSeed.swift +++ b/myApp/Keging/IOS/DebugSeed.swift @@ -17,6 +17,10 @@ enum DebugSeed { if CommandLine.arguments.contains("-quickConfig") { SettingsStore.shared.config = WorkoutConfig(contractSeconds: 1, relaxSeconds: 1, reps: 2) } + // 기본 세팅 복원 + if CommandLine.arguments.contains("-resetConfig") { + SettingsStore.shared.config = WorkoutConfig() + } } private static func seedStats() { diff --git a/myApp/Keging/IOS/HelpView.swift b/myApp/Keging/IOS/HelpView.swift new file mode 100644 index 0000000..3abefaa --- /dev/null +++ b/myApp/Keging/IOS/HelpView.swift @@ -0,0 +1,133 @@ +// +// HelpView.swift +// Keging +// +// 도움말 — 앱의 모든 기능과 사용법. 첫 화면 물음표 버튼으로 진입. +// + +import SwiftUI + +struct HelpView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + List { + Section { + helpRow( + symbol: "play.circle.fill", + title: Text("바로 시작하기"), + body: Text("첫 화면의 큰 시작 버튼을 누르면 저장된 세팅대로 바로 운동이 시작돼요. 화면의 원이 수축할 때 함께 조이고, 커질 때 힘을 풀면 돼요. 정해진 횟수를 모두 마치면 완료 화면이 뜨고 기록이 저장돼요.") + ) + helpRow( + symbol: "slider.horizontal.3", + title: Text("운동 세팅 바꾸기"), + body: Text("오른쪽 위 슬라이더 버튼이 운동 세팅이에요. 수축 시간(1~60초), 이완 시간(1~60초), 반복 횟수(1~100회)를 정할 수 있어요. 예를 들어 수축 10초·이완 5초·20회로 설정하면, 수축과 이완을 20번 반복하는 한 세트가 약 5분이 돼요. 시작 버튼 아래의 요약 문구를 눌러도 이 화면이 열려요.") + ) + helpRow( + symbol: "iphone.radiowaves.left.and.right", + title: Text("진동으로 구분하기"), + body: Text("수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요. 그래서 화면을 보지 않아도 지금 조여야 할지 풀어야 할지 알 수 있어요. 진동 패턴은 운동 세팅에서 국면별로 고를 수 있고(한 번·두 번·세 번·심장 박동), '미리 느끼기' 버튼으로 실제 진동을 확인할 수 있어요. 진동은 실기기에서만 느껴져요.") + ) + helpRow( + symbol: "stop.circle", + title: Text("중간에 중단하면"), + body: Text("운동 중 아래의 중단 버튼을 누르면 확인 후 세트가 끝나요. 중단한 세트는 아예 하지 않은 것으로 처리돼서 기록과 통계에 남지 않아요. 끝까지 마친 세트만 기록돼요.") + ) + } header: { + Text("기본 사용법") + } + + Section { + helpRow( + symbol: "chart.xyaxis.line", + title: Text("통계 열기"), + body: Text("첫 화면을 아래로 쓸어내리거나 위쪽 가운데의 손잡이를 누르면 통계 화면이 열려요. 닫을 때는 위로 쓸어올리거나 X 버튼을 누르면 돼요.") + ) + helpRow( + symbol: "calendar", + title: Text("하루 · 주간 · 월간"), + body: Text("하루 탭에서는 오늘 몇 시에 몇 회를 했는지 목록과 시간대별 꺾은선 그래프로 보여줘요. 주간·월간 탭에서는 일별 횟수를 꺾은선 그래프와 합계(세트·횟수)로 보여줘요. 주는 월요일에 시작해요.") + ) + helpRow( + symbol: "arrow.left.arrow.right", + title: Text("이번 주 ↔ 지난 7일 전환"), + body: Text("주간·월간 화면 위의 스위치로 두 가지 방식을 오갈 수 있어요. '이번 주 / 이번 달'은 달력 기준(월요일~일요일, 1일~말일)이고, '지난 7일 / 지난 30일'은 오늘을 기준으로 거슬러 세는 방식이에요.") + ) + } header: { + Text("통계") + } + + Section { + helpRow( + symbol: "bolt.horizontal.circle", + title: Text("다이나믹 아일랜드"), + body: Text("운동 중에 앱을 나가면 다이나믹 아일랜드에 현재 몇 번째인지와 수축·이완 상태가 떠요. 길게 누르면 남은 시간과 진행 바가 보이는 큰 화면이 열려요. 진동도 계속 울리니까 다른 앱을 쓰면서도 운동을 이어갈 수 있어요.") + ) + helpRow( + symbol: "square.and.arrow.up", + title: Text("단축어와 시리"), + body: Text("단축어 앱에서 '케겔 운동 시작' 동작을 추가할 수 있어요. 실행하면 케깅이 열리면서 저장된 세팅대로 바로 시작돼요. 홈 화면 아이콘·잠금화면 위젯·뒷면 탭 등 단축어를 부를 수 있는 곳이라면 어디서든 쓸 수 있어요.") + ) + } header: { + Text("앱 밖에서") + } + + Section { + helpRow( + symbol: "applewatch", + title: Text("애플워치에서 운동하기"), + body: Text("워치 앱을 열면 시작 버튼 하나만 있어요. 누르면 바로 시작되고, 화면에 수축·이완 상태와 남은 시간, 몇 번째인지가 한눈에 보여요. 워치에서 실행하면 진동은 워치에서만 울려요. 중단 버튼을 누르면 그 세트는 기록되지 않아요.") + ) + helpRow( + symbol: "arrow.triangle.2.circlepath", + title: Text("워치와 아이폰 동기화"), + body: Text("운동 세팅은 아이폰에서만 바꿀 수 있고, 바꾸면 워치에 자동으로 전달돼요. 워치에서 끝까지 마친 세트는 아이폰으로 넘어와 통계에 합쳐져요. 워치가 아이폰과 떨어져 있어도 기록은 쌓였다가 연결되면 자동으로 전송돼요.") + ) + } header: { + Text("애플워치") + } + + Section { + helpRow( + symbol: "paintbrush", + title: Text("테마와 언어"), + body: Text("왼쪽 위 톱니바퀴 버튼이 설정이에요. 화면 모드(시스템·라이트·다크)와 언어(시스템·한국어·English·日本語)를 고를 수 있어요. 언어는 앱을 완전히 종료했다가 다시 실행하면 적용돼요. 워치는 워치의 시스템 언어를 따라요.") + ) + helpRow( + symbol: "lock.shield", + title: Text("데이터와 개인정보"), + body: Text("케깅은 광고와 결제가 없는 완전 무료 앱이에요. 모든 기록은 이 기기(그리고 워치)에만 저장되고, 외부 서버로 전송되지 않아요.") + ) + } header: { + Text("설정·기타") + } + } + .navigationTitle("도움말") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("완료") { dismiss() } + } + } + } + } + + private func helpRow(symbol: String, title: Text, body: Text) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 8) { + Image(systemName: symbol) + .font(.body) + .foregroundStyle(AppTheme.green) + .frame(width: 24) + title + .font(.headline) + } + body + .font(.subheadline) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.vertical, 4) + } +} diff --git a/myApp/Keging/IOS/InfoPlist.xcstrings b/myApp/Keging/IOS/InfoPlist.xcstrings new file mode 100644 index 0000000..06afc2b --- /dev/null +++ b/myApp/Keging/IOS/InfoPlist.xcstrings @@ -0,0 +1,29 @@ +{ + "sourceLanguage" : "ko", + "strings" : { + "CFBundleDisplayName" : { + "extractionState" : "manual", + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케깅" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/myApp/Keging/IOS/Localizable.xcstrings b/myApp/Keging/IOS/Localizable.xcstrings new file mode 100644 index 0000000..997bab2 --- /dev/null +++ b/myApp/Keging/IOS/Localizable.xcstrings @@ -0,0 +1,1616 @@ +{ + "sourceLanguage": "ko", + "strings": { + "%lld / %lld": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "%1$lld / %2$lld" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld / %lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld / %lld" + } + } + } + }, + "%lld분": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld min" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld分" + } + } + } + }, + "%lld분 %lld초": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "%1$lld분 %2$lld초" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld min %lld s" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld分%lld秒" + } + } + } + }, + "%lld시": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld:00" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld時" + } + } + } + }, + "%lld초": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld s" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld秒" + } + } + } + }, + "%lld회": { + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld rep" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld reps" + } + } + } + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld回" + } + } + } + }, + "%lld회를 모두 마쳤어요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You completed all %lld reps." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld回すべて完了しました。" + } + } + } + }, + "계속하기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep Going" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "続ける" + } + } + } + }, + "기간": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Period" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "期間" + } + } + } + }, + "기본 사용법": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Basics" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "基本の使い方" + } + } + } + }, + "날짜": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Date" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "日付" + } + } + } + }, + "다이나믹 아일랜드": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dynamic Island" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Dynamic Island" + } + } + } + }, + "다크": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dark" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ダーク" + } + } + } + }, + "단축어 앱에서 '케겔 운동 시작' 동작을 추가할 수 있어요. 실행하면 케깅이 열리면서 저장된 세팅대로 바로 시작돼요. 홈 화면 아이콘·잠금화면 위젯·뒷면 탭 등 단축어를 부를 수 있는 곳이라면 어디서든 쓸 수 있어요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "In the Shortcuts app, you can add the 'Start Kegel Workout' action. Running it opens Keging and starts right away with your saved setup. Use it anywhere shortcuts can be triggered — a Home Screen icon, a Lock Screen widget, Back Tap, and more." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ショートカットアプリで「ケーゲル運動を開始」アクションを追加できます。実行するとKegingが開き、保存した設定ですぐに始まります。ホーム画面アイコン・ロック画面ウィジェット・背面タップなど、ショートカットを呼べる場所ならどこでも使えます。" + } + } + } + }, + "단축어와 시리": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shortcuts & Siri" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ショートカットとSiri" + } + } + } + }, + "데이터와 개인정보": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Data & Privacy" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "データとプライバシー" + } + } + } + }, + "도움말": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Help" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ヘルプ" + } + } + } + }, + "두 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Twice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "2回" + } + } + } + }, + "라이트": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Light" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ライト" + } + } + } + }, + "바로 시작하기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Getting Started" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "すぐに始める" + } + } + } + }, + "버전": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "バージョン" + } + } + } + }, + "범위": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Range" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "範囲" + } + } + } + }, + "사이클": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cycle" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイクル" + } + } + } + }, + "설정": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "設定" + } + } + } + }, + "설정·기타": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings & More" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "設定・その他" + } + } + } + }, + "설정해 둔 수축·이완 사이클로 케겔 운동을 바로 시작해요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Starts a Kegel workout right away with your saved squeeze–relax cycle." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "保存した締める・ゆるめるサイクルで、ケーゲル運動をすぐに開始します。" + } + } + } + }, + "세 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Three Times" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "3回" + } + } + } + }, + "세트": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sets" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "セット" + } + } + } + }, + "수축": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める" + } + } + } + }, + "수축 %lld초 · 이완 %lld초 · %lld회": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze %lld s · Relax %lld s · ×%lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める%lld秒 · ゆるめる%lld秒 · %lld回" + } + } + } + }, + "수축 → 이완을 %lld회 반복해요. 한 세트 약 %@.": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "수축 → 이완을 %1$lld회 반복해요. 한 세트 약 %2$@." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Repeats squeeze → relax %lld times. One set takes about %@." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める→ゆるめるを%lld回くり返します。1セットは約%@です。" + } + } + } + }, + "수축 진동": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze Haptic" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締めるときの振動" + } + } + } + }, + "수축 진동 미리 느끼기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preview Squeeze Haptic" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める振動を試す" + } + } + } + }, + "수축과 이완의 진동이 달라 화면을 보지 않아도 구분할 수 있어요. 진동은 실기기에서만 느껴져요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze and relax use different vibrations, so you can follow along without looking at the screen. Haptics can only be felt on a real device." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締めるときとゆるめるときで振動が異なるので、画面を見なくても区別できます。振動は実機でのみ感じられます。" + } + } + } + }, + "수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요. 그래서 화면을 보지 않아도 지금 조여야 할지 풀어야 할지 알 수 있어요. 진동 패턴은 운동 세팅에서 국면별로 고를 수 있고(한 번·두 번·세 번·심장 박동), '미리 느끼기' 버튼으로 실제 진동을 확인할 수 있어요. 진동은 실기기에서만 느껴져요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A different vibration plays when a squeeze starts and when a relax starts, so you always know whether to squeeze or release without looking at the screen. You can choose a pattern for each phase in the workout setup (Once, Twice, Three Times, Heartbeat) and try them with the preview buttons. Haptics can only be felt on a real device." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締め始めとゆるめ始めで異なる振動が鳴るので、画面を見なくても今どちらなのか分かります。振動パターンはワークアウト設定でそれぞれ選べて(1回・2回・3回・心拍)、「試す」ボタンで実際に確認できます。振動は実機でのみ感じられます。" + } + } + } + }, + "시": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hour" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "時" + } + } + } + }, + "시간대별 횟수": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reps by Hour" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "時間帯別の回数" + } + } + } + }, + "시스템": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "System" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "システム" + } + } + } + }, + "시작": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "開始" + } + } + } + }, + "심장 박동": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Heartbeat" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "心拍" + } + } + } + }, + "애플워치": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Apple Watch" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Apple Watch" + } + } + } + }, + "애플워치에서 운동하기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Working Out on Apple Watch" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Apple Watchでワークアウト" + } + } + } + }, + "앱 밖에서": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Outside the App" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アプリの外で" + } + } + } + }, + "언어": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Language" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "言語" + } + } + } + }, + "언어를 바꾸면 앱을 완전히 종료했다가 다시 실행했을 때 적용돼요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The new language takes effect after you fully quit and relaunch the app." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "言語を変更すると、アプリを完全に終了して再起動したときに適用されます。" + } + } + } + }, + "오늘 기록": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Today's Sessions" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今日の記録" + } + } + } + }, + "오늘은 아직 기록이 없어요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No sessions yet today." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今日はまだ記録がありません。" + } + } + } + }, + "오른쪽 위 슬라이더 버튼이 운동 세팅이에요. 수축 시간(1~60초), 이완 시간(1~60초), 반복 횟수(1~100회)를 정할 수 있어요. 예를 들어 수축 10초·이완 5초·20회로 설정하면, 수축과 이완을 20번 반복하는 한 세트가 약 5분이 돼요. 시작 버튼 아래의 요약 문구를 눌러도 이 화면이 열려요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The slider button at the top right opens the workout setup. You can set the squeeze time (1–60 s), relax time (1–60 s), and the number of reps (1–100). For example, with squeeze 10 s · relax 5 s · 20 reps, one set takes about 5 minutes. Tapping the summary text under the Start button also opens this screen." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "右上のスライダーボタンがワークアウト設定です。締める時間(1〜60秒)、ゆるめる時間(1〜60秒)、回数(1〜100回)を設定できます。例えば締める10秒・ゆるめる5秒・20回なら、1セットは約5分です。開始ボタンの下の概要文を押してもこの画面が開きます。" + } + } + } + }, + "완료": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Done" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "完了" + } + } + } + }, + "완료!": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Done!" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "完了!" + } + } + } + }, + "왼쪽 위 톱니바퀴 버튼이 설정이에요. 화면 모드(시스템·라이트·다크)와 언어(시스템·한국어·English·日本語)를 고를 수 있어요. 언어는 앱을 완전히 종료했다가 다시 실행하면 적용돼요. 워치는 워치의 시스템 언어를 따라요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The gear button at the top left opens Settings. You can choose the appearance (System, Light, Dark) and the language (System, 한국어, English, 日本語). Language changes take effect after fully quitting and relaunching the app. The watch follows its own system language." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "左上の歯車ボタンが設定です。画面モード(システム・ライト・ダーク)と言語(システム・한국어・English・日本語)を選べます。言語はアプリを完全に終了して再起動すると適用されます。ウォッチはウォッチのシステム言語に従います。" + } + } + } + }, + "운동 세팅": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Workout Setup" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウト設定" + } + } + } + }, + "운동 세팅 바꾸기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Changing Your Setup" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウト設定を変える" + } + } + } + }, + "운동 세팅 열기 — 현재 %@": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open workout setup — currently %@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウト設定を開く — 現在 %@" + } + } + } + }, + "운동 세팅은 아이폰에서만 바꿀 수 있고, 바꾸면 워치에 자동으로 전달돼요. 워치에서 끝까지 마친 세트는 아이폰으로 넘어와 통계에 합쳐져요. 워치가 아이폰과 떨어져 있어도 기록은 쌓였다가 연결되면 자동으로 전송돼요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Workout settings can only be changed on the iPhone, and changes are sent to the watch automatically. Sets finished on the watch are sent back to the iPhone and merged into your statistics. Even when the watch is away from the iPhone, records are queued and transferred automatically once connected." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウト設定はiPhoneでのみ変更でき、変更は自動的にウォッチへ送られます。ウォッチで完了したセットはiPhoneに送られ、統計に合算されます。ウォッチがiPhoneと離れていても、記録はたまって接続時に自動送信されます。" + } + } + } + }, + "운동 중 아래의 중단 버튼을 누르면 확인 후 세트가 끝나요. 중단한 세트는 아예 하지 않은 것으로 처리돼서 기록과 통계에 남지 않아요. 끝까지 마친 세트만 기록돼요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "During a workout, tap the Stop button below and confirm to end the set. A stopped set is treated as if it never happened — it won't appear in your history or statistics. Only sets you finish are recorded." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウト中に下の中断ボタンを押して確認すると、セットが終了します。中断したセットは最初からなかったものとして扱われ、記録や統計には残りません。最後までやり切ったセットだけが記録されます。" + } + } + } + }, + "운동 중에 앱을 나가면 다이나믹 아일랜드에 현재 몇 번째인지와 수축·이완 상태가 떠요. 길게 누르면 남은 시간과 진행 바가 보이는 큰 화면이 열려요. 진동도 계속 울리니까 다른 앱을 쓰면서도 운동을 이어갈 수 있어요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "If you leave the app during a workout, the Dynamic Island shows your current rep and whether to squeeze or relax. Press and hold it for a larger view with the remaining time and a progress bar. Vibrations keep playing, so you can continue while using other apps." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウト中にアプリを離れると、Dynamic Islandに現在の回数と締める・ゆるめるの状態が表示されます。長押しすると残り時間と進行バーが見える大きな画面が開きます。振動も鳴り続けるので、他のアプリを使いながら続けられます。" + } + } + } + }, + "운동을 중단할까요?": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop this workout?" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウトを中断しますか?" + } + } + } + }, + "워치 앱을 열면 시작 버튼 하나만 있어요. 누르면 바로 시작되고, 화면에 수축·이완 상태와 남은 시간, 몇 번째인지가 한눈에 보여요. 워치에서 실행하면 진동은 워치에서만 울려요. 중단 버튼을 누르면 그 세트는 기록되지 않아요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the watch app and there's just one Start button. Tap it to begin immediately — the screen shows the current phase, the remaining time, and which rep you're on, all at a glance. When you start from the watch, vibrations play only on the watch. Tapping Stop discards that set." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ウォッチアプリを開くと開始ボタンが1つだけあります。押すとすぐに始まり、画面に締める・ゆるめるの状態、残り時間、何回目かが一目で分かります。ウォッチから実行すると振動はウォッチだけで鳴ります。中断するとそのセットは記録されません。" + } + } + } + }, + "워치와 아이폰 동기화": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Watch & iPhone Sync" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ウォッチとiPhoneの同期" + } + } + } + }, + "월간": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Monthly" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "月間" + } + } + } + }, + "이 기간에는 아직 기록이 없어요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No sessions in this period yet." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "この期間の記録はまだありません。" + } + } + } + }, + "이번 달": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This Month" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今月" + } + } + } + }, + "이번 주": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This Week" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今週" + } + } + } + }, + "이번 주 ↔ 지난 7일 전환": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This Week ↔ Last 7 Days" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今週 ↔ 過去7日の切り替え" + } + } + } + }, + "이완": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Relax" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ゆるめる" + } + } + } + }, + "이완 진동": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Relax Haptic" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ゆるめるときの振動" + } + } + } + }, + "이완 진동 미리 느끼기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preview Relax Haptic" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ゆるめる振動を試す" + } + } + } + }, + "일별 횟수": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reps by Day" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "日別の回数" + } + } + } + }, + "주간": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Weekly" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "週間" + } + } + } + }, + "주간·월간 화면 위의 스위치로 두 가지 방식을 오갈 수 있어요. '이번 주 / 이번 달'은 달력 기준(월요일~일요일, 1일~말일)이고, '지난 7일 / 지난 30일'은 오늘을 기준으로 거슬러 세는 방식이에요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use the switch above the weekly and monthly views to toggle between two modes: 'This Week / This Month' follows the calendar (Monday–Sunday, 1st to last day), while 'Last 7 Days / Last 30 Days' counts back from today." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "週間・月間画面の上のスイッチで2つの方式を切り替えられます。「今週/今月」はカレンダー基準(月〜日、1日〜末日)、「過去7日/過去30日」は今日からさかのぼって数える方式です。" + } + } + } + }, + "중간에 중단하면": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stopping Midway" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "途中で中断すると" + } + } + } + }, + "중단": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "中断" + } + } + } + }, + "중단하기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "中断する" + } + } + } + }, + "중단하면 이 세트는 기록되지 않아요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "If you stop, this set won't be recorded." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "中断すると、このセットは記録されません。" + } + } + } + }, + "지난 30일": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Last 30 Days" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "過去30日" + } + } + } + }, + "지난 7일": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Last 7 Days" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "過去7日" + } + } + } + }, + "진동 패턴": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Haptic Patterns" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "振動パターン" + } + } + } + }, + "진동으로 구분하기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Follow by Vibration" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "振動で区別する" + } + } + } + }, + "첫 화면을 아래로 쓸어내리거나 위쪽 가운데의 손잡이를 누르면 통계 화면이 열려요. 닫을 때는 위로 쓸어올리거나 X 버튼을 누르면 돼요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Swipe down on the main screen, or tap the handle at the top center, to open statistics. To close, swipe up or tap the X button." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "最初の画面を下にスワイプするか、上部中央のハンドルを押すと統計画面が開きます。閉じるときは上にスワイプするか、Xボタンを押します。" + } + } + } + }, + "첫 화면의 큰 시작 버튼을 누르면 저장된 세팅대로 바로 운동이 시작돼요. 화면의 원이 수축할 때 함께 조이고, 커질 때 힘을 풀면 돼요. 정해진 횟수를 모두 마치면 완료 화면이 뜨고 기록이 저장돼요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Tap the big Start button on the main screen to begin right away with your saved setup. Squeeze while the circle on screen shrinks, and release as it grows. When you finish all the reps, a completion screen appears and the set is recorded." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "最初の画面の大きな開始ボタンを押すと、保存した設定ですぐにワークアウトが始まります。画面の円が縮むときに締めて、大きくなるときに力を抜きます。設定した回数をすべて終えると完了画面が表示され、記録が保存されます。" + } + } + } + }, + "케겔 운동 시작": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start Kegel Workout" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ケーゲル運動を開始" + } + } + } + }, + "케깅은 광고와 결제가 없는 완전 무료 앱이에요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keging is completely free — no ads, no purchases." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Kegingは広告も課金もない完全無料アプリです。" + } + } + } + }, + "케깅은 광고와 결제가 없는 완전 무료 앱이에요. 모든 기록은 이 기기(그리고 워치)에만 저장되고, 외부 서버로 전송되지 않아요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keging is completely free, with no ads and no purchases. All records are stored only on this device (and your watch) and are never sent to any server." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Kegingは広告も課金もない完全無料アプリです。すべての記録はこの端末(とウォッチ)にのみ保存され、外部サーバーへ送信されることはありません。" + } + } + } + }, + "테마": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Theme" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テーマ" + } + } + } + }, + "테마와 언어": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Theme & Language" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テーマと言語" + } + } + } + }, + "통계": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Statistics" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "統計" + } + } + } + }, + "통계 닫기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Close statistics" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "統計を閉じる" + } + } + } + }, + "통계 보기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show statistics" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "統計を見る" + } + } + } + }, + "통계 열기": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Opening Statistics" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "統計を開く" + } + } + } + }, + "하루": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Day" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "1日" + } + } + } + }, + "하루 · 주간 · 월간": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Day · Weekly · Monthly" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "1日・週間・月間" + } + } + } + }, + "하루 탭에서는 오늘 몇 시에 몇 회를 했는지 목록과 시간대별 꺾은선 그래프로 보여줘요. 주간·월간 탭에서는 일별 횟수를 꺾은선 그래프와 합계(세트·횟수)로 보여줘요. 주는 월요일에 시작해요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The Day tab shows what time you worked out today and how many reps, as a list and an hourly line chart. The Weekly and Monthly tabs show daily reps as a line chart with totals (sets and reps). Weeks start on Monday." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "1日タブでは、今日何時に何回行ったかをリストと時間帯別の折れ線グラフで表示します。週間・月間タブでは日別の回数を折れ線グラフと合計(セット・回数)で表示します。週は月曜日から始まります。" + } + } + } + }, + "한 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Once" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "1回" + } + } + } + }, + "현재 %lld번째, 전체 %lld회": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "현재 %1$lld번째, 전체 %2$lld회" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Rep %lld of %lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld回目、全%lld回" + } + } + } + }, + "화면 모드": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Appearance" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "画面モード" + } + } + } + }, + "확인": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "OK" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "OK" + } + } + } + }, + "횟수": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reps" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "回数" + } + } + } + } + }, + "version": "1.0" +} \ No newline at end of file diff --git a/myApp/Keging/IOS/StatsView.swift b/myApp/Keging/IOS/StatsView.swift index 9156066..266fdd9 100644 --- a/myApp/Keging/IOS/StatsView.swift +++ b/myApp/Keging/IOS/StatsView.swift @@ -10,8 +10,16 @@ import SwiftUI import Charts enum StatScope: String, CaseIterable, Identifiable { - case day = "하루", week = "주간", month = "월간" + case day, week, month var id: String { rawValue } + + var title: LocalizedStringKey { + switch self { + case .day: "하루" + case .week: "주간" + case .month: "월간" + } + } } enum StatRangeMode: String { @@ -39,7 +47,7 @@ struct StatsView: View { VStack(spacing: 12) { header Picker("기간", selection: $scope) { - ForEach(StatScope.allCases) { Text($0.rawValue).tag($0) } + ForEach(StatScope.allCases) { Text($0.title).tag($0) } } .pickerStyle(.segmented) .padding(.horizontal, 20) @@ -93,7 +101,7 @@ struct StatsView: View { .background(AppTheme.surface, in: Circle()) } .buttonStyle(.plain) - .accessibilityLabel("통계 닫기") + .accessibilityLabel(Text("통계 닫기")) } .padding(.horizontal, 20) .padding(.top, 12) @@ -173,22 +181,22 @@ struct StatsView: View { private var summaryCard: some View { HStack(spacing: 0) { - summaryItem(value: "\(totalSets)", label: "세트") + summaryItem(value: "\(totalSets)", label: Text("세트")) Divider().frame(height: 36) - summaryItem(value: "\(totalReps)", label: "횟수") + summaryItem(value: "\(totalReps)", label: Text("횟수")) } .padding(.vertical, 16) .frame(maxWidth: .infinity) .background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 16)) } - private func summaryItem(value: String, label: String) -> some View { + private func summaryItem(value: String, label: Text) -> some View { VStack(spacing: 4) { Text(value) .font(.title.bold()) .monospacedDigit() .foregroundStyle(AppTheme.green) - Text(label) + label .font(.caption) .foregroundStyle(.secondary) } @@ -283,7 +291,7 @@ struct StatsView: View { } else { ForEach(todayRecords.sorted { $0.startedAt > $1.startedAt }) { record in HStack { - Text(record.startedAt.formatted(date: .omitted, time: .shortened)) + Text(record.startedAt, format: .dateTime.hour().minute()) .font(.body) Spacer() Text("\(record.reps)회") diff --git a/myApp/Keging/IOS/TimerSettingsView.swift b/myApp/Keging/IOS/TimerSettingsView.swift index b275884..58ce47c 100644 --- a/myApp/Keging/IOS/TimerSettingsView.swift +++ b/myApp/Keging/IOS/TimerSettingsView.swift @@ -16,13 +16,13 @@ struct TimerSettingsView: View { Form { Section { Stepper(value: $settings.config.contractSeconds, in: 1...60) { - LabeledContent("수축", value: "\(settings.config.contractSeconds)초") + LabeledContent("수축", value: String(localized: "\(settings.config.contractSeconds)초")) } Stepper(value: $settings.config.relaxSeconds, in: 1...60) { - LabeledContent("이완", value: "\(settings.config.relaxSeconds)초") + LabeledContent("이완", value: String(localized: "\(settings.config.relaxSeconds)초")) } Stepper(value: $settings.config.reps, in: 1...100) { - LabeledContent("횟수", value: "\(settings.config.reps)회") + LabeledContent("횟수", value: String(localized: "\(settings.config.reps)회")) } } header: { Text("사이클") @@ -59,8 +59,8 @@ struct TimerSettingsView: View { let total = Int(settings.config.totalDuration) let minutes = total / 60 let seconds = total % 60 - if minutes == 0 { return "\(seconds)초" } - if seconds == 0 { return "\(minutes)분" } - return "\(minutes)분 \(seconds)초" + if minutes == 0 { return String(localized: "\(seconds)초") } + if seconds == 0 { return String(localized: "\(minutes)분") } + return String(localized: "\(minutes)분 \(seconds)초") } } diff --git a/myApp/Keging/IOS/WorkoutView.swift b/myApp/Keging/IOS/WorkoutView.swift index d0f2f13..b42da16 100644 --- a/myApp/Keging/IOS/WorkoutView.swift +++ b/myApp/Keging/IOS/WorkoutView.swift @@ -32,7 +32,7 @@ struct WorkoutView: View { .monospacedDigit() .foregroundStyle(.secondary) .padding(.top, 24) - .accessibilityLabel("현재 \(rep)번째, 전체 \(engine.config.reps)회") + .accessibilityLabel(Text("현재 \(rep)번째, 전체 \(engine.config.reps)회")) Spacer() diff --git a/myApp/Keging/Keging.xcodeproj/project.pbxproj b/myApp/Keging/Keging.xcodeproj/project.pbxproj index 9baed40..1da0b98 100644 --- a/myApp/Keging/Keging.xcodeproj/project.pbxproj +++ b/myApp/Keging/Keging.xcodeproj/project.pbxproj @@ -277,10 +277,12 @@ }; }; buildConfigurationList = 7217F81030425E4B001577AB /* Build configuration list for PBXProject "Keging" */; - developmentRegion = en; + developmentRegion = ko; hasScannedForEncodings = 0; knownRegions = ( + ko, en, + ja, Base, ); mainGroup = 7217F80C30425E4B001577AB; diff --git a/myApp/Keging/KegingWatch Watch App/ContentView.swift b/myApp/Keging/KegingWatch Watch App/ContentView.swift index b53dbfd..151ff9d 100644 --- a/myApp/Keging/KegingWatch Watch App/ContentView.swift +++ b/myApp/Keging/KegingWatch Watch App/ContentView.swift @@ -52,7 +52,7 @@ struct WatchContentView: View { .frame(width: 108, height: 108) } .buttonStyle(.plain) - .accessibilityLabel("케겔 운동 시작") + .accessibilityLabel(Text("케겔 운동 시작")) Text(SettingsStore.shared.config.summaryText) .font(.system(size: 11)) @@ -75,7 +75,7 @@ struct WatchContentView: View { .font(.footnote) .monospacedDigit() .foregroundStyle(.secondary) - .accessibilityLabel("현재 \(rep)번째, 전체 \(engine.config.reps)회") + .accessibilityLabel(Text("현재 \(rep)번째, 전체 \(engine.config.reps)회")) Button { WatchCoordinator.shared.stopWorkout() } label: { diff --git a/myApp/Keging/KegingWatch Watch App/InfoPlist.xcstrings b/myApp/Keging/KegingWatch Watch App/InfoPlist.xcstrings new file mode 100644 index 0000000..06afc2b --- /dev/null +++ b/myApp/Keging/KegingWatch Watch App/InfoPlist.xcstrings @@ -0,0 +1,29 @@ +{ + "sourceLanguage" : "ko", + "strings" : { + "CFBundleDisplayName" : { + "extractionState" : "manual", + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케깅" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings b/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings new file mode 100644 index 0000000..6d57318 --- /dev/null +++ b/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings @@ -0,0 +1,276 @@ +{ + "sourceLanguage": "ko", + "strings": { + "%lld / %lld": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "%1$lld / %2$lld" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld / %lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld / %lld" + } + } + } + }, + "%lld회 완주": { + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld rep done" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld reps done" + } + } + } + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld回完了" + } + } + } + }, + "두 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Twice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "2回" + } + } + } + }, + "세 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Three Times" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "3回" + } + } + } + }, + "수축": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める" + } + } + } + }, + "수축 %lld초 · 이완 %lld초 · %lld회": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze %lld s · Relax %lld s · ×%lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める%lld秒 · ゆるめる%lld秒 · %lld回" + } + } + } + }, + "시작": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "開始" + } + } + } + }, + "심장 박동": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Heartbeat" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "心拍" + } + } + } + }, + "완료!": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Done!" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "完了!" + } + } + } + }, + "이완": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Relax" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ゆるめる" + } + } + } + }, + "중단": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "中断" + } + } + } + }, + "케겔 운동 시작": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start Kegel Workout" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ケーゲル運動を開始" + } + } + } + }, + "한 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Once" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "1回" + } + } + } + }, + "현재 %lld번째, 전체 %lld회": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "현재 %1$lld번째, 전체 %2$lld회" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Rep %lld of %lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld回目、全%lld回" + } + } + } + }, + "확인": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "OK" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "OK" + } + } + } + } + }, + "version": "1.0" +} \ No newline at end of file diff --git a/myApp/Keging/KegingWidgets/Localizable.xcstrings b/myApp/Keging/KegingWidgets/Localizable.xcstrings new file mode 100644 index 0000000..a8b6533 --- /dev/null +++ b/myApp/Keging/KegingWidgets/Localizable.xcstrings @@ -0,0 +1,168 @@ +{ + "sourceLanguage": "ko", + "strings": { + "%lld / %lld": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "%1$lld / %2$lld" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld / %lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld / %lld" + } + } + } + }, + "%lld/%lld": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "%1$lld/%2$lld" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld/%lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld/%lld" + } + } + } + }, + "두 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Twice" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "2回" + } + } + } + }, + "세 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Three Times" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "3回" + } + } + } + }, + "수축": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める" + } + } + } + }, + "수축 %lld초 · 이완 %lld초 · %lld회": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Squeeze %lld s · Relax %lld s · ×%lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "締める%lld秒 · ゆるめる%lld秒 · %lld回" + } + } + } + }, + "심장 박동": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Heartbeat" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "心拍" + } + } + } + }, + "이완": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Relax" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ゆるめる" + } + } + } + }, + "한 번": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Once" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "1回" + } + } + } + } + }, + "version": "1.0" +} \ No newline at end of file diff --git a/myApp/Keging/Shared/KegelEngine.swift b/myApp/Keging/Shared/KegelEngine.swift index 5bcbd06..d7fac22 100644 --- a/myApp/Keging/Shared/KegelEngine.swift +++ b/myApp/Keging/Shared/KegelEngine.swift @@ -12,7 +12,9 @@ import Combine nonisolated enum KegelPhase: String, Codable, Hashable { case contract, relax - var label: String { self == .contract ? "수축" : "이완" } + var label: String { + self == .contract ? String(localized: "수축") : String(localized: "이완") + } var symbolName: String { self == .contract ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right" } diff --git a/myApp/Keging/Shared/WorkoutSettings.swift b/myApp/Keging/Shared/WorkoutSettings.swift index 8b73087..957447c 100644 --- a/myApp/Keging/Shared/WorkoutSettings.swift +++ b/myApp/Keging/Shared/WorkoutSettings.swift @@ -16,10 +16,10 @@ nonisolated enum HapticPattern: String, CaseIterable, Identifiable, Codable { var label: String { switch self { - case .once: "한 번" - case .twice: "두 번" - case .thrice: "세 번" - case .heartbeat: "심장 박동" + case .once: String(localized: "한 번") + case .twice: String(localized: "두 번") + case .thrice: String(localized: "세 번") + case .heartbeat: String(localized: "심장 박동") } } @@ -44,7 +44,9 @@ nonisolated struct WorkoutConfig: Codable, Equatable { var totalDuration: TimeInterval { Double(reps * (contractSeconds + relaxSeconds)) } - var summaryText: String { "수축 \(contractSeconds)초 · 이완 \(relaxSeconds)초 · \(reps)회" } + var summaryText: String { + String(localized: "수축 \(contractSeconds)초 · 이완 \(relaxSeconds)초 · \(reps)회") + } } @MainActor