* feat: Add TDD tests for core package Adds a new `tdd/` directory for TDD-style contract tests. Implements a comprehensive test suite for the `pkg/core` package, covering: - `New()` - `WithService()` - `WithName()` - `WithWails()` - `WithAssets()` - `WithServiceLock()` - `RegisterService()` - `Service()` - `ServiceFor()` - `MustServiceFor()` - `ACTION()` - `RegisterAction()` - `RegisterActions()` To support testing, a public `Assets()` method was added to the `Core` struct. * feat: Add TDD tests for e, io, runtime, and config packages Adds comprehensive TDD tests to the `tdd/` directory for the following packages: - `pkg/e` - `pkg/io` - `pkg/runtime` - `pkg/config` This significantly improves the test coverage of the project. To support testing the `runtime` package, the `newWithFactories` function was exported as `NewWithFactories`. The existing tests for the `config` package were moved from the `internal` package to the `tdd/` directory and adapted to use the public API. * fix: Update tdd tests for config, core, and runtime Updates the TDD tests for the `config`, `core`, and `runtime` packages to improve their coverage and correctness. - In `tdd/config_test.go`, the `TestIsFeatureEnabled` test is updated to use `s.Set` to modify the `features` slice, ensuring that the persistence logic is exercised. - In `tdd/core_test.go`, the `TestCore_WithAssets_Good` test is updated to use a real embedded filesystem with `//go:embed` to verify the contents of a test file. - In `tdd/runtime_test.go`, the `TestNew_Good` test is converted to a table-driven test to cover the happy path, error cases, and a case with a non-nil `application.App`. * fix: Fix build and improve test coverage This commit fixes a build failure in the `pkg/runtime` tests and significantly improves the test coverage for several packages. - Fixes a build failure in `pkg/runtime/runtime_test.go` by updating a call to an exported function. - Moves TDD tests for `config` and `e` packages into their respective package directories to ensure accurate coverage reporting. - Adds a new test suite for the `pkg/i18n` package, including a test helper to inject a mock i18n bundle. - Moves and updates tests for the `pkg/crypt` package to use its public API. - The coverage for `config` and `e` is now 100%. - The coverage for `crypt` and `i18n` has been significantly improved. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
187 lines
4.9 KiB
Go
187 lines
4.9 KiB
Go
package i18n
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/Snider/Core/pkg/core"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
//go:embed locales/*.json
|
|
var localeFS embed.FS
|
|
|
|
// Options holds configuration for the i18n service.
|
|
type Options struct{}
|
|
|
|
// Service provides internationalization and localization.
|
|
type Service struct {
|
|
*core.Runtime[Options]
|
|
bundle *i18n.Bundle
|
|
localizer *i18n.Localizer
|
|
availableLangs []language.Tag
|
|
}
|
|
|
|
// newI18nService contains the common logic for initializing a Service struct.
|
|
func newI18nService() (*Service, error) {
|
|
bundle := i18n.NewBundle(language.English)
|
|
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
|
|
|
availableLangs, err := getAvailableLanguages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, lang := range availableLangs {
|
|
filePath := fmt.Sprintf("locales/%s.json", lang.String())
|
|
if _, err := bundle.LoadMessageFileFS(localeFS, filePath); err != nil {
|
|
return nil, fmt.Errorf("failed to load message file %s: %w", filePath, err)
|
|
}
|
|
}
|
|
|
|
s := &Service{
|
|
bundle: bundle,
|
|
availableLangs: availableLangs,
|
|
}
|
|
// Language will be set during ServiceStartup after config is available.
|
|
return s, nil
|
|
}
|
|
|
|
// New is the constructor for static dependency injection.
|
|
// It creates a Service instance without initializing the core.Runtime field.
|
|
// Dependencies are passed directly here.
|
|
func New() (*Service, error) {
|
|
s, err := newI18nService()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// Register is the constructor for dynamic dependency injection (used with core.WithService).
|
|
// It creates a Service instance and initializes its core.Runtime field.
|
|
// Dependencies are injected during ServiceStartup.
|
|
func Register(c *core.Core) (any, error) {
|
|
s, err := newI18nService()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.Runtime = core.NewRuntime(c, Options{})
|
|
return s, nil
|
|
}
|
|
|
|
// HandleIPCEvents processes IPC messages, including injecting dependencies on startup.
|
|
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|
switch m := msg.(type) {
|
|
case core.ActionServiceStartup:
|
|
return s.ServiceStartup(context.Background(), application.ServiceOptions{})
|
|
default:
|
|
c.App.Logger.Error("Display: Unknown message type", "type", fmt.Sprintf("%T", m))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServiceStartup is called when the app starts, after dependencies are injected.
|
|
func (s *Service) ServiceStartup(context.Context, application.ServiceOptions) error {
|
|
// Determine initial language after config is available.
|
|
initialLang := "en"
|
|
var lang string
|
|
_ = s.Config().Get("language", &lang)
|
|
if lang != "" {
|
|
initialLang = lang
|
|
}
|
|
err := s.SetLanguage(initialLang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.Core().App.Logger.Info("I18n service started")
|
|
return nil
|
|
}
|
|
|
|
// --- Language Management ---
|
|
|
|
func getAvailableLanguages() ([]language.Tag, error) {
|
|
files, err := localeFS.ReadDir("locales")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read embedded locales directory: %w", err)
|
|
}
|
|
|
|
var availableLangs []language.Tag
|
|
for _, file := range files {
|
|
lang := strings.TrimSuffix(file.Name(), ".json")
|
|
tag := language.Make(lang)
|
|
availableLangs = append(availableLangs, tag)
|
|
}
|
|
return availableLangs, nil
|
|
}
|
|
|
|
func detectLanguage(supported []language.Tag) (string, error) {
|
|
langEnv := os.Getenv("LANG")
|
|
if langEnv == "" {
|
|
return "", nil
|
|
}
|
|
|
|
baseLang := strings.Split(langEnv, ".")[0]
|
|
parsedLang, err := language.Parse(baseLang)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse language tag '%s': %w", baseLang, err)
|
|
}
|
|
|
|
if len(supported) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
matcher := language.NewMatcher(supported)
|
|
_, index, confidence := matcher.Match(parsedLang)
|
|
|
|
if confidence >= language.Low {
|
|
return supported[index].String(), nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// --- Public Service Methods ---
|
|
|
|
func (s *Service) SetLanguage(lang string) error {
|
|
requestedLang, err := language.Parse(lang)
|
|
if err != nil {
|
|
return fmt.Errorf("i18n: failed to parse language tag \"%s\": %w", lang, err)
|
|
}
|
|
|
|
if len(s.availableLangs) == 0 {
|
|
return fmt.Errorf("i18n: no available languages loaded in the bundle")
|
|
}
|
|
|
|
matcher := language.NewMatcher(s.availableLangs)
|
|
bestMatch, _, confidence := matcher.Match(requestedLang)
|
|
|
|
if confidence == language.No {
|
|
return fmt.Errorf("i18n: unsupported language: %s", lang)
|
|
}
|
|
|
|
s.localizer = i18n.NewLocalizer(s.bundle, bestMatch.String())
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) Translate(messageID string) string {
|
|
translation, err := s.localizer.Localize(&i18n.LocalizeConfig{MessageID: messageID})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "i18n: translation for key \"%s\" not found\n", messageID)
|
|
return messageID
|
|
}
|
|
return translation
|
|
}
|
|
|
|
// Ensure Service implements the core.I18n interface.
|
|
var _ core.I18n = (*Service)(nil)
|
|
|
|
// SetBundle is a test helper to inject a bundle.
|
|
func (s *Service) SetBundle(bundle *i18n.Bundle) {
|
|
s.bundle = bundle
|
|
}
|