Go is a language known for its simplicity, and its two-letter name says it all. Backed by Google and getting attention from developers—some even switching from Python—it’s no surprise that Go internationalization (i18n) has become crucial for making apps truly global. And if you’re building with Go, learning how to internationalize your app is almost a must, right?
When you’re developing Go apps, who’s your target audience? Odds are, you’re thinking globally. But if your app is only available in one language, and you’re relying on ineffective translation management or expecting non-English users to figure it out themselves, you’re missing out on a lot of potential.
Implementing a robust localization process not only enhances user experience but also significantly broadens your app’s appeal in diverse markets. Asking users to handle translations on their own is a surefire way to lose them.
So, let’s get right into why and how you should internationalize your Go app!
In this tutorial, we’ll guide you step-by-step on how to localize a Go app for multiple languages.
The source code for this article is available on GitHub.
Prerequisites for Go internationalization
Before we dive into this tutorial, make sure you have the following:
- Basic knowledge of Golang (Go)
- Go 1.23 or higher installed on your machine
- Your favorite code editor
Create a basic Go internationalization project
First, let’s create a Go project as our playground for learning Go internationalization.
Open your favorite code editor and create a directory named GoI18n
. Inside that directory, we’ll set up the Go project.
Since we’ll be using a couple of external Go packages later, it’s a good idea to initialize a Go module in this project. To do this, open a terminal or command prompt in the goI18n
directory and run the following command:
go mod init goi18n
This will generate a go.mod
file in the root of your project, which will handle dependencies as we add them.
Next, create a file named main.go
and add the following basic code:
package main func main() {}
It might not seem like much, but congratulations—you’ve already created a fully functioning Go application (though it doesn’t do anything yet).
In the following sections, we’ll start adding code to load translations and transform this basic project into an internationalized Go app!
Go ahead with Go internationalization
Now that we’ve set up our basic goi18n
project, it’s time to jump into internationalization.
Install go-i18n package
First, let’s install the go-i18n
library, which provides the localization functions we’ll use.
Open a terminal in your project’s root directory and run the following command:
go get github.com/nicksnyder/go-i18n/v2/i18n
This command will download, build, and install the go-i18n
library, adding it to your module’s cache. If you check your go.mod
file, you’ll notice a new require
line confirming that the library has been added to your project.
Import go-i18n package
Now, let’s add some hardcoded language resources to see how localization works in Go. Update the main.go
file like this:
package main import ( "fmt" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) func main() { messageEn := i18n.Message{ ID: "hello", Other: "Hello!", } messageFr := i18n.Message{ ID: "hello", Other: "Bonjour!", } }
Here, we’re using the Message
struct provided by the go-i18n
package to define translatable strings.
- The
messageEn
variable holds a message for the IDhello
with the English text “Hello!”. - The
messageFr
variable holds the same message IDhello
but with the French text “Bonjour!”.
Note: The “Other” field stores the default translation. This is useful when we deal with pluralization, which we’ll cover in a later section.
We also import the language package package, which is part of the Go text
package suite. It’s commonly used for handling language tags (like en
, fr
, etc.), and in the case of go-i18n
, it will be used to specify the default and fallback languages for the application.
Basic localization (l10n)
To perform localization, add the following code below the messages in the main
function:
// ... your messages here ... bundle := i18n.NewBundle(language.English) bundle.AddMessages(language.English, &messageEn) bundle.AddMessages(language.French, &messageFr) localizer := i18n.NewLocalizer(bundle, language.French.String(), language.English.String()) localizeConfig := i18n.LocalizeConfig{ MessageID: "hello", } localization, _ := localizer.Localize(&localizeConfig) fmt.Println(localization)
Here’s what’s happening step by step:
- We create a
Bundle
usingi18n.NewBundle
, with English as the default language. ABundle
in Go stores messages (language resources) for multiple languages, unlike the usual locale-specific bundles in some other languages like Java. - We add the English message (
messageEn
) to the bundle usingBundle.AddMessages
. - We add the French message (
messageFr
) to the bundle in the same way. - We create a
Localizer
withi18n.NewLocalizer
, passing in the bundle and specifying French as the first language and English as the fallback. - We configure a
LocalizeConfig
to look for the message ID"hello"
. - Finally, we call
Localize
on the localizer, passing thelocalizeConfig
to get the localized message.
Test it out
To see the result, add this line to print the localized value:
fmt.Println(localization)
When you run the app with the go run .
command, it should print: “Bonjour!”.
In this case, the localizer checks the languages provided (French and then English) and finds the "hello"
message in the French resource, returning "Bonjour!"
.
Message fallback behavior
So far, we’ve seen how to retrieve a hardcoded language resource for a specific key. But what happens if the key we’re looking for isn’t in the bundle? That’s where the DefaultMessage
option in LocalizeConfig
comes in handy.
Adding a fallback message
Let’s open the main.go
file in our project and update the main
function with a new message. This message will act as a fallback in case a key isn’t found in the bundle:
// ... other messages ... defaultmessageEn := i18n.Message{ ID: "welcome", Other: "Welcome to my app!", } // ... bundle and l10n ...
In this case, we’ve created a fallback message with the ID welcome
and an English text value of “Welcome to my app!”.
Configuring LocalizeConfig
with DefaultMessage
Next, let’s create a LocalizeConfig
variable that includes the DefaultMessage
we just defined:
// ... other messages ... defaultmessageEn := i18n.Message{ ID: "welcome", Other: "Welcome to my app!", } bundle := i18n.NewBundle(language.English) bundle.AddMessages(language.English, &messageEn) bundle.AddMessages(language.French, &messageFr) localizer := i18n.NewLocalizer(bundle, language.French.String(), language.English.String()) localizeConfigWithDefault := i18n.LocalizeConfig{ // <----- change this MessageID: "welcome", DefaultMessage: &defaultmessageEn, } // ... other code ...
Here’s what’s happening:
- The
MessageID
is set to"welcome"
, a key that doesn’t exist in our bundle. - The
DefaultMessage
field is assigned the pointer to thedefaultmessageEn
message, acting as a fallback if the key is not found in the bundle.
Localizing with a fallback
Now, let’s pass the localizeConfigWithDefault
to the Localizer.Localize
method:
// ... other code ... localizationReturningDefault, _ := localizer.Localize(&localizeConfigWithDefault) fmt.Println(localizationReturningDefault)
This will attempt to localize the message with the key welcome
. Since the key is not in the bundle, it will fall back to defaultmessageEn
.
Test it out
When you run the app, it should print: “Welcome to my app!”.
Even though the bundle didn’t have a language resource for the welcome
message ID, the defaultmessageEn
message stepped in and returned its value as a fallback.
Localization using message files
It’s about time we cleaned up those hardcoded language resources in our goi18n
app, don’t you think? Let’s move those resources into separate files and load them dynamically when the app runs.
Step 1: Create message files
First, create a resources
directory inside your goi18n
project. Then, add a new file en.json
to hold the English translated messages:
{ "hello": "Hello!", "welcome": "Welcome to my app!" }
Next, add a fr.json
file for the French language resources:
{ "hello": "Bonjour!", "welcome": "Bienvenue sur mon appli!" }
Note: You can name these JSON files anything you like, as you’ll be referencing them by name later in the code.
Step 2: Modify main.go
to load message files
Now, let’s update the main.go
file to load the language resources from these JSON files.
First, declare two global variables in the file and update imports:
import ( "encoding/json" "fmt" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) var localizer *i18n.Localizer // Global localizer var bundle *i18n.Bundle // Global bundle
Then, create an init
function to automatically load these resources when the application starts:
func init() { bundle = i18n.NewBundle(language.English) // Default language bundle.RegisterUnmarshalFunc("json", json.Unmarshal) // Register JSON unmarshal function bundle.LoadMessageFile("resources/en.json") // Load English messages bundle.LoadMessageFile("resources/fr.json") // Load French messages localizer = i18n.NewLocalizer(bundle, language.English.String(), language.French.String()) // Initialize localizer }
- The
bundle
is initialized with English as the default language. - We use
RegisterUnmarshalFunc
to register a function that tells thebundle
how to unmarshal JSON data. - Then,
LoadMessageFile
is used to load the contents of theen.json
andfr.json
files into the bundle. - Finally, we initialize the
localizer
to look up messages from the bundle, prioritizing English and falling back to French.
Step 3: Perform localization
In the main
function, add the following code to localize the “welcome” message using the resources loaded from the JSON files:
func main() { localizeConfigWelcome := i18n.LocalizeConfig{ MessageID: "welcome", } localizationUsingJson, _ := localizer.Localize(&localizeConfigWelcome) fmt.Println(localizationUsingJson) }
- We define
localizeConfigWelcome
to hold theMessageID
“welcome”. - The
Localize
method looks up this ID in the bundle and returns the corresponding localized string.
Step 4: Test it out
Now, when you run the application, it should print: “Welcome to my app!”.
The message was returned in English since we prioritized English when setting up the localizer
. If you reverse the order of languages, the app would return the French message: "Bienvenue sur mon appli!"
.
Use HTTP requests for l10n
Previously, we learned how to use message files for localization in our goi18n
app. However, we were still relying on hardcoded LocalizeConfig
values. Now, let’s explore how to use HTTP requests to not only handle localization but also set language preferences dynamically.
Set language preferences
First, let’s modify the main.go
file to register a handler for setting language preferences via an HTTP request. Update the init
function like this and update imports:
import ( "encoding/json" "fmt" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" "net/http" ) func init() { // ... existing code ... http.HandleFunc("/setlang/", SetLangPreferences) // Register handler http.ListenAndServe(":8080", nil) // Start server on port 8080 }
http.HandleFunc
registers theSetLangPreferences
handler for the/setlang/
path.http.ListenAndServe
starts a server, listening on port 8080.
Next, add the SetLangPreferences
handler function to process language preferences:
func SetLangPreferences(_ http.ResponseWriter, request *http.Request) { lang := request.FormValue("lang") // Get "lang" parameter from form accept := request.Header.Get("Accept-Language") // Get "Accept-Language" header localizer = i18n.NewLocalizer(bundle, lang, accept) // Create new localizer }
request.FormValue("lang")
extracts thelang
parameter from the request.request.Header.Get("Accept-Language")
retrieves theAccept-Language
HTTP header.- We create a new
localizer
that checks thelang
parameter first and falls back to theAccept-Language
header if no parameter is provided.
Localize using GET request parameters
Next, let’s add a handler to localize messages based on the GET request parameters. In the init
function, add this line:
func init() { // ... other code ... http.HandleFunc("/setlang/", SetLangPreferences) // Register handler http.HandleFunc("/localize/", Localize) // Register localization handler http.ListenAndServe(":8080", nil) // Start server on port 8080 }
Make sure to register the handler before the http.ListenAndServe
call.
Now, define the Localize
handler function to perform the localization:
func Localize(responseWriter http.ResponseWriter, request *http.Request) { valToLocalize := request.URL.Query().Get("msg") // Extract "msg" from query parameters localizeConfig := i18n.LocalizeConfig{ MessageID: valToLocalize, } localization, _ := localizer.Localize(&localizeConfig) fmt.Fprintln(responseWriter, localization) // Write localization to response }
request.URL.Query().Get("msg")
gets themsg
parameter from the URL’s query string.- We configure
LocalizeConfig
to look up the message ID from themsg
parameter. - The localized message is returned and printed to the response using
fmt.Fprintln
.
Test it: Localize using a message parameter
Start the goi18n
application and make a GET request to http://localhost:8080/localize?msg=hello
. You can use your browser or a tool like Postman or curl
. You’ll see the “Hello!” message.
This shows that the app is using the English locale to return the localized string for "hello"
.
Test it: Set the language preference
Next, choose a specific language by making a GET request to http://localhost:8080/setlang?lang=fr
. This sets the language of the app to French.
Finally, repeat the first request to localize the hello
message again: http://localhost:8080/localize?msg=hello
. This time, it should return the French localized string: “Bonjour!”.
Change language using the Accept-Language header
To change the app’s language, you first need to set the preferred language by calling the /setlang
route with the lang
parameter. Once the language preference is set, subsequent requests to the /localize
route will use this language for localization.
To set the language to French, make a GET request to the /setlang
route with the lang
parameter. Here’s how you can do it using curl
:
curl "http://localhost:8080/setlang/?lang=fr"
This request tells the server to use French as the preferred language for localization.
Now, make a GET request to the /localize
route to localize a message. For example, you can request the hello
message like this:
curl -H "Accept-Language: en-US" "http://localhost:8080/localize/?msg=hello"
Since you’ve set the preferred language to French, the response will be: “Bonjour!”.
By using the /setlang
route to set the language preference, you can dynamically adjust the app’s language for subsequent requests without needing to resend the language in every request. This approach is particularly useful for managing user preferences in applications that need to support multiple languages.
Some Go internationalization extras
Awesome! We’ve covered the essentials of Go internationalization. Now let’s dive into a few additional features to extend the functionality.
Using placeholders
Let’s explore how to use placeholders within our goi18n
app, following the text/template syntax. This time, we will update our en.json
and fr.json
files to handle messages with placeholders.
Step 1: Update your JSON files
In your resources/en.json
and resources/fr.json
files, add a message that uses a placeholder.
resources/en.json
:
{ "greeting": { "other": "Hello {{.Name}}!" } }
resources/fr.json
:
{ "greeting": { "other": "Bonjour {{.Name}}!" } }
Step 2: Modify your Go code
Now, modify the main.go
file to localize the greeting
message with a placeholder.
func Localize(responseWriter http.ResponseWriter, request *http.Request) { valToLocalize := request.URL.Query().Get("msg") userName := request.URL.Query().Get("username") localizeConfig := i18n.LocalizeConfig{ MessageID: valToLocalize, TemplateData: map[string]string{ "Name": userName, }, } localization, _ := localizer.Localize(&localizeConfig) fmt.Fprintln(responseWriter, localization) }
This will output the following, based on the language preference you’ve set:
- English:
Hello John!
- French:
Bonjour John!
Run your server, proceed to http://localhost:8080/localize/?msg=greeting&username=John
(replace username
with any other name) and observe the result.
Custom template delimiter
By default, go-i18n uses the {{
and }}
delimiters for placeholders. However, you can customize these delimiters to fit your needs. Let’s update the JSON translation files and your Go code to demonstrate custom delimiters.
Step 1: Update your JSON files
First, we need to define a message with custom delimiters in the JSON file.
en.json
:
{ "greeting_custom": { "left_delim": "<<", "right_delim": ">>", "other": "Hello <<.Name>>!" } }
fr.json
:
{ "greeting_custom": { "left_delim": "<<", "right_delim": ">>", "other": "Bonjour <<.Name>>!" } }
In these JSON files:
left_delim
: Custom left delimiter (<<
)right_delim
: Custom right delimiter (>>
)other
: The message string with placeholders using the custom delimiters.
Step 2: Modify your Go code
In the Go code, modify your method to localize the message using custom delimiters:
localizeConfig := i18n.LocalizeConfig{ MessageID: "greeting_custom", // The message ID from the JSON file TemplateData: map[string]string{ "Name": "John", // Placeholder value }, }
This will load the message from the JSON file and apply the custom delimiters (<<
and >>
) for the Name
placeholder.
Pluralization
Pluralization is another important feature when localizing content. Let’s see how we can implement proper pluralization using JSON files in the goi18n
app.
Step 1: Update the JSON files for pluralization
We need to define pluralization rules in both the English and French JSON files. Here’s how to update them:
resources/en.json
:
{ "dogrescue": { "one": "{{.Name}} rescued {{.Count}} dog.", "other": "{{.Name}} rescued {{.Count}} dogs." } }
resources/fr.json
:
{ "dogrescue": { "one": "{{.Name}} a sauvé {{.Count}} chien.", "other": "{{.Name}} a sauvé {{.Count}} chiens." } }
Step 2: Modify your Go code for pluralization
Now, update the Localize
method in your main.go
file to handle pluralization using the PluralCount
field in LocalizeConfig
.
func Localize(responseWriter http.ResponseWriter, request *http.Request) { valToLocalize := request.URL.Query().Get("msg") // Get the message ID from the query parameters (e.g., "dogrescue") name := request.URL.Query().Get("name") // Get the name for the placeholder (e.g., "John") // Singular case (1 dog) translationOne, _ := localizer.Localize(&i18n.LocalizeConfig{ MessageID: valToLocalize, // The ID of the message to localize (e.g., "dogrescue") TemplateData: map[string]interface{}{ "Name": name, // Insert the "Name" value into the placeholder (e.g., "John") "Count": 1, // Set the "Count" value to 1 for singular case }, PluralCount: 1, // The PluralCount determines which plural form to use ("one") }) fmt.Fprintln(responseWriter, translationOne) // Print the localized singular message // Plural case (2 dogs) translationMany, _ := localizer.Localize(&i18n.LocalizeConfig{ MessageID: valToLocalize, // Same message ID ("dogrescue"), but different plural case TemplateData: map[string]interface{}{ "Name": name, // Insert the same "Name" value (e.g., "John") "Count": 2, // Set the "Count" value to 2 for plural case }, PluralCount: 2, // The PluralCount determines which plural form to use ("other") }) fmt.Fprintln(responseWriter, translationMany) // Print the localized plural message }
- Extract query parameters:
msg
determines the message ID to be localized (e.g., “dogrescue”).name
fills the{{.Name}}
placeholder in the message. It contains a person name.
- Singular case (1 item):
LocalizeConfig
is created withMessageID
, andTemplateData
containsName
andCount = 1
.PluralCount: 1
triggers the singular form (“one”) and prints the message (e.g.,John rescued 1 dog.
).
- Plural case (2 items):
- Another
LocalizeConfig
is created withCount = 2
. PluralCount: 2
triggers the plural form (“other”) and prints the plural message (e.g.,John rescued 2 dogs.
).
- Another
- Output:
- The function outputs two lines: singular and plural forms.
Step 3: Test it out
When you run the app and proceed to http://localhost:8080/localize/?msg=dogrescue&name=John
, it should print out the translation string with pluralization based on the language preference and the value of Count
:
- For English:
John rescued 1 dog.
John rescued 2 dogs.
- For French:
John a sauvé 1 chien.
John a sauvé 2 chiens.
Date and time in Go localization
The Go time
package offers various tools for working with dates and times—essential for applications that handle Go localization across different regions.
Get current date and time
To get the current date and time in our goi18n
app, let’s add the following to the Localize
method in main.go
:
currentTime := time.Now() fmt.Fprintln(responseWriter, "Current date-time is:", currentTime.String())
Also make sure to add the time
package to the list of imports.
Date and time formatting
While time.Now()
provides detailed output, it’s often too verbose for user interfaces, especially in localized apps. Go offers a unique, easy way to parse and format date and time using a special template rather than the typical YYYY-MM-DD
or hh:mm:ss
formats.
Go’s custom format is based on this reference:
Mon Jan 2 15:04:05 -0700 MST 2006
You can rearrange these components to format the date and time however you like. Let’s format the current time in a cleaner way:
currentTime := time.Now() formattedTime := currentTime.Format("15:04:05 Mon 2 Jan 2006") fmt.Fprintln(responseWriter, "Formatted current date-time is:", formattedTime)
Running this and proceeding to the localize/
path will give you something like:
Formatted current date-time is: 17:48:31 Mon 14 Oct 2024
As you can see, the output is much cleaner and easier to display in a localized application, making it more readable for users across different language tags or regions.
Go for Lokalise, Lokalise for Go
If your team chose Go as the language for your project, it was likely due to its simplicity and minimal learning curve. But when it comes to Go internationalization, things aren’t always as straightforward, right? Go, being a relatively young language, still has some growing to do, and internationalization can be a bit tricky.
But why struggle with manual translations when there’s a much faster, easier way to handle Go internationalization?
Meet Lokalise—the translation management system that streamlines the entire process for you. With features like:
- Easy integration with various services
- Collaborative translation tools
- Built-in quality assurance for your translations
- A central dashboard to manage all your localization files
Lokalise simplifies the way you manage translations, letting you scale your Go application to any locale you need with ease.
Getting started is easy:
- Sign up for a free trial (no credit card required).
- Log in to your account.
- Create a new project, name it however you like.
- Upload your translation files and start editing.
And that’s it! You’re already on your way to enhancing your Go internationalization efforts with Lokalise. For more detailed help, check out our Getting started section, and for technical specifics, refer to the Lokalise API Documentation.
With Lokalise, your Go application is ready to go global—without the headaches.
Conclusion
In this tutorial, we explored how to get started with Go internationalization and localization for multiple languages. We built a basic Go project and leveraged the go-i18n
package for various localization tasks. Starting with hardcoded messages, we transitioned to managing translations through JSON files and localized content via HTTP requests. We also implemented message fallback behavior with DefaultMessage
and used the text/template syntax for dynamic placeholders.
Furthermore, we covered how to handle pluralization for nouns and explored how the time package can help with date and time localization.
That wraps things up! If you have any questions or feedback, feel free to drop a comment.
Till next time, keep pushing your app beyond its locale limits—and while you’re at it, make sure to stay safe and well!