mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2024-12-12 05:20:26 +00:00
3f7de07da9
This has several advantages over using an external CLI tool to generate the files, such as having fewer dependencies, less generated files bloat and more flexibility over the localization code. "Sadly", this solution doesn't check for validity of JSON files at compile-time (the only advantage of using an external tool such as go-localize). However, I easily fixed this huge "issue" by making the program crash at startup if any locale files are invalid. Also, no more "go-localize removed from go.mod" "go-localize added to go.mod" "go-localize removed from go.mod" spam. A utility for making sure all translation stay in sync soon! (not sure where to put it)
252 lines
7.2 KiB
Go
252 lines
7.2 KiB
Go
// Package l18n handles everything language-related for Mycorrhiza.
|
|
package l18n
|
|
|
|
/*
|
|
Some code is borrowed from github.com/m1/go-localize. The copyright notice is
|
|
included as required by the MIT License:
|
|
|
|
Copyright (c) 2019 Miles Croxford <hello@milescroxford.com>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
*/
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"golang.org/x/text/feature/plural"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
type Replacements map[string]interface{}
|
|
|
|
type Localizer struct {
|
|
Locale string
|
|
FallbackLocale string
|
|
Localizations map[string]string
|
|
}
|
|
|
|
// locales is a filesystem containing all localization files.
|
|
//go:embed en ru
|
|
var locales embed.FS
|
|
|
|
// localization maps localizable keys to strings.
|
|
var localizations = make(map[string]string)
|
|
|
|
// supportedLocales is a language.Matcher configured for supported languages.
|
|
var supportedLocales = language.NewMatcher([]language.Tag{
|
|
language.Make("en"),
|
|
language.Make("ru"),
|
|
})
|
|
|
|
var formNames = map[plural.Form]string{
|
|
plural.Other: "other",
|
|
plural.Zero: "zero",
|
|
plural.One: "one",
|
|
plural.Two: "two",
|
|
plural.Few: "few",
|
|
plural.Many: "many",
|
|
}
|
|
|
|
func init() {
|
|
fs.WalkDir(locales, ".", func(path string, d fs.DirEntry, err error) error {
|
|
ext := filepath.Ext(path)
|
|
if !d.IsDir() && ext == ".json" {
|
|
basename := path[:len(path)-len(ext)]
|
|
// Note: embed.FS always uses a forward slash as the path separator.
|
|
segments := strings.Split(basename, "/")
|
|
prefix := strings.Join(segments, ".") + "."
|
|
|
|
contents, err := locales.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var strings map[string]string
|
|
if err := json.Unmarshal(contents, &strings); err != nil {
|
|
log.Fatalf("error while parsing %s: %v", path, err)
|
|
}
|
|
|
|
for key, value := range strings {
|
|
localizations[prefix+key] = value
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// New creates a new Localizer with locales set. This operation is cheap.
|
|
func New(locale string, fallbackLocale string) *Localizer {
|
|
t := &Localizer{Locale: locale, FallbackLocale: fallbackLocale}
|
|
t.Localizations = localizations
|
|
return t
|
|
}
|
|
|
|
// FromRequest takes a HTTP request and picks the most appropriate localizer
|
|
// with English as the fallback language.
|
|
func FromRequest(r *http.Request) *Localizer {
|
|
t, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
|
|
tag, _, _ := supportedLocales.Match(t...)
|
|
// TODO: support subtags such as en-US, en-GB, zh-Hans
|
|
base, _ := tag.Base()
|
|
return New(base.String(), "en")
|
|
}
|
|
|
|
// SetLocales changes locales used by the localizer.
|
|
func (t Localizer) SetLocales(locale, fallback string) Localizer {
|
|
t.Locale = locale
|
|
t.FallbackLocale = fallback
|
|
return t
|
|
}
|
|
|
|
// SetLocale changes the preferred locale used by the localizer. The fallback
|
|
// locale is kept unchanged.
|
|
func (t Localizer) SetLocale(locale string) Localizer {
|
|
t.Locale = locale
|
|
return t
|
|
}
|
|
|
|
// SetFallbackLocale changes the fallback locale used by the localizer. The
|
|
// preferred locale is kept unchanged.
|
|
func (t Localizer) SetFallbackLocale(fallback string) Localizer {
|
|
t.FallbackLocale = fallback
|
|
return t
|
|
}
|
|
|
|
// GetWithLocale returns a localized string for the provided key in a specific
|
|
// locale with optional replacements executed on the string.
|
|
func (t Localizer) GetWithLocale(locale, key string, replacements ...*Replacements) string {
|
|
str, ok := t.Localizations[getLocalizationKey(locale, key)]
|
|
if !ok {
|
|
str, ok = t.Localizations[getLocalizationKey(t.FallbackLocale, key)]
|
|
if !ok {
|
|
return key
|
|
}
|
|
}
|
|
|
|
// If the str doesn't have any substitutions, no need to
|
|
// template.Execute.
|
|
if strings.Index(str, "}}") == -1 {
|
|
return str
|
|
}
|
|
|
|
return t.replace(str, replacements...)
|
|
}
|
|
|
|
// Get returns a localized string for the provided key with optional
|
|
// replacements executed on the string.
|
|
func (t Localizer) Get(key string, replacements ...*Replacements) string {
|
|
str := t.GetWithLocale(t.Locale, key, replacements...)
|
|
return str
|
|
}
|
|
|
|
// GetPlural returns a localized string respecting locale-specific plural rules.
|
|
// Technically, it replaces %s token with +form subkey and proceeds as usual.
|
|
func (t Localizer) GetPlural(key string, n int, replacements ...*Replacements) string {
|
|
str, ok := t.rawPlural(t.Locale, key, n)
|
|
if !ok {
|
|
str, ok = t.rawPlural(t.FallbackLocale, key, n)
|
|
if !ok {
|
|
return key
|
|
}
|
|
}
|
|
|
|
// As in the original, we skip templating if have nothing to replace
|
|
// (however, it's strange case for plurals)
|
|
if strings.Index(str, "}}") == -1 {
|
|
return str
|
|
}
|
|
|
|
return t.replace(str, append(replacements, &Replacements{"n": n})...)
|
|
}
|
|
|
|
// GetPlural64 is ditto for int64
|
|
func (t Localizer) GetPlural64(key string, n int64, replacements ...*Replacements) string {
|
|
str, ok := t.rawPlural(t.Locale, key, int(n%1000000))
|
|
if !ok {
|
|
str, ok = t.rawPlural(t.FallbackLocale, key, int(n%1000000))
|
|
if !ok {
|
|
return key
|
|
}
|
|
}
|
|
|
|
// As in the original, we skip templating if have nothing to replace
|
|
// (however, it's strange case for plurals)
|
|
if strings.Index(str, "}}") == -1 {
|
|
return str
|
|
}
|
|
|
|
return t.replace(str, append(replacements, &Replacements{"n": n})...)
|
|
}
|
|
|
|
func (t Localizer) replace(str string, replacements ...*Replacements) string {
|
|
b := &bytes.Buffer{}
|
|
tmpl, err := template.New("").Parse(str)
|
|
if err != nil {
|
|
return str
|
|
}
|
|
|
|
replacementsMerge := Replacements{}
|
|
for _, replacement := range replacements {
|
|
for k, v := range *replacement {
|
|
replacementsMerge[k] = v
|
|
}
|
|
}
|
|
|
|
err = template.Must(tmpl, err).Execute(b, replacementsMerge)
|
|
if err != nil {
|
|
return str
|
|
}
|
|
buff := b.String()
|
|
return buff
|
|
}
|
|
|
|
func (t Localizer) rawPlural(lang, rawKey string, n int) (string, bool) {
|
|
key := getLocalizationKey(lang, rawKey)
|
|
str, ok := t.Localizations[key]
|
|
if !ok {
|
|
return key, false
|
|
}
|
|
var (
|
|
formIdx = plural.Cardinal.MatchPlural(language.Make(lang), n, 0, 0, 0, 0)
|
|
form = formNames[formIdx]
|
|
)
|
|
plural, plOk := t.Localizations[fmt.Sprintf("%v+%v", key, form)]
|
|
if !plOk {
|
|
return key, false
|
|
}
|
|
return fmt.Sprintf(str, plural), true
|
|
}
|
|
|
|
func getLocalizationKey(locale string, key string) string {
|
|
return fmt.Sprintf("%v.%v", locale, key)
|
|
}
|
|
|
|
/* chekoopa: Missing translation features:
|
|
- history records (they use Git description, the possible solution is to parse and translate)
|
|
- history dates (history.WithRevisions doesn't consider locale, Monday package is bad idea)
|
|
- probably error messages (which are scattered across the code)
|
|
- default top bar (it is static from one-shot cfg.SetDefaultHeaderLinks, but it is possible to track default-ness in templates)
|
|
- alt solution is implementing "special" links
|
|
- dynamic UI (JS are static, though we may send some strings through templates)
|
|
- help switches, like,
|
|
- "Read in your language"
|
|
- "Try reading it in English", if no page found in a foreign locale
|
|
- feeds (it seems diffcult to pull locale here)
|
|
We do not translate:
|
|
- stdout traces (logging is English-only)
|
|
*/
|