mirror of
https://github.com/asticode/go-astikit.git
synced 2025-12-24 11:50:53 +08:00
276 lines
6.4 KiB
Go
276 lines
6.4 KiB
Go
package astikit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Translator represents an object capable of translating stuff
|
|
type Translator struct {
|
|
defaultLanguage string
|
|
m *sync.RWMutex // Lock p
|
|
p map[string]string
|
|
validLanguages map[string]bool
|
|
}
|
|
|
|
// TranslatorOptions represents Translator options
|
|
type TranslatorOptions struct {
|
|
DefaultLanguage string
|
|
ValidLanguages []string
|
|
}
|
|
|
|
// NewTranslator creates a new Translator
|
|
func NewTranslator(o TranslatorOptions) (t *Translator) {
|
|
t = &Translator{
|
|
defaultLanguage: o.DefaultLanguage,
|
|
m: &sync.RWMutex{},
|
|
p: make(map[string]string),
|
|
validLanguages: make(map[string]bool),
|
|
}
|
|
for _, l := range o.ValidLanguages {
|
|
t.validLanguages[l] = true
|
|
}
|
|
return
|
|
}
|
|
|
|
// ParseDir adds translations located in ".json" files in the specified dir
|
|
// If ".json" files are located in child dirs, keys will be prefixed with their paths
|
|
func (t *Translator) ParseDir(dirPath string) (err error) {
|
|
// Default dir path
|
|
if dirPath == "" {
|
|
if dirPath, err = os.Getwd(); err != nil {
|
|
err = fmt.Errorf("astikit: getwd failed: %w", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Make sure to clean dir path so that we get consistent path separator with filepath.Walk
|
|
dirPath = filepath.Clean(dirPath)
|
|
|
|
// Walk through dir
|
|
if err = filepath.Walk(dirPath, func(path string, info os.FileInfo, e error) (err error) {
|
|
// Check input error
|
|
if e != nil {
|
|
err = fmt.Errorf("astikit: walking %s has an input error for path %s: %w", dirPath, path, e)
|
|
return
|
|
}
|
|
|
|
// Only process files
|
|
if info.IsDir() {
|
|
return
|
|
}
|
|
|
|
// Only process ".json" files
|
|
if filepath.Ext(path) != ".json" {
|
|
return
|
|
}
|
|
|
|
// Parse file
|
|
if err = t.ParseFile(dirPath, path); err != nil {
|
|
err = fmt.Errorf("astikit: parsing %s failed: %w", path, err)
|
|
return
|
|
}
|
|
return
|
|
}); err != nil {
|
|
err = fmt.Errorf("astikit: walking %s failed: %w", dirPath, err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// ParseFile adds translation located in the provided path
|
|
func (t *Translator) ParseFile(dirPath, path string) (err error) {
|
|
// Lock
|
|
t.m.Lock()
|
|
defer t.m.Unlock()
|
|
|
|
// Open file
|
|
var f *os.File
|
|
if f, err = os.Open(path); err != nil {
|
|
err = fmt.Errorf("astikit: opening %s failed: %w", path, err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
// Unmarshal
|
|
var p map[string]any
|
|
if err = json.NewDecoder(f).Decode(&p); err != nil {
|
|
err = fmt.Errorf("astikit: unmarshaling %s failed: %w", path, err)
|
|
return
|
|
}
|
|
|
|
// Get language
|
|
language := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
|
|
|
// Update valid languages
|
|
t.validLanguages[language] = true
|
|
|
|
// Get prefix
|
|
prefix := language
|
|
if dp := filepath.Dir(path); dp != dirPath {
|
|
var fs []string
|
|
for _, v := range strings.Split(strings.TrimPrefix(dp, dirPath), string(os.PathSeparator)) {
|
|
if v != "" {
|
|
fs = append(fs, v)
|
|
}
|
|
}
|
|
prefix += "." + strings.Join(fs, ".")
|
|
}
|
|
|
|
// Parse
|
|
t.parse(p, prefix)
|
|
return
|
|
}
|
|
|
|
func (t *Translator) key(prefix, key string) string {
|
|
return prefix + "." + key
|
|
}
|
|
|
|
func (t *Translator) parse(i map[string]any, prefix string) {
|
|
for k, v := range i {
|
|
p := t.key(prefix, k)
|
|
switch a := v.(type) {
|
|
case string:
|
|
t.p[p] = a
|
|
case map[string]any:
|
|
t.parse(a, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HTTPMiddleware is the Translator HTTP middleware
|
|
func (t *Translator) HTTPMiddleware(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
// Store language in context
|
|
if l := r.Header.Get("Accept-Language"); l != "" {
|
|
*r = *r.WithContext(contextWithTranslatorLanguage(r.Context(), t.parseAcceptLanguage(l)))
|
|
}
|
|
|
|
// Next handler
|
|
h.ServeHTTP(rw, r)
|
|
})
|
|
}
|
|
|
|
func (t *Translator) parseAcceptLanguage(h string) string {
|
|
// Split on comma
|
|
var qs []float64
|
|
ls := make(map[float64][]string)
|
|
for _, c := range strings.Split(strings.TrimSpace(h), ",") {
|
|
// Empty
|
|
c = strings.TrimSpace(c)
|
|
if c == "" {
|
|
continue
|
|
}
|
|
|
|
// Split on semi colon
|
|
ss := strings.Split(c, ";")
|
|
|
|
// Parse coefficient
|
|
q := float64(1)
|
|
if len(ss) > 1 {
|
|
s := strings.TrimSpace(ss[1])
|
|
if strings.HasPrefix(s, "q=") {
|
|
var err error
|
|
if q, err = strconv.ParseFloat(strings.TrimPrefix(s, "q="), 64); err != nil {
|
|
q = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add
|
|
if _, ok := ls[q]; !ok {
|
|
qs = append(qs, q)
|
|
}
|
|
ls[q] = append(ls[q], strings.TrimSpace(ss[0]))
|
|
}
|
|
|
|
// Order coefficients
|
|
sort.Float64s(qs)
|
|
|
|
// Loop through coefficients in reverse order
|
|
for idx := len(qs) - 1; idx >= 0; idx-- {
|
|
for _, l := range ls[qs[idx]] {
|
|
if _, ok := t.validLanguages[l]; ok {
|
|
return l
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
const contextKeyTranslatorLanguage = contextKey("astikit.translator.language")
|
|
|
|
type contextKey string
|
|
|
|
func contextWithTranslatorLanguage(ctx context.Context, language string) context.Context {
|
|
return context.WithValue(ctx, contextKeyTranslatorLanguage, language)
|
|
}
|
|
|
|
func translatorLanguageFromContext(ctx context.Context) string {
|
|
v, ok := ctx.Value(contextKeyTranslatorLanguage).(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (t *Translator) language(language string) string {
|
|
if language == "" {
|
|
return t.defaultLanguage
|
|
}
|
|
return language
|
|
}
|
|
|
|
// LanguageCtx returns the translator language from the context, or the default language if not in the context
|
|
func (t *Translator) LanguageCtx(ctx context.Context) string {
|
|
return t.language(translatorLanguageFromContext(ctx))
|
|
}
|
|
|
|
// Translate translates a key into a specific language
|
|
func (t *Translator) Translate(language, key string) string {
|
|
// Lock
|
|
t.m.RLock()
|
|
defer t.m.RUnlock()
|
|
|
|
// Get translation
|
|
k1 := t.key(t.language(language), key)
|
|
v, ok := t.p[k1]
|
|
if ok {
|
|
return v
|
|
}
|
|
|
|
// Default translation
|
|
k2 := t.key(t.defaultLanguage, key)
|
|
if v, ok = t.p[k2]; ok {
|
|
return v
|
|
}
|
|
return k1
|
|
}
|
|
|
|
// Translatef translates a key into a specific language with optional formatting args
|
|
func (t *Translator) Translatef(language, key string, args ...any) string {
|
|
return fmt.Sprintf(t.Translate(language, key), args...)
|
|
}
|
|
|
|
// TranslateCtx is an alias for TranslateC
|
|
func (t *Translator) TranslateCtx(ctx context.Context, key string) string {
|
|
return t.TranslateC(ctx, key)
|
|
}
|
|
|
|
// TranslateC translates a key using the language specified in the context
|
|
func (t *Translator) TranslateC(ctx context.Context, key string) string {
|
|
return t.Translate(translatorLanguageFromContext(ctx), key)
|
|
}
|
|
|
|
func (t *Translator) TranslateCf(ctx context.Context, key string, args ...any) string {
|
|
return t.Translatef(translatorLanguageFromContext(ctx), key, args...)
|
|
}
|