go-i18n/core_service.go
Virgil 0baf2d3e7f
Some checks are pending
Test / test (push) Waiting to run
Security Scan / security (push) Successful in 11s
fix(i18n): preserve missing-key hooks in core service
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 02:07:11 +00:00

131 lines
3.4 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package i18n
import (
"context"
"io/fs"
"sync"
"dappco.re/go/core"
)
// CoreService wraps the i18n Service as a Core framework service.
// Register with: core.WithName("i18n", i18n.NewCoreService(i18n.ServiceOptions{}))
type CoreService struct {
*core.ServiceRuntime[ServiceOptions]
svc *Service
missingKeys []MissingKey
missingKeysMu sync.Mutex
hookInstalled bool
}
// ServiceOptions configures the i18n Core service.
type ServiceOptions struct {
// Language overrides auto-detection (e.g., "en-GB", "de")
Language string
// Mode sets the translation mode (Normal, Strict, Collect)
Mode Mode
// ExtraFS loads additional translation files on top of the embedded defaults.
// Each entry is an fs.FS + directory path within it.
ExtraFS []FSSource
}
// FSSource pairs a filesystem with a directory path for loading translations.
type FSSource struct {
FS fs.FS
Dir string
}
// NewCoreService creates an i18n Core service factory.
// Automatically loads locale filesystems from:
// 1. Embedded go-i18n base translations (grammar, verbs, nouns)
// 2. ExtraFS sources passed via ServiceOptions
func NewCoreService(opts ServiceOptions) func(*core.Core) (any, error) {
return func(c *core.Core) (any, error) {
svc, err := New()
if err != nil {
return nil, err
}
for _, src := range opts.ExtraFS {
loader := NewFSLoader(src.FS, src.Dir)
if addErr := svc.AddLoader(loader); addErr != nil {
// Non-fatal — skip sources that fail (e.g. missing language files)
continue
}
}
// Preserve the same init-time locale registration behaviour used by Init().
// Core bootstrap should not bypass packages that registered locale files
// before the service was constructed.
loadRegisteredLocales(svc)
if opts.Language != "" {
if langErr := svc.SetLanguage(opts.Language); langErr != nil {
return nil, core.Wrap(langErr, "NewCoreService", core.Sprintf("i18n: invalid language %q", opts.Language))
}
}
svc.SetMode(opts.Mode)
SetDefault(svc)
return &CoreService{
ServiceRuntime: core.NewServiceRuntime(c, opts),
svc: svc,
missingKeys: make([]MissingKey, 0),
}, nil
}
}
// OnStartup initialises the i18n service.
func (s *CoreService) OnStartup(_ context.Context) error {
if s.svc.Mode() == ModeCollect {
s.ensureMissingKeyCollector()
}
return nil
}
func (s *CoreService) ensureMissingKeyCollector() {
if s.hookInstalled {
return
}
appendMissingKeyHandler(s.handleMissingKey)
s.hookInstalled = true
}
func (s *CoreService) handleMissingKey(mk MissingKey) {
s.missingKeysMu.Lock()
defer s.missingKeysMu.Unlock()
s.missingKeys = append(s.missingKeys, mk)
}
// MissingKeys returns all missing keys collected in collect mode.
func (s *CoreService) MissingKeys() []MissingKey {
s.missingKeysMu.Lock()
defer s.missingKeysMu.Unlock()
result := make([]MissingKey, len(s.missingKeys))
copy(result, s.missingKeys)
return result
}
// ClearMissingKeys resets the collected missing keys.
func (s *CoreService) ClearMissingKeys() {
s.missingKeysMu.Lock()
defer s.missingKeysMu.Unlock()
s.missingKeys = s.missingKeys[:0]
}
// SetMode changes the translation mode.
func (s *CoreService) SetMode(mode Mode) {
s.svc.SetMode(mode)
if mode == ModeCollect {
s.ensureMissingKeyCollector()
}
}
// Mode returns the current translation mode.
func (s *CoreService) Mode() Mode {
return s.svc.Mode()
}