chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
// I18n i18n config
|
||||
type I18n struct {
|
||||
BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir" yaml:"bundle_dir"`
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
Reference in New Issue
Block a user