docs(i18n): add comprehensive package documentation
- README.md: Quick start, all features, configuration options - GRAMMAR.md: Verb conjugation, pluralisation, articles, templates - EXTENDING.md: Custom loaders, handlers, framework integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f4e52417ef
commit
e9be13bcd3
3 changed files with 1047 additions and 0 deletions
399
docs/pkg/i18n/EXTENDING.md
Normal file
399
docs/pkg/i18n/EXTENDING.md
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
# Extending the i18n Package
|
||||
|
||||
This guide covers how to extend the i18n package with custom loaders, handlers, and integrations.
|
||||
|
||||
## Custom Loaders
|
||||
|
||||
The `Loader` interface allows loading translations from any source:
|
||||
|
||||
```go
|
||||
type Loader interface {
|
||||
Load(lang string) (map[string]Message, *GrammarData, error)
|
||||
Languages() []string
|
||||
}
|
||||
```
|
||||
|
||||
### Database Loader Example
|
||||
|
||||
```go
|
||||
type PostgresLoader struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (l *PostgresLoader) Languages() []string {
|
||||
rows, err := l.db.Query("SELECT DISTINCT lang FROM translations")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var langs []string
|
||||
for rows.Next() {
|
||||
var lang string
|
||||
rows.Scan(&lang)
|
||||
langs = append(langs, lang)
|
||||
}
|
||||
return langs
|
||||
}
|
||||
|
||||
func (l *PostgresLoader) Load(lang string) (map[string]i18n.Message, *i18n.GrammarData, error) {
|
||||
rows, err := l.db.Query(
|
||||
"SELECT key, text, plural_one, plural_other FROM translations WHERE lang = $1",
|
||||
lang,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
messages := make(map[string]i18n.Message)
|
||||
for rows.Next() {
|
||||
var key, text string
|
||||
var one, other sql.NullString
|
||||
rows.Scan(&key, &text, &one, &other)
|
||||
|
||||
if one.Valid || other.Valid {
|
||||
messages[key] = i18n.Message{One: one.String, Other: other.String}
|
||||
} else {
|
||||
messages[key] = i18n.Message{Text: text}
|
||||
}
|
||||
}
|
||||
|
||||
return messages, nil, nil
|
||||
}
|
||||
|
||||
// Usage
|
||||
svc, err := i18n.NewWithLoader(&PostgresLoader{db: db})
|
||||
```
|
||||
|
||||
### Remote API Loader Example
|
||||
|
||||
```go
|
||||
type APILoader struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (l *APILoader) Languages() []string {
|
||||
resp, _ := l.client.Get(l.baseURL + "/languages")
|
||||
defer resp.Body.Close()
|
||||
|
||||
var langs []string
|
||||
json.NewDecoder(resp.Body).Decode(&langs)
|
||||
return langs
|
||||
}
|
||||
|
||||
func (l *APILoader) Load(lang string) (map[string]i18n.Message, *i18n.GrammarData, error) {
|
||||
resp, err := l.client.Get(l.baseURL + "/translations/" + lang)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var data struct {
|
||||
Messages map[string]i18n.Message `json:"messages"`
|
||||
Grammar *i18n.GrammarData `json:"grammar"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&data)
|
||||
|
||||
return data.Messages, data.Grammar, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Source Loader
|
||||
|
||||
Combine multiple loaders with fallback:
|
||||
|
||||
```go
|
||||
type FallbackLoader struct {
|
||||
primary i18n.Loader
|
||||
secondary i18n.Loader
|
||||
}
|
||||
|
||||
func (l *FallbackLoader) Languages() []string {
|
||||
// Merge languages from both sources
|
||||
langs := make(map[string]bool)
|
||||
for _, lang := range l.primary.Languages() {
|
||||
langs[lang] = true
|
||||
}
|
||||
for _, lang := range l.secondary.Languages() {
|
||||
langs[lang] = true
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(langs))
|
||||
for lang := range langs {
|
||||
result = append(result, lang)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *FallbackLoader) Load(lang string) (map[string]i18n.Message, *i18n.GrammarData, error) {
|
||||
msgs, grammar, err := l.primary.Load(lang)
|
||||
if err != nil {
|
||||
return l.secondary.Load(lang)
|
||||
}
|
||||
|
||||
// Merge with secondary for missing keys
|
||||
secondary, secGrammar, _ := l.secondary.Load(lang)
|
||||
for k, v := range secondary {
|
||||
if _, exists := msgs[k]; !exists {
|
||||
msgs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if grammar == nil {
|
||||
grammar = secGrammar
|
||||
}
|
||||
|
||||
return msgs, grammar, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Handlers
|
||||
|
||||
Handlers process keys before standard lookup. Use for dynamic patterns.
|
||||
|
||||
### Handler Interface
|
||||
|
||||
```go
|
||||
type KeyHandler interface {
|
||||
Match(key string) bool
|
||||
Handle(key string, args []any, next func() string) string
|
||||
}
|
||||
```
|
||||
|
||||
### Emoji Handler Example
|
||||
|
||||
```go
|
||||
type EmojiHandler struct{}
|
||||
|
||||
func (h EmojiHandler) Match(key string) bool {
|
||||
return strings.HasPrefix(key, "emoji.")
|
||||
}
|
||||
|
||||
func (h EmojiHandler) Handle(key string, args []any, next func() string) string {
|
||||
name := strings.TrimPrefix(key, "emoji.")
|
||||
emojis := map[string]string{
|
||||
"success": "✅",
|
||||
"error": "❌",
|
||||
"warning": "⚠️",
|
||||
"info": "ℹ️",
|
||||
}
|
||||
if emoji, ok := emojis[name]; ok {
|
||||
return emoji
|
||||
}
|
||||
return next() // Delegate to next handler
|
||||
}
|
||||
|
||||
// Usage
|
||||
i18n.AddHandler(EmojiHandler{})
|
||||
i18n.T("emoji.success") // "✅"
|
||||
```
|
||||
|
||||
### Conditional Handler Example
|
||||
|
||||
```go
|
||||
type FeatureFlagHandler struct {
|
||||
flags map[string]bool
|
||||
}
|
||||
|
||||
func (h FeatureFlagHandler) Match(key string) bool {
|
||||
return strings.HasPrefix(key, "feature.")
|
||||
}
|
||||
|
||||
func (h FeatureFlagHandler) Handle(key string, args []any, next func() string) string {
|
||||
feature := strings.TrimPrefix(key, "feature.")
|
||||
parts := strings.SplitN(feature, ".", 2)
|
||||
|
||||
if len(parts) < 2 {
|
||||
return next()
|
||||
}
|
||||
|
||||
flag, subkey := parts[0], parts[1]
|
||||
if h.flags[flag] {
|
||||
// Feature enabled - translate the subkey
|
||||
return i18n.T(subkey, args...)
|
||||
}
|
||||
|
||||
// Feature disabled - return empty or fallback
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
### Handler Chain Priority
|
||||
|
||||
```go
|
||||
// Prepend for highest priority (runs first)
|
||||
svc.PrependHandler(CriticalHandler{})
|
||||
|
||||
// Append for lower priority (runs after defaults)
|
||||
svc.AddHandler(FallbackHandler{})
|
||||
|
||||
// Clear all handlers
|
||||
svc.ClearHandlers()
|
||||
|
||||
// Add back defaults
|
||||
svc.AddHandler(i18n.DefaultHandlers()...)
|
||||
```
|
||||
|
||||
## Integrating with Frameworks
|
||||
|
||||
### Cobra CLI
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// Initialise i18n before command setup
|
||||
if err := i18n.Init(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "myapp",
|
||||
Short: i18n.T("cmd.root.short"),
|
||||
Long: i18n.T("cmd.root.long"),
|
||||
}
|
||||
|
||||
var buildCmd = &cobra.Command{
|
||||
Use: "build",
|
||||
Short: i18n.T("cmd.build.short"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println(i18n.T("i18n.progress.build"))
|
||||
// ...
|
||||
fmt.Println(i18n.T("i18n.done.build", "project"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Error Messages
|
||||
|
||||
```go
|
||||
type LocalisedError struct {
|
||||
Key string
|
||||
Args map[string]any
|
||||
}
|
||||
|
||||
func (e LocalisedError) Error() string {
|
||||
return i18n.T(e.Key, e.Args)
|
||||
}
|
||||
|
||||
// Usage
|
||||
return LocalisedError{
|
||||
Key: "error.file_not_found",
|
||||
Args: map[string]any{"Name": filename},
|
||||
}
|
||||
```
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```go
|
||||
func LogInfo(key string, args ...any) {
|
||||
msg := i18n.T(key, args...)
|
||||
slog.Info(msg, "i18n_key", key)
|
||||
}
|
||||
|
||||
func LogError(key string, err error, args ...any) {
|
||||
msg := i18n.T(key, args...)
|
||||
slog.Error(msg, "i18n_key", key, "error", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Mock Loader for Tests
|
||||
|
||||
```go
|
||||
type MockLoader struct {
|
||||
messages map[string]map[string]i18n.Message
|
||||
}
|
||||
|
||||
func (l *MockLoader) Languages() []string {
|
||||
langs := make([]string, 0, len(l.messages))
|
||||
for lang := range l.messages {
|
||||
langs = append(langs, lang)
|
||||
}
|
||||
return langs
|
||||
}
|
||||
|
||||
func (l *MockLoader) Load(lang string) (map[string]i18n.Message, *i18n.GrammarData, error) {
|
||||
if msgs, ok := l.messages[lang]; ok {
|
||||
return msgs, nil, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("language not found: %s", lang)
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
func TestMyFeature(t *testing.T) {
|
||||
loader := &MockLoader{
|
||||
messages: map[string]map[string]i18n.Message{
|
||||
"en-GB": {
|
||||
"test.greeting": {Text: "Hello"},
|
||||
"test.farewell": {Text: "Goodbye"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
svc, _ := i18n.NewWithLoader(loader)
|
||||
i18n.SetDefault(svc)
|
||||
|
||||
// Test your code
|
||||
assert.Equal(t, "Hello", i18n.T("test.greeting"))
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Missing Keys
|
||||
|
||||
```go
|
||||
func TestMissingKeys(t *testing.T) {
|
||||
svc, _ := i18n.New(i18n.WithMode(i18n.ModeCollect))
|
||||
i18n.SetDefault(svc)
|
||||
|
||||
var missing []string
|
||||
i18n.OnMissingKey(func(m i18n.MissingKey) {
|
||||
missing = append(missing, m.Key)
|
||||
})
|
||||
|
||||
// Run your code that uses translations
|
||||
runMyFeature()
|
||||
|
||||
// Check for missing keys
|
||||
assert.Empty(t, missing, "Found missing translation keys: %v", missing)
|
||||
}
|
||||
```
|
||||
|
||||
## Hot Reloading
|
||||
|
||||
Implement a loader that watches for file changes:
|
||||
|
||||
```go
|
||||
type HotReloadLoader struct {
|
||||
base *i18n.FSLoader
|
||||
service *i18n.Service
|
||||
watcher *fsnotify.Watcher
|
||||
}
|
||||
|
||||
func (l *HotReloadLoader) Watch() {
|
||||
for {
|
||||
select {
|
||||
case event := <-l.watcher.Events:
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
// Reload translations
|
||||
l.service.LoadFS(os.DirFS("."), "locales")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Cache translations**: The service caches all loaded messages
|
||||
2. **Template caching**: Parsed templates are cached in `sync.Map`
|
||||
3. **Handler chain**: Keep chain short (6 default handlers is fine)
|
||||
4. **Grammar cache**: Grammar lookups are cached per-language
|
||||
|
||||
For high-throughput applications:
|
||||
- Pre-warm the cache by calling common translations at startup
|
||||
- Consider using `Raw()` to bypass handler chain when not needed
|
||||
- Profile with `go test -bench` if performance is critical
|
||||
228
docs/pkg/i18n/GRAMMAR.md
Normal file
228
docs/pkg/i18n/GRAMMAR.md
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
# Grammar Engine
|
||||
|
||||
The i18n grammar engine automatically handles verb conjugation, noun pluralisation, and article selection. It uses a combination of locale-defined rules and built-in English defaults.
|
||||
|
||||
## Verb Conjugation
|
||||
|
||||
### Past Tense
|
||||
|
||||
```go
|
||||
i18n.PastTense("delete") // "deleted"
|
||||
i18n.PastTense("create") // "created"
|
||||
i18n.PastTense("run") // "ran" (irregular)
|
||||
i18n.PastTense("build") // "built" (irregular)
|
||||
```
|
||||
|
||||
**Rules applied (in order):**
|
||||
|
||||
1. Check locale JSON `gram.verb.{verb}.past`
|
||||
2. Check built-in irregular verbs map
|
||||
3. Apply regular conjugation rules:
|
||||
- Ends in 'e' → add 'd' (delete → deleted)
|
||||
- Ends in consonant + 'y' → change to 'ied' (try → tried)
|
||||
- Short verb ending in CVC → double consonant (stop → stopped)
|
||||
- Otherwise → add 'ed' (walk → walked)
|
||||
|
||||
### Gerund (-ing form)
|
||||
|
||||
```go
|
||||
i18n.Gerund("build") // "building"
|
||||
i18n.Gerund("run") // "running"
|
||||
i18n.Gerund("make") // "making"
|
||||
i18n.Gerund("die") // "dying"
|
||||
```
|
||||
|
||||
**Rules applied:**
|
||||
|
||||
1. Check locale JSON `gram.verb.{verb}.gerund`
|
||||
2. Check built-in irregular verbs map
|
||||
3. Apply regular rules:
|
||||
- Ends in 'ie' → change to 'ying' (die → dying)
|
||||
- Ends in 'e' (not 'ee') → drop 'e', add 'ing' (make → making)
|
||||
- Short verb ending in CVC → double consonant (run → running)
|
||||
- Otherwise → add 'ing' (build → building)
|
||||
|
||||
## Noun Pluralisation
|
||||
|
||||
```go
|
||||
i18n.Pluralize("file", 1) // "file"
|
||||
i18n.Pluralize("file", 5) // "files"
|
||||
i18n.Pluralize("child", 2) // "children" (irregular)
|
||||
i18n.Pluralize("analysis", 3) // "analyses" (Latin)
|
||||
```
|
||||
|
||||
**Rules applied (in order):**
|
||||
|
||||
1. Check locale JSON `gram.noun.{noun}.other`
|
||||
2. Check built-in irregular nouns map
|
||||
3. Apply regular rules:
|
||||
- Ends in 's', 'x', 'z', 'ch', 'sh' → add 'es'
|
||||
- Ends in consonant + 'y' → change to 'ies'
|
||||
- Ends in 'f' or 'fe' → change to 'ves' (leaf → leaves)
|
||||
- Otherwise → add 's'
|
||||
|
||||
### Built-in Irregular Nouns
|
||||
|
||||
| Singular | Plural |
|
||||
|----------|--------|
|
||||
| child | children |
|
||||
| person | people |
|
||||
| man | men |
|
||||
| woman | women |
|
||||
| foot | feet |
|
||||
| tooth | teeth |
|
||||
| mouse | mice |
|
||||
| datum | data |
|
||||
| index | indices |
|
||||
| crisis | crises |
|
||||
| fish | fish |
|
||||
| sheep | sheep |
|
||||
|
||||
## Articles
|
||||
|
||||
```go
|
||||
i18n.Article("apple") // "an apple"
|
||||
i18n.Article("banana") // "a banana"
|
||||
i18n.Article("hour") // "an hour" (silent h)
|
||||
i18n.Article("user") // "a user" (y sound)
|
||||
i18n.Article("umbrella") // "an umbrella"
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. Vowel sound words get "an" (a, e, i, o, u start)
|
||||
2. Consonant sound words get "a"
|
||||
3. Exception lists handle:
|
||||
- Silent 'h' words: hour, honest, honour, heir, herb
|
||||
- 'Y' sound words: user, union, unique, unit, universe
|
||||
|
||||
## Composed Messages
|
||||
|
||||
### Labels
|
||||
|
||||
```go
|
||||
i18n.Label("status") // "Status:"
|
||||
i18n.Label("version") // "Version:"
|
||||
```
|
||||
|
||||
Uses `gram.punct.label` suffix (default `:`) from locale.
|
||||
|
||||
### Progress Messages
|
||||
|
||||
```go
|
||||
i18n.Progress("build") // "Building..."
|
||||
i18n.ProgressSubject("check", "config") // "Checking config..."
|
||||
```
|
||||
|
||||
Uses `gram.punct.progress` suffix (default `...`) from locale.
|
||||
|
||||
### Action Results
|
||||
|
||||
```go
|
||||
i18n.ActionResult("delete", "file") // "File deleted"
|
||||
i18n.ActionResult("create", "project") // "Project created"
|
||||
```
|
||||
|
||||
Pattern: `{Title(subject)} {past(verb)}`
|
||||
|
||||
### Action Failures
|
||||
|
||||
```go
|
||||
i18n.ActionFailed("delete", "file") // "Failed to delete file"
|
||||
i18n.ActionFailed("save", "config") // "Failed to save config"
|
||||
```
|
||||
|
||||
Pattern: `Failed to {verb} {subject}`
|
||||
|
||||
## Locale Configuration
|
||||
|
||||
Define grammar in your locale JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"gram": {
|
||||
"verb": {
|
||||
"deploy": {
|
||||
"past": "deployed",
|
||||
"gerund": "deploying"
|
||||
},
|
||||
"sync": {
|
||||
"past": "synced",
|
||||
"gerund": "syncing"
|
||||
}
|
||||
},
|
||||
"noun": {
|
||||
"repository": {
|
||||
"one": "repository",
|
||||
"other": "repositories"
|
||||
},
|
||||
"schema": {
|
||||
"one": "schema",
|
||||
"other": "schemata"
|
||||
}
|
||||
},
|
||||
"article": {
|
||||
"indefinite": {
|
||||
"default": "a",
|
||||
"vowel": "an"
|
||||
},
|
||||
"definite": "the"
|
||||
},
|
||||
"punct": {
|
||||
"label": ":",
|
||||
"progress": "..."
|
||||
},
|
||||
"word": {
|
||||
"status": "status",
|
||||
"version": "version"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template Functions
|
||||
|
||||
Use grammar functions in templates:
|
||||
|
||||
```go
|
||||
template.New("").Funcs(i18n.TemplateFuncs())
|
||||
```
|
||||
|
||||
| Function | Example | Result |
|
||||
|----------|---------|--------|
|
||||
| `past` | `{{past "delete"}}` | "deleted" |
|
||||
| `gerund` | `{{gerund "build"}}` | "building" |
|
||||
| `plural` | `{{plural "file" 5}}` | "files" |
|
||||
| `article` | `{{article "apple"}}` | "an apple" |
|
||||
| `title` | `{{title "hello world"}}` | "Hello World" |
|
||||
| `lower` | `{{lower "HELLO"}}` | "hello" |
|
||||
| `upper` | `{{upper "hello"}}` | "HELLO" |
|
||||
| `quote` | `{{quote "text"}}` | `"text"` |
|
||||
|
||||
## Language-Specific Grammar
|
||||
|
||||
The grammar engine loads language-specific data when available:
|
||||
|
||||
```go
|
||||
// Get grammar data for a language
|
||||
data := i18n.GetGrammarData("de-DE")
|
||||
if data != nil {
|
||||
// Access verb forms, noun forms, etc.
|
||||
}
|
||||
|
||||
// Set grammar data programmatically
|
||||
i18n.SetGrammarData("de-DE", &i18n.GrammarData{
|
||||
Verbs: map[string]i18n.VerbForms{
|
||||
"machen": {Past: "gemacht", Gerund: "machend"},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
Grammar results are computed on-demand but templates are cached:
|
||||
|
||||
- First call: Parse template + apply grammar
|
||||
- Subsequent calls: Reuse cached template
|
||||
|
||||
The template cache uses `sync.Map` for thread-safe concurrent access.
|
||||
420
docs/pkg/i18n/README.md
Normal file
420
docs/pkg/i18n/README.md
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
# i18n Package
|
||||
|
||||
The `pkg/i18n` package provides internationalisation and localisation for Go CLI applications. It features a grammar engine for automatic verb conjugation and noun pluralisation, CLDR plural support, and an extensible handler chain for dynamic key patterns.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
import "github.com/host-uk/core/pkg/i18n"
|
||||
|
||||
func main() {
|
||||
// Initialise with embedded locales
|
||||
svc, err := i18n.New()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
i18n.SetDefault(svc)
|
||||
|
||||
// Translate messages
|
||||
fmt.Println(i18n.T("cli.success")) // "Operation completed"
|
||||
fmt.Println(i18n.T("i18n.count.file", 5)) // "5 files"
|
||||
fmt.Println(i18n.T("i18n.progress.build")) // "Building..."
|
||||
fmt.Println(i18n.T("i18n.done.delete", "config.yaml")) // "Config.yaml deleted"
|
||||
}
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Basic Translation](#basic-translation)
|
||||
- [Template Variables](#template-variables)
|
||||
- [Pluralisation](#pluralisation)
|
||||
- [Magic Namespaces](#magic-namespaces)
|
||||
- [Subjects](#subjects)
|
||||
- [Grammar Engine](#grammar-engine)
|
||||
- [Formality](#formality)
|
||||
- [Modes](#modes)
|
||||
- [Custom Loaders](#custom-loaders)
|
||||
- [Custom Handlers](#custom-handlers)
|
||||
- [Locale File Format](#locale-file-format)
|
||||
|
||||
## Basic Translation
|
||||
|
||||
The `T()` function translates message keys:
|
||||
|
||||
```go
|
||||
// Simple translation
|
||||
msg := i18n.T("cli.success")
|
||||
|
||||
// With template variables
|
||||
msg := i18n.T("error.not_found", map[string]any{
|
||||
"Name": "config.yaml",
|
||||
})
|
||||
```
|
||||
|
||||
Use `Raw()` to bypass magic namespace handling:
|
||||
|
||||
```go
|
||||
// T() handles i18n.* magic
|
||||
i18n.T("i18n.label.status") // "Status:"
|
||||
|
||||
// Raw() does direct lookup only
|
||||
i18n.Raw("i18n.label.status") // Returns key as-is (not in JSON)
|
||||
```
|
||||
|
||||
## Template Variables
|
||||
|
||||
Translation strings support Go templates:
|
||||
|
||||
```json
|
||||
{
|
||||
"greeting": "Hello, {{.Name}}!",
|
||||
"summary": "Found {{.Count}} {{if eq .Count 1}}item{{else}}items{{end}}"
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
i18n.T("greeting", map[string]any{"Name": "World"}) // "Hello, World!"
|
||||
i18n.T("summary", map[string]any{"Count": 3}) // "Found 3 items"
|
||||
```
|
||||
|
||||
### Available Template Functions
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `title` | Title case | `{{title .Name}}` |
|
||||
| `lower` | Lowercase | `{{lower .Name}}` |
|
||||
| `upper` | Uppercase | `{{upper .Name}}` |
|
||||
| `past` | Past tense | `{{past "delete"}}` → "deleted" |
|
||||
| `gerund` | -ing form | `{{gerund "build"}}` → "building" |
|
||||
| `plural` | Pluralise | `{{plural "file" .Count}}` |
|
||||
| `article` | Add article | `{{article "apple"}}` → "an apple" |
|
||||
| `quote` | Add quotes | `{{quote .Name}}` → `"name"` |
|
||||
|
||||
## Pluralisation
|
||||
|
||||
The package supports full CLDR plural categories:
|
||||
|
||||
```json
|
||||
{
|
||||
"item_count": {
|
||||
"zero": "No items",
|
||||
"one": "{{.Count}} item",
|
||||
"two": "{{.Count}} items",
|
||||
"few": "{{.Count}} items",
|
||||
"many": "{{.Count}} items",
|
||||
"other": "{{.Count}} items"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
i18n.T("item_count", map[string]any{"Count": 0}) // "No items" (if zero defined)
|
||||
i18n.T("item_count", map[string]any{"Count": 1}) // "1 item"
|
||||
i18n.T("item_count", map[string]any{"Count": 5}) // "5 items"
|
||||
```
|
||||
|
||||
For simple cases, use `i18n.count.*`:
|
||||
|
||||
```go
|
||||
i18n.T("i18n.count.file", 1) // "1 file"
|
||||
i18n.T("i18n.count.file", 5) // "5 files"
|
||||
```
|
||||
|
||||
## Magic Namespaces
|
||||
|
||||
The `i18n.*` namespace provides automatic message composition:
|
||||
|
||||
### Labels (`i18n.label.*`)
|
||||
|
||||
```go
|
||||
i18n.T("i18n.label.status") // "Status:"
|
||||
i18n.T("i18n.label.version") // "Version:"
|
||||
```
|
||||
|
||||
### Progress (`i18n.progress.*`)
|
||||
|
||||
```go
|
||||
i18n.T("i18n.progress.build") // "Building..."
|
||||
i18n.T("i18n.progress.check", "config") // "Checking config..."
|
||||
```
|
||||
|
||||
### Counts (`i18n.count.*`)
|
||||
|
||||
```go
|
||||
i18n.T("i18n.count.file", 1) // "1 file"
|
||||
i18n.T("i18n.count.file", 5) // "5 files"
|
||||
i18n.T("i18n.count.repo", 10) // "10 repos"
|
||||
```
|
||||
|
||||
### Done (`i18n.done.*`)
|
||||
|
||||
```go
|
||||
i18n.T("i18n.done.delete", "file") // "File deleted"
|
||||
i18n.T("i18n.done.create", "project") // "Project created"
|
||||
```
|
||||
|
||||
### Fail (`i18n.fail.*`)
|
||||
|
||||
```go
|
||||
i18n.T("i18n.fail.delete", "file") // "Failed to delete file"
|
||||
i18n.T("i18n.fail.save", "config") // "Failed to save config"
|
||||
```
|
||||
|
||||
### Numeric (`i18n.numeric.*`)
|
||||
|
||||
```go
|
||||
i18n.N("number", 1234567) // "1,234,567"
|
||||
i18n.N("percent", 0.85) // "85%"
|
||||
i18n.N("bytes", 1536000) // "1.46 MB"
|
||||
i18n.N("ordinal", 1) // "1st"
|
||||
```
|
||||
|
||||
## Subjects
|
||||
|
||||
Subjects provide typed context for translations:
|
||||
|
||||
```go
|
||||
// Create a subject
|
||||
subj := i18n.S("file", "config.yaml")
|
||||
|
||||
// Chain methods for additional context
|
||||
subj := i18n.S("file", files).
|
||||
Count(len(files)).
|
||||
In("workspace").
|
||||
Formal()
|
||||
|
||||
// Use in translations
|
||||
i18n.T("i18n.done.delete", subj.String())
|
||||
```
|
||||
|
||||
### Subject Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `Count(n)` | Set count for pluralisation |
|
||||
| `Gender(g)` | Set grammatical gender |
|
||||
| `In(loc)` | Set location context |
|
||||
| `Formal()` | Set formal address |
|
||||
| `Informal()` | Set informal address |
|
||||
|
||||
## Grammar Engine
|
||||
|
||||
The grammar engine handles verb conjugation and noun forms:
|
||||
|
||||
```go
|
||||
// Verb conjugation
|
||||
i18n.PastTense("delete") // "deleted"
|
||||
i18n.PastTense("run") // "ran" (irregular)
|
||||
i18n.Gerund("build") // "building"
|
||||
i18n.Gerund("run") // "running"
|
||||
|
||||
// Noun pluralisation
|
||||
i18n.Pluralize("file", 1) // "file"
|
||||
i18n.Pluralize("file", 5) // "files"
|
||||
i18n.Pluralize("child", 2) // "children" (irregular)
|
||||
|
||||
// Articles
|
||||
i18n.Article("apple") // "an apple"
|
||||
i18n.Article("banana") // "a banana"
|
||||
|
||||
// Composed messages
|
||||
i18n.Label("status") // "Status:"
|
||||
i18n.Progress("build") // "Building..."
|
||||
i18n.ProgressSubject("check", "cfg") // "Checking cfg..."
|
||||
i18n.ActionResult("delete", "file") // "File deleted"
|
||||
i18n.ActionFailed("save", "config") // "Failed to save config"
|
||||
```
|
||||
|
||||
### Customising Grammar
|
||||
|
||||
Add irregular forms in your locale JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"gram": {
|
||||
"verb": {
|
||||
"deploy": { "past": "deployed", "gerund": "deploying" }
|
||||
},
|
||||
"noun": {
|
||||
"repository": { "one": "repository", "other": "repositories" }
|
||||
},
|
||||
"punct": {
|
||||
"label": ":",
|
||||
"progress": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Formality
|
||||
|
||||
For languages with formal/informal address (German Sie/du, French vous/tu):
|
||||
|
||||
```go
|
||||
// Set service-wide formality
|
||||
svc.SetFormality(i18n.FormalityFormal)
|
||||
|
||||
// Per-translation formality via Subject
|
||||
i18n.T("greeting", i18n.S("user", name).Formal())
|
||||
i18n.T("greeting", i18n.S("user", name).Informal())
|
||||
|
||||
// Per-translation via TranslationContext
|
||||
i18n.T("greeting", i18n.C("customer support").Formal())
|
||||
```
|
||||
|
||||
Define formality variants in JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"greeting": "Hello",
|
||||
"greeting._formal": "Good morning, sir",
|
||||
"greeting._informal": "Hey there"
|
||||
}
|
||||
```
|
||||
|
||||
## Modes
|
||||
|
||||
Three modes control missing key behaviour:
|
||||
|
||||
```go
|
||||
// Normal (default): Returns key as-is
|
||||
i18n.SetMode(i18n.ModeNormal)
|
||||
i18n.T("missing.key") // "missing.key"
|
||||
|
||||
// Strict: Panics on missing keys (dev/CI)
|
||||
i18n.SetMode(i18n.ModeStrict)
|
||||
i18n.T("missing.key") // panic!
|
||||
|
||||
// Collect: Dispatches to handler (QA testing)
|
||||
i18n.SetMode(i18n.ModeCollect)
|
||||
i18n.OnMissingKey(func(m i18n.MissingKey) {
|
||||
log.Printf("MISSING: %s at %s:%d", m.Key, m.CallerFile, m.CallerLine)
|
||||
})
|
||||
```
|
||||
|
||||
## Custom Loaders
|
||||
|
||||
Implement the `Loader` interface for custom storage:
|
||||
|
||||
```go
|
||||
type Loader interface {
|
||||
Load(lang string) (map[string]Message, *GrammarData, error)
|
||||
Languages() []string
|
||||
}
|
||||
```
|
||||
|
||||
Example database loader:
|
||||
|
||||
```go
|
||||
type DBLoader struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (l *DBLoader) Languages() []string {
|
||||
// Query available languages from database
|
||||
}
|
||||
|
||||
func (l *DBLoader) Load(lang string) (map[string]i18n.Message, *i18n.GrammarData, error) {
|
||||
// Load translations from database
|
||||
}
|
||||
|
||||
// Use custom loader
|
||||
svc, err := i18n.NewWithLoader(&DBLoader{db: db})
|
||||
```
|
||||
|
||||
## Custom Handlers
|
||||
|
||||
Add custom key handlers for dynamic patterns:
|
||||
|
||||
```go
|
||||
type MyHandler struct{}
|
||||
|
||||
func (h MyHandler) Match(key string) bool {
|
||||
return strings.HasPrefix(key, "my.prefix.")
|
||||
}
|
||||
|
||||
func (h MyHandler) Handle(key string, args []any, next func() string) string {
|
||||
// Handle the key or call next() to delegate
|
||||
return "custom result"
|
||||
}
|
||||
|
||||
// Add to handler chain
|
||||
svc.AddHandler(MyHandler{}) // Append (lower priority)
|
||||
svc.PrependHandler(MyHandler{}) // Prepend (higher priority)
|
||||
```
|
||||
|
||||
## Locale File Format
|
||||
|
||||
Locale files use nested JSON with dot-notation access:
|
||||
|
||||
```json
|
||||
{
|
||||
"cli": {
|
||||
"success": "Operation completed",
|
||||
"error": {
|
||||
"not_found": "{{.Name}} not found"
|
||||
}
|
||||
},
|
||||
"cmd": {
|
||||
"build": {
|
||||
"short": "Build the project",
|
||||
"long": "Build compiles source files into an executable"
|
||||
}
|
||||
},
|
||||
"gram": {
|
||||
"verb": {
|
||||
"build": { "past": "built", "gerund": "building" }
|
||||
},
|
||||
"noun": {
|
||||
"file": { "one": "file", "other": "files" }
|
||||
},
|
||||
"punct": {
|
||||
"label": ":",
|
||||
"progress": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Access keys with dot notation:
|
||||
|
||||
```go
|
||||
i18n.T("cli.success") // "Operation completed"
|
||||
i18n.T("cli.error.not_found") // "{{.Name}} not found"
|
||||
i18n.T("cmd.build.short") // "Build the project"
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Use functional options when creating a service:
|
||||
|
||||
```go
|
||||
svc, err := i18n.New(
|
||||
i18n.WithFallback("de-DE"), // Fallback language
|
||||
i18n.WithFormality(i18n.FormalityFormal), // Default formality
|
||||
i18n.WithMode(i18n.ModeStrict), // Missing key mode
|
||||
i18n.WithDebug(true), // Show [key] prefix
|
||||
)
|
||||
```
|
||||
|
||||
## Thread Safety
|
||||
|
||||
The package is fully thread-safe:
|
||||
|
||||
- `Service` uses `sync.RWMutex` for state
|
||||
- Global `Default()` uses `atomic.Pointer`
|
||||
- `OnMissingKey` uses `atomic.Value`
|
||||
- `FSLoader.Languages()` uses `sync.Once`
|
||||
|
||||
Safe for concurrent use from multiple goroutines.
|
||||
|
||||
## Debug Mode
|
||||
|
||||
Enable debug mode to see translation keys:
|
||||
|
||||
```go
|
||||
i18n.SetDebug(true)
|
||||
i18n.T("cli.success") // "[cli.success] Operation completed"
|
||||
```
|
||||
|
||||
Useful for identifying which keys are used where.
|
||||
Loading…
Add table
Reference in a new issue