Skip to content

Commit 9f2e49e

Browse files
Merge pull request #2711 from rajithacharith/role-assignment
Add composite store support for translations
2 parents 5542152 + ece7444 commit 9f2e49e

8 files changed

Lines changed: 623 additions & 27 deletions

File tree

backend/cmd/server/repository/resources/conf/default.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
"layout": {
211211
"store": "composite"
212212
},
213+
"translation": {
214+
"store": "composite"
215+
},
213216
"authn_provider": {
214217
"type": "default",
215218
"rest": {

backend/internal/system/config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,16 @@ type LayoutConfig struct {
454454
Store string `yaml:"store" json:"store"`
455455
}
456456

457+
// TranslationConfig holds the translation service configuration.
458+
type TranslationConfig struct {
459+
// Store defines the storage mode for translations.
460+
// Valid values: "mutable", "declarative", "composite" (hybrid mode)
461+
// If not specified, falls back to global DeclarativeResources.Enabled setting:
462+
// - If DeclarativeResources.Enabled = true: behaves as "declarative"
463+
// - If DeclarativeResources.Enabled = false: behaves as "mutable"
464+
Store string `yaml:"store" json:"store"`
465+
}
466+
457467
// PasskeyConfig holds the passkey configuration details.
458468
type PasskeyConfig struct {
459469
AllowedOrigins []string `yaml:"allowed_origins" json:"allowed_origins"`
@@ -646,6 +656,7 @@ type Config struct {
646656
Role RoleConfig `yaml:"role" json:"role"`
647657
Theme ThemeConfig `yaml:"theme" json:"theme"`
648658
Layout LayoutConfig `yaml:"layout" json:"layout"`
659+
Translation TranslationConfig `yaml:"translation" json:"translation"`
649660
Email EmailConfig `yaml:"email" json:"email"`
650661
Consent ConsentConfig `yaml:"consent" json:"consent"`
651662
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
package mgt
20+
21+
import (
22+
"context"
23+
)
24+
25+
// compositeI18nStore combines a file-backed (declarative) store and a database-backed
26+
// (mutable) store for translations.
27+
// - Read operations merge both stores; database values (overrides) take precedence over
28+
// file-based values (base translations) for the same key and language.
29+
// - Write operations target the database store only.
30+
type compositeI18nStore struct {
31+
fileStore i18nStoreInterface
32+
dbStore i18nStoreInterface
33+
}
34+
35+
func newCompositeI18nStore(fileStore, dbStore i18nStoreInterface) i18nStoreInterface {
36+
return &compositeI18nStore{fileStore: fileStore, dbStore: dbStore}
37+
}
38+
39+
// GetDistinctLanguages returns the union of languages from both stores.
40+
func (c *compositeI18nStore) GetDistinctLanguages() ([]string, error) {
41+
dbLangs, err := c.dbStore.GetDistinctLanguages()
42+
if err != nil {
43+
return nil, err
44+
}
45+
fileLangs, err := c.fileStore.GetDistinctLanguages()
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
seen := make(map[string]bool, len(dbLangs)+len(fileLangs))
51+
result := make([]string, 0, len(dbLangs)+len(fileLangs))
52+
for _, l := range dbLangs {
53+
if !seen[l] {
54+
seen[l] = true
55+
result = append(result, l)
56+
}
57+
}
58+
for _, l := range fileLangs {
59+
if !seen[l] {
60+
seen[l] = true
61+
result = append(result, l)
62+
}
63+
}
64+
return result, nil
65+
}
66+
67+
// GetTranslations merges translations from both stores.
68+
// DB values (overrides) take precedence over file-based values for the same key+language.
69+
func (c *compositeI18nStore) GetTranslations() (map[string]map[string]Translation, error) {
70+
fileTrans, err := c.fileStore.GetTranslations()
71+
if err != nil {
72+
return nil, err
73+
}
74+
dbTrans, err := c.dbStore.GetTranslations()
75+
if err != nil {
76+
return nil, err
77+
}
78+
return mergeTranslationMaps(fileTrans, dbTrans), nil
79+
}
80+
81+
// GetTranslationsByNamespace merges translations for a namespace from both stores.
82+
// DB values take precedence over file-based values for the same key+language.
83+
func (c *compositeI18nStore) GetTranslationsByNamespace(namespace string) (
84+
map[string]map[string]Translation, error,
85+
) {
86+
fileTrans, err := c.fileStore.GetTranslationsByNamespace(namespace)
87+
if err != nil {
88+
return nil, err
89+
}
90+
dbTrans, err := c.dbStore.GetTranslationsByNamespace(namespace)
91+
if err != nil {
92+
return nil, err
93+
}
94+
return mergeTranslationMaps(fileTrans, dbTrans), nil
95+
}
96+
97+
// GetTranslationsByKey retrieves a translation for a specific key and namespace.
98+
// Returns the DB override if present, otherwise falls back to the file-based value.
99+
func (c *compositeI18nStore) GetTranslationsByKey(key string, namespace string) (
100+
map[string]Translation, error,
101+
) {
102+
fileTrans, err := c.fileStore.GetTranslationsByKey(key, namespace)
103+
if err != nil {
104+
return nil, err
105+
}
106+
dbTrans, err := c.dbStore.GetTranslationsByKey(key, namespace)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
// Start with file-based values, then let DB override per language.
112+
result := make(map[string]Translation, len(fileTrans)+len(dbTrans))
113+
for lang, t := range fileTrans {
114+
result[lang] = t
115+
}
116+
for lang, t := range dbTrans {
117+
result[lang] = t
118+
}
119+
return result, nil
120+
}
121+
122+
func (c *compositeI18nStore) UpsertTranslationsByLanguage(language string, translations []Translation) error {
123+
return c.dbStore.UpsertTranslationsByLanguage(language, translations)
124+
}
125+
126+
func (c *compositeI18nStore) UpsertTranslation(trans Translation) error {
127+
return c.dbStore.UpsertTranslation(trans)
128+
}
129+
130+
func (c *compositeI18nStore) UpsertTranslations(ctx context.Context, translations []Translation) error {
131+
return c.dbStore.UpsertTranslations(ctx, translations)
132+
}
133+
134+
func (c *compositeI18nStore) DeleteTranslationsByLanguage(language string) error {
135+
return c.dbStore.DeleteTranslationsByLanguage(language)
136+
}
137+
138+
func (c *compositeI18nStore) DeleteTranslation(language string, key string, namespace string) error {
139+
return c.dbStore.DeleteTranslation(language, key, namespace)
140+
}
141+
142+
func (c *compositeI18nStore) DeleteTranslationsByNamespace(ctx context.Context, namespace string) error {
143+
return c.dbStore.DeleteTranslationsByNamespace(ctx, namespace)
144+
}
145+
146+
func (c *compositeI18nStore) DeleteTranslationsByKey(ctx context.Context, namespace string, key string) error {
147+
return c.dbStore.DeleteTranslationsByKey(ctx, namespace, key)
148+
}
149+
150+
// mergeTranslationMaps merges base (file) and override (DB) translation maps.
151+
// For each compositeKey (namespace|key), DB values take precedence over file values
152+
// per language, so a DB override for a specific language replaces the file-based value.
153+
func mergeTranslationMaps(
154+
base, overrides map[string]map[string]Translation,
155+
) map[string]map[string]Translation {
156+
result := make(map[string]map[string]Translation, len(base)+len(overrides))
157+
158+
for compositeKey, langMap := range base {
159+
result[compositeKey] = make(map[string]Translation, len(langMap))
160+
for lang, t := range langMap {
161+
result[compositeKey][lang] = t
162+
}
163+
}
164+
165+
for compositeKey, langMap := range overrides {
166+
if result[compositeKey] == nil {
167+
result[compositeKey] = make(map[string]Translation, len(langMap))
168+
}
169+
for lang, t := range langMap {
170+
result[compositeKey][lang] = t
171+
}
172+
}
173+
174+
return result
175+
}

0 commit comments

Comments
 (0)