mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2024-12-11 21:10:26 +00:00
234 lines
6.6 KiB
Go
234 lines
6.6 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")
|
|
}
|
|
|
|
// 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.Contains(str, "}}") {
|
|
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.Contains(str, "}}") {
|
|
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.Contains(str, "}}") {
|
|
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)
|
|
|
|
bouncepaw:
|
|
- more error messages
|
|
*/
|