i18n

package module
v0.0.0-...-3e76e2a Latest Latest
Warning

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

Go to latest
Published: Dec 14, 2020 License: Apache-2.0 Imports: 19 Imported by: 3

README

i18n wip draft Travis-CI Go Report Card GoDoc

A library (ready) and a go (golang) generator (wip) which creates code based and type safe translation units.

milestones and road map

  • Android xml support
  • CLDR plural support
  • CLDR language tag support
  • support priority matching of wanted locales and available locales
  • dynamic fallthrough resources, if strings are missing
  • compile time checker for kind of value and placeholders
  • runtime checker for kind of value and placeholders
  • runtime checker for consistent placeholders across translations
  • type safe generator for accessor facade

library usage

  1. use the Android XML Format.
  2. import the i18n dependency go get github.com/golangee/i18n in your module.
  3. configuration and usage is as easy as this
        package main
    
        import "github.com/golangee/i18n"
    
        func main(){
           err := i18n.ImportFile(i18n.AndroidImporter{}, "en-US", "usecase/strings.xml")
           if err != nil {
              panic(err)
           }
    
           res := i18n.From("de-DE")   
    
           str, err := res.QuantityText("x_has_y_cats2", 1, "nick", 1)
           if err != nil {
              panic(err)
           }
    
           fmt.Println(str)
        }   
    

Popular existing libraries are go-18n or i18n4go. There is also a pending localization proposal.

goals and design decisions

The known tools are at their core simple libraries and fall entirely short when it comes to type safety. This can only be avoided by one of the following two approaches:

  1. create a linter which runs before any compilation and proofs that whatever text based solution you use, you have consistent translations (e.g. a translation for each key, equal placeholders and plurals for each key) and that you use the keys and formatting methods correctly and consistently (e.g. correct sprintf directives for correct types) OR
  2. create a generator which creates source code from your text based translation configuration and solve all the hassle using simply the type system of your programming language. Even if your language does not provide type safety, the generator can also provide the role of a linter.

The following decisions have been discussed

  1. A new tool should support go modules and go packages. Instead of writing the code first, we assume that it is equally fine or better to write a default translation first, to ensure that you have always a valid text at your hand.
  2. Every access should only be made by type safe accessors, which provides type safe parameters for ordered placeholders and pluralization.
  3. A good encapsulation strategy requires to put related things together, sometimes just on module level but in larger projects also per package level. So this applies also to translations, which may be scattered across packages to fit best to your divide and conquer strategy.
  4. However, scattering translation files wildly across a module, or even worse, across modules of modules, is probably not desirable for your translation (agency) process and perhaps not feasible at all, because you may be out of control of some modules. At best, you have to provide a single file per language in a common format and get the translated languages also back the same way.
  5. The conclusion is to have a single state of truth at the top of your root module, which aggregates and merges all translations together and is also the truth for the generated type safe accessors.
  6. A statically proofed translation cannot be guaranteed, if the values can be overridden after generation time. So there should be also a runtime checker at startup, because the trade of for a slower start is better than a malfunction or crash of your productive service.
  7. The value of introducing a central dependency to a translation dictionary is better than to expect that a developer is aware of registering each translatable package from unknown modules by hand. This can only be accomplished with a singleton.
  8. The supported file format must be a well known format, so that common translation software used by agencies can simply import and export them (see also for example available SDL Trados file formats). Obviously a custom JSON or even TOML format is usually a bad choice.

go generate usage

  1. use the Android XML Format. In contrast to the specification, the file name is important and must be prefixed with strings- and postfixed with the locale, e.g. mymodule/myusecase/strings-en-US.xml. For the default fallback language the name strings.xml is sufficient.
  2. import the i18n dependency go get github.com/golangee/i18n in your module.
  3. create a generator file, e.g. mymodule/gen/i18n.go
    package main
    
    import "github.com/golangee/i18n" 
    
    func main(){
       // invoke the generator in your current project. It will process the entire module.
       i18n.Bundle()
    }
    
  4. create a file in the root of your module, e.g. in myproject/gen.go
    package myproject
    
    //go:generate go run gen/i18n.go
    
  5. invoke go generate and you are done. For each file set within a package you have now a strings_gen.go file, which contains a Strings struct and an according constructor.

The example output for this example would be mymodule/myusecase/strings.go:

// Code generated by go generate; DO NOT EDIT.
// This file was generated by github.com/golangee/i18n

package example

import (
	"fmt"
	i18n "github.com/golangee/i18n"
)

func init() {
	var tag string

	// from strings-de-DE.xml
	tag = "de-DE"

	i18n.ImportValue(i18n.NewText(tag, "x_runs_around_Y_and_sings_z", "%[1]s runs around the %[2]s and sings %[3]s"))
	i18n.ImportValue(i18n.NewTextArray(tag, "selector_details_array2", "a", "b", "c", "d"))
	i18n.ImportValue(i18n.NewQuantityText(tag, "x_has_y_cats").One("%[1]s has %[2]d cat").Other("the owner of %[2]d cats is %[1]s").Other("the owner of %[2]d cats is %[1]s"))
	i18n.ImportValue(i18n.NewQuantityText(tag, "x_has_y_cats2").One("%[1]s has %[2]d cat2").Other("the owner of %[2]d cats2 is %[1]s").Other("the owner of %[2]d cats2 is %[1]s"))
	i18n.ImportValue(i18n.NewTextArray(tag, "selector_details_array", "first line", "second line", "third line", "fourth line"))
	i18n.ImportValue(i18n.NewText(tag, "app_name", "LeichteApp"))
	i18n.ImportValue(i18n.NewText(tag, "hello_world", "Hallo Welt"))
	i18n.ImportValue(i18n.NewText(tag, "hello_x", "Hello %s"))
	i18n.ImportValue(i18n.NewText(tag, "bad_0", "@ ? < & ' \" \" '"))
	i18n.ImportValue(i18n.NewText(tag, "bad_1", "hallo '"))

	// from strings_test.xml
	tag = "und"

	i18n.ImportValue(i18n.NewText(tag, "app_name", "EasyApp"))
	i18n.ImportValue(i18n.NewText(tag, "bad_0", "@ ? < & ' \" \" '"))
	i18n.ImportValue(i18n.NewText(tag, "bad_1", "hello '"))
	i18n.ImportValue(i18n.NewQuantityText(tag, "x_has_y_cats").One("%[1]s has %[2]d cat").Other("the owner of %[2]d cats is %[1]s").Other("the owner of %[2]d cats is %[1]s"))
	i18n.ImportValue(i18n.NewTextArray(tag, "selector_details_array", "first line", "second line", "third line", "fourth line"))
	i18n.ImportValue(i18n.NewText(tag, "hello_world", "Hello World"))
	i18n.ImportValue(i18n.NewText(tag, "hello_x", "Hello %s"))
	i18n.ImportValue(i18n.NewText(tag, "x_runs_around_Y_and_sings_z", "%[1]s runs around the %[2]s and sings %[3]s"))
	i18n.ImportValue(i18n.NewQuantityText(tag, "x_has_y_cats2").One("%[1]s has %[2]d cat2").Other("the owner of %[2]d cats2 is %[1]s").Other("the owner of %[2]d cats2 is %[1]s"))
	i18n.ImportValue(i18n.NewTextArray(tag, "selector_details_array2", "a", "b", "c", "d"))

}

// Resources wraps the package strings to get invoked safely.
type Resources struct {
	res *i18n.Resources
}

// NewResources creates a new localized resource instance.
func NewResources(locale string) Resources {
	return Resources{i18n.From(locale)}
}

// AppName returns a translated text for "EasyApp"
func (r Resources) AppName() string {
	str, err := r.res.Text("app_name")
	if err != nil {
		return fmt.Errorf("MISS!app_name: %w", err).Error()
	}
	return str
}

// Bad0 returns a translated text for "@ ? < & ' " " '"
func (r Resources) Bad0() string {
	str, err := r.res.Text("bad_0")
	if err != nil {
		return fmt.Errorf("MISS!bad_0: %w", err).Error()
	}
	return str
}

// Bad1 returns a translated text for "hello '"
func (r Resources) Bad1() string {
	str, err := r.res.Text("bad_1")
	if err != nil {
		return fmt.Errorf("MISS!bad_1: %w", err).Error()
	}
	return str
}

// HelloWorld returns a translated text for "Hello World"
func (r Resources) HelloWorld() string {
	str, err := r.res.Text("hello_world")
	if err != nil {
		return fmt.Errorf("MISS!hello_world: %w", err).Error()
	}
	return str
}

// HelloX returns a translated text for "Hello %s"
func (r Resources) HelloX(str0 string) string {
	str, err := r.res.Text("hello_x", str0)
	if err != nil {
		return fmt.Errorf("MISS!hello_x: %w", err).Error()
	}
	return str
}

// SelectorDetailsArray returns a translated text for "first line"
func (r Resources) SelectorDetailsArray() []string {
	str, err := r.res.TextArray("selector_details_array")
	if err != nil {
		return []string{fmt.Errorf("MISS!selector_details_array: %w", err).Error()}
	}
	return str
}

// SelectorDetailsArray2 returns a translated text for "a"
func (r Resources) SelectorDetailsArray2() []string {
	str, err := r.res.TextArray("selector_details_array2")
	if err != nil {
		return []string{fmt.Errorf("MISS!selector_details_array2: %w", err).Error()}
	}
	return str
}

// XHasYCats returns a translated text for "the owner of %[2]d cats is %[1]s"
func (r Resources) XHasYCats(quantity int, str0 string, num1 int) string {
	str, err := r.res.QuantityText("x_has_y_cats", quantity, str0, num1)
	if err != nil {
		return fmt.Errorf("MISS!x_has_y_cats: %w", err).Error()
	}
	return str
}

// XHasYCats2 returns a translated text for "the owner of %[2]d cats2 is %[1]s"
func (r Resources) XHasYCats2(quantity int, str0 string, num1 int) string {
	str, err := r.res.QuantityText("x_has_y_cats2", quantity, str0, num1)
	if err != nil {
		return fmt.Errorf("MISS!x_has_y_cats2: %w", err).Error()
	}
	return str
}

// XRunsAroundYAndSingsZ returns a translated text for "%[1]s runs around the %[2]s and sings %[3]s"
func (r Resources) XRunsAroundYAndSingsZ(str0 string, str1 string, str2 string) string {
	str, err := r.res.Text("x_runs_around_Y_and_sings_z", str0, str1, str2)
	if err != nil {
		return fmt.Errorf("MISS!x_runs_around_Y_and_sings_z: %w", err).Error()
	}
	return str
}

// FuncMap returns the named functions to be used with a template
func (r Resources) FuncMap() map[string]interface{} {
	m := make(map[string]interface{})
	m["AppName"] = r.AppName
	m["Bad0"] = r.Bad0
	m["Bad1"] = r.Bad1
	m["HelloWorld"] = r.HelloWorld
	m["HelloX"] = r.HelloX
	m["SelectorDetailsArray"] = r.SelectorDetailsArray
	m["SelectorDetailsArray2"] = r.SelectorDetailsArray2
	m["XHasYCats"] = r.XHasYCats
	m["XHasYCats2"] = r.XHasYCats2
	m["XRunsAroundYAndSingsZ"] = r.XRunsAroundYAndSingsZ
	return m
}


releases

No code has been written yet.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrTextNotFound = fmt.Errorf("string not found")

ErrTextNotFound is the sentinel error for a named string which is not available

Functions

func Bundle

func Bundle() error

Bundle (re)generates all localizations in the current working directory.

func Import

func Import(importer Importer, locale string, src io.Reader) error

Import takes the importer and locale and updates the according internal localization resources. The order of import is relevant, because it determines the fallback matching logic. Import your default fallback language first.

func ImportFile

func ImportFile(importer Importer, fname string) error

ImportFile is a convenience method for Import. It detects the locale from the file name

func ImportValue

func ImportValue(value Value)

ImportValue adds or replaces any existing value

func Locales

func Locales() []string

Locales returns all translated locales

func TranslationPriority

func TranslationPriority(locales ...string)

TranslationPriority updates the resolution order and removes unwanted translations. "und" is the undefined default locale.

func Validate

func Validate() error

Validates checks the current state of the global localizations to see if everything is fine. If no error is returned, you can be sure that at least every key is translated in every language and the printf directives are consistent with each other.

Types

type AndroidImporter

type AndroidImporter struct {
}

An AndroidImporter supports the android strings xml format with simple strings, interpolation, indices, plurals and arrays. However, references and indices with more than 9 not.

func (AndroidImporter) Import

func (a AndroidImporter) Import(dst *Resources, src io.Reader) error

Import tries to parse the src bytes and imports that into the given resources.

type ErrArrayCountMismatch

type ErrArrayCountMismatch struct {
	Value0 Value
	Count0 int
	Value1 Value
	Count1 int
}

ErrArrayCountMismatch indicates that two arrays must have the same amount of entries

func (ErrArrayCountMismatch) Error

func (e ErrArrayCountMismatch) Error() string

type ErrFormatSpecifierCountMismatch

type ErrFormatSpecifierCountMismatch struct {
	Value0 Value
	Specs0 []PrintfFormatSpecifier
	Value1 Value
	Specs1 []PrintfFormatSpecifier
}

ErrFormatSpecifierCountMismatch is used to indicates that the number printf formatting directives is different but they must be equal.

func (*ErrFormatSpecifierCountMismatch) Error

type ErrList

type ErrList struct {
	Errs []error
}

ErrList is a list of errors

func (ErrList) Error

func (e ErrList) Error() string

type ErrMissingValue

type ErrMissingValue struct {
	Value           Value
	MissingInLocale string
}

ErrMissingValue contains an example value and the resources which is missing it

func (ErrMissingValue) Error

func (e ErrMissingValue) Error() string

type ErrOtherMissing

type ErrOtherMissing struct {
	Value Value
}

ErrOtherMissing indicates a missing "other" value for a plural. You may omit everything else but other is the fallback and must not be empty at least.

func (ErrOtherMissing) Error

func (e ErrOtherMissing) Error() string

type ErrTypeMismatch

type ErrTypeMismatch struct {
	Value0 Value
	Value1 Value
}

ErrTypeMismatch contains two Values of two different values which have different types, which is not allowed.

func (ErrTypeMismatch) Error

func (e ErrTypeMismatch) Error() string

type ErrUnexpectedAmountOfFormatSpecifiers

type ErrUnexpectedAmountOfFormatSpecifiers struct {
	Value    Value
	Found    int
	Expected int
	Text     string
}

ErrUnexpectedAmountOfFormatSpecifiers indicates that a value has an unexpected amount of specifiers. E.g. arrays must not contain any specifiers.

func (*ErrUnexpectedAmountOfFormatSpecifiers) Error

type ErrVerbConflict

type ErrVerbConflict struct {
	Value0 Value
	Verb0  PrintfFormatSpecifier
	Value1 Value
	Verb1  PrintfFormatSpecifier
}

ErrVerbConflict is returned, if two strings have different verb specifiers for the same position

func (*ErrVerbConflict) Error

func (e *ErrVerbConflict) Error() string

type Importer

type Importer interface {
	// Import tries to parse the src bytes and imports that into the given resources.
	Import(dst *Resources, src io.Reader) error
}

An Importer parses data formats and imports that into resources.

type PluralBuilder

type PluralBuilder interface {
	Value
	Zero(text string) PluralBuilder
	One(text string) PluralBuilder
	Two(text string) PluralBuilder
	Few(text string) PluralBuilder
	Many(text string) PluralBuilder
	Other(text string) PluralBuilder
}

func NewQuantityText

func NewQuantityText(locale string, id string) PluralBuilder

type PrintfFormatSpecifier

type PrintfFormatSpecifier struct {
	Src      string // Src is the entire string
	Pos      int    // Pos is the index in src where this specifier is located
	End      int    // End is the end index in src where this specifier is located
	Index    int    // Index is the argument position. Either this is the natural order or derived by the indexed position.
	PosIndex int    // PosIndex is the positional index, which is the order as parsed
}

PrintFormatSpecifier represents a part of a printf string, like %s, %[5]d or %10.10s.

func ParsePrintf

func ParsePrintf(str string) []PrintfFormatSpecifier

ParsePrintf returns all found format specifiers and returns them in a sorted order by index position

func (*PrintfFormatSpecifier) String

func (f *PrintfFormatSpecifier) String() string

String returns the entire format specifier

func (*PrintfFormatSpecifier) Verb

func (f *PrintfFormatSpecifier) Verb() byte

Verb returns the single character and not the entire formatting directive.

type Resources

type Resources struct {
	// contains filtered or unexported fields
}

Resources is a type for accessing an applications text resources. It is safe to use concurrently.

func From

func From(locales ...string) *Resources

From returns the best matching text Resources to the given set of matching locales

func (*Resources) Keys

func (l *Resources) Keys() []string

Keys returns all available text resource keys

func (*Resources) QuantityText

func (l *Resources) QuantityText(id string, quantity int, args ...interface{}) (string, error)

QuantityText returns a translated and grammatically correct pluralization string or ErrTextNotFound

func (*Resources) Text

func (l *Resources) Text(id string, args ...interface{}) (string, error)

Text returns a translated string or ErrTextNotFound

func (*Resources) TextArray

func (l *Resources) TextArray(id string) ([]string, error)

TextArray returns a defensive copy of the according string array or ErrTextNotFound.

func (*Resources) Value

func (l *Resources) Value(key string) Value

Values returns the value for the key or nil

type Value

type Value interface {
	ID() string

	// TextArray returns the backing strings array
	TextArray() ([]string, error)

	// Text formats the value with the given arguments
	Text(args ...interface{}) (string, error)

	// QuantityText formats the value with the given arguments
	QuantityText(quantity int, args ...interface{}) (string, error)

	// Locale returns the CLDR language tag
	Locale() string
	// contains filtered or unexported methods
}

A Value is a contract which is implemented by each kind of message Value, like simple, array or plural.

func NewText

func NewText(locale string, id string, text string) Value

NewText returns a

func NewTextArray

func NewTextArray(locale string, id string, items ...string) Value

NewTextArray creates a new translated array value

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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