mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Groundwork for cross-platform i18n with libintl for libghostty/macOS (#6619)
This builds on @pluiedev's excellent #6004. ## Background: The macOS (and libghostty consumer) Plan Broadly, the decision I've come to is that for cross-platform translations (i.e. strings shared across libghostty), we will be using gettext and libghostty will export helper methods to call those (e.g. `ghostty_translate` in this PR for singular forms). To be clear, **this only applies to strings owned by libghostty**. For application-level strings such as macOS-specific menu items and so on, we still have choice but will likely using native features. The reason for this is because converting gettext translations (`po`) to native formats (Xcode String Catalog, `.strings`/`.stringsdict`) is nightmare level, in particular for plural forms. I don't see a robust path to doing it. And if we don't convert and don't use gettext, then translators would have to maintain an identical translation in multiple locations. To make matters worse, the macOS translation formats require Apple-tooling for now unless you want to edit raw JSON. Leveraging gettext lets us share translations across platforms and take advantage of proven tech. ## PR Contents **`pkg/libintl` builds and statically links libintl for macOS.** macOS doesn't ship libintl with the system while Linux generally does with libc, so we need to build this ourselves. This makes gettext available to macOS. libintl is LGPL and we remain in compliance with that despite static linking because our build process is fully open source, so downstream consumers can modify our build scripts to replace it if they wanted to. ~~**`src/os/locale.zig` now sets the `LANGUAGE` environment variable on macOS based on the app's preferred languages.** macOS lets you configure the system locale separate from preferred language. We previously relied solely on `NSLocale.currentLocale`, but this only represents the system locale. We now also look at `NSLocale.preferredLanguages` (a list in priority order) and if we support a given language we set `LANGUAGE` so gettext translates properly. Notably, the above lets us debug translations in Xcode by setting alternate languages for debug builds only.~~ Removed this for a future PR since it was problematic. **`build.zig` unconditionally builds binary `mo` files** since they're required for all apprts now. **The macOS app bundles the translation strings.** This includes our GTK-specific translation strings but the size of these is so small it isn't worth the complexity of splitting up into multiple `pot`s at this time, I think. **i18n APIs moved to `src/os` from `src/apprt/gtk`.** Since these are now cross-platform/cross-apprt, they're a core API. The only notable change here is that `_` now maps to `dgettext` and explicitly specifies our domain so that it's library-friendly. The GTK apprt calls `initGlobalDomain` so that blueprint translations still work. ## Next Steps This PR is all groundwork. The macOS app doesn't leverage any of this yet, although I've verified it all works (e.g. calling the `ghostty_translate` API from Swift). For next steps, we need to have a use case for cross-platform translations and the first one I was looking at was configuration error messages and other core strings.
This commit is contained in:
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -5,5 +5,7 @@ vendor/** linguist-vendored
|
||||
website/** linguist-documentation
|
||||
pkg/breakpad/vendor/** linguist-vendored
|
||||
pkg/cimgui/vendor/** linguist-vendored
|
||||
pkg/libintl/config.h linguist-generated=true
|
||||
pkg/libintl/libintl.h linguist-generated=true
|
||||
pkg/simdutf/vendor/** linguist-vendored
|
||||
src/terminal/res/** linguist-vendored
|
||||
|
11
build.zig
11
build.zig
@ -60,6 +60,7 @@ pub fn build(b: *std.Build) !void {
|
||||
// The xcframework build always installs resources because our
|
||||
// macOS xcode project contains references to them.
|
||||
resources.install();
|
||||
i18n.install();
|
||||
|
||||
// If we aren't emitting docs we need to emit a placeholder so
|
||||
// our macOS xcodeproject builds.
|
||||
@ -82,6 +83,16 @@ pub fn build(b: *std.Build) !void {
|
||||
{
|
||||
const run_cmd = b.addRunArtifact(exe.exe);
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
|
||||
// Set the proper resources dir so things like shell integration
|
||||
// work correctly. If we're running `zig build run` in Ghostty,
|
||||
// this also ensures it overwrites the release one with our debug
|
||||
// build.
|
||||
run_cmd.setEnvironmentVariable(
|
||||
"GHOSTTY_RESOURCES_DIR",
|
||||
b.getInstallPath(.prefix, "share/ghostty"),
|
||||
);
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
|
@ -63,6 +63,7 @@
|
||||
.gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell" },
|
||||
.harfbuzz = .{ .path = "./pkg/harfbuzz" },
|
||||
.highway = .{ .path = "./pkg/highway" },
|
||||
.libintl = .{ .path = "./pkg/libintl" },
|
||||
.libpng = .{ .path = "./pkg/libpng" },
|
||||
.macos = .{ .path = "./pkg/macos" },
|
||||
.oniguruma = .{ .path = "./pkg/oniguruma" },
|
||||
|
8
build.zig.zon.nix
generated
8
build.zig.zon.nix
generated
@ -331,6 +331,14 @@ in
|
||||
hash = "sha256-NUqLRTm1iOcLmOxwhEJz4/J0EwLEw3e8xOgbPRhm98k=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "1220f870c853529233ea64a108acaaa81f8d06d7ff4b66c76930be7d78d508aff7a2";
|
||||
path = fetchZigArtifact {
|
||||
name = "gettext";
|
||||
url = "https://deps.files.ghostty.org/gettext-0.24.tar.gz";
|
||||
hash = "sha256-yRhQPVk9cNr0hE0XWhPYFq+stmfAb7oeydzVACwVGLc=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb";
|
||||
path = fetchZigArtifact {
|
||||
|
9
build.zig.zon.txt
generated
9
build.zig.zon.txt
generated
@ -4,6 +4,7 @@ git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e
|
||||
https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz
|
||||
https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
|
||||
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
|
||||
https://deps.files.ghostty.org/gettext-0.24.tar.gz
|
||||
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
|
||||
https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst
|
||||
https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz
|
||||
@ -22,13 +23,13 @@ https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23c
|
||||
https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
|
||||
https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz
|
||||
https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz
|
||||
https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
|
||||
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
|
||||
https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz
|
||||
https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz
|
||||
https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
|
||||
https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz
|
||||
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
|
||||
https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz
|
||||
https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e21d5ffd19605741d0e3e19d7c5a8c6c25648673.tar.gz
|
||||
https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz
|
||||
https://github.com/mitchellh/libxev/archive/8943932a668f338cb2c500f6e1a7396bacd8b55d.tar.gz
|
||||
|
5
build.zig.zon2json-lock
generated
5
build.zig.zon2json-lock
generated
@ -154,6 +154,11 @@
|
||||
"url": "https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz",
|
||||
"hash": "sha256-NUqLRTm1iOcLmOxwhEJz4/J0EwLEw3e8xOgbPRhm98k="
|
||||
},
|
||||
"1220f870c853529233ea64a108acaaa81f8d06d7ff4b66c76930be7d78d508aff7a2": {
|
||||
"name": "gettext",
|
||||
"url": "https://deps.files.ghostty.org/gettext-0.24.tar.gz",
|
||||
"hash": "sha256-yRhQPVk9cNr0hE0XWhPYFq+stmfAb7oeydzVACwVGLc="
|
||||
},
|
||||
"1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb": {
|
||||
"name": "oniguruma",
|
||||
"url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz",
|
||||
|
@ -669,6 +669,7 @@ typedef struct {
|
||||
int ghostty_init(void);
|
||||
void ghostty_cli_main(uintptr_t, char**);
|
||||
ghostty_info_s ghostty_info(void);
|
||||
const char* ghostty_translate(const char*);
|
||||
|
||||
ghostty_config_t ghostty_config_new();
|
||||
void ghostty_config_free(ghostty_config_t);
|
||||
|
@ -40,6 +40,7 @@
|
||||
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
||||
A546F1142D7B68D7003B11A0 /* locale in Resources */ = {isa = PBXBuildFile; fileRef = A546F1132D7B68D7003B11A0 /* locale */; };
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */; };
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */; };
|
||||
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; };
|
||||
@ -138,6 +139,7 @@
|
||||
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = "<group>"; };
|
||||
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
|
||||
A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = "<group>"; };
|
||||
A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconView.swift; sourceTree = "<group>"; };
|
||||
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extension.swift"; sourceTree = "<group>"; };
|
||||
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = "<group>"; };
|
||||
@ -424,6 +426,7 @@
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */,
|
||||
A586167B2B7703CC009BDB1D /* fish */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
A546F1132D7B68D7003B11A0 /* locale */,
|
||||
A5985CE52C33060F00C57AD3 /* man */,
|
||||
9351BE8E2D22937F003B3499 /* nvim */,
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||
@ -593,20 +596,21 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */,
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */,
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */,
|
||||
A546F1142D7B68D7003B11A0 /* locale in Resources */,
|
||||
A5985CE62C33060F00C57AD3 /* man in Resources */,
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */,
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||
552964E62B34A9B400030505 /* vim in Resources */,
|
||||
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */,
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
||||
A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */,
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */,
|
||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */,
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
|
||||
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */,
|
||||
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */,
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */,
|
||||
A5985CE62C33060F00C57AD3 /* man in Resources */,
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||
552964E62B34A9B400030505 /* vim in Resources */,
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */,
|
||||
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
101
pkg/libintl/build.zig
Normal file
101
pkg/libintl/build.zig
Normal file
@ -0,0 +1,101 @@
|
||||
//! Provides libintl for macOS.
|
||||
//!
|
||||
//! IMPORTANT: This is only for macOS. We could support other platforms
|
||||
//! if/when we need to but generally Linux provides libintl in libc.
|
||||
//! Windows we'll have to figure out when we get there.
|
||||
//!
|
||||
//! Since this is only for macOS, there's a lot of hardcoded stuff
|
||||
//! here that assumes macOS. For example, I generated the config.h
|
||||
//! on my own machine (a Mac) and then copied it here. This isn't
|
||||
//! ideal since we should do the same detection that gettext's configure
|
||||
//! script does, but its quite a bit of work to do that.
|
||||
//!
|
||||
//! UPGRADING: If you need to upgrade gettext, then the only thing to
|
||||
//! really watch out for is the xlocale.h include we added manually
|
||||
//! at the end of config.h. The comment there notes why. When we upgrade
|
||||
//! we should audit our config.h and make sure we add that back (if we
|
||||
//! have to).
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const upstream = b.dependency("gettext", .{});
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
try flags.appendSlice(&.{
|
||||
"-DHAVE_CONFIG_H",
|
||||
"-DLOCALEDIR=\"\"",
|
||||
});
|
||||
|
||||
{
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "intl",
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib.linkLibC();
|
||||
lib.addIncludePath(b.path(""));
|
||||
lib.addIncludePath(upstream.path("gettext-runtime/intl"));
|
||||
lib.addIncludePath(upstream.path("gettext-runtime/intl/gnulib-lib"));
|
||||
|
||||
if (target.result.isDarwin()) {
|
||||
const apple_sdk = @import("apple_sdk");
|
||||
try apple_sdk.addPaths(b, &lib.root_module);
|
||||
}
|
||||
|
||||
lib.addCSourceFiles(.{
|
||||
.root = upstream.path("gettext-runtime/intl"),
|
||||
.files = srcs,
|
||||
.flags = flags.items,
|
||||
});
|
||||
|
||||
lib.installHeader(b.path("libintl.h"), "libintl.h");
|
||||
b.installArtifact(lib);
|
||||
}
|
||||
}
|
||||
|
||||
const srcs: []const []const u8 = &.{
|
||||
"bindtextdom.c",
|
||||
"dcgettext.c",
|
||||
"dcigettext.c",
|
||||
"dcngettext.c",
|
||||
"dgettext.c",
|
||||
"dngettext.c",
|
||||
"explodename.c",
|
||||
"finddomain.c",
|
||||
"gettext.c",
|
||||
"hash-string.c",
|
||||
"intl-compat.c",
|
||||
"l10nflist.c",
|
||||
"langprefs.c",
|
||||
"loadmsgcat.c",
|
||||
"localealias.c",
|
||||
"log.c",
|
||||
"ngettext.c",
|
||||
"plural-exp.c",
|
||||
"plural.c",
|
||||
"setlocale.c",
|
||||
"textdomain.c",
|
||||
"version.c",
|
||||
"compat.c",
|
||||
|
||||
// There's probably a better way to detect that we need these, but
|
||||
// these are hardcoded for now for macOS.
|
||||
"gnulib-lib/getlocalename_l-unsafe.c",
|
||||
"gnulib-lib/localename.c",
|
||||
"gnulib-lib/localename-environ.c",
|
||||
"gnulib-lib/localename-unsafe.c",
|
||||
"gnulib-lib/setlocale-lock.c",
|
||||
"gnulib-lib/setlocale_null.c",
|
||||
"gnulib-lib/setlocale_null-unlocked.c",
|
||||
|
||||
// Not needed for macOS, but we might need them for other platforms.
|
||||
// If we expand this to support other platforms, we should uncomment
|
||||
// these.
|
||||
// "osdep.c",
|
||||
// "printf.c",
|
||||
};
|
13
pkg/libintl/build.zig.zon
Normal file
13
pkg/libintl/build.zig.zon
Normal file
@ -0,0 +1,13 @@
|
||||
.{
|
||||
.name = "libintl",
|
||||
.version = "0.24.0",
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
.gettext = .{
|
||||
.url = "https://deps.files.ghostty.org/gettext-0.24.tar.gz",
|
||||
.hash = "1220f870c853529233ea64a108acaaa81f8d06d7ff4b66c76930be7d78d508aff7a2",
|
||||
},
|
||||
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
},
|
||||
}
|
2370
pkg/libintl/config.h
generated
Normal file
2370
pkg/libintl/config.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
pkg/libintl/libgnuintl.h
Normal file
1
pkg/libintl/libgnuintl.h
Normal file
@ -0,0 +1 @@
|
||||
#include "libintl.h"
|
1168
pkg/libintl/libintl.h
generated
Normal file
1168
pkg/libintl/libintl.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -131,14 +131,18 @@ which should be filled in accordingly. You can then add your translations
|
||||
within the newly created translation file.
|
||||
|
||||
Afterwards, you need to update the list of known locales within Ghostty's
|
||||
build system. To do so, open `src/build/GhosttyI18n.zig` and find the list
|
||||
of locales under the `locale` variable, then append the full locale name
|
||||
build system. To do so, open `src/os/i18n.zig` and find the list
|
||||
of locales under the `locales` variable, then add the full locale name
|
||||
into the list.
|
||||
|
||||
The order matters, so make sure to place your locale in the correct position.
|
||||
Read the comment above the variable for more details on the order. If you're
|
||||
unsure, place it at the end of the list.
|
||||
|
||||
```zig
|
||||
const locales = [_][]const u8{
|
||||
"zh_CN.UTF-8",
|
||||
// <- Add your locale here
|
||||
// <- Add your locale here (probably)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -40,7 +40,6 @@ const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const i18n = @import("i18n.zig");
|
||||
const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
@ -101,11 +100,6 @@ quit_timer: union(enum) {
|
||||
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
_ = opts;
|
||||
|
||||
// This can technically be placed *anywhere* because we don't have any
|
||||
// localized log messages. It just has to be placed before any localized
|
||||
// widgets are drawn.
|
||||
try i18n.init(core_app.alloc);
|
||||
|
||||
// Log our GTK version
|
||||
log.info("GTK version build={d}.{d}.{d} runtime={d}.{d}.{d}", .{
|
||||
c.GTK_MAJOR_VERSION,
|
||||
@ -124,6 +118,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
c.adw_get_micro_version(),
|
||||
});
|
||||
|
||||
// Set gettext global domain to be our app so that our unqualified
|
||||
// translations map to our translations.
|
||||
try internal_os.i18n.initGlobalDomain();
|
||||
|
||||
// Load our configuration
|
||||
var config = try Config.load(core_app.alloc);
|
||||
errdefer config.deinit();
|
||||
|
@ -6,12 +6,12 @@ const gio = @import("gio");
|
||||
const adw = @import("adw");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const i18n = @import("../../os/main.zig").i18n;
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
const i18n = @import("i18n.zig");
|
||||
|
||||
const log = std.log.scoped(.close_dialog);
|
||||
|
||||
|
@ -16,6 +16,7 @@ const build_options = @import("build_options");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const i18n = @import("../../os/main.zig").i18n;
|
||||
const input = @import("../../input.zig");
|
||||
const renderer = @import("../../renderer.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
@ -36,7 +37,6 @@ const gtk_key = @import("key.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const Builder = @import("Builder.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
const i18n = @import("i18n.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_surface);
|
||||
|
||||
|
@ -18,6 +18,7 @@ const gtk = @import("gtk");
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const i18n = @import("../../os/main.zig").i18n;
|
||||
const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
@ -34,7 +35,6 @@ const HeaderBar = @import("headerbar.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
const version = @import("version.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
const i18n = @import("i18n.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
|
@ -1,37 +0,0 @@
|
||||
//! I18n support for the GTK frontend based on gettext/libintl
|
||||
//!
|
||||
//! This is normally built into the C standard library for the *vast* majority
|
||||
//! of users who use glibc, but for musl users we fall back to the `gettext-tiny`
|
||||
//! stub implementation which provides all of the necessary interfaces.
|
||||
//! Musl users who do want to use localization should know what they need to do.
|
||||
|
||||
const std = @import("std");
|
||||
const global = &@import("../../global.zig").state;
|
||||
const build_config = @import("../../build_config.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_i18n);
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !void {
|
||||
const resources_dir = global.resources_dir orelse {
|
||||
log.warn("resource dir not found; not localizing", .{});
|
||||
return;
|
||||
};
|
||||
const share_dir = std.fs.path.dirname(resources_dir) orelse {
|
||||
log.warn("resource dir not placed in a share/ directory; not localizing", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const locale_dir = try std.fs.path.joinZ(alloc, &.{ share_dir, "locale" });
|
||||
defer alloc.free(locale_dir);
|
||||
|
||||
// The only way these calls can fail is if we're out of memory
|
||||
_ = bindtextdomain(build_config.bundle_id, locale_dir.ptr) orelse return error.OutOfMemory;
|
||||
_ = textdomain(build_config.bundle_id) orelse return error.OutOfMemory;
|
||||
}
|
||||
|
||||
// Manually include function definitions for the gettext functions
|
||||
// as libintl.h isn't always easily available (e.g. in musl)
|
||||
extern fn bindtextdomain(domainname: [*:0]const u8, dirname: [*:0]const u8) ?[*:0]const u8;
|
||||
extern fn textdomain(domainname: [*:0]const u8) ?[*:0]const u8;
|
||||
pub extern fn gettext(msgid: [*:0]const u8) [*:0]const u8;
|
||||
pub const _ = gettext;
|
@ -3,13 +3,10 @@ const GhosttyI18n = @This();
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
const domain = "com.mitchellh.ghostty";
|
||||
|
||||
const locales = [_][]const u8{
|
||||
"zh_CN.UTF-8",
|
||||
};
|
||||
|
||||
owner: *std.Build,
|
||||
steps: []*std.Build.Step,
|
||||
|
||||
@ -18,23 +15,22 @@ steps: []*std.Build.Step,
|
||||
update_step: *std.Build.Step,
|
||||
|
||||
pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
|
||||
_ = cfg;
|
||||
|
||||
var steps = std.ArrayList(*std.Build.Step).init(b.allocator);
|
||||
defer steps.deinit();
|
||||
|
||||
if (cfg.app_runtime == .gtk) {
|
||||
// Output the .mo files used by the GTK apprt
|
||||
inline for (locales) |locale| {
|
||||
const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" });
|
||||
msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po"));
|
||||
inline for (internal_os.i18n.locales) |locale| {
|
||||
const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" });
|
||||
msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po"));
|
||||
|
||||
try steps.append(&b.addInstallFile(
|
||||
msgfmt.captureStdOut(),
|
||||
std.fmt.comptimePrint(
|
||||
"share/locale/{s}/LC_MESSAGES/{s}.mo",
|
||||
.{ locale, domain },
|
||||
),
|
||||
).step);
|
||||
}
|
||||
try steps.append(&b.addInstallFile(
|
||||
msgfmt.captureStdOut(),
|
||||
std.fmt.comptimePrint(
|
||||
"share/locale/{s}/LC_MESSAGES/{s}.mo",
|
||||
.{ locale, domain },
|
||||
),
|
||||
).step);
|
||||
}
|
||||
|
||||
return .{
|
||||
@ -107,7 +103,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
|
||||
"po/" ++ domain ++ ".pot",
|
||||
);
|
||||
|
||||
inline for (locales) |locale| {
|
||||
inline for (internal_os.i18n.locales) |locale| {
|
||||
const msgmerge = b.addSystemCommand(&.{ "msgmerge", "-q" });
|
||||
msgmerge.addFileArg(b.path("po/" ++ locale ++ ".po"));
|
||||
msgmerge.addFileArg(xgettext.captureStdOut());
|
||||
|
@ -381,6 +381,17 @@ pub fn add(
|
||||
if (self.config.renderer == .opengl) {
|
||||
step.linkFramework("OpenGL");
|
||||
}
|
||||
|
||||
// Apple platforms do not include libc libintl so we bundle it.
|
||||
// This is LGPL but since our source code is open source we are
|
||||
// in compliance with the LGPL since end users can modify this
|
||||
// build script to replace the bundled libintl with their own.
|
||||
const libintl_dep = b.dependency("libintl", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
step.linkLibrary(libintl_dep.artifact("intl"));
|
||||
try static_libs.append(libintl_dep.artifact("intl").getEmittedBin());
|
||||
}
|
||||
|
||||
// cimgui
|
||||
|
@ -172,6 +172,11 @@ 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);
|
||||
|
||||
// Setup i18n
|
||||
if (self.resources_dir) |v| internal_os.i18n.init(v) catch |err| {
|
||||
std.log.warn("failed to init i18n, translations will not be available err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Cleans up the global state. This doesn't _need_ to be called but
|
||||
|
@ -15,6 +15,7 @@ const build_config = @import("build_config.zig");
|
||||
const main = @import("main_ghostty.zig");
|
||||
const state = &@import("global.zig").state;
|
||||
const apprt = @import("apprt.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
|
||||
// Some comptime assertions that our C API depends on.
|
||||
comptime {
|
||||
@ -88,3 +89,13 @@ export fn ghostty_info() Info {
|
||||
.version_len = build_config.version_string.len,
|
||||
};
|
||||
}
|
||||
|
||||
/// Translate a string maintained by libghostty into the current
|
||||
/// application language. This will return the same string (same pointer)
|
||||
/// if no translation is found, so the pointer must be stable through
|
||||
/// the function call.
|
||||
///
|
||||
/// This should only be used for singular strings maintained by Ghostty.
|
||||
export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
|
||||
return internal_os.i18n._(msgid);
|
||||
}
|
||||
|
102
src/os/i18n.zig
Normal file
102
src/os/i18n.zig
Normal file
@ -0,0 +1,102 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_config = @import("../build_config.zig");
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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 script code, then we'd pick
|
||||
/// the first locale that matches `zh`.
|
||||
///
|
||||
/// For ordering, we prefer:
|
||||
///
|
||||
/// 1. The most common locales first, since there are places in the code
|
||||
/// where we do linear searches for a locale and we want to minimize
|
||||
/// the number of iterations for the common case.
|
||||
///
|
||||
/// 2. Alphabetical for otherwise equally common locales.
|
||||
///
|
||||
/// 3. Most preferred locale for a language without a country code.
|
||||
///
|
||||
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,
|
||||
};
|
||||
|
||||
/// Initialize i18n support for the application. This should be
|
||||
/// called automatically by the global state initialization
|
||||
/// in global.zig.
|
||||
///
|
||||
/// This calls `bindtextdomain` for gettext with the proper directory
|
||||
/// of translations. This does NOT call `textdomain` as we don't
|
||||
/// want to set the domain for the entire application since this is also
|
||||
/// used by libghostty.
|
||||
pub fn init(resources_dir: []const u8) InitError!void {
|
||||
// i18n is unsupported on Windows
|
||||
if (builtin.os.tag == .windows) return;
|
||||
|
||||
// Our resources dir is always nested below the share dir that
|
||||
// is standard for translations.
|
||||
const share_dir = std.fs.path.dirname(resources_dir) orelse
|
||||
return error.InvalidResourcesDir;
|
||||
|
||||
// Build our locale path
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch
|
||||
return error.OutOfMemory;
|
||||
|
||||
// Bind our bundle ID to the given locale path
|
||||
log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path });
|
||||
_ = bindtextdomain(build_config.bundle_id, path.ptr) orelse
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
|
||||
/// Set the global gettext domain to our bundle ID, allowing unqualified
|
||||
/// `gettext` (`_`) calls to look up translations for our application.
|
||||
///
|
||||
/// This should only be called for apprts that are fully owning the
|
||||
/// Ghostty application. This should not be called for libghostty users.
|
||||
pub fn initGlobalDomain() error{OutOfMemory}!void {
|
||||
_ = textdomain(build_config.bundle_id) orelse return error.OutOfMemory;
|
||||
}
|
||||
|
||||
/// Translate a message for the Ghostty domain.
|
||||
pub fn _(msgid: [*:0]const u8) [*:0]const u8 {
|
||||
return dgettext(build_config.bundle_id, msgid);
|
||||
}
|
||||
|
||||
/// This can be called at any point a compile-time-known locale is
|
||||
/// available. This will use comptime to verify the locale is supported.
|
||||
pub fn staticLocale(comptime v: [*:0]const u8) [*:0]const u8 {
|
||||
comptime {
|
||||
for (locales) |locale| {
|
||||
if (std.mem.eql(u8, locale, v)) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
@compileError("unsupported locale");
|
||||
}
|
||||
}
|
||||
|
||||
// Manually include function definitions for the gettext functions
|
||||
// as libintl.h isn't always easily available (e.g. in musl)
|
||||
extern fn bindtextdomain(domainname: [*:0]const u8, dirname: [*:0]const u8) ?[*:0]const u8;
|
||||
extern fn textdomain(domainname: [*:0]const u8) ?[*:0]const u8;
|
||||
extern fn dgettext(domainname: [*:0]const u8, msgid: [*:0]const u8) [*:0]const u8;
|
@ -1,10 +1,12 @@
|
||||
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 i18n = internal_os.i18n;
|
||||
|
||||
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 +62,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 +73,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,14 +94,14 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ const resourcesdir = @import("resourcesdir.zig");
|
||||
pub const args = @import("args.zig");
|
||||
pub const cgroup = @import("cgroup.zig");
|
||||
pub const hostname = @import("hostname.zig");
|
||||
pub const i18n = @import("i18n.zig");
|
||||
pub const passwd = @import("passwd.zig");
|
||||
pub const xdg = @import("xdg.zig");
|
||||
pub const windows = @import("windows.zig");
|
||||
|
Reference in New Issue
Block a user