go-i18n/core_service.go
Virgil 9d4af96d3d
Some checks failed
Security Scan / security (push) Successful in 18s
Test / test (push) Has been cancelled
feat(i18n): add service state snapshot aliases
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 12:48:49 +00:00

387 lines
11 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
// Fallback sets the fallback language for missing translations.
Fallback string
// Formality sets the default formality level.
Formality Formality
// Location sets the default location context.
Location string
// Mode sets the translation mode (Normal, Strict, Collect)
Mode Mode
// Debug prefixes translated output with the message key.
Debug bool
// 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, langErr
}
}
if opts.Fallback != "" {
svc.SetFallback(opts.Fallback)
}
if opts.Formality != FormalityNeutral {
svc.SetFormality(opts.Formality)
}
if opts.Location != "" {
svc.SetLocation(opts.Location)
}
svc.SetMode(opts.Mode)
svc.SetDebug(opts.Debug)
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
}
AddMissingKeyHandler(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()
}
// CurrentMode returns the current translation mode.
func (s *CoreService) CurrentMode() Mode {
return s.Mode()
}
// T translates a message through the wrapped i18n service.
func (s *CoreService) T(messageID string, args ...any) string {
return s.svc.T(messageID, args...)
}
// Translate translates a message through the wrapped i18n service.
func (s *CoreService) Translate(messageID string, args ...any) core.Result {
return s.svc.Translate(messageID, args...)
}
// Raw translates without namespace handler magic.
func (s *CoreService) Raw(messageID string, args ...any) string {
return s.svc.Raw(messageID, args...)
}
// AddMessages adds message strings to the wrapped service.
func (s *CoreService) AddMessages(lang string, messages map[string]string) {
s.svc.AddMessages(lang, messages)
}
// SetLanguage changes the wrapped service language.
func (s *CoreService) SetLanguage(lang string) error {
return s.svc.SetLanguage(lang)
}
// Language returns the wrapped service language.
func (s *CoreService) Language() string {
return s.svc.Language()
}
// CurrentLanguage returns the wrapped service language.
func (s *CoreService) CurrentLanguage() string {
return s.Language()
}
// CurrentLang is a short alias for CurrentLanguage.
func (s *CoreService) CurrentLang() string {
return s.CurrentLanguage()
}
// Prompt translates a prompt key from the prompt namespace using the wrapped service.
func (s *CoreService) Prompt(key string) string {
return s.svc.Prompt(key)
}
// CurrentPrompt is a short alias for Prompt.
func (s *CoreService) CurrentPrompt(key string) string {
return s.Prompt(key)
}
// Lang translates a language label from the lang namespace using the wrapped service.
func (s *CoreService) Lang(key string) string {
return s.svc.Lang(key)
}
// SetFallback changes the wrapped service fallback language.
func (s *CoreService) SetFallback(lang string) {
s.svc.SetFallback(lang)
}
// Fallback returns the wrapped service fallback language.
func (s *CoreService) Fallback() string {
return s.svc.Fallback()
}
// CurrentFallback returns the wrapped service fallback language.
func (s *CoreService) CurrentFallback() string {
return s.Fallback()
}
// SetFormality changes the wrapped service default formality.
func (s *CoreService) SetFormality(f Formality) {
s.svc.SetFormality(f)
}
// Formality returns the wrapped service default formality.
func (s *CoreService) Formality() Formality {
return s.svc.Formality()
}
// CurrentFormality returns the wrapped service default formality.
func (s *CoreService) CurrentFormality() Formality {
return s.Formality()
}
// SetLocation changes the wrapped service default location.
func (s *CoreService) SetLocation(location string) {
s.svc.SetLocation(location)
}
// Location returns the wrapped service default location.
func (s *CoreService) Location() string {
return s.svc.Location()
}
// CurrentLocation returns the wrapped service default location.
func (s *CoreService) CurrentLocation() string {
return s.Location()
}
// SetDebug changes the wrapped service debug mode.
func (s *CoreService) SetDebug(enabled bool) {
s.svc.SetDebug(enabled)
}
// Debug reports whether wrapped service debug mode is enabled.
func (s *CoreService) Debug() bool {
return s.svc.Debug()
}
// CurrentDebug reports whether wrapped service debug mode is enabled.
func (s *CoreService) CurrentDebug() bool {
return s.Debug()
}
// State returns a copy-safe snapshot of the wrapped service configuration.
func (s *CoreService) State() ServiceState {
if s == nil || s.svc == nil {
return State()
}
return s.svc.State()
}
// CurrentState is a more explicit alias for State.
func (s *CoreService) CurrentState() ServiceState {
return s.State()
}
// AddHandler appends handlers to the wrapped service's chain.
func (s *CoreService) AddHandler(handlers ...KeyHandler) {
s.svc.AddHandler(handlers...)
}
// SetHandlers replaces the wrapped service's handler chain.
func (s *CoreService) SetHandlers(handlers ...KeyHandler) {
s.svc.SetHandlers(handlers...)
}
// PrependHandler inserts handlers at the front of the wrapped service's chain.
func (s *CoreService) PrependHandler(handlers ...KeyHandler) {
s.svc.PrependHandler(handlers...)
}
// ClearHandlers removes all handlers from the wrapped service.
func (s *CoreService) ClearHandlers() {
s.svc.ClearHandlers()
}
// ResetHandlers restores the wrapped service's default handler chain.
func (s *CoreService) ResetHandlers() {
s.svc.ResetHandlers()
}
// Handlers returns a copy of the wrapped service's handler chain.
func (s *CoreService) Handlers() []KeyHandler {
return s.svc.Handlers()
}
// CurrentHandlers returns a copy of the wrapped service's handler chain.
func (s *CoreService) CurrentHandlers() []KeyHandler {
return s.Handlers()
}
// AddLoader loads extra locale data into the wrapped service.
func (s *CoreService) AddLoader(loader Loader) error {
return s.svc.AddLoader(loader)
}
// LoadFS loads locale data from a filesystem into the wrapped service.
func (s *CoreService) LoadFS(fsys fs.FS, dir string) error {
return s.svc.LoadFS(fsys, dir)
}
// AvailableLanguages returns the wrapped service languages.
func (s *CoreService) AvailableLanguages() []string {
return s.svc.AvailableLanguages()
}
// CurrentAvailableLanguages returns the wrapped service languages.
func (s *CoreService) CurrentAvailableLanguages() []string {
return s.AvailableLanguages()
}
// Direction returns the wrapped service text direction.
func (s *CoreService) Direction() TextDirection {
return s.svc.Direction()
}
// CurrentDirection returns the wrapped service text direction.
func (s *CoreService) CurrentDirection() TextDirection {
return s.Direction()
}
// CurrentTextDirection is a more explicit alias for CurrentDirection.
func (s *CoreService) CurrentTextDirection() TextDirection {
return s.CurrentDirection()
}
// IsRTL reports whether the wrapped service language is right-to-left.
func (s *CoreService) IsRTL() bool {
return s.svc.IsRTL()
}
// RTL reports whether the wrapped service language is right-to-left.
func (s *CoreService) RTL() bool {
return s.IsRTL()
}
// CurrentIsRTL reports whether the wrapped service language is right-to-left.
func (s *CoreService) CurrentIsRTL() bool {
return s.IsRTL()
}
// CurrentRTL reports whether the wrapped service language is right-to-left.
func (s *CoreService) CurrentRTL() bool {
return s.CurrentIsRTL()
}
// PluralCategory returns the plural category for the wrapped service language.
func (s *CoreService) PluralCategory(n int) PluralCategory {
return s.svc.PluralCategory(n)
}
// CurrentPluralCategory returns the plural category for the wrapped service language.
func (s *CoreService) CurrentPluralCategory(n int) PluralCategory {
return s.PluralCategory(n)
}
// PluralCategoryOf is a short alias for CurrentPluralCategory.
func (s *CoreService) PluralCategoryOf(n int) PluralCategory {
return s.CurrentPluralCategory(n)
}