diff --git a/Makefile b/Makefile index 9280982..38d84b4 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,45 @@ MAIN := ./cmd/nirilayout SRCS := $(wildcard *.go cmd/nirilayout/*.go) go.mod go.sum style.css -nirilayout: $(SRCS) +# Localization (gettext). Source files to scan for translatable T("...") calls. +I18N_SRCS := $(wildcard *.go cmd/nirilayout/*.go) +POT := locales/nirilayout.pot +POFILES := $(wildcard locales/*/LC_MESSAGES/*.po) +MOFILES := $(POFILES:.po=.mo) + +nirilayout: $(SRCS) $(MOFILES) go build -o $@ $(MAIN) -nirilayout-profile: $(SRCS) +nirilayout-profile: $(SRCS) $(MOFILES) go build -o $@ -tags profile $(MAIN) -install: $(SRCS) +install: $(SRCS) $(MOFILES) go install $(MAIN) +# Regenerate the .pot template from the Go sources. +pot: $(POT) +$(POT): $(I18N_SRCS) + xgettext --language=C --from-code=UTF-8 --keyword=T:1 --keyword=Tf:1 --keyword=N:1 \ + --package-name=nirilayout \ + --msgid-bugs-address="https://github.com/calico32/nirilayout/issues" \ + --copyright-holder="nirilayout contributors" \ + -o $@ $(I18N_SRCS) + +# Merge the latest .pot into every existing .po, preserving translations. +update-po: $(POT) + @for po in $(POFILES); do \ + echo "msgmerge $$po"; \ + msgmerge --update --backup=none $$po $(POT); \ + done + +# Compile .po files to .mo. Build targets depend on these. +%.mo: %.po + msgfmt --check -o $@ $< + +# Regenerate translations: refresh .po files from sources, then recompile .mo. +i18n: update-po $(MOFILES) + clean: rm -f nirilayout nirilayout-profile -.PHONY: clean +.PHONY: clean pot update-po i18n diff --git a/README.md b/README.md index 3013c67..41ef005 100644 --- a/README.md +++ b/README.md @@ -188,10 +188,99 @@ prefixes of each other. Layouts are presented in lexicographical order by name. If you want to change the order, you can rename the files in the config directory. +## Command-line options + +| Option | Description | +| ------------- | -------------------------------------------------------------------------------------------- | +| `-c
`| Interface language code, e.g. `it`. Overrides the system locale. See [Localization](#localization). |
+| `-lowercase` | Render all of nirilayout's own interface text in lowercase. See [Casing](#casing). |
+| `-leftalign` | Left-align the text in the search box. See [Search box alignment](#search-box-alignment). |
+
+Run `nirilayout -h` for the full list.
+
+## Appearance
+
+### Casing
+
+By default nirilayout uses standard sentence casing for its own interface text
+(for example `Esc to quit`). If you prefer everything in lowercase, pass the
+`-lowercase` flag:
+
+```sh
+nirilayout -lowercase
+```
+
+This only affects nirilayout's own interface strings — the names of your layouts
+and outputs are always shown exactly as you wrote them.
+
+### Search box alignment
+
+The text you type into the search box is centered by default. Pass the
+`-leftalign` flag to left-align it instead:
+
+```sh
+nirilayout -leftalign
+```
+
+## Localization
+
+nirilayout's interface can be translated. The language is chosen automatically,
+with the following precedence:
+
+1. The `-lang` flag, when set, wins over everything (e.g. `nirilayout -lang it`).
+2. Otherwise the operating system locale is used, read from the `LC_ALL`,
+ `LC_MESSAGES`, and `LANG` environment variables (in that order). A locale
+ like `it_IT.UTF-8` is matched to its base language `it`.
+3. Otherwise English is used.
+
+If the selected language has no translation, nirilayout falls back to English.
+
+Currently available languages:
+
+- English (`en`, source language)
+- Italian (`it`)
+
+### Adding a translation
+
+Translations use [gettext](https://www.gnu.org/software/gettext/). The compiled
+`.mo` catalogs are committed to the repository and embedded into the binary, so
+**building or running nirilayout never requires any gettext tooling**. You only
+need the gettext tools (`xgettext`, `msgmerge`, `msgfmt`) to *edit* translations.
+
+To add a new language (using French, `fr`, as an example):
+
+1. Refresh the message template and existing translations from the source:
+
+ ```sh
+ make update-po
+ ```
+
+2. Create the catalog directory and initialize the `.po` file from the template:
+
+ ```sh
+ mkdir -p locales/fr/LC_MESSAGES
+ msginit -i locales/nirilayout.pot -o locales/fr/LC_MESSAGES/nirilayout.po -l fr
+ ```
+
+3. Translate the `msgstr` entries in `locales/fr/LC_MESSAGES/nirilayout.po`.
+
+4. Compile the catalogs (this produces the committed `.mo` files):
+
+ ```sh
+ make i18n
+ ```
+
+5. Add the new language to the list above and open a pull request.
+
+To update an existing translation, edit its `.po` file, run `make i18n`, and
+commit both the `.po` and the regenerated `.mo`.
+
# Contributing
Contributions are welcome! If you find a bug or have a feature request, please
-open an issue or a pull request.
+open an issue or a pull request. Translations are especially welcome — see
+[Adding a translation](#adding-a-translation).
# License
diff --git a/cmd/nirilayout/main.go b/cmd/nirilayout/main.go
index 8fb4de1..79a69f6 100644
--- a/cmd/nirilayout/main.go
+++ b/cmd/nirilayout/main.go
@@ -15,15 +15,28 @@ import (
)
func main() {
+ // Initialize i18n from the system locale first so that flag.Usage (which
+ // flag.Parse may invoke on -h or a parse error) is already localized. It
+ // is re-initialized after flag.Parse to honor -lang and -lowercase.
+ nirilayout.InitI18n()
+
flag.Usage = func() {
- fmt.Printf("nirilayout is a layout switcher for niri.\n\nCommand-line options:\n")
+ fmt.Print(nirilayout.T("nirilayout is a layout switcher for niri.\n\nCommand-line options:\n"))
+ // Translate each flag's usage string in place, then let the flag
+ // package handle the formatting (types, defaults, alignment).
+ flag.VisitAll(func(f *flag.Flag) {
+ f.Usage = nirilayout.T(f.Usage)
+ })
flag.PrintDefaults()
- fmt.Printf("\nTo use nirilayout, create layouts in files called ~/.config/niri/layout_.kdl and run nirilayout.\nSee the README for more details:\n")
- fmt.Printf(" https://github.com/calico32/nirilayout/blob/main/README.md\n")
+ fmt.Print(nirilayout.T("\nTo use nirilayout, create layouts in files called ~/.config/niri/layout_.kdl and run nirilayout.\nSee the README for more details:\n"))
+ fmt.Print(" https://github.com/calico32/nirilayout/blob/main/README.md\n")
}
flag.Parse()
+ // Apply -lang and -lowercase now that flags are parsed.
+ nirilayout.InitI18n()
+
var layouts []nirilayout.Layout
var current string
diff --git a/config.go b/config.go
index f43857f..08a30cb 100644
--- a/config.go
+++ b/config.go
@@ -26,7 +26,13 @@ const (
defaultLineSpacing = 4
)
-var niriConfigDir = flag.String("c", "~/.config/niri", "niri config directory")
+var niriConfigDir = flag.String("c", "~/.config/niri", N("niri config directory"))
+
+var (
+ langFlag = flag.String("lang", "", N("interface language code (e.g. \"it\"); overrides the system locale"))
+ lowercaseFlag = flag.Bool("lowercase", false, N("render all interface text in lowercase"))
+ leftAlignFlag = flag.Bool("leftalign", false, N("left-align the text in the search box (default is centered)"))
+)
func GetNiriConfigDir() (configDir string, err error) {
if strings.HasPrefix(*niriConfigDir, "~") {
diff --git a/draw.go b/draw.go
index 4342f50..12c9216 100644
--- a/draw.go
+++ b/draw.go
@@ -123,10 +123,11 @@ func drawLayout(layout Layout) *gtk.DrawingArea {
cr.MoveTo(0, 0)
if len(outputs) == 0 {
- extents := cr.TextExtents("no preview available")
+ msg := T("No preview available")
+ extents := cr.TextExtents(msg)
cr.MoveTo(float64(width)/2-extents.Width/2-extents.XBearing, float64(height)/2-extents.Height/2-extents.YBearing)
cr.SetSourceRGBA(rgba(gray400))
- cr.ShowText("no preview available")
+ cr.ShowText(msg)
return
}
diff --git a/go.mod b/go.mod
index ec36b14..fa69781 100644
--- a/go.mod
+++ b/go.mod
@@ -17,4 +17,5 @@ require (
github.com/diamondburned/gotk4-layer-shell/pkg v0.0.0-20240109211357-6efa9f6dc438
github.com/diamondburned/gotk4/pkg v0.3.1
github.com/google/go-cmp v0.7.0
+ github.com/leonelquinteros/gotext v1.7.2
)
diff --git a/go.sum b/go.sum
index 320a040..0abdbe4 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,8 @@ github.com/diamondburned/gotk4/pkg v0.3.1 h1:uhkXSUPUsCyz3yujdvl7DSN8jiLS2BgNTQE
github.com/diamondburned/gotk4/pkg v0.3.1/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
+github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
diff --git a/i18n.go b/i18n.go
new file mode 100644
index 0000000..4267446
--- /dev/null
+++ b/i18n.go
@@ -0,0 +1,190 @@
+package nirilayout
+
+import (
+ "embed"
+ "fmt"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/leonelquinteros/gotext"
+)
+
+// Compiled message catalogs, one per language, laid out in the standard
+// gettext directory structure: locales//LC_MESSAGES/nirilayout.mo.
+// English is the source language (the msgids themselves), so it has no
+// catalog and is never loaded here.
+//
+//go:embed locales
+var localesFS embed.FS
+
+const i18nDomain = "nirilayout"
+
+var (
+ // catalog maps source (English) msgids to their translation in the active
+ // language. It is nil when the active language is English or no catalog
+ // could be loaded, in which case the source msgids are used verbatim.
+ catalog map[string]string
+ // lowercaseEnabled forces all translated interface text to lowercase.
+ lowercaseEnabled bool
+)
+
+// InitI18n selects the interface language and lowercase preference. It must be
+// called after flag.Parse. Language selection follows this precedence:
+//
+// 1. the -lang flag, when set, overrides everything;
+// 2. otherwise the operating system locale, read from LC_ALL, then
+// LC_MESSAGES, then LANG;
+// 3. otherwise English.
+//
+// If the selected language has no compiled catalog, the source (English)
+// msgids are used as a fallback.
+func InitI18n() {
+ lang := *langFlag
+ lowercase := *lowercaseFlag
+
+ // The flags may not be parsed yet — InitI18n is also called before
+ // flag.Parse so that flag.Usage (triggered by -h during parsing) is
+ // already localized. In that case, fall back to scanning the raw
+ // arguments so that "-lang"/"-lowercase" still take effect.
+ if lang == "" || !lowercase {
+ argLang, argLower := scanI18nArgs(os.Args[1:])
+ if lang == "" {
+ lang = argLang
+ }
+ lowercase = lowercase || argLower
+ }
+
+ lowercaseEnabled = lowercase
+
+ if lang == "" {
+ lang = detectSystemLang()
+ }
+ lang = normalizeLang(lang)
+
+ if lang == "en" {
+ catalog = nil
+ return
+ }
+
+ data, err := localesFS.ReadFile(path.Join("locales", lang, "LC_MESSAGES", i18nDomain+".mo"))
+ if err != nil {
+ catalog = nil
+ return
+ }
+
+ catalog = parseCatalog(data)
+}
+
+// parseCatalog parses a compiled gettext .mo file and returns a map from source
+// msgid to its translation. Empty or identity translations are omitted so that
+// lookups fall back to the source msgid.
+func parseCatalog(data []byte) map[string]string {
+ mo := gotext.NewMo()
+ mo.Parse(data)
+
+ m := make(map[string]string)
+ for id, tr := range mo.GetDomain().GetTranslations() {
+ if s := tr.Get(); s != "" && s != id {
+ m[id] = s
+ }
+ }
+ return m
+}
+
+// lookup returns the translation for msgid, or msgid itself when there is no
+// active catalog or no entry for it.
+func lookup(msgid string) string {
+ if catalog != nil {
+ if s, ok := catalog[msgid]; ok {
+ return s
+ }
+ }
+ return msgid
+}
+
+// scanI18nArgs extracts the -lang value and the -lowercase presence directly
+// from the raw command-line arguments. It is used to localize flag.Usage,
+// which the flag package may invoke (on -h) before the flags are parsed.
+func scanI18nArgs(args []string) (lang string, lowercase bool) {
+ for i := 0; i < len(args); i++ {
+ a := args[i]
+ switch {
+ case a == "--":
+ return lang, lowercase
+ case a == "-lang" || a == "--lang":
+ if i+1 < len(args) {
+ lang = args[i+1]
+ i++
+ }
+ case strings.HasPrefix(a, "-lang="):
+ lang = strings.TrimPrefix(a, "-lang=")
+ case strings.HasPrefix(a, "--lang="):
+ lang = strings.TrimPrefix(a, "--lang=")
+ case a == "-lowercase" || a == "--lowercase",
+ a == "-lowercase=true" || a == "--lowercase=true":
+ lowercase = true
+ }
+ }
+ return lang, lowercase
+}
+
+// detectSystemLang reads the OS locale from the standard POSIX environment
+// variables, in order of precedence. Returns "" if none are set.
+func detectSystemLang() string {
+ for _, env := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} {
+ if v := os.Getenv(env); v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
+// normalizeLang reduces a raw locale string such as "it_IT.UTF-8@euro" to its
+// base language code ("it"). The "C" and "POSIX" locales, as well as an empty
+// value, map to English.
+func normalizeLang(raw string) string {
+ s := raw
+ if i := strings.IndexAny(s, ".@"); i >= 0 {
+ s = s[:i]
+ }
+ if i := strings.IndexAny(s, "_-"); i >= 0 {
+ s = s[:i]
+ }
+ s = strings.ToLower(strings.TrimSpace(s))
+ if s == "" || s == "c" || s == "posix" {
+ return "en"
+ }
+ return s
+}
+
+// N marks a string for translation extraction without translating it. It is a
+// no-op at runtime, returning its argument unchanged. Use it where the string
+// must be a plain literal at the point of definition (e.g. flag usage strings
+// registered before InitI18n runs) but should still be picked up by xgettext.
+// The recorded msgid can then be translated later with T.
+func N(s string) string { return s }
+
+// T translates msgid into the active language. When -lowercase is active the
+// result is lowercased. Untranslated msgids fall back to English. The msgid may
+// be a non-constant string (e.g. a flag usage string looked up at runtime); for
+// translating a format string with arguments, use Tf.
+func T(msgid string) string {
+ s := lookup(msgid)
+ if lowercaseEnabled {
+ s = strings.ToLower(s)
+ }
+ return s
+}
+
+// Tf translates format into the active language and then formats it with the
+// given arguments using fmt.Sprintf semantics. When -lowercase is active the
+// final (formatted) string is lowercased. Untranslated formats fall back to
+// English.
+func Tf(format string, args ...any) string {
+ s := fmt.Sprintf(lookup(format), args...)
+ if lowercaseEnabled {
+ s = strings.ToLower(s)
+ }
+ return s
+}
diff --git a/i18n_test.go b/i18n_test.go
new file mode 100644
index 0000000..853eed8
--- /dev/null
+++ b/i18n_test.go
@@ -0,0 +1,75 @@
+package nirilayout
+
+import "testing"
+
+func TestNormalizeLang(t *testing.T) {
+ cases := map[string]string{
+ "it": "it",
+ "it_IT": "it",
+ "it_IT.UTF-8": "it",
+ "it_IT.UTF-8@eu": "it",
+ "en_US.UTF-8": "en",
+ "pt-BR": "pt",
+ "C": "en",
+ "POSIX": "en",
+ "": "en",
+ " IT ": "it",
+ }
+ for in, want := range cases {
+ if got := normalizeLang(in); got != want {
+ t.Errorf("normalizeLang(%q) = %q, want %q", in, got, want)
+ }
+ }
+}
+
+func TestTFallbackToMsgid(t *testing.T) {
+ defer restoreI18n(catalog, lowercaseEnabled)
+ catalog = nil
+ lowercaseEnabled = false
+
+ if got := T("Esc to quit"); got != "Esc to quit" {
+ t.Errorf("T fallback = %q, want %q", got, "Esc to quit")
+ }
+ if got := Tf("Error loading layouts: %v", "boom"); got != "Error loading layouts: boom" {
+ t.Errorf("Tf fallback with args = %q, want %q", got, "Error loading layouts: boom")
+ }
+}
+
+func TestTLowercase(t *testing.T) {
+ defer restoreI18n(catalog, lowercaseEnabled)
+ catalog = nil
+ lowercaseEnabled = true
+
+ if got := T("Name or shortcut…"); got != "name or shortcut…" {
+ t.Errorf("T lowercase = %q, want %q", got, "name or shortcut…")
+ }
+ if got := Tf("Error loading layouts: %v", "BOOM"); got != "error loading layouts: boom" {
+ t.Errorf("Tf lowercase with args = %q, want %q", got, "error loading layouts: boom")
+ }
+}
+
+func TestTItalianCatalog(t *testing.T) {
+ defer restoreI18n(catalog, lowercaseEnabled)
+ lowercaseEnabled = false
+
+ data, err := localesFS.ReadFile("locales/it/LC_MESSAGES/nirilayout.mo")
+ if err != nil {
+ t.Fatalf("embedded Italian catalog missing: %v", err)
+ }
+ catalog = parseCatalog(data)
+
+ if got := T("Esc to quit"); got != "Esc per uscire" {
+ t.Errorf("T(it) = %q, want %q", got, "Esc per uscire")
+ }
+ // An unknown msgid must fall back to the source string. Use a variable so
+ // xgettext does not extract this test-only string into the catalog.
+ unknown := "Totally untranslated string"
+ if got := T(unknown); got != unknown {
+ t.Errorf("T(it) unknown = %q, want fallback to source", got)
+ }
+}
+
+func restoreI18n(c map[string]string, lower bool) {
+ catalog = c
+ lowercaseEnabled = lower
+}
diff --git a/locales/it/LC_MESSAGES/nirilayout.mo b/locales/it/LC_MESSAGES/nirilayout.mo
new file mode 100644
index 0000000..4d38dd0
Binary files /dev/null and b/locales/it/LC_MESSAGES/nirilayout.mo differ
diff --git a/locales/it/LC_MESSAGES/nirilayout.po b/locales/it/LC_MESSAGES/nirilayout.po
new file mode 100644
index 0000000..44cfca5
--- /dev/null
+++ b/locales/it/LC_MESSAGES/nirilayout.po
@@ -0,0 +1,82 @@
+# Italian translation for nirilayout.
+# Copyright (C) 2026 nirilayout contributors
+# This file is distributed under the same license as the nirilayout package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: nirilayout\n"
+"Report-Msgid-Bugs-To: https://github.com/calico32/nirilayout/issues\n"
+"POT-Creation-Date: 2026-06-14 07:36+0200\n"
+"PO-Revision-Date: 2026-06-13 21:00+0200\n"
+"Last-Translator: nirilayout contributors\n"
+"Language-Team: Italian\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: config.go:29
+msgid "niri config directory"
+msgstr "directory di configurazione di niri"
+
+#: config.go:32
+msgid "interface language code (e.g. \"it\"); overrides the system locale"
+msgstr ""
+"codice lingua dell'interfaccia (es. \"it\"); ha la precedenza sul locale di "
+"sistema"
+
+#: config.go:33
+msgid "render all interface text in lowercase"
+msgstr "mostra tutto il testo dell'interfaccia in minuscolo"
+
+#: config.go:34
+msgid "left-align the text in the search box (default is centered)"
+msgstr ""
+"allinea a sinistra il testo nella casella di ricerca (predefinito: centrato)"
+
+#: draw.go:126
+msgid "No preview available"
+msgstr "Nessuna anteprima disponibile"
+
+#: i18n_test.go:34 i18n_test.go:67 nirilayout.go:159
+msgid "Esc to quit"
+msgstr "Esc per uscire"
+
+#: i18n_test.go:37 i18n_test.go:50 nirilayout.go:80
+msgid "Error loading layouts: %v"
+msgstr "Errore nel caricamento dei layout: %v"
+
+#: i18n_test.go:47 nirilayout.go:139
+msgid "Name or shortcut…"
+msgstr "Nome o scorciatoia…"
+
+#: nirilayout.go:84
+msgid ""
+"No layouts found. Please create layout_.kdl files in ~/.config/niri to "
+"use nirilayout."
+msgstr ""
+"Nessun layout trovato. Crea dei file layout_.kdl in ~/.config/niri per "
+"usare nirilayout."
+
+#: cmd/nirilayout/main.go:24
+msgid ""
+"nirilayout is a layout switcher for niri.\n"
+"\n"
+"Command-line options:\n"
+msgstr ""
+"nirilayout è un selettore di layout per niri.\n"
+"\n"
+"Opzioni da riga di comando:\n"
+
+#: cmd/nirilayout/main.go:31
+msgid ""
+"\n"
+"To use nirilayout, create layouts in files called ~/.config/niri/"
+"layout_.kdl and run nirilayout.\n"
+"See the README for more details:\n"
+msgstr ""
+"\n"
+"Per usare nirilayout, crea dei layout in file chiamati ~/.config/niri/"
+"layout_.kdl ed esegui nirilayout.\n"
+"Consulta il README per maggiori dettagli:\n"
diff --git a/locales/nirilayout.pot b/locales/nirilayout.pot
new file mode 100644
index 0000000..ea4f978
--- /dev/null
+++ b/locales/nirilayout.pot
@@ -0,0 +1,71 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR nirilayout contributors
+# This file is distributed under the same license as the nirilayout package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: nirilayout\n"
+"Report-Msgid-Bugs-To: https://github.com/calico32/nirilayout/issues\n"
+"POT-Creation-Date: 2026-06-14 07:39+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: config.go:29
+msgid "niri config directory"
+msgstr ""
+
+#: config.go:32
+msgid "interface language code (e.g. \"it\"); overrides the system locale"
+msgstr ""
+
+#: config.go:33
+msgid "render all interface text in lowercase"
+msgstr ""
+
+#: config.go:34
+msgid "left-align the text in the search box (default is centered)"
+msgstr ""
+
+#: draw.go:126
+msgid "No preview available"
+msgstr ""
+
+#: i18n_test.go:34 i18n_test.go:67 nirilayout.go:159
+msgid "Esc to quit"
+msgstr ""
+
+#: i18n_test.go:37 i18n_test.go:50 nirilayout.go:80
+msgid "Error loading layouts: %v"
+msgstr ""
+
+#: i18n_test.go:47 nirilayout.go:139
+msgid "Name or shortcut…"
+msgstr ""
+
+#: nirilayout.go:84
+msgid ""
+"No layouts found. Please create layout_.kdl files in ~/.config/niri to "
+"use nirilayout."
+msgstr ""
+
+#: cmd/nirilayout/main.go:24
+msgid ""
+"nirilayout is a layout switcher for niri.\n"
+"\n"
+"Command-line options:\n"
+msgstr ""
+
+#: cmd/nirilayout/main.go:31
+msgid ""
+"\n"
+"To use nirilayout, create layouts in files called ~/.config/niri/"
+"layout_.kdl and run nirilayout.\n"
+"See the README for more details:\n"
+msgstr ""
diff --git a/nirilayout.go b/nirilayout.go
index c7a2797..edb1280 100644
--- a/nirilayout.go
+++ b/nirilayout.go
@@ -14,7 +14,7 @@ import (
"github.com/diamondburned/gotk4/pkg/gtk/v4"
)
-const version = `nirilayout v0.3.0`
+const version = `nirilayout v0.4.0`
//go:embed style.css
var appStylesheet string
@@ -77,11 +77,11 @@ func Run(app *gtk.Application, layouts []Layout, startIndex int, err error) {
var selector *gtk.FlowBox
if err != nil {
- label := gtk.NewLabel(fmt.Sprintf("Error loading layouts: %v", err))
+ label := gtk.NewLabel(Tf("Error loading layouts: %v", err))
label.SetHAlign(gtk.AlignCenter)
root.Append(label)
} else if len(layouts) == 0 {
- label := gtk.NewLabel("No layouts found. Please create layout_.kdl files ~/.config/niri to use nirilayout.")
+ label := gtk.NewLabel(T("No layouts found. Please create layout_.kdl files in ~/.config/niri to use nirilayout."))
label.SetHAlign(gtk.AlignCenter)
root.Append(label)
} else {
@@ -131,8 +131,12 @@ func Run(app *gtk.Application, layouts []Layout, startIndex int, err error) {
input := gtk.NewEntry()
input.SetSizeRequest(400, 0)
- input.SetAlignment(0.5)
- input.SetPlaceholderText("name or shortcut...")
+ if *leftAlignFlag {
+ input.SetAlignment(0)
+ } else {
+ input.SetAlignment(0.5)
+ }
+ input.SetPlaceholderText(T("Name or shortcut…"))
input.ConnectChanged(func() {
text := input.Text()
for _, layout := range layouts {
@@ -152,7 +156,7 @@ func Run(app *gtk.Application, layouts []Layout, startIndex int, err error) {
label.SetSensitive(false)
label.SetMarginEnd(16)
inputBox.SetStartWidget(label)
- label = gtk.NewLabel("esc to quit")
+ label = gtk.NewLabel(T("Esc to quit"))
label.SetSensitive(false)
label.SetMarginStart(16)
inputBox.SetEndWidget(label)