Skip to content

Commit e5e9d8c

Browse files
committed
Add UI localization layer and actor-based core
1 parent b0c5986 commit e5e9d8c

18 files changed

Lines changed: 544 additions & 112 deletions

CHANGELOG.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
66

77
## [Unreleased]
88

9+
## [0.4.0] - 2026-03-13
10+
11+
### Added
12+
- Added the new `LiveLocalizationUI` product for UI-level localization wrappers.
13+
- Added `LiveLocalizedText` for SwiftUI.
14+
- Added `LiveLocalizedLabel` for UIKit.
15+
- Added `LiveLocalizationTextAnimation` with `.none` and `.fade` styles.
16+
- Added UI-layer request coordination tests and demo coverage for the new wrappers.
17+
918
### Changed
10-
- Renamed the package surface from `DebugLocalization` to `LiveLocalizationKit`.
11-
- Renamed primary modules to `LiveLocalizationCore` and `LiveLocalizationTranslationSupport`.
12-
- Renamed the shared runtime API to `LiveLocalization`, `LiveLocalizer`, `LiveLocalizationError`, and `LiveLocalizationVersion`.
19+
- Moved shared localization state to actor-based concurrency instead of relying on `@unchecked Sendable`.
20+
- Updated `LiveLocalization` shared configuration flow to async usage.
21+
- Updated the demo app to show both direct core API usage and the new UI wrapper usage in SwiftUI and UIKit.
22+
- Updated README examples to reflect the current package naming, install path, and UI-layer APIs.
23+
- Continued the package rename from the previous `DebugLocalization*` surface to the `LiveLocalization*` naming now used across the repository.
1324

1425
## [0.3.0] - 2026-03-11
1526

LiveLocalizationDemo.xcodeproj/project.pbxproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
94B75D632F609AAB0000BA49 /* LiveLocalizationCore in Frameworks */ = {isa = PBXBuildFile; productRef = 94B75D622F609AAB0000BA49 /* LiveLocalizationCore */; };
1111
94B75D652F609AAB0000BA49 /* LiveLocalizationTranslationSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 94B75D642F609AAB0000BA49 /* LiveLocalizationTranslationSupport */; };
12+
94CDC0012F7000010000BA49 /* LiveLocalizationUI in Frameworks */ = {isa = PBXBuildFile; productRef = 94CDC0002F7000010000BA49 /* LiveLocalizationUI */; };
1213
/* End PBXBuildFile section */
1314

1415
/* Begin PBXContainerItemProxy section */
@@ -58,6 +59,7 @@
5859
buildActionMask = 2147483647;
5960
files = (
6061
94B75D632F609AAB0000BA49 /* LiveLocalizationCore in Frameworks */,
62+
94CDC0012F7000010000BA49 /* LiveLocalizationUI in Frameworks */,
6163
94B75D652F609AAB0000BA49 /* LiveLocalizationTranslationSupport in Frameworks */,
6264
);
6365
runOnlyForDeploymentPostprocessing = 0;
@@ -120,6 +122,7 @@
120122
name = LiveLocalizationKit;
121123
packageProductDependencies = (
122124
94B75D622F609AAB0000BA49 /* LiveLocalizationCore */,
125+
94CDC0002F7000010000BA49 /* LiveLocalizationUI */,
123126
94B75D642F609AAB0000BA49 /* LiveLocalizationTranslationSupport */,
124127
);
125128
productName = LiveLocalizationKit;
@@ -607,6 +610,10 @@
607610
isa = XCSwiftPackageProductDependency;
608611
productName = LiveLocalizationTranslationSupport;
609612
};
613+
94CDC0002F7000010000BA49 /* LiveLocalizationUI */ = {
614+
isa = XCSwiftPackageProductDependency;
615+
productName = LiveLocalizationUI;
616+
};
610617
/* End XCSwiftPackageProductDependency section */
611618
};
612619
rootObject = 946450652F5F665E00F1661D /* Project object */;

LiveLocalizationDemo.xcodeproj/xcshareddata/xcschemes/LiveLocalizationKit.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
buildConfiguration = "Debug"
3535
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
3636
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37-
language = "zh-Hant"
37+
language = "ko"
3838
launchStyle = "0"
3939
useCustomWorkingDirectory = "NO"
4040
ignoresPersistentStateOnLaunch = "NO"

LiveLocalizationKit/ContentView.swift

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,46 @@
11
import SwiftUI
22
import LiveLocalizationCore
3+
import LiveLocalizationUI
34

45
struct ContentView: View {
56
private let englishSourceText = "Payment successful"
7+
private let wrapperSourceText = "Continue"
68

7-
@State private var displayedText: String
8-
9-
init() {
10-
_displayedText = State(initialValue: englishSourceText)
11-
}
9+
@State private var coreLocalizedText = "Payment successful"
1210

1311
var body: some View {
14-
VStack(spacing: 16) {
12+
VStack(spacing: 24) {
1513
Text("SwiftUI Demo")
1614
.font(.headline)
17-
Text(displayedText)
15+
16+
VStack(spacing: 8) {
17+
Text("Core API")
18+
.font(.subheadline)
19+
.foregroundStyle(.secondary)
20+
21+
Text(coreLocalizedText)
22+
.font(.title2)
23+
.multilineTextAlignment(.center)
24+
.padding(.horizontal)
25+
}
26+
27+
VStack(spacing: 8) {
28+
Text("UI Wrapper")
29+
.font(.subheadline)
30+
.foregroundStyle(.secondary)
31+
32+
LiveLocalizedText(
33+
wrapperSourceText,
34+
animationStyle: .fade
35+
)
1836
.font(.title2)
1937
.multilineTextAlignment(.center)
2038
.padding(.horizontal)
39+
}
2140
}
2241
.padding()
2342
.task {
24-
displayedText = await englishSourceText.localize()
43+
coreLocalizedText = await englishSourceText.localize()
2544
}
2645
}
2746
}

LiveLocalizationKit/LiveLocalizationKitApp.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@ import LiveLocalizationTranslationSupport
55
@main
66
struct LiveLocalizationKitApp: App {
77
private let shouldPresentPreparationGate: Bool
8+
@State private var isConfigured = false
89

910
init() {
1011
shouldPresentPreparationGate = true
11-
LiveLocalization.configure(provider: AppleTranslationProvider())
1212
}
1313

1414
var body: some Scene {
1515
WindowGroup {
16-
RootDemoView(shouldPresentPreparationGate: shouldPresentPreparationGate)
16+
Group {
17+
if isConfigured {
18+
RootDemoView(shouldPresentPreparationGate: shouldPresentPreparationGate)
19+
} else {
20+
ProgressView()
21+
}
22+
}
23+
.task {
24+
guard !isConfigured else { return }
25+
await LiveLocalization.configure(provider: AppleTranslationProvider())
26+
isConfigured = true
27+
}
1728
}
1829
}
1930
}
Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import UIKit
22
import LiveLocalizationCore
3+
import LiveLocalizationUI
34

45
final class UIKitDemoViewController: UIViewController {
5-
private let englishSourceText = "Delete"
6+
private let coreSourceText = "Delete"
7+
private let wrapperSourceText = "Continue"
68

7-
private let label: UILabel = {
9+
private let coreTitleLabel: UILabel = {
10+
let label = UILabel()
11+
label.numberOfLines = 1
12+
label.textAlignment = .center
13+
label.font = .preferredFont(forTextStyle: .headline)
14+
label.text = "Core API"
15+
label.translatesAutoresizingMaskIntoConstraints = false
16+
return label
17+
}()
18+
19+
private let coreLabel: UILabel = {
820
let label = UILabel()
921
label.numberOfLines = 0
1022
label.textAlignment = .center
@@ -13,6 +25,26 @@ final class UIKitDemoViewController: UIViewController {
1325
return label
1426
}()
1527

28+
private let wrapperTitleLabel: UILabel = {
29+
let label = UILabel()
30+
label.numberOfLines = 1
31+
label.textAlignment = .center
32+
label.font = .preferredFont(forTextStyle: .headline)
33+
label.text = "UI Wrapper"
34+
label.translatesAutoresizingMaskIntoConstraints = false
35+
return label
36+
}()
37+
38+
private let wrapperLabel: LiveLocalizedLabel = {
39+
let label = LiveLocalizedLabel()
40+
label.numberOfLines = 0
41+
label.textAlignment = .center
42+
label.font = .preferredFont(forTextStyle: .title2)
43+
label.animationStyle = .fade
44+
label.translatesAutoresizingMaskIntoConstraints = false
45+
return label
46+
}()
47+
1648
@available(*, unavailable)
1749
required init?(coder: NSCoder) {
1850
fatalError("init(coder:) has not been implemented")
@@ -26,21 +58,32 @@ final class UIKitDemoViewController: UIViewController {
2658
override func viewDidLoad() {
2759
super.viewDidLoad()
2860
view.backgroundColor = .systemBackground
29-
view.addSubview(label)
61+
let stackView = UIStackView(arrangedSubviews: [
62+
coreTitleLabel,
63+
coreLabel,
64+
wrapperTitleLabel,
65+
wrapperLabel
66+
])
67+
stackView.axis = .vertical
68+
stackView.spacing = 12
69+
stackView.translatesAutoresizingMaskIntoConstraints = false
70+
view.addSubview(stackView)
3071

3172
NSLayoutConstraint.activate([
32-
label.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
33-
label.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
34-
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
73+
stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
74+
stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
75+
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
3576
])
3677

37-
label.text = englishSourceText
78+
coreLabel.text = coreSourceText
3879
Task { [weak self] in
3980
guard let self else { return }
40-
let localized = await englishSourceText.localize()
81+
let localized = await coreSourceText.localize()
4182
await MainActor.run {
42-
self.label.text = localized
83+
self.coreLabel.text = localized
4384
}
4485
}
86+
87+
wrapperLabel.setLocalizedText(wrapperSourceText)
4588
}
4689
}

Package.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ let package = Package(
1313
name: "LiveLocalizationCore",
1414
targets: ["LiveLocalizationCore"]
1515
),
16+
.library(
17+
name: "LiveLocalizationUI",
18+
targets: ["LiveLocalizationUI"]
19+
),
1620
.library(
1721
name: "LiveLocalizationTranslationSupport",
1822
targets: ["LiveLocalizationTranslationSupport"]
@@ -22,6 +26,10 @@ let package = Package(
2226
.target(
2327
name: "LiveLocalizationCore"
2428
),
29+
.target(
30+
name: "LiveLocalizationUI",
31+
dependencies: ["LiveLocalizationCore"]
32+
),
2533
.target(
2634
name: "LiveLocalizationTranslationSupport",
2735
dependencies: ["LiveLocalizationCore"]
@@ -30,6 +38,7 @@ let package = Package(
3038
name: "LiveLocalizationCoreTests",
3139
dependencies: [
3240
"LiveLocalizationCore",
41+
"LiveLocalizationUI",
3342
"LiveLocalizationTranslationSupport"
3443
]
3544
)

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ https://github.com/MikeChen1109/LiveLocalizationKit.git
2222
Available products:
2323

2424
- `LiveLocalizationCore`
25+
- `LiveLocalizationUI`
2526
- `LiveLocalizationTranslationSupport`
2627

2728
## Quick Start
@@ -32,12 +33,35 @@ Configure Apple Translation-based preview:
3233
import LiveLocalizationCore
3334
import LiveLocalizationTranslationSupport
3435

35-
LiveLocalization.configure(provider: AppleTranslationProvider())
36+
await LiveLocalization.configure(provider: AppleTranslationProvider())
3637
let localized = await "Settings".localize()
3738
```
3839

3940
For lightweight development flows, `LiveLocalizationCore` also includes providers such as `PseudoLocalizationProvider`, `MockLocalizationProvider`, and `PassthroughLocalizationProvider`.
4041

42+
## UI Layer
43+
44+
`LiveLocalizationUI` adds simple view wrappers on top of the core localizer layer.
45+
46+
SwiftUI:
47+
48+
```swift
49+
import SwiftUI
50+
import LiveLocalizationUI
51+
52+
LiveLocalizedText("Continue")
53+
```
54+
55+
UIKit:
56+
57+
```swift
58+
import UIKit
59+
import LiveLocalizationUI
60+
61+
let label = LiveLocalizedLabel()
62+
label.setLocalizedText("Continue")
63+
```
64+
4165
## Apple Translation Preview
4266

4367
If you want a debug flow that checks whether required language packs are available and guides preparation before showing the UI:

Sources/LiveLocalizationCore/LiveLocalization.swift

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,45 @@ import Foundation
33
public enum LiveLocalization {
44
private static let sharedStore = SharedLocalizerStore()
55

6-
public static func configure(provider: any LocalizationProvider) {
7-
sharedStore.setLocalizer(LiveLocalizer(provider: provider))
6+
public static func configure(provider: any LocalizationProvider) async {
7+
await sharedStore.setLocalizer(LiveLocalizer(provider: provider))
88
}
99

10-
public static func configure(localizer: LiveLocalizer) {
11-
sharedStore.setLocalizer(localizer)
10+
public static func configure(localizer: LiveLocalizer) async {
11+
await sharedStore.setLocalizer(localizer)
1212
}
1313

1414
public static var localizer: LiveLocalizer {
15-
sharedStore.localizer
15+
get async {
16+
await sharedStore.localizer
17+
}
1618
}
1719

1820
public static var canLocalizeSynchronously: Bool {
19-
localizer.canLocalizeSynchronously
21+
get async {
22+
let localizer = await localizer
23+
return await localizer.canLocalizeSynchronously
24+
}
2025
}
2126

22-
public static func clearCache() {
23-
localizer.clearCache()
27+
public static func clearCache() async {
28+
let localizer = await localizer
29+
await localizer.clearCache()
2430
}
2531

26-
public static func reset() {
27-
configure(provider: PseudoLocalizationProvider())
32+
public static func reset() async {
33+
await configure(provider: PseudoLocalizationProvider())
2834
}
2935
}
3036

31-
private final class SharedLocalizerStore: @unchecked Sendable {
32-
private let lock = NSLock()
37+
private actor SharedLocalizerStore {
3338
private var currentLocalizer = LiveLocalizer(provider: PseudoLocalizationProvider())
3439

3540
var localizer: LiveLocalizer {
36-
lock.lock()
37-
defer { lock.unlock() }
3841
return currentLocalizer
3942
}
4043

4144
func setLocalizer(_ localizer: LiveLocalizer) {
42-
lock.lock()
4345
currentLocalizer = localizer
44-
lock.unlock()
4546
}
4647
}

0 commit comments

Comments
 (0)