ctxi18n

package module
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 25, 2024 License: Apache-2.0 Imports: 4 Imported by: 1

README

ctxi18n

Lint Test Go Go Report Card codecov GoDoc Latest Tag

Go Context Internationalization - translating apps easily.

Introduction

ctxi18n is heavily influenced by internationalization in Ruby on Rails and aims to make it just as straightforward in Go applications.

As the name suggests, ctxi18n focusses on making i18n data available inside an application's context instances, but is sufficiently flexible to used directly if needed.

Key Features:

  • Loads locale files written in YAML or JSON with a similar structure those in Ruby i18n.
  • Makes it easy to add a locale object to the context.
  • Supports fs.FS to load data.
  • Short method names like i18n.T() or i18n.N().
  • Support for simple interpolation using keys, e.g. Some %{key} text
  • Support for pluralization rules.
  • Default values when translations are missing.

Usage

Import the library with:

import "github.com/invopop/ctxi18n"

First you'll need to load YAML or JSON translation definitions. Files may be named and structured however you like, but the contents must always follow the same pattern of language and properties, for example:

en:
  welcome:
    title: "Welcome to our application!"
    login: "Log in"
    signup: "Sign up"
    forgot-password: "Forgot Password?"
es:
  welcome:
    title: "¡Bienvenido a nuestra aplicación!"
    login: "Ingresarse"
    signup: "Registrarse"
    forgot-password: "¿Olvidaste tu contraseña?

The first level of properties of the object must always define the locale that the rest of sub-object's contents will provide translations for.

Files will all be deep-merged on top of each other so you can safely extend dictionaries from multiple sources.

To load the dictionary run something like the following where the asset.Content is a package containing embedded files:

if err := ctxi18n.Load(assets.Content); err != nil {
    panic(err)
}

If you'd like to set a default base language to try to use for any missing translations, load the assets with a default:

if err := ctxi18n.LoadWithDefault(assets.Content, "en"); err != nil {
    panic(err)
}

You'll now have a global set of locales prepared in memory and ready to use. Assuming your application uses some kind of context such as from an HTTP or gRPC request, you'll want to add a single locale to it:

ctx = ctxi18n.WithLocale(ctx, "en")

Locale selection is performed according to RFC9110 and the Accept-Language header, so you can pass in a code string and an attempt will be made to find the best match:

ctx = ctxi18n.WithLocale(ctx, "en-US,en;q=0.9,es;q=0.8")

In this example, the first locale to matched will be en-US, followed by just en, then es:

Getting translations is straightforward, you have two options:

  1. call methods defined in the package with the context, or,
  2. extract the locale from the context and use.

To translate without extracting the locale, you'll need to load the i18n package which contains all the structures and methods used by the main ctxi18n without any globals:

import "github.com/invopop/ctxi18n/i18n"

Then use it with the context:

fmt.Println(i18n.T(ctx, "welcome.title"))

Notice in the example that title was previously defined inside the welcome object in the source YAML, and we're accessing it here by defining the path welcome.title.

To use the Locale object directly, extract it from the context and call the methods:

l := ctxi18n.Locale(ctx)
fmt.Println(l.T("welcome.title"))

There is no preferred way on how to use this library, so please use whatever best first your application and coding style. Sometimes it makes sense to pass in the context in every call, other times the code can be shorter and more concise by extracting it.

Defaults

If a translation is missing from the locale a "missing" text will be produced, for example:

fmt.Println(l.T("welcome.no.text"))

Will return a text that follows the fmt.Sprintf missing convention:

!(MISSING welcome.no.text)

This can be useful for translators to figure out which texts are missing, but sometimes a default value is more appropriate:

fmt.Println(i18n.T(ctx, "welcome.question", i18n.Default("Just ask!")))
// output: "Just ask!"
code := "EUR"
fmt.Println(i18n.T(ctx, "currencies."+code, i18n.Default(code)))
// output: "EUR"
Interpolation

Go's default approach for interpolation using the fmt.Sprintf and related methods is good for simple use-cases. For example, given the following translation:

en:
  welcome:
    title: "Hi %s, welcome to our App!"

You can get the translated text and interpolate with:

i18n.T(ctx, "welcome.title", "Sam")

This however is an anti-pattern when it comes to translating applications as translators may need to change the order of replaced words. To get around this, ctxi18n supports simple named interpolation as follows:

en:
  welcome:
    title: "Hi %{name}, welcome to our App!"
i18n.T(ctx, "welcome.title", i18n.M{"name":"Sam"})

The i18n.M map is used to perform a simple find and replace on the matching translation. The fmt.Sprint method is used to convert values into strings, so you don't need to worry about simple serialization like for numbers.

Interpolation can also be used alongside default values:

i18n.T(ctx, "welcome.title", i18n.Default("Hi %{name}"), i18n.M{"name":"Sam"})

Pluralization

When texts include references to numbers we need internationalization libraries like ctxi18n that help define multiple possible translations according to a number. Pluralized translations are defined like this:

en:
  inbox:
    emails:
      zero: "You have no emails."
      one: "You have %{count} email."
      other: "You have %{count} emails.

The inbox.emails tag has a sub-object that defines all the translations we need according to the pluralization rules of the language. In the case of English which uses the default rule set, zero is an optional definition that will be used if provided and fallback on other if not.

To use these translations, call the i18n.N method:

count := 2
fmt.Println(i18n.N(ctx, "inbox.emails", count, i18n.M{"count": count}))

The output from this will be: "You have 2 emails."

In the current implementation of ctxi18n there are very few pluralization rules defined, please submit PRs if your language is not covered!

Scopes

As your application gets more complex, it can get repetitive having to use the same base keys. To get around this, use the WithScope helper method inside a context:

ctx := i18n.WithScope(ctx, "welcome")
i18n.T(ctx, ".title", i18n.M{"name":"Sam"})

Anything with the . at the beginning will append the scope. You can continue to use any other key in the locale by not using the . at the front.

Templ

Templ is a templating library that helps you create components that render fragments of HTML and compose them to create screens, pages, documents or apps.

The following "Hello World" example is taken from the Templ Guide and shows how you can quickly add translations the leverage the built-in ctx variable provided by Templ.

en:
  welcome:
    hello: "Hello, %{name}"
package main

import "github.com/invopop/ctxi18n/i18n"

templ Hello(name string) {
  <span class="hello">
    { i18n.T(ctx, "welcome.hello", i18n.M{"name": name}) }
  </span>
}

templ Greeting(person Person) {
  <div class="greeting">
    @Hello(person.Name)
  </div>
}

To save even more typing, it might be worth defining your own templ wrappers around those defined in the i18n package. Check out the gobl.html t package for an example.

Examples

The following is a list of Open Source projects using this library from which you can see working examples for your own solutions. Please send in a PR if you'd like to add your project!

  • GOBL HTML - generate HTML files like invoices from GOBL documents.

Documentation

Overview

Package ctxi18n is used to internationalize applications using the context for the locale.

Index

Constants

This section is empty.

Variables

View Source
var (
	// DefaultLocale defines the default or fallback locale code to use
	// if no other match inside the packages list was found.
	DefaultLocale i18n.Code = "en"
)
View Source
var (
	// ErrMissingLocale implies that the requested locale was not found
	// in the current index.
	ErrMissingLocale = errors.New("locale not defined")
)

Functions

func Get

func Get(code i18n.Code) *i18n.Locale

Get provides the Locale object for the matching code.

func Load

func Load(fs fs.FS) error

Load walks through all the files in provided File System and prepares an internal global list of locales ready to use.

func LoadWithDefault added in v0.6.0

func LoadWithDefault(fs fs.FS, locale i18n.Code) error

LoadWithDefault performs the regular load operation, but will merge the default locale with every other locale, ensuring that every text has at least the value from the default locale.

func Locale

func Locale(ctx context.Context) *i18n.Locale

Locale provides the locale object currently stored in the context.

func Match

func Match(locale string) *i18n.Locale

Match attempts to find the best possible matching locale based on the locale string provided. The locale string is parsed according to the "Accept-Language" header format defined in RFC9110.

func WithLocale

func WithLocale(ctx context.Context, locale string) (context.Context, error)

WithLocale tries to match the provided code with a locale and ensures it is available inside the context.

Types

This section is empty.

Directories

Path Synopsis
Package i18n is responsible for keeping the key internationalization in one place.
Package i18n is responsible for keeping the key internationalization in one place.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL