212 lines
6.1 KiB
Go
212 lines
6.1 KiB
Go
// Package lang introduces a translation and localisation API for Fyne applications
|
|
//
|
|
// Since 2.5
|
|
package lang
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
|
|
"github.com/jeandeaual/go-locale"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
|
|
"fyne.io/fyne/v2"
|
|
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
var (
|
|
// L is a shortcut to localize a string, similar to the gettext "_" function.
|
|
// More info available on the `Localize` function.
|
|
L = Localize
|
|
|
|
// N is a shortcut to localize a string with plural forms, similar to the ngettext function.
|
|
// More info available on the `LocalizePlural` function.
|
|
N = LocalizePlural
|
|
|
|
// X is a shortcut to get the localization of a string with specified key, similar to pgettext.
|
|
// More info available on the `LocalizeKey` function.
|
|
X = LocalizeKey
|
|
|
|
// XN is a shortcut to get the localization plural form of a string with specified key, similar to npgettext.
|
|
// More info available on the `LocalizePluralKey` function.
|
|
XN = LocalizePluralKey
|
|
|
|
bundle *i18n.Bundle
|
|
localizer *i18n.Localizer
|
|
setupOnce sync.Once
|
|
|
|
//go:embed translations
|
|
translations embed.FS
|
|
translated []language.Tag
|
|
)
|
|
|
|
// Localize asks the translation engine to translate a string, this behaves like the gettext "_" function.
|
|
// The string can be templated and the template data can be passed as a struct with exported fields,
|
|
// or as a map of string keys to any suitable value.
|
|
func Localize(in string, data ...any) string {
|
|
return LocalizeKey(in, in, data...)
|
|
}
|
|
|
|
// LocalizeKey asks the translation engine for the translation with specific ID.
|
|
// If it cannot be found then the fallback will be used.
|
|
// The string can be templated and the template data can be passed as a struct with exported fields,
|
|
// or as a map of string keys to any suitable value.
|
|
func LocalizeKey(key, fallback string, data ...any) string {
|
|
var d0 any
|
|
if len(data) > 0 {
|
|
d0 = data[0]
|
|
}
|
|
|
|
ret, err := localizer.Localize(&i18n.LocalizeConfig{
|
|
DefaultMessage: &i18n.Message{
|
|
ID: key,
|
|
Other: fallback,
|
|
},
|
|
TemplateData: d0,
|
|
})
|
|
|
|
if err != nil {
|
|
fyne.LogError("Translation failure", err)
|
|
return fallbackWithData(key, fallback, d0)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// LocalizePlural asks the translation engine to translate a string from one of a number of plural forms.
|
|
// This behaves like the ngettext function, with the `count` parameter determining the plurality looked up.
|
|
// The string can be templated and the template data can be passed as a struct with exported fields,
|
|
// or as a map of string keys to any suitable value.
|
|
func LocalizePlural(in string, count int, data ...any) string {
|
|
return LocalizePluralKey(in, in, count, data...)
|
|
}
|
|
|
|
// LocalizePluralKey asks the translation engine for the translation with specific ID in plural form.
|
|
// This behaves like the npgettext function, with the `count` parameter determining the plurality looked up.
|
|
// If it cannot be found then the fallback will be used.
|
|
// The string can be templated and the template data can be passed as a struct with exported fields,
|
|
// or as a map of string keys to any suitable value.
|
|
func LocalizePluralKey(key, fallback string, count int, data ...any) string {
|
|
var d0 any
|
|
if len(data) > 0 {
|
|
d0 = data[0]
|
|
}
|
|
|
|
ret, err := localizer.Localize(&i18n.LocalizeConfig{
|
|
DefaultMessage: &i18n.Message{
|
|
ID: key,
|
|
Other: fallback,
|
|
},
|
|
PluralCount: count,
|
|
TemplateData: d0,
|
|
})
|
|
|
|
if err != nil {
|
|
fyne.LogError("Translation failure", err)
|
|
return fallbackWithData(key, fallback, d0)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// AddTranslations allows an app to load a bundle of translations.
|
|
// The language that this relates to will be inferred from the resource name, for example "fr.json".
|
|
// The data should be in json format.
|
|
func AddTranslations(r fyne.Resource) error {
|
|
defer updateLocalizer()
|
|
return addLanguage(r.Content(), r.Name())
|
|
}
|
|
|
|
// AddTranslationsForLocale allows an app to load a bundle of translations for a specified locale.
|
|
// The data should be in json format.
|
|
func AddTranslationsForLocale(data []byte, l fyne.Locale) error {
|
|
defer updateLocalizer()
|
|
return addLanguage(data, l.String()+".json")
|
|
}
|
|
|
|
// AddTranslationsFS supports adding all translations in one calling using an `embed.FS` setup.
|
|
// The `dir` parameter specifies the name or path of the directory containing translation files
|
|
// inside this embedded filesystem.
|
|
// Each file should be a json file with the name following pattern [prefix.]lang.json.
|
|
func AddTranslationsFS(fs embed.FS, dir string) (retErr error) {
|
|
files, err := fs.ReadDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, f := range files {
|
|
name := f.Name()
|
|
data, err := fs.ReadFile(dir + "/" + name)
|
|
if err != nil {
|
|
if retErr == nil {
|
|
retErr = err
|
|
}
|
|
continue
|
|
}
|
|
|
|
err = addLanguage(data, name)
|
|
if err != nil {
|
|
if retErr == nil {
|
|
retErr = err
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
updateLocalizer()
|
|
|
|
return retErr
|
|
}
|
|
|
|
func addLanguage(data []byte, name string) error {
|
|
f, err := bundle.ParseMessageFileBytes(data, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
translated = append(translated, f.Tag)
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
bundle = i18n.NewBundle(language.English)
|
|
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
|
|
|
translated = []language.Tag{language.Make("en")} // the first item in this list will be the fallback if none match
|
|
err := AddTranslationsFS(translations, "translations")
|
|
if err != nil {
|
|
fyne.LogError("Error occurred loading built-in translations", err)
|
|
}
|
|
}
|
|
|
|
func fallbackWithData(key, fallback string, data any) string {
|
|
t, err := template.New(key).Parse(fallback)
|
|
if err != nil {
|
|
log.Println("Could not parse fallback template")
|
|
return fallback
|
|
}
|
|
str := &strings.Builder{}
|
|
_ = t.Execute(str, data)
|
|
return str.String()
|
|
}
|
|
|
|
// A utility for setting up languages - available to unit tests for overriding system
|
|
func setupLang(lang string) {
|
|
localizer = i18n.NewLocalizer(bundle, lang)
|
|
}
|
|
|
|
// updateLocalizer Finds the closest translation from the user's locale list and sets it up
|
|
func updateLocalizer() {
|
|
setupOnce.Do(initRuntime)
|
|
|
|
all, err := locale.GetLocales()
|
|
if err != nil {
|
|
fyne.LogError("Failed to load user locales", err)
|
|
all = []string{"en"}
|
|
}
|
|
setupLang(closestSupportedLocale(all).LanguageString())
|
|
}
|