summaryrefslogtreecommitdiff
path: root/l10n/translator.go
diff options
from:
to:
context:
space:
mode:
authorGravatar Saya Andy <145215889+SayaAndy@users.noreply.github.com> 2026-05-18 01:33:14 +0700
committerGravatar GitHub <noreply@github.com> 2026-05-18 01:33:14 +0700
commit2c9d7e7c3110153c025c5324e0c4cc28d58cd2c1 (patch)
tree7d091ef937b8389d72ca3b0d28a6abaefb9c083e /l10n/translator.go
parent55f0ca98d42b3d540870dc1c9430b9a26ff19807 (diff)
parenta43cbea2e501371390709005055c019459554bb2 (diff)
downloadweb-0.16.0.tar.gz
web-0.16.0.zip
v0.16.0 (#40)v0.16.0
* [feat: all color themes now have light & dark variations depending on preference](https://github.com/SayaAndy/saya-today-web/commit/44b72cbc80fb89788470bda75eeb9c0bb3dee2f5) * [feat: use self-hosted pmtiles for map](https://github.com/SayaAndy/saya-today-web/commit/fa52ed00e8ef3303f14d7c953f1591eb19042635) * [feat: subheader for blog pages with medley-related ones](https://github.com/SayaAndy/saya-today-web/commit/c07da125bc5b0bb65f58c4edce912874e46f6ed8) * feat: color all leaflet elements depending on color theme & scheme * [feat: combined map widget](https://github.com/SayaAndy/saya-today-web/pull/40/commits/6418da32d978a8b70d38f1911a154e4dcc33202e) * feat: migrate static & photos to gcore cdn * [refactor: rewrite localization for dynamic set and get](https://github.com/SayaAndy/saya-today-web/commit/e9353a66bf51486f702eaa0f591dea54c9bd4cd1) * [feat: highlight the blog page visited earlier in blog catalogue](https://github.com/SayaAndy/saya-today-web/commit/ed476a1f0d3f970f977626688ac316f8b251d3a0) * fix: inline shadow now sticks to scrolling * [feat: take all metadata from centralized index.json](https://github.com/SayaAndy/saya-today-web/commit/1c441f5eab9a2a24fe5309178ce010f32adac1c7) * [fix: font in like button on smartphone screens](https://github.com/SayaAndy/saya-today-web/commit/551295ecacebbb94019def022f70397f74c44775) * [feat: use "page // medley" format for title](https://github.com/SayaAndy/saya-today-web/pull/40/commits/0dbb85f25c69b5e7929bb832e7967a709a76ff5c) * feat: multitone backgrounds implementation * feat: change font to m-plus for short description * fix: theme wheel position on different dpi screens in global map * feat: integrate package minor updates * refactor: convert color vars into oklch * [refactor: rm scanning by listing for s3 client](https://github.com/SayaAndy/saya-today-web/commit/1a95e20cc4e050244a76a3ea4cccc5ab30ff5a91) * fix: on errors with blog client init write correct type
Diffstat (limited to 'l10n/translator.go')
-rw-r--r--l10n/translator.go167
1 files changed, 167 insertions, 0 deletions
diff --git a/l10n/translator.go b/l10n/translator.go
new file mode 100644
index 0000000..4d4b4eb
--- /dev/null
+++ b/l10n/translator.go
@@ -0,0 +1,167 @@
+package l10n
+
+import (
+ "errors"
+ "fmt"
+ "log/slog"
+ "os"
+ "strings"
+ "sync"
+
+ "gopkg.in/yaml.v3"
+)
+
+var T *Translator
+
+func init() {
+ var err error
+ if T, err = NewTranslator("ru", "en"); err != nil {
+ slog.Error("fail to load localization", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+}
+
+type Translator struct {
+ l map[string]any
+ mu sync.RWMutex
+}
+
+func NewTranslator(locales ...string) (*Translator, error) {
+ t := &Translator{l: make(map[string]any)}
+ errs := make([]error, 0)
+
+ for _, locale := range locales {
+ contentBytes, err := os.ReadFile(fmt.Sprintf("l10n/%s.yaml", locale))
+ if err != nil {
+ errs = append(errs, fmt.Errorf("opening one of files: l10n/%s.yaml: %w", locale, err))
+ continue
+ }
+
+ var content map[string]any
+ if err := yaml.Unmarshal(contentBytes, &content); err != nil {
+ errs = append(errs, fmt.Errorf("parsing one of files: l10n/%s.yaml: %w", locale, err))
+ continue
+ }
+
+ errs = append(errs, t.initMap("."+locale, content)...)
+ }
+
+ return t, errors.Join(errs...)
+}
+
+func (t *Translator) Set(key string, val any, toOverwrite bool) (alreadySet bool, err error) {
+ t.mu.RLock()
+ if _, alreadySet = t.l[key]; !alreadySet || (alreadySet && toOverwrite) {
+ t.mu.RUnlock()
+ return alreadySet, errors.Join(t.setSafe(key, val)...)
+ }
+ t.mu.RUnlock()
+ return alreadySet, nil
+}
+
+func (t *Translator) SetPath(val any, toOverwrite bool, path ...any) (alreadySet bool, err error) {
+ key, err := formatPath(path...)
+ if err != nil {
+ return false, err
+ }
+ return t.Set(key, val, toOverwrite)
+}
+
+func (t *Translator) Get(key string) any {
+ return t.GetDefault(key, key[strings.LastIndex(key, ".")+1:])
+}
+
+func (t *Translator) GetPath(path ...any) any {
+ return t.GetDefaultPath(path[len(path)-1], path...)
+}
+
+func (t *Translator) GetDefault(key string, def any) any {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+ if v, ok := t.l[key]; ok {
+ return v
+ }
+ slog.Warn("failed to find localization for a key", slog.String("key", key))
+ return def
+}
+
+func (t *Translator) GetDefaultPath(def any, path ...any) any {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+ key, err := formatPath(path...)
+ if err != nil {
+ slog.Error("get localized value by path: path is invalid", slog.String("error", err.Error()))
+ return def
+ }
+ return t.GetDefault(key, def)
+}
+
+func (t *Translator) initSlice(parent string, level []any) []error {
+ errs := make([]error, 0)
+ for i, v := range level {
+ newParent := parent + fmt.Sprintf("[%d]", i)
+ t.setSafe(newParent, v)
+ }
+ return errs
+}
+
+func (t *Translator) initMap(parent string, level map[string]any) []error {
+ errs := make([]error, 0)
+ for k, v := range level {
+ key, err := formatPath(k)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+
+ errs = append(errs, t.setSafe(parent+key, v)...)
+ }
+ return errs
+}
+
+func (t *Translator) setSafe(k string, v any) []error {
+ errs := make([]error, 0)
+ t.mu.Lock()
+ switch vv := v.(type) {
+ case int:
+ t.l[k] = vv
+ t.mu.Unlock()
+ case float64:
+ t.l[k] = vv
+ t.mu.Unlock()
+ case string:
+ t.l[k] = vv
+ t.mu.Unlock()
+ case []any:
+ t.mu.Unlock()
+ errs = append(errs, t.initSlice(k, vv)...)
+ case map[string]any:
+ t.mu.Unlock()
+ errs = append(errs, t.initMap(k, vv)...)
+ default:
+ t.mu.Unlock()
+ errs = append(errs, fmt.Errorf("invalid value type: %s, %v", k, vv))
+ }
+ return errs
+}
+
+func formatPath(path ...any) (string, error) {
+ var key string
+ for _, part := range path {
+ switch k := part.(type) {
+ case int:
+ key += fmt.Sprintf("[%d]", k)
+ case string:
+ key += "." + strings.ReplaceAll(k, ".", "\\.")
+ case []any:
+ add, err := formatPath(k...)
+ if err != nil {
+ return "", fmt.Errorf("while formatting nested slice: %w", err)
+ }
+ key += add
+ default:
+ return "", fmt.Errorf("invalid key part: after %s follows %v", key, k)
+ }
+ }
+ return key, nil
+}