# Languages

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.

```json
{
  "filesSelected": "{count, plural, =0 {No files selected} one {# file selected} other {# files selected}}"
}
```

```go
ui.Text(c, locales.FilesSelected(len(app.selected))) // "3 files selected"
```

Package [`i18n`](#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](#in-pages)).

## Catalogs

Turn the catalogs on in the [configuration](https://mygo.egoist.dev/docs/configuration.md#i18n), in
`mygo.json` (or `mygo.config.ts`):

```json
{
  "i18n": { "dir": "locales", "source": "en" }
}
```

`dir` (default `locales`) holds a catalog per language, named after its
[BCP 47](https://www.rfc-editor.org/info/bcp47) 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:

```json
{
  "@@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}!"
}
```

```json
{
  "@@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`](#mygos-own-strings)). 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](#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](https://cldr.unicode.org)
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:

```go
import "example.com/notes/locales"

title := locales.FilesSelected(3)                // in the language the app shows
german := locales.In("de").FilesSelected(3)      // in another
```

Each 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:

```go
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:

```go
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", "简体中文"):

```go
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](#when-the-language-changes)), and pages hear
it ([In pages](#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:

```go
ui.Text(c, "「直」").Lang("ja") // a Japanese quote in a Chinese app
```

Han 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:

```go
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](https://mygo.egoist.dev/docs/ui/calendar.md)
starts its weeks on the region's first day, the
[date input](https://mygo.egoist.dev/docs/ui/date-input.md) shows the region's numeric date, the
[time input](https://mygo.egoist.dev/docs/ui/time-input.md) 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.

```go
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-latn` asks for Latin ones). Fraction digits
  are rounded half away from zero, as `Intl` rounds them, or half to even
  (`HalfEven`). `NumberFormatter` keeps a formatter.
* **Dates and times** by style (`FullStyle` to `ShortStyle`, as
  `dateStyle` and `timeStyle`) 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's `DateTimePatternGenerator` finds
  it, so `{year: "numeric", month: "short", day: "numeric"}` in `Intl`
  and `yMMMd` here write the same. `j` is the hour in the locale's
  cycle; `HourCycle` (or `-u-hc-h23` in the locale) makes it 12 or 24.
  Dates are in the Gregorian calendar, times in their `time.Location`.
  `DateFormatter` keeps a formatter, and `Pattern` says the pattern.
* **Names**: `Months`, `Weekdays` and `DayPeriods` in a `Width`, standing
  alone (a calendar's title, "январь") or inside a date ("января");
  `Autonym` is a language's name in itself, for a menu of languages
  ("Deutsch", "简体中文", "British English").
* **Week and clock**: `FirstWeekday` and `HourCycle` of a locale's region
  (or its language's likely one), `RegionFor` both.

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:

```go
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:

```json
{
  "mygo.undo": "Rückgängig",
  "mygo.menuQuitApp": "{app} beenden"
}
```

The keys, their messages and their arguments are MyGo's catalog,
[internal/l10n/en.json](https://github.com/egoist/mygo/blob/main/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](https://mygo.egoist.dev/docs/distribution.md#languages)).

## 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 `@@locale` differs;
* 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 `String` counted by `plural`);
* 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-XA` accents 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-XB` shows 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](https://mygo.egoist.dev/docs/bindings.md#the-generated-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.

```ts
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`](https://pkg.go.dev/github.com/egoist/mygo/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](#formatting-numbers-and-dates). Its data comes from
CLDR `i18n.CLDRVersion`.
