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
This commit is contained in:
songyc macbook 2026-07-10 15:03:24 +09:00
parent 1e62c082d3
commit a6c040cd6a
33 changed files with 11163 additions and 1176 deletions

View File

@ -18,5 +18,9 @@
<array>
<string>group.com.yechan.HaruDanim</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yechan.HaruDanim</string>
</array>
</dict>
</plist>

View File

@ -106,6 +106,7 @@
membershipExceptions = (
HaruDanimWidgets.swift,
Info.plist,
Localizable.xcstrings,
);
target = 725A9E832FFCC65000846FFB /* Haru_Danim */;
};
@ -115,6 +116,7 @@
Assets.xcassets,
Haru_DanimWatchWidgetsBundle.swift,
Info.plist,
Localizable.xcstrings,
);
target = 7263D0DF3000433300EA47FC /* Haru_DanimWatch Watch App */;
};
@ -662,6 +664,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
@ -677,6 +680,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.HaruDanim.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@ -696,6 +700,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
@ -711,6 +716,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.HaruDanim.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@ -730,6 +736,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
@ -746,6 +753,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.yechan.HaruDanim.watchkitapp.Haru-DanimWatchWidgets";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@ -764,6 +772,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
@ -780,6 +789,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.yechan.HaruDanim.watchkitapp.Haru-DanimWatchWidgets";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;

View File

@ -41,9 +41,6 @@
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnableBacktraceRecording = "Yes">
<StoreKitConfigurationFileReference
identifier = "../../HaruDanim.storekit">
</StoreKitConfigurationFileReference>
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
@ -54,6 +51,9 @@
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../../HaruDanim.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2660"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7263D0DF3000433300EA47FC"
BuildableName = "Haru_DanimWatch Watch App.app"
BlueprintName = "Haru_DanimWatch Watch App"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "725A9E832FFCC65000846FFB"
BuildableName = "Haru_Danim.app"
BlueprintName = "Haru_Danim"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnableBacktraceRecording = "Yes">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7263D0DF3000433300EA47FC"
BuildableName = "Haru_DanimWatch Watch App.app"
BlueprintName = "Haru_DanimWatch Watch App"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7263D0DF3000433300EA47FC"
BuildableName = "Haru_DanimWatch Watch App.app"
BlueprintName = "Haru_DanimWatch Watch App"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2660"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7263D0F3300044E900EA47FC"
BuildableName = "Haru_DanimWatchWidgetsExtension.appex"
BlueprintName = "Haru_DanimWatchWidgetsExtension"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "725A9E832FFCC65000846FFB"
BuildableName = "Haru_Danim.app"
BlueprintName = "Haru_Danim"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7263D0DF3000433300EA47FC"
BuildableName = "Haru_DanimWatch Watch App.app"
BlueprintName = "Haru_DanimWatch Watch App"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2"
queueDebuggingEnableBacktraceRecording = "Yes">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "725A9E832FFCC65000846FFB"
BuildableName = "Haru_Danim.app"
BlueprintName = "Haru_Danim"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7263D0F3300044E900EA47FC"
BuildableName = "Haru_DanimWatchWidgetsExtension.appex"
BlueprintName = "Haru_DanimWatchWidgetsExtension"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2660"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AA0000000000000000000003"
BuildableName = "Haru_DanimWidgets.appex"
BlueprintName = "Haru_DanimWidgets"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "725A9E832FFCC65000846FFB"
BuildableName = "Haru_Danim.app"
BlueprintName = "Haru_Danim"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2"
queueDebuggingEnableBacktraceRecording = "Yes">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.springboard">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AA0000000000000000000003"
BuildableName = "Haru_DanimWidgets.appex"
BlueprintName = "Haru_DanimWidgets"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "725A9E832FFCC65000846FFB"
BuildableName = "Haru_Danim.app"
BlueprintName = "Haru_Danim"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "725A9E832FFCC65000846FFB"
BuildableName = "Haru_Danim.app"
BlueprintName = "Haru_Danim"
ReferencedContainer = "container:Haru_Danim.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -206,8 +206,12 @@ enum WatchFormat {
let total = Int(seconds.rounded())
let h = total / 3600
let m = (total % 3600) / 60
if h > 0 { return m > 0 ? "\(h)시간 \(m)" : "\(h)시간" }
if m > 0 { return "\(m)" }
return "\(total)"
if h > 0 {
return m > 0
? String(localized: "\(h)시간 \(m)")
: String(localized: "\(h)시간")
}
if m > 0 { return String(localized: "\(m)") }
return String(localized: "\(total)")
}
}

View File

@ -6,5 +6,9 @@
<array>
<string>group.com.yechan.HaruDanim</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yechan.HaruDanim</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,718 @@
{
"sourceLanguage": "ko",
"strings": {
"%@": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@"
}
}
}
},
"%@ %@": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@ %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ %@"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$@ %2$@"
}
}
}
},
"%@ %lld%%": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@ %lld%%"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ %lld%%"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$@ %2$lld%%"
}
}
}
},
"%@ +%lld": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@ +%lld"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ +%lld"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$@ +%2$lld"
}
}
}
},
"%lld": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lld"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld"
}
}
}
},
"%lld%%": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lld%%"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld%%"
}
}
}
},
"%lld분": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lldm"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld分"
}
}
}
},
"%lld시간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lldh"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld時間"
}
}
}
},
"%lld시간 %lld분": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lldh %lldm"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld時間%lld分"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld시간 %2$lld분"
}
}
}
},
"%lld초": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%llds"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld秒"
}
}
}
},
"+%lld": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "+%lld"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "+%lld"
}
}
}
},
"iPhone 앱의 설정 → 프리미엄에서 잠금 해제해 주세요": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Unlock in the iPhone app: Settings → Premium"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "iPhoneアプリの設定→プレミアムでロックを解除してください"
}
}
}
},
"iPhone과 연결 중...": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Connecting to iPhone..."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "iPhoneに接続中..."
}
}
}
},
"iPhone에 연결할 수 없어요. 근처에 있는지 확인해 주세요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Can't reach your iPhone. Make sure it's nearby."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "iPhoneに接続できません。近くにあるか確認してください。"
}
}
}
},
"기간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Period"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "期間"
}
}
}
},
"꼬리표 없음": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "No tag"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "タグなし"
}
}
}
},
"다짐": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Quest"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クエスト"
}
}
}
},
"다짐 달성률": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Quest Progress"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クエスト達成率"
}
}
}
},
"다짐 달성률 (%@)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Quest progress (%@)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クエスト達成率(%@"
}
}
}
},
"목표": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Goals"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標"
}
}
}
},
"목표 달성률": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Goal Progress"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標達成率"
}
}
}
},
"목표 달성률 (%@)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Goal progress (%@)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標達成率(%@"
}
}
}
},
"선택한 다짐의 달성률을 표시해요. 한도형 다짐은 상태로 표시돼요. (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shows the selected quest's progress. Limit quests show their status. (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "選択したクエストの達成率を表示します。上限型はステータス表示になります。(プレミアム)"
}
}
}
},
"선택한 목표의 하루/주간/월간 달성률을 표시해요. (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shows the selected goal's daily/weekly/monthly progress. (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "選択した目標の日/週/月の達成率を表示します。(プレミアム)"
}
}
}
},
"실행 중 아님": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Not running"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "実行中なし"
}
}
}
},
"없음": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "None"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "なし"
}
}
}
},
"오늘 %@": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Today %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "今日 %@"
}
}
}
},
"오늘 %lld회": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Today %lld times"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "今日%lld回"
}
}
}
},
"월간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Monthly"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "月間"
}
}
}
},
"주간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Weekly"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "週間"
}
}
}
},
"초과": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Over"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "超過"
}
}
}
},
"측정 중": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Tracking"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "計測中"
}
}
}
},
"측정 중인 행동과 경과 시간을 표시해요. (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shows the running action and elapsed time. (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "計測中のアクションと経過時間を表示します。(プレミアム)"
}
}
}
},
"표시할 다짐과 기간을 선택하세요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Choose a quest and period to display."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "表示するクエストと期間を選んでください。"
}
}
}
},
"표시할 목표와 기간을 선택하세요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Choose a goal and period to display."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "表示する目標と期間を選んでください。"
}
}
}
},
"프리미엄": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Premium"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "プレミアム"
}
}
}
},
"프리미엄 기능": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Premium Features"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "プレミアム機能"
}
}
}
},
"하루": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Daily"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "1日"
}
}
}
},
"하루다님": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "HaruDanim"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ハルダニム"
}
}
}
},
"한도": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Limit"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上限"
}
}
}
},
"한도 안": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Within"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上限内"
}
}
}
},
"한도 초과": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Over limit"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上限超過"
}
}
}
},
"현재 현황": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Current Status"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "現在の状況"
}
}
}
}
},
"version": "1.0"
}

View File

@ -37,9 +37,9 @@ enum WatchSpanOption: String, AppEnum {
var label: String {
switch self {
case .day: return "하루"
case .week: return "주간"
case .month: return "월간"
case .day: return String(localized: "하루")
case .week: return String(localized: "주간")
case .month: return String(localized: "월간")
}
}
}

View File

@ -0,0 +1,536 @@
{
"sourceLanguage": "ko",
"strings": {
"%@": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@"
}
}
}
},
"%@ %@": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@ %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ %@"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$@ %2$@"
}
}
}
},
"%@ %lld%%": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@ %lld%%"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ %lld%%"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$@ %2$lld%%"
}
}
}
},
"%@ +%lld": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%@ +%lld"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ +%lld"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$@ +%2$lld"
}
}
}
},
"%lld": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lld"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld"
}
}
}
},
"%lld%%": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lld%%"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%lld%%"
}
}
}
},
"+%lld": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "+%lld"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "+%lld"
}
}
}
},
"기간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Period"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "期間"
}
}
}
},
"다짐": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Quest"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クエスト"
}
}
}
},
"다짐 달성률": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Quest Progress"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クエスト達成率"
}
}
}
},
"다짐 달성률 (%@)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Quest progress (%@)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クエスト達成率(%@"
}
}
}
},
"목표": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Goals"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標"
}
}
}
},
"목표 달성률": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Goal Progress"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標達成率"
}
}
}
},
"목표 달성률 (%@)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Goal progress (%@)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標達成率(%@"
}
}
}
},
"선택한 다짐의 달성률을 표시해요. 한도형 다짐은 상태로 표시돼요. (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shows the selected quest's progress. Limit quests show their status. (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "選択したクエストの達成率を表示します。上限型はステータス表示になります。(プレミアム)"
}
}
}
},
"선택한 목표의 하루/주간/월간 달성률을 표시해요. (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shows the selected goal's daily/weekly/monthly progress. (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "選択した目標の日/週/月の達成率を表示します。(プレミアム)"
}
}
}
},
"실행 중 아님": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Not running"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "実行中なし"
}
}
}
},
"없음": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "None"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "なし"
}
}
}
},
"월간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Monthly"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "月間"
}
}
}
},
"주간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Weekly"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "週間"
}
}
}
},
"초과": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Over"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "超過"
}
}
}
},
"측정 중": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Tracking"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "計測中"
}
}
}
},
"측정 중인 행동과 경과 시간을 표시해요. (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shows the running action and elapsed time. (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "計測中のアクションと経過時間を表示します。(プレミアム)"
}
}
}
},
"표시할 다짐과 기간을 선택하세요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Choose a quest and period to display."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "表示するクエストと期間を選んでください。"
}
}
}
},
"표시할 목표와 기간을 선택하세요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Choose a goal and period to display."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "表示する目標と期間を選んでください。"
}
}
}
},
"프리미엄": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Premium"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "プレミアム"
}
}
}
},
"하루": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Daily"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "1日"
}
}
}
},
"하루다님": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "HaruDanim"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ハルダニム"
}
}
}
},
"한도": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Limit"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上限"
}
}
}
},
"한도 안": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Within"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上限内"
}
}
}
},
"한도 초과": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Over limit"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上限超過"
}
}
}
},
"현재 현황": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Current Status"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "現在の状況"
}
}
}
}
},
"version": "1.0"
}

View File

@ -261,7 +261,7 @@ struct QuestRateComplicationView: View {
Image(systemName: quest.achieved(forSpanRaw: entry.spanRaw)
? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
.font(.system(size: 16))
Text(quest.achieved(forSpanRaw: entry.spanRaw) ? "한도 안" : "초과")
Text(quest.achieved(forSpanRaw: entry.spanRaw) ? String(localized: "한도 안") : String(localized: "초과"))
.font(.system(size: 9))
}
} else {
@ -277,7 +277,7 @@ struct QuestRateComplicationView: View {
private func inlineText(_ quest: WatchQuestInfo) -> String {
if quest.isAtMost {
return quest.achieved(forSpanRaw: entry.spanRaw) ? "한도 안" : "한도 초과"
return quest.achieved(forSpanRaw: entry.spanRaw) ? String(localized: "한도 안") : String(localized: "한도 초과")
}
return "\(Int((quest.ratio(forSpanRaw: entry.spanRaw) * 100).rounded()))%"
}

View File

@ -6,5 +6,9 @@
<array>
<string>group.com.yechan.HaruDanim</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yechan.HaruDanim</string>
</array>
</dict>
</plist>

View File

@ -151,13 +151,13 @@ enum AppTab: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .main: return "모음"
case .action: return "행동"
case .tag: return "꼬리표"
case .goal: return "목표"
case .history: return "기록"
case .stats: return "통계"
case .settings: return "설정"
case .main: return String(localized: "모음")
case .action: return String(localized: "행동")
case .tag: return String(localized: "꼬리표")
case .goal: return String(localized: "목표")
case .history: return String(localized: "기록")
case .stats: return String(localized: "통계")
case .settings: return String(localized: "설정")
}
}

View File

@ -26,14 +26,14 @@ enum SymbolCatalog {
}
private static let rawCategories: [(String, [String])] = [
("인기", [
(String(localized: "인기"), [
"star.fill", "heart.fill", "flame.fill", "bolt.fill", "leaf.fill",
"book.fill", "pencil", "figure.walk", "figure.run", "dumbbell.fill",
"cup.and.saucer.fill", "fork.knife", "bed.double.fill", "alarm.fill",
"laptopcomputer", "gamecontroller.fill", "music.note", "drop.fill",
"pills.fill", "brain.head.profile", "checkmark.circle.fill", "timer",
]),
("운동", [
(String(localized: "운동"), [
"figure.walk", "figure.run", "figure.hiking", "figure.yoga",
"figure.cooldown", "figure.strengthtraining.traditional",
"figure.strengthtraining.functional", "figure.core.training",
@ -50,7 +50,7 @@ enum SymbolCatalog {
"volleyball.fill", "american.football.fill", "cricket.ball.fill",
"hockey.puck.fill", "medal.fill", "trophy.fill", "stopwatch.fill",
]),
("건강", [
(String(localized: "건강"), [
"heart.fill", "heart.text.square.fill", "cross.case.fill",
"pills.fill", "pill.fill", "syringe.fill", "bandage.fill",
"stethoscope", "thermometer.variable", "lungs.fill", "brain.fill",
@ -60,7 +60,7 @@ enum SymbolCatalog {
"staroflife.fill", "leaf.fill", "carrot.fill", "scalemass.fill",
"toilet.fill", "shower.fill", "bathtub.fill", "comb.fill",
]),
("학습", [
(String(localized: "학습"), [
"book.fill", "book.closed.fill", "books.vertical.fill",
"text.book.closed.fill", "bookmark.fill", "graduationcap.fill",
"pencil", "pencil.and.ruler.fill", "highlighter",
@ -72,7 +72,7 @@ enum SymbolCatalog {
"brain.head.profile", "lightbulb.fill", "puzzlepiece.fill",
"backpack.fill", "ruler.fill", "paperclip", "folder.fill",
]),
("일·업무", [
(String(localized: "일·업무"), [
"briefcase.fill", "laptopcomputer", "desktopcomputer",
"keyboard.fill", "printer.fill", "envelope.fill", "phone.fill",
"video.fill", "person.2.fill", "person.3.fill", "calendar",
@ -84,7 +84,7 @@ enum SymbolCatalog {
"checklist", "list.bullet.clipboard.fill", "tray.full.fill",
"archivebox.fill", "presentation.fill" ,"megaphone.fill",
]),
("생활", [
(String(localized: "생활"), [
"house.fill", "sofa.fill", "bed.double.fill", "lamp.desk.fill",
"lightbulb.fill", "washer.fill", "dryer.fill", "dishwasher.fill",
"refrigerator.fill", "oven.fill", "stove.fill", "microwave.fill",
@ -97,14 +97,14 @@ enum SymbolCatalog {
"pawprint.fill", "dog.fill", "cat.fill", "fish.fill",
"bird.fill", "teddybear.fill", "stroller.fill",
]),
("음식", [
(String(localized: "음식"), [
"fork.knife", "cup.and.saucer.fill", "mug.fill", "wineglass.fill",
"waterbottle.fill", "takeoutbag.and.cup.and.straw.fill",
"birthday.cake.fill", "carrot.fill", "fish.fill",
"frying.pan.fill", "popcorn.fill", "cooktop.fill",
"drop.fill", "flame.fill", "leaf.fill",
]),
("취미", [
(String(localized: "취미"), [
"gamecontroller.fill", "arcade.stick.console.fill", "dice.fill",
"puzzlepiece.fill", "music.note", "music.mic", "guitars.fill",
"pianokeys", "headphones", "hifispeaker.fill", "radio.fill",
@ -116,7 +116,7 @@ enum SymbolCatalog {
"fishing.rod" ,"tree.fill", "camera.macro", "bird.fill",
"balloon.2.fill", "party.popper.fill", "crown.fill",
]),
("이동", [
(String(localized: "이동"), [
"car.fill", "bus.fill", "tram.fill", "train.side.front.car",
"airplane", "ferry.fill", "sailboat.fill", "bicycle",
"scooter", "motorcycle.fill" ,"fuelpump.fill", "ev.charger.fill",
@ -124,7 +124,7 @@ enum SymbolCatalog {
"mappin.and.ellipse", "signpost.right.fill", "road.lanes",
"parkingsign", "steeringwheel", "suitcase.rolling.fill",
]),
("자연", [
(String(localized: "자연"), [
"sun.max.fill", "moon.fill", "moon.stars.fill", "sparkles",
"cloud.fill", "cloud.rain.fill", "cloud.snow.fill",
"cloud.sun.fill", "wind", "snowflake", "drop.fill",
@ -132,7 +132,7 @@ enum SymbolCatalog {
"camera.macro", "mountain.2.fill", "water.waves",
"globe.asia.australia.fill", "star.fill", "rainbow",
]),
("기호", [
(String(localized: "기호"), [
"circle.fill", "square.fill", "triangle.fill", "diamond.fill",
"seal.fill", "shield.fill", "flag.fill", "flag.checkered",
"tag.fill", "bell.fill", "bookmark.fill", "pin.fill",

File diff suppressed because it is too large Load Diff

View File

@ -121,7 +121,7 @@ struct ActionListView: View {
action.isFavorite.toggle()
} label: {
Label(
action.isFavorite ? "즐겨찾기 해제" : "즐겨찾기",
action.isFavorite ? String(localized: "즐겨찾기 해제") : String(localized: "즐겨찾기"),
systemImage: action.isFavorite ? "star.slash" : "star.fill"
)
}
@ -295,13 +295,13 @@ struct ActionDetailView: View {
let now = Date.now
switch action.trackingType {
case .time:
statRow("오늘", Format.durationShort(agg.seconds(for: action, in: math.dayRange(containing: now))))
statRow("이번 주", Format.durationShort(agg.seconds(for: action, in: math.weekRange(containing: now))))
statRow("이번 달", Format.durationShort(agg.seconds(for: action, in: math.monthRange(containing: now))))
statRow(String(localized: "오늘"), Format.durationShort(agg.seconds(for: action, in: math.dayRange(containing: now))))
statRow(String(localized: "이번 주"), Format.durationShort(agg.seconds(for: action, in: math.weekRange(containing: now))))
statRow(String(localized: "이번 달"), Format.durationShort(agg.seconds(for: action, in: math.monthRange(containing: now))))
case .count:
statRow("오늘", "\(agg.count(for: action, in: math.dayRange(containing: now)))")
statRow("이번 주", "\(agg.count(for: action, in: math.weekRange(containing: now)))")
statRow("이번 달", "\(agg.count(for: action, in: math.monthRange(containing: now)))")
statRow(String(localized: "오늘"), String(localized: "\(agg.count(for: action, in: math.dayRange(containing: now)))"))
statRow(String(localized: "이번 주"), String(localized: "\(agg.count(for: action, in: math.weekRange(containing: now)))"))
statRow(String(localized: "이번 달"), String(localized: "\(agg.count(for: action, in: math.monthRange(containing: now)))"))
}
}
@ -408,11 +408,11 @@ struct ActionEditorView: View {
}
} footer: {
Text(trackingType == .time
? "켜면 시간 측정을 종료할 때 메모 창이 떠요. 건너뛸 수 있어요."
: "켜면 횟수를 추가할 때마다 메모 창이 떠요. 건너뛸 수 있어요.")
? String(localized: "켜면 시간 측정을 종료할 때 메모 창이 떠요. 건너뛸 수 있어요.")
: String(localized: "켜면 횟수를 추가할 때마다 메모 창이 떠요. 건너뛸 수 있어요."))
}
}
.navigationTitle(action == nil ? "행동 추가" : "행동 수정")
.navigationTitle(action == nil ? String(localized: "행동 추가") : String(localized: "행동 수정"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {

View File

@ -108,7 +108,7 @@ struct GoalListView: View {
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if activeGoals.count > 1 {
Button(isReordering ? "완료" : "순서") {
Button(isReordering ? String(localized: "완료") : String(localized: "순서")) {
withAnimation {
isReordering.toggle()
editMode = isReordering ? .active : .inactive
@ -260,17 +260,17 @@ struct GoalRow: View {
if let end = goal.endDate {
return "\(Format.shortDate(goal.startDate)) ~ \(Format.shortDate(end))"
}
return "\(Format.shortDate(goal.startDate)) 시작 · 종료일 없음"
return String(localized: "\(Format.shortDate(goal.startDate)) 시작 · 종료일 없음")
}
@ViewBuilder
private var statusBadge: some View {
let (text, color): (String, Color) = {
if goal.needsConfirmation() { return ("확인 필요", AppTheme.yellow) }
if goal.needsConfirmation() { return (String(localized: "확인 필요"), AppTheme.yellow) }
switch goal.status {
case .inProgress: return ("진행 중", AppTheme.green)
case .achieved: return ("달성", AppTheme.green)
case .notAchieved: return ("미달성", Color.secondary)
case .inProgress: return (String(localized: "진행 중"), AppTheme.green)
case .achieved: return (String(localized: "달성"), AppTheme.green)
case .notAchieved: return (String(localized: "미달성"), Color.secondary)
}
}()
Text(text)
@ -453,7 +453,7 @@ struct GoalDetailView: View {
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if !goal.quests.isEmpty {
Button(editMode == .active ? "완료" : "순서") {
Button(editMode == .active ? String(localized: "완료") : String(localized: "순서")) {
withAnimation {
editMode = editMode == .active ? .inactive : .active
}
@ -566,7 +566,7 @@ struct GoalEditorView: View {
}
}
}
.navigationTitle(goal == nil ? "목표 추가" : "목표 수정")
.navigationTitle(goal == nil ? String(localized: "목표 추가") : String(localized: "목표 수정"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {

View File

@ -15,8 +15,8 @@ enum HistoryMode: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .list: return "목록"
case .timetable: return "타임테이블"
case .list: return String(localized: "목록")
case .timetable: return String(localized: "타임테이블")
}
}
}
@ -174,7 +174,7 @@ struct HistoryView: View {
private var filterLabel: String {
let names = allActions.filter(passesFilter).map(\.name)
if names.isEmpty { return "표시할 행동 없음" }
if names.isEmpty { return String(localized: "표시할 행동 없음") }
return names.count <= 2
? names.joined(separator: ", ")
: "행동 \(names.count)/\(allActions.count)개 표시 중"

View File

@ -67,7 +67,7 @@ struct MainView: View {
.navigationTitle("모음")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(isEditing ? "완료" : "배치 편집") {
Button(isEditing ? String(localized: "완료") : String(localized: "배치 편집")) {
withAnimation {
isEditing.toggle()
}
@ -283,7 +283,7 @@ struct MainView: View {
recordsAction = action
} label: {
Label(
action.trackingType == .time ? "시작·종료 시각 수동 입력" : "횟수 직접 입력·수정",
action.trackingType == .time ? String(localized: "시작·종료 시각 수동 입력") : String(localized: "횟수 직접 입력·수정"),
systemImage: "square.and.pencil"
)
}
@ -801,6 +801,8 @@ struct ActionButtonCell: View {
.foregroundStyle(.white.opacity(0.85))
}
.lineLimit(1)
// ("times")
.minimumScaleFactor(0.6)
.animation(.snappy(duration: 0.3), value: todayCount)
}
}
@ -808,7 +810,9 @@ struct ActionButtonCell: View {
private var todayLabel: String {
let agg = Aggregator()
let seconds = agg.seconds(for: action, in: agg.math.dayRange(containing: .now))
return seconds > 0 ? "오늘 \(Format.durationShort(seconds))" : "오늘 0분"
return seconds > 0
? String(localized: "오늘 \(Format.durationShort(seconds))")
: String(localized: "오늘 0분")
}
private var todayCount: Int {

View File

@ -21,7 +21,7 @@ struct QuestEditorView: View {
enum TargetKind: String, CaseIterable, Identifiable {
case action, tag
var id: String { rawValue }
var label: String { self == .action ? "행동" : "꼬리표" }
var label: String { self == .action ? String(localized: "행동") : String(localized: "꼬리표") }
}
//
@ -53,7 +53,7 @@ struct QuestEditorView: View {
periodSection
amountSection
}
.navigationTitle(quest == nil ? "다짐 추가" : "다짐 수정")
.navigationTitle(quest == nil ? String(localized: "다짐 추가") : String(localized: "다짐 수정"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {

View File

@ -37,7 +37,7 @@ struct ActionRecordsSheet: View {
showingAdd = true
} label: {
Label(
action.trackingType == .time ? "기록 직접 추가" : "횟수 직접 추가",
action.trackingType == .time ? String(localized: "기록 직접 추가") : String(localized: "횟수 직접 추가"),
systemImage: "plus.circle"
)
.foregroundStyle(AppTheme.green)

View File

@ -43,7 +43,7 @@ struct SettingsView: View {
var body: some View {
ScrollViewReader { proxy in
Form {
Section("화면") {
Section {
Picker("테마", selection: $theme) {
Text("라이트").tag("light")
Text("다크").tag("dark")
@ -53,6 +53,10 @@ struct SettingsView: View {
Text(lang.label).tag(lang.rawValue)
}
}
} header: {
Text("화면")
} footer: {
Text("언어를 바꾸면 앱을 완전히 종료했다가 다시 실행했을 때 적용돼요.")
}
Section {
ForEach(AppTab.allCases) { tab in
@ -126,7 +130,7 @@ struct SettingsView: View {
Label("프리미엄 기능", systemImage: "crown.fill")
.foregroundStyle(AppTheme.yellow)
Spacer()
Text(premium.isPremium ? "사용 중" : "무료 사용 중")
Text(premium.isPremium ? String(localized: "사용 중") : String(localized: "무료 사용 중"))
.font(.caption)
.foregroundStyle(.secondary)
}
@ -148,6 +152,10 @@ struct SettingsView: View {
// ' '
WidgetCenter.shared.reloadAllTimelines()
}
.onChange(of: language) {
// :
UserDefaults.standard.set([language], forKey: "AppleLanguages")
}
#if DEBUG
// : -settingsScrollGoal YES , -settingsScrollPremium YES
.onAppear {

View File

@ -184,9 +184,9 @@ struct StatsTabView: View {
private var currentPeriodLabel: String {
switch span {
case .day: return "오늘"
case .week: return "이번 주"
case .month: return "이번 달"
case .day: return String(localized: "오늘")
case .week: return String(localized: "이번 주")
case .month: return String(localized: "이번 달")
}
}
@ -310,7 +310,7 @@ struct StatsTabView: View {
var colors: [String: Color] = [:]
func tagNames(for action: Action) -> [(String, Color)] {
if action.tags.isEmpty { return [("꼬리표 없음", Color.gray)] }
if action.tags.isEmpty { return [(String(localized: "꼬리표 없음"), Color.gray)] }
return action.sortedTags.map { ($0.name, $0.color) }
}
@ -533,7 +533,7 @@ struct StatsTabView: View {
private func valueLabel(_ value: Double, type: TrackingType) -> String {
switch type {
case .time: return Format.durationShort(value)
case .count: return "\(Int(value.rounded()))"
case .count: return String(localized: "\(Int(value.rounded()))")
}
}
@ -551,7 +551,7 @@ struct StatsTabView: View {
Chart(dailyPoints(for: actions, type: type)) { point in
LineMark(
x: .value("날짜", point.dayKey, unit: .day),
y: .value(type == .time ? "시간" : "횟수", point.value)
y: .value(type == .time ? String(localized: "시간") : String(localized: "횟수"), point.value)
)
.foregroundStyle(by: .value("행동", point.actionName))
.symbol(by: .value("행동", point.actionName))
@ -575,7 +575,7 @@ struct StatsTabView: View {
}
}
}
.chartYAxisLabel(type == .time ? "시간(h)" : "횟수")
.chartYAxisLabel(type == .time ? String(localized: "시간(h)") : String(localized: "횟수"))
.frame(height: 200)
}
}
@ -593,14 +593,14 @@ struct StatsTabView: View {
Chart(weeklyPoints(for: actions, type: type)) { point in
LineMark(
x: .value("주차", point.weekLabel),
y: .value(type == .time ? "시간" : "횟수", point.value)
y: .value(type == .time ? String(localized: "시간") : String(localized: "횟수"), point.value)
)
.foregroundStyle(by: .value("행동", point.actionName))
.symbol(by: .value("행동", point.actionName))
.interpolationMethod(.monotone)
}
.chartForegroundStyleScale(domain: names, range: colors)
.chartYAxisLabel(type == .time ? "시간(h)" : "횟수")
.chartYAxisLabel(type == .time ? String(localized: "시간(h)") : String(localized: "횟수"))
.frame(height: 200)
}
}
@ -617,7 +617,7 @@ struct StatsTabView: View {
} else {
Chart(items) { item in
BarMark(
x: .value(type == .time ? "시간" : "횟수", type == .time ? item.value / 3600 : item.value),
x: .value(type == .time ? String(localized: "시간") : String(localized: "횟수"), type == .time ? item.value / 3600 : item.value),
y: .value("행동", item.name)
)
.foregroundStyle(item.color)
@ -628,7 +628,7 @@ struct StatsTabView: View {
.foregroundStyle(.secondary)
}
}
.chartXAxisLabel(type == .time ? "시간(h)" : "횟수")
.chartXAxisLabel(type == .time ? String(localized: "시간(h)") : String(localized: "횟수"))
.frame(height: CGFloat(items.count) * 40 + 30)
}
}

View File

@ -152,7 +152,7 @@ struct TagEditorView: View {
Text("미리 보기")
}
}
.navigationTitle(tag == nil ? "꼬리표 추가" : "꼬리표 수정")
.navigationTitle(tag == nil ? String(localized: "꼬리표 추가") : String(localized: "꼬리표 수정"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {

View File

@ -22,14 +22,24 @@ enum Format {
let h = total / 3600
let m = (total % 3600) / 60
if h > 0 {
return m > 0 ? "\(h)시간 \(m)" : "\(h)시간"
return m > 0
? String(localized: "\(h)시간 \(m)")
: String(localized: "\(h)시간")
}
if m > 0 { return "\(m)" }
return "\(total)"
if m > 0 { return String(localized: "\(m)") }
return String(localized: "\(total)")
}
static func weekdayShort(_ weekday: Int) -> String {
let names = ["", "", "", "", "", "", ""]
let names = [
String(localized: "", comment: "요일 축약(일요일)"),
String(localized: "", comment: "요일 축약(월요일)"),
String(localized: "", comment: "요일 축약(화요일)"),
String(localized: "", comment: "요일 축약(수요일)"),
String(localized: "", comment: "요일 축약(목요일)"),
String(localized: "", comment: "요일 축약(금요일)"),
String(localized: "", comment: "요일 축약(토요일)"),
]
let index = (weekday - 1) % 7
return names[max(0, index)]
}

View File

@ -106,9 +106,9 @@ enum SpanOption: String, AppEnum {
var label: String {
switch self {
case .day: return "오늘"
case .week: return "이번 주"
case .month: return "이번 달"
case .day: return String(localized: "오늘")
case .week: return String(localized: "이번 주")
case .month: return String(localized: "이번 달")
}
}
}
@ -127,7 +127,9 @@ struct ActionEntity: AppEntity {
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: trackingTypeRaw == TrackingType.count.rawValue ? "횟수 기록" : "시간 측정"
subtitle: trackingTypeRaw == TrackingType.count.rawValue
? LocalizedStringResource("횟수 기록")
: LocalizedStringResource("시간 측정")
)
}
@ -477,7 +479,7 @@ struct QuestProgressIntent: AppIntent {
case .atLeast:
detail = "\(Int(percent))%"
case .atMost:
detail = result.isAchieved ? "한도 안에서 잘 지키고 있어요" : "한도를 넘었어요"
detail = result.isAchieved ? String(localized: "한도 안에서 잘 지키고 있어요") : String(localized: "한도를 넘었어요")
}
return .result(
value: percent,

View File

@ -19,8 +19,8 @@ enum TrackingType: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .time: return "시간 측정"
case .count: return "횟수 기록"
case .time: return String(localized: "시간 측정")
case .count: return String(localized: "횟수 기록")
}
}
@ -209,9 +209,9 @@ enum GoalStatus: String, CaseIterable {
var label: String {
switch self {
case .inProgress: return "진행 중"
case .achieved: return "달성 완료"
case .notAchieved: return "미달성 종료"
case .inProgress: return String(localized: "진행 중")
case .achieved: return String(localized: "달성 완료")
case .notAchieved: return String(localized: "미달성 종료")
}
}
}
@ -302,10 +302,10 @@ enum QuestPeriod: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .daily: return "하루 단위"
case .weekly: return "일주일 단위"
case .monthly: return "한 달 단위"
case .custom: return "특정 기간"
case .daily: return String(localized: "하루 단위")
case .weekly: return String(localized: "일주일 단위")
case .monthly: return String(localized: "한 달 단위")
case .custom: return String(localized: "특정 기간")
}
}
}
@ -321,10 +321,10 @@ enum QuestScheduleMode: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .everyDay: return "매일"
case .weekdays: return "요일 지정"
case .monthDays: return "날짜 지정"
case .ordinalWeekday: return "몇째 주 요일"
case .everyDay: return String(localized: "매일")
case .weekdays: return String(localized: "요일 지정")
case .monthDays: return String(localized: "날짜 지정")
case .ordinalWeekday: return String(localized: "몇째 주 요일")
}
}
}
@ -337,8 +337,8 @@ enum QuestDirection: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .atLeast: return "이상 달성"
case .atMost: return "이하 유지"
case .atLeast: return String(localized: "이상 달성")
case .atMost: return String(localized: "이하 유지")
}
}
}
@ -410,7 +410,7 @@ extension Quest {
var targetName: String {
if let targetAction { return targetAction.name }
if let targetTag { return "#\(targetTag.name)" }
return "대상 없음"
return String(localized: "대상 없음")
}
var targetSymbol: String {
@ -437,7 +437,7 @@ extension Quest {
}
var targetValueLabel: String {
measure == .time ? Format.durationShort(targetSeconds) : "\(targetCount)"
measure == .time ? Format.durationShort(targetSeconds) : String(localized: "\(targetCount)")
}
/// (: " ··")
@ -446,22 +446,22 @@ extension Quest {
case .daily:
switch scheduleMode {
case .everyDay:
return "매일"
return String(localized: "매일")
case .weekdays:
let names = weekdays.sorted().map { Format.weekdayShort($0) }
return "매주 " + names.joined(separator: "·")
return String(localized: "매주 \(names.joined(separator: "·"))")
case .monthDays:
let names = monthDays.sorted().map { "\($0)" }
return "매달 " + names.joined(separator: ", ")
let names = monthDays.sorted().map { String(localized: "\($0)") }
return String(localized: "매달 \(names.joined(separator: ", "))")
case .ordinalWeekday:
return "매달 \(ordinalWeek)째 주 \(Format.weekdayShort(ordinalWeekday))요일"
return String(localized: "매달 \(ordinalWeek)째 주 \(Format.weekdayShort(ordinalWeekday))요일")
}
case .weekly:
return "매주"
return String(localized: "매주")
case .monthly:
return "매달"
return String(localized: "매달")
case .custom:
guard let customStart, let customEnd else { return "특정 기간" }
guard let customStart, let customEnd else { return String(localized: "특정 기간") }
return "\(Format.shortDate(customStart)) ~ \(Format.shortDate(customEnd))"
}
}

View File

@ -15,9 +15,9 @@ enum StatSpan: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .day: return "하루"
case .week: return "주간"
case .month: return "월간"
case .day: return String(localized: "하루")
case .week: return String(localized: "주간")
case .month: return String(localized: "월간")
}
}
}

View File

@ -40,8 +40,8 @@ enum GoalCardStyle: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .perQuest: return "다짐별로 각각"
case .combined: return "전체 다짐 합산"
case .perQuest: return String(localized: "다짐별로 각각")
case .combined: return String(localized: "전체 다짐 합산")
}
}
}
@ -56,12 +56,12 @@ enum TabBarConfig {
/// ()
enum MinSessionOption {
static let choices: [(seconds: Int, label: String)] = [
(0, "사용 안 함"),
(3, "3초"),
(5, "5초"),
(10, "10초"),
(30, "30초"),
(60, "1분"),
(0, String(localized: "사용 안 함")),
(3, String(localized: "3초")),
(5, String(localized: "5초")),
(10, String(localized: "10초")),
(30, String(localized: "30초")),
(60, String(localized: "1분")),
]
}
@ -73,8 +73,8 @@ enum LiveActivityMode: String, CaseIterable, Identifiable {
var label: String {
switch self {
case .latest: return "가장 나중에 시작한 행동"
case .earliest: return "가장 먼저 시작한 행동"
case .latest: return String(localized: "가장 나중에 시작한 행동")
case .earliest: return String(localized: "가장 먼저 시작한 행동")
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -126,7 +126,9 @@ struct QuestRingActionCellView: View {
private var percentText: String {
if cell.isAtMost {
return cell.isAchieved ? "\(spanLabel) 한도 지킴" : "\(spanLabel) 한도 초과"
return cell.isAchieved
? String(localized: "\(spanLabel) 한도 지킴")
: String(localized: "\(spanLabel) 한도 초과")
}
return "\(spanLabel) \(Format.percent(cell.displayRatio))"
}

View File

@ -27,9 +27,9 @@ enum StatsChartSpan: String, AppEnum {
var title: String {
switch self {
case .week: return "일주일 통계"
case .monthByDay: return "한 달 통계 (일별)"
case .monthByWeek: return "한 달 통계 (주별)"
case .week: return String(localized: "일주일 통계")
case .monthByDay: return String(localized: "한 달 통계 (일별)")
case .monthByWeek: return String(localized: "한 달 통계 (주별)")
}
}
}
@ -203,14 +203,14 @@ struct StatsChartWidgetView: View {
if let day = point.day {
LineMark(
x: .value("날짜", day, unit: .day),
y: .value(entry.isTimeType ? "시간" : "", point.value)
y: .value(entry.isTimeType ? String(localized: "시간") : String(localized: ""), point.value)
)
.foregroundStyle(by: .value("행동", point.actionName))
.interpolationMethod(.monotone)
} else if let weekLabel = point.weekLabel {
LineMark(
x: .value("주차", weekLabel),
y: .value(entry.isTimeType ? "시간" : "", point.value)
y: .value(entry.isTimeType ? String(localized: "시간") : String(localized: ""), point.value)
)
.foregroundStyle(by: .value("행동", point.actionName))
.interpolationMethod(.monotone)
@ -218,7 +218,7 @@ struct StatsChartWidgetView: View {
}
.chartForegroundStyleScale(domain: entry.names, range: colors)
.chartLegend(family == .systemSmall ? .hidden : .visible)
.chartYAxisLabel(family == .systemSmall ? "" : (entry.isTimeType ? "시간(h)" : "횟수"))
.chartYAxisLabel(family == .systemSmall ? "" : (entry.isTimeType ? String(localized: "시간(h)") : String(localized: "횟수")))
.chartXAxis {
if entry.isWeekAxis {
AxisMarks { _ in