From 3c49bc5086681f5bed98067a64fa8edddc9f175c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 7 Mar 2025 09:50:33 -0800 Subject: [PATCH] os: locale automatically sets LANGUAGE based on macOS preferred --- .../xcshareddata/xcschemes/Ghostty.xcscheme | 1 - src/os/i18n.zig | 26 ++++++- src/os/locale.zig | 71 +++++++++++++++++-- 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme index c2e61f1c2..5900042f2 100644 --- a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme +++ b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme @@ -33,7 +33,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "zh-Hans" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 004ae9477..a9000e18d 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -6,10 +6,22 @@ const log = std.log.scoped(.i18n); /// Supported locales for the application. This must be kept up to date /// with the translations available in the `po/` directory; this is used /// by our build process as well runtime libghostty APIs. -pub const locales = [_][]const u8{ +/// +/// The order also matters. For incomplete locale information (i.e. only +/// a language code available), the first match is used. For example, if +/// we know the user requested `zh` but has no region code, then we'd pick +/// the first locale that matches `zh`. +pub const locales = [_][:0]const u8{ "zh_CN.UTF-8", }; +/// Set for faster membership lookup of locales. +pub const locales_map = map: { + var kvs: [locales.len]struct { []const u8 } = undefined; + for (locales, 0..) |locale, i| kvs[i] = .{locale}; + break :map std.StaticStringMap(void).initComptime(kvs); +}; + pub const InitError = error{ InvalidResourcesDir, OutOfMemory, @@ -40,6 +52,18 @@ pub fn init(resources_dir: []const u8) InitError!void { return error.OutOfMemory; } +/// Finds the closest matching locale for a given language code. +pub fn closestLocaleForLanguage(lang: []const u8) ?[:0]const u8 { + for (locales) |locale| { + const idx = std.mem.indexOfScalar(u8, locale, '_') orelse continue; + if (std.mem.eql(u8, locale[0..idx], lang)) { + return locale; + } + } + + return null; +} + /// Translate a message for the Ghostty domain. pub fn _(msgid: [*:0]const u8) [*:0]const u8 { return dgettext(build_config.bundle_id, msgid); diff --git a/src/os/locale.zig b/src/os/locale.zig index 361f4fe62..e96ab5e3e 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -1,10 +1,11 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; +const macos = @import("macos"); const objc = @import("objc"); const internal_os = @import("main.zig"); -const log = std.log.scoped(.os); +const log = std.log.scoped(.os_locale); /// Ensure that the locale is set. pub fn ensureLocale(alloc: std.mem.Allocator) !void { @@ -60,7 +61,7 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void { _ = internal_os.setenv("LANG", "en_US.UTF-8"); log.info("setlocale default result={s}", .{v}); return; - } else log.err("setlocale failed even with the fallback, uncertain results", .{}); + } else log.warn("setlocale failed even with the fallback, uncertain results", .{}); } /// This sets the LANG environment variable based on the macOS system @@ -71,7 +72,7 @@ fn setLangFromCocoa() void { // The classes we're going to need. const NSLocale = objc.getClass("NSLocale") orelse { - log.err("NSLocale class not found. Locale may be incorrect.", .{}); + log.warn("NSLocale class not found. Locale may be incorrect.", .{}); return; }; @@ -92,16 +93,76 @@ fn setLangFromCocoa() void { // Format them into a buffer var buf: [128]u8 = undefined; const env_value = std.fmt.bufPrintZ(&buf, "{s}_{s}.UTF-8", .{ z_lang, z_country }) catch |err| { - log.err("error setting locale from system. err={}", .{err}); + log.warn("error setting locale from system. err={}", .{err}); return; }; log.info("detected system locale={s}", .{env_value}); // Set it onto our environment if (internal_os.setenv("LANG", env_value) < 0) { - log.err("error setting locale env var", .{}); + log.warn("error setting locale env var", .{}); return; } + + // We also want to set our LANGUAGE for translations. We do this using + // NSLocale.preferredLanguages over our system locale since we want to + // match our app's preferred languages. + language: { + const i18n = internal_os.i18n; + + // We need to get our app's preferred languages. These may not + // match the system locale (NSLocale.currentLocale). + const preferred: *macos.foundation.Array = array: { + const ns = NSLocale.msgSend( + objc.Object, + objc.sel("preferredLanguages"), + .{}, + ); + break :array @ptrCast(ns.value); + }; + for (0..preferred.getCount()) |i| { + const str = preferred.getValueAtIndex(macos.foundation.String, i); + const c_str = c_str: { + const raw = str.cstring(&buf, .utf8) orelse { + // I don't think this can happen but if it does then I want + // to know about it if a user has translation issues. + log.warn("failed to convert a preferred language to UTF-8", .{}); + continue; + }; + + // We want to strip at "-" since we only care about the language + // code, not the region code. i.e. "zh-Hans" -> "zh" + const idx = std.mem.indexOfScalar(u8, raw, '-') orelse raw.len; + break :c_str raw[0..idx]; + }; + + // If our preferred language is equal to our system language + // then we can be done, since the locale above we set everything. + if (std.mem.eql(u8, c_str, z_lang)) { + log.debug("preferred language matches system locale={s}", .{c_str}); + break :language; + } + + // Note: there are many improvements that can be made here to make + // this more and more robust. For example, we can try to search for + // the MOST matching supported locale for translations. Right now + // we fall directly back to language code. + log.debug("searching for closest matching locale preferred={s}", .{c_str}); + if (i18n.closestLocaleForLanguage(c_str)) |i18n_locale| { + log.info("setting LANGUAGE to closest matching locale={s}", .{i18n_locale}); + _ = internal_os.setenv("LANGUAGE", i18n_locale); + break :language; + } + } + + // No matches or our preferred languages are empty. As a final + // try we try to match our system locale. + if (i18n.closestLocaleForLanguage(z_lang)) |i18n_locale| { + log.info("setting LANGUAGE to closest matching locale={s}", .{i18n_locale}); + _ = internal_os.setenv("LANGUAGE", i18n_locale); + break :language; + } + } } const LC_ALL: c_int = 6; // from locale.h