1/*
2Copyright 2016 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package i18n
18
19import (
20	"archive/zip"
21	"bytes"
22	"embed"
23	"errors"
24	"fmt"
25	"os"
26	"strings"
27
28	"github.com/chai2010/gettext-go/gettext"
29	"k8s.io/klog/v2"
30)
31
32//go:embed translations
33var translations embed.FS
34
35var knownTranslations = map[string][]string{
36	"kubectl": {
37		"default",
38		"en_US",
39		"fr_FR",
40		"zh_CN",
41		"ja_JP",
42		"zh_TW",
43		"it_IT",
44		"de_DE",
45		"ko_KR",
46		"pt_BR",
47	},
48	// only used for unit tests.
49	"test": {
50		"default",
51		"en_US",
52	},
53}
54
55func loadSystemLanguage() string {
56	// Implements the following locale priority order: LC_ALL, LC_MESSAGES, LANG
57	// Similarly to: https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
58	langStr := os.Getenv("LC_ALL")
59	if langStr == "" {
60		langStr = os.Getenv("LC_MESSAGES")
61	}
62	if langStr == "" {
63		langStr = os.Getenv("LANG")
64	}
65
66	if langStr == "" {
67		klog.V(3).Infof("Couldn't find the LC_ALL, LC_MESSAGES or LANG environment variables, defaulting to en_US")
68		return "default"
69	}
70	pieces := strings.Split(langStr, ".")
71	if len(pieces) != 2 {
72		klog.V(3).Infof("Unexpected system language (%s), defaulting to en_US", langStr)
73		return "default"
74	}
75	return pieces[0]
76}
77
78func findLanguage(root string, getLanguageFn func() string) string {
79	langStr := getLanguageFn()
80
81	translations := knownTranslations[root]
82	for ix := range translations {
83		if translations[ix] == langStr {
84			return langStr
85		}
86	}
87	klog.V(3).Infof("Couldn't find translations for %s, using default", langStr)
88	return "default"
89}
90
91// LoadTranslations loads translation files. getLanguageFn should return a language
92// string (e.g. 'en-US'). If getLanguageFn is nil, then the loadSystemLanguage function
93// is used, which uses the 'LANG' environment variable.
94func LoadTranslations(root string, getLanguageFn func() string) error {
95	if getLanguageFn == nil {
96		getLanguageFn = loadSystemLanguage
97	}
98
99	langStr := findLanguage(root, getLanguageFn)
100	translationFiles := []string{
101		fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.po", root, langStr),
102		fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.mo", root, langStr),
103	}
104
105	klog.V(3).Infof("Setting language to %s", langStr)
106	// TODO: list the directory and load all files.
107	buf := new(bytes.Buffer)
108	w := zip.NewWriter(buf)
109
110	// Make sure to check the error on Close.
111	for _, file := range translationFiles {
112		filename := "translations/" + file
113		f, err := w.Create(file)
114		if err != nil {
115			return err
116		}
117		data, err := translations.ReadFile(filename)
118		if err != nil {
119			return err
120		}
121		if _, err := f.Write(data); err != nil {
122			return nil
123		}
124	}
125	if err := w.Close(); err != nil {
126		return err
127	}
128	gettext.BindTextdomain("k8s", root+".zip", buf.Bytes())
129	gettext.Textdomain("k8s")
130	gettext.SetLocale(langStr)
131	return nil
132}
133
134// T translates a string, possibly substituting arguments into it along
135// the way. If len(args) is > 0, args1 is assumed to be the plural value
136// and plural translation is used.
137func T(defaultValue string, args ...int) string {
138	if len(args) == 0 {
139		return gettext.PGettext("", defaultValue)
140	}
141	return fmt.Sprintf(gettext.PNGettext("", defaultValue, defaultValue+".plural", args[0]),
142		args[0])
143}
144
145// Errorf produces an error with a translated error string.
146// Substitution is performed via the `T` function above, following
147// the same rules.
148func Errorf(defaultValue string, args ...int) error {
149	return errors.New(T(defaultValue, args...))
150}
151