언어 설정 화면 추가 및 언어 헤더 적용

설정에서 시스템/한국어/영어/일본어 선택을 지원한다.

선택 시 Accept-Language 헤더와 UI locale을 즉시 반영한다.

언어 변경 후 스플래시를 거쳐 메인으로 소프트 재시작한다.
This commit is contained in:
Yu Sung
2025-12-16 22:56:37 +09:00
parent b2c94a44d9
commit 0285f62ecb
22 changed files with 512 additions and 61 deletions

View File

@@ -0,0 +1,108 @@
//
// LanguageService.swift
// SodaLive
//
// Created by Junie (AI) on 2025/12/16.
//
import Foundation
enum LanguageHeaderProvider {
private static let defaultsKey = "app.language"
// (ko|en|ja). initialize() .
static var current: String = {
// OS
return mapPreferredToSupported(Locale.preferredLanguages.first)
}()
/// : , OS .
static func initialize() {
if let raw = UserDefaults.standard.string(forKey: defaultsKey),
let option = LanguageOption(rawValue: raw),
option != .system {
//
current = option.rawValue
return
}
// OS
current = mapPreferredToSupported(Locale.preferredLanguages.first)
}
private static func mapPreferredToSupported(_ tag: String?) -> String {
guard let tag = tag else { return "en" }
if tag.hasPrefix("ko") { return "ko" }
if tag.hasPrefix("ja") { return "ja" }
return "en"
}
}
actor LanguageService {
private let repository: LanguageRepository
private let environment: LanguageEnvironment
init(repository: LanguageRepository, environment: LanguageEnvironment) {
self.repository = repository
self.environment = environment
}
func bootstrap() async {
let saved = (try? await repository.load()) ?? .system
await applyLanguage(saved)
}
func applyLanguage(_ option: LanguageOption) async {
do { try await repository.save(option) } catch { DEBUG_LOG("[Language] save failed: \(error)") }
let locale = resolveLocale(option)
let header = resolveHeader(option, locale: locale)
LanguageHeaderProvider.current = header
await MainActor.run { [weak environment] in
environment?.locale = locale
}
DEBUG_LOG("[Language] changed to option=\(option.rawValue) locale=\(locale.identifier) header=\(header)")
}
func currentLocale() async -> Locale {
await MainActor.run { environment.locale }
}
func currentLanguageCodeForHeader() async -> String { LanguageHeaderProvider.current }
// MARK: - Private
private func resolveLocale(_ option: LanguageOption) -> Locale {
switch option {
case .system:
guard let first = Locale.preferredLanguages.first else { return Locale(identifier: "en_US") }
return mapToSupported(first)
case .ko: return Locale(identifier: "ko_KR")
case .en: return Locale(identifier: "en_US")
case .ja: return Locale(identifier: "ja_JP")
}
}
private func resolveHeader(_ option: LanguageOption, locale: Locale) -> String {
switch option {
case .system:
let id = locale.identifier
if id.hasPrefix("ko") { return "ko" }
if id.hasPrefix("ja") { return "ja" }
return "en"
case .ko: return "ko"
case .en: return "en"
case .ja: return "ja"
}
}
private func mapToSupported(_ tag: String) -> Locale {
if tag.hasPrefix("ko") { return .init(identifier: "ko_KR") }
if tag.hasPrefix("ja") { return .init(identifier: "ja_JP") }
return .init(identifier: "en_US")
}
}
// Composition root for language feature
enum LanguageContainer {
static let environment = LanguageEnvironment()
static let repository: LanguageRepository = UserDefaultsLanguageRepository()
static let service = LanguageService(repository: repository, environment: environment)
}