Go Internationalization

Golang internationalization & localization examples with go-i18n

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 ID hello with the English text “Hello!”.
    • The messageFr variable holds the same message ID hello 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:

    1. We create a Bundle using i18n.NewBundle, with English as the default language. A Bundle in Go stores messages (language resources) for multiple languages, unlike the usual locale-specific bundles in some other languages like Java.
    2. We add the English message (messageEn) to the bundle using Bundle.AddMessages.
    3. We add the French message (messageFr) to the bundle in the same way.
    4. We create a Localizer with i18n.NewLocalizer, passing in the bundle and specifying French as the first language and English as the fallback.
    5. We configure a LocalizeConfig to look for the message ID "hello".
    6. Finally, we call Localize on the localizer, passing the localizeConfig 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:

    1. The MessageID is set to "welcome", a key that doesn’t exist in our bundle.
    2. The DefaultMessage field is assigned the pointer to the defaultmessageEn 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 the bundle how to unmarshal JSON data.
    • Then, LoadMessageFile is used to load the contents of the en.json and fr.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)
    }
    1. We define localizeConfigWelcome to hold the MessageID “welcome”.
    2. 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 the SetLangPreferences 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 the lang parameter from the request.
    • request.Header.Get("Accept-Language") retrieves the Accept-Language HTTP header.
    • We create a new localizer that checks the lang parameter first and falls back to the Accept-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 the msg parameter from the URL’s query string.
    • We configure LocalizeConfig to look up the message ID from the msg 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
    }
    1. 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.
    2. Singular case (1 item):
      • LocalizeConfig is created with MessageID, and TemplateData contains Name and Count = 1.
      • PluralCount: 1 triggers the singular form (“one”) and prints the message (e.g., John rescued 1 dog.).
    3. Plural case (2 items):
      • Another LocalizeConfig is created with Count = 2.
      • PluralCount: 2 triggers the plural form (“other”) and prints the plural message (e.g., John rescued 2 dogs.).
    4. 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:

    1. Sign up for a free trial (no credit card required).
    2. Log in to your account.
    3. Create a new project, name it however you like.
    4. 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!

    Related articles
    Stop wasting time with manual localization tasks. 

    Launch global products days from now.