Building the app
Languages
The app's messages in the languages it ships, with catalogs of ICU messages, typed Go and TypeScript functions, plural forms, MyGo's own strings, and pseudo locales to test layouts.
A MyGo app says its messages in the language of the people who use it.
Its messages live in catalogs, a JSON file per language, from which
mygo dev and mygo build write a Go package with a typed function per
message,
which says it in the language the app shows, with the plural forms of
that language. The whole app shows one language: its own messages,
MyGo's controls and menus, and its pages if it has any.
{
"filesSelected": "{count, plural, =0 {No files selected} one {# file selected} other {# files selected}}"
}ui.Text(c, locales.FilesSelected(len(app.selected))) // "3 files selected"Package i18n underneath formats numbers and dates
with the data of the Unicode CLDR, and an app with a frontend gets the
same messages as a TypeScript module (In pages).
Catalogs
Turn the catalogs on in the configuration, in
mygo.json (or mygo.config.ts):
{
"i18n": { "dir": "locales", "source": "en" }
}dir (default locales) holds a catalog per language, named after its
BCP 47 tag: en.json,
de.json, pt-BR.json, zh-Hans.json, zh-Hant.json. source
(default en) is the language the app is written in: its catalog has
every message, and the others fall back to it. The languages the app
ships are the files of dir, which holds nothing else but Go files; an
app without catalogs ships English.
A catalog is an object of messages, in the ARB format, whose keys name
them. The source language's also describes them, under the key
with an @, for the translators and the generated code:
{
"@@locale": "en",
"filesSelected": "{count, plural, =0 {No files selected} one {# file selected} other {# files selected}}",
"@filesSelected": {
"description": "The status bar, while files are selected.",
"placeholders": {
"count": { "type": "int", "example": "3" }
}
},
"greeting": "Hello, {name}!"
}{
"@@locale": "de",
"filesSelected": "{count, plural, =0 {Keine Dateien ausgewählt} one {# Datei ausgewählt} other {# Dateien ausgewählt}}",
"greeting": "Hallo, {name}!"
}A key is a letter, then letters, digits and _; the functions of a key
are named after it. A placeholder's type is String, int, double,
num, DateTime or Object, which makes the type of the functions'
parameter (string, int, float64, float64, time.Time and any in
Go; string, number, Date and unknown in TypeScript). Without one,
an argument counted by plural, selectordinal or number is an int, a
date or time argument a DateTime, and any other a String. The
parameters come in the order of placeholders, then of the message.
@@locale is optional, and must name the file's language.
Keys are flat: a dot in a key is for MyGo's own strings
(mygo.undo). Name related messages with a common
start, as sidebarTitle, sidebarEmpty and sidebarNewNote, which sort
together in the catalogs and in your editor's completion
(locales.Sidebar…).
Messages
Messages are written in ICU MessageFormat, the format of ARB files, of Android and of Format.JS:
| Message | Says |
|---|---|
Hello, {name}! |
the value of name in its place |
{count, plural, one {# file} other {# files}} |
the case of the plural category of count in the message's language; # is the number |
{count, plural, =0 {No files} one {# file} other {# files}} |
=0 is for exactly 0, before the categories |
{n, plural, offset:1 =0 {Nobody} =1 {{host}} one {{host} and # other} other {{host} and # others}} |
with offset:1, the categories and # count n - 1 |
{place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} |
the case of the ordinal category: 1st, 22nd, 103rd, 11th |
{gender, select, female {She} male {He} other {They}} replied. |
the case named after the value, else other |
{price, number}, {ratio, number, percent}, {n, number, integer} |
a number in the language's way (1,234.5; 1.234,5; 12,34,567) |
{price, number, ::.00}, {n, number, ::group-off} |
a number with exactly two fraction digits, or without separators of thousands |
{day, date, short}, {day, time, short} |
a date, or a time, in a style: short, medium, long or full |
{day, date, ::yMMMd} |
a date of the fields of a skeleton of CLDR, in the language's order (Jan 5, 2026; 5. Jan. 2026) |
Cases nest, and every plural, selectordinal and select has an
other case. A number in {n} is written as {n, number} writes it: up
to three fraction digits, grouped, in the language's digits; a number
given as a string of a decimal, "1.50", keeps its fraction digits. A
plural case is the one of the number as # writes it, as ICU picks it:
1.0004 is written 1, whose case in English is one. A time in {t} is
written as {t, date, short} and {t, time, short} together. Numbers and
dates follow CLDR's data in Go and Intl in pages, which agree (see
Formatting numbers and dates). Of ICU's
number skeletons, Go and pages read percent, integer, the precision of
fraction digits (.00, .0#) and group-off; pages also read
currency/EUR, which Go writes as a plain number. The
plural categories are those of the Unicode CLDR
for the message's language: one and other in English, only other
in Chinese and Japanese, one, few, many and other in Russian, six in
Arabic. Write the ones the language has; other is required.
An apostrophe quotes the next brace, or # in a plural case: '{' says
{, '{name}' says {name}, and two apostrophes say one. Any other
apostrophe is just one, so don't needs no quoting.
In Go
mygo dev, mygo build and mygo generate write catalog_gen.go into
the catalogs' directory, a Go package of its own (named after the
directory, or as its other Go files say), which embeds the catalogs:
import "example.com/notes/locales"
title := locales.FilesSelected(3) // in the language the app shows
german := locales.In("de").FilesSelected(3) // in anotherEach key has a function with the typed parameters of its placeholders,
which says the message in the language the app shows, and a method of
locales.In(tag) for another language. A message without arguments is
kept once said, so saying it again costs a map lookup and allocates
nothing: a view of native UI says its messages every frame.
The package registers its catalog with package i18n as it initializes,
which makes the catalogs' languages the app's: import it, with
import _ "example.com/notes/locales" if the Go code says no message
itself. mygo dev and mygo build warn when the app does not.
mygo dev writes the functions as it starts and again whenever a catalog
changes, and mygo build before it compiles the app; mygo generate
writes them alone. They are generated code: commit them with the
catalogs, so that go build and go test work without the CLI.
When the language changes
A view of native UI is built again for every frame, so what it says follows the language as it changes:
func (app *notes) view(c *ui.Context) {
ui.Column(c).Gap(8).Children(func() {
ui.Text(c, locales.FilesSelected(len(app.selected)))
if ui.Button(c, locales.NewNote()).Clicked() {
app.add()
}
})
}Menu items labelled by their role ("Undo", "Quit Notes") take the new language's labels themselves. What the app set once, as a window's title or the labels of its own menu items, it sets again when it hears the change:
win := mygo.NewWindow(mygo.WindowOptions{Title: locales.Title(), Content: ui.View(app.view)})
mygo.App.OnLocaleChanged(func(string) {
win.SetTitle(locales.Title())
mygo.App.SetMenu(app.menu()) // the menu, made again with locales' labels
})The language the app shows
The app shows one language, which mygo.App.Locale() returns: of the
languages it is written in, the one the user reads best, by the languages
they prefer in the order the system lists them
(App.PreferredLanguages()), as zh-Hant for a reader of zh-TW; else
the app's source language. An app written in English alone shows English
to a reader of Chinese, MyGo's controls and menus too, as AppKit does. The
languages the app is written in are its catalogs', which App.SetLanguages
replaces for an app without them.
App.SetLocale("de") shows another language at once, and keeps it for the
next launches; App.SetLocale("") follows the user's languages again. A
language menu, each language named in itself ("Deutsch", "简体中文"):
for _, tag := range mygo.App.Languages() {
items = append(items, &mygo.MenuItem{Label: i18n.Autonym(tag), Type: mygo.MenuItemRadio,
Checked: tag == mygo.App.Locale(),
Click: func(*mygo.MenuItem, *mygo.Window) { mygo.App.SetLocale(tag) }})
}macOS keeps the language as the app's language in System Settings
(AppleLanguages of the app's defaults), which AppKit and WebKit say their
own texts in from the next launch; browsers keep it in localStorage;
Windows and Linux in the file locale of App.Path(mygo.PathUserData).
App.OnLocaleChanged hears the language change, as SetLocale changes it
or the user's languages do (macOS, Windows and browsers report theirs;
Linux reads the environment, LANGUAGE, LC_ALL, LC_MESSAGES and
LANG, once).
When the language changes, windows of native UI draw anew, menu items
labelled by their role say the new language's labels (see
When the language changes), and pages hear
it (In pages). The app is laid out in the language's
direction, right to left in Arabic and Hebrew, unless MYGO_DIRECTION
(rtl or ltr) says otherwise: native UI, pages (mygo.dir) and, on
Linux, GTK's own widgets, as menus, agree.
Native UI
ui.Context.Locale() and ui.Context.Region() return the app's language
and formats; a frame follows their changes. Text is drawn in the app's
language, which picks the glyphs the system's text stack draws for it, as
the Han characters Chinese and Japanese draw apart. Element.Lang sets the
language of an element's text and of the elements inside it, as HTML's
lang does, Span.Lang of a span of a RichText, Font.Lang of text a
Painter shapes:
ui.Text(c, "「直」").Lang("ja") // a Japanese quote in a Chinese appHan characters in a language that does not write them, as a Chinese name in English text, keep the glyphs the system picks for the user, as browsers do. Browsers draw the text of native UI in the page's language, which MyGo makes the app's.
examples/gallery has a Languages page: a menu of languages, numbers and
dates in each, the controls in Chinese, and the pseudo locales.
Regional formats
How dates, times and numbers are written is a setting of its own:
App.Region() returns the locale of the user's formats, which may be
another language's than the one the app shows, the first day of the week
and whether times have 12 or 24 hours, with the changes the user made in
the system's settings. App.OnRegionChanged hears them change. What the
system does not say, as a libc without the data or a browser that does
not tell, comes from CLDR's week and time data for the region
(i18n.FirstWeekday, i18n.HourCycle).
Region.FormatLocale(language) puts the two together as Apple's
Locale.current does: English read with British formats is
en-GB-u-fw-mon-hc-h23, with German ones en-DE-u-fw-mon-hc-h23; format
with it to write dates in the app's language the way the user set them:
formats := mygo.App.Region().FormatLocale(mygo.App.Locale())
label := i18n.FormatDate(formats, due, i18n.DateOptions{Date: i18n.MediumStyle})MyGo's date and time controls do so: the calendar starts its weeks on the region's first day, the date input shows the region's numeric date, the time input its hour cycle, with AM and PM in the window's language.
Formatting numbers and dates
Package i18n writes numbers, dates and times as ICU and the browsers'
Intl write them, with the data of the Unicode CLDR for the locales of
its modern coverage (430 locales of 104 languages: en, en-GB, de-CH,
pt-BR, zh-Hant-HK, ar-EG...). A locale it has no data of formats as
the nearest it has (zh-TW as zh-Hant, en-AU as en-001), else as
English.
i18n.FormatNumber("de", 1234567.891, i18n.NumberOptions{}) // 1.234.567,891
i18n.FormatNumber("hi", 1234567, i18n.NumberOptions{}) // 12,34,567
i18n.FormatNumber("fa", 0.25, i18n.NumberOptions{Percent: true}) // ۲۵٪
i18n.FormatNumber("en", 1.5, i18n.NumberOptions{MinFractionDigits: 2}) // 1.50
i18n.FormatDate("en", t, i18n.DateOptions{Date: i18n.FullStyle}) // Monday, January 5, 2026
i18n.FormatDate("ja", t, i18n.DateOptions{Date: i18n.LongStyle, Time: i18n.ShortStyle}) // 2026年1月5日 13:07
i18n.FormatDate("ru", t, i18n.DateOptions{Skeleton: "yMMMM"}) // январь 2026 г.
i18n.FormatDate("en", t, i18n.DateOptions{Skeleton: "jm", HourCycle: 24}) // 13:07- Numbers: the language's separators and grouping (the Indian
12,34,567, and none in four digits where it groups from five, as
Spanish and Polish do), its minus and percent signs, and the digits of
its numbering system (Arabic-Indic in Persian and Egyptian Arabic,
Bengali in Bengali;
-u-nu-latnasks for Latin ones). Fraction digits are rounded half away from zero, asIntlrounds them, or half to even (HalfEven).NumberFormatterkeeps a formatter. - Dates and times by style (
FullStyletoShortStyle, asdateStyleandtimeStyle) or by skeleton, the fields to show in any order (yMMMd,MMMd,Ed,jm): the locale's pattern of CLDR that suits it best is found as ICU'sDateTimePatternGeneratorfinds it, so{year: "numeric", month: "short", day: "numeric"}inIntlandyMMMdhere write the same.jis the hour in the locale's cycle;HourCycle(or-u-hc-h23in the locale) makes it 12 or 24. Dates are in the Gregorian calendar, times in theirtime.Location.DateFormatterkeeps a formatter, andPatternsays the pattern. - Names:
Months,WeekdaysandDayPeriodsin aWidth, standing alone (a calendar's title, "январь") or inside a date ("января");Autonymis a language's name in itself, for a menu of languages ("Deutsch", "简体中文", "British English"). - Week and clock:
FirstWeekdayandHourCycleof a locale's region (or its language's likely one),RegionForboth.
Formatting allocates its result only; formatters and the locales' data
are made once and kept. The data is compressed and inflated on first use
in under a millisecond. Every app links the formatting of numbers, as
MyGo's own strings format them, with their data, about 3 KB; the
formatting of dates and their data, with the names of languages, are
linked into an app only when it formats them: with a catalog of its own
(NewCatalog, which mygo generate writes), ParseMessage, the
formatters or the functions that read the data. It is one package, not one per language: the
user's regional formats may be any locale's, whatever languages the app
ships.
What differs from ICU (i18n/format_icu_test.go lists each case the
tests skip): time zones other than UTC are written as offsets from GMT
(GMT+8, GMT+05:30) where ICU names some ("China Standard Time"); the
calendar is always the Gregorian one, where ICU's default for Thai or
Persian is another; times write spaces where CLDR has narrow no-break
ones, as V8 does; and the few locales whose data ICU 78 (CLDR 48.0) lacks
or has older (Haitian Creole, polytonic Greek) follow CLDR 48.2.
MyGo's own strings
MyGo's controls, its code editor, diff viewer, rich text editor and
Markdown view, the terminal plugin, its document dialogs, the buttons of
pages' alert, confirm and prompt, the titles of file dialogs, and its
menus say their own words:
"Undo", "Find", "Next month", "Do you want to save the changes made to the
document “Notes”?", "Quit Notes", and the names assistive technology reads
("hours", "Saturation and brightness"). They say the language the app
shows: in native UI, the window's (ui.Context.Locale). An app that
ships only English shows them in English to a Chinese reader, as AppKit
does. Saying one again in the same language costs a map lookup and
allocates nothing, so controls say theirs every frame.
English is always linked. The translations to other languages, Simplified
Chinese (zh-Hans) and Traditional Chinese (zh-Hant) so far, are
packages of their own, so that an app carries only those of the languages
it ships. The code mygo generate writes for an app's catalogs imports
them for the app's languages: an app with a zh-TW.json gets MyGo's
zh-Hant. An app without catalogs, which sets its languages with
App.SetLanguages, imports them itself:
import _ "github.com/egoist/mygo/locales/zh-Hans"An app that ships a language MyGo is translated to without importing the
translation is told so as it starts, and shows MyGo's strings in English
there. An app changes MyGo's strings, or gives them its other languages,
with keys mygo.<key> in its catalogs:
{
"mygo.undo": "Rückgängig",
"mygo.menuQuitApp": "{app} beenden"
}The keys, their messages and their arguments are MyGo's catalog, internal/l10n/en.json. A string resolves as the app's own do: in the app's language, then the languages it falls back to, then the app's source language, then English. The inspector of native UI, a tool of developers, stays in English.
Adding a language to MyGo
MyGo's own strings are internal/l10n/en.json, and each translation is
locales/<tag>/<tag>.json. To translate MyGo to a language, add
locales/<tag>/<tag>.json with every key of en.json (the descriptions
say where each shows), run go generate ./internal/l10n, which writes the
translation's package and adds it to the languages mygo generate links
for apps, and go test ./internal/l10n, which checks it as mygo generate
checks an app's catalogs: every message in every language, with the
source's placeholders and the language's plural cases.
mygo.appName and mygo.appDescription are the app's name and
description in a language, which packaging writes where systems list the
app (see building and distributing).
Which message
A message is the one of the catalog's language that suits the language
asked for best: zh-Hant for zh-TW, pt-PT for pt-AO, en-GB for
en-AU. When that language lacks it, it falls back along CLDR's parent
locales (en-AU, en-001, en; zh-Hant-HK, zh-Hant), then to the
source language: a missing translation says the source's message rather
than nothing. Its plural cases follow the rules of the language the
message is written in; numbers and dates, the conventions of the language
asked for.
Checking the catalogs
mygo generate and mygo build check the catalogs, and so does
mygo i18n check. Errors stop them:
- a file not named after a canonical tag, or whose
@@localediffers; - a message that does not parse;
- a key a translation has and the source does not, or a key
mygo.<key>MyGo has no message for; - an argument a translation has and the source's message does not, or used
in a way its type cannot be (a
Stringcounted byplural); - two keys whose functions would have the same name.
Warnings do not: messages a translation lacks, which say the source
language's; arguments it leaves out; plural cases its language does not
have (few in English) or lacks (few in Russian, which says other in
its place); placeholders a message does not use.
Trying a language
mygo dev -lang de runs the app in German without changing the language
it keeps. Two pseudo locales test layouts before any translation exists,
as Android's do:
en-XAaccents the letters of the source language's messages, makes them a third longer and brackets them,[Šéļéçţ ƒîļéš one two]: text that stays plain is not in a catalog, cut text is too short a space, and a sentence of several brackets is made of pieces a translation cannot reorder.ar-XBshows each word right to left, and lays the app out right to left: what does not mirror shows.
In pages
An app with a frontend says the same messages in its pages. Next to the
TypeScript client, the CLI writes a
module named after the catalogs' directory, such as
src/locales.ts, with a typed function per key; an app without a
frontend gets none.
import { languages, messages, messagesIn } from "./locales";
heading.textContent = messages.filesSelected(3); // in the language the page shows
messagesIn("de").greeting("Ada"); // "Hallo, Ada!"The module holds the messages of every language, and mygo-runtime/i18n
formats them with the browser's Intl, so the pages and Go say the same
words in the same plural forms. The messages are bundled with the
frontend rather than fetched: the first paint is in the right language,
without waiting for a request, also in a page of the dev server opened in
a browser. messages says each message in the language of the moment:
the app's, which MyGo gives the page before its scripts run, else the
browser's.
mygo.locale and mygo.dir are there before the page's scripts run.
When the language changes, pages hear the mygo:locale event
(onLocaleChange of mygo-runtime): draw the page again. Pages loaded
later start in the new language. Web views take the language too, for
their own texts: WebView2 as its environment is made, which keeps it (a
change shows from the next launch), WebKitGTK at once, WKWebView from the
next launch.
formatMessage(locale, message, args) from mygo-runtime/i18n formats a
message of your own.
Package i18n
The generated code is built on package
i18n, which apps can
use directly: NewCatalog reads catalogs from any fs.FS, ParseMessage
and Message.Format format a message, PluralCategory and
OrdinalCategory give the CLDR plural category of a number, written as an
integer, a float or a decimal string whose fraction digits count ("1.50"
has two), Parent the locale a locale falls back to, and the formatting
of numbers and dates. Its data comes from
CLDR i18n.CLDRVersion.