Skip to content

Commit 3c49bc5

Browse files
committed
os: locale automatically sets LANGUAGE based on macOS preferred
1 parent edf6192 commit 3c49bc5

File tree

3 files changed

+91
-7
lines changed

3 files changed

+91
-7
lines changed

macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
buildConfiguration = "Debug"
3434
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
3535
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
36-
language = "zh-Hans"
3736
launchStyle = "0"
3837
useCustomWorkingDirectory = "NO"
3938
ignoresPersistentStateOnLaunch = "NO"

src/os/i18n.zig

+25-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,22 @@ const log = std.log.scoped(.i18n);
66
/// Supported locales for the application. This must be kept up to date
77
/// with the translations available in the `po/` directory; this is used
88
/// by our build process as well runtime libghostty APIs.
9-
pub const locales = [_][]const u8{
9+
///
10+
/// The order also matters. For incomplete locale information (i.e. only
11+
/// a language code available), the first match is used. For example, if
12+
/// we know the user requested `zh` but has no region code, then we'd pick
13+
/// the first locale that matches `zh`.
14+
pub const locales = [_][:0]const u8{
1015
"zh_CN.UTF-8",
1116
};
1217

18+
/// Set for faster membership lookup of locales.
19+
pub const locales_map = map: {
20+
var kvs: [locales.len]struct { []const u8 } = undefined;
21+
for (locales, 0..) |locale, i| kvs[i] = .{locale};
22+
break :map std.StaticStringMap(void).initComptime(kvs);
23+
};
24+
1325
pub const InitError = error{
1426
InvalidResourcesDir,
1527
OutOfMemory,
@@ -40,6 +52,18 @@ pub fn init(resources_dir: []const u8) InitError!void {
4052
return error.OutOfMemory;
4153
}
4254

55+
/// Finds the closest matching locale for a given language code.
56+
pub fn closestLocaleForLanguage(lang: []const u8) ?[:0]const u8 {
57+
for (locales) |locale| {
58+
const idx = std.mem.indexOfScalar(u8, locale, '_') orelse continue;
59+
if (std.mem.eql(u8, locale[0..idx], lang)) {
60+
return locale;
61+
}
62+
}
63+
64+
return null;
65+
}
66+
4367
/// Translate a message for the Ghostty domain.
4468
pub fn _(msgid: [*:0]const u8) [*:0]const u8 {
4569
return dgettext(build_config.bundle_id, msgid);

src/os/locale.zig

+66-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
const std = @import("std");
22
const builtin = @import("builtin");
33
const assert = std.debug.assert;
4+
const macos = @import("macos");
45
const objc = @import("objc");
56
const internal_os = @import("main.zig");
67

7-
const log = std.log.scoped(.os);
8+
const log = std.log.scoped(.os_locale);
89

910
/// Ensure that the locale is set.
1011
pub fn ensureLocale(alloc: std.mem.Allocator) !void {
@@ -60,7 +61,7 @@ pub fn ensureLocale(alloc: std.mem.Allocator) !void {
6061
_ = internal_os.setenv("LANG", "en_US.UTF-8");
6162
log.info("setlocale default result={s}", .{v});
6263
return;
63-
} else log.err("setlocale failed even with the fallback, uncertain results", .{});
64+
} else log.warn("setlocale failed even with the fallback, uncertain results", .{});
6465
}
6566

6667
/// This sets the LANG environment variable based on the macOS system
@@ -71,7 +72,7 @@ fn setLangFromCocoa() void {
7172

7273
// The classes we're going to need.
7374
const NSLocale = objc.getClass("NSLocale") orelse {
74-
log.err("NSLocale class not found. Locale may be incorrect.", .{});
75+
log.warn("NSLocale class not found. Locale may be incorrect.", .{});
7576
return;
7677
};
7778

@@ -92,16 +93,76 @@ fn setLangFromCocoa() void {
9293
// Format them into a buffer
9394
var buf: [128]u8 = undefined;
9495
const env_value = std.fmt.bufPrintZ(&buf, "{s}_{s}.UTF-8", .{ z_lang, z_country }) catch |err| {
95-
log.err("error setting locale from system. err={}", .{err});
96+
log.warn("error setting locale from system. err={}", .{err});
9697
return;
9798
};
9899
log.info("detected system locale={s}", .{env_value});
99100

100101
// Set it onto our environment
101102
if (internal_os.setenv("LANG", env_value) < 0) {
102-
log.err("error setting locale env var", .{});
103+
log.warn("error setting locale env var", .{});
103104
return;
104105
}
106+
107+
// We also want to set our LANGUAGE for translations. We do this using
108+
// NSLocale.preferredLanguages over our system locale since we want to
109+
// match our app's preferred languages.
110+
language: {
111+
const i18n = internal_os.i18n;
112+
113+
// We need to get our app's preferred languages. These may not
114+
// match the system locale (NSLocale.currentLocale).
115+
const preferred: *macos.foundation.Array = array: {
116+
const ns = NSLocale.msgSend(
117+
objc.Object,
118+
objc.sel("preferredLanguages"),
119+
.{},
120+
);
121+
break :array @ptrCast(ns.value);
122+
};
123+
for (0..preferred.getCount()) |i| {
124+
const str = preferred.getValueAtIndex(macos.foundation.String, i);
125+
const c_str = c_str: {
126+
const raw = str.cstring(&buf, .utf8) orelse {
127+
// I don't think this can happen but if it does then I want
128+
// to know about it if a user has translation issues.
129+
log.warn("failed to convert a preferred language to UTF-8", .{});
130+
continue;
131+
};
132+
133+
// We want to strip at "-" since we only care about the language
134+
// code, not the region code. i.e. "zh-Hans" -> "zh"
135+
const idx = std.mem.indexOfScalar(u8, raw, '-') orelse raw.len;
136+
break :c_str raw[0..idx];
137+
};
138+
139+
// If our preferred language is equal to our system language
140+
// then we can be done, since the locale above we set everything.
141+
if (std.mem.eql(u8, c_str, z_lang)) {
142+
log.debug("preferred language matches system locale={s}", .{c_str});
143+
break :language;
144+
}
145+
146+
// Note: there are many improvements that can be made here to make
147+
// this more and more robust. For example, we can try to search for
148+
// the MOST matching supported locale for translations. Right now
149+
// we fall directly back to language code.
150+
log.debug("searching for closest matching locale preferred={s}", .{c_str});
151+
if (i18n.closestLocaleForLanguage(c_str)) |i18n_locale| {
152+
log.info("setting LANGUAGE to closest matching locale={s}", .{i18n_locale});
153+
_ = internal_os.setenv("LANGUAGE", i18n_locale);
154+
break :language;
155+
}
156+
}
157+
158+
// No matches or our preferred languages are empty. As a final
159+
// try we try to match our system locale.
160+
if (i18n.closestLocaleForLanguage(z_lang)) |i18n_locale| {
161+
log.info("setting LANGUAGE to closest matching locale={s}", .{i18n_locale});
162+
_ = internal_os.setenv("LANGUAGE", i18n_locale);
163+
break :language;
164+
}
165+
}
105166
}
106167

107168
const LC_ALL: c_int = 6; // from locale.h

0 commit comments

Comments
 (0)