mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-03-06 04:49:48 -06:00
Merge pull request #9722 from 2403905/issue-9700
Added a generic way to translate the necessary fields in composite entities.
This commit is contained in:
8
changelog/unreleased/added-generyc-translator.md
Normal file
8
changelog/unreleased/added-generyc-translator.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Added generic way to translate composite entities
|
||||
|
||||
Added a generic way to translate the necessary fields in composite entities.
|
||||
The function takes the entity, translation function and fields to translate that are described by the TranslateField function.
|
||||
The function supports nested structs and slices of structs.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/9722
|
||||
https://github.com/owncloud/ocis/issues/9700
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
@@ -14,8 +15,13 @@ import (
|
||||
micrometadata "go-micro.dev/v4/metadata"
|
||||
)
|
||||
|
||||
// HeaderAcceptLanguage is the header key for the accept-language header
|
||||
var HeaderAcceptLanguage = "Accept-Language"
|
||||
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 }
|
||||
@@ -63,6 +69,61 @@ func (t Translator) Locale(locale string) *gotext.Locale {
|
||||
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 != "" {
|
||||
@@ -91,3 +152,194 @@ func GetUserLocale(ctx context.Context, userID string, vc settingssvc.ValueServi
|
||||
}
|
||||
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
ocis-pkg/l10n/l10n_test.go
Normal file
414
ocis-pkg/l10n/l10n_test.go
Normal 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, ...interface{}) string {
|
||||
return func(s string, i ...interface{}) 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
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ SHELL := bash
|
||||
NAME := graph
|
||||
|
||||
# Where to write the files generated by this makefile.
|
||||
OUTPUT_DIR = ./pkg/service/v0/l10n
|
||||
TEMPLATE_FILE = ./pkg/service/v0/l10n/graph.pot
|
||||
OUTPUT_DIR = ./pkg/l10n
|
||||
TEMPLATE_FILE = ./pkg/l10n/graph.pot
|
||||
|
||||
include ../../.make/recursion.mk
|
||||
|
||||
@@ -45,7 +45,7 @@ l10n-push:
|
||||
|
||||
.PHONY: l10n-read
|
||||
l10n-read: $(GO_XGETTEXT)
|
||||
go-xgettext -o $(OUTPUT_DIR)/graph.pot --keyword=l10n.Template --add-comments -s pkg/service/v0/spacetemplates.go
|
||||
go-xgettext -o $(OUTPUT_DIR)/graph.pot --keyword=l10n.Template --add-comments -s pkg/service/v0/spacetemplates.go -s pkg/unifiedrole/unifiedrole.go
|
||||
|
||||
.PHONY: l10n-write
|
||||
l10n-write:
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#
|
||||
# Translators:
|
||||
# Alex <hostspepc@gmail.com>, 2024
|
||||
#
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
32
services/graph/pkg/l10n/translation.go
Normal file
32
services/graph/pkg/l10n/translation.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package l10n
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed locale
|
||||
_localeFS embed.FS
|
||||
)
|
||||
|
||||
const (
|
||||
// subfolder where the translation files are stored
|
||||
_localeSubPath = "locale"
|
||||
|
||||
// domain of the graph service (transifex)
|
||||
_domain = "graph"
|
||||
)
|
||||
|
||||
// Translate translates a string based on the locale and default locale
|
||||
func Translate(content, locale, defaultLocale string) string {
|
||||
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
|
||||
return t.Translate(content, locale)
|
||||
}
|
||||
|
||||
// TranslateEntity returns a function that translates a struct or slice based on the locale
|
||||
func TranslateEntity(locale, defaultLocale string, entity any, opts ...l10n.TranslateOption) error {
|
||||
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
|
||||
return t.TranslateEntity(locale, entity, opts...)
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
l10n_pkg "github.com/owncloud/ocis/v2/services/graph/pkg/l10n"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
@@ -632,6 +634,20 @@ func (api DriveItemPermissionsApi) ListPermissions(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
loc := r.Header.Get(l10n.HeaderAcceptLanguage)
|
||||
w.Header().Add("Content-Language", loc)
|
||||
if loc != "" && loc != "en" {
|
||||
err := l10n_pkg.TranslateEntity(loc, "en", permissions,
|
||||
l10n.TranslateEach("LibreGraphPermissionsRolesAllowedValues",
|
||||
l10n.TranslateField("Description"),
|
||||
l10n.TranslateField("DisplayName"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
api.logger.Error().Err(err).Msg("tranlation error")
|
||||
}
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, permissions)
|
||||
}
|
||||
@@ -653,6 +669,20 @@ func (api DriveItemPermissionsApi) ListSpaceRootPermissions(w http.ResponseWrite
|
||||
return
|
||||
}
|
||||
|
||||
loc := r.Header.Get(l10n.HeaderAcceptLanguage)
|
||||
w.Header().Add("Content-Language", loc)
|
||||
if loc != "" && loc != "en" {
|
||||
err := l10n_pkg.TranslateEntity(loc, "en", permissions,
|
||||
l10n.TranslateEach("LibreGraphPermissionsRolesAllowedValues",
|
||||
l10n.TranslateField("Description"),
|
||||
l10n.TranslateField("DisplayName"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
api.logger.Error().Err(err).Msg("tranlation error")
|
||||
}
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, permissions)
|
||||
}
|
||||
|
||||
@@ -15,18 +15,13 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
l10n_pkg "github.com/owncloud/ocis/v2/services/graph/pkg/l10n"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed spacetemplate/*
|
||||
_spaceTemplateFS embed.FS
|
||||
|
||||
//go:embed l10n/locale
|
||||
_localeFS embed.FS
|
||||
|
||||
// subfolder where the translation files are stored
|
||||
_localeSubPath = "l10n/locale"
|
||||
|
||||
// name of the secret space folder
|
||||
_spaceFolderName = ".space"
|
||||
|
||||
@@ -39,9 +34,6 @@ var (
|
||||
// name of the readme.md file
|
||||
_readmeName = "readme.md"
|
||||
|
||||
// domain of the graph service (transifex)
|
||||
_domain = "graph"
|
||||
|
||||
// HeaderAcceptLanguage is the header key for the accept-language header
|
||||
HeaderAcceptLanguage = "Accept-Language"
|
||||
|
||||
@@ -121,10 +113,9 @@ func imageUpload(ctx context.Context, mdc *metadata.CS3) (string, error) {
|
||||
}
|
||||
|
||||
func readmeUpload(ctx context.Context, mdc *metadata.CS3, locale string, defaultLocale string) (string, error) {
|
||||
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
|
||||
res, err := mdc.Upload(ctx, metadata.UploadRequest{
|
||||
Path: filepath.Join(_spaceFolderName, _readmeName),
|
||||
Content: []byte(t.Translate(_readmeText, locale)),
|
||||
Content: []byte(l10n_pkg.Translate(_readmeText, locale, defaultLocale)),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/conversions"
|
||||
@@ -71,12 +72,54 @@ var legacyNames map[string]string = map[string]string{
|
||||
UnifiedRoleSecureViewerID: conversions.RoleSecureViewer,
|
||||
}
|
||||
|
||||
var (
|
||||
// UnifiedRole Viewer, Role Description (resolves directly)
|
||||
_viewerUnifiedRoleDescription = l10n.Template("View and download.")
|
||||
// UnifiedRole Viewer, Role DisplayName (resolves directly)
|
||||
_viewerUnifiedRoleDisplayName = l10n.Template("Can view")
|
||||
|
||||
// UnifiedRole SpaceViewer, Role Description (resolves directly)
|
||||
_spaceViewerUnifiedRoleDescription = l10n.Template("View and download.")
|
||||
// UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
|
||||
_spaceViewerUnifiedRoleDisplayName = l10n.Template("Can view")
|
||||
|
||||
// UnifiedRole Editor, Role Description (resolves directly)
|
||||
_editorUnifiedRoleDescription = l10n.Template("View, download, upload, edit, add and delete.")
|
||||
// UnifiedRole Editor, Role DisplayName (resolves directly)
|
||||
_editorUnifiedRoleDisplayName = l10n.Template("Can edit")
|
||||
|
||||
// UnifiedRole SpaseEditor, Role Description (resolves directly)
|
||||
_spaceEditorUnifiedRoleDescription = l10n.Template("View, download, upload, edit, add and delete.")
|
||||
// UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
|
||||
_spaceEditorUnifiedRoleDisplayName = l10n.Template("Can edit")
|
||||
|
||||
// UnifiedRole FileEditor, Role Description (resolves directly)
|
||||
_fileEditorUnifiedRoleDescription = l10n.Template("View, download and edit.")
|
||||
// UnifiedRole FileEditor, Role DisplayName (resolves directly)
|
||||
_fileEditorUnifiedRoleDisplayName = l10n.Template("Can edit")
|
||||
|
||||
// UnifiedRole EditorLite, Role Description (resolves directly)
|
||||
_editorLiteUnifiedRoleDescription = l10n.Template("View, download and upload.")
|
||||
// UnifiedRole EditorLite, Role DisplayName (resolves directly)
|
||||
_editorLiteUnifiedRoleDisplayName = l10n.Template("Can upload")
|
||||
|
||||
// UnifiedRole Manager, Role Description (resolves directly)
|
||||
_managerUnifiedRoleDescription = l10n.Template("View, download, upload, edit, add, delete and manage members.")
|
||||
// UnifiedRole Manager, Role DisplayName (resolves directly)
|
||||
_managerUnifiedRoleDisplayName = l10n.Template("Can manage")
|
||||
|
||||
// UnifiedRole SecureViewer, Role Description (resolves directly)
|
||||
_secureViewerUnifiedRoleDescription = l10n.Template("View only documents, images and PDFs. Watermarks will be applied.")
|
||||
// UnifiedRole SecureViewer, Role DisplayName (resolves directly)
|
||||
_secureViewerUnifiedRoleDisplayName = l10n.Template("Can view (secure)")
|
||||
)
|
||||
|
||||
// NewViewerUnifiedRole creates a viewer role.
|
||||
func NewViewerUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewViewerRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleViewerID),
|
||||
Description: proto.String("View and download."),
|
||||
Description: proto.String(_viewerUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -97,7 +140,7 @@ func NewSpaceViewerUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewSpaceViewerRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleSpaceViewerID),
|
||||
Description: proto.String("View and download."),
|
||||
Description: proto.String(_spaceViewerUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -114,7 +157,7 @@ func NewEditorUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewEditorRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleEditorID),
|
||||
Description: proto.String("View, download, upload, edit, add and delete."),
|
||||
Description: proto.String(_editorUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -131,7 +174,7 @@ func NewSpaceEditorUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewSpaceEditorRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleSpaceEditorID),
|
||||
Description: proto.String("View, download, upload, edit, add and delete."),
|
||||
Description: proto.String(_spaceEditorUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -148,7 +191,7 @@ func NewFileEditorUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewFileEditorRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleFileEditorID),
|
||||
Description: proto.String("View, download and edit."),
|
||||
Description: proto.String(_fileEditorUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -165,7 +208,7 @@ func NewEditorLiteUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewEditorLiteRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleEditorLiteID),
|
||||
Description: proto.String("View, download and upload."),
|
||||
Description: proto.String(_editorLiteUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -182,7 +225,7 @@ func NewManagerUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewManagerRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleManagerID),
|
||||
Description: proto.String("View, download, upload, edit, add, delete and manage members."),
|
||||
Description: proto.String(_managerUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -199,7 +242,7 @@ func NewSecureViewerUnifiedRole() *libregraph.UnifiedRoleDefinition {
|
||||
r := conversions.NewSecureViewerRole()
|
||||
return &libregraph.UnifiedRoleDefinition{
|
||||
Id: proto.String(UnifiedRoleSecureViewerID),
|
||||
Description: proto.String("View only documents, images and PDFs. Watermarks will be applied."),
|
||||
Description: proto.String(_secureViewerUnifiedRoleDescription),
|
||||
DisplayName: displayName(r),
|
||||
RolePermissions: []libregraph.UnifiedRolePermission{
|
||||
{
|
||||
@@ -482,27 +525,24 @@ func displayName(role *conversions.Role) *string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// linter wants this to be a var
|
||||
canEdit := "Can edit"
|
||||
|
||||
var displayName string
|
||||
switch role.Name {
|
||||
case conversions.RoleViewer:
|
||||
displayName = "Can view"
|
||||
displayName = _viewerUnifiedRoleDisplayName
|
||||
case conversions.RoleSpaceViewer:
|
||||
displayName = "Can view"
|
||||
displayName = _spaceViewerUnifiedRoleDisplayName
|
||||
case conversions.RoleEditor:
|
||||
displayName = canEdit
|
||||
displayName = _editorUnifiedRoleDisplayName
|
||||
case conversions.RoleSpaceEditor:
|
||||
displayName = canEdit
|
||||
displayName = _spaceEditorUnifiedRoleDisplayName
|
||||
case conversions.RoleFileEditor:
|
||||
displayName = canEdit
|
||||
displayName = _fileEditorUnifiedRoleDisplayName
|
||||
case conversions.RoleEditorLite:
|
||||
displayName = "Can upload"
|
||||
displayName = _editorLiteUnifiedRoleDisplayName
|
||||
case conversions.RoleManager:
|
||||
displayName = "Can manage"
|
||||
displayName = _managerUnifiedRoleDisplayName
|
||||
case conversions.RoleSecureViewer:
|
||||
displayName = "Can view (secure)"
|
||||
displayName = _secureViewerUnifiedRoleDisplayName
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user