mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
178 lines
4.6 KiB
Go
178 lines
4.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package i18n
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"html/template"
|
|
"slices"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/log"
|
|
)
|
|
|
|
// This file implements the static LocaleStore that will not watch for changes
|
|
|
|
type locale struct {
|
|
store *localeStore
|
|
langName string
|
|
idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
|
|
}
|
|
|
|
var _ Locale = (*locale)(nil)
|
|
|
|
type localeStore struct {
|
|
// After initializing has finished, these fields are read-only.
|
|
langNames []string
|
|
langDescs []string
|
|
|
|
localeMap map[string]*locale
|
|
trKeyToIdxMap map[string]int
|
|
|
|
defaultLang string
|
|
}
|
|
|
|
// NewLocaleStore creates a static locale store
|
|
func NewLocaleStore() LocaleStore {
|
|
return &localeStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)}
|
|
}
|
|
|
|
// AddLocaleByJSON adds locale by JSON into the store
|
|
func (store *localeStore) AddLocaleByJSON(langName, langDesc string, source, moreSource []byte) error {
|
|
if _, ok := store.localeMap[langName]; ok {
|
|
return errors.New("lang has already been added")
|
|
}
|
|
|
|
store.langNames = append(store.langNames, langName)
|
|
store.langDescs = append(store.langDescs, langDesc)
|
|
|
|
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)}
|
|
store.localeMap[l.langName] = l
|
|
|
|
addFunc := func(source []byte) error {
|
|
if len(source) == 0 {
|
|
return nil
|
|
}
|
|
|
|
values := make(map[string]any)
|
|
if err := json.Unmarshal(source, &values); err != nil {
|
|
return fmt.Errorf("unable to load json: %w", err)
|
|
}
|
|
for trKey, value := range values {
|
|
switch v := value.(type) {
|
|
case string:
|
|
idx, ok := store.trKeyToIdxMap[trKey]
|
|
if !ok {
|
|
idx = len(store.trKeyToIdxMap)
|
|
store.trKeyToIdxMap[trKey] = idx
|
|
}
|
|
l.idxToMsgMap[idx] = v
|
|
case map[string]any:
|
|
for key, val := range v {
|
|
idx, ok := store.trKeyToIdxMap[trKey+"."+key]
|
|
if !ok {
|
|
idx = len(store.trKeyToIdxMap)
|
|
store.trKeyToIdxMap[trKey+"."+key] = idx
|
|
}
|
|
l.idxToMsgMap[idx] = val.(string)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported value type %T for key %q", v, trKey)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := addFunc(source); err != nil {
|
|
return fmt.Errorf("unable to load json: %w", err)
|
|
}
|
|
if err := addFunc(moreSource); err != nil {
|
|
return fmt.Errorf("unable to load json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *localeStore) HasLang(langName string) bool {
|
|
_, ok := store.localeMap[langName]
|
|
return ok
|
|
}
|
|
|
|
func (store *localeStore) ListLangNameDesc() (names, desc []string) {
|
|
return store.langNames, store.langDescs
|
|
}
|
|
|
|
// SetDefaultLang sets default language as a fallback
|
|
func (store *localeStore) SetDefaultLang(lang string) {
|
|
store.defaultLang = lang
|
|
}
|
|
|
|
// Locale returns the locale for the lang or the default language
|
|
func (store *localeStore) Locale(lang string) (Locale, bool) {
|
|
l, found := store.localeMap[lang]
|
|
if !found {
|
|
var ok bool
|
|
l, ok = store.localeMap[store.defaultLang]
|
|
if !ok {
|
|
// no default - return an empty locale
|
|
l = &locale{store: store, idxToMsgMap: make(map[int]string)}
|
|
}
|
|
}
|
|
return l, found
|
|
}
|
|
|
|
func (store *localeStore) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (l *locale) TrString(trKey string, trArgs ...any) string {
|
|
var format string
|
|
idx, ok := l.store.trKeyToIdxMap[trKey]
|
|
if ok {
|
|
format = l.idxToMsgMap[idx]
|
|
if format == "" { // missing translation in this locale, fallback to default
|
|
if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
|
|
// try to use default locale's translation
|
|
format = def.idxToMsgMap[idx]
|
|
}
|
|
}
|
|
}
|
|
if format == "" { // still missing, use the key itself
|
|
format = html.EscapeString(trKey)
|
|
}
|
|
msg, err := Format(format, trArgs...)
|
|
if err != nil {
|
|
log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
|
|
args := slices.Clone(trArgs)
|
|
for i, v := range args {
|
|
switch v := v.(type) {
|
|
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
|
|
// for most basic types (including template.HTML which is safe), just do nothing and use it
|
|
case string:
|
|
args[i] = template.HTMLEscapeString(v)
|
|
case fmt.Stringer:
|
|
args[i] = template.HTMLEscapeString(v.String())
|
|
default:
|
|
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
|
|
}
|
|
}
|
|
return template.HTML(l.TrString(trKey, args...))
|
|
}
|
|
|
|
// HasKey returns whether a key is present in this locale or not
|
|
func (l *locale) HasKey(trKey string) bool {
|
|
idx, ok := l.store.trKeyToIdxMap[trKey]
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, ok = l.idxToMsgMap[idx]
|
|
return ok
|
|
}
|