Commit avant changement d'agent vers devstral
This commit is contained in:
139
internal/i18n/i18n.go
Normal file
139
internal/i18n/i18n.go
Normal file
@ -0,0 +1,139 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Translator manages translations for multiple languages
|
||||
type Translator struct {
|
||||
translations map[string]map[string]interface{}
|
||||
mu sync.RWMutex
|
||||
defaultLang string
|
||||
}
|
||||
|
||||
// New creates a new Translator with the specified default language
|
||||
func New(defaultLang string) *Translator {
|
||||
t := &Translator{
|
||||
translations: make(map[string]map[string]interface{}),
|
||||
defaultLang: defaultLang,
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// LoadFromDir loads all translation files from a directory
|
||||
func (t *Translator) LoadFromDir(dir string) error {
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list translation files: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
lang := strings.TrimSuffix(filepath.Base(file), ".json")
|
||||
if err := t.LoadLanguage(lang, file); err != nil {
|
||||
return fmt.Errorf("failed to load language %s: %w", lang, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadLanguage loads translations for a specific language from a JSON file
|
||||
func (t *Translator) LoadLanguage(lang, filePath string) error {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read translation file: %w", err)
|
||||
}
|
||||
|
||||
var translations map[string]interface{}
|
||||
if err := json.Unmarshal(data, &translations); err != nil {
|
||||
return fmt.Errorf("failed to parse translation file: %w", err)
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
t.translations[lang] = translations
|
||||
t.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// T translates a key for the given language with optional arguments
|
||||
// Key format: "section.subsection.key" (e.g., "menu.home")
|
||||
// Arguments can be passed as a map for variable interpolation
|
||||
func (t *Translator) T(lang, key string, args ...map[string]string) string {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
// Try to get translation for specified language
|
||||
translation := t.getTranslation(lang, key)
|
||||
|
||||
// Fallback to default language if not found
|
||||
if translation == "" && lang != t.defaultLang {
|
||||
translation = t.getTranslation(t.defaultLang, key)
|
||||
}
|
||||
|
||||
// Return key if no translation found
|
||||
if translation == "" {
|
||||
return key
|
||||
}
|
||||
|
||||
// Interpolate variables if args provided
|
||||
if len(args) > 0 && args[0] != nil {
|
||||
for k, v := range args[0] {
|
||||
placeholder := fmt.Sprintf("{{%s}}", k)
|
||||
translation = strings.ReplaceAll(translation, placeholder, v)
|
||||
}
|
||||
}
|
||||
|
||||
return translation
|
||||
}
|
||||
|
||||
// getTranslation retrieves a translation by key using dot notation
|
||||
func (t *Translator) getTranslation(lang, key string) string {
|
||||
langTranslations, ok := t.translations[lang]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(key, ".")
|
||||
var current interface{} = langTranslations
|
||||
|
||||
for _, part := range parts {
|
||||
if m, ok := current.(map[string]interface{}); ok {
|
||||
current = m[part]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
if str, ok := current.(string); ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetAvailableLanguages returns a list of loaded languages
|
||||
func (t *Translator) GetAvailableLanguages() []string {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
langs := make([]string, 0, len(t.translations))
|
||||
for lang := range t.translations {
|
||||
langs = append(langs, lang)
|
||||
}
|
||||
return langs
|
||||
}
|
||||
|
||||
// GetTranslations returns all translations for a specific language
|
||||
func (t *Translator) GetTranslations(lang string) (map[string]interface{}, bool) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
translations, ok := t.translations[lang]
|
||||
return translations, ok
|
||||
}
|
||||
123
internal/i18n/i18n_test.go
Normal file
123
internal/i18n/i18n_test.go
Normal file
@ -0,0 +1,123 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTranslator(t *testing.T) {
|
||||
// Create temporary test translations
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
enFile := filepath.Join(tmpDir, "en.json")
|
||||
enContent := `{
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"search": "Search"
|
||||
},
|
||||
"editor": {
|
||||
"confirmDelete": "Are you sure you want to delete {{filename}}?"
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(enFile, []byte(enContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
frFile := filepath.Join(tmpDir, "fr.json")
|
||||
frContent := `{
|
||||
"menu": {
|
||||
"home": "Accueil",
|
||||
"search": "Rechercher"
|
||||
},
|
||||
"editor": {
|
||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer {{filename}} ?"
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(frFile, []byte(frContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test translator
|
||||
trans := New("en")
|
||||
if err := trans.LoadFromDir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to load translations: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lang string
|
||||
key string
|
||||
args map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "English simple key",
|
||||
lang: "en",
|
||||
key: "menu.home",
|
||||
expected: "Home",
|
||||
},
|
||||
{
|
||||
name: "French simple key",
|
||||
lang: "fr",
|
||||
key: "menu.search",
|
||||
expected: "Rechercher",
|
||||
},
|
||||
{
|
||||
name: "English with interpolation",
|
||||
lang: "en",
|
||||
key: "editor.confirmDelete",
|
||||
args: map[string]string{"filename": "test.md"},
|
||||
expected: "Are you sure you want to delete test.md?",
|
||||
},
|
||||
{
|
||||
name: "French with interpolation",
|
||||
lang: "fr",
|
||||
key: "editor.confirmDelete",
|
||||
args: map[string]string{"filename": "test.md"},
|
||||
expected: "Êtes-vous sûr de vouloir supprimer test.md ?",
|
||||
},
|
||||
{
|
||||
name: "Missing key returns key",
|
||||
lang: "en",
|
||||
key: "missing.key",
|
||||
expected: "missing.key",
|
||||
},
|
||||
{
|
||||
name: "Fallback to default language",
|
||||
lang: "es", // Spanish not loaded, should fallback to English
|
||||
key: "menu.home",
|
||||
expected: "Home",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var result string
|
||||
if tt.args != nil {
|
||||
result = trans.T(tt.lang, tt.key, tt.args)
|
||||
} else {
|
||||
result = trans.T(tt.lang, tt.key)
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("T(%s, %s) = %s, want %s", tt.lang, tt.key, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test GetAvailableLanguages
|
||||
langs := trans.GetAvailableLanguages()
|
||||
if len(langs) != 2 {
|
||||
t.Errorf("Expected 2 languages, got %d", len(langs))
|
||||
}
|
||||
|
||||
// Test GetTranslations
|
||||
enTrans, ok := trans.GetTranslations("en")
|
||||
if !ok {
|
||||
t.Error("Expected to find English translations")
|
||||
}
|
||||
if enTrans == nil {
|
||||
t.Error("English translations should not be nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user