diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 403f94599..900ac1391 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -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. diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 26009ef79..46cd80512 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -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"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 8ca64efe9..769979759 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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: