/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 translator import ( "fmt" "os" "path/filepath" "sort" "strings" "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" "gopkg.in/yaml.v3" ) // ProviderSet is providers. var ProviderSet = wire.NewSet(NewTranslator) var GlobalTrans i18n.Translator // LangOption language option type LangOption struct { Label string `json:"label"` Value string `json:"value"` // Translation completion percentage Progress int `json:"progress"` } // DefaultLangOption default language option. If user config the language is default, the language option is admin choose. const DefaultLangOption = "Default" var ( // LanguageOptions language LanguageOptions []*LangOption ) // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { entries, err := os.ReadDir(c.BundleDir) if err != nil { return nil, err } // read the Bundle resources file from entries for _, file := range entries { // ignore directory if file.IsDir() { continue } // ignore non-YAML file if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" { continue } log.Debugf("try to read file: %s", file.Name()) buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name())) if err != nil { return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err) } // parse the backend translation originalTr := struct { Backend map[string]map[string]any `yaml:"backend"` UI map[string]any `yaml:"ui"` Plugin map[string]any `yaml:"plugin"` }{} if err = yaml.Unmarshal(buf, &originalTr); err != nil { return nil, err } translation := make(map[string]any, 0) for k, v := range originalTr.Backend { translation[k] = v } translation["backend"] = originalTr.Backend translation["ui"] = originalTr.UI translation["plugin"] = originalTr.Plugin content, err := yaml.Marshal(translation) if err != nil { log.Debugf("marshal translation content failed: %s %s", file.Name(), err) continue } // add translator use backend translation if err = myTran.AddTranslator(content, file.Name()); err != nil { log.Debugf("add translator failed: %s %s", file.Name(), err) reportTranslatorFormatError(file.Name(), buf) continue } } GlobalTrans = myTran.GlobalTrans i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml")) if err != nil { return nil, fmt.Errorf("read i18n file failed: %s", err) } s := struct { LangOption []*LangOption `yaml:"language_options"` }{} err = yaml.Unmarshal(i18nFile, &s) if err != nil { return nil, fmt.Errorf("i18n file parsing failed: %s", err) } LanguageOptions = s.LangOption for _, option := range LanguageOptions { option.Label = fmt.Sprintf("%s (%d%%)", option.Label, option.Progress) } return GlobalTrans, err } // CheckLanguageIsValid check user input language is valid func CheckLanguageIsValid(lang string) bool { if lang == DefaultLangOption { return true } for _, option := range LanguageOptions { if option.Value == lang { return true } } return false } // Tr use language to translate data. If this language translation is not available, return default english translation. func Tr(lang i18n.Language, data string) string { if GlobalTrans == nil { return data } translation := GlobalTrans.Tr(lang, data) if translation == data { return GlobalTrans.Tr(i18n.DefaultLanguage, data) } return translation } // TrWithData translate key with template data, it will replace the template data {{ .PlaceHolder }} in the translation. func TrWithData(lang i18n.Language, key string, templateData any) string { if GlobalTrans == nil { return key } translation := GlobalTrans.TrWithData(lang, key, templateData) if translation == key { return GlobalTrans.TrWithData(i18n.DefaultLanguage, key, templateData) } return translation } // reportTranslatorFormatError re-parses the YAML file to locate the invalid entry // when go-i18n fails to add the translator. func reportTranslatorFormatError(fileName string, content []byte) { var raw any if err := yaml.Unmarshal(content, &raw); err != nil { log.Errorf("parse translator file %s failed when diagnosing format error: %s", fileName, err) return } if err := inspectTranslatorNode(raw, nil, true); err != nil { log.Errorf("translator file %s invalid: %s", fileName, err) } } func inspectTranslatorNode(node any, path []string, isRoot bool) error { switch data := node.(type) { case nil: if isRoot { return fmt.Errorf("root value is empty") } return fmt.Errorf("%s contains an empty value", formatTranslationPath(path)) case string: if isRoot { return fmt.Errorf("root value must be an object but found string") } return nil case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: if isRoot { return fmt.Errorf("root value must be an object but found %T", data) } return fmt.Errorf("%s expects a string translation but found %T", formatTranslationPath(path), data) case map[string]any: if isMessageMap(data) { return nil } keys := make([]string, 0, len(data)) for key := range data { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { if err := inspectTranslatorNode(data[key], append(path, key), false); err != nil { return err } } return nil case map[string]string: mapped := make(map[string]any, len(data)) for k, v := range data { mapped[k] = v } return inspectTranslatorNode(mapped, path, isRoot) case map[any]any: if isMessageMap(data) { return nil } type kv struct { key string val any } items := make([]kv, 0, len(data)) for key, val := range data { strKey, ok := key.(string) if !ok { return fmt.Errorf("%s uses a non-string key %#v", formatTranslationPath(path), key) } items = append(items, kv{key: strKey, val: val}) } sort.Slice(items, func(i, j int) bool { return items[i].key < items[j].key }) for _, item := range items { if err := inspectTranslatorNode(item.val, append(path, item.key), false); err != nil { return err } } return nil case []any: for idx, child := range data { if err := inspectTranslatorNode(child, append(path, fmt.Sprintf("[%d]", idx)), false); err != nil { return err } } return nil case []map[string]any: for idx, child := range data { if err := inspectTranslatorNode(child, append(path, fmt.Sprintf("[%d]", idx)), false); err != nil { return err } } return nil default: if isRoot { return fmt.Errorf("root value must be an object but found %T", data) } return fmt.Errorf("%s contains unsupported value type %T", formatTranslationPath(path), data) } } var translatorReservedKeys = []string{ "id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other", } func isMessageMap(data any) bool { switch v := data.(type) { case map[string]any: for _, key := range translatorReservedKeys { val, ok := v[key] if !ok { continue } if _, ok := val.(string); ok { return true } } case map[string]string: for _, key := range translatorReservedKeys { val, ok := v[key] if !ok { continue } if val != "" { return true } } case map[any]any: for _, key := range translatorReservedKeys { val, ok := v[key] if !ok { continue } if _, ok := val.(string); ok { return true } } } return false } func formatTranslationPath(path []string) string { if len(path) == 0 { return "root" } var b strings.Builder for _, part := range path { if part == "" { continue } if part[0] == '[' { b.WriteString(part) continue } if b.Len() > 0 { b.WriteByte('.') } b.WriteString(part) } return b.String() }