diff options
author | Bjørn Erik Pedersen <[email protected]> | 2017-02-05 10:20:06 +0700 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2017-02-17 17:15:26 +0100 |
commit | 93ca7c9e958e34469a337e4efcc7c75774ec50fd (patch) | |
tree | 5dfa296cfe74fd5ef8f0d41ea4078704f453aa04 /i18n | |
parent | e34af6ee30f70f5780a281e2fd8f4ed9b487ee61 (diff) | |
download | hugo-93ca7c9e958e34469a337e4efcc7c75774ec50fd.tar.gz hugo-93ca7c9e958e34469a337e4efcc7c75774ec50fd.zip |
all: Refactor to nonglobal Viper, i18n etc.
This is a final rewrite that removes all the global state in Hugo, which also enables
the use if `t.Parallel` in tests.
Updates #2701
Fixes #3016
Diffstat (limited to 'i18n')
-rw-r--r-- | i18n/i18n.go | 96 | ||||
-rw-r--r-- | i18n/i18n_test.go | 155 | ||||
-rw-r--r-- | i18n/translationProvider.go | 73 |
3 files changed, 324 insertions, 0 deletions
diff --git a/i18n/i18n.go b/i18n/i18n.go new file mode 100644 index 000000000..ce268fac3 --- /dev/null +++ b/i18n/i18n.go @@ -0,0 +1,96 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18n + +import ( + "github.com/nicksnyder/go-i18n/i18n/bundle" + "github.com/spf13/hugo/config" + "github.com/spf13/hugo/helpers" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + i18nWarningLogger = helpers.NewDistinctFeedbackLogger() +) + +// Translator handles i18n translations. +type Translator struct { + translateFuncs map[string]bundle.TranslateFunc + cfg config.Provider + logger *jww.Notepad +} + +// NewTranslator creates a new Translator for the given language bundle and configuration. +func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *jww.Notepad) Translator { + t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]bundle.TranslateFunc)} + t.initFuncs(b) + return t +} + +// Func gets the translate func for the given language, or for the default +// configured language if not found. +func (t Translator) Func(lang string) bundle.TranslateFunc { + if f, ok := t.translateFuncs[lang]; ok { + return f + } + t.logger.WARN.Printf("Translation func for language %v not found, use default.", lang) + if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok { + return f + } + t.logger.WARN.Println("i18n not initialized, check that you have language file (in i18n) that matches the site language or the default language.") + return func(translationID string, args ...interface{}) string { + return "" + } + +} + +func (t Translator) initFuncs(bndl *bundle.Bundle) { + defaultContentLanguage := t.cfg.GetString("defaultContentLanguage") + var ( + defaultT bundle.TranslateFunc + err error + ) + + defaultT, err = bndl.Tfunc(defaultContentLanguage) + + if err != nil { + jww.WARN.Printf("No translation bundle found for default language %q", defaultContentLanguage) + } + + enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders") + for _, lang := range bndl.LanguageTags() { + currentLang := lang + + t.translateFuncs[currentLang] = func(translationID string, args ...interface{}) string { + tFunc, err := bndl.Tfunc(currentLang) + if err != nil { + jww.WARN.Printf("could not load translations for language %q (%s), will use default content language.\n", lang, err) + } else if translated := tFunc(translationID, args...); translated != translationID { + return translated + } + if t.cfg.GetBool("logI18nWarnings") { + i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLang, translationID) + } + if enableMissingTranslationPlaceholders { + return "[i18n] " + translationID + } + if defaultT != nil { + if translated := defaultT(translationID, args...); translated != translationID { + return translated + } + } + return "" + } + } +} diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go new file mode 100644 index 000000000..fd9c91b3e --- /dev/null +++ b/i18n/i18n_test.go @@ -0,0 +1,155 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18n + +import ( + "testing" + + "io/ioutil" + "os" + + "log" + + "github.com/nicksnyder/go-i18n/i18n/bundle" + "github.com/spf13/hugo/config" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +var logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + +type i18nTest struct { + data map[string][]byte + args interface{} + lang, id, expected, expectedFlag string +} + +var i18nTests = []i18nTest{ + // All translations present + { + data: map[string][]byte{ + "en.yaml": []byte("- id: \"hello\"\n translation: \"Hello, World!\""), + "es.yaml": []byte("- id: \"hello\"\n translation: \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in current language but present in default + { + data: map[string][]byte{ + "en.yaml": []byte("- id: \"hello\"\n translation: \"Hello, World!\""), + "es.yaml": []byte("- id: \"goodbye\"\n translation: \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "Hello, World!", + expectedFlag: "[i18n] hello", + }, + // Translation missing in default language but present in current + { + data: map[string][]byte{ + "en.yaml": []byte("- id: \"goodybe\"\n translation: \"Goodbye, World!\""), + "es.yaml": []byte("- id: \"hello\"\n translation: \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in both default and current language + { + data: map[string][]byte{ + "en.yaml": []byte("- id: \"goodbye\"\n translation: \"Goodbye, World!\""), + "es.yaml": []byte("- id: \"goodbye\"\n translation: \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Default translation file missing or empty + { + data: map[string][]byte{ + "en.yaml": []byte(""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Context provided + { + data: map[string][]byte{ + "en.yaml": []byte("- id: \"wordCount\"\n translation: \"Hello, {{.WordCount}} people!\""), + "es.yaml": []byte("- id: \"wordCount\"\n translation: \"¡Hola, {{.WordCount}} gente!\""), + }, + args: struct { + WordCount int + }{ + 50, + }, + lang: "es", + id: "wordCount", + expected: "¡Hola, 50 gente!", + expectedFlag: "¡Hola, 50 gente!", + }, +} + +func doTestI18nTranslate(t *testing.T, data map[string][]byte, lang, id string, args interface{}, cfg config.Provider) string { + i18nBundle := bundle.New() + + for file, content := range data { + err := i18nBundle.ParseTranslationFileBytes(file, content) + if err != nil { + t.Errorf("Error parsing translation file: %s", err) + } + } + + translator := NewTranslator(i18nBundle, cfg, logger) + + f := translator.Func(lang) + + translated := f(id, args) + + return translated +} + +func TestI18nTranslate(t *testing.T) { + var actual, expected string + v := viper.New() + v.SetDefault("defaultContentLanguage", "en") + + // Test without and with placeholders + for _, enablePlaceholders := range []bool{false, true} { + v.Set("enableMissingTranslationPlaceholders", enablePlaceholders) + + for _, test := range i18nTests { + if enablePlaceholders { + expected = test.expectedFlag + } else { + expected = test.expected + } + actual = doTestI18nTranslate(t, test.data, test.lang, test.id, test.args, v) + require.Equal(t, expected, actual) + } + } +} diff --git a/i18n/translationProvider.go b/i18n/translationProvider.go new file mode 100644 index 000000000..34558cffb --- /dev/null +++ b/i18n/translationProvider.go @@ -0,0 +1,73 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18n + +import ( + "fmt" + + "github.com/nicksnyder/go-i18n/i18n/bundle" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/source" +) + +// TranslationProvider provides translation handling, i.e. loading +// of bundles etc. +type TranslationProvider struct { + t Translator +} + +// NewTranslationProvider creates a new translation provider. +func NewTranslationProvider() *TranslationProvider { + return &TranslationProvider{} +} + +// Update updates the i18n func in the provided Deps. +func (tp *TranslationProvider) Update(d *deps.Deps) error { + dir := d.PathSpec.AbsPathify(d.Cfg.GetString("i18nDir")) + sp := source.NewSourceSpec(d.Cfg, d.Fs) + sources := []source.Input{sp.NewFilesystem(dir)} + + themeI18nDir, err := d.PathSpec.GetThemeI18nDirPath() + + if err == nil { + sources = []source.Input{sp.NewFilesystem(themeI18nDir), sources[0]} + } + + d.Log.DEBUG.Printf("Load I18n from %q", sources) + + i18nBundle := bundle.New() + + for _, currentSource := range sources { + for _, r := range currentSource.Files() { + err := i18nBundle.ParseTranslationFileBytes(r.LogicalName(), r.Bytes()) + if err != nil { + return fmt.Errorf("Failed to load translations in file %q: %s", r.LogicalName(), err) + } + } + } + + tp.t = NewTranslator(i18nBundle, d.Cfg, d.Log) + + d.Translate = tp.t.Func(d.Language.Lang) + + return nil + +} + +// Clone sets the language func for the new language. +func (tp *TranslationProvider) Clone(d *deps.Deps) error { + d.Translate = tp.t.Func(d.Language.Lang) + + return nil +} |