core/gtk: add language config entry to override GUI localization (#10428)

Fixes #10276

<img width="853" height="637" alt="Screenshot From 2026-01-23 16-45-11"
src="https://github.com/user-attachments/assets/aff9d2f8-eb3e-411e-bd3d-ebd32e5c7973"
/>
This commit is contained in:
Leah Amelia Chen
2026-02-15 00:24:30 +08:00
committed by GitHub
3 changed files with 65 additions and 12 deletions

View File

@@ -213,6 +213,11 @@ pub const Application = extern struct {
/// Providers for loading custom stylesheets defined by user
custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .empty,
/// A copy of the LANG environment variable that was provided to Ghostty
/// by the system. If this is null, the LANG environment variable did
/// not exist in Ghostty's environment variable.
saved_language: ?[:0]const u8 = null,
pub var offset: c_int = 0;
};
@@ -249,15 +254,6 @@ pub const Application = extern struct {
gtk_version.logVersion();
adw_version.logVersion();
// Set gettext global domain to be our app so that our unqualified
// translations map to our translations.
internal_os.i18n.initGlobalDomain() catch |err| {
// Failures shuldn't stop application startup. Our app may
// not translate correctly but it should still work. In the
// future we may want to add this to the GUI to show.
log.warn("i18n initialization failed error={}", .{err});
};
// Load our configuration.
var config = CoreConfig.load(alloc) catch |err| err: {
// If we fail to load the configuration, then we should log
@@ -275,6 +271,27 @@ pub const Application = extern struct {
};
defer config.deinit();
const saved_language: ?[:0]const u8 = saved_language: {
const old_language = old_language: {
const result = (internal_os.getenv(alloc, "LANG") catch break :old_language null) orelse break :old_language null;
defer result.deinit(alloc);
break :old_language alloc.dupeZ(u8, result.value) catch break :old_language null;
};
if (config.language) |language| _ = internal_os.setenv("LANG", language);
break :saved_language old_language;
};
// Set gettext global domain to be our app so that our unqualified
// translations map to our translations.
internal_os.i18n.initGlobalDomain() catch |err| {
// Failures shuldn't stop application startup. Our app may
// not translate correctly but it should still work. In the
// future we may want to add this to the GUI to show.
log.warn("i18n initialization failed error={}", .{err});
};
// Setup our GTK init env vars
setGtkEnv(&config) catch |err| switch (err) {
error.NoSpaceLeft => {
@@ -374,7 +391,7 @@ pub const Application = extern struct {
// Setup our private state. More setup is done in the init
// callback that GObject calls, but we can't pass this data through
// to there (and we don't need it there directly) so this is here.
const priv = self.private();
const priv: *Private = self.private();
priv.* = .{
.rt_app = rt_app,
.core_app = core_app,
@@ -383,6 +400,7 @@ pub const Application = extern struct {
.css_provider = css_provider,
.custom_css_providers = .empty,
.global_shortcuts = gobject.ext.newInstance(GlobalShortcuts, .{}),
.saved_language = saved_language,
};
// Signals
@@ -415,11 +433,12 @@ pub const Application = extern struct {
/// ensures that our memory is cleaned up properly.
pub fn deinit(self: *Self) void {
const alloc = self.allocator();
const priv = self.private();
const priv: *Private = self.private();
priv.config.unref();
priv.winproto.deinit(alloc);
priv.global_shortcuts.unref();
if (priv.transient_cgroup_base) |base| alloc.free(base);
if (priv.saved_language) |language| alloc.free(language);
if (gdk.Display.getDefault()) |display| {
gtk.StyleContext.removeProviderForDisplay(
display,
@@ -445,6 +464,12 @@ pub const Application = extern struct {
return self.private().core_app.alloc;
}
/// Get the original language that Ghostty was launched with. This returns a
/// pointer to internal memory so it must be copied by callers.
pub fn savedLanguage(self: *Self) ?[:0]const u8 {
return self.private().saved_language;
}
/// Run the application. This is a replacement for `gio.Application.run`
/// because we want more tight control over our event loop so we can
/// integrate it with libghostty.

View File

@@ -1595,10 +1595,17 @@ pub const Surface = extern struct {
}
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
const alloc = Application.default().allocator();
const app = Application.default();
const alloc = app.allocator();
var env = try internal_os.getEnvMap(alloc);
errdefer env.deinit();
if (app.savedLanguage()) |language| {
try env.put("LANG", language);
} else {
env.remove("LANG");
}
// Don't leak these GTK environment variables to child processes.
env.remove("GDK_DEBUG");
env.remove("GDK_DISABLE");

View File

@@ -94,6 +94,27 @@ pub const compatibility = std.StaticStringMap(
.{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior },
});
/// Set Ghostty's graphical user interface language to a language other than the
/// system default language. The language must be fully specified, including the
/// encoding. For example:
///
/// language = de_DE.UTF-8
///
/// will force the strings in Ghostty's graphical user interface to be in German
/// rather than the system default.
///
/// This will not affect the language used by programs run _within_ Ghostty.
/// Those will continue to use the default system language. There are also many
/// non-GUI elements in Ghostty that are not translated - this setting will have
/// no effect on those.
///
/// Warning: This setting cannot be reloaded at runtime. To change the language
/// you must fully restart Ghostty.
///
/// GTK only.
/// Available since 1.3.0.
language: ?[:0]const u8 = null,
/// The font families to use.
///
/// You can generate the list of valid values using the CLI: