248 lines
7.6 KiB
Go
248 lines
7.6 KiB
Go
package i18n
|
|
|
|
import (
|
|
"fmt"
|
|
texttemplate "text/template"
|
|
|
|
"github.com/nicksnyder/go-i18n/v2/i18n/template"
|
|
"github.com/nicksnyder/go-i18n/v2/internal/plural"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
// Localizer provides Localize and MustLocalize methods that return localized messages.
|
|
// Localize and MustLocalize methods use a language.Tag matching algorithm based
|
|
// on the best possible value. This algorithm may cause an unexpected language.Tag returned
|
|
// value depending on the order of the tags stored in memory. For example, if the bundle
|
|
// used to create a Localizer instance ingested locales following this order
|
|
// ["en-US", "en-GB", "en-IE", "en"] and the locale "en" is asked, the underlying matching
|
|
// algorithm will return "en-US" thinking it is the best match possible. More information
|
|
// about the algorithm in this Github issue: https://github.com/golang/go/issues/49176.
|
|
// There is additionnal informations inside the Go code base:
|
|
// https://github.com/golang/text/blob/master/language/match.go#L142
|
|
type Localizer struct {
|
|
// bundle contains the messages that can be returned by the Localizer.
|
|
bundle *Bundle
|
|
|
|
// tags is the list of language tags that the Localizer checks
|
|
// in order when localizing a message.
|
|
tags []language.Tag
|
|
}
|
|
|
|
// NewLocalizer returns a new Localizer that looks up messages
|
|
// in the bundle according to the language preferences in langs.
|
|
// It can parse Accept-Language headers as defined in http://www.ietf.org/rfc/rfc2616.txt.
|
|
func NewLocalizer(bundle *Bundle, langs ...string) *Localizer {
|
|
return &Localizer{
|
|
bundle: bundle,
|
|
tags: parseTags(langs),
|
|
}
|
|
}
|
|
|
|
func parseTags(langs []string) []language.Tag {
|
|
tags := []language.Tag{}
|
|
for _, lang := range langs {
|
|
t, _, err := language.ParseAcceptLanguage(lang)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
tags = append(tags, t...)
|
|
}
|
|
return tags
|
|
}
|
|
|
|
// LocalizeConfig configures a call to the Localize method on Localizer.
|
|
type LocalizeConfig struct {
|
|
// MessageID is the id of the message to lookup.
|
|
// This field is ignored if DefaultMessage is set.
|
|
MessageID string
|
|
|
|
// TemplateData is the data passed when executing the message's template.
|
|
// If TemplateData is nil and PluralCount is not nil, then the message template
|
|
// will be executed with data that contains the plural count.
|
|
TemplateData interface{}
|
|
|
|
// PluralCount determines which plural form of the message is used.
|
|
PluralCount interface{}
|
|
|
|
// DefaultMessage is used if the message is not found in any message files.
|
|
DefaultMessage *Message
|
|
|
|
// Funcs is used to configure a template.TextParser if TemplateParser is not set.
|
|
Funcs texttemplate.FuncMap
|
|
|
|
// The TemplateParser to use for parsing templates.
|
|
// If one is not set, a template.TextParser is used (configured with Funcs if it is set).
|
|
TemplateParser template.Parser
|
|
}
|
|
|
|
var defaultTextParser = &template.TextParser{}
|
|
|
|
func (lc *LocalizeConfig) getTemplateParser() template.Parser {
|
|
if lc.TemplateParser != nil {
|
|
return lc.TemplateParser
|
|
}
|
|
if lc.Funcs != nil {
|
|
return &template.TextParser{
|
|
Funcs: lc.Funcs,
|
|
}
|
|
}
|
|
return defaultTextParser
|
|
}
|
|
|
|
type invalidPluralCountErr struct {
|
|
messageID string
|
|
pluralCount interface{}
|
|
err error
|
|
}
|
|
|
|
func (e *invalidPluralCountErr) Error() string {
|
|
return fmt.Sprintf("invalid plural count %#v for message id %q: %s", e.pluralCount, e.messageID, e.err)
|
|
}
|
|
|
|
// MessageNotFoundErr is returned from Localize when a message could not be found.
|
|
type MessageNotFoundErr struct {
|
|
Tag language.Tag
|
|
MessageID string
|
|
}
|
|
|
|
func (e *MessageNotFoundErr) Error() string {
|
|
return fmt.Sprintf("message %q not found in language %q", e.MessageID, e.Tag)
|
|
}
|
|
|
|
type messageIDMismatchErr struct {
|
|
messageID string
|
|
defaultMessageID string
|
|
}
|
|
|
|
func (e *messageIDMismatchErr) Error() string {
|
|
return fmt.Sprintf("message id %q does not match default message id %q", e.messageID, e.defaultMessageID)
|
|
}
|
|
|
|
// Localize returns a localized message.
|
|
func (l *Localizer) Localize(lc *LocalizeConfig) (string, error) {
|
|
msg, _, err := l.LocalizeWithTag(lc)
|
|
return msg, err
|
|
}
|
|
|
|
// LocalizeMessage returns a localized message.
|
|
func (l *Localizer) LocalizeMessage(msg *Message) (string, error) {
|
|
return l.Localize(&LocalizeConfig{
|
|
DefaultMessage: msg,
|
|
})
|
|
}
|
|
|
|
// TODO: uncomment this (and the test) when extract has been updated to extract these call sites too.
|
|
// Localize returns a localized message.
|
|
// func (l *Localizer) LocalizeMessageID(messageID string) (string, error) {
|
|
// return l.Localize(&LocalizeConfig{
|
|
// MessageID: messageID,
|
|
// })
|
|
// }
|
|
|
|
// LocalizeWithTag returns a localized message and the language tag.
|
|
// It may return a best effort localized message even if an error happens.
|
|
func (l *Localizer) LocalizeWithTag(lc *LocalizeConfig) (string, language.Tag, error) {
|
|
messageID := lc.MessageID
|
|
if lc.DefaultMessage != nil {
|
|
if messageID != "" && messageID != lc.DefaultMessage.ID {
|
|
return "", language.Und, &messageIDMismatchErr{messageID: messageID, defaultMessageID: lc.DefaultMessage.ID}
|
|
}
|
|
messageID = lc.DefaultMessage.ID
|
|
}
|
|
|
|
var operands *plural.Operands
|
|
templateData := lc.TemplateData
|
|
if lc.PluralCount != nil {
|
|
var err error
|
|
operands, err = plural.NewOperands(lc.PluralCount)
|
|
if err != nil {
|
|
return "", language.Und, &invalidPluralCountErr{messageID: messageID, pluralCount: lc.PluralCount, err: err}
|
|
}
|
|
if templateData == nil {
|
|
templateData = map[string]interface{}{
|
|
"PluralCount": lc.PluralCount,
|
|
}
|
|
}
|
|
}
|
|
|
|
tag, template, err := l.getMessageTemplate(messageID, lc.DefaultMessage)
|
|
if template == nil {
|
|
return "", language.Und, err
|
|
}
|
|
|
|
pluralForm := l.pluralForm(tag, operands)
|
|
templateParser := lc.getTemplateParser()
|
|
msg, err2 := template.execute(pluralForm, templateData, templateParser)
|
|
if err2 != nil {
|
|
if err == nil {
|
|
err = err2
|
|
}
|
|
|
|
// Attempt to fallback to "Other" pluralization in case translations are incomplete.
|
|
if pluralForm != plural.Other {
|
|
msg2, err3 := template.execute(plural.Other, templateData, templateParser)
|
|
if err3 == nil {
|
|
msg = msg2
|
|
}
|
|
}
|
|
}
|
|
return msg, tag, err
|
|
}
|
|
|
|
func (l *Localizer) getMessageTemplate(id string, defaultMessage *Message) (language.Tag, *MessageTemplate, error) {
|
|
_, i, _ := l.bundle.matcher.Match(l.tags...)
|
|
tag := l.bundle.tags[i]
|
|
mt := l.bundle.getMessageTemplate(tag, id)
|
|
if mt != nil {
|
|
return tag, mt, nil
|
|
}
|
|
|
|
if tag == l.bundle.defaultLanguage {
|
|
if defaultMessage == nil {
|
|
return language.Und, nil, &MessageNotFoundErr{Tag: tag, MessageID: id}
|
|
}
|
|
mt := NewMessageTemplate(defaultMessage)
|
|
if mt == nil {
|
|
return language.Und, nil, &MessageNotFoundErr{Tag: tag, MessageID: id}
|
|
}
|
|
return tag, mt, nil
|
|
}
|
|
|
|
// Fallback to default language in bundle.
|
|
mt = l.bundle.getMessageTemplate(l.bundle.defaultLanguage, id)
|
|
if mt != nil {
|
|
return l.bundle.defaultLanguage, mt, &MessageNotFoundErr{Tag: tag, MessageID: id}
|
|
}
|
|
|
|
// Fallback to default message.
|
|
if defaultMessage == nil {
|
|
return language.Und, nil, &MessageNotFoundErr{Tag: tag, MessageID: id}
|
|
}
|
|
return l.bundle.defaultLanguage, NewMessageTemplate(defaultMessage), &MessageNotFoundErr{Tag: tag, MessageID: id}
|
|
}
|
|
|
|
func (l *Localizer) pluralForm(tag language.Tag, operands *plural.Operands) plural.Form {
|
|
if operands == nil {
|
|
return plural.Other
|
|
}
|
|
return l.bundle.pluralRules.Rule(tag).PluralFormFunc(operands)
|
|
}
|
|
|
|
// MustLocalize is similar to Localize, except it panics if an error happens.
|
|
func (l *Localizer) MustLocalize(lc *LocalizeConfig) string {
|
|
localized, err := l.Localize(lc)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return localized
|
|
}
|
|
|
|
// MustLocalizeMessage is similar to LocalizeMessage, except it panics if an error happens.
|
|
func (l *Localizer) MustLocalizeMessage(msg *Message) string {
|
|
localized, err := l.LocalizeMessage(msg)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return localized
|
|
}
|