aboutsummaryrefslogtreecommitdiffhomepage
path: root/doc/i18n.md
blob: 475a5129de7b005fc7ed349148d89c12b361ecbc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 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()`.

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

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

```html
<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`.

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

```html
{{ $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     |

```html
<!-- <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.