Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 1 | # Proposal: Localization support in Go |
| 2 | |
Austin Clements | d617678 | 2015-10-01 14:26:47 -0400 | [diff] [blame] | 3 | Discussion at https://golang.org/issue/12750. |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 4 | |
| 5 | ## Abstract |
| 6 | This proposal gives a big-picture overview of localization support for |
| 7 | Go, explaining how all pieces fit together. |
| 8 | It is intended as a guide to designing the individual packages and to allow |
| 9 | catching design issues early. |
| 10 | |
| 11 | ## Background |
| 12 | Localization can be a complex matter. |
| 13 | For many languages, localization is more than just translating an English format |
| 14 | string. |
| 15 | For example, a sentence may change depending on properties of the arguments such |
| 16 | as gender or plurality. |
| 17 | In turn, the rendering of the arguments may be influenced by, for example: |
| 18 | language, sentence context (start, middle, list item, standalone, etc.), |
| 19 | role within the sentence (case: dative, nominative, genitive, etc.), |
| 20 | formatting options, and |
| 21 | user-specific settings, like measurement system. |
| 22 | |
| 23 | In other words, the format string is selected based on the arguments and the |
| 24 | arguments may be rendered differently based on the format string, or even the |
| 25 | position within the format string. |
| 26 | |
| 27 | A localization framework should provide at least the following features: |
| 28 | |
| 29 | 1. mark and extract text in code to be translated, |
| 30 | 1. injecting translated text received from a translator, and |
| 31 | 1. formatting values, such as numbers, currencies, units, names, etc. |
| 32 | |
| 33 | Language-specific parsing of values belongs in this list as well, |
| 34 | but we consider it to be out of scope for now. |
| 35 | |
| 36 | ### Localization in Go |
| 37 | Although we have drawn some ideas for the design from other localization |
| 38 | libraries, the design will inevitably be different in various aspects for Go. |
| 39 | |
| 40 | Most frameworks center around the concept of a single user per machine. |
| 41 | This leads to concepts like default locale, per-locale loadable files, etc. |
| 42 | Go applications tend to be multi-user and single static libraries. |
| 43 | |
| 44 | Also many frameworks predate CLDR-provided features such as varying values |
| 45 | based on plural and gender. |
| 46 | Retrofitting frameworks to use this data is hard and often results in clunky APIs. |
| 47 | Designing a framework from scratch allows designing with such features in mind. |
| 48 | |
| 49 | ### Definitions |
| 50 | We call a **message** the abstract notion of some semantic content to be |
| 51 | conveyed to the user. |
| 52 | Each message is identified by a key, which will often be |
| 53 | a fmt- or template-style format string. |
| 54 | A message definition defines concrete format strings for a message |
| 55 | called **variants**. |
| 56 | A single message will have at least one variant per supported language. |
| 57 | |
| 58 | A message may take **arguments** to be substituted at given insertion points. |
| 59 | An argument may have 0 or more features. |
| 60 | An argument **feature** is a key-value pair derived from the value of this argument. |
| 61 | Features are used to select the specific variant for a message for a given |
| 62 | language at runtime. |
| 63 | A **feature value** is the value of an argument feature. |
| 64 | The set of possible feature values for an attribute can vary per language. |
| 65 | A **selector** is a user-provided string to select a variant based on a feature |
| 66 | or argument value. |
| 67 | |
| 68 | ## Proposal |
| 69 | Most messages in Go programs pass through either the fmt or one of the template |
| 70 | packages. |
| 71 | We treat each of these two types of packages separately. |
| 72 | |
| 73 | ### Package golang.org/x/text/message |
| 74 | Package message has drop-in replacements for most functions in the fmt package. |
| 75 | Replacing one of the print functions in fmt with the equivalent in package |
| 76 | message flags the string for extraction and causes language-specific rendering. |
| 77 | |
| 78 | Consider a traditional use of fmt: |
| 79 | |
| 80 | ```go |
| 81 | fmt.Printf("%s went to %s.", person, city) |
| 82 | ``` |
| 83 | |
| 84 | To localize this message, replace fmt with a message.Printer for a given language: |
| 85 | |
| 86 | ```go |
| 87 | p := message.NewPrinter(userLang) |
| 88 | p.Printf("%s went to %s.", person, city) |
| 89 | ``` |
| 90 | |
| 91 | To localize all strings in a certain scope, the user could assign such a printer |
| 92 | to `fmt`. |
| 93 | |
| 94 | Using the Printf of `message.Printer` has the following consequences: |
| 95 | |
| 96 | * it flags the format string for translation, |
| 97 | * the format string is now a key used for looking up translations (the format |
| 98 | string is still used as a format string in case of a missing translation), |
| 99 | * localizable types, like numbers are rendered corresponding to p's language. |
| 100 | |
| 101 | |
| 102 | In practice translations will be automatically injected from |
| 103 | a translator-supplied data source. |
| 104 | But let’s do this manually for now. |
| 105 | The following adds a localized variant for Dutch: |
| 106 | |
| 107 | ```go |
| 108 | message.Set(language.Dutch, "%s went to %s.", "%s is in %s geweest.") |
| 109 | ``` |
| 110 | |
| 111 | Assuming p is configured with `language.Dutch`, the Printf above will now print |
| 112 | the message in Dutch. |
| 113 | |
| 114 | In practice, translators do not see the code and may need more context than just |
| 115 | the format string. |
| 116 | The user may add context to the message by simply commenting the Go code: |
| 117 | |
| 118 | ```go |
| 119 | p.Printf("%s went to %s.", // Describes the location a person visited. |
| 120 | person, // The Person going to the location. |
| 121 | city, // The location visited. |
| 122 | ) |
| 123 | ``` |
| 124 | |
| 125 | The message extraction tool can pick up these comments and pass them to the |
| 126 | translator. |
| 127 | |
| 128 | The section on Features and the Rationale chapter present more details on package |
| 129 | message. |
| 130 | |
| 131 | ### Package golang.org/x/text/{template|html/template} |
| 132 | Templates can be localized by using the drop-in replacement packages of equal name. |
| 133 | They add the following functionality: |
| 134 | |
| 135 | * mark to-be-localized text in templates, |
| 136 | * substitute variants of localized text based on the language, and |
| 137 | * use the localized versions of the print builtins, if applicable. |
| 138 | |
| 139 | The `msg` action marks text in templates for localization analogous to the |
| 140 | namesake construct in Soy. |
| 141 | |
| 142 | Consider code using core’s text/template: |
| 143 | |
| 144 | ```go |
| 145 | import "text/template" |
| 146 | import "golang.org/x/text/language" |
| 147 | |
| 148 | const letter = ` |
| 149 | Dear {{.Name}}, |
| 150 | {{if .Attended}} |
| 151 | It was a pleasure to see you at the wedding.{{else}} |
| 152 | It is a shame you couldn't make it to the wedding.{{end}} |
| 153 | Best wishes, |
| 154 | Josie |
| 155 | ` |
| 156 | // Prepare some data to insert into the template. |
| 157 | type Recipient struct { |
| 158 | Name string |
| 159 | Attended bool |
| 160 | Language language.Tag |
| 161 | } |
| 162 | var recipients = []Recipient{ |
| 163 | {"Mildred", true, language.English}, |
| 164 | {"Aurélie", false, language.French}, |
| 165 | {"Rens", false, language.Dutch}, |
| 166 | } |
| 167 | func main() { |
| 168 | // Create a new template and parse the letter into it. |
| 169 | t := template.Must(template.New("letter").Parse(letter)) |
| 170 | |
| 171 | // Execute the template for each recipient. |
| 172 | for _, r := range recipients { |
| 173 | if err := t.Execute(os.Stdout, r); err != nil { |
| 174 | log.Println("executing template:", err) |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 | ``` |
| 179 | |
| 180 | To localize this program the user may adopt the program as follows: |
| 181 | |
| 182 | ```go |
| 183 | import "golang.org/x/text/template" |
| 184 | |
| 185 | const letter = ` |
| 186 | {{msg "Opening of a letter"}}Dear {{.Name}},{{end}} |
| 187 | {{if .Attended}} |
| 188 | {{msg}}It was a pleasure to see you at the wedding.{{end}}{{else}} |
| 189 | {{msg}}It is a shame you couldn't make it to the wedding.{{end}}{{end}} |
| 190 | {{msg "Closing of a letter, followed by name (f)"}}Best wishes,{{end}} |
| 191 | Josie |
| 192 | ` |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 193 | ``` |
| 194 | |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 195 | and |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 196 | |
| 197 | ```go |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 198 | func main() { |
| 199 | // Create a new template and parse the letter into it. |
| 200 | t := template.Must(template.New("letter").Parse(letter)) |
| 201 | |
| 202 | // Execute the template for each recipient. |
| 203 | for _, r := range recipients { |
| 204 | if err := t.Language(r.Language).Execute(os.Stdout, r); err != nil { |
| 205 | log.Println("executing template:", err) |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | ``` |
| 210 | |
| 211 | To make this work, we distinguish between normal and language-specific templates. |
| 212 | A normal template behaves exactly like a template in core, but may be associated |
| 213 | with a set of language-specific templates. |
| 214 | |
| 215 | A language-specific template differs from a normal template as follows: |
| 216 | It is associated with exactly one normal template, which we call its base template. |
| 217 | |
| 218 | 1. A Lookup of an associated template will find the first non-empty result of |
| 219 | a Lookup on: |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 220 | 1. the language-specific template itself, |
| 221 | 1. recursively, the result of Lookup on the template for the parent language |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 222 | (as defined by language.Tag.Parent) associated with its base template, or |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 223 | 1. the base template. |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 224 | 1. Any template obtained from a lookup on a language-specific template will itself |
| 225 | be a language-specific template for the same language. |
| 226 | The same lookup algorithm applies for such templates. |
| 227 | 1. The builtins print, println, and printf will respectively call the Sprint, |
| 228 | Sprintln, and Sprintf methods of a message.Printer for the associated language. |
| 229 | |
| 230 | A top-level template called `Messages` holds all translations of messages |
| 231 | in language-specific templates. This allows registering of variants using |
| 232 | existing methods defined on templates. |
| 233 | |
| 234 | |
| 235 | ```go |
| 236 | dutch := template.Messages.Language(language.Dutch) |
| 237 | template.Must(dutch.New(`Dear {{.Name}},`).Parse(`Lieve {{.Name}},`)) |
| 238 | template.Must(dutch. |
| 239 | New(`It was a pleasure to see you at the wedding.`). |
| 240 | Parse(`Het was een genoegen om je op de bruiloft te zien.`)) |
| 241 | // etc. |
| 242 | ``` |
| 243 | |
| 244 | ### Package golang.org/x/text/feature |
| 245 | So far we have addressed cases where messages get translated one-to-one in |
| 246 | different languages. |
| 247 | Translations are often not as simple. |
| 248 | Consider the message `"%[1]s went to %[2]"`, which has the arguments P (a person) |
| 249 | and D (a destination). |
| 250 | This one variant suffices for English. |
| 251 | In French, one needs two: |
| 252 | |
| 253 | gender of P is female: "%[1]s est allée à %[2]s.", and |
| 254 | gender of P is male: "%[1]s est allé à %[2]s." |
| 255 | |
| 256 | The number of variants needed to properly translate a message can vary |
| 257 | wildly per language. |
| 258 | For example, Arabic has six plural forms. |
| 259 | At worst, the number of variants for a language is equal to the Cartesian product |
| 260 | of all possible values for the argument features for this language. |
| 261 | |
| 262 | Package feature defines a mechanism for selecting message variants based on |
| 263 | linguistic features of its arguments. |
| 264 | Both the message and template packages allow selecting variants based on features. |
| 265 | CLDR provides data for plural and gender features. |
| 266 | Likewise-named packages in the text repo provide support for each. |
| 267 | |
| 268 | |
| 269 | An argument may have multiple features. |
| 270 | For example, a list of persons can have both a count attribute (the number of |
| 271 | people in the list) as well as a gender attribute (the combined gender of the |
| 272 | group of people in the list, the determination of which varies per language). |
| 273 | |
| 274 | The feature.Select struct defines a mapping of selectors to variants. |
| 275 | In practice, it is created by a feature-specific, high-level wrapper. |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 276 | For the above example, such a definition may look like: |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 277 | |
| 278 | ```go |
| 279 | message.SetSelect(language.French, "%s went to %s", |
| 280 | gender.Select(1, // Select on gender of the first argument. |
| 281 | "female", "%[1]s est allée à %[2]s.", |
| 282 | "other", "%[1]s est allé à %[2]s.")) |
| 283 | ``` |
| 284 | |
| 285 | The "1" in the Select statement refers to the first argument, which was our person. |
| 286 | The message definition now expects the first argument to support the gender feature. |
| 287 | For example: |
| 288 | |
| 289 | ```go |
| 290 | type Person struct { |
| 291 | Name string |
| 292 | gender.Gender |
| 293 | } |
| 294 | person := Person{ "Joe", gender.Male } |
| 295 | p.Printf("%s went to %s.", person, city) |
| 296 | ``` |
| 297 | |
| 298 | The plural package defines a feature type for plural forms. |
| 299 | An obvious consumer is the numbers package. |
| 300 | But any package that has any kind of amount or cardinality (e.g. lists) can use it. |
| 301 | An example usage: |
| 302 | |
| 303 | ```go |
| 304 | message.SetSelect(language.English, "There are %d file(s) remaining.", |
| 305 | plural.Select(1, |
| 306 | "zero", "Done!", |
| 307 | "one", "One file remaining", |
| 308 | "other", "There are %d files remaining.")) |
| 309 | ``` |
| 310 | |
| 311 | This works in English because the CLDR category "zero" and "one" correspond |
| 312 | exclusively to the values 0 and 1. |
| 313 | This is not the case, for example, for Serbian, where "one" is really a category |
| 314 | for a broad range of numbers ending in 1 but not 11. |
| 315 | To deal with such cases, we borrow a notation from ICU to support exact matching: |
| 316 | |
| 317 | ```go |
| 318 | message.SetSelect(language.English, "There are %d file(s) remaining.", |
| 319 | plural.Select(1, |
| 320 | "=0", "Done!", |
| 321 | "=1", "One file remaining", |
| 322 | "other", "There are %d files remaining.")) |
| 323 | ``` |
| 324 | |
| 325 | Besides "=", and in addition to ICU, we will also support the "<" and ">" comparators. |
| 326 | |
| 327 | The template packages would add a corresponding ParseSelect to add translation variants. |
| 328 | |
| 329 | ### Value formatting |
| 330 | We now move from localizing messages to localizing values. |
| 331 | This is a non-exhaustive list of value type that support localized rendering: |
| 332 | |
| 333 | * numbers |
| 334 | * currencies |
| 335 | * units |
| 336 | * lists |
| 337 | * dates (calendars, formatting with spell-out, intervals) |
| 338 | * time zones |
| 339 | * phone numbers |
| 340 | * postal addresses |
| 341 | |
| 342 | Each type maps to a separate package that roughly provides the same types: |
| 343 | |
| 344 | * Value: encapsulates a value and implements fmt.Formatter. |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 345 | For example, currency.Value encapsulates the amount, the currency, and |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 346 | whether it should be rendered as cash, accounting, etc. |
| 347 | * Formatter: a func of the form func(x interface{}) Value that creates or wraps |
| 348 | a Value to be rendered according to the Formatter's purpose. |
| 349 | |
| 350 | Since a Formatter leaves the actual printing to the implementation of |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 351 | fmt.Formatter, the value is not printed until after it is passed to one of the |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 352 | print methods. |
| 353 | This allows formatting flags, as well as other context information to influence |
| 354 | the rendering. |
| 355 | |
| 356 | The State object passed to Format needs to provide more information than |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 357 | what is passed by fmt.State, namely: |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 358 | |
| 359 | * a `language.Tag`, |
| 360 | * locale settings that a user may override relative to the user locale setting |
| 361 | (e.g. preferred time format, measurement system), |
| 362 | * sentence context, such as standalone, start-, mid-, or end-of-sentence, and |
| 363 | * formatting options, possibly defined by the translator. |
| 364 | |
| 365 | To accommodate this, we either need to define a text repo-specific State |
| 366 | implementation that Format implementations can type assert to or |
| 367 | define a different Formatter interface. |
| 368 | |
| 369 | #### Example: Currencies |
| 370 | We consider this pattern applied to currencies. The Value and Formatter type: |
| 371 | |
| 372 | ```go |
| 373 | // A Formatter associates formatting information with the given value. x may be a |
| 374 | // Currency, a Value, or a number if the Formatter is associated with a default currency. |
| 375 | type Formatter func(x interface{}) Value |
| 376 | |
| 377 | func (f Formatter) NumberFormat(f number.Formatter) Formatter |
| 378 | ... |
| 379 | |
| 380 | var Default Formatter = Formatter(formISO) |
| 381 | var Symbol Formatter = Formatter(formSymbol) |
| 382 | var SpellOut Formatter = Formatter(formSpellOut) |
| 383 | |
| 384 | type Value interface { |
| 385 | amount interface{} |
| 386 | currency Currency |
| 387 | formatter *settings |
| 388 | } |
| 389 | |
| 390 | // Format formats v. If State is a format.State, the value is formatted |
| 391 | // according to the given language. If State is not language-specific, it will |
| 392 | // use number plus ISO code for values and the ISO code for Currency. |
| 393 | func (v Value) Format(s fmt.State, verb rune) |
| 394 | func (v Value) Amount() interface{} |
| 395 | func (v Value) Float() (float64, error) |
| 396 | func (v Value) Currency() Currency |
| 397 | ... |
| 398 | ``` |
| 399 | |
| 400 | Usage examples: |
| 401 | |
| 402 | ```go |
| 403 | p := message.NewPrinter(language.AmericanEnglish) |
| 404 | p.Printf("You pay %s.", currency.USD.Value(3)) // You pay USD 3. |
| 405 | p.Printf("You pay %s.", currency.Symbol(currency.USD.Value(3))) // You pay $3. |
| 406 | p.Printf("You pay %s.", currency.SpellOut(currency.USD.Value(1)) // You pay 1 US Dollar. |
| 407 | spellout := currency.SpellOut.NumberFormat(number.SpellOut) |
| 408 | p.Printf("You pay %s.", spellout(currency.USD.Value(3))) // You pay three US Dollars. |
| 409 | ``` |
| 410 | |
| 411 | Formatters have option methods for creating new formatters. |
| 412 | Under the hood all formatter implementations use the same settings type, a |
| 413 | pointer of which is included as a field in Value. |
| 414 | So option methods can access a formatter’s settings by formatting a dummy value. |
| 415 | |
| 416 | Different types of currency types are available for different localized rounding |
| 417 | and accounting practices. |
| 418 | |
| 419 | ```go |
| 420 | v := currency.CHF.Value(3.123) |
| 421 | p.Printf("You pay %s.", currency.Cash.Value(v)) // You pay CHF 3.15. |
| 422 | |
| 423 | spellCash := currency.SpellOut.Kind(currency.Cash).NumberFormat(number.SpellOut) |
| 424 | p.Printf("You pay %s.", spellCash(v)) // You pay three point fifteen Swiss Francs. |
| 425 | ``` |
| 426 | |
| 427 | The API ensures unused tables are not linked in. |
| 428 | For example, the rather large tables for spelling out numbers and currencies |
| 429 | needed for number.SpellOut and currency.SpellOut are only linked in when |
| 430 | the respective formatters are called. |
| 431 | |
| 432 | #### Example: units |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 433 | Units are like currencies but have the added complexity that the amount and |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 434 | unit may change per locale. |
| 435 | The Formatter and Value types are analogous to those of Currency. |
| 436 | It defines "constructors" for a selection of unit types. |
| 437 | |
| 438 | ```go |
| 439 | type Formatter func(x interface{}) Value |
| 440 | var ( |
| 441 | Symbol Formatter = Formatter(formSymbol) |
| 442 | SpellOut Formatter = Formatter(formSpellOut) |
| 443 | ) |
| 444 | // Unit sets the default unit for the formatter. This allows the formatter to |
| 445 | // create values directly from numbers. |
| 446 | func (f Formatter) Unit(u Unit) Formatter |
| 447 | |
| 448 | // create formatted values: |
| 449 | func (f Formatter) Value(x interface{}, u Unit) Value |
| 450 | func (f Formatter) Meters(x interface{}) Value |
| 451 | func (f Formatter) KilometersPerHour(x interface{}) Value |
| 452 | … |
| 453 | |
| 454 | type Unit int |
| 455 | const SpeedKilometersPerHour Unit = ... |
| 456 | |
| 457 | type Kind int |
| 458 | const Speed Kind = ... |
| 459 | ``` |
| 460 | |
| 461 | Usage examples: |
| 462 | |
| 463 | ```go |
| 464 | p := message.NewPrinter(language.AmericanEnglish) |
| 465 | p.Printf("%d", unit.KilometersPerHour(250)) // 155 mph |
| 466 | ``` |
| 467 | |
| 468 | spelling out the unit names: |
| 469 | |
| 470 | ```go |
| 471 | p.Print(unit.SpellOut.KilometersPerHour(250)) // 155.343 miles per hour |
| 472 | ``` |
| 473 | |
| 474 | Associating a default unit with a formatter allows it to format numbers directly: |
| 475 | |
| 476 | ```go |
| 477 | kmh := unit.SpellOut.Unit(unit.SpeedKilometersPerHour) |
| 478 | p.Print(kmh(250)) // 155.343 miles per hour |
| 479 | ``` |
| 480 | |
| 481 | Spell out the number as well: |
| 482 | |
| 483 | ```go |
| 484 | spellout := unit.SpellOut.NumberFormat(number.SpellOut) |
| 485 | p.Print(spellout.KilometersPerHour(250)) |
| 486 | // one hundred fifty-five point three four three miles per hour |
| 487 | ``` |
| 488 | |
| 489 | or perhaps also |
| 490 | |
| 491 | ```go |
| 492 | p.Print(unit.SpellOut.KilometersPerHour(number.SpellOut(250))) |
| 493 | // one hundred fifty-five point three four three miles per hour |
| 494 | ``` |
| 495 | |
| 496 | Using a formatter, like `number.SpellOut(250)`, just returns a Value wrapped |
| 497 | with the new formatting settings. |
| 498 | The underlying value is retained, allowing its features to select |
| 499 | the proper unit names. |
| 500 | |
| 501 | There may be an ambiguity as to which unit to convert to when converting from |
| 502 | US to the metric system. |
| 503 | For example, feet can be converted to meters or centimeters. |
| 504 | Moreover, which one is to prefer may differ per language. |
| 505 | If this is an issue we may consider allowing overriding the default unit to |
| 506 | convert in a message. |
| 507 | For example: |
| 508 | |
| 509 | %[2:unit=km]f |
| 510 | |
| 511 | Such a construct would allow translators to annotate the preferred unit override. |
| 512 | |
| 513 | |
| 514 | ## Details and Rationale |
| 515 | |
| 516 | ### Formatting |
| 517 | |
| 518 | The proposed Go API deviates from a common pattern in other localization APIs by |
| 519 | _not_ associating a Formatter with a language. |
| 520 | Passing the language through State has several advantages: |
| 521 | |
| 522 | 1. the user needs to specify a language for a message only once, which means |
| 523 | 1. less typing, |
| 524 | 1. no possibility of mismatch, and |
| 525 | 1. no need to initialize a formatter for each language (which may mean on |
| 526 | every usage), |
| 527 | 1. the value is preserved up till selecting the variant, and |
| 528 | 1. a string is not rendered until its context is known. |
| 529 | |
| 530 | It prevents strings from being rendered prematurely, which, in turn, helps |
| 531 | picking the proper variant and allows translators to pass in options in |
| 532 | formatting strings. |
| 533 | The Formatter construct is a natural way of allowing for this flexibility and |
| 534 | allows for a straightforward and natural API for something that is otherwise |
| 535 | quite complex. |
| 536 | |
| 537 | The Value types of the formatting packages conflate data with formatting. |
| 538 | However, formatting types often are strongly correlated to types. |
| 539 | Combining formatting types with values is not unlike associating the time zone |
| 540 | with a Time or rounding information with a number. |
| 541 | Combined with the fact that localized formatting is one of the main purposes |
| 542 | of the text repo, it seems to make sense. |
| 543 | |
| 544 | #### Differences from the fmt package |
| 545 | Formatted printing in the message package differs from the equivalent in the |
| 546 | fmt package in various ways: |
| 547 | |
| 548 | * An argument may be used solely for its features, or may be unused for |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 549 | specific variants. |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 550 | It is therefore possible to have a format string that has no |
| 551 | substitutions even in the presence of arguments. |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 552 | * Package message dynamically selects a variant based on the |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 553 | arguments’ features and the configured language. |
| 554 | The format string passed to a formatted print method is mostly used as a |
| 555 | reference or key. |
| 556 | * The variant selection mechanism allows for the definition of variables |
| 557 | (see the section on package feature). |
| 558 | It seems unnatural to refer to these by position. |
| 559 | We contemplate the usage of named arguments for such variables: `%[name]s`. |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 560 | * Rendered text is always natural language and values render accordingly. |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 561 | For example, `[]int{1, 2, 3}` will be rendered, in English, as `"1, 2 and 3"`, |
| 562 | instead of `"[1 2 3]"`. |
| 563 | * Formatters may use information about sentence context. |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 564 | Such meta data must be derived by automated analysis or supplied by a |
| 565 | translator. |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 566 | |
| 567 | Considering the differences with fmt we expect package message to do its own |
| 568 | parsing. |
| 569 | Different substitution points of the same argument may require a different State |
| 570 | object to be passed. |
| 571 | Using fmt’s parser would require rewriting such arguments into different forms |
| 572 | and/or exposing more internals of fmt in the API. |
| 573 | It seems more straightforward for package message to do its own parsing. |
| 574 | Nonetheless, we aim to utilize as much of the fmt package as possible. |
| 575 | |
| 576 | #### Currency |
| 577 | Currency is its own package. |
| 578 | In most localization APIs the currency formatter is part of the number formatter. |
| 579 | Currency data is large, though, and putting it in its own package |
| 580 | avoids linking it in unnecessarily. |
| 581 | Separating the currency package also allows greater control over options. |
| 582 | Currencies have specific locale-sensitive rounding and scale settings that |
| 583 | may interact poorly with options provided for a number formatter. |
| 584 | |
| 585 | #### Units |
| 586 | We propose to have one large package that includes all unit types. |
| 587 | We could split this package up in, for example, packages for energy, mass, |
| 588 | length, speed etc. |
| 589 | However, there is a lot of overlap in data (e.g. kilometers and kilometers per hour). |
| 590 | Spreading the tables across packages will make sharing data harder. |
| 591 | Also, not all units belong naturally in a specific package. |
| 592 | |
| 593 | To mitigate the impact of including large tables, we can have composable modules |
| 594 | of data from which user can compose smaller formatters |
| 595 | (similar to the display package). |
| 596 | |
| 597 | |
| 598 | ### Features |
| 599 | |
| 600 | The proposed mechanism for features takes a somewhat different approach |
| 601 | to OS X and ICU. |
| 602 | It allows mitigating the combinatorial explosion that may occur when combining |
| 603 | features while still being legible. |
| 604 | |
| 605 | #### Matching algorithm |
| 606 | The matching algorithm returns the first match on a depth-first search on all cases. |
| 607 | We also allow for variable assignment. |
| 608 | We define the following types (in Go-ey pseudo code): |
| 609 | |
| 610 | Select struct { |
| 611 | Feature string // identifier of feature type |
| 612 | Argument interface{} // Argument reference |
| 613 | Cases []Case // The variants. |
| 614 | } |
| 615 | Case struct { Selector string; Value interface{} } |
| 616 | Var: struct { Name string; Value interface{} } |
| 617 | Value: Select or String |
| 618 | SelectSequence: [](Select or Var) |
| 619 | |
| 620 | To select a variant given a set of arguments: |
| 621 | |
| 622 | |
| 623 | 1. Initialize a map m from argument name to argument value. |
| 624 | 1. For each v in s: |
| 625 | 1. If v is of type Var, update m[v.Name] = Eval(v.Value, m) |
| 626 | 1. If v is of type Select, then let v be Eval(v, m). |
| 627 | 1. If v is of type string, return v. |
| 628 | |
| 629 | Eval(v, m): Value |
| 630 | |
| 631 | 1. If v is a string, return it. |
| 632 | 1. Let f be the feature value for feature v.Feature of argument v.Argument. |
| 633 | 1. For each case in v.Cases, |
| 634 | 1. return Eval(v) if f.Match(case.Selector, f, v.Argument) |
| 635 | 1. Return nil (no match) |
| 636 | |
| 637 | Match(s, cat, arg): string x string x interface{} // Implementation for numbers. |
| 638 | |
| 639 | 1. If s[0] == ‘=’ return int(s[1:]) == arg. |
| 640 | 1. If s[0] == ‘<’ return int(s[1:]) < arg. |
| 641 | 1. If s[0] == ‘>’ return int(s[1:]) > arg. |
| 642 | 1. If s == cat return true. |
| 643 | 1. return s == "other" |
| 644 | |
| 645 | A simple data structure encodes the entire Select procedure, which makes it |
| 646 | trivially machine-readable, a condition for including it in a translation pipeline. |
| 647 | |
| 648 | #### Full Example |
| 649 | |
| 650 | Consider the message `"%[1]s invite %[2] to their party"`, where argument 1 an 2 |
| 651 | are lists of respectively hosts and guests, and data: |
| 652 | |
| 653 | |
| 654 | ```go |
| 655 | map[string]interface{}{ |
| 656 | "Hosts": []gender.String{ |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 657 | gender.Male.String("Andy"), |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 658 | gender.Female.String("Sheila"), |
| 659 | }, |
| 660 | "Guests": []string{ "Andy", "Mary", "Bob", "Linda", "Carl", "Danny" }, |
| 661 | } |
| 662 | ``` |
| 663 | |
| 664 | |
| 665 | The following variant selector covers various cases for different values of the |
| 666 | arguments. |
| 667 | It limits the number of guests listed to 4. |
| 668 | |
| 669 | ```go |
| 670 | message.SetSelect(en, "%[1]s invite %[2]s and %[3]d other guests to their party.", |
| 671 | plural.Select(1, // Hosts |
| 672 | "=0", `There is no party. Move on!`, |
| 673 | "=1", plural.Select(2, // Guests |
| 674 | "=0", `%[1]s does not give a party.`, |
| 675 | "other", plural.Select(3, // Other guests count |
| 676 | "=0", gender.Select(1, // Hosts |
| 677 | "female", "%[1]s invites %[2]s to her party.", |
| 678 | "other ", "%[1]s invites %[2]s to his party."), |
| 679 | "=1", gender.Select(1, // Hosts |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 680 | "female", "%[1]s invites %#[2]s and one other person to her party.", |
| 681 | "other ", "%[1]s invites %#[2]s and one other person to his party."), |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 682 | "other", gender.Select(1, // Hosts |
| 683 | "female", "%[1]s invites %#[2]s and %[3]d other people to her party.", |
| 684 | "other ", "%[1]s invites %#[2]s and %[3]d other people to his party.")), |
| 685 | "other", plural.Select(2, // Guests, |
| 686 | "=0 ", "%[1]s do not give a party.", |
| 687 | "other", plural.Select(3, // Other guests count |
| 688 | "=0", "%[1]s invite %[2]s to their party.", |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 689 | "=1", "%[1]s invite %#[2]s and one other person to their party.", |
| 690 | "other ", "%[1]s invite %#[2]s and %[3]d other people to their party.")))) |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 691 | ``` |
| 692 | |
| 693 | <!-- ```go |
| 694 | template.Language(language.English). |
| 695 | New("{{.Hosts}} invite {{.Guests}} to their party."). |
| 696 | ParseSelect(plural.Select(".Hosts", |
| 697 | "=0", `There is no party. Move on!`, |
| 698 | "=1", plural.Select(".Guests", |
| 699 | "=0", `{{.Hosts}} does not give a party.`, |
| 700 | "<5", gender.Select(".Hosts", |
| 701 | "female", `{{.Hosts}} invites {{.Guests}} to her party.`, |
| 702 | "other ", `{{.Hosts}} invites {{.Guests}} to his party.`), |
| 703 | "=5", gender.Select(".Hosts", |
| 704 | "female", `{{.Hosts}} invites {{first 4 .Guests}} and one other |
| 705 | person to her party.`, |
| 706 | "other ", `{{.Hosts}} invites {{first 4 .Guests}} and one other |
| 707 | person to his party.`), |
| 708 | "other", gender.Select(".Hosts", |
| 709 | "female", `{{.Hosts}} invites {{first 4 .Guests}} and {{offset 4 .Guests}} |
| 710 | other people to her party.`, |
| 711 | "other ", `{{.Hosts}} invites {{first 4 .Guests}} and {{offset 4 .Guests}} |
| 712 | other people to his party.`), |
| 713 | ), |
| 714 | "other", plural.Select(".Guests", |
| 715 | "=0 ", `{{.Hosts}} do not give a party.`, |
| 716 | "<5 ", `{{.Hosts}} invite {{.Guests}} to their party.`, |
| 717 | "=5 ", `{{.Hosts}} invite {{first 4 .Guests}} and one other person |
| 718 | to their party.`, |
| 719 | "other ", `{{.Hosts}} invite {{first 4 .Guests}} and |
| 720 | {{offset 4 .Guests}} other people to their party.`))) |
| 721 | ``` --> |
| 722 | |
| 723 | For English, we have three variables to deal with: |
| 724 | the plural form of the hosts and guests and the gender of the hosts. |
| 725 | Both guests and hosts are slices. |
| 726 | Slices have a plural feature (its cardinality) and gender (based on CLDR data). |
| 727 | We define the flag `#` as an alternate form for lists to drop the comma. |
| 728 | |
| 729 | It should be clear how quickly things can blow up with when dealing with |
| 730 | multiple features. |
| 731 | There are 12 variants. |
| 732 | For other languages this could be quite a bit more. |
| 733 | Using the properties of the matching algorithm one can often mitigate this issue. |
| 734 | With a bit of creativity, we can remove the two cases where `Len(Guests) == 0` |
| 735 | and add another select block at the start of the list: |
| 736 | |
| 737 | |
| 738 | |
| 739 | ```go |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 740 | message.SetSelect(en, "%[1]s invite %[2]s and %[3]d other guests to their party.", |
| 741 | plural.Select(2, "=0", `There is no party. Move on!`), |
| 742 | plural.Select(1, |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 743 | "=0", `There is no party. Move on!`, |
| 744 | … |
| 745 | ``` |
| 746 | |
| 747 | <!-- ```go |
| 748 | template.Language(language.English). |
| 749 | New("{{.Hosts}} invite {{.Guests}} to their party."). |
| 750 | ParseSelect( |
| 751 | plural.Select(".Guests", "=0", `There is no party. Move on!`), |
| 752 | plural.Select(".Hosts", |
| 753 | "=0", `There is no party. Move on!`, |
| 754 | … |
| 755 | ``` --> |
| 756 | |
| 757 | The algorithm will return from the first select when `len(Guests) == 0`, |
| 758 | so this case will not have to be considered later. |
| 759 | |
| 760 | Using Var we can do a lot better, though: |
| 761 | |
| 762 | ```go |
| 763 | message.SetSelect(en, "%[1]s invite %[2]s and %[3]d other guests to their party.", |
| 764 | feature.Var("noParty", "There is no party. Move on!"), |
| 765 | plural.Select(1, "=0", "%[noParty]s"), |
| 766 | plural.Select(2, "=0", "%[noParty]s"), |
| 767 | |
| 768 | feature.Var("their", gender.Select(1, "female", "her", "other ", "his")), |
| 769 | // Variables may be overwritten. |
| 770 | feature.Var("their", plural.Select(1, ">1", "their")), |
| 771 | feature.Var("invite", plural.Select(1, "=1", "invites", "other ", "invite")), |
| 772 | |
| 773 | feature.Var("guests", plural.Select(3, // other guests |
| 774 | "=0", "%[2]s", |
| 775 | "=1", "%#[2]s and one other person", |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 776 | "other", "%#[2]s and %[3]d other people"), |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 777 | feature.String("%[1]s %[invite]s %[guests]s to %[their]s party.")) |
| 778 | ``` |
| 779 | |
| 780 | <!--```go |
| 781 | template.Language(language.English). |
| 782 | New("{{.Hosts}} invite {{.Guests}} to their party."). |
| 783 | ParseSelect( |
| 784 | feature.Var("noParty", "There is no party. Move on!"), |
| 785 | plural.Select(".Hosts", "=0", `{{$noParty}}`), |
| 786 | plural.Select(".Guests", "=0", `{{$noParty}}`), |
| 787 | |
| 788 | feature.Var("their", gender.Select(".Hosts", |
| 789 | "female", "her", |
| 790 | "other ", "his")), |
| 791 | // Variables may be overwritten. |
| 792 | feature.Var("their", plural.Select(".Hosts", ">1", "their")), |
| 793 | feature.Var("invite", plural.Select(".Hosts", |
| 794 | "=1", "invites", |
| 795 | "other ", "invite")), |
| 796 | |
| 797 | plural.Select(".Guests", |
| 798 | "<5", `{{.Hosts}} {{$invite}} {{.Guests}} to {{$their}} party.`, |
| 799 | "=5", `{{.Hosts}} {{$invite}} {{first 4 .Guests}} and one other person |
| 800 | to {{$their}} party.`, |
| 801 | "other", `{{.Hosts}} {{$invite}} {{first 4 .Guests | printf "%#v"}} |
| 802 | and {{offset 4 .Guests}} other people to {{$their}} party.`)) |
| 803 | ```--> |
| 804 | |
| 805 | |
| 806 | This is essentially the same as the example before, but with the use of |
| 807 | variables to reduce the verbosity. |
| 808 | If one always shows all guests, there would only be one variant for describing |
| 809 | the guests attending a party! |
| 810 | |
| 811 | #### Comparison to ICU |
| 812 | ICU has a similar approach to dealing with gender and plurals. |
| 813 | The above example roughly translates to: |
| 814 | |
| 815 | ``` |
| 816 | `{num_hosts, plural, |
| 817 | =0 {There is no party. Move on!} |
| 818 | other { |
| 819 | {gender_of_host, select, |
| 820 | female { |
| 821 | {num_guests, plural, offset:1 |
| 822 | =0 {{host} does not give a party.} |
| 823 | =1 {{host} invites {guest} to her party.} |
| 824 | =2 {{host} invites {guest} and one other person to her party.} |
| 825 | other {{host} invites {guest} and # other people to her party.}}} |
| 826 | male { |
| 827 | {num_guests, plural, offset:1 |
| 828 | =0 {{host} does not give a party.} |
| 829 | =1 {{host} invites {guest} to his party.} |
| 830 | =2 {{host} invites {guest} and one other person to his party.} |
| 831 | other {{host} invites {guest} and # other people to his party.}}} |
| 832 | other { |
| 833 | {num_guests, plural, offset:1 |
| 834 | =0 {{host} do not give a party.} |
| 835 | =1 {{host} invite {guest} to their party.} |
| 836 | =2 {{host} invite {guest} and one other person to their party.} |
| 837 | other {{host} invite {guest} and # other people to their party.}}}}}}` |
| 838 | ``` |
| 839 | |
| 840 | Comparison: |
| 841 | |
| 842 | * In Go, features are associated with values, instead of passed separately. |
| 843 | * There is no Var construct in ICU. |
| 844 | * Instead the ICU notation is more flexible and allows for notations like: |
| 845 | |
| 846 | ``` |
| 847 | "{1, plural, |
| 848 | zero {Personne ne se rendit} |
| 849 | one {{0} est {2, select, female {allée} other {allé}}} |
| 850 | other {{0} sont {2, select, female {allées} other {allés}}}} à {3}" |
| 851 | ``` |
| 852 | |
| 853 | * In Go, strings can only be assigned to variables or used in leaf nodes of a |
| 854 | select. We find this to result in more readable definitions. |
| 855 | * The Go notation is fully expressed in terms of Go structs: |
| 856 | * There is no separate syntax to learn. |
| 857 | * Most of the syntax is checked at compile time. |
| 858 | * It is serializable and machine readable without needing another parser. |
| 859 | * In Go, feature types are fully generic. |
| 860 | * Go has no special syntax for constructs like offset (see the third argument |
| 861 | in ICU’s plural select and the "#" for substituting offsets). |
| 862 | We can solve this with pipelines in templates and special interpretation for |
| 863 | flag and verb types for the Format implementation of lists. |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 864 | * ICU's algorithm seems to prohibit the user of ‘<’ and ‘>’ selectors. |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 865 | |
| 866 | #### Comparison to OS X |
| 867 | |
| 868 | OS X recently introduced support for handling plurals and prepared for support |
| 869 | for gender. |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 870 | The data for selecting variants is stored in the stringsdict file. |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 871 | This example from the referenced link shows how to vary sentences for |
| 872 | "number of files selected" in English: |
| 873 | |
| 874 | ``` |
| 875 | <?xml version="1.0" encoding="UTF-8"?> |
| 876 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 877 | <plist version="1.0"> |
| 878 | <dict> |
| 879 | <key>%d files are selected</key> |
| 880 | <dict> |
| 881 | <key>NSStringLocalizedFormatKey</key> |
| 882 | <string>%#@num_files_are@ selected</string> |
| 883 | <key>num_files_are</key> |
| 884 | <dict> |
| 885 | <key>NSStringFormatSpecTypeKey</key> |
| 886 | <string>NSStringPluralRuleType</string> |
| 887 | <key>NSStringFormatValueTypeKey</key> |
| 888 | <string>d</string> |
| 889 | <key>zero</key> |
| 890 | <string>No file is</string> |
| 891 | <key>one</key> |
| 892 | <string>A file is</string> |
| 893 | <key>other</key> |
| 894 | <string>%d files are</string> |
| 895 | </dict> |
| 896 | </dict> |
| 897 | </dict> |
| 898 | </plist> |
| 899 | ``` |
| 900 | |
| 901 | The equivalent in the proposed Go format: |
| 902 | |
| 903 | ```go |
| 904 | message.SetSelect(language.English, "%d files are selected", |
| 905 | feature.Var("numFilesAre", plural.Select(1, |
| 906 | "zero", "No file is", |
| 907 | "one", "A file is", |
| 908 | "other", "%d files are")), |
| 909 | feature.String("%[numFilesAre]s selected")) |
| 910 | ``` |
| 911 | |
| 912 | A comparison between OS X and the proposed design: |
| 913 | |
| 914 | * In both cases, the selection of variants can be represented in a data structure. |
| 915 | * OS X does not have a specific API for defining the variant selection in code. |
| 916 | * Both approaches allow for arbitrary feature implementations. |
| 917 | * OS X allows for a similar construct to Var to allow substitution of substrings. |
| 918 | * OS X has extended its printf-style format specifier to allow for named substitutions. |
| 919 | The substitution string `"%#@foo@"` will substitute the variable foo. |
| 920 | The equivalent in Go is the less offensive `"%[foo]v"`. |
| 921 | |
| 922 | ### Code organization |
| 923 | The typical Go deployment is that of a single statically linked binary. |
| 924 | Traditionally, though, most localization frameworks have grouped data in |
| 925 | per-language dynamically-loaded files. |
| 926 | We suggested some code organization methods for both use cases. |
| 927 | |
| 928 | #### Example: statically linked package |
| 929 | |
| 930 | In the following code, a single file called messages.go contains all collected |
| 931 | translations: |
| 932 | |
| 933 | ```go |
| 934 | import "golang.org/x/text/message" |
| 935 | func init() { |
| 936 | for _, e := range entries{ |
| 937 | for _, t := range e { |
| 938 | message.SetSelect(e.lang, t.key, t.value) |
| 939 | } |
| 940 | } |
| 941 | } |
| 942 | type entry struct { |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 943 | key string |
| 944 | value feature.Value |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 945 | } |
| 946 | var entries = []struct{ |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 947 | lang language.Tag |
| 948 | entry []entry |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 949 | }{ |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 950 | { language.French, []entry{ |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 951 | { "Hello", feature.String("Bonjour") }, |
| 952 | { "%s went to %s", feature.Select{ … } }, |
Ben Lubar | 3c72fa4 | 2016-02-19 21:56:53 -0600 | [diff] [blame] | 953 | … |
| 954 | }, |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 955 | } |
| 956 | |
| 957 | ``` |
| 958 | |
| 959 | #### Example: dynamically loaded files |
| 960 | |
| 961 | We suggest storing per-language data files in a messages subdirectory: |
| 962 | |
| 963 | ```go |
| 964 | func NewPrinter(t language.Tag) *message.Printer { |
| 965 | r, err := os.Open(filepath.Join("messages", t.String() + ".json")) |
| 966 | // handle error |
| 967 | cat := message.NewCatalog() |
| 968 | d := json.NewDecoder(r) |
| 969 | for { |
Marcel van Lohuizen | fe0e521 | 2015-09-28 10:14:26 +0200 | [diff] [blame] | 970 | var msg struct{ Key string; Value []feature.Value } |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 971 | if err := d.Decode(&msg); err == io.EOF { |
| 972 | break |
| 973 | } else if err != nil { |
| 974 | // handle error |
| 975 | } |
| 976 | cat.SetSelect(t, msg.Key, msg.Value...) |
| 977 | } |
| 978 | return cat.NewPrinter(t) |
| 979 | } |
| 980 | ``` |
| 981 | |
Marcel van Lohuizen | 6d71073 | 2015-09-25 21:12:15 +0200 | [diff] [blame] | 982 | ## Compatibility |
| 983 | |
| 984 | The implementation of the `msg` action will require some modification to core’s |
| 985 | template/parse package. |
| 986 | Such a change would be backward compatible. |
| 987 | |
| 988 | ## Implementation Plan |
| 989 | |
| 990 | Implementation would start with some of the rudimentary package in the text |
| 991 | repo, most notably format. |
| 992 | Subsequently, this allows the implementation of the formatting of some specific |
| 993 | types, like currencies. |
| 994 | The messages package will be implemented first. |
| 995 | The template package is more invasive and will be implemented at a later stage. |
| 996 | Work on infrastructure for extraction messages from templates and print |
| 997 | statements will allow integrating the tools with translation pipelines. |