Compare commits

..

No commits in common. "main" and "v2.0.0" have entirely different histories.
main ... v2.0.0

15 changed files with 132 additions and 560 deletions

View File

@ -30,7 +30,7 @@ You must have a folder containing JSON files. Each file should hold an object wi
}
```
Next, you can create a Go file that embeds these JSON files using fs.Files.
Next, you can create a Go file that embeds these JSON files using embed.FS.
- translations/files.go
@ -49,18 +49,21 @@ Now, use the translation package to set up and retrieve translations on demand:
package main
import (
di "git.apihub24.de/admin/generic-di"
"git.apihub24.de/admin/translation"
translations "{link to your translations folder}"
)
func main() {
// get the Service from DI
translationService := di.Inject[translation.ITranslationService]()
// give the Init Function the translations fs.Files
translation.Init(translations.Files)
translationService.Init(translations.Files)
// Optional you can change the Fallback Language
translation.SetDefaultCulture("de")
translationService.SetDefaultCulture("de")
// get the Translation Values
translation.Get("SOME_KEY", "de")
translationService.Get("SOME_KEY", "de")
}
```
@ -87,7 +90,7 @@ For example, if you want to split your translation files into modules, you can n
And the translation call would look like this:
```go
translation.Get("SOME_KEY", "global_de")
translationService.Get("SOME_KEY", "global_de")
```
### Parameters
@ -103,5 +106,5 @@ For example:
```
```go
translation.Get("DYNAMIC", "de", "12,50", "€")
translationService.Get("DYNAMIC", "de", "12,50", "€")
```

2
go.mod
View File

@ -1,3 +1,5 @@
module git.apihub24.de/admin/translation
go 1.22.0
require git.apihub24.de/admin/generic-di v1.4.0

View File

View File

@ -5,22 +5,44 @@ import (
"encoding/json"
"fmt"
"strings"
"git.apihub24.de/admin/generic-di"
)
var defaultCulture = "en"
var sources = make(map[string]map[string]string)
var translationFiles embed.FS
func SetDefaultCulture(culture string) {
defaultCulture = strings.ToLower(culture)
func init() {
di.Injectable(newTranslationService)
}
func Init(files embed.FS) {
translationFiles = files
type ITranslationService interface {
Init(files embed.FS)
SetDefaultCulture(culture string)
Get(key string, culture string, args ...string) string
}
func Get(key string, culture string, args ...string) string {
source, err := loadSource(culture)
type translationService struct {
defaultCulture string
translationFiles embed.FS
sources map[string]map[string]string
}
func newTranslationService() ITranslationService {
return &translationService{
defaultCulture: "en",
translationFiles: embed.FS{},
sources: make(map[string]map[string]string),
}
}
func (translation *translationService) Init(files embed.FS) {
translation.translationFiles = files
}
func (translation *translationService) SetDefaultCulture(culture string) {
translation.defaultCulture = strings.ToLower(culture)
}
func (translation *translationService) Get(key string, culture string, args ...string) string {
source, err := translation.loadSource(culture)
if err != nil {
return fmt.Sprintf("unknown error: %s", err.Error())
}
@ -38,22 +60,22 @@ func Get(key string, culture string, args ...string) string {
return value
}
func loadSource(culture string) (map[string]string, error) {
source, ok := sources[culture]
func (translation *translationService) loadSource(culture string) (map[string]string, error) {
source, ok := translation.sources[culture]
if ok {
return source, nil
}
data, err := translationFiles.ReadFile(fmt.Sprintf("%s.json", strings.ToLower(culture)))
data, err := translation.translationFiles.ReadFile(fmt.Sprintf("%s.json", strings.ToLower(culture)))
if err != nil {
data, err = translationFiles.ReadFile(fmt.Sprintf("%s.json", defaultCulture))
data, err = translation.translationFiles.ReadFile(fmt.Sprintf("%s.json", translation.defaultCulture))
if err != nil {
return source, fmt.Errorf("can not load translation source for culture %s DefaultCulture: %s", culture, defaultCulture)
return source, fmt.Errorf("can not load translation source for culture %s DefaultCulture: %s", culture, translation.defaultCulture)
}
}
err = json.Unmarshal(data, &source)
if err != nil {
return source, fmt.Errorf("can not parse translation source %s", culture)
}
sources[culture] = source
return sources[culture], nil
translation.sources[culture] = source
return translation.sources[culture], nil
}

View File

@ -1,190 +1,89 @@
package translation_test
import (
"os"
"testing"
di "git.apihub24.de/admin/generic-di"
"git.apihub24.de/admin/translation"
exampletranslations "git.apihub24.de/admin/translation/example_translations"
"testing"
)
const (
paramTranslationValue = "tlv"
)
type testCaseError struct {
format string
params []string
}
type testCase struct {
name string
fallbackCulture string
keys []string
keyParams [][]string
cultures []string
expectedValues []string
errorMessages []testCaseError
}
func getTestCases() []testCase {
return []testCase{
{
name: "Test Translation.Init",
fallbackCulture: "",
keys: []string{"KEY"},
keyParams: [][]string{},
cultures: []string{"de"},
expectedValues: []string{"WERT"},
errorMessages: []testCaseError{
{
format: "translations not initialized!",
params: []string{},
},
},
},
{
name: "Test Translation.Get de",
fallbackCulture: "",
keys: []string{"KEY"},
keyParams: [][]string{},
cultures: []string{"de"},
expectedValues: []string{"WERT"},
errorMessages: []testCaseError{
{
format: "expect German KEY to have Value 'WERT' but was %[1]s",
params: []string{paramTranslationValue},
},
},
},
{
name: "Test Translation.Get en",
fallbackCulture: "",
keys: []string{"KEY"},
keyParams: [][]string{},
cultures: []string{"en"},
expectedValues: []string{"VALUE"},
errorMessages: []testCaseError{
{
format: "expect English KEY to have Value 'VALUE' but was %[1]s",
params: []string{paramTranslationValue},
},
},
},
{
name: "Test Translation.Get fr",
fallbackCulture: "",
keys: []string{"KEY"},
keyParams: [][]string{},
cultures: []string{"fr"},
expectedValues: []string{"VALEUR"},
errorMessages: []testCaseError{
{
format: "expect France KEY to have Value 'VALEUR' but was %[1]s",
params: []string{paramTranslationValue},
},
},
},
{
name: "Test Translation.Get missing Culture Fallback to English",
fallbackCulture: "",
keys: []string{"KEY"},
keyParams: [][]string{},
cultures: []string{"notexists"},
expectedValues: []string{"VALUE"},
errorMessages: []testCaseError{
{
format: "expect Fallback KEY to have Value 'VALUE' but was %[1]s",
params: []string{paramTranslationValue},
},
},
},
{
name: "Test Translation.Get can change Fallback to German",
fallbackCulture: "de",
keys: []string{"KEY"},
keyParams: [][]string{},
cultures: []string{"notexists"},
expectedValues: []string{"WERT"},
errorMessages: []testCaseError{
{
format: "expect Fallback KEY to have Value 'WERT' but was %[1]s",
params: []string{paramTranslationValue},
},
},
},
{
name: "Test Translation.Get return Error message when Key not exists",
fallbackCulture: "",
keys: []string{"notexists"},
keyParams: [][]string{},
cultures: []string{"de"},
expectedValues: []string{"no value for key notexists found in source de"},
errorMessages: []testCaseError{
{
format: "expect to get the missing key and culture but get: %[1]s",
params: []string{paramTranslationValue},
},
},
},
{
name: "Test Translation.Get with Parameters",
fallbackCulture: "",
keys: []string{"WITH_PARAM", "WITH_PARAM", "WITH_PARAM"},
keyParams: [][]string{{"a", "b"}, {"a", "b"}, {"a", "b"}},
cultures: []string{"de", "en", "fr"},
expectedValues: []string{"WERT a_b", "VALUE a_b", "VALEUR a_b"},
errorMessages: []testCaseError{
{
format: "expect German KEY to have Value 'WERT a_b' but was %[1]s",
params: []string{paramTranslationValue},
},
{
format: "expect English KEY to have Value 'VALUE a_b' but was %[1]s",
params: []string{paramTranslationValue},
},
{
format: "expect France KEY to have Value 'VALEUR a_b' but was %[1]s",
params: []string{paramTranslationValue},
},
},
},
}
}
func TestMain(m *testing.M) {
translation.Init(exampletranslations.Files)
code := m.Run()
os.Exit(code)
}
func Test(t *testing.T) {
for _, tc := range getTestCases() {
t.Run(tc.name, func(t *testing.T) {
if len(tc.fallbackCulture) > 0 {
translation.SetDefaultCulture(tc.fallbackCulture)
}
for idx := range tc.keys {
value := translation.Get(tc.keys[idx], tc.cultures[idx])
if idx < len(tc.keyParams) {
value = translation.Get(tc.keys[idx], tc.cultures[idx], tc.keyParams[idx]...)
}
if len(tc.fallbackCulture) > 0 {
translation.SetDefaultCulture("en")
}
if value != tc.expectedValues[idx] {
params := make([]any, 0)
for _, key := range tc.errorMessages[idx].params {
if key == paramTranslationValue {
params = append(params, value)
continue
}
params = append(params, key)
}
t.Errorf(tc.errorMessages[idx].format, params...)
func TestTranslationService_Init_And_Get_de(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("KEY", "de")
if value != "WERT" {
t.Errorf("expect 'WERT' for key 'KEY' but was %s", value)
return
}
}
func TestTranslationService_Get_de_en_fr(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("KEY", "de")
if value != "WERT" {
t.Errorf("expect 'WERT' for key 'KEY' but was %s", value)
return
}
})
value = translationService.Get("KEY", "en")
if value != "VALUE" {
t.Errorf("expect 'VALUE' for key 'KEY' but was %s", value)
return
}
value = translationService.Get("KEY", "fr")
if value != "VALEUR" {
t.Errorf("expect 'VALEUR' for key 'KEY' but was %s", value)
return
}
}
func TestTranslationService_Fallback_Is_en(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]("TestTranslationService_Fallback_Is_en")
translationService.Init(exampletranslations.Files)
value := translationService.Get("KEY", "notexists")
if value != "VALUE" {
t.Errorf("expect 'VALUE' for key 'KEY' as Fallback but was %s", value)
return
}
}
func TestTranslationService_SetDefaultCulture_de(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]("TestTranslationService_SetDefaultCulture_de")
translationService.Init(exampletranslations.Files)
translationService.SetDefaultCulture("de")
value := translationService.Get("KEY", "notexists")
if value != "WERT" {
t.Errorf("expect 'WERT' for key 'KEY' as Fallback but was %s", value)
return
}
}
func TestTranslationService_Error_Message_On_Key_Not_Exists(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("notexists", "de")
if value != "no value for key notexists found in source de" {
t.Errorf("expect Error Message 'no value for key notexists found in source de' but was %s", value)
return
}
}
func TestTranslationService_Get_Key_With_Parameter(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("WITH_PARAM", "de", "a", "b")
if value != "WERT a_b" {
t.Errorf("expect 'WERT a_b' for key 'WITH_PARAM' but was %s", value)
return
}
value = translationService.Get("WITH_PARAM", "en", "a", "b")
if value != "VALUE a_b" {
t.Errorf("expect 'VALUE a_b' for key 'WITH_PARAM' but was %s", value)
return
}
value = translationService.Get("WITH_PARAM", "fr", "a", "b")
if value != "VALEUR a_b" {
t.Errorf("expect 'VALEUR a_b' for key 'WITH_PARAM' but was %s", value)
return
}
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Markus Morgenstern
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,110 +0,0 @@
# Translation
A package that handles translations using JSON files.
## Installation
To include the translation package in your Go project, run the following command:
```bash
go get git.apihub24.de/admin/translation
```
## Usage
You must have a folder containing JSON files. Each file should hold an object with key-value pairs where the value is a string.
- translations/de.json
```json
{
"SOME_KEY": "ein Wert"
}
```
- translations/en.json
```json
{
"SOME_KEY": "a Value"
}
```
Next, you can create a Go file that embeds these JSON files using embed.FS.
- translations/files.go
```go
package translations
import "embed"
//go:embed *.json
var Files embed.FS
```
Now, use the translation package to set up and retrieve translations on demand:
```go
package main
import (
di "git.apihub24.de/admin/generic-di"
"git.apihub24.de/admin/translation"
translations "{link to your translations folder}"
)
func main() {
// get the Service from DI
translationService := di.Inject[translation.ITranslationService]()
// give the Init Function the translations fs.Files
translationService.Init(translations.Files)
// Optional you can change the Fallback Language
translationService.SetDefaultCulture("de")
// get the Translation Values
translationService.Get("SOME_KEY", "de")
}
```
### Error Handling & Fallback Behavior
- If a translation key is not found for the requested language, the package will attempt to retrieve the translation from the default culture (set via SetDefaultCulture).
- If the key is not found even in the default culture, or if the specified language file does not exist, translation.Get will return the following string 'no value for key {key} found in source {culture}'. This helps in identifying missing translations directly in your application.
## Features
### Translation Keys
You can use any translation key you want. The only thing to keep in mind is that your translation files must have the translation key as part of their filename.
For example, if you want to split your translation files into modules, you can name them like this:
- translations/global_de.json
- translations/global_en.json
- translations/mod1_de.json
- translations/mod1_en.json
- translations/mod2_de.json
- translations/mod2_en.json
And the translation call would look like this:
```go
translationService.Get("SOME_KEY", "global_de")
```
### Parameters
You can use placeholders to create dynamic translation values.
For example:
```json
{
"DYNAMIC": "Preis: %[1]s %[1]s"
}
```
```go
translationService.Get("DYNAMIC", "de", "12,50", "€")
```

View File

@ -1,4 +0,0 @@
{
"KEY": "WERT",
"WITH_PARAM": "WERT %[1]s_%[2]s"
}

View File

@ -1,4 +0,0 @@
{
"KEY": "VALUE",
"WITH_PARAM": "VALUE %[1]s_%[2]s"
}

View File

@ -1,6 +0,0 @@
package exampletranslations
import "embed"
//go:embed *.json
var Files embed.FS

View File

@ -1,4 +0,0 @@
{
"KEY": "VALEUR",
"WITH_PARAM": "VALEUR %[1]s_%[2]s"
}

View File

@ -1,5 +0,0 @@
module git.apihub24.de/admin/translation/v2
go 1.22.0
require git.apihub24.de/admin/generic-di v1.4.0

View File

@ -1,2 +0,0 @@
test:
- go test .\...

View File

@ -1,90 +0,0 @@
package translation
import (
"embed"
"encoding/json"
"fmt"
"strings"
di "git.apihub24.de/admin/generic-di"
)
func init() {
di.Injectable(newTranslationService)
}
type ITranslationService interface {
Init(files embed.FS)
SetDefaultCulture(culture string)
Get(key string, culture string, args ...string) string
GetSource(culture string) map[string]string
}
type translationService struct {
defaultCulture string
translationFiles embed.FS
sources map[string]map[string]string
}
func newTranslationService() ITranslationService {
return &translationService{
defaultCulture: "en",
translationFiles: embed.FS{},
sources: make(map[string]map[string]string),
}
}
func (translation *translationService) Init(files embed.FS) {
translation.translationFiles = files
}
func (translation *translationService) SetDefaultCulture(culture string) {
translation.defaultCulture = strings.ToLower(culture)
}
func (translation *translationService) Get(key string, culture string, args ...string) string {
source, err := translation.loadSource(culture)
if err != nil {
return fmt.Sprintf("unknown error: %s", err.Error())
}
value, ok := source[key]
if !ok {
return fmt.Sprintf("no value for key %s found in source %s", key, culture)
}
if len(args) > 0 {
var tmpArgs = make([]any, len(args))
for idx, arg := range args {
tmpArgs[idx] = arg
}
return fmt.Sprintf(value, tmpArgs...)
}
return value
}
func (translation *translationService) GetSource(culture string) map[string]string {
source, err := translation.loadSource(culture)
if err != nil {
return make(map[string]string)
}
return source
}
func (translation *translationService) loadSource(culture string) (map[string]string, error) {
source, ok := translation.sources[culture]
if ok {
return source, nil
}
data, err := translation.translationFiles.ReadFile(fmt.Sprintf("%s.json", strings.ToLower(culture)))
if err != nil {
data, err = translation.translationFiles.ReadFile(fmt.Sprintf("%s.json", translation.defaultCulture))
if err != nil {
return source, fmt.Errorf("can not load translation source for culture %s DefaultCulture: %s", culture, translation.defaultCulture)
}
}
err = json.Unmarshal(data, &source)
if err != nil {
return source, fmt.Errorf("can not parse translation source %s", culture)
}
translation.sources[culture] = source
return translation.sources[culture], nil
}

View File

@ -1,108 +0,0 @@
package translation_test
import (
"testing"
di "git.apihub24.de/admin/generic-di"
"git.apihub24.de/admin/translation/v2"
exampletranslations "git.apihub24.de/admin/translation/v2/example_translations"
)
func TestTranslationService_Init_And_Get_de(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("KEY", "de")
if value != "WERT" {
t.Errorf("expect 'WERT' for key 'KEY' but was %s", value)
return
}
}
func TestTranslationService_Get_de_en_fr(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("KEY", "de")
if value != "WERT" {
t.Errorf("expect 'WERT' for key 'KEY' but was %s", value)
return
}
value = translationService.Get("KEY", "en")
if value != "VALUE" {
t.Errorf("expect 'VALUE' for key 'KEY' but was %s", value)
return
}
value = translationService.Get("KEY", "fr")
if value != "VALEUR" {
t.Errorf("expect 'VALEUR' for key 'KEY' but was %s", value)
return
}
}
func TestTranslationService_Fallback_Is_en(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]("TestTranslationService_Fallback_Is_en")
translationService.Init(exampletranslations.Files)
value := translationService.Get("KEY", "notexists")
if value != "VALUE" {
t.Errorf("expect 'VALUE' for key 'KEY' as Fallback but was %s", value)
return
}
}
func TestTranslationService_SetDefaultCulture_de(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]("TestTranslationService_SetDefaultCulture_de")
translationService.Init(exampletranslations.Files)
translationService.SetDefaultCulture("de")
value := translationService.Get("KEY", "notexists")
if value != "WERT" {
t.Errorf("expect 'WERT' for key 'KEY' as Fallback but was %s", value)
return
}
}
func TestTranslationService_Error_Message_On_Key_Not_Exists(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("notexists", "de")
if value != "no value for key notexists found in source de" {
t.Errorf("expect Error Message 'no value for key notexists found in source de' but was %s", value)
return
}
}
func TestTranslationService_Get_Key_With_Parameter(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
value := translationService.Get("WITH_PARAM", "de", "a", "b")
if value != "WERT a_b" {
t.Errorf("expect 'WERT a_b' for key 'WITH_PARAM' but was %s", value)
return
}
value = translationService.Get("WITH_PARAM", "en", "a", "b")
if value != "VALUE a_b" {
t.Errorf("expect 'VALUE a_b' for key 'WITH_PARAM' but was %s", value)
return
}
value = translationService.Get("WITH_PARAM", "fr", "a", "b")
if value != "VALEUR a_b" {
t.Errorf("expect 'VALEUR a_b' for key 'WITH_PARAM' but was %s", value)
return
}
}
func TestTranslationService_GetSource(t *testing.T) {
translationService := di.Inject[translation.ITranslationService]()
translationService.Init(exampletranslations.Files)
data := translationService.GetSource("de")
if data == nil {
t.Errorf("no map get from source")
return
}
if v, ok := data["KEY"]; !ok || v != "WERT" {
t.Errorf("missing key 'KEY' with value 'WERT'")
return
}
if v, ok := data["WITH_PARAM"]; !ok || v != "WERT %[1]s_%[2]s" {
t.Errorf("missing key 'WITH_PARAM' with value 'WERT %%[1]s_%%[2]s'")
return
}
}