Vic ShΓ³stak
Posted on April 12, 2021
Introduction
γγηοΌβοΈ Let's talk about one of the important topics, if you're preparing a Go application for a multilingual audience or just need support for different languages in the REST APIs.
This topic is not as simple as it seems. Because each language has its own special elements in terms of the word form when using numerals. For example, in Russian there are 3 different variants for items quantities:
-
one
, 1 item; -
few
, 2 items; -
many
, 3+ items;
π€ And this must be understood when translating the application!
Don't worry, everything will soon fall into place.
π Table of contents
- Source code of the project
- Prepare the project for translation
- Extracting the original language
- Launch the application and playing with languages
- Afterword
Source code of the project
Yeah, for those who like to see the code first, I created a repository on GitHub:
koddr / tutorial-go-i18n
π Tutorial: An easy way to translate your Golang application
Prepare the project for translation
I've looked at many packages for this operation (including the one built into the Go core), but nicksnyder/go-i18n was the only one I enjoyed working with in my projects. We will create our demo application using this particular package.
π Please write in the comments which package for i18n you use and why!
Website application
Yes, let's take the Fiber web framework as the core for our application, which has excellent template support (with smoothly reload function) and is easy to write and read code.
π₯ Please read comments in code!
// ./main.go
package main
import (
"log"
"strconv"
"github.com/BurntSushi/toml"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func main() {
// Create a new i18n bundle with default language.
bundle := i18n.NewBundle(language.English)
// Register a toml unmarshal function for i18n bundle.
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// Load translations from toml files for non-default languages.
bundle.MustLoadMessageFile("./lang/active.es.toml")
bundle.MustLoadMessageFile("./lang/active.ru.toml")
// Create a new engine by passing the template folder
// and template extension.
engine := html.New("./templates", ".html")
// Reload the templates on each render, good for development.
// Optional, default is false.
engine.Reload(true)
// After you created your engine, you can pass it
// to Fiber's Views Engine.
app := fiber.New(fiber.Config{
Views: engine,
})
// Register a new route.
app.Get("/", func(c *fiber.Ctx) error {
lang := c.Query("lang") // parse language from query
accept := c.Get("Accept-Language") // or, parse from Header
// Create a new localizer.
localizer := i18n.NewLocalizer(bundle, lang, accept)
// Set title message.
helloPerson := localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "HelloPerson", // set translation ID
Other: "Hello {{.Name}}", // set default translation
},
TemplateData: &fiber.Map{
"Name": "John",
},
})
// Parse and set unread count of emails.
unreadEmailCount, _ := strconv.ParseInt(c.Query("unread"), 10, 64)
// Config for translation of email count.
unreadEmailConfig := &i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "MyUnreadEmails",
One: "You have {{.PluralCount}} unread email.",
Other: "You have {{.PluralCount}} unread emails.",
},
PluralCount: unreadEmailCount,
}
// Set localizer for unread emails.
unreadEmails := localizer.MustLocalize(unreadEmailConfig)
// Return data as JSON.
if c.Query("format") == "json" {
return c.JSON(&fiber.Map{
"name": helloPerson,
"unread_emails": unreadEmails,
})
}
// Return rendered template.
return c.Render("index", fiber.Map{
"Title": helloPerson,
"UnreadEmails": unreadEmails,
})
})
// Start server on port 3000.
log.Fatal(app.Listen(":3000"))
}
Template for display
Normally, I don't like to take pre-made CSS libraries, but for the simplicity and nice look of this demo, I took the Bootstrap 5 (v5.0.0-beta3
) library:
<!-- ./templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{.Title}}</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6"
crossorigin="anonymous"
/>
<style>
* {
font-family: sans-serif;
color: #333333;
}
</style>
</head>
<body>
<div class="col-lg-8 mx-auto p-3 py-md-5">
<h1>{{.Title}}</h1>
<br />
<div class="row g-5">
<div class="col-md-6">
<ul class="icon-list">
<li>{{.UnreadEmails}}</li>
</ul>
</div>
</div>
<footer class="pt-5 my-5 text-muted border-top">
Switch to π¬π§ <a href="/">English</a>, πͺπΈ
<a href="/?lang=es">EspaΓ±ol</a>, π·πΊ <a href="/?lang=ru">Π ΡΡΡΠΊΠΈΠΉ</a>.
</footer>
</div>
</body>
</html>
Extracting the original language
- First, install
goi18n
CLI:
go get -u github.com/nicksnyder/go-i18n/v2/goi18n
- Extract all
i18n.Message
struct literals in our Go source files to a message file for translation (by default,active.en.toml
):
goi18n extract
# ./active.en.toml
HelloPerson = "Hello {{.Name}}"
[MyUnreadEmails]
one = "You have {{.PluralCount}} unread email."
other = "You have {{.PluralCount}} unread emails."
- Create an empty messages files for the language that you want to add (in this example,
translate.es.toml
andtranslate.ru.toml
).
touch translate.es.toml translate.ru.toml
- Run
goi18n merge
command with this messages files to be translated:
# For EspaΓ±ol:
goi18n merge active.en.toml translate.es.toml
# For Russian:
goi18n merge active.en.toml translate.ru.toml
- Open messages files and do the lines translation. As you remember from the beginning of this tutorial, the Russian language has its own peculiarities for displaying the number of objects. Therefore, I will give an example of translation for this language:
# ./translate.ru.toml
[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "ΠΡΠΈΠ²Π΅Ρ, {{.Name}}"
[MyUnreadEmails]
hash = "sha1-6a65d17f53981a3657db1897630e9cb069053ea8"
one = "Π£ Π²Π°Ρ Π΅ΡΡΡ {{.PluralCount}} Π½Π΅ΠΏΡΠΎΡΠΈΡΠ°Π½Π½ΠΎΠ΅ ΠΏΠΈΡΡΠΌΠΎ."
other = "Π£ Π²Π°Ρ Π΅ΡΡΡ {{.PluralCount}} Π½Π΅ΠΏΡΠΎΡΠΈΡΠ°Π½Π½ΡΡ
ΠΏΠΈΡΠ΅ΠΌ."
few = "Π£ Π²Π°Ρ Π΅ΡΡΡ {{.PluralCount}} Π½Π΅ΠΏΡΠΎΡΠΈΡΠ°Π½Π½ΡΡ
ΠΏΠΈΡΡΠΌΠ°." # <-- new row for "few" count
many = "Π£ Π²Π°Ρ Π΅ΡΡΡ {{.PluralCount}} Π½Π΅ΠΏΡΠΎΡΠΈΡΠ°Π½Π½ΡΡ
ΠΏΠΈΡΠ΅ΠΌ." # <-- new row for "many" count
When all have been translated, rename them to
active.es.toml
andactive.ru.toml
and place to the./lang
folder.That's it!
Launch the application and playing with languages
We're finally ready to launch our application:
go run main.go
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β Fiber v2.7.1 β
# β http://127.0.0.1:3000 β
# β (bound on host 0.0.0.0 and port 3000) β
# β β
# β Handlers ............. 2 Processes ........... 1 β
# β Prefork ....... Disabled PID ............. 64479 β
# βββββββββββββββββββββββββββββββββββββββββββββββββββββ
OK. Open http://localhost:3000/
page:
As you can see, by default the website will always open in π¬π§ English, as specified in the application settings.
π‘ In Golang unset
int
values will always have0
, notnull
orNone
as in JavaScript or Python. That's why if we don't specify theunread
parameter in a query, the template will be set it to0
.
Next, let's switch language to the πͺπΈ EspaΓ±ol. Click to the link at the page bottom and add query parameter unread
with some integer:
And, go to another language, π·πΊ Russian:
π You can play around with the value of
unread
to see how the word form automatically changes after a numeral for these languages.
Also, to demonstrate how JSON works, please add format=json
parameter to the query to see how Fiber web framework will give you the same content, but in JSON format:
Afterword
In real web applications, you can create different variants of REST API methods to deliver translations to the frontend. But the main thing to remember is that if you do international projects, think about the specifics of the language of those countries in the first place.
And Golang will help with everything else! π
Photos and videos by
- Vic ShΓ³stak https://shostak.dev
P.S.
If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! π»
βοΈ You can support me on Boosty, both on a permanent and on a one-time basis. All proceeds from this way will go to support my OSS projects and will energize me to create new products and articles for the community.
And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!
My main projects that need your help (and stars) π
- π₯ gowebly: A next-generation CLI tool that makes it easy to create amazing web applications with Go on the backend, using htmx, hyperscript or Alpine.js and the most popular CSS frameworks on the frontend.
- β¨ create-go-app: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
Posted on April 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 27, 2021