Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gtk: add localization support, take 3 #6004

Merged
merged 6 commits into from
Mar 5, 2025

Conversation

pluiedev
Copy link
Member

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

@pluiedev pluiedev requested a review from a team as a code owner February 26, 2025 13:39
@pluiedev pluiedev force-pushed the push-nxwlqouoqluy branch 5 times, most recently from 69882c0 to 7c90d20 Compare February 26, 2025 13:53
Copy link
Contributor

@mitchellh mitchellh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm supportive of this but just noting I want to review this. I'm less experienced with this so want to better understand some things that are perhaps trivial or obvious:

  1. Is it typical for xgettext to run during binary build time or should we start considering this as a step we run prior to packaging up our source tarballs for packagers? i.e. is this a dependency on packaging or is this a dependency on source building?

Making it all one step for day to day dev makes total sense, just thinking about the final release time packaging here. I know it's typical for source tarballs to contain some preprocessing complete but unsure if this falls in that category.

  1. I know it's typical for po files to go at the root, but we may want to make a note this is only for GTK currently. I need to do research on what the recommended approach for macOS is (we don't need to solve that in this PR but I want to make sure we're structured right for it)

@pluiedev
Copy link
Member Author

pluiedev commented Feb 26, 2025

  1. xgettext is usually run by developers as a standalone tool outside of the build system, therefore it's neither a build-time nor a runtime dependency. Its sole purpose is to generate/update the PO template, and once the template is there only msgfmt is required for the build process (which does .po -> .mo)

  2. On macOS there's two ways to go about it: either do it the "legacy" way of using .strings files that can be added to Xcode, or the "new" (Xcode 15+) way of using what's known as "string catalogs" (.xcstrings), which are essentially JSON files with a fancy editing interface inside Xcode.

What I've gathered is that while string catalogs are definitely the recommended translation mechanism from Apple moving forward and are a lot more convenient to use for the macOS apprt (Texts in SwiftUI transparently use them), they're kind of a pain for thirdparty integrations. The gettext stack has native support for legacy .strings files and so we can unify translations for both apprts, while .xcstrings is completely foreign to it (or rather, any program that's not Xcode). I think it would be up to you and other macOS devs to decide which format we should use on the macOS side.

@mitchellh
Copy link
Contributor

xgettext is usually run by developers as a standalone tool outside of the build system, therefore it's neither a build-time nor a runtime dependency.

Right, but we're only committing the po files to the repo, right?

So if I'm understanding properly, we should plan to run this prior to generating the source tarball so packagers do not need to do this.

@pluiedev
Copy link
Member Author

pluiedev commented Feb 26, 2025

Yeah, my plan is to always update the PO template whenever strings are added, changed or removed in PRs, enforced via CI. That way the template is always accurate to the source files (which is already desirable since they include source locations) and translators always have an up-to-date picture of what strings need to be translated. Packagers don't need to do anything.

@jcollie
Copy link
Member

jcollie commented Feb 26, 2025

I don't do any i18n/l10n, but I've never been comfortable having the .pot file checked into source. We at least should have a CI job that fails if the strings change and the template is out of date.

@pluiedev
Copy link
Member Author

That's what I've been planning to do :p

@tristan957
Copy link
Member

xgettext is usually run by developers as a standalone tool outside of the build system

It is better when it is part of the build system, coming from a Meson world.

@pluiedev
Copy link
Member Author

Well it is part of the build system (I've added a new top-level step to run it via zig run update-translations), just not part of the normal build process, because altering source files in the Zig build system without explicit user intervention is frowned upon.

@tristan957
Copy link
Member

Cool, cool, cool.

@pluiedev pluiedev requested a review from mitchellh February 26, 2025 19:55
@pluiedev pluiedev force-pushed the push-nxwlqouoqluy branch 2 times, most recently from 6b1aa3d to b782779 Compare February 27, 2025 13:16
@pluiedev
Copy link
Member Author

@kenvandine Looks like the snap package needs to be updated with the dependency on gettext. I am completely clueless about snaps, however, so I'd really appreciate some assistance on this front 😅

Copy link
Contributor

@mitchellh mitchellh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. I want to run this locally later today just to make sure I really understand the full build path here, and that may result in some other code comments but they're likely superficial.

//! 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Ghostty even builds for musl right now anyways but this is a good thought regardless to not give us any more debt to work on there. (I think the debt is mainly we have other deps that don't play nice with musl)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah honestly like... I don't even know how people who use musl even manage to coax GTK & Friends to work on it, but if they do manage to do it then I presume they have enough know-how to get localization working

Copy link
Contributor

@kenvandine kenvandine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the snap you need to add gettext to build-packages. Something like the following will work:

diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 49d452ef..9822619d 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -75,6 +75,7 @@ parts:
       - libadwaita-1-dev
       - libxml2-utils
       - git
+      - gettext
       - patchelf
     override-build: |
       craftctl set version=$(git describe --abbrev=8)

@pluiedev
Copy link
Member Author

Got it. However, I wonder if if'd work OOTB - I see that LC_ALL is forcibly set to C.UTF-8 and there's some forum threads that suggest more work would be needed for it to work. (e.g. https://forum.snapcraft.io/t/the-gettext-launch-launcher-fix-gettext-based-internationalization-in-the-snap-runtime/9111, https://forum.snapcraft.io/t/lack-of-compiled-locales-breaks-gettext-based-localisation/3758) Is this still accurate?

@pluiedev pluiedev requested a review from a team as a code owner February 27, 2025 20:18
Copy link
Contributor

@mitchellh mitchellh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One very easy change requested. The rest looks good.

I made one commit to update the build files. The only real change there is I think that top-level steps should only be created in build.zig and our helper structs should create unattached *std.Build.Step that can be depended on instead.

I also did some minor style modifications to fit an 80 character width, which despite having a large screen I still find helpful for splits.

Copy link
Contributor

@kenvandine kenvandine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snap changes look good, thanks!

@pluiedev pluiedev force-pushed the push-nxwlqouoqluy branch 5 times, most recently from 18fa342 to 02e6cc1 Compare February 28, 2025 21:47
Copy link
Contributor

@00-kat 00-kat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some small™ things about the documentation (the Zig looks fine to me).

@pluiedev pluiedev force-pushed the push-nxwlqouoqluy branch from 02e6cc1 to 782bae1 Compare March 3, 2025 08:24
@pluiedev pluiedev force-pushed the push-nxwlqouoqluy branch 4 times, most recently from 59482e4 to 3c93e04 Compare March 3, 2025 09:18
pluiedev and others added 6 commits March 3, 2025 10:19
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.
When one develops Ghostty while using Ghostty it could lead to an
interesting conundrum: the freshly built Ghostty would use the parent
Ghostty's resources, which would be stale and not reflect any new
changes to resources. This is especially bad for translators, since
their translations would not be reflected in the newly built Ghostty
if they happen to run it under older Ghostty, which is not only
counterintuitive and also painful in terms of workflow.

Now, on debug builds we always try to use the terminfo detection method
first in order to locate the zig-out/share/ghostty folder, and only fall
back to GHOSTTY_RESOURCES_DIR if the executable is for some reason no
longer in zig-out. You can test this behavior by manually moving the
Ghostty executable out of zig-out, and then launching it with and without
Ghostty.
@pluiedev pluiedev force-pushed the push-nxwlqouoqluy branch from 3c93e04 to 6373399 Compare March 3, 2025 09:20
@pluiedev pluiedev merged commit 2f65f01 into ghostty-org:main Mar 5, 2025
30 checks passed
@github-actions github-actions bot added this to the 1.2.0 milestone Mar 5, 2025
mitchellh added a commit that referenced this pull request Mar 7, 2025
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants