Skip to content

Commit 69dfc52

Browse files
committed
Add Localization Support and Language Switching Feature
• Introduced multilingual support with localizations for English (en), Greek (el), and Albanian (sq). • Added Localizable.xcstrings file for managing translated strings. • Implemented language switching in the app’s menu with emoji flag options. • Created a Bundle extension for dynamic language switching without restarting the app. • Updated UI to reflect the selected language dynamically. • Enhanced localization handling for menu items, alerts, and messages. • Modified Info.plist to include supported localizations. • Improved loadImage function for better error handling and added priority control. • Ensured all strings are extracted for localization consistency. This commit enhances user experience by allowing seamless language customization.
1 parent 88f5f6b commit 69dfc52

File tree

5 files changed

+381
-13
lines changed

5 files changed

+381
-13
lines changed

Moti/Moti.xcodeproj/project.pbxproj

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
2AD9D0392CE6104D00B9873A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2AD9D0382CE6104D00B9873A /* Localizable.xcstrings */; };
1011
2C062A512C7A97CF0010D9F4 /* Moti.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C062A502C7A97CF0010D9F4 /* Moti.swift */; };
1112
2C062A532C7A97CF0010D9F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C062A522C7A97CF0010D9F4 /* ContentView.swift */; };
1213
2C062A552C7A97CF0010D9F4 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C062A542C7A97CF0010D9F4 /* Item.swift */; };
@@ -38,6 +39,7 @@
3839

3940
/* Begin PBXFileReference section */
4041
1CD041F23F0293042067A6C1 /* Pods-Moti.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Moti.release.xcconfig"; path = "Target Support Files/Pods-Moti/Pods-Moti.release.xcconfig"; sourceTree = "<group>"; };
42+
2AD9D0382CE6104D00B9873A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
4143
2C062A4D2C7A97CF0010D9F4 /* Moti.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Moti.app; sourceTree = BUILT_PRODUCTS_DIR; };
4244
2C062A502C7A97CF0010D9F4 /* Moti.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Moti.swift; sourceTree = "<group>"; };
4345
2C062A522C7A97CF0010D9F4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -157,6 +159,7 @@
157159
2C062A6C2C7AA2FF0010D9F4 /* Supporting Files */ = {
158160
isa = PBXGroup;
159161
children = (
162+
2AD9D0382CE6104D00B9873A /* Localizable.xcstrings */,
160163
);
161164
path = "Supporting Files";
162165
sourceTree = "<group>";
@@ -201,7 +204,7 @@
201204
attributes = {
202205
BuildIndependentTargetsInParallel = 1;
203206
LastSwiftUpdateCheck = 1540;
204-
LastUpgradeCheck = 1540;
207+
LastUpgradeCheck = 1610;
205208
TargetAttributes = {
206209
2C062A4C2C7A97CF0010D9F4 = {
207210
CreatedOnToolsVersion = 15.4;
@@ -215,6 +218,9 @@
215218
knownRegions = (
216219
en,
217220
Base,
221+
shq,
222+
el,
223+
sq,
218224
);
219225
mainGroup = 2C062A442C7A97CF0010D9F4;
220226
productRefGroup = 2C062A4E2C7A97CF0010D9F4 /* Products */;
@@ -233,6 +239,7 @@
233239
files = (
234240
2C062A5A2C7A97D00010D9F4 /* Preview Assets.xcassets in Resources */,
235241
2CADE6792C833A17001FE1C2 /* GoogleService-Info.plist in Resources */,
242+
2AD9D0392CE6104D00B9873A /* Localizable.xcstrings in Resources */,
236243
2C062A572C7A97D00010D9F4 /* Assets.xcassets in Resources */,
237244
2C062A662C7A9FCB0010D9F4 /* Info.plist in Resources */,
238245
);
@@ -333,6 +340,7 @@
333340
CLANG_WARN_UNREACHABLE_CODE = YES;
334341
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
335342
COPY_PHASE_STRIP = NO;
343+
DEAD_CODE_STRIPPING = YES;
336344
DEBUG_INFORMATION_FORMAT = dwarf;
337345
ENABLE_STRICT_OBJC_MSGSEND = YES;
338346
ENABLE_TESTABILITY = YES;
@@ -359,6 +367,7 @@
359367
ONLY_ACTIVE_ARCH = YES;
360368
SDKROOT = macosx;
361369
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
370+
SWIFT_EMIT_LOC_STRINGS = YES;
362371
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
363372
};
364373
name = Debug;
@@ -397,6 +406,7 @@
397406
CLANG_WARN_UNREACHABLE_CODE = YES;
398407
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
399408
COPY_PHASE_STRIP = NO;
409+
DEAD_CODE_STRIPPING = YES;
400410
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
401411
ENABLE_NS_ASSERTIONS = NO;
402412
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -416,6 +426,7 @@
416426
MTL_FAST_MATH = YES;
417427
SDKROOT = macosx;
418428
SWIFT_COMPILATION_MODE = wholemodule;
429+
SWIFT_EMIT_LOC_STRINGS = YES;
419430
};
420431
name = Release;
421432
};
@@ -430,6 +441,7 @@
430441
CODE_SIGN_STYLE = Automatic;
431442
COMBINE_HIDPI_IMAGES = YES;
432443
CURRENT_PROJECT_VERSION = 1;
444+
DEAD_CODE_STRIPPING = YES;
433445
DEVELOPMENT_ASSET_PATHS = "\"Moti/Preview Content\"";
434446
DEVELOPMENT_TEAM = R8JF2W45R3;
435447
ENABLE_HARDENED_RUNTIME = YES;
@@ -463,6 +475,7 @@
463475
CODE_SIGN_STYLE = Automatic;
464476
COMBINE_HIDPI_IMAGES = YES;
465477
CURRENT_PROJECT_VERSION = 1;
478+
DEAD_CODE_STRIPPING = YES;
466479
DEVELOPMENT_ASSET_PATHS = "\"Moti/Preview Content\"";
467480
DEVELOPMENT_TEAM = R8JF2W45R3;
468481
ENABLE_HARDENED_RUNTIME = YES;

Moti/Moti.xcodeproj/xcshareddata/xcschemes/Moti.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1540"
3+
LastUpgradeVersion = "1610"
44
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"

Moti/Moti/Info.plist

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
<key>NSLocationUsageDescription</key>
2222
<string>Tempethéra requires access to your location to provide accurate weather information.</string>
2323

24+
<key>CFBundleLocalizations</key>
25+
<array>
26+
<string>en</string>
27+
<string>el</string>
28+
<string>sq</string>
29+
</array>
30+
2431
<key>NSAppTransportSecurity</key>
2532
<dict>
2633
<key>NSExceptionDomains</key>

Moti/Moti/Moti.swift

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3232
private var cancellables = Set<AnyCancellable>()
3333
private var globalClickMonitor: Any?
3434
private var isAlwaysOnTop = false
35+
@Published var currentLanguage: String = "en"
3536

3637
func applicationDidFinishLaunching(_ notification: Notification) {
3738
setupStatusItem()
@@ -85,19 +86,55 @@ class AppDelegate: NSObject, NSApplicationDelegate {
8586

8687
private func showContextMenu(_ sender: NSStatusBarButton) {
8788
let menu = NSMenu()
89+
90+
// Refresh option
8891
menu.addItem(
89-
NSMenuItem(title: "Refresh", action: #selector(refreshWeather), keyEquivalent: "r"))
92+
NSMenuItem(
93+
title: NSLocalizedString("refresh", comment: ""), action: #selector(refreshWeather),
94+
keyEquivalent: "r"))
95+
96+
// Search Another Location option
9097
menu.addItem(
9198
NSMenuItem(
92-
title: "Search Another Location", action: #selector(searchAnotherLocation),
99+
title: NSLocalizedString("search_another_location", comment: ""),
100+
action: #selector(searchAnotherLocation),
93101
keyEquivalent: ""))
94-
let alwaysOnTopItem = NSMenuItem(
95-
title: "Always on Top", action: #selector(toggleAlwaysOnTop), keyEquivalent: "")
96-
alwaysOnTopItem.state = isAlwaysOnTop ? .on : .off
97-
menu.addItem(alwaysOnTopItem)
98-
menu.addItem(NSMenuItem(title: "About", action: #selector(showAbout), keyEquivalent: ""))
102+
103+
// Language submenu with emoji flags
104+
let languageMenuItem = NSMenuItem(
105+
title: NSLocalizedString("change_language", comment: ""), action: nil, keyEquivalent: ""
106+
)
107+
let languageSubMenu = NSMenu()
108+
109+
let englishItem = NSMenuItem(
110+
title: "English", action: #selector(setLanguage(_:)), keyEquivalent: "")
111+
englishItem.tag = 0
112+
113+
let greekItem = NSMenuItem(
114+
title: "Ελληνικά", action: #selector(setLanguage(_:)), keyEquivalent: "")
115+
greekItem.tag = 1
116+
117+
let albanianItem = NSMenuItem(
118+
title: "Shqip", action: #selector(setLanguage(_:)), keyEquivalent: "")
119+
albanianItem.tag = 2
120+
121+
languageSubMenu.addItem(englishItem)
122+
languageSubMenu.addItem(greekItem)
123+
languageSubMenu.addItem(albanianItem)
124+
languageMenuItem.submenu = languageSubMenu
125+
menu.addItem(languageMenuItem)
126+
127+
// About and Quit options
99128
menu.addItem(NSMenuItem.separator())
100-
menu.addItem(NSMenuItem(title: "Quit", action: #selector(quitApp), keyEquivalent: "q"))
129+
menu.addItem(
130+
NSMenuItem(
131+
title: NSLocalizedString("about", comment: ""), action: #selector(showAbout),
132+
keyEquivalent: ""))
133+
menu.addItem(
134+
NSMenuItem(
135+
title: NSLocalizedString("quit", comment: ""), action: #selector(quitApp),
136+
keyEquivalent: "q"))
137+
101138
statusItem.menu = menu
102139
statusItem.button?.performClick(nil)
103140
statusItem.menu = nil
@@ -162,11 +199,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
162199
}
163200

164201
private func loadImage(from url: URL, completion: @escaping (NSImage) -> Void) {
165-
DispatchQueue.global().async {
166-
if let data = try? Data(contentsOf: url), let image = NSImage(data: data) {
167-
DispatchQueue.main.async { completion(image) }
202+
let task = URLSession.shared.dataTask(with: url) { data, response, error in
203+
if let data = data, let image = NSImage(data: data) {
204+
DispatchQueue.main.async {
205+
completion(image)
206+
}
207+
} else if let error = error {
208+
print("Failed to load image from \(url): \(error.localizedDescription)")
168209
}
169210
}
211+
task.priority = URLSessionTask.highPriority // Set a high priority if necessary
212+
task.resume()
170213
}
171214

172215
private func resizeImage(image: NSImage, toFit targetSize: CGSize) -> NSImage {
@@ -319,6 +362,30 @@ class AppDelegate: NSObject, NSApplicationDelegate {
319362
}
320363
}
321364

365+
@objc private func setLanguage(_ sender: NSMenuItem) {
366+
switch sender.tag {
367+
case 0:
368+
currentLanguage = "en"
369+
case 1:
370+
currentLanguage = "el"
371+
case 2:
372+
currentLanguage = "sq"
373+
default:
374+
break
375+
}
376+
print("Current language set to: \(currentLanguage)") // Print the current language
377+
updateLanguage()
378+
}
379+
380+
private func updateLanguage() {
381+
print("Updating UI language to: \(currentLanguage)") // Print the language during update
382+
Bundle.setLanguage(currentLanguage)
383+
384+
// Reload the app's UI with the new language setting
385+
popover.contentViewController = NSHostingController(
386+
rootView: ContentView(weatherManager: weatherManager, state: contentViewState))
387+
}
388+
322389
@objc func quitApp() {
323390
NSApp.terminate(nil)
324391
}
@@ -337,3 +404,35 @@ class AppDelegate: NSObject, NSApplicationDelegate {
337404
}
338405
}
339406
}
407+
408+
// MARK: - Bundle Extension for Language Switching
409+
410+
extension Bundle {
411+
private static var onLanguageDispatchOnce: () -> Void = {
412+
object_setClass(Bundle.main, LanguageBundle.self)
413+
}
414+
415+
static func setLanguage(_ language: String) {
416+
onLanguageDispatchOnce()
417+
UserDefaults.standard.set([language], forKey: "AppleLanguages")
418+
UserDefaults.standard.synchronize()
419+
}
420+
}
421+
422+
private class LanguageBundle: Bundle, @unchecked Sendable {
423+
override func localizedString(forKey key: String, value: String?, table tableName: String?)
424+
-> String
425+
{
426+
let language = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first ?? "en"
427+
428+
// Check if the language-specific bundle path is available
429+
guard let bundlePath = Bundle.main.path(forResource: language, ofType: "lproj"),
430+
let languageBundle = Bundle(path: bundlePath)
431+
else {
432+
print("Fallback to main bundle for language: \(language)") // Debugging output
433+
return super.localizedString(forKey: key, value: value, table: tableName)
434+
}
435+
436+
return languageBundle.localizedString(forKey: key, value: value, table: tableName)
437+
}
438+
}

0 commit comments

Comments
 (0)