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? 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 tutorial, we will be taking a step-by-step approach to how to localize a Go application into multiple languages.
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.
- Add a
messageEn
variable holding amessage
for an ID of “hello” and an English localization value of “Hello!”. - Add a
messageFr
variable holding amessage
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
- 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.
- Use Bundle.AddMessages to add the
messageEn
pointer to the bundle as a message for English localization. - Equally, use
Bundle.AddMessages
to add themessageFr
pointer to the bundle as a message for French localization. - 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. - Configure a call using LocalizeConfig to later pass it over to the Localizer.Localize method to find a message with an ID of “hello”.
- Pass a pointer to
localizeConfig
created on step 5 to the Localize method insidelocalizer
.
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 }
localizeConfigWithDefault
variable holding a “welcome” message ID that doesn’t exist inside our project’sbundle
.DefaultMessage
variable set with a pointer to thedefaultmessageEn
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 }
- Define a
localizer
global variable of typeLocalizer
pointer. - Define a
bundle
global variable of typeBundle
pointer. - Create an init function inside
goi18n.go
. This is to place code that will automatically run before any other part of the package executes. bundle
global variable is initialized as a Bundle pointer using i18n.NewBundle function passing English as its default language.- Use Bundle.UnmarshalFunc to register an UnmarshalFunc for JSON format.
- Unmarshal the content inside the
en.json
file and load it as resources tobundle
. - Unmarshal the content inside the
fr.json
file and load it as resources tobundle
. - Use i18n.NewLocalizer to initialize
localizer
global variable as aLocalizer
pointer.localizer
looks up messages inbundle
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)
localizeConfigWelcome
variable holding a “welcome” message ID.- Pass a pointer to
localizeConfigWelcome
to the Localize method inside thelocalizer
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 }
- Call http.HandleFunc which registers a
SetLangPreferences
handler function for a “/setlang/” HTTP pattern. - 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 }
- request.FormValue extracts a “lang” parameter within the HTTP request. This acquired value is saved in a
lang
variable as a string. - request.Header.Get acquires the “Accept-Language” header value inside the HTTP request. This value is stored inside an
accept
string variable. - 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 insidelang
and secondly for the language stored inaccept
.
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 }
- 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. - Configure a call using LocalizeConfig to later pass it over to the Localizer.Localize method to find a message with an ID of
valToLocalize
. - Pass a pointer to
localizeConfig
created on the previous step to the Localize method insidelocalizer
. The returned value is saved inside alocalization
variable. - Use fmt.Fprintln to write the
localization
response toresponseWriter
.
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", }, })
- Create a Message with a “greeting” ID and pass its pointer to
messageWithPlaceholder
. - Set the “other” CLDR plural form of the message pointed by
messageWithPlaceholder
with a “Hello {{.Name}}!” value. This value holds aName
placeholder according to text/template syntax. -
localizer.Localize
called passing a pointer to ani18n.LocalizeConfig
with itsDefaultMessage
field set tomessageWithPlaceholder
. TemplateData
field oni18n.LocalizeConfig
is filled with amap
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 }
- Create a Message with a “greeting” ID and pass its pointer to
messageWithCustomTemplateDelimiter
. - Set the
LeftDelim
field of the message pointed bymessageWithCustomTemplateDelimiter
as “<<“. - Put the
RightDelim
field of the message pointed bymessageWithCustomTemplateDelimiter
as “>>”. - Set the “other” CLDR plural form of the message pointed by
messageWithCustomTemplateDelimiter
with a “Hello <<.Name>>!” value holding aName
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 })
- Create a Message with a “dogrescue” ID, and its “one” & “other” CLDR plural forms specified.
- Pointer to a
LocalizeConfig
holdingmessageWithPlurals
for its “DefaultMessage” field is passed over tolocalizer.Localize
. The result is stored intranslationOne
. LocalizeConfig
sets its “TemplateData” field with amap
holding a value of “Ryan Reynolds” for the “Name” placeholder and a value of 1 for the placeholder “Count”.- “PluralCount” field of
LocalizeConfig
is set to 1. This is to make our app choose the “one” plural form of the message. - Pointer to a
LocalizeConfig
holdingmessageWithPlurals
for its “DefaultMessage” field is passed over tolocalizer.Localize
. The result is stored intranslationMany
. LocalizeConfig
sets its “TemplateData” field with amap
holding a value of “Kaley Cuoco” for the “Name” placeholder and a value of 2 for the placeholder “Count”.- 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.