Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+345
View File
@@ -0,0 +1,345 @@
// package l10n holds translation mechanics that are used by user facing services (notifications, userlog, graph)
package l10n
import (
"context"
"errors"
"io/fs"
"os"
"reflect"
"github.com/leonelquinteros/gotext"
"github.com/qsfera/server/pkg/middleware"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
micrometadata "go-micro.dev/v4/metadata"
)
var (
// HeaderAcceptLanguage is the header key for the accept-language header
HeaderAcceptLanguage = "Accept-Language"
// ErrUnsupportedType is returned when the type is not supported
ErrUnsupportedType = errors.New("unsupported type")
)
// Template marks a string as translatable
func Template(s string) string { return s }
// Translator is able to translate strings
type Translator struct {
fs fs.FS
defaultLocale string
domain string
}
// NewTranslator creates a Translator with library path and language code and load default domain
func NewTranslator(defaultLocale string, domain string, fsys fs.FS) Translator {
return Translator{
fs: fsys,
defaultLocale: defaultLocale,
domain: domain,
}
}
// NewTranslatorFromCommonConfig creates a new Translator from legacy config
func NewTranslatorFromCommonConfig(defaultLocale string, domain string, path string, fsys fs.FS, fsSubPath string) Translator {
var filesystem fs.FS
if path == "" {
filesystem, _ = fs.Sub(fsys, fsSubPath)
} else { // use custom path instead
filesystem = os.DirFS(path)
}
return NewTranslator(defaultLocale, domain, filesystem)
}
// Translate translates a string to the locale
func (t Translator) Translate(str, locale string) string {
return t.Locale(locale).Get("%s", str)
}
// Locale returns the gotext.Locale, use `.Get` method to translate strings
func (t Translator) Locale(locale string) *gotext.Locale {
l := gotext.NewLocaleFS(locale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
if locale != "en" && len(l.GetTranslations()) == 0 {
l = gotext.NewLocaleFS(t.defaultLocale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
}
return l
}
// TranslateEntity function provides the generic way to translate a struct, array or slice.
// Support for maps is also provided, but non-pointer values will not work.
// The function also takes the entity with fields to translate.
// The function supports nested structs and slices of structs.
/*
tr := NewTranslator("en", _domain, _fsys)
// a slice of translatables can be passed directly
val := []string{"description", "display name"}
err := tr.TranslateEntity(tr, s, val)
// string maps work the same way
val := map[string]string{
"entryOne": "description",
"entryTwo": "display name",
}
err := TranslateEntity(tr, val)
// struct fields need to be specified
type Struct struct {
Description string
DisplayName string
MetaInformation string
}
val := Struct{}
err := TranslateEntity(tr, val,
l10n.TranslateField("Description"),
l10n.TranslateField("DisplayName"),
)
// nested structures are supported
type InnerStruct struct {
Description string
Roles []string
}
type OuterStruct struct {
DisplayName string
First InnerStruct
Others map[string]InnerStruct
}
val := OuterStruct{}
err := TranslateEntity(tr, val,
l10n.TranslateField("DisplayName"),
l10n.TranslateStruct("First",
l10n.TranslateField("Description"),
l10n.TranslateEach("Roles"),
),
l10n.TranslateMap("Others",
l10n.TranslateField("Description"),
},
*/
func (t Translator) TranslateEntity(locale string, entity any, opts ...TranslateOption) error {
return TranslateEntity(t.Locale(locale).Get, entity, opts...)
}
// MustGetUserLocale returns the locale the user wants to use, omitting errors
func MustGetUserLocale(ctx context.Context, userID string, preferedLang string, vc settingssvc.ValueService) string {
if preferedLang != "" {
return preferedLang
}
locale, _ := GetUserLocale(ctx, userID, vc)
return locale
}
// GetUserLocale returns the locale of the user
func GetUserLocale(ctx context.Context, userID string, vc settingssvc.ValueService) (string, error) {
resp, err := vc.GetValueByUniqueIdentifiers(
micrometadata.Set(ctx, middleware.AccountID, userID),
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userID,
// this defaults.SettingUUIDProfileLanguage. Copied here to avoid import cycles.
SettingId: "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f",
},
)
if err != nil {
return "", err
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return "", errors.New("no language setting found")
}
return val[0].GetStringValue(), nil
}
// TranslateOption is used to specify fields in structs to translate
type TranslateOption func() (string, FieldType, []TranslateOption)
// FieldType is used to specify the type of field to translate
type FieldType int
const (
// FieldTypeString is a string field
FieldTypeString FieldType = iota
// FieldTypeStruct is a struct field
FieldTypeStruct
// FieldTypeIterable is a slice or array field
FieldTypeIterable
// FieldTypeMap is a map field
FieldTypeMap
)
// TranslateField function provides the generic way to translate the necessary field in composite entities.
func TranslateField(fieldName string) TranslateOption {
return func() (string, FieldType, []TranslateOption) {
return fieldName, FieldTypeString, nil
}
}
// TranslateStruct function provides the generic way to translate the nested fields in composite entities.
func TranslateStruct(fieldName string, args ...TranslateOption) TranslateOption {
return func() (string, FieldType, []TranslateOption) {
return fieldName, FieldTypeStruct, args
}
}
// TranslateEach function provides the generic way to translate the necessary fields in slices or nested entities.
func TranslateEach(fieldName string, args ...TranslateOption) TranslateOption {
return func() (string, FieldType, []TranslateOption) {
return fieldName, FieldTypeIterable, args
}
}
// TranslateMap function provides the generic way to translate the necessary fields in maps.
func TranslateMap(fieldName string, args ...TranslateOption) TranslateOption {
return func() (string, FieldType, []TranslateOption) {
return fieldName, FieldTypeMap, args
}
}
// TranslateEntity translates a slice, array or struct
// See Translator.TranslateEntity for more information
func TranslateEntity(tr func(string, ...any) string, entity any, opts ...TranslateOption) error {
value := reflect.ValueOf(entity)
value, ok := cleanValue(value)
if !ok {
return errors.New("entity is not valid")
}
switch value.Kind() {
case reflect.Struct:
rangeOverArgs(tr, value, opts...)
case reflect.Slice, reflect.Array, reflect.Map:
translateEach(tr, value, opts...)
case reflect.String:
translateField(tr, value)
default:
return ErrUnsupportedType
}
return nil
}
func translateEach(tr func(string, ...any) string, value reflect.Value, args ...TranslateOption) {
value, ok := cleanValue(value)
if !ok {
return
}
switch value.Kind() {
case reflect.Array, reflect.Slice:
for i := 0; i < value.Len(); i++ {
v := value.Index(i)
switch v.Kind() {
case reflect.Struct, reflect.Ptr:
rangeOverArgs(tr, v, args...)
case reflect.String:
translateField(tr, v)
case reflect.Slice, reflect.Array, reflect.Map:
translateEach(tr, v, args...)
}
}
case reflect.Map:
for _, k := range value.MapKeys() {
v := value.MapIndex(k)
switch v.Kind() {
case reflect.Struct:
// FIXME: add support for non-pointer values
case reflect.Pointer:
rangeOverArgs(tr, v, args...)
case reflect.String:
if nv := tr(v.String()); nv != "" {
value.SetMapIndex(k, reflect.ValueOf(nv))
}
case reflect.Slice, reflect.Array, reflect.Map:
translateEach(tr, v, args...)
}
}
}
}
func rangeOverArgs(tr func(string, ...any) string, value reflect.Value, args ...TranslateOption) {
value, ok := cleanValue(value)
if !ok {
return
}
for _, arg := range args {
fieldName, fieldType, opts := arg()
switch fieldType {
case FieldTypeString:
f := value.FieldByName(fieldName)
translateField(tr, f)
case FieldTypeStruct:
innerValue := value.FieldByName(fieldName)
if !innerValue.IsValid() || !isStruct(innerValue) {
return
}
rangeOverArgs(tr, innerValue, opts...)
case FieldTypeIterable:
innerValue := value.FieldByName(fieldName)
if !innerValue.IsValid() {
return
}
if kind := innerValue.Kind(); kind != reflect.Array && kind != reflect.Slice {
return
}
translateEach(tr, innerValue, opts...)
case FieldTypeMap:
innerValue := value.FieldByName(fieldName)
if !innerValue.IsValid() {
return
}
if kind := innerValue.Kind(); kind != reflect.Map {
return
}
translateEach(tr, innerValue, opts...)
}
}
}
func translateField(tr func(string, ...any) string, f reflect.Value) {
if f.IsValid() {
if f.Kind() == reflect.Ptr {
if f.IsNil() {
return
}
f = f.Elem()
}
// A Value can be changed only if it is
// addressable and was not obtained by
// the use of unexported struct fields.
if f.CanSet() {
// change value
if f.Kind() == reflect.String {
val := tr(f.String())
if val == "" {
return
}
f.SetString(val)
}
}
}
}
func isStruct(r reflect.Value) bool {
if r.Kind() == reflect.Ptr {
r = r.Elem()
}
return r.Kind() == reflect.Struct
}
func cleanValue(v reflect.Value) (reflect.Value, bool) {
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
if v.IsNil() {
return v, false
}
v = v.Elem()
}
if !v.IsValid() {
return v, false
}
return v, true
}
+414
View File
@@ -0,0 +1,414 @@
package l10n
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTranslateStruct(t *testing.T) {
type InnerStruct struct {
Description string
DisplayName *string
}
type TopLevelStruct struct {
Description string
DisplayName *string
SubStruct *InnerStruct
}
type WrapperStruct struct {
Description string
StructList []*InnerStruct
}
toStrPointer := func(str string) *string {
return &str
}
tests := []struct {
name string
entity any
args []TranslateOption
expected any
wantErr bool
}{
{
name: "top level slice of struct",
entity: []*InnerStruct{
{
Description: "inner 1",
DisplayName: toStrPointer("innerDisplayName 1"),
},
{
Description: "inner 2",
DisplayName: toStrPointer("innerDisplayName 2"),
},
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
},
expected: []*InnerStruct{
{
Description: "new Inner 1",
DisplayName: toStrPointer("new InnerDisplayName 1"),
},
{
Description: "new Inner 2",
DisplayName: toStrPointer("new InnerDisplayName 2"),
},
},
},
{
name: "top level slice of string",
entity: []string{
"inner 1",
"inner 2",
},
expected: []string{
"new Inner 1",
"new Inner 2",
},
},
{
name: "top level slice of struct",
entity: []*TopLevelStruct{
{
Description: "inner 1",
DisplayName: toStrPointer("innerDisplayName 1"),
SubStruct: &InnerStruct{
Description: "inner",
DisplayName: toStrPointer("innerDisplayName"),
},
},
{
Description: "inner 2",
DisplayName: toStrPointer("innerDisplayName 2"),
},
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
TranslateStruct("SubStruct",
TranslateField("Description"),
TranslateField("DisplayName"),
),
},
expected: []*TopLevelStruct{
{
Description: "new Inner 1",
DisplayName: toStrPointer("new InnerDisplayName 1"),
SubStruct: &InnerStruct{
Description: "new Inner",
DisplayName: toStrPointer("new InnerDisplayName"),
},
},
{
Description: "new Inner 2",
DisplayName: toStrPointer("new InnerDisplayName 2"),
},
},
},
{
name: "wrapped struct full",
entity: &WrapperStruct{
StructList: []*InnerStruct{
{
Description: "inner 1",
DisplayName: toStrPointer("innerDisplayName 1"),
},
{
Description: "inner 2",
DisplayName: toStrPointer("innerDisplayName 2"),
},
},
},
args: []TranslateOption{
TranslateEach("StructList",
TranslateField("Description"),
TranslateField("DisplayName"),
),
},
expected: &WrapperStruct{
StructList: []*InnerStruct{
{
Description: "new Inner 1",
DisplayName: toStrPointer("new InnerDisplayName 1"),
},
{
Description: "new Inner 2",
DisplayName: toStrPointer("new InnerDisplayName 2"),
},
},
},
},
{
name: "empty struct, NotExistingSubStructName",
entity: &TopLevelStruct{},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
TranslateStruct("NotExistingSubStructName",
TranslateField("Description"),
TranslateField("DisplayName"),
),
},
expected: &TopLevelStruct{},
},
{
name: "empty struct",
entity: &TopLevelStruct{},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
TranslateStruct("SubStruct",
TranslateField("Description"),
TranslateField("DisplayName"),
),
},
expected: &TopLevelStruct{},
},
{
name: "empty struct, not existing field",
entity: &TopLevelStruct{
Description: "description",
DisplayName: toStrPointer("displayName"),
},
args: []TranslateOption{
TranslateField("NotExistingFieldName"),
TranslateStruct("SubStruct",
TranslateField("NotExistingFieldName"),
),
},
expected: &TopLevelStruct{
Description: "description",
DisplayName: toStrPointer("displayName"),
},
},
{
name: "inner struct DisplayName empy",
entity: &TopLevelStruct{
Description: "description",
DisplayName: toStrPointer("displayName"),
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
TranslateStruct("SubStruct",
TranslateField("Description"),
TranslateField("DisplayName"),
),
},
expected: &TopLevelStruct{
Description: "new Description",
DisplayName: toStrPointer("new DisplayName"),
},
},
{
name: "inner struct full",
entity: &TopLevelStruct{
Description: "description",
DisplayName: toStrPointer("displayName"),
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
TranslateStruct("SubStruct",
TranslateField("Description"),
TranslateField("DisplayName"),
),
},
expected: &TopLevelStruct{
Description: "new Description",
DisplayName: toStrPointer("new DisplayName"),
},
},
{
name: "full struct",
entity: &TopLevelStruct{
Description: "description",
DisplayName: toStrPointer("displayName"),
SubStruct: &InnerStruct{
Description: "inner",
DisplayName: toStrPointer("innerDisplayName"),
},
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
TranslateStruct("SubStruct",
TranslateField("Description"),
TranslateField("DisplayName"),
),
},
expected: &TopLevelStruct{
Description: "new Description",
DisplayName: toStrPointer("new DisplayName"),
SubStruct: &InnerStruct{
Description: "new Inner",
DisplayName: toStrPointer("new InnerDisplayName"),
},
},
},
{
name: "nil",
wantErr: true,
},
{
name: "empty slice",
wantErr: true,
},
{
name: "string slice",
entity: []string{"description", "inner"},
expected: []string{"new Description", "new Inner"},
},
{
name: "string map",
entity: map[string]string{
"entryOne": "description",
"entryTwo": "inner",
},
expected: map[string]string{
"entryOne": "new Description",
"entryTwo": "new Inner",
},
},
{
name: "pointer struct map",
entity: map[string]*InnerStruct{
"entryOne": {Description: "description", DisplayName: toStrPointer("displayName")},
"entryTwo": {Description: "inner", DisplayName: toStrPointer("innerDisplayName")},
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
},
expected: map[string]*InnerStruct{
"entryOne": {Description: "new Description", DisplayName: toStrPointer("new DisplayName")},
"entryTwo": {Description: "new Inner", DisplayName: toStrPointer("new InnerDisplayName")},
},
},
/* FIXME: non pointer maps are currently not working
{
name: "struct map",
entity: map[string]InnerStruct{
"entryOne": {Description: "description", DisplayName: toStrPointer("displayName")},
"entryTwo": {Description: "inner", DisplayName: toStrPointer("innerDisplayName")},
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
},
expected: map[string]InnerStruct{
"entryOne": {Description: "new Description", DisplayName: toStrPointer("new DisplayName")},
"entryTwo": {Description: "new Inner", DisplayName: toStrPointer("new InnerDisplayName")},
},
},
*/
{
name: "slice map",
entity: map[string][]string{
"entryOne": {"description", "inner"},
"entryTwo": {"inner 2", "innerDisplayName 2"},
},
expected: map[string][]string{
"entryOne": {"new Description", "new Inner"},
"entryTwo": {"new Inner 2", "new InnerDisplayName 2"},
},
},
{
name: "double slice",
entity: [][]string{
{"description", "inner"},
{"inner 2", "innerDisplayName 2"},
},
expected: [][]string{
{"new Description", "new Inner"},
{"new Inner 2", "new InnerDisplayName 2"},
},
},
{
name: "nested structs",
entity: [][]*InnerStruct{
{
&InnerStruct{Description: "description", DisplayName: toStrPointer("displayName")},
&InnerStruct{Description: "inner", DisplayName: toStrPointer("innerDisplayName")},
},
{
&InnerStruct{Description: "inner 2", DisplayName: toStrPointer("innerDisplayName 2")},
},
},
args: []TranslateOption{
TranslateField("Description"),
TranslateField("DisplayName"),
},
expected: [][]*InnerStruct{
{
&InnerStruct{Description: "new Description", DisplayName: toStrPointer("new DisplayName")},
&InnerStruct{Description: "new Inner", DisplayName: toStrPointer("new InnerDisplayName")},
},
{
&InnerStruct{Description: "new Inner 2", DisplayName: toStrPointer("new InnerDisplayName 2")},
},
},
},
{
name: "double mapslices",
entity: []map[string][]string{
{
"entryOne": {"inner 1", "innerDisplayName 1"},
"entryTwo": {"inner 2", "innerDisplayName 2"},
},
{
"entryOne": {"description", "displayName"},
},
},
expected: []map[string][]string{
{
"entryOne": {"new Inner 1", "new InnerDisplayName 1"},
"entryTwo": {"new Inner 2", "new InnerDisplayName 2"},
},
{
"entryOne": {"new Description", "new DisplayName"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := TranslateEntity(mock(), tt.entity, tt.args...)
if (err != nil) != tt.wantErr {
t.Errorf("TranslateEntity() error = %v, wantErr %v", err, tt.wantErr)
}
assert.Equal(t, tt.expected, tt.entity)
})
}
}
func mock() func(string, ...any) string {
return func(s string, i ...any) string {
switch s {
case "description":
return "new Description"
case "displayName":
return "new DisplayName"
case "inner":
return "new Inner"
case "innerDisplayName":
return "new InnerDisplayName"
case "inner 1":
return "new Inner 1"
case "innerDisplayName 1":
return "new InnerDisplayName 1"
case "inner 2":
return "new Inner 2"
case "innerDisplayName 2":
return "new InnerDisplayName 2"
}
return s
}
}