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 |
{price, number, ::currency/EUR} |
an amount of euros in the language's way (€1,842.00; 1.842,00 €) |
{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#), group-off and currency/EUR, an amount
with the currency's symbol and fraction digits. 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.
Before 0.5, App.Locale() returned the user's first preferred language
whatever the app is written in. An app that translates its texts itself,
without catalogs or App.SetLanguages, now gets "en" from it: declare
its languages with App.SetLanguages, or read the user's from
App.PreferredLanguages().
App.SetLocale("de") shows another language at once, and keeps it for the
next launches; App.SetLocale("") follows the user's languages again.
App.ChosenLocale() returns what SetLocale chose and keeps, normalized
("de", "zh-Hans"), or "" while the app follows the user's languages,
which App.Locale() can't tell from the language they resolve to. A
language menu, with an item that follows the system and each language
named in itself ("Deutsch", "简体中文"):
chosen := mygo.App.ChosenLocale()
items := []*mygo.MenuItem{{Label: "System", Type: mygo.MenuItemRadio,
Checked: chosen == "",
Click: func(*mygo.MenuItem, *mygo.Window) { mygo.App.SetLocale("") }}}
for _, tag := range mygo.App.Languages() {
items = append(items, &mygo.MenuItem{Label: i18n.Autonym(tag), Type: mygo.MenuItemRadio,
Checked: chosen == tag,
Click: func(*mygo.MenuItem, *mygo.Window) { mygo.App.SetLocale(tag) }})
}ChosenLocale reports the tag as it was chosen, normalized, even one the
app is not written in: an app that passes SetLocale other tags than
those of App.Languages(), as "de-AT", gets those back, to compare with
its own or match with i18n.Match. While MYGO_LOCALE chooses the
language for a run, it reports that one. From another goroutine than the
main thread it waits for it, so it reports the SetLocale calls made
before it.
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. On Windows, whose font fallback goes by the locale of the user's regional formats, they take the first of the user's preferred languages that writes them, when that locale does not: on English Windows with Chinese among the preferred languages, a Chinese song title in an English app is drawn in one Chinese font, not in a mix of a Japanese font and a Chinese one, of two weights. Browsers draw the text of native UI in the page's language, which MyGo makes the app's.
Korean text (ko, ko-KR, …) breaks its lines between words, at spaces
and after punctuation, as Korean is written and as Korean sites have
browsers break it with CSS's word-break: keep-all, rather than between
any two syllables as Unicode's line breaking allows; a word longer than a
line still breaks within it, unless the text keeps words whole anyway
(TextShape.NoBreakWords). An app showing Korean
(App.SetLanguages("ko")) gets it in Text, TextInput, TextArea and
the text it shapes, on every platform; Lang("ko") gives it to Korean
text in an app of another language.
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.FormatNumber("en", 1234, i18n.NumberOptions{SignDisplay: i18n.SignAlways}) // +1,234
i18n.FormatNumber("en", 0, i18n.NumberOptions{SignDisplay: i18n.SignExceptZero}) // 0
i18n.FormatNumber("en", 1234, i18n.NumberOptions{Notation: i18n.CompactShort}) // 1.2K
i18n.FormatNumber("fr", 1234, i18n.NumberOptions{Notation: i18n.CompactLong}) // 1,2 millier
usd := i18n.CurrencyOf("USD")
i18n.FormatNumber("en", 1842, i18n.NumberOptions{Currency: usd}) // $1,842.00
i18n.FormatNumber("en", -1842, i18n.NumberOptions{Currency: usd, Accounting: true}) // ($1,842.00)
i18n.FormatNumber("de", 1842, i18n.NumberOptions{Currency: i18n.CurrencyOf("EUR")}) // 1.842,00 €
i18n.FormatNumber("ja", 1842, i18n.NumberOptions{Currency: i18n.CurrencyOf("JPY")}) // ¥1,842
i18n.FormatNumber("en", 1842, i18n.NumberOptions{Currency: usd, CurrencyDisplay: i18n.CurrencyName}) // 1,842.00 US dollars
i18n.FormatNumber("en", 4.2e6, i18n.NumberOptions{Currency: usd, Notation: i18n.CompactShort}) // $4.2M
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
i18n.FormatDateRange("en", start, end, i18n.DateOptions{Skeleton: "yMMMd"}) // Jan 5 – 9, 2026- 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. - The sign:
SignDisplayisIntl'ssignDisplay:SignAuto(the default) writes the minus of a negative number only,SignNegativetoo but no minus before a negative zero,SignAlwaysa plus before a number that is not negative, andSignExceptZeroone, but not before a number that rounds to zero;SignNeverwrites neither. The plus goes where the locale puts its minus, before the percent sign ("+%25" in Turkish) and the currency's ("+$1,842.00"), or after it where that is where the locale's negative pattern puts it ("€ +1.234,00" in Dutch, "CHF+1'234.00" in Swiss German), and a plus of a compact number too ("+1.2K"). - Compact numbers, as a chart's axis labels them:
Notation: i18n.CompactShortwrites 1234 as "1.2K" in English, "1,2 k" in French, 1.2万 in Japanese, andi18n.CompactLongas "1.2 thousand" and "1,2 millier", asIntl'snotation: "compact"withcompactDisplaywrites them: two significant digits, or the whole number of two digits or more (12K, 123K), unless the options give fraction digits; the plural form of what is written ("2 millions"); and the decimal pattern where the locale has no compact one (German writes thousands whole, 12.345). - Amounts of money, as a price, a total or a balance shows them:
Currency: i18n.CurrencyOf("EUR")(a code of ISO 4217) writes the number asIntl'sstyle: "currency"does, in the locale's pattern of amounts, which places the currency's sign and spaces it from the digits ("$1,842.00", "1.842,00 €", "EUR 1,842.00"), with the currency's fraction digits of CLDR'scurrencyData(two for USD, none for JPY, three for BHD), whichMinFractionDigits,MaxFractionDigitsandIntegeroverride asIntl's options do.CurrencyDisplayiscurrencyDisplay: the symbol (CurrencySymbol, the default: "US$" in British English),CurrencyNarrowSymbol("$"),CurrencyCode("USD") orCurrencyName("1,842.00 US dollars", in the amount's plural form).AccountingiscurrencySign: "accounting": "($1,842.00)" where the locale writes negative amounts so. WithNotation, amounts are compact asIntlwrites them ("$4.2M"; "4.2 million euros" with names). - 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. - Ranges of dates and times, as a date-range picker's trigger labels
them:
FormatDateRange(orDateFormatter.FormatRange) writes once what the two share and twice what they do not, asIntl'sformatRangedoes with CLDR's interval formats: "Jan 5 – 9, 2026", "Jan 5 – Feb 9, 2026", "10:00 – 11:30 AM", or one date when they differ in no field the options write. The end is written in the start's location. - 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.
Relative times
FormatRelativeTime writes a value and unit as Intl.RelativeTimeFormat
does, for timestamps, reminders and activity feeds:
i18n.FormatRelativeTime("en", -3, i18n.RelativeMinute, i18n.RelativeTimeOptions{}) // 3 minutes ago
i18n.FormatRelativeTime("zh", 1, i18n.RelativeDay, i18n.RelativeTimeOptions{Auto: true}) // 明天
i18n.FormatRelativeTime("fr", -2, i18n.RelativeDay, i18n.RelativeTimeOptions{Auto: true}) // avant-hier
i18n.FormatRelativeTime("en", -3, i18n.RelativeMinute, i18n.RelativeTimeOptions{Style: i18n.RelativeNarrow}) // 3m agoUnits run from RelativeSecond through RelativeYear, including weeks,
months and quarters. The zero options use full names and always include a
number; RelativeShort and RelativeNarrow shorten the unit names.
Auto uses the locale's words such as “yesterday”, “tomorrow” and “now”
where it has them, and numbers elsewhere. As ICU does, it chooses words
by rounding the offset to hundredths: 1.001 days can say “tomorrow”.
Numbers round to at most three fraction digits, ties to even, and pick
the plural form of the rounded number; negative zero means the past.
NewRelativeTimeFormatter keeps the locale and options for repeated,
concurrent use. Unknown units or styles and non-finite values panic.
The caller computes the value and chooses its unit; a month or year is
a calendar unit, not a fixed duration. Formatting reads no clock and
starts no timer. Web frontends can use Intl.RelativeTimeFormat directly.
The Go formatter uses CLDR's own words, including locales ICU 78 lacks
(ht, el-polyton) and be-tarask's short and narrow words for the
current minute and hour, which ICU 78 inherits from be.
Formatting data
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. The patterns of compact
numbers are linked only into an app that names CompactShort or
CompactLong, and those of ranges only into one that formats a range;
messages read neither (ICU's compact-short skeleton is written as a
plain number). The patterns and symbols of currencies (6 KB of data,
about 40 KB with their code) are linked into an app that calls
CurrencyOf or has messages of its own (a catalog, ParseMessage),
whose skeletons may name a currency; their names, about 300 KB, only into
one that names CurrencyName (messages write the symbol for ICU's
unit-width-full-name, as for the other stems they do not read); and the
patterns of compact amounts, 10 KB, only into one that also names a
compact notation. Relative times and their patterns are linked only into
an app that calls FormatRelativeTime or NewRelativeTimeFormatter.
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, though ranges keep CLDR's thin and narrow no-break
spaces, as V8's formatRange does (Safari's writes spaces); and the few
locales whose data ICU 78 (CLDR 48.0) lacks
or has older (Haitian Creole, polytonic Greek) follow CLDR 48.2, as do
ICU's Burmese names of currencies (written after the amount, where CLDR
puts them before) and Serbian Cyrillic of Montenegro's (in Latin there).
Amounts follow ICU's quirks: the formats CLDR gives a currency in a
locale (euros in English of Germany, "€1,842.00") apply to any currency
in a locale of the region that uses it ("US$1,842.00" in en-DE), as
ICU's symbols keep them. The fraction digits of currencies are CLDR's,
as V8 (Chrome, Edge and WebView2) has them; JavaScriptCore (Safari,
WKWebView and WebKitGTK) takes ISO 4217's, which differ for a few
(COP, HUF, IDR, IQD, IRR, LAK and PKR have none in CLDR, two or three in
ISO 4217). Compact amounts with MinFractionDigits keep up to three
fraction digits, as ECMA-402 and JavaScriptCore have them ("$1.234K");
V8 keeps as many as the minimum ("$1.23K").
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, Japanese (ja),
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.
The Windows installer's finish page uses mygo.desktopShortcut for the
check box that creates a desktop shortcut. The build reads it from the
app's catalogs, including the source language's, or uses MyGo's translation
of that language, falling back to English (see distribution).
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.