aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/libghostty-vt/src/build/GhosttyI18n.zig
blob: eb6ee5ee2df831cd8f9f6dcddefd92433a60e8c7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
const GhosttyI18n = @This();

const std = @import("std");
const builtin = @import("builtin");
const Config = @import("Config.zig");
const gresource = @import("../apprt/gtk/build/gresource.zig");
const locales = @import("../os/i18n_locales.zig").locales;

const domain = "com.mitchellh.ghostty";

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 {
    _ = cfg;

    var steps: std.ArrayList(*std.Build.Step) = .empty;
    defer steps.deinit(b.allocator);

    inline for (locales) |locale| {
        // There is no encoding suffix in the LC_MESSAGES path on FreeBSD,
        // so we need to remove it from `locale` to have a correct destination string.
        // (/usr/local/share/locale/en_AU/LC_MESSAGES)
        const target_locale = comptime if (builtin.target.os.tag == .freebsd)
            std.mem.trimEnd(u8, locale, ".UTF-8")
        else
            locale;

        const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" });
        msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po"));

        try steps.append(b.allocator, &b.addInstallFile(
            msgfmt.captureStdOut(.{}),
            std.fmt.comptimePrint(
                "share/locale/{s}/LC_MESSAGES/{s}.mo",
                .{ target_locale, domain },
            ),
        ).step);
    }

    return .{
        .owner = b,
        .update_step = try createUpdateStep(b),
        .steps = try steps.toOwnedSlice(b.allocator),
    };
}

pub fn install(self: *const GhosttyI18n) void {
    self.addStepDependencies(self.owner.getInstallStep());
}

pub fn addStepDependencies(
    self: *const GhosttyI18n,
    other_step: *std.Build.Step,
) void {
    for (self.steps) |step| other_step.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",
        "--keyword=_",
        "--keyword=N_",
        "--keyword=C_:1c,2",
    });

    // Collect to intermediate .pot file
    xgettext.addArg("-o");
    const gtk_pot = xgettext.addOutputFileArg("gtk.pot");

    // Not cacheable due to the gresource files
    xgettext.has_side_effects = true;

    inline for (gresource.blueprints) |blp| {
        const path = std.fmt.comptimePrint(
            "src/apprt/gtk/ui/{[major]}.{[minor]}/{[name]s}.blp",
            blp,
        );
        // The arguments to xgettext must be the relative path in the build root
        // or the resulting files will contain the absolute path. This will cause
        // a lot of churn because not everyone has the Ghostty code checked out in
        // exactly the same location.
        xgettext.addArg(path);
        // Mark the file as an input so that the Zig build system caching will work.
        xgettext.addFileInput(b.path(path));
    }

    {
        // Iterate over all of the files underneath `src/apprt/gtk`. We store
        // them in an array so that they can be sorted into a determininistic
        // order. That will minimize code churn as directory walking is not
        // guaranteed to happen in any particular order.

        var gtk_files: std.ArrayListUnmanaged([]const u8) = .empty;
        defer {
            for (gtk_files.items) |item| b.allocator.free(item);
            gtk_files.deinit(b.allocator);
        }

        var gtk_dir = try b.build_root.handle.openDir(
            b.graph.io,
            "src/apprt/gtk",
            .{ .iterate = true },
        );
        defer gtk_dir.close(b.graph.io);

        var walk = try gtk_dir.walk(b.allocator);
        defer walk.deinit();
        while (try walk.next(b.graph.io)) |src| {
            switch (src.kind) {
                .file => if (!std.mem.endsWith(
                    u8,
                    src.basename,
                    ".zig",
                )) continue,

                else => continue,
            }

            try gtk_files.append(b.allocator, try b.allocator.dupe(u8, src.path));
        }

        std.mem.sort(
            []const u8,
            gtk_files.items,
            {},
            struct {
                fn lt(_: void, lhs: []const u8, rhs: []const u8) bool {
                    return std.mem.order(u8, lhs, rhs) == .lt;
                }
            }.lt,
        );

        for (gtk_files.items) |item| {
            const path = b.pathJoin(&.{ "src/apprt/gtk", item });
            // The arguments to xgettext must be the relative path in the build root
            // or the resulting files will contain the absolute path. This will
            // cause a lot of churn because not everyone has the Ghostty code
            // checked out in exactly the same location.
            xgettext.addArg(path);
            // Mark the file as an input so that the Zig build system caching will work.
            xgettext.addFileInput(b.path(path));
        }
    }

    // For localization of command palette
    const command_palette_path = "src/input/command.zig";
    xgettext.addArg(command_palette_path);
    xgettext.addFileInput(b.path(command_palette_path));

    // Add support for localizing our `nautilus` integration
    const xgettext_py = b.addSystemCommand(&.{
        "xgettext",
        "--language=Python",
        "--from-code=UTF-8",
    });

    // Collect to intermediate .pot file
    xgettext_py.addArg("-o");
    const py_pot = xgettext_py.addOutputFileArg("py.pot");

    const nautilus_script_path = "dist/linux/ghostty_nautilus.py";
    xgettext_py.addArg(nautilus_script_path);
    xgettext_py.addFileInput(b.path(nautilus_script_path));

    // Merge pot files
    const xgettext_merge = b.addSystemCommand(&.{
        "xgettext",
        "--add-comments=Translators",
        "--package-name=" ++ domain,
        "--msgid-bugs-address=m@mitchellh.com",
        "--copyright-holder=\"Mitchell Hashimoto, Ghostty contributors\"",
        "-o",
        "-",
    });
    // py_pot needs to be first on merge order because of `xgettext` behavior around
    // charset when merging the two `.pot` files
    xgettext_merge.addFileArg(py_pot);
    xgettext_merge.addFileArg(gtk_pot);
    const usf = b.addUpdateSourceFiles();
    usf.addCopyFileToSource(
        xgettext_merge.captureStdOut(.{}),
        "po/" ++ domain ++ ".pot",
    );

    inline for (locales) |locale| {
        const msgmerge = b.addSystemCommand(&.{ "msgmerge", "--quiet", "--no-fuzzy-matching" });
        msgmerge.addFileArg(b.path("po/" ++ locale ++ ".po"));
        msgmerge.addFileArg(xgettext_merge.captureStdOut(.{}));
        usf.addCopyFileToSource(msgmerge.captureStdOut(.{}), "po/" ++ locale ++ ".po");
    }

    return &usf.step;
}