The Internationalization System
Extracting Translations
For translators to be able to translate text, and for us to be able to
work with those translations, we need translation files. These files are
located in the /po
directory. These files are automatically generated
by running the make po
command. In order to extract translations from
the source code, the make po
command will search for calls to the
Get()
family of functions, such as Get()
and GetN()
.
func example(n int) {
/* Not extracted for translation */
fmt.Println("Hello, Sailor!")
/* Extracted for translation */
fmt.Println(i18n.GetN("1 book", "{N} books", n,
map[string]any{"N": n}))
}
Sometimes you want to provide additional context about a translation to the translators. You can do this by placing a comment above the translated string with the prefix ‘TRANSLATORS:’.
func example() string {
/* TRANSLATORS: ‘Home’ button on the navigation bar */
return i18n.Get("Home")
}
Especially when working in HTML templates, you may have a string that you
want to go through the formatting system but not be marked for
translation. You can do this by calling the underlying Sprintf()
function instead.
<p>{{ .Printer.Sprintf "{N:m}" (map "N" 2) }}</p>
The Formatting System
For string formatting we use a custom implementation of Sprintf()
, and
all Get*()
functions format the translated strings using our own
Sprintf()
.
Do note that all Sprintf()
output is automatically HTML escaped.
Unlike the standard Sprintf()
, we use named placeholders enclosed by
curly braces instead of percent codes. As placeholders are named, they
need to be passed via a map of type map[string]any
.
func example() {
status := "late"
/* Go’s Sprintf */
_ = fmt.Sprintf("The bus is %s", status)
/* Our Sprintf */
_ = i18n.Sprintf("The bus is {LateOrEarly}",
map[string]any{"LateOrEarly": status})
}
The result is a lot more visually-noisy than what Go does — mostly due to the map syntax — but it offers infinitely more information to translators, which is important.
Translation functions can be provided multiple argument maps, and maps
can be easily created in HTML templates using the map
function.
{{ $nlargs := (map "DutchStart" `<span lang="nl"><em>`
"DutchEnd" `em,span`) }}
<p>{{ .Get "{Name} said ‘{DutchStart:r}{Quote}{DutchEnd:E}’!"
(map "Name" "Thomas" "Quote" "...") $nlargs }}</p>
In a placeholder you can also use a colon and additional character code to customize how formatting is performed. The default behaviour is as follows:
- Strings are printed verbatim
- Numbers (
int
) and floats (float64
) are formatted with the locale-specific grouping- and decimal separators - Dates (
time.Time
) are formatted according to the current locale - Other types are coerced to a string by
fmt.Sprintf()
and then printed verbatim
The following character codes are available with the following behaviour:
Code | Mnemonic | Description |
---|---|---|
e |
[e]mail | inserts a link to an email address |
E |
[E]nd | inserts closing-tags for the comma-separated HTML tags |
l |
[l]ink (internal) | links to an internal page |
L |
[L]ink (external) | links to an external page |
m |
[m]onetary | formats the given int or float64 as an amount of Euros. |
r |
[r]aw | inserts the given string verbatim without HTML escaping |
<!-- <a href="mailto:help@euro-cash.eu">help@euro-cash.eu</a> -->
<p>{{ .Get "{Email:e}" (map "Email" "help@euro-cash.eu") }}</p>
<!-- <span lang="sv"><em>Växjösjön</em></span> -->
<p>{{ .Get "{SwedishStart:r}Växjösjön{SwedishEnd:E}"
(map "SwedishStart" `<span lang="sv"><em>`
"SwedishEnd" "em,span") }}</p>
<!-- Click <a href="/banknotes">here</a>! -->
<p>{{ .Get "Click {Link:l}here{-:E}!"
(map "Link" "/banknotes") }}</p>
<!-- Click <a href="https://euro-cash.eu" target="_blank">here</a>! -->
<p>{{ .Get "Click {Link:L}here{-:E}!"
(map "Link" "https://euro-cash.eu") }}</p>
<!-- Varies per locale -->
<!-- €1 and €1.00 -->
<p>{{ .Get "{1:m} and {1.00:m}" (map "1" 1 "1.00" 1.00) }}</p>
Some additional notes:
- The -
name is special. {-:E}
inserts </a>
. This exists because
of how often you need to do this.
- The m
character code won’t include decimals when the argument is an
integer, and will include the decimals when the argument is a
floating-point number.