os: locale automatically sets LANGUAGE based on macOS preferred

This commit is contained in:
Mitchell Hashimoto
2025-03-07 09:50:33 -08:00
parent edf619205c
commit 3c49bc5086
3 changed files with 91 additions and 7 deletions

View File

@ -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"

View File

@ -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);

View File

@ -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