feat: initial implementation of i18n/l10n

This commit is contained in:
Leah Amelia Chen
2025-01-18 11:47:59 +01:00
parent c5508e7d19
commit 37f222eaf1
21 changed files with 501 additions and 59 deletions

View File

@ -41,6 +41,10 @@
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
},
.zig_gettext = .{
.url = "git+https://github.com/pluiedev/zig-gettext/?ref=main#c1ff3a954ba0d9ad369ccf96810437c7e46cfc2a",
.hash = "122046140255a2dadfc3b3f55a68e750776f0ae2330865665f892d381d8d8f438c69",
},
// C libs
.cimgui = .{ .path = "./pkg/cimgui" },
@ -48,6 +52,7 @@
.freetype = .{ .path = "./pkg/freetype" },
.harfbuzz = .{ .path = "./pkg/harfbuzz" },
.highway = .{ .path = "./pkg/highway" },
.libintl = .{ .path = "./pkg/libintl" },
.libpng = .{ .path = "./pkg/libpng" },
.macos = .{ .path = "./pkg/macos" },
.oniguruma = .{ .path = "./pkg/oniguruma" },

View File

@ -54,6 +54,7 @@
wayland,
wayland-scanner,
wayland-protocols,
gettext,
}: let
# See package.nix. Keep in sync.
rpathLibs =
@ -84,6 +85,7 @@
gtk4
glib
wayland
gettext
];
in
mkShell {

View File

@ -20,6 +20,7 @@
pkg-config,
zig_0_13,
pandoc,
gettext,
revision ? "dirty",
optimize ? "Debug",
enableX11 ? true,
@ -56,6 +57,7 @@
../images
../include
../pkg
../po
../src
../vendor
../build.zig
@ -125,6 +127,7 @@ in
pkg-config
zig_hook
wrapGAppsHook4
gettext
]
++ lib.optionals enableWayland [
wayland-scanner

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
"sha256-Bjy31evaKgpRX1mGwAFkai44eiiorTV1gW3VdP9Ins8="
"sha256-ar4f1+7Mx45K446X2/4/n9gVFZklAcQyJHQ+Ehn2p4U="

40
pkg/libintl/build.zig Normal file
View File

@ -0,0 +1,40 @@
const std = @import("std");
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
.preferred_link_mode = .dynamic,
.search_strategy = .mode_first,
};
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const module = b.addModule("intl", .{
.root_source_file = b.path("main.zig"),
.target = target,
.optimize = optimize,
});
if (b.systemIntegrationOption("libintl", .{
.default = !target.result.isGnuLibC(),
})) {
// On non-glibc platforms we don't have libintl
// built into libc, so we have to do more work.
// In GNU's infinite wisdom, there's no easy pkg-config file for
// you to consume and integrate into build systems other than autoconf.
// Users must rely on system library/include paths, or manually
// add libintl to the Zig search path.
module.linkSystemLibrary("intl", dynamic_link_opts);
}
// switch (target.result.os.tag) {
// .windows => {
// const msys2 = b.dependency("libintl_msys2", .{});
// lib.addLibraryPath(msys2.path("usr/bin"));
// module.linkSystemLibrary2("msys-intl-8", .{
// .preferred_link_mode = .dynamic,
// .search_strategy = .mode_first,
// });
// },
// }
}

View File

@ -0,0 +1,6 @@
.{
.name = "libintl",
.version = "0.0.1",
.paths = .{""},
.dependencies = .{},
}

41
pkg/libintl/c.zig Normal file
View File

@ -0,0 +1,41 @@
pub const locale = @cImport(@cInclude("locale.h"));
pub extern fn gettext(
msgid: [*:0]const u8,
) [*:0]const u8;
pub extern fn dgettext(
domainname: [*:0]const u8,
msgid: [*:0]const u8,
) [*:0]const u8;
pub extern fn dcgettext(
domainname: [*:0]const u8,
msgid: [*:0]const u8,
category: c_int,
) [*:0]const u8;
pub extern fn ngettext(
msgid1: [*:0]const u8,
msgid2: [*:0]const u8,
n: c_ulong,
) [*:0]const u8;
pub extern fn dngettext(
domainname: [*:0]const u8,
msgid1: [*:0]const u8,
msgid2: [*:0]const u8,
n: c_ulong,
) [*:0]const u8;
pub extern fn dcngettext(
domainname: [*:0]const u8,
msgid1: [*:0]const u8,
msgid2: [*:0]const u8,
n: c_ulong,
category: c_int,
) [*:0]const u8;
pub extern fn bindtextdomain(
domainname: [*:0]const u8,
dirname: [*:0]const u8,
) ?[*]const u8;
pub extern fn textdomain(
domainname: ?[*:0]const u8,
) ?[*]const u8;

73
pkg/libintl/main.zig Normal file
View File

@ -0,0 +1,73 @@
const std = @import("std");
const c = @import("c.zig");
pub const Category = enum(c_int) {
messages = c.locale.LC_MESSAGES,
collate = c.locale.LC_COLLATE,
ctype = c.locale.LC_CTYPE,
monetary = c.locale.LC_MONETARY,
numeric = c.locale.LC_NUMERIC,
time = c.locale.LC_TIME,
_,
};
pub const Query = struct {
msg: [:0]const u8,
plural: ?struct {
msg: [:0]const u8,
number: c_ulong,
} = null,
domain: ?[:0]const u8 = null,
category: ?Category = null,
};
pub const _ = gettext;
pub fn gettext(comptime msg: [:0]const u8) [:0]const u8 {
return std.mem.span(c.gettext(msg));
}
pub fn dgettext(comptime msg: [:0]const u8, domain: [:0]const u8) [:0]const u8 {
return std.mem.span(c.dgettext(domain, msg));
}
pub fn dcgettext(comptime msg: [:0]const u8, domain: [:0]const u8, category: Category) [:0]const u8 {
return std.mem.span(c.dcgettext(domain, msg, category));
}
pub fn ngettext(
comptime msg: [:0]const u8,
comptime msg_plural: [:0]const u8,
number: c_ulong,
) [:0]const u8 {
return std.mem.span(c.ngettext(msg, msg_plural, number));
}
pub fn dngettext(
comptime msg: [:0]const u8,
comptime msg_plural: [:0]const u8,
number: c_ulong,
domain: [:0]const u8,
) [:0]const u8 {
return std.mem.span(c.dngettext(domain, msg, msg_plural, number));
}
pub fn dcngettext(
comptime msg: [:0]const u8,
comptime msg_plural: [:0]const u8,
number: c_ulong,
domain: [:0]const u8,
category: Category,
) [:0]const u8 {
return std.mem.span(c.dcngettext(
domain,
msg,
msg_plural,
number,
category,
));
}
pub fn bindTextDomain(domain: [:0]const u8, dir: [:0]const u8) std.mem.Allocator.Error!void {
// ENOMEM is the only possible error
if (c.bindtextdomain(domain, dir) == null) return error.OutOfMemory;
}
pub fn setTextDomain(domain: [:0]const u8) std.mem.Allocator.Error!void {
// ENOMEM is the only possible error
if (c.textdomain(domain) == null) return error.OutOfMemory;
}

0
po/LINGUAS Normal file
View File

159
po/messages.pot Normal file
View File

@ -0,0 +1,159 @@
msgid ""
msgstr ""
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/apprt/gtk/App.zig:1827 src/apprt/gtk/Window.zig:918
msgid "About Ghostty"
msgstr ""
#: src/apprt/gtk/Tab.zig:147
msgid "All terminal sessions in this tab will be terminated."
msgstr ""
#: src/apprt/gtk/Window.zig:800
msgid "All terminal sessions in this window will be terminated."
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:170
msgid "Allow"
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:248
msgid ""
"An application is attempting to read from the clipboard.\n"
"The current clipboard contents are shown below.\n"
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:252
msgid ""
"An application is attempting to write to the clipboard.\n"
"The content to write is shown below.\n"
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:237
msgid "Authorize Clipboard Access"
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:169
msgid "Cancel"
msgstr ""
#: src/apprt/gtk/App.zig:1814
msgid "Close Tab"
msgstr ""
#: src/apprt/gtk/App.zig:1817
msgid "Close Window"
msgstr ""
#: src/apprt/gtk/Tab.zig:143
msgid "Close this tab?"
msgstr ""
#: src/apprt/gtk/Surface.zig:728
msgid "Close this terminal?"
msgstr ""
#: src/apprt/gtk/Window.zig:796
msgid "Close this window?"
msgstr ""
#: src/apprt/gtk/Surface.zig:1141
msgid "Copied to clipboard"
msgstr ""
#: src/apprt/gtk/App.zig:1848
msgid "Copy"
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:170
msgid "Deny"
msgstr ""
#: src/apprt/gtk/Window.zig:899
msgid "Ghostty Developers"
msgstr ""
#: src/apprt/gtk/inspector.zig:143
msgid "Ghostty: Terminal Inspector"
msgstr ""
#: src/apprt/gtk/App.zig:1874
msgid "Menu"
msgstr ""
#: src/apprt/gtk/App.zig:1813 src/apprt/gtk/Window.zig:201
msgid "New Tab"
msgstr ""
#: src/apprt/gtk/App.zig:1812
msgid "New Window"
msgstr ""
#: src/apprt/gtk/App.zig:1825
msgid "Open Configuration"
msgstr ""
#: src/apprt/gtk/App.zig:1849 src/apprt/gtk/ClipboardConfirmationWindow.zig:169
msgid "Paste"
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:245
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.\n"
msgstr ""
#: src/apprt/gtk/App.zig:1826
msgid "Reload Configuration"
msgstr ""
#: src/apprt/gtk/App.zig:1864
msgid "Reset"
msgstr ""
#: src/apprt/gtk/App.zig:1816 src/apprt/gtk/App.zig:1857
msgid "Split Down"
msgstr ""
#: src/apprt/gtk/App.zig:1815 src/apprt/gtk/App.zig:1856
msgid "Split Right"
msgstr ""
#: src/apprt/gtk/App.zig:1824 src/apprt/gtk/App.zig:1865
msgid "Terminal Inspector"
msgstr ""
#: src/apprt/gtk/Surface.zig:732
msgid ""
"There is still a running process in the terminal. Closing the terminal will kill this process.\n"
"Are you sure you want to close the terminal?\n"
"\n"
"Click 'No' to cancel and return to your terminal.\n"
msgstr ""
#: src/apprt/gtk/Window.zig:174
msgid "View Open Tabs"
msgstr ""
#: src/apprt/gtk/ClipboardConfirmationWindow.zig:236
msgid "Warning: Potentially Unsafe Paste"
msgstr ""
#: src/apprt/gtk/ResizeOverlay.zig:107
msgid "c"
msgid_plural "c"
msgstr[0] ""
msgstr[1] ""
#: src/apprt/gtk/ResizeOverlay.zig:111
msgid "r"
msgid_plural "r"
msgstr[0] ""
msgstr[1] ""
#: src/apprt/gtk/Window.zig:220
msgid "⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""

View File

@ -38,6 +38,7 @@ const inspector = @import("inspector.zig");
const key = @import("key.zig");
const winproto = @import("winproto.zig");
const testing = std.testing;
const intl = @import("intl");
const log = std.log.scoped(.gtk);
@ -1809,22 +1810,22 @@ fn initMenuContent(menu: *c.GMenu) void {
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "New Window", "win.new_window");
c.g_menu_append(section, "New Tab", "win.new_tab");
c.g_menu_append(section, "Close Tab", "win.close_tab");
c.g_menu_append(section, "Split Right", "win.split_right");
c.g_menu_append(section, "Split Down", "win.split_down");
c.g_menu_append(section, "Close Window", "win.close");
c.g_menu_append(section, intl._("New Window"), "win.new_window");
c.g_menu_append(section, intl._("New Tab"), "win.new_tab");
c.g_menu_append(section, intl._("Close Tab"), "win.close_tab");
c.g_menu_append(section, intl._("Split Right"), "win.split_right");
c.g_menu_append(section, intl._("Split Down"), "win.split_down");
c.g_menu_append(section, intl._("Close Window"), "win.close");
}
{
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
c.g_menu_append(section, "Open Configuration", "app.open-config");
c.g_menu_append(section, "Reload Configuration", "app.reload-config");
c.g_menu_append(section, "About Ghostty", "win.about");
c.g_menu_append(section, intl._("Terminal Inspector"), "win.toggle_inspector");
c.g_menu_append(section, intl._("Open Configuration"), "app.open-config");
c.g_menu_append(section, intl._("Reload Configuration"), "app.reload-config");
c.g_menu_append(section, intl._("About Ghostty"), "win.about");
}
}
@ -1845,24 +1846,24 @@ fn initContextMenu(self: *App) void {
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "Copy", "win.copy");
c.g_menu_append(section, "Paste", "win.paste");
c.g_menu_append(section, intl._("Copy"), "win.copy");
c.g_menu_append(section, intl._("Paste"), "win.paste");
}
{
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "Split Right", "win.split_right");
c.g_menu_append(section, "Split Down", "win.split_down");
c.g_menu_append(section, intl._("Split Right"), "win.split_right");
c.g_menu_append(section, intl._("Split Down"), "win.split_down");
}
{
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "Reset", "win.reset");
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
c.g_menu_append(section, intl._("Reset"), "win.reset");
c.g_menu_append(section, intl._("Terminal Inspector"), "win.toggle_inspector");
}
const section = c.g_menu_new();
@ -1871,7 +1872,7 @@ fn initContextMenu(self: *App) void {
defer c.g_object_unref(submenu);
initMenuContent(@ptrCast(submenu));
c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
c.g_menu_append_submenu(section, intl._("Menu"), @ptrCast(@alignCast(submenu)));
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
self.context_menu = menu;

View File

@ -9,6 +9,7 @@ const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig");
const View = @import("View.zig");
const c = @import("c.zig").c;
const intl = @import("intl");
const log = std.log.scoped(.gtk);
@ -166,8 +167,8 @@ const ButtonsView = struct {
pub fn init(root: *ClipboardConfirmation) !ButtonsView {
const cancel_text, const confirm_text = switch (root.pending_req) {
.paste => .{ "Cancel", "Paste" },
.osc_52_read, .osc_52_write => .{ "Deny", "Allow" },
.paste => .{ intl._("Cancel"), intl._("Paste") },
.osc_52_read, .osc_52_write => .{ intl._("Deny"), intl._("Allow") },
};
const cancel_button = c.gtk_button_new_with_label(cancel_text);
@ -233,8 +234,8 @@ const ButtonsView = struct {
/// The title of the window, based on the reason the prompt is being shown.
fn titleText(req: apprt.ClipboardRequest) [:0]const u8 {
return switch (req) {
.paste => "Warning: Potentially Unsafe Paste",
.osc_52_read, .osc_52_write => "Authorize Clipboard Access",
.paste => intl._("Warning: Potentially Unsafe Paste"),
.osc_52_read, .osc_52_write => intl._("Authorize Clipboard Access"),
};
}
@ -242,16 +243,16 @@ fn titleText(req: apprt.ClipboardRequest) [:0]const u8 {
/// is being shown.
fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
return switch (req) {
.paste =>
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
,
.osc_52_read =>
\\An application is attempting to read from the clipboard.
\\The current clipboard contents are shown below.
,
.osc_52_write =>
\\An application is attempting to write to the clipboard.
\\The content to write is shown below.
,
.paste => intl._(
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
),
.osc_52_read => intl._(
\\An application is attempting to read from the clipboard.
\\The current clipboard contents are shown below.
),
.osc_52_write => intl._(
\\An application is attempting to write to the clipboard.
\\The content to write is shown below.
),
};
}

View File

@ -4,6 +4,7 @@ const std = @import("std");
const c = @import("c.zig").c;
const configpkg = @import("../../config.zig");
const Surface = @import("Surface.zig");
const intl = @import("intl");
const log = std.log.scoped(.gtk);
@ -95,13 +96,20 @@ fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c.gboolean {
};
const grid_size = surface.core_surface.size.grid();
var buf: [32]u8 = undefined;
var buf: [64]u8 = undefined;
const text = std.fmt.bufPrintZ(
&buf,
"{d}c {d}r",
"{d}{s} {d}{s}",
.{
grid_size.columns,
// Translators: the abbreviation for "column" (of a grid) in your language.
// If the abbreviation cannot be intuitively understood, use the full word.
intl.ngettext("c", "c", grid_size.columns),
grid_size.rows,
// Translators: the abbreviation for "row" (of a grid) in your language.
// If the abbreviation cannot be intuitively understood, use the full word.
intl.ngettext("r", "r", grid_size.rows),
},
) catch |err| {
log.err("unable to format text: {}", .{err});

View File

@ -25,6 +25,7 @@ const ResizeOverlay = @import("ResizeOverlay.zig");
const inspector = @import("inspector.zig");
const gtk_key = @import("key.zig");
const c = @import("c.zig").c;
const intl = @import("intl");
const log = std.log.scoped(.gtk_surface);
@ -725,14 +726,16 @@ pub fn close(self: *Surface, processActive: bool) void {
c.GTK_DIALOG_MODAL,
c.GTK_MESSAGE_QUESTION,
c.GTK_BUTTONS_YES_NO,
"Close this terminal?",
intl._("Close this terminal?"),
);
c.gtk_message_dialog_format_secondary_text(
@ptrCast(alert),
"There is still a running process in the terminal. " ++
"Closing the terminal will kill this process. " ++
"Are you sure you want to close the terminal?\n\n" ++
"Click 'No' to cancel and return to your terminal.",
intl._(
\\There is still a running process in the terminal. Closing the terminal will kill this process.
\\Are you sure you want to close the terminal?
\\
\\Click 'No' to cancel and return to your terminal.
),
);
// We want the "yes" to appear destructive.
@ -1136,7 +1139,7 @@ pub fn setClipboardString(
self.app.config.@"app-notifications".@"clipboard-copy")
{
if (self.container.window()) |window|
window.sendToast("Copied to clipboard");
window.sendToast(intl._("Copied to clipboard"));
}
return;
}

View File

@ -13,6 +13,7 @@ const CoreSurface = @import("../../Surface.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
const c = @import("c.zig").c;
const intl = @import("intl");
const log = std.log.scoped(.gtk);
@ -140,11 +141,11 @@ pub fn closeWithConfirmation(tab: *Tab) void {
c.GTK_DIALOG_MODAL,
c.GTK_MESSAGE_QUESTION,
c.GTK_BUTTONS_YES_NO,
"Close this tab?",
intl._("Close this tab?"),
);
c.gtk_message_dialog_format_secondary_text(
@ptrCast(alert),
"All terminal sessions in this tab will be terminated.",
intl._("All terminal sessions in this tab will be terminated."),
);
// We want the "yes" to appear destructive.

View File

@ -26,6 +26,7 @@ const Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar;
const version = @import("version.zig");
const winproto = @import("winproto.zig");
const intl = @import("intl");
const log = std.log.scoped(.gtk);
@ -171,7 +172,7 @@ pub fn init(self: *Window, app: *App) !void {
const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: {
const btn = c.gtk_toggle_button_new();
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
c.gtk_widget_set_tooltip_text(btn, intl._("View Open Tabs"));
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
_ = c.g_object_bind_property(
btn,
@ -198,7 +199,7 @@ pub fn init(self: *Window, app: *App) !void {
{
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
c.gtk_widget_set_tooltip_text(btn, "New Tab");
c.gtk_widget_set_tooltip_text(btn, intl._("New Tab"));
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(&gtkTabNewClick), self, null, c.G_CONNECT_DEFAULT);
self.headerbar.packStart(btn);
}
@ -217,7 +218,7 @@ pub fn init(self: *Window, app: *App) !void {
// This is a really common issue where people build from source in debug and performance is really bad.
if (comptime std.debug.runtime_safety) {
const warning_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded.";
const warning_text = intl._("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
if ((comptime adwaita.versionAtLeast(1, 3, 0)) and
adwaita.enabled(&app.config) and
adwaita.versionAtLeast(1, 3, 0))
@ -793,11 +794,11 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
c.GTK_DIALOG_MODAL,
c.GTK_MESSAGE_QUESTION,
c.GTK_BUTTONS_YES_NO,
"Close this window?",
intl._("Close this window?"),
);
c.gtk_message_dialog_format_secondary_text(
@ptrCast(alert),
"All terminal sessions in this window will be terminated.",
intl._("All terminal sessions in this window will be terminated."),
);
// We want the "yes" to appear destructive.
@ -896,7 +897,7 @@ fn gtkActionAbout(
"application-name",
name,
"developer-name",
"Ghostty Developers",
intl._("Ghostty Developers").ptr,
"application-icon",
icon,
"version",
@ -915,7 +916,7 @@ fn gtkActionAbout(
"logo-icon-name",
icon,
"title",
"About Ghostty",
intl._("About Ghostty").ptr,
"version",
build_config.version_string.ptr,
"website",

View File

@ -9,6 +9,7 @@ const TerminalWindow = @import("Window.zig");
const ImguiWidget = @import("ImguiWidget.zig");
const c = @import("c.zig").c;
const CoreInspector = @import("../../inspector/main.zig").Inspector;
const intl = @import("intl");
const log = std.log.scoped(.inspector);
@ -140,7 +141,7 @@ const Window = struct {
const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window);
self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
c.gtk_window_set_title(gtk_window, intl._("Ghostty: Terminal Inspector"));
c.gtk_window_set_default_size(gtk_window, 1000, 600);
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");

View File

@ -14,6 +14,64 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
var steps = std.ArrayList(*std.Build.Step).init(b.allocator);
errdefer steps.deinit();
// Localization files (.pot, .po, .mo)
{
const update = b.step("update-translations", "Update translation files");
const gettext = b.dependency("zig_gettext", .{
// We're running this on the host, so we need to compile it for the host
.target = b.graph.host,
.optimize = cfg.optimize,
});
const xgettext = b.addRunArtifact(gettext.artifact("xgettext"));
const pot = pot: {
var src_files = try b.build_root.handle.openDir("src", .{ .iterate = true });
defer src_files.close();
var walk = try src_files.walk(b.allocator);
defer walk.deinit();
while (try walk.next()) |src| {
switch (src.kind) {
.file => if (!std.mem.endsWith(u8, src.basename, ".zig")) continue,
else => continue,
}
xgettext.addFileArg(b.path(b.pathJoin(&.{ "src", src.path })));
}
break :pot xgettext.captureStdOut();
};
// TODO: Use UpdateSourceFiles when Zig 0.14 releases
var wf_update = b.addWriteFiles();
wf_update.addCopyFileToSource(pot, "po/messages.pot");
update.dependOn(&wf_update.step);
var wf_mo = b.addWriteFiles();
var linguas = try b.build_root.handle.openFile("po/LINGUAS", .{});
defer linguas.close();
var reader = linguas.reader();
var buf: [64]u8 = undefined;
while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |locale| {
const po = b.fmt("po/{s}.po", .{locale});
const mo = b.addRunArtifact(gettext.artifact("msgfmt"));
mo.addFileArg(b.path(po));
_ = wf_mo.addCopyFile(
mo.captureStdOut(),
b.pathJoin(&.{ "share", "locale", locale, "LC_MESSAGES", "messages.mo" }),
);
const msgmerge = b.addSystemCommand(&.{ "msgmerge", "--update", "--quiet" });
msgmerge.addFileArg(b.path(po));
msgmerge.addFileArg(pot);
update.dependOn(&msgmerge.step);
}
try steps.append(&wf_mo.step);
}
// Terminfo
terminfo: {
// Encode our terminfo

View File

@ -358,6 +358,10 @@ pub fn add(
.optimize = optimize,
.with_tui = false,
}).module("zf"));
step.root_module.addImport("intl", b.dependency("libintl", .{
.target = target,
.optimize = optimize,
}).module("intl"));
// Mac Stuff
if (step.rootModuleTarget().isDarwin()) {

View File

@ -10,6 +10,7 @@ const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const renderer = @import("renderer.zig");
const xev = @import("xev");
const intl = @import("intl");
/// Global process state. This is initialized in main() for exe artifacts
/// and by ghostty_init() for lib artifacts. This should ONLY be used by
@ -162,6 +163,8 @@ pub const GlobalState = struct {
// hereafter can use this cached value.
self.resources_dir = try internal_os.resourcesDir(self.alloc);
errdefer if (self.resources_dir) |dir| self.alloc.free(dir);
try self.initI18n();
}
/// Cleans up the global state. This doesn't _need_ to be called but
@ -200,6 +203,20 @@ pub const GlobalState = struct {
std.log.warn("failed to ignore SIGPIPE err={}", .{err});
};
}
fn initI18n(self: *GlobalState) std.mem.Allocator.Error!void {
const resources = self.resources_dir orelse {
std.log.warn("resources dir not found: not localizing", .{});
return;
};
const share = std.fs.path.dirname(resources) orelse return;
const locale = try std.fs.path.joinZ(self.alloc, &.{ share, "locale" });
defer self.alloc.free(locale);
std.log.warn("locale={s}", .{locale});
try intl.bindTextDomain("messages", locale);
try intl.setTextDomain("messages");
}
};
/// Maintains the Unix resource limits that we set for our process. This

View File

@ -9,14 +9,13 @@ const Allocator = std.mem.Allocator;
/// This is highly Ghostty-specific and can likely be generalized at
/// some point but we can cross that bridge if we ever need to.
pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// If we have an environment variable set, we always use that.
// Note: we ALWAYS want to allocate here because the result is always
// freed, do not try to use internal_os.getenv or posix getenv.
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
// If we have an environment variable set, we prefer that *only* in release mode.
//
// The reason is that debug builds built by developers may have updated
// resources, and debug Ghostty launched from release Ghostty should not
// inherit old/stale resources of release Ghostty.
if (comptime builtin.mode != .Debug) {
if (try getDirFromEnv(alloc)) |dir| return dir;
}
// This is the sentinel value we look for in the path to know
@ -52,6 +51,25 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
}
}
// If we are in debug mode and we couldn't find freshly-built
// resources for some reason, we fall back to using the env var
if (comptime builtin.mode == .Debug) {
if (try getDirFromEnv(alloc)) |dir| return dir;
}
return null;
}
fn getDirFromEnv(alloc: Allocator) !?[]u8 {
// Note: we ALWAYS want to allocate here because the result is always
// freed, do not try to use internal_os.getenv or posix getenv.
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
}
return null;
}