package cli import ( "context" "sync" "forge.lthn.ai/core/go/pkg/framework" "forge.lthn.ai/core/go/pkg/i18n" ) // I18nService wraps i18n as a Core service. type I18nService struct { *framework.ServiceRuntime[I18nOptions] svc *i18n.Service // Collect mode state missingKeys []i18n.MissingKey missingKeysMu sync.Mutex } // I18nOptions configures the i18n service. type I18nOptions struct { // Language overrides auto-detection (e.g., "en-GB", "de") Language string // Mode sets the translation mode (Normal, Strict, Collect) Mode i18n.Mode } // NewI18nService creates an i18n service factory. func NewI18nService(opts I18nOptions) func(*framework.Core) (any, error) { return func(c *framework.Core) (any, error) { svc, err := i18n.New() if err != nil { return nil, err } if opts.Language != "" { _ = svc.SetLanguage(opts.Language) } // Set mode if specified svc.SetMode(opts.Mode) // Set as global default so i18n.T() works everywhere i18n.SetDefault(svc) return &I18nService{ ServiceRuntime: framework.NewServiceRuntime(c, opts), svc: svc, missingKeys: make([]i18n.MissingKey, 0), }, nil } } // OnStartup initialises the i18n service. func (s *I18nService) OnStartup(ctx context.Context) error { s.Core().RegisterQuery(s.handleQuery) // Register action handler for collect mode if s.svc.Mode() == i18n.ModeCollect { i18n.OnMissingKey(s.handleMissingKey) } return nil } // handleMissingKey accumulates missing keys in collect mode. func (s *I18nService) handleMissingKey(mk i18n.MissingKey) { s.missingKeysMu.Lock() defer s.missingKeysMu.Unlock() s.missingKeys = append(s.missingKeys, mk) } // MissingKeys returns all missing keys collected in collect mode. // Call this at the end of a QA session to report missing translations. func (s *I18nService) MissingKeys() []i18n.MissingKey { s.missingKeysMu.Lock() defer s.missingKeysMu.Unlock() result := make([]i18n.MissingKey, len(s.missingKeys)) copy(result, s.missingKeys) return result } // ClearMissingKeys resets the collected missing keys. func (s *I18nService) ClearMissingKeys() { s.missingKeysMu.Lock() defer s.missingKeysMu.Unlock() s.missingKeys = s.missingKeys[:0] } // SetMode changes the translation mode. func (s *I18nService) SetMode(mode i18n.Mode) { s.svc.SetMode(mode) // Update action handler registration if mode == i18n.ModeCollect { i18n.OnMissingKey(s.handleMissingKey) } else { i18n.OnMissingKey(nil) } } // Mode returns the current translation mode. func (s *I18nService) Mode() i18n.Mode { return s.svc.Mode() } // Queries for i18n service // QueryTranslate requests a translation. type QueryTranslate struct { Key string Args map[string]any } func (s *I18nService) handleQuery(c *framework.Core, q framework.Query) (any, bool, error) { switch m := q.(type) { case QueryTranslate: return s.svc.T(m.Key, m.Args), true, nil } return nil, false, nil } // T translates a key with optional arguments. func (s *I18nService) T(key string, args ...map[string]any) string { if len(args) > 0 { return s.svc.T(key, args[0]) } return s.svc.T(key) } // SetLanguage changes the current language. func (s *I18nService) SetLanguage(lang string) { _ = s.svc.SetLanguage(lang) } // Language returns the current language. func (s *I18nService) Language() string { return s.svc.Language() } // AvailableLanguages returns all available languages. func (s *I18nService) AvailableLanguages() []string { return s.svc.AvailableLanguages() } // --- Package-level convenience --- // T translates a key using the CLI's i18n service. // Falls back to the global i18n.T if CLI not initialised. func T(key string, args ...map[string]any) string { if instance == nil { // CLI not initialised, use global i18n if len(args) > 0 { return i18n.T(key, args[0]) } return i18n.T(key) } svc, err := framework.ServiceFor[*I18nService](instance.core, "i18n") if err != nil { // i18n service not registered, use global if len(args) > 0 { return i18n.T(key, args[0]) } return i18n.T(key) } return svc.T(key, args...) }