最近在开发一个需求时,需要将英文转为中文,这就需要进行本地化的处理,通过查找相关的库,决定使用 gettext-go 来进行本地化的处理,本篇文章主要简单介绍 gettext-go 和 它在k8s kubectl 中的运用。

gettext-go 简单介绍和使用

gettext-go 简单来说就是读取预先编写的 po或mo文件来进行本地化的处理.

po和mo文件是什么呢? 接下来介绍一下po和mo文件

po和mo文件介绍

.po文件,.mo文件是由gettext程序生成或者使用的源代码和编译结果。

1、.pot文件

是一种模板文件,其实质与.po文件一样,其中包含了从源代码中提取所有的翻译字符串的列表,主要提供给翻译人员使用。

2、.po文件

(1)用程序msginit来分析pot文件,生成各语言对应的po文件,比如中文就是zh_CN.po,法语就是fr.po文件。

(2)PO是Portable Object(可移植对象)的缩写形式,它是面向翻译人员的、提取于源代码的一种资源文件。

(3).po文件可以用任何编辑器如poEdit,vi,Emacs,editplus打开,交给翻译人员来将其中的文字翻译成本国语言。

3、.mo文件

(1)用msgfmt将.po文件编译成mo文件,这是一个二进制文件,不能直接编辑。

(2)MO是Machine Object(机器对象)的缩写形式,它是面向计算机的、由.po文件通过GNU gettext工具包编译而成的二进制文件,应用程序通过读取.mo文件使自身的界面转换成用户使用的语言,如简体中文。

(3)可以用工具如msgunfmt命令将.mo文件反编译为.po文件。

很多软件都是通过这些文件实现多语言的功能。

gettext-go 简单使用

package main

import (
	"fmt"

	"github.com/chai2010/gettext-go"
)

func main() {
	gettext := gettext.New("hello", "./examples/locale").SetLanguage("zh_CN")
	fmt.Println(gettext.Gettext("Hello, world!"))

	// Output: 你好, 世界!
}

这段代码主要就是读取预先定义的 po或mo文件,选择中文翻译,将 Hello, world! 转为中文。这就是这个库的主要功能,接下来我们来看看这个库在k8s中使用的例子。

k8s 中的使用

k8s 中使用gettext-go 的地方是在kubectl中,主要是命令行的本地化。先简单看一下代码。

// https://raw.githubusercontent.com/kubernetes/kubernetes/master/staging/src/k8s.io/kubectl/pkg/util/i18n/i18n.go
/*
Copyright 2016 The Kubernetes Authors.

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 (
	"archive/zip"
	"bytes"
	"embed"
	"errors"
	"fmt"
	"os"
	"strings"
	"sync"

	"github.com/chai2010/gettext-go"

	"k8s.io/klog/v2"
)

//go:embed translations
var translations embed.FS

var knownTranslations = map[string][]string{
	"kubectl": {
		"default",
		"en_US",
		"fr_FR",
		"zh_CN",
		"ja_JP",
		"zh_TW",
		"it_IT",
		"de_DE",
		"ko_KR",
		"pt_BR",
	},
	// only used for unit tests.
	"test": {
		"default",
		"en_US",
	},
}

var (
	lazyLoadTranslationsOnce sync.Once
	LoadTranslationsFunc     = func() error {
		return LoadTranslations("kubectl", nil)
	}
	translationsLoaded bool
)

// SetLoadTranslationsFunc sets the function called to lazy load translations.
// It must be called in an init() func that occurs BEFORE any i18n.T() calls are made by any package. You can
// accomplish this by creating a separate package containing your init() func, and then importing that package BEFORE
// any other packages that call i18n.T().
//
// Example Usage:
//
//	package myi18n
//
//	import "k8s.io/kubectl/pkg/util/i18n"
//
//	func init() {
//		if err := i18n.SetLoadTranslationsFunc(loadCustomTranslations); err != nil {
//			panic(err)
//		}
//	}
//
//	func loadCustomTranslations() error {
//		// Load your custom translations here...
//	}
//
// And then in your main or root command package, import your custom package like this:
//
//	import (
//		// Other imports that don't need i18n...
//		_ "example.com/myapp/myi18n"
//		// Other imports that do need i18n...
//	)
func SetLoadTranslationsFunc(f func() error) error {
	if translationsLoaded {
		return errors.New("translations have already been loaded")
	}
	LoadTranslationsFunc = func() error {
		if err := f(); err != nil {
			return err
		}
		translationsLoaded = true
		return nil
	}
	return nil
}

func loadSystemLanguage() string {
	// Implements the following locale priority order: LC_ALL, LC_MESSAGES, LANG
	// Similarly to: https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
	langStr := os.Getenv("LC_ALL")
	if langStr == "" {
		langStr = os.Getenv("LC_MESSAGES")
	}
	if langStr == "" {
		langStr = os.Getenv("LANG")
	}

	if langStr == "" {
		klog.V(3).Infof("Couldn't find the LC_ALL, LC_MESSAGES or LANG environment variables, defaulting to en_US")
		return "default"
	}
	pieces := strings.Split(langStr, ".")
	if len(pieces) != 2 {
		klog.V(3).Infof("Unexpected system language (%s), defaulting to en_US", langStr)
		return "default"
	}
	return pieces[0]
}

func findLanguage(root string, getLanguageFn func() string) string {
	langStr := getLanguageFn()

	translations := knownTranslations[root]
	for ix := range translations {
		if translations[ix] == langStr {
			return langStr
		}
	}
	klog.V(3).Infof("Couldn't find translations for %s, using default", langStr)
	return "default"
}

// LoadTranslations loads translation files. getLanguageFn should return a language
// string (e.g. 'en-US'). If getLanguageFn is nil, then the loadSystemLanguage function
// is used, which uses the 'LANG' environment variable.
func LoadTranslations(root string, getLanguageFn func() string) error {
	if getLanguageFn == nil {
		getLanguageFn = loadSystemLanguage
	}

	langStr := findLanguage(root, getLanguageFn)
	translationFiles := []string{
		fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.po", root, langStr),
		fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.mo", root, langStr),
	}

	klog.V(3).Infof("Setting language to %s", langStr)
	// TODO: list the directory and load all files.
	buf := new(bytes.Buffer)
	w := zip.NewWriter(buf)

	// Make sure to check the error on Close.
	for _, file := range translationFiles {
		filename := "translations/" + file
		f, err := w.Create(file)
		if err != nil {
			return err
		}
		data, err := translations.ReadFile(filename)
		if err != nil {
			return err
		}
		if _, err := f.Write(data); err != nil {
			return nil
		}
	}
	if err := w.Close(); err != nil {
		return err
	}
	gettext.BindLocale(gettext.New("k8s", root+".zip", buf.Bytes()))
	gettext.SetDomain("k8s")
	gettext.SetLanguage(langStr)
	translationsLoaded = true
	return nil
}

func lazyLoadTranslations() {
	lazyLoadTranslationsOnce.Do(func() {
		if translationsLoaded {
			return
		}
		if err := LoadTranslationsFunc(); err != nil {
			klog.Warning("Failed to load translations")
		}
	})
}

// T translates a string, possibly substituting arguments into it along
// the way. If len(args) is > 0, args1 is assumed to be the plural value
// and plural translation is used.
func T(defaultValue string, args ...int) string {
	lazyLoadTranslations()
	if len(args) == 0 {
		return gettext.PGettext("", defaultValue)
	}
	return fmt.Sprintf(gettext.PNGettext("", defaultValue, defaultValue+".plural", args[0]),
		args[0])
}

// Errorf produces an error with a translated error string.
// Substitution is performed via the `T` function above, following
// the same rules.
func Errorf(defaultValue string, args ...int) error {
	return errors.New(T(defaultValue, args...))
}

简单看一下代码可以看出,本地化资源文件是通过 go1.16新特性 embed 方式嵌入到了 translations 变量中。主要看几个外部方法:

一) SetLoadTranslationsFunc

这个方法主要就是开发者自定义加载语言文件的函数,如果翻译已经被加载,这个函数会返回一个错误。,通过注释可以得知,这个方法必须在 init() 方法中调用,

二) LoadTranslations

LoadTranslations 方法是默认的加载语言文件的方法,这个方法负责加载翻译文件。它首先尝试找到系统的语言或者是用户提供的语言,然后加载相应的翻译文件。

三)T

T 方法主要就是翻译函数,它会根据给定的 defaultValue 和参数来翻译字符串。

四) Errorf(defaultValue string, args …int) error:

这个函数和 T 类似,但是它返回一个新的错误,错误信息是翻译后的字符串。

参考