Skip to content

Commit 7380b77

Browse files
author
Albert
committed
feat(i18n): add English localization with in-app language picker
The app shipped with Chinese strings hard-coded in SwiftUI views. Because SwiftUI treats Text("…") / Button("…") / .help("…") as LocalizedStringKey lookups, all that's needed for full English support is a string catalog — no code rewrite of the views. Changes: - Resources/Localizable.xcstrings: Xcode String Catalog with the 165 unique UI strings (162 extracted from views + 3 new ones for the picker itself). Source language is zh-Hans; each entry carries a vetted English translation. - project.yml: declare developmentLanguage zh-Hans and KNOWN_REGIONS "en zh-Hans Base" so the build pipeline compiles the catalog into per-locale .lproj/Localizable.strings. - SettingsService.PreferredLanguage enum (system / zh-Hans / en), persisted to UserDefaults at key fd.preferredLanguage. - SettingsView: new "语言 / Language" picker bound to the enum. - FreeDisplayApp: applies the resolved Locale via .environment(\.locale, ...) on the MenuBarView root so SwiftUI re-renders with the chosen language without an app restart. Verified with xcodebuild Release: BUILD SUCCEEDED, only pre-existing Swift 6 concurrency warnings (no new errors).
1 parent c4bd222 commit 7380b77

6 files changed

Lines changed: 2896 additions & 2 deletions

File tree

FreeDisplay.xcodeproj/project.pbxproj

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
E061011F318208D7FF94D963 /* CGHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5BF8AFF876572E97D370A0 /* CGHelpers.swift */; };
4949
E201AC4CE5BCEC009C926206 /* ArrangementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22030BFC8A680A596A5764C9 /* ArrangementService.swift */; };
5050
E6D99B12980703693C61A687 /* SettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5483CA367C0673369C00E5A /* SettingsService.swift */; };
51+
F17E84B38238640BEBC366AA /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = E0CF278C47F118CA2F2DDC62 /* Localizable.xcstrings */; };
5152
F9A1FF13C0CAC41003C5B201 /* SavePresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58474F9C406F992241963112 /* SavePresetView.swift */; };
5253
FE0FA160B19EA273B4F766CC /* BrightnessSliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76D8EE5271581552ED081AF /* BrightnessSliderView.swift */; };
5354
/* End PBXBuildFile section */
@@ -96,6 +97,7 @@
9697
D732A8A7009C298C78D6895C /* FreeDisplayApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeDisplayApp.swift; sourceTree = "<group>"; };
9798
D76D8EE5271581552ED081AF /* BrightnessSliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightnessSliderView.swift; sourceTree = "<group>"; };
9899
DB5BF8AFF876572E97D370A0 /* CGHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGHelpers.swift; sourceTree = "<group>"; };
100+
E0CF278C47F118CA2F2DDC62 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
99101
E5483CA367C0673369C00E5A /* SettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsService.swift; sourceTree = "<group>"; };
100102
F22B3A7EF51E72B078901699 /* ImageAdjustmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAdjustmentView.swift; sourceTree = "<group>"; };
101103
F606C5D467CD44404890F6EC /* VirtualDisplayService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualDisplayService.swift; sourceTree = "<group>"; };
@@ -191,6 +193,7 @@
191193
isa = PBXGroup;
192194
children = (
193195
AED959D5249F419ADBDBA1F4 /* Assets.xcassets */,
196+
E0CF278C47F118CA2F2DDC62 /* Localizable.xcstrings */,
194197
);
195198
path = Resources;
196199
sourceTree = "<group>";
@@ -254,16 +257,17 @@
254257
};
255258
};
256259
buildConfigurationList = 8EA410054BD32F42B7BB9BC3 /* Build configuration list for PBXProject "FreeDisplay" */;
257-
compatibilityVersion = "Xcode 14.0";
258-
developmentRegion = en;
260+
developmentRegion = "zh-Hans";
259261
hasScannedForEncodings = 0;
260262
knownRegions = (
261263
Base,
262264
en,
265+
"zh-Hans",
263266
);
264267
mainGroup = AD8C2B4D7F682DFFA53EF6B7;
265268
minimizedProjectReferenceProxies = 1;
266269
preferredProjectObjectVersion = 77;
270+
productRefGroup = 4D2DDE7731949C0697A6167C /* Products */;
267271
projectDirPath = "";
268272
projectRoot = "";
269273
targets = (
@@ -279,6 +283,7 @@
279283
files = (
280284
80F888F001C7F53E4FA362AD /* Assets.xcassets in Resources */,
281285
7BC8C11889A477C6501E5BC8 /* Assets.xcassets in Resources */,
286+
F17E84B38238640BEBC366AA /* Localizable.xcstrings in Resources */,
282287
);
283288
runOnlyForDeploymentPostprocessing = 0;
284289
};
@@ -406,6 +411,7 @@
406411
INFOPLIST_KEY_LSUIElement = YES;
407412
INFOPLIST_KEY_NSHumanReadableCopyright = "FreeDisplay - Free & Open Source";
408413
INFOPLIST_KEY_NSScreenCaptureUsageDescription = "FreeDisplay 需要屏幕录制权限以显示各显示器的实时画面预览。";
414+
KNOWN_REGIONS = "en zh-Hans Base";
409415
LD_RUNPATH_SEARCH_PATHS = (
410416
"$(inherited)",
411417
"@executable_path/../Frameworks",
@@ -433,6 +439,7 @@
433439
INFOPLIST_KEY_LSUIElement = YES;
434440
INFOPLIST_KEY_NSHumanReadableCopyright = "FreeDisplay - Free & Open Source";
435441
INFOPLIST_KEY_NSScreenCaptureUsageDescription = "FreeDisplay 需要屏幕录制权限以显示各显示器的实时画面预览。";
442+
KNOWN_REGIONS = "en zh-Hans Base";
436443
LD_RUNPATH_SEARCH_PATHS = (
437444
"$(inherited)",
438445
"@executable_path/../Frameworks",

FreeDisplay/App/FreeDisplayApp.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import SwiftUI
44
struct FreeDisplayApp: App {
55
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
66
@StateObject private var displayManager = DisplayManager()
7+
// Observe SettingsService so the menu re-renders when the user changes
8+
// language via the Settings picker — SwiftUI's LocalizedStringKey lookups
9+
// honour the .environment(\.locale, ...) override we apply below.
10+
@ObservedObject private var settings = SettingsService.shared
711

812
var body: some Scene {
913
MenuBarExtra {
1014
MenuBarView()
1115
.environmentObject(displayManager)
16+
.environment(\.locale, settings.preferredLanguage.resolvedLocale)
1217
.task {
1318
// Enable "external above built-in" arrangement by default on first launch.
1419
let defaults = UserDefaults.standard

0 commit comments

Comments
 (0)