gtk: add localization support, take 3 (#6004)

This is my third (!) attempt at implementing localization support. By
leveraging GTK builder to do most of the `gettext` calls, I can avoid
the whole mess about missing symbols on non-glibc platforms.

Added some documentation too for contributors and translators, just for
good measure.

Supersedes #5214, resolves the GTK half of #2357
This commit is contained in:
Leah Amelia Chen
2025-03-05 20:12:52 +01:00
committed by GitHub
18 changed files with 925 additions and 15 deletions

View File

@ -25,6 +25,7 @@ jobs:
- prettier
- alejandra
- typos
- translations
- test-pkg-linux
- test-debian-12
steps:
@ -593,6 +594,42 @@ jobs:
- name: typos check
run: nix develop -c typos
translations:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-sm
timeout-minutes: 60
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.0
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v15
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
skipPush: true
useDaemon: false # sometimes fails on short jobs
- name: check translations
run: |
old_pot=$(mktemp)
cp po/com.mitchellh.ghostty.pot "$old_pot"
nix develop -c zig build update-translations
# Compare previous POT to current POT
msgcmp "$old_pot" po/com.mitchellh.ghostty.pot --use-untranslated
# Compare all other POs to current POT
for f in po/*.po; do msgcmp "$f" po/com.mitchellh.ghostty.pot; done
test-pkg-linux:
strategy:
fail-fast: false

View File

@ -21,6 +21,15 @@ All issues are actionable. Pick one and start working on it. Thank you.
If you need help or guidance, comment on the issue. Issues that are extra
friendly to new contributors are tagged with "contributor friendly".
**I'd like to translate Ghostty to my language!**
We have written a [Translator's Guide](po/README_CONTRIBUTORS.md) for
everyone interested in contributing translations to Ghostty.
Translations usually do not need to go through the process of issue triage
and you can submit pull requests directly, although please make sure that
our [Style Guide](po/README_CONTRIBUTORS.md#style-guide) is followed before
submission.
**I have a bug!**
1. Search the issue tracker and discussions for similar issues.

View File

@ -11,6 +11,7 @@ pub fn build(b: *std.Build) !void {
// Ghostty resources like terminfo, shell integration, themes, etc.
const resources = try buildpkg.GhosttyResources.init(b, &config);
const i18n = try buildpkg.GhosttyI18n.init(b, &config);
// Ghostty dependencies used by many artifacts.
const deps = try buildpkg.SharedDeps.init(b, &config);
@ -39,6 +40,7 @@ pub fn build(b: *std.Build) !void {
if (config.app_runtime != .none) {
exe.install();
resources.install();
i18n.install();
}
// Libghostty
@ -103,4 +105,11 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&test_run.step);
}
}
// update-translations does what it sounds like and updates the "pot"
// files. These should be committed to the repo.
{
const step = b.step("update-translations", "Update translation files");
step.dependOn(i18n.update_step);
}
}

View File

@ -35,6 +35,7 @@
gobject-introspection,
libadwaita,
blueprint-compiler,
gettext,
adwaita-icon-theme,
hicolor-icon-theme,
harfbuzz,
@ -129,6 +130,9 @@ in
# wasm
wabt
wasmtime
# Localization
gettext
]
++ lib.optionals stdenv.hostPlatform.isLinux [
# My nix shell environment installs the non-interactive version

View File

@ -18,6 +18,7 @@
libadwaita,
blueprint-compiler,
libxml2,
gettext,
wrapGAppsHook4,
gsettings-desktop-schemas,
git,
@ -64,6 +65,7 @@ in
../dist/linux
../images
../include
../po
../pkg
../src
../vendor
@ -87,6 +89,7 @@ in
wrapGAppsHook4
blueprint-compiler
libxml2 # for xmllint
gettext
]
++ lib.optionals enableWayland [
wayland-scanner

72
po/README_CONTRIBUTORS.md Normal file
View File

@ -0,0 +1,72 @@
# Localizing Ghostty: The Contributors' Guide
Ghostty uses the `gettext` library/framework for localization, which has the
distinct benefit of being able to be consumed directly by our two main
app runtimes: macOS and GTK (Linux). The core would ideally remain agnostic
to localization efforts, as not all consumers of libghostty would be interested
in localization support. Thus, implementors of app runtimes are left responsible
for any localization that they may add.
## GTK
In the GTK app runtime, translable strings are mainly sourced from Blueprint
files (located under `src/apprt/gtk/ui`). Blueprints have a native syntax for
translatable strings, which look like this:
```zig
// Translators: This is the name of the button that opens the about dialog.
title: _("About Ghostty");
```
The `// Translators:` comment provides additional context to the translator
if the string itself is unclear as to what its purpose is or where it's located.
By default identical strings are collapsed together into one translatable entry.
To avoid this, assign a _context_ to the string:
```zig
label: C_("menu action", "Copy");
```
Translatable strings can also be sourced from Zig source files. This is useful
when the string must be chosen dynamically at runtime, or when it requires
additional formatting. The `i18n.` prefix is necessary as `_` is not allowed
as a bare identifier in Zig.
```zig
const i18n = @import("i18n.zig");
const text = if (awesome)
i18n._("My awesome label :D")
else
i18n._("My not-so-awesome label :(");
const label = gtk.Label.new(text);
```
All translatable strings are extracted into the _translation template file_,
located under `po/com.mitchellh.ghostty.pot`. **This file must stay in sync with
the list of translatable strings present in source code or Blueprints at all times.**
A CI action would be run for every PR, which checks if the translation template
requires any updates. You can update the translation template by running
`zig build update-translations`, which would also synchronize translation files
for other locales (`.po` files) to reflect the state of the template file.
During the build process, each locale in `.po` files is compiled
into binary `.mo` files, stored under `share/locale/<LOCALE>/LC_MESSAGES/com.mitchellh.ghostty.mo`.
This can be directly accessed by `libintl`, which provide the various `gettext`
C functions that can be called either by Zig code directly, or by the GTK builder
(recommended).
> [!NOTE]
> For the vast majority of users, no additional library needs to be installed
> in order to get localizations, since `libintl` is a part of the GNU C standard
> library. For users using alternative C standard libraries like musl, they must
> use a stub implementation such as [`gettext-tiny`](https://github.com/sabotage-linux/gettext-tiny)
> that offer no-op symbols for the translation functions, or by using a build of
> `libintl` that works for them.
## macOS
> [!NOTE]
> The localization system is not yet implemented for macOS.

183
po/README_TRANSLATORS.md Normal file
View File

@ -0,0 +1,183 @@
# Localizing Ghostty: The Translators' Guide
First of all, thanks for helping us localize Ghostty!
To begin contributing, please make sure that you have installed `gettext`
on your system, which should be available under the `gettext` package
for most Linux and macOS package managers.
You can install `gettext` on Windows in a few ways:
- Through [an installer](https://mlocati.github.io/articles/gettext-iconv-windows.html);
- Through package managers like Scoop (`scoop install gettext`) and
WinGet (`winget install gettext`) which repackages the aforementioned installer;
- Through Unix-like environments like [Cygwin](https://cygwin.com/cygwin/packages/summary/gettext.html)
and [MSYS2](https://packages.msys2.org/base/gettext).
> [!WARNING]
>
> Unlike what some tutorials suggest, **we do not recommend installing `gettext`
> through GnuWin32**, since it hasn't been updated since 2010 and very likely
> does not work on modern Windows versions.
You can verify that `gettext` has been successfully installed by running the
command `gettext -V`. If everything went correctly, you should see an output like this:
```console
$ gettext -V
gettext (GNU gettext-runtime) 0.21.1
Copyright (C) 1995-2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by Ulrich Drepper.
```
With this, you're ready to localize!
## Editing translation files
All translation files lie in the `po/` directory, including the main _template_
file called `com.mitchellh.ghostty.pot`. **Do not edit this file.** The
template is generated automatically from Ghostty's code and resources, and are
intended to be regenerated by code contributors. If there is a problem with
the template file, please reach out to a code contributor.
Instead, only edit the translation file corresponding to your language/locale,
identified via the its _locale name_: for example, `de_DE.UTF-8.po` would be
the translation file for German (language code `de`) as spoken in Germany
(country code `DE`). The GNU `gettext` manual contains
[further information about locale names](https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Names-1),
including a list of language and country codes.
> [!NOTE]
>
> If the translation file for your locale does not yet exist, see the
> ["Creating new translation files" section](#creating-new-translation-files)
> of this document on how to create one.
The `.po` file contains a list of entries that look like this:
```po
#. Translators: the category in the right-click context menu that contains split items for all directions
#: src/apprt/gtk/ui/1.0/menu-surface-context-menu.blp:38
# 译注:其他终端程序对 Split 皆有不同翻译,此处采取最直观的翻译方式
msgctxt "Context menu"
msgid "Split"
msgstr "分屏"
```
The `msgid` line contains the original string in English, and the `msgstr` line
should contain the translation for your language. Occasionally there will also
be a `msgctxt` line that differentiates strings that are identical in English,
based on its context.
Lines beginning with `#` are comments, of which there are several kinds:
- Pay attention to comments beginning with `#.`. They are comments left
by _developers_ providing more context to the string.
- Comments that begin with `#:` are _source comments_: they link
the string to source code or resource files. You normally don't need to
consider these in your translations.
- You may also leave comments of your own to be read by _other translators_,
beginning with `# `. These comments are specific to your locale and don't
affect translators in other locales.
The first entry of the `.po` file has an empty `msgid`. This entry is special
as it stores the metadata related to the `.po` file itself. You usually do
not need to modify it.
## Creating new translation files
You can use the `msginit` tool to create new translation files.
Run the command below, optionally replacing `$LANG` with the name of a locale
that is _different_ to your system locale, or if the `LANG` environmental
variable is not set.
```console
$ msginit -i po/com.mitchellh.ghostty.pot -l $LANG -o "po/$LANG.po"
```
> [!NOTE]
>
> Ghostty enforces the convention that all parts of the locale, including the
> language code, country code, encoding, and possible regional variants
> **must** be communicated in the file name. Files like `pt.po` are not
> acceptable, while `pt_BR.UTF-8.po` is.
>
> This is to allow us to more easily accommodate regional variants of a
> language in the future, and to reject translations that may not be applicable
> to all speakers of a language (e.g. an unqualified `zh.po` may contain
> terminology specific to Chinese speakers in Mainland China, which are not
> found in Taiwan. Using `zh_CN.UTF-8.po` would allow that difference to be
> communicated.)
> [!WARNING]
>
> **Make sure your selected locale uses the UTF-8 encoding, as it is the sole
> encoding supported by Ghostty and its dependencies.**
>
> For backwards compatibility reasons, some locales may default to a non-UTF-8
> encoding when an encoding is not specified. For instance, `de_DE` defaults
> to using the legacy ISO-8859-1 encoding, which is incompatible with UTF-8.
> You need to manually instruct `msginit` to use UTF-8 in these instances,
> by appending `.UTF-8` to the end of the locale name (e.g. `de_DE.UTF-8`).
`msginit` may prompt you for other information such as your email address,
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
into the list.
```zig
const locales = [_][]const u8{
"zh_CN.UTF-8",
// <- Add your locale here
}
```
You should then be able to run `zig build` and see your translations in action.
## Style Guide
These are general style guidelines for translations. Naturally, the specific
recommended standards will differ based on the specific language/locale,
but these should serve as a baseline for the tone and voice of any translation.
- **Prefer an instructive, yet professional tone.**
In languages that exhibit distinctions based on formality,
prefer the formality that is expected of instructive material on the internet.
The user should be considered an equal peer of the program and the translator,
not an esteemed customer.
- **Use simple to understand language and avoid jargon.**
Explain concepts that may be familiar in an English-speaking context,
but are uncommon in your language.
- **Do not overly literally translate foreign concepts to your language.**
Care should be taken so that your translations make sense to a reader without
any background knowledge of the English source text. To _localize_ is to
transfer a concept between languages, not to translate each word at face value.
- **Be consistent with stylistic and tonal choices.**
Consult the translations made by previous translators, and try to emulate them.
Do not overwrite someone else's hard work without substantial justification.
- **Make Ghostty fit in with surrounding applications.**
Follow existing translations for terms and concepts if possible, even when
they are suboptimal. Follow the writing styles prescribed by the human
interface guidelines of each platform Ghostty is available for, including the
[GNOME Human Interface Guidelines](https://developer.gnome.org/hig/guidelines/writing-style.html)
on Linux, and [Apple's Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/writing)
on macOS.

View File

@ -0,0 +1,204 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-02-28 22:12+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr ""
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr ""
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10
msgid "Cancel"
msgstr ""
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
msgid "Copy"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
msgid "Paste"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
msgid "Clear"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
msgid "Reset"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
msgid "Split"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
msgid "Change Title…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
msgid "Split Up"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
msgid "Split Down"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
msgid "Split Left"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
msgid "Split Right"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
msgid "Tab"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
#: src/apprt/gtk/Window.zig:239
msgid "New Tab"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35
msgid "Close Tab"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73
msgid "Window"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18
msgid "New Window"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23
msgid "Close Window"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Open Configuration"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Reload Configuration"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Terminal Inspector"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102
#: src/apprt/gtk/Window.zig:923
msgid "About Ghostty"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
msgid "Quit"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
#: src/apprt/gtk/Window.zig:192
msgid "Main Menu"
msgstr ""
#: src/apprt/gtk/Window.zig:212
msgid "View Open Tabs"
msgstr ""
#: src/apprt/gtk/Window.zig:257
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
#: src/apprt/gtk/Window.zig:660
msgid "Reloaded the configuration"
msgstr ""
#: src/apprt/gtk/Window.zig:904
msgid "Ghostty Developers"
msgstr ""
#: src/apprt/gtk/Surface.zig:1236
msgid "Copied to clipboard"
msgstr ""

204
po/zh_CN.UTF-8.po Normal file
View File

@ -0,0 +1,204 @@
# Chinese translations for com.mitchellh.ghostty package
# com.mitchellh.ghostty 软件包的简体中文翻译.
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Leah <hi@pluie.me>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-02-28 22:12+0100\n"
"PO-Revision-Date: 2025-02-27 09:16+0100\n"
"Last-Translator: Leah <hi@pluie.me>\n"
"Language-Team: Chinese (simplified) <i18n-zh@googlegroups.com>\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr "更改终端标题"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "留空以重置至默认标题。"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10
msgid "Cancel"
msgstr "取消"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
msgstr "确认"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
msgid "Copy"
msgstr "复制"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
msgid "Paste"
msgstr "粘贴"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
msgid "Clear"
msgstr "清除界面"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
msgid "Reset"
msgstr "重置终端"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
msgid "Split"
msgstr "分屏"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
msgid "Change Title…"
msgstr "更改标题……"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
msgid "Split Up"
msgstr "向上分屏"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
msgid "Split Down"
msgstr "向下分屏"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
msgid "Split Left"
msgstr "向左分屏"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
msgid "Split Right"
msgstr "向右分屏"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
msgid "Tab"
msgstr "标签页"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
#: src/apprt/gtk/Window.zig:239
msgid "New Tab"
msgstr "新建标签页"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35
msgid "Close Tab"
msgstr "关闭标签页"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73
msgid "Window"
msgstr "窗口"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18
msgid "New Window"
msgstr "新建窗口"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23
msgid "Close Window"
msgstr "关闭窗口"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "设置"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Open Configuration"
msgstr "打开设置文件"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Reload Configuration"
msgstr "重新加载设置"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Terminal Inspector"
msgstr "终端检视器"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102
#: src/apprt/gtk/Window.zig:923
msgid "About Ghostty"
msgstr "关于 Ghostty"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
msgid "Quit"
msgstr "退出"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "剪切板访问授权"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr "一个应用正在试图从剪切板读取内容。剪切板目前的内容如下:"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "拒绝"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr "允许"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr "一个应用正在试图向剪切板写入内容。剪切板目前的内容如下:"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr "警告:粘贴内容可能不安全"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr "将以下内容粘贴至终端内将可能执行有害命令。"
#: src/apprt/gtk/Window.zig:192
msgid "Main Menu"
msgstr "主菜单"
#: src/apprt/gtk/Window.zig:212
msgid "View Open Tabs"
msgstr "浏览标签页"
#: src/apprt/gtk/Window.zig:257
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。"
#: src/apprt/gtk/Window.zig:660
msgid "Reloaded the configuration"
msgstr "已重新加载设置"
#: src/apprt/gtk/Window.zig:904
msgid "Ghostty Developers"
msgstr "Ghostty 开发团队"
#: src/apprt/gtk/Surface.zig:1236
msgid "Copied to clipboard"
msgstr "已复制至剪切板"

View File

@ -78,6 +78,7 @@ parts:
- libxml2-utils
- git
- patchelf
- gettext
override-build: |
craftctl set version=$(git describe --abbrev=8)
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast

View File

@ -39,6 +39,7 @@ const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.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");
@ -98,6 +99,11 @@ 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,

View File

@ -35,6 +35,7 @@ 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);
@ -1152,7 +1153,7 @@ pub fn setClipboardString(
self.app.config.@"app-notifications".@"clipboard-copy")
{
if (self.container.window()) |window|
window.sendToast("Copied to clipboard");
window.sendToast(i18n._("Copied to clipboard"));
}
return;
}

View File

@ -33,6 +33,7 @@ const TabView = @import("TabView.zig");
const HeaderBar = @import("headerbar.zig");
const version = @import("version.zig");
const winproto = @import("winproto.zig");
const i18n = @import("i18n.zig");
const log = std.log.scoped(.gtk);
@ -192,7 +193,7 @@ pub fn init(self: *Window, app: *App) !void {
{
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
c.gtk_widget_set_tooltip_text(btn, i18n._("Main Menu"));
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
c.gtk_menu_button_set_popover(@ptrCast(btn), @ptrCast(@alignCast(self.titlebar_menu.asWidget())));
_ = c.g_signal_connect_data(
@ -212,7 +213,7 @@ pub fn init(self: *Window, app: *App) !void {
const btn = switch (self.config.gtk_tabs_location) {
.top, .bottom => 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, i18n._("View Open Tabs"));
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
_ = c.g_object_bind_property(
btn,
@ -239,7 +240,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, i18n._("New Tab"));
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(&gtkTabNewClick), self, null, c.G_CONNECT_DEFAULT);
self.headerbar.packStart(btn);
}
@ -257,7 +258,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 = i18n._("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
if (adwaita.versionAtLeast(1, 3, 0)) {
const banner = c.adw_banner_new(warning_text);
c.adw_banner_set_revealed(@ptrCast(banner), 1);
@ -674,10 +675,10 @@ pub fn focusCurrentTab(self: *Window) void {
}
pub fn onConfigReloaded(self: *Window) void {
self.sendToast("Reloaded the configuration");
self.sendToast(i18n._("Reloaded the configuration"));
}
pub fn sendToast(self: *Window, title: [:0]const u8) void {
pub fn sendToast(self: *Window, title: [*:0]const u8) void {
const toast = c.adw_toast_new(title);
c.adw_toast_set_timeout(toast, 3);
c.adw_toast_overlay_add_toast(@ptrCast(self.toast_overlay), toast);
@ -930,7 +931,7 @@ fn gtkActionAbout(
"application-name",
name,
"developer-name",
"Ghostty Developers",
i18n._("Ghostty Developers"),
"application-icon",
icon,
"version",
@ -949,7 +950,7 @@ fn gtkActionAbout(
"logo-icon-name",
icon,
"title",
"About Ghostty",
i18n._("About Ghostty"),
"version",
build_config.version_string.ptr,
"website",

37
src/apprt/gtk/i18n.zig Normal file
View File

@ -0,0 +1,37 @@
//! 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;

118
src/build/GhosttyI18n.zig Normal file
View File

@ -0,0 +1,118 @@
const GhosttyI18n = @This();
const std = @import("std");
const Config = @import("Config.zig");
const gresource = @import("../apprt/gtk/gresource.zig");
const domain = "com.mitchellh.ghostty";
const locales = [_][]const u8{
"zh_CN.UTF-8",
};
owner: *std.Build,
steps: []*std.Build.Step,
/// This step updates the translation files on disk that should be
/// committed to the repo.
update_step: *std.Build.Step,
pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
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"));
try steps.append(&b.addInstallFile(
msgfmt.captureStdOut(),
std.fmt.comptimePrint(
"share/locale/{s}/LC_MESSAGES/{s}.mo",
.{ locale, domain },
),
).step);
}
}
return .{
.owner = b,
.update_step = try createUpdateStep(b),
.steps = try steps.toOwnedSlice(),
};
}
pub fn install(self: *const GhosttyI18n) void {
for (self.steps) |step| self.owner.getInstallStep().dependOn(step);
}
fn createUpdateStep(b: *std.Build) !*std.Build.Step {
const xgettext = b.addSystemCommand(&.{
"xgettext",
"--language=C", // Silence the "unknown extension" errors
"--from-code=UTF-8",
"--add-comments=Translators",
"--keyword=_",
"--keyword=C_:1c,2",
"--package-name=" ++ domain,
"--msgid-bugs-address=m@mitchellh.com",
"--copyright-holder=Mitchell Hashimoto",
"-o",
"-",
});
// Not cacheable due to the gresource files
xgettext.has_side_effects = true;
inline for (gresource.blueprint_files) |blp| {
// We avoid using addFileArg here since the full, absolute file path
// would be added to the file as its location, which differs for
// everyone's checkout of the repository.
// This comes at a cost of losing per-file caching, of course.
xgettext.addArg(std.fmt.comptimePrint(
"src/apprt/gtk/ui/{[major]}.{[minor]}/{[name]s}.blp",
blp,
));
}
{
var gtk_files = try b.build_root.handle.openDir(
"src/apprt/gtk",
.{ .iterate = true },
);
defer gtk_files.close();
var walk = try gtk_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.addArg((b.pathJoin(&.{ "src/apprt/gtk", src.path })));
}
}
const wf = b.addWriteFiles();
wf.addCopyFileToSource(
xgettext.captureStdOut(),
"po/" ++ domain ++ ".pot",
);
inline for (locales) |locale| {
const msgmerge = b.addSystemCommand(&.{ "msgmerge", "-q" });
msgmerge.addFileArg(b.path("po/" ++ locale ++ ".po"));
msgmerge.addFileArg(xgettext.captureStdOut());
wf.addCopyFileToSource(msgmerge.captureStdOut(), "po/" ++ locale ++ ".po");
}
return &wf.step;
}

View File

@ -40,6 +40,7 @@ COPY ./dist/linux /src/dist/linux
COPY ./images /src/images
COPY ./include /src/include
COPY ./pkg /src/pkg
COPY ./po /src/po
COPY ./nix /src/nix
COPY ./vendor /src/vendor
COPY ./build.zig /src/build.zig

View File

@ -13,6 +13,7 @@ pub const GhosttyExe = @import("GhosttyExe.zig");
pub const GhosttyFrameData = @import("GhosttyFrameData.zig");
pub const GhosttyLib = @import("GhosttyLib.zig");
pub const GhosttyResources = @import("GhosttyResources.zig");
pub const GhosttyI18n = @import("GhosttyI18n.zig");
pub const GhosttyXCFramework = @import("GhosttyXCFramework.zig");
pub const GhosttyWebdata = @import("GhosttyWebdata.zig");
pub const HelpStrings = @import("HelpStrings.zig");

View File

@ -9,14 +9,22 @@ 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.
// Use the GHOSTTY_RESOURCES_DIR environment variable in release builds.
//
// In debug builds we try using terminfo detection first instead, since
// if debug Ghostty is launched by an older version of Ghostty, it
// would inherit the old, stale resources of older Ghostty instead of the
// freshly built ones under zig-out/share/ghostty.
//
// 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 (comptime builtin.mode != .Debug) {
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
}
}
// This is the sentinel value we look for in the path to know
@ -52,6 +60,17 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
}
}
// If terminfo detection failed in debug builds (somehow),
// fallback and use the provided resources dir.
if (comptime builtin.mode == .Debug) {
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;
}