Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Commit

Permalink
First round of i18n in acs-engine based on gettext. Subsequent change…
Browse files Browse the repository at this point in the history
…s will replace and extract resource strings in acs-engine and make acsengine/api package's i18n used by other go program.
  • Loading branch information
JiangtianLi committed May 16, 2017
1 parent 8b365e0 commit ecdd9b9
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 11 deletions.
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ENV AZURE_CLI_VERSION 2.0.3

RUN apt-get update \
&& apt-get -y upgrade \
&& apt-get -y install python-pip make build-essential curl openssl vim jq \
&& apt-get -y install python-pip make build-essential curl openssl vim jq gettext \
&& rm -rf /var/lib/apt/lists/*

RUN mkdir /tmp/godeb \
Expand All @@ -29,6 +29,10 @@ RUN git clone https://github.com/akesterson/cmdarg.git /tmp/cmdarg \
RUN git clone https://github.com/akesterson/shunit.git /tmp/shunit \
&& cd /tmp/shunit && make install && rm -rf /tmp/shunit

# Go tool for internationalization and localization
RUN go get github.com/gosexy/gettext/... \
&& go install github.com/gosexy/gettext/...

# Used by some CI jobs
ADD ./test/bootstrap/checkout-pr.sh /tmp/checkout-pr.sh

Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ prereqs:
go get github.com/jteeuwen/go-bindata/...
go get github.com/Sirupsen/logrus
go get github.com/spf13/cobra
go get github.com/onsi/gomega
go get github.com/leonelquinteros/gotext

build: prereqs
go generate -v ./...
Expand Down
32 changes: 24 additions & 8 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

"github.com/Azure/acs-engine/pkg/acsengine"
"github.com/Azure/acs-engine/pkg/api"
"github.com/Azure/acs-engine/pkg/i18n"
"github.com/leonelquinteros/gotext"
)

const (
Expand All @@ -18,17 +20,19 @@ const (
)

type generateCmd struct {
apimodelPath string
outputDirectory string // can be auto-determined from clusterDefinition
caCertificatePath string
caPrivateKeyPath string
classicMode bool
noPrettyPrint bool
parametersOnly bool
translationsDirectory string
apimodelPath string
outputDirectory string // can be auto-determined from clusterDefinition
caCertificatePath string
caPrivateKeyPath string
classicMode bool
noPrettyPrint bool
parametersOnly bool

// Parsed from inputs
containerService *api.ContainerService
apiVersion string
locale *gotext.Locale
}

func NewGenerateCmd() *cobra.Command {
Expand All @@ -44,6 +48,7 @@ func NewGenerateCmd() *cobra.Command {
}

f := generateCmd.Flags()
f.StringVar(&gc.translationsDirectory, "translations-directory", "", "translations directory (translations in the current directory if absent)")
f.StringVar(&gc.apimodelPath, "api-model", "", "")
f.StringVar(&gc.outputDirectory, "output-directory", "", "output directory (derived from FQDN if absent)")
f.StringVar(&gc.caCertificatePath, "ca-certificate-path", "", "path to the CA certificate to use for Kubernetes PKI assets")
Expand All @@ -59,6 +64,13 @@ func (gc *generateCmd) validate(cmd *cobra.Command, args []string) {
var caCertificateBytes []byte
var caKeyBytes []byte

locale, err := i18n.LoadTranslations(gc.translationsDirectory)
if err != nil {
log.Fatalf("error loading translation files: %s", err.Error())
}

i18n.Initialize(locale)

if gc.apimodelPath == "" {
if len(args) > 0 {
gc.apimodelPath = args[0]
Expand Down Expand Up @@ -93,13 +105,17 @@ func (gc *generateCmd) validate(cmd *cobra.Command, args []string) {

gc.containerService = containerService
gc.apiVersion = apiVersion
gc.locale = locale
}

func (gc *generateCmd) run(cmd *cobra.Command, args []string) error {
gc.validate(cmd, args)
log.Infoln("Generating...")

templateGenerator, err := acsengine.InitializeTemplateGenerator(gc.classicMode)
ctx := acsengine.Context{
Locale: gc.locale,
}
templateGenerator, err := acsengine.InitializeTemplateGenerator(ctx, gc.classicMode)
if err != nil {
log.Fatalln("failed to initialize template generator: %s", err.Error())
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/acsengine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,14 @@ func (t *TemplateGenerator) verifyFiles() error {
// TemplateGenerator represents the object that performs the template generation.
type TemplateGenerator struct {
ClassicMode bool
Context Context
}

// InitializeTemplateGenerator creates a new template generator object
func InitializeTemplateGenerator(classicMode bool) (*TemplateGenerator, error) {
func InitializeTemplateGenerator(ctx Context, classicMode bool) (*TemplateGenerator, error) {
t := &TemplateGenerator{
ClassicMode: classicMode,
Context: ctx,
}

if err := t.verifyFiles(); err != nil {
Expand Down
12 changes: 11 additions & 1 deletion pkg/acsengine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import (
"bytes"
"fmt"
"io/ioutil"
"path"
"path/filepath"
"strings"
"testing"

"github.com/Azure/acs-engine/pkg/api"
"github.com/Azure/acs-engine/pkg/api/v20160330"
"github.com/Azure/acs-engine/pkg/api/vlabs"
"github.com/Azure/acs-engine/pkg/i18n"
"github.com/leonelquinteros/gotext"
)

const (
TestDataDir = "./testdata"
)

func TestExpected(t *testing.T) {
// Initialize locale for translation
locale := gotext.NewLocale(path.Join("..", "..", "translations"), "en_US")
i18n.Initialize(locale)

// iterate the test data directory
apiModelTestFiles := &[]APIModelTestFile{}
if e := IterateTestFilesDirectory(TestDataDir, apiModelTestFiles); e != nil {
Expand Down Expand Up @@ -50,7 +57,10 @@ func TestExpected(t *testing.T) {
// 1. first time tests loaded containerService
// 2. second time tests generated containerService
// 3. third time tests the generated containerService from the generated containerService
templateGenerator, e3 := InitializeTemplateGenerator(isClassicMode)
ctx := Context{
Locale: locale,
}
templateGenerator, e3 := InitializeTemplateGenerator(ctx, isClassicMode)
if e3 != nil {
t.Error(e3.Error())
continue
Expand Down
6 changes: 6 additions & 0 deletions pkg/acsengine/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/Azure/acs-engine/pkg/api"
"github.com/Azure/acs-engine/pkg/api/v20160330"
"github.com/Azure/acs-engine/pkg/api/vlabs"
"github.com/leonelquinteros/gotext"
)

// DCOSNodeType represents the type of DCOS Node
Expand Down Expand Up @@ -51,3 +52,8 @@ type AzureEnvironmentSpecConfig struct {
KubernetesSpecConfig KubernetesSpecConfig
DCOSSpecConfig DCOSSpecConfig
}

// Context represents the object that is passed to the package
type Context struct {
Locale *gotext.Locale
}
107 changes: 107 additions & 0 deletions pkg/i18n/i18n.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package i18n

import (
"errors"
"fmt"
"os"
"strings"

"github.com/leonelquinteros/gotext"
)

const (
defaultLanguage = "default"
defaultDomain = "acsengine"
defaultLocalDir = "translations"
)

var supportedTranslations = map[string]bool{
defaultLanguage: true,
"en_US": true,
"zh_CN": true,
}

func loadSystemLanguage() string {
language := os.Getenv("LANG")
if language == "" {
return defaultLanguage
}

// Posix locale name usually has the ll_CC.encoding syntax.
parts := strings.Split(language, ".")
if len(parts) == 0 {
return defaultLanguage
}
return parts[0]
}

// LoadTranslations loads translation files and sets the locale to
// the system locale. It should be called by the main program.
func LoadTranslations(translationsDir string) (*gotext.Locale, error) {
dir := defaultLocalDir
if translationsDir != "" {
dir = translationsDir
}

if stat, err := os.Stat(dir); os.IsNotExist(err) || !stat.IsDir() {
return nil, fmt.Errorf("Translations directory %s does not exist: %v", dir, err)
}

lang := loadSystemLanguage()
SetLanguage(lang)

locale := gotext.NewLocale(dir, lang)
Initialize(locale)

return locale, nil
}

// Initialize is the translation initialization function shared by the main program and package.
func Initialize(locale *gotext.Locale) error {
if locale == nil {
return fmt.Errorf("Initialize expected locale but got nil")
}
locale.AddDomain(defaultDomain)
return nil
}

// SetLanguage sets the program's current locale. If the language is not
// supported, then the default locale is used.
func SetLanguage(language string) {
if _, ok := supportedTranslations[language]; ok {
gotext.SetLanguage(language)
return
}
gotext.SetLanguage(defaultLanguage)
}

// GetLanguage queries the program's current locale.
func GetLanguage() string {
return gotext.GetLanguage()
}

// Translator is a wrapper over gotext's Locale and provides interface to
// translate text string and produce translated error
type Translator struct {
Locale *gotext.Locale
}

// T translates a text string, based on GNU's gettext library.
func (t *Translator) T(msgid string, vars ...interface{}) string {
return t.Locale.GetD(defaultDomain, msgid, vars...)
}

// NT translates a text string into the appropriate plural form, based on GNU's gettext library.
func (t *Translator) NT(msgid, msgidPlural string, n int, vars ...interface{}) string {
return t.Locale.GetND(defaultDomain, msgid, msgidPlural, n, vars...)
}

// Errorf produces an error with a translated error string.
func (t *Translator) Errorf(msgid string, vars ...interface{}) error {
return errors.New(t.T(msgid, vars...))
}

// NErrorf produces an error with a translated error string in the appropriate plural form.
func (t *Translator) NErrorf(msgid, msgidPlural string, n int, vars ...interface{}) error {
return errors.New(t.NT(msgid, msgidPlural, n, vars...))
}
81 changes: 81 additions & 0 deletions pkg/i18n/i18n_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package i18n

import (
"os"
"path"
"testing"

. "github.com/onsi/gomega"
)

func TestLoadTranslations(t *testing.T) {
RegisterTestingT(t)

_, err := LoadTranslations(path.Join("..", "..", "translations", "test"))
Expect(err).Should(BeNil())

_, err = LoadTranslations("non_existing_directory")
Expect(err).ShouldNot(BeNil())
}

func TestTranslationLanguage(t *testing.T) {
RegisterTestingT(t)

origLang := os.Getenv("LANG")
os.Setenv("LANG", "en_US.UTF-8")
_, err := LoadTranslations(path.Join("..", "..", "translations", "test"))
Expect(err).Should(BeNil())

lang := GetLanguage()
Expect(lang).Should(Equal("en_US"))

os.Setenv("LANG", origLang)
}

func TestTranslationLanguageDefault(t *testing.T) {
RegisterTestingT(t)

origLang := os.Getenv("LANG")
os.Setenv("LANG", "ll_CC.UTF-8")
_, err := LoadTranslations(path.Join("..", "..", "translations", "test"))
Expect(err).Should(BeNil())

lang := GetLanguage()
Expect(lang).Should(Equal("default"))

os.Setenv("LANG", origLang)
}

func TestTranslations(t *testing.T) {
RegisterTestingT(t)

l, err := LoadTranslations(path.Join("..", "..", "translations", "test"))
Expect(err).Should(BeNil())

translator := &Translator{
Locale: l,
}

msg := translator.T("Aloha")
Expect(msg).Should(Equal("Aloha"))

msg = translator.T("Hello %s", "World")
Expect(msg).Should(Equal("Hello World"))
}

func TestTranslationsPlural(t *testing.T) {
RegisterTestingT(t)

l, err := LoadTranslations(path.Join("..", "..", "translations", "test"))
Expect(err).Should(BeNil())

translator := &Translator{
Locale: l,
}

msg := translator.NT("There is %d parameter in resource %s", "There are %d parameters in resource %s", 1, 1, "Foo")
Expect(msg).Should(Equal("There is 1 parameter in resource Foo"))

msg = translator.NT("There is %d parameter in resource %s", "There are %d parameters in resource %s", 9, 9, "Foo")
Expect(msg).Should(Equal("There are 9 parameters in resource Foo"))
}
Loading

0 comments on commit ecdd9b9

Please sign in to comment.