Go Internationalization Using go-i18n

Signifying simplicity from its two-letter name itself, Go has won the hearts of programmers as a verbose yet easy-to-learn language. As a language backed by Google and one that’s making even Python programmers make a switch, it would be a crime not to learn how to internationalize a Go application, don’t you think?

As a Golang developer creating your Go apps, who do you (or your organization) plan to reach? Especially considering the pandemic-plagued society we’re living in at the time of writing, quite possibly your answer would be something similar to “the world“. But, if your strategy on outreaching to the whole globe is presenting your Go web application in a single language and asking your non-English speaking app users to translate the app language themselves, I’m afraid it’s safe to say you’d be losing loads of your viable customers.

Hence, with its importance figured out, let us head straight into Go app internationalization!

In this article, we will be taking a step-by-step approach to how to localize a Go application.

We will cover the following topics in this tutorial:

  • Go i18n/l10n (internationalization/localization).
  • A step-by-step guide to making a Go app go from ‘basic’ to ‘internationalized’.
  • Using go-i18n package functionalities for Go app localization.
  • DefaultMessage for message fallback behavior.
  • Localization using Message structs and JSON message files.
  • Performing localizations through HTTP requests.
  • Using text/template syntax for placeholders.
  • Handling pluralization of nouns.
  • Date-time retrieval and formatting with help of the time package.

Assumptions

Basic knowledge of:

  • Golang (Go)

Prerequisites

A local environment set up with:

  • Go 1.11 or higher
  • Golang supported IDE
  • Any API Client (e.g.: Postman)

Note: Mentioned Go version or greater is required for module support.

Environment

I will be using the following environment for my development purposes:

  • Go 1.16.3 windows/amd64
  • GoLand 2020.1 Build 201.6668.125
  • Postman 8.3.1

The source code is available on GitHub.

Create a basic Go project

As our first step, let’s create a Go project we can use as our playground for upcoming internationalization stuff.

Let us open up our IDE and create a Go project named GoI18n inside a directory of the same name.

Since we’ll use a couple of Go packages as we go on, it’d be a good idea to make a Go module within our project to hold them. So secondly, let’s open up a command prompt within our GoI18n project’s root directory, and enter the following command:

go mod init GoI18n

Note: As you must have noticed, this creates a go.mod file within our GoI18n project’s root folder.

Next up, let’s create a goi18n.go file within our GoI18n project and fill it as follows:

package main
func main() {}

Believe it or not, we’ve already made a fully functioning Go application—which obviously does nothing for the time being. So, let’s find out in the upcoming sections how our empty GoI18n project can transform into an internationalized Go application!

Go ahead with internationalization

Okay, we’ve got our GoI18n application waiting to get internationalized. Let’s have a go at it, shall we?

Install go-i18n package

Firstly, let’s download the go-i18n library needed for our GoI18n project’s localization functions.

Let’s open up a console inside our project’s home directory and insert a command as follows:

go get github.com/nicksnyder/go-i18n/v2/i18n

This go get command downloads, builds, and installs the go-i18n library to our GoI18n project’s GoI18n module cache. If we head over to the go.mod file within the project we’ll be able to notice a new require line marking the relevant library was imported to our GoI18n application.

Elementary l10n

Let’s add some hardcoded language resources to our GoI18n app just to get a taste of localization in Go. Fill up the main method inside our project’s goi18n.go file like this:

messageEn := i18n.Message {  //1
  ID: "hello",
  Other: "Hello!",
}
messageFr := i18n.Message {  //2
  ID: "hello",
  Other: "Bonjour!",
}

go-i18n package provides a Message struct we can use to define strings that can be localized.

  1. Add a messageEn variable holding a message for an ID of “hello” and an English localization value of “Hello!”.
  2. Add a messageFr variable holding a message for the same ID “hello” but this time with its French localization value.

Note: At the moment you might be curious about the use of an “Other” key to store the localization values. This simply boils down to pluralization which we’ll have a look at in our upcoming sections.

Now, to perform our localizations, let’s add this code inside the main method just below the messages we added:

bundle := i18n.NewBundle(language.English)  //1
bundle.AddMessages(language.English, &messageEn)  //2
bundle.AddMessages(language.French, &messageFr)  //3

localizer := i18n.NewLocalizer(bundle,  //4
             language.French.String(),
             language.English.String())

localizeConfig := i18n.LocalizeConfig {  //5
  MessageID: "hello",
}

localization, _ := localizer.Localize(&localizeConfig)  //6
  1. A Bundle is created using i18n.NewBundle function passing in English as its default language. A pointer to the Bundle is saved inside bundle.

Note: It’s worthy to note that a Bundle in Go stores messages(language resources) for multiple languages. This is in contrast with the usual way of ‘bundles holding locale-specific values’; for instance, as with Java resource bundles.

  1. Use Bundle.AddMessages to add the messageEn pointer to the bundle as a message for English localization.
  2. Equally, use Bundle.AddMessages to add the messageFr pointer to the bundle as a message for French localization.
  3. Use i18n.NewLocalizer to create a new localizer that looks up messages in bundle in sequential order of the languages provided, i.e., French first and English second.
  4. Configure a call using LocalizeConfig to later pass it over to the Localizer.Localize method to find a message with an ID of “hello”.
  5. Pass a pointer to localizeConfig created on step 5 to the Localize method inside localizer.

Test it out

Let’s print out the localization value using a simple fmt.Println(localization) line. Running the app now should show us a value of Bonjour!.

Hence, we can see that localizer has scanned the languages passed over to it one by one for a “hello” key. Since it found the key in French resources, it was returned to us.

Message fallback behavior

Alright, we know how to retrieve a simple hardcoded language resource for a given key. But, what would happen if the key our LocalizeConfig asked to retrieve simply wasn’t there inside the bundle?

This is when the DefaultMessage variable inside LocalizeConfig comes in handy.

Firstly, let’s open the goi18n.go file within our GoI18n app. Time to fill its main method with a new Message. But this time, the message would act as a fallback for a key that doesn’t resolve inside our project’s bundle:

defaultmessageEn := i18n.Message{
  ID: "welcome",
  Other: "Welcome to my app!",
}

Secondly, let’s create a LocalizeConfig variable including the DefaultMessage we just defined:

localizeConfigWithDefault := i18n.LocalizeConfig {
  MessageID: "welcome",  //1
  DefaultMessage: &defaultmessageEn,  //2
}
  1. localizeConfigWithDefault variable holding a “welcome” message ID that doesn’t exist inside our project’s bundle.
  2. DefaultMessage variable set with a pointer to the defaultmessageEn message we created in the previous step.

Finally, let us pass this localizeConfigWithDefault to Localizer.Localize method:

localizationReturningDefault, _ := localizer.Localize(&localizeConfigWithDefault)
fmt.Println(localizationReturningDefault)

Test it out

Running our GoI18n application should now successfully print a value as follows:

Welcome to my app!

Even though our bundle didn’t hold a language resource with a “welcome” message ID, defaultmessageEn came to the rescue and showed its value as a default.

Localization using message files

About time we removed those ugly hardcoded language resources in our GoI18n app, wouldn’t you agree?

So, let’s see how we can move those resources into separate resource files and make them usable from the time the application starts executing.

Firstly, let us create a new resources directory inside our GoI18n project. Then, let’s add a new en.json file inside it to hold a few English language resources:

{
  "hello": "Hello!",
  "welcome": "Welcome to my app!"
}

Likewise, let us add an fr.json file as well to keep French language resources:

{
  "hello": "Bonjour!",
  "welcome": "Bienvenue sur mon appli!"
}

Note: Since later we’ll be accessing these resources by name, you’re free to choose any name you prefer for these JSON files.

Secondly, let’s make the following changes inside the goi18n.go file within our GoI18n project:

var localizer *i18n.Localizer  //1
var bundle *i18n.Bundle  //2

func init() {  //3
  bundle = i18n.NewBundle(language.English)  //4

  bundle.RegisterUnmarshalFunc("json", json.Unmarshal)  //5
  bundle.LoadMessageFile("resources/en.json")  //6
  bundle.LoadMessageFile("resources/fr.json")  //7

  localizer = i18n.NewLocalizer(bundle, language.English.String(), language.French.String())  //8
}
  1. Define a localizer global variable of type Localizer pointer.
  2. Define a bundle global variable of type Bundle pointer.
  3. Create an init function inside goi18n.go. This is to place code that will automatically run before any other part of the package executes.
  4. bundle global variable is initialized as a Bundle pointer using i18n.NewBundle function passing English as its default language.
  5. Use Bundle.UnmarshalFunc to register an UnmarshalFunc for JSON format.
  6. Unmarshal the content inside the en.json file and load it as resources to bundle.
  7. Unmarshal the content inside the fr.json file and load it as resources to bundle.
  8. Use i18n.NewLocalizer to initialize localizer global variable as a Localizer pointer. localizer looks up messages in bundle in sequential order of the languages provided, i.e., English first and French second.

Thirdly, let’s add this code inside goi18n.go file’s main method:

localizeConfigWelcome := i18n.LocalizeConfig{
  MessageID: "welcome",  //1
}
localizationUsingJson, _ := localizer.Localize(&localizeConfigWelcome)  //2

fmt.Println(localizationUsingJson)
  1. localizeConfigWelcome variable holding a “welcome” message ID.
  2. Pass a pointer to localizeConfigWelcome to the Localize method inside the localizer global variable.

Test it out

Hence, running our GoI18n application should effectively print a value as follows, this time without any hardcoded values provided:

Welcome to my app!

Notice this time the returned value was in English locale. This occurred since we passed the English language prior to French when setting the localizer global variable.

Use HTTP requests for l10n

Last time, we learned how our GoI18n app could provide internationalization using message files. But, at the end of the day, we still passed over LocalizeConfig variables holding hardcoded message IDs!

So next up, let’s see how we can use HTTP requests to not only make localizations but also to set our Go internationalization app’s language preferences.

Set language preferences

Firstly, let’s open the goi18n.go file within our GoI18n app and add the following inside its init method:

func init() {
    .
    http.HandleFunc("/setlang/", SetLangPreferences)  //1

    http.ListenAndServe(":8080", nil)  //2
}
  1. Call http.HandleFunc which registers a SetLangPreferences handler function for a “/setlang/” HTTP pattern.
  2. Use http.ListenAndServe to ask our GoI18n application to start a server that listens on port 8080.

Then, let us create this SetLangPreferences function inside the goi18n.go file:

func SetLangPreferences(_ http.ResponseWriter, request *http.Request) {
  lang := request.FormValue("lang")  //1
  accept := request.Header.Get("Accept-Language")  //2
  localizer = i18n.NewLocalizer(bundle, lang, accept)  //3
}
  1. request.FormValue extracts a “lang” parameter within the HTTP request. This acquired value is saved in a lang variable as a string.
  2. request.Header.Get acquires the “Accept-Language” header value inside the HTTP request. This value is stored inside an accept string variable.
  3. Use i18n.NewLocalizer to create a new localizer that looks up messages in bundle in sequential order of the languages provided, i.e., Firstly for the language stored inside lang and secondly for the language stored in accept.

Localize using GET request parameters

Let’s see how we can perform localizations passing our values through GET request parameters.

First up, let’s head over back the goi18n.go file within our GoI18n app and add the following line inside its init method:

func init() {
    .
    http.HandleFunc("/localize/", Localize)
    .
}

Important Note: Make sure to place the line before the call to http.ListenAndServe in order to register the handler before server initialization.

Here, the call to http.HandleFunc registers a Localize handler function for a “/localize/” HTTP pattern.

Secondly, let’s code the missing Localize handler function inside the goi18n.go file:

func Localize(responseWriter http.ResponseWriter, request *http.Request) {
  valToLocalize := request.URL.Query().Get("msg")  //1

  localizeConfig := i18n.LocalizeConfig{  //2
    MessageID: valToLocalize,
  }

  localization, _ := localizer.Localize(&localizeConfig)  //3

  fmt.Fprintln(responseWriter, localization)  //4
}
  1. Requesting for Query values for the request URL returns a Values map. We search this query values map for a “msg” key using the Values.Get method. The returned value is stored inside a valToLocalize variable.
  2. Configure a call using LocalizeConfig to later pass it over to the Localizer.Localize method to find a message with an ID of valToLocalize.
  3. Pass a pointer to localizeConfig created on the previous step to the Localize method inside localizer. The returned value is saved inside a localization variable.
  4. Use fmt.Fprintln to write the localization response to responseWriter.

Test it out

Firstly, let’s run our GoI18n app and make a GET request to “http://localhost:8080/localize” domain passing a “msg” parameter along with it:

http://localhost:8080/localize?msg=hello

Using our API client app to make this request should provide us a value as follows:

Secondly, let’s set our language preference.

This time around, let’s execute a GET request to “http://localhost:8080/setlang” passing a “lang” parameter like this:

http://localhost:8080/setlang?lang=fr

As you can see, we’ve asked to set the language of our GoI18n app to French.

Finally, let’s run the exact same request we sent on the first step to pass a “msg” parameter of value “hello”:

http://localhost:8080/localize?msg=hello

Almost like some sort of wizardry took place, this time we should see the value for “hello” localized to French:

Change language using Accept-Language header

Alternatively, we could change our Go internationalization app’s language using the “Accept-Language” header to the network request.

Let’s repeat the same steps as the last time. But, instead of sending a “lang” parameter, let’s add an “Accept-Language” header to our GET request:

Sending our usual GET request to “http://localhost:8080/localize” with the “msg” parameter should show that the localization language of our GoI18n app has been reverted to English:

Some Go internationalization extras

Awesome, we learned the most essential features of Go internationalization! But, surely it wouldn’t hurt to learn a few more extra features now, would it?

Using placeholders

Let’s see how we can use placeholders within our GoI18n app using the text/template syntax. Let us add the following inside the main method in our GoI18n application’s goi18n.go file:

bundle := i18n.NewBundle(language.English)
localizer := i18n.NewLocalizer(bundle, language.English.String())

messageWithPlaceholder := &i18n.Message{
  ID: "greeting",  //1
  Other: "Hello {{.Name}}!",  //2
}

localization, _ :=
  localizer.Localize(&i18n.LocalizeConfig {
    DefaultMessage: messageWithPlaceholder,  //3
    TemplateData: map[string]string {  //4
      "Name": "Dasun",
    },
  })
  1. Create a Message with a “greeting” ID and pass its pointer to messageWithPlaceholder.
  2. Set the “other” CLDR plural form of the message pointed by messageWithPlaceholder with a “Hello {{.Name}}!” value. This value holds a Name placeholder according to text/template syntax.
  3.  localizer.Localize called passing a pointer to an i18n.LocalizeConfig with its DefaultMessage field set to messageWithPlaceholder.
  4. TemplateData field on i18n.LocalizeConfig is filled with a map holding a “Name” key and a value of “Dasun”.

Test it out

Running our GoI18n application and printing out the localization value should show us a message with its placeholder correctly filled up:

Hello Dasun!

Custom template delimiter

Apparently, we’re not restricted to using the default {{ delimiter when setting our placeholders.

Let’s run the same code as earlier; but this time, passing in a new message with a few additional gimmicks:

.
messageWithCustomTemplateDelimiter := &i18n.Message{  //1
  ID: "greeting",
  LeftDelim: "<<", //2 
  RightDelim: ">>", //3
  Other: "Hello <<.Name>>!",  //4

}
  1. Create a Message with a “greeting” ID and pass its pointer to messageWithCustomTemplateDelimiter.
  2. Set the LeftDelim field of the message pointed by messageWithCustomTemplateDelimiter as “<<“.
  3. Put the RightDelim field of the message pointed by messageWithCustomTemplateDelimiter as “>>”.
  4. Set the “other” CLDR plural form of the message pointed by messageWithCustomTemplateDelimiter with a “Hello <<.Name>>!” value holding a Name placeholder. Notice that this time we’re using “<<” and “>>” as delimiters of our placeholder.

That’s it! Running our GoI18n app and printing out the localization value should give us the same result as before:

Hello Dasun!

Pluralization of nouns

Localizing nouns often come with a pluralization hurdle we have to cross. Imagine our GoI18n app—for whatever obscure reason—happened to state how many dogs particular celebrities rescued.

Wouldn’t we get sued by them if our Go app displayed it like “Ryan Reynolds rescued 1 dogs” or “Kaley Cuoco rescued 3 dog“? So, let’s see how we can potentially save thousands of dollars by pluralizing nouns the right way!

Let us open up GoI18n app’s goi18n.go file and add the following inside its main method:

bundle := i18n.NewBundle(language.English)
localizer := i18n.NewLocalizer(bundle, language.English.String())

var messageWithPlurals = &i18n.Message{  //1
  ID:    "dogrescue",
  One:   "{{.Name}} rescued {{.Count}} dog.",
  Other: "{{.Name}} rescued {{.Count}} dogs.",
}

translationOne, _ :=
  localizer.Localize(&i18n.LocalizeConfig{  //2
    DefaultMessage: messageWithPlurals,
    TemplateData: map[string]interface{}{  //3
      "Name":  "Ryan Reynolds",
      "Count": 1,
    },
    PluralCount: 1,  //4
  })

translationMany, _ :=
  localizer.Localize(&i18n.LocalizeConfig{ //5
    DefaultMessage: messageWithPlurals,
    TemplateData: map[string]interface{}{  //6
      "Name":  "Kaley Cuoco",
      "Count": 2,
    },
    PluralCount: 2,  //7
  })
  1. Create a Message with a “dogrescue” ID, and its “one” & “other” CLDR plural forms specified.
  2. Pointer to a LocalizeConfig holding messageWithPlurals for its “DefaultMessage” field is passed over to localizer.Localize. The result is stored in translationOne.
  3. LocalizeConfig sets its “TemplateData” field with a map holding a value of “Ryan Reynolds” for the “Name” placeholder and a value of 1 for the placeholder “Count”.
  4. “PluralCount” field of LocalizeConfig is set to 1. This is to make our app choose the “one” plural form of the message.
  5. Pointer to a LocalizeConfig holding messageWithPlurals for its “DefaultMessage” field is passed over to localizer.Localize. The result is stored in translationMany.
  6. LocalizeConfig sets its “TemplateData” field with a map holding a value of “Kaley Cuoco” for the “Name” placeholder and a value of 2 for the placeholder “Count”.
  7. The “PluralCount” field of LocalizeConfig is set to 2. This is to make our app choose the “other” plural form of the message.

Test it out

Printing out the values of translationOne and translationMany should give us a properly pluralized result like this:

Ryan Reynolds rescued 1 dog.
Kaley Cuoco rescued 2 dogs.

Date and time

We can make use of the time package to perform various date and time-related tasks which would be required in Go internationalization.

Get current date and time

Let us head over to the main method inside our GoI18n app’s goi18n.go file and insert a simple code as follows:

currentTime := time.Now()
fmt.Println("Current date-time is:", currentTime.String())

Running our project would now print a long output similar to this:

Current date-time is: 2021-05-15 18:10:11.8177734 +0530 +0530 m=+0.005484601

Date and time formatting

Obviously, the output from time.Now is detailed, but it’s a bit too verbose to be printed in an application interface, wouldn’t you agree?

But in fact, the developers of the Go language haven’t stranded us. Instead, they have opted to offer us a pretty unique, easy, and more practical way to help us define the date and time formats we need. And, it involves no MMs, DDs, YYYYs, or hh:mm:sss!

Take note of their special textual representation:

Mon Jan 2 15:04:05 -0700 MST 2006

We can mix and match this line’s components any way we like and pass it as a parameter to their time.Format method. Let’s see this in action, shall we?

Let’s open up our GoI18n app’s goi18n.go file and put a code like this in its main method:

currentTime := time.Now()
formattedTime := currentTime.Format("15:04:05 Mon 2 Jan 2006")
fmt.Println("Formatted current date-time is:", formattedTime)

As you can see, we’ve provided currentTime.Format method with an all-over-the-place scramble of their textual representation. But guess what? the printed result value successfully matches the exact date and time format we supplied:

Formatted current date-time is: 18:10:11 Sat 15 May 2021

Go for Lokalise, Lokalise for Go

If you or your team chose Go as your language, I’m pretty sure the pivotal reason behind the choice was the simplicity and the small learning curve of it.

But, if you paid enough attention, you must have caught the irony here. That Go internationalization itself isn’t quite the piece of cake, am I right? Especially as an unbattered language still passing its first steps in the developer community, at the moment Go internationalization leaves things to be desired.

But, why should you bother if there’s a much easier, 1000x faster, and more convenient, way to step on this Go internationalization venture?

Meet Lokalise, the translation management system that takes care of all your Go application internationalization needs. With features like:

  • Easy integration with various other services
  • Collaborative translations
  • Quality assurance tools for translations
  • Easy management of your translations through a central dashboard
  • Plus, loads of others

Lokalise will make your life a whole lot easier by letting you expand your Go internationalization-powered app to all the locales you’ll ever plan to reach.

Start with Lokalise in just a few steps:

  • Sign up for a free trial (no credit card information required).
  • Log in to your account.
  • Create a new project under any name you like.
  • Upload your translation files and edit them as required.

That’s all! You have already completed the baby steps toward Lokalise-ing your Go application. See the Getting Started section for a collection of articles that will provide all the help you’ll need to kick-start the Lokalise journey. Also, refer to Lokalise API Documentation for a complete list of REST commands you can call on your Lokalise internationalization project.

Conclusion

In this tutorial, we took a look at how to make a Go application approach internationalization by localizing it to multiple locales. We implemented a basic Go project and explored various use cases of the go-i18n package for localization. Moreover, we performed basic localization through hardcoded messages, learned how to move messages over to JSON files, and localized through HTTP requests. We examined how to set message fallback behavior using DefaultMessage and how we could use the text/template syntax to define placeholders.

Additionally, we checked out how we can pluralize nouns and ways we can use the time package for our date and time localization needs.

And with that, my time has come for another wrap-up. Drop me a line if you have any questions, and don’t hesitate to leave a comment.

Till we meet again, go beyond your localhost! But in real life, it’s probably a good idea to stay home for the time being.

Related posts

Sign up to our newsletter

Get the latest articles on all things localization and translation management delivered straight to your inbox.

Read also
Localization made easy. Why wait?
The preferred localization tool of 2000+ companies