Skip to content

[sync] go-i18n: 9 commits from Forge#1

Merged
Snider merged 9 commits intomainfrom
dev
Mar 17, 2026
Merged

[sync] go-i18n: 9 commits from Forge#1
Snider merged 9 commits intomainfrom
dev

Conversation

@Snider
Copy link
Contributor

@Snider Snider commented Mar 17, 2026

Forge → GitHub Sync

Commits: 9
Files changed: 6

Automated sync from Forge (forge.lthn.ai) to GitHub mirror.


Co-Authored-By: Virgil virgil@lethean.io

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated internationalisation with the Core framework, enabling configurable language settings and additional translation sources.
    • Added runtime translation loading to dynamically merge new translations into the active service.
    • Implemented missing translation key tracking and retrieval for diagnostics.
  • Chores

    • Standardised error naming conventions with backward compatibility maintained.
    • Updated dependencies for improved stability.

Snider and others added 9 commits March 15, 2026 15:28
Co-Authored-By: Virgil <virgil@lethean.io>
Rename ErrServiceNotInitialized → ErrServiceNotInitialised and update
the error message string. Keeps a deprecated alias for the old name
to avoid breaking the 21 downstream consumers.

Co-Authored-By: Virgil <virgil@lethean.io>
NewCoreService() creates an i18n Core service factory so any binary
can register i18n without depending on core/cli.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
ServiceOptions.ExtraFS accepts []FSSource — each pairs an fs.FS with
a directory path. Translations load on top of the embedded defaults.
Enables consuming packages to ship their own locale files.

Co-Authored-By: Virgil <virgil@lethean.io>
Packages can now load translations in init():
  i18n.LoadFS(locales.FS, ".")

Co-Authored-By: Virgil <virgil@lethean.io>
i18n service now loads translations from three sources:
1. Embedded go-i18n base (grammar, verbs, nouns)
2. ExtraFS from ServiceOptions
3. Core.Locales() — collected from services implementing LocaleProvider

Services just implement Locales() fs.FS and translations load automatically.

Co-Authored-By: Virgil <virgil@lethean.io>
ExtraFS and Core.Locales() both go through AddLoader now for proper
grammar handling. Package-level T() not resolving — needs debug.

Co-Authored-By: Virgil <virgil@lethean.io>
Init().defaultOnce.Do() was replacing the service set by SetDefault().
Now Default() checks the atomic pointer first, and Init() skips if
a service was already set. Fixes translations not appearing in CLI.

Co-Authored-By: Virgil <virgil@lethean.io>
@Snider
Copy link
Contributor Author

Snider commented Mar 17, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

Warning

Rate limit exceeded

@Snider has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3fa045df-ed6b-4cfb-9fde-c4ec776169a9

📥 Commits

Reviewing files that changed from the base of the PR and between 92e2b56 and f317c00.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
📝 Walkthrough

Walkthrough

Introduces Core framework integration for i18n via CoreService, adds runtime loader support for merging translations, renames error identifier to British spelling with backward compatibility alias, and updates dependency versions including new forge.lthn.ai/core/go requirement.

Changes

Cohort / File(s) Summary
Core Framework Integration
core_service.go
New CoreService wrapper providing i18n integration with Core framework runtime. Supports configuration via ServiceOptions (language, mode, extra translation sources), missing-key collection and retrieval, and startup hook integration.
Dependency Updates
go.mod
Upgraded golang.org/x/text to v0.35.0. Added forge.lthn.ai/core/go v0.3.1 as new dependency. Updated forge.lthn.ai/core/go-inference to v0.1.4 and restructured require block.
Error Naming & Backward Compatibility
i18n.go, i18n_test.go
Renamed ErrServiceNotInitialized to ErrServiceNotInitialised (British spelling). Added deprecated alias for backward compatibility. Updated SetLanguage error path and test expectations accordingly.
Runtime Loader Integration
service.go
Added AddLoader package-level function and Service.AddLoader method for merging additional translations at runtime. Modified Init and Default functions to preserve existing default service. Updated initialization flow to support dynamic locale data integration with grammar support.

Sequence Diagrams

sequenceDiagram
    participant Core as Core Framework
    participant NewCS as NewCoreService Factory
    participant CS as CoreService
    participant IS as i18n Service
    participant Loader as FSLoader
    participant MissingKeyCollector as Missing Key Collector

    Core->>NewCS: NewCoreService(opts ServiceOptions)
    NewCS->>IS: New() - Create i18n Service
    NewCS->>Loader: Load translations from ExtraFS
    Loader->>IS: Merge translation sources
    NewCS->>IS: Apply language, mode, settings
    NewCS->>CS: Return CoreService instance
    NewCS->>Core: Register CoreService in runtime

    Core->>CS: OnStartup()
    CS->>IS: Check if in collect mode
    alt Collect Mode Enabled
        CS->>MissingKeyCollector: Subscribe to missing keys
        IS->>MissingKeyCollector: Emit missing keys during translation
        MissingKeyCollector->>CS: Store missing keys (thread-safe)
    end

    Core->>CS: MissingKeys()
    CS-->>Core: Return collected missing keys
Loading
sequenceDiagram
    participant App as Application
    participant PkgFunc as Package AddLoader()
    participant Svc as Service
    participant Loader as Loader Instance
    participant LangHandler as Language Handler

    App->>PkgFunc: AddLoader(loader)
    PkgFunc->>Svc: Get Default service
    PkgFunc->>Svc: AddLoader(loader) method
    
    loop For Each Language in Loader
        Loader->>LangHandler: GetMessages(language)
        LangHandler-->>Svc: Messages
        Svc->>Svc: Merge messages (thread-safe)
        
        alt Grammar Data Present
            Loader->>LangHandler: GetGrammarData(language)
            LangHandler-->>Svc: Grammar data
            Svc->>Svc: SetGrammarData(grammar)
        end
        
        Svc->>Svc: Update available languages
    end
    
    Svc-->>PkgFunc: Return error status
    PkgFunc-->>App: Complete AddLoader operation
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[sync] go-i18n: 9 commits from Forge' accurately reflects the PR's main purpose: a synced batch of 9 commits from the Forge repository containing i18n service enhancements and dependency updates.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@Snider
Copy link
Contributor Author

Snider commented Mar 17, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core_service.go`:
- Around line 67-69: The call to svc.SetLanguage(opts.Language) currently
ignores its error, allowing invalid ServiceOptions.Language values to silently
fall back to auto-detection; update the code path where SetLanguage is invoked
(the svc.SetLanguage(opts.Language) call) to capture and return (or propagate)
the error instead of discarding it so that invalid/unsupported language
overrides result in a visible error to the caller.

In `@service.go`:
- Around line 479-481: The call to SetGrammarData(lang, grammar) replaces the
entire cached grammar and should instead merge package-specific entries into the
existing grammar; retrieve the current grammar for lang (e.g., via
GetGrammarData(lang) or the cache accessor), create/ensure a non-nil target
object, merge grammar.Verbs, grammar.Nouns and grammar.Words into the existing
maps/slices (appending or copying keys/values as appropriate while avoiding
overwrites if desired), then call SetGrammarData(lang, mergedGrammar) (or update
the cache in place) so you augment rather than overwrite the base grammar loaded
by New; use the local variables lang and grammar and the SetGrammarData function
name to locate and apply the change.
- Around line 167-179: AddLoader currently mutates only the service returned by
Default() at call time, so loaders registered during package init are lost when
NewCoreService creates a fresh Service via New() and publishes it with
SetDefault(svc). Fix by introducing a persistent registry or by seeding new
services from the existing default: maintain a package-level slice/map of Loader
entries (e.g., loadersRegistry) that AddLoader(appends to) and ensure SetDefault
or NewCoreService copies/applies all entries from that registry into the new
Service (or have New/Service constructor consult the registry), referencing
AddLoader, Default, SetDefault, NewCoreService and New to locate where to add
the registry and apply the loaders.
- Around line 135-143: The current default init uses defaultOnce.Do with a
separate defaultService.Load() check and defaultService.Store(svc), which is a
TOCTOU race against concurrent SetDefault calls; fix by making the hand-off
atomic: after creating svc via New() inside the defaultOnce.Do, use
defaultService.CompareAndSwap(nil, svc) to store only if still nil (or wrap the
check/store with the same mutex used by SetDefault), and if CompareAndSwap
fails, close/cleanup svc if necessary to avoid leaking resources; update any
SetDefault/Init logic to rely on the atomic CompareAndSwap or the shared mutex.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ccedf9d4-1f3c-47de-9baa-431e5088b1c4

📥 Commits

Reviewing files that changed from the base of the PR and between 1aecb7b and 92e2b56.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • core_service.go
  • go.mod
  • i18n.go
  • i18n_test.go
  • service.go

Comment on lines +67 to +69
if opts.Language != "" {
_ = svc.SetLanguage(opts.Language)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate invalid ServiceOptions.Language values.

Ignoring svc.SetLanguage(opts.Language) turns an invalid or unsupported override into a silent fallback to auto-detection, so the service can start in the wrong language with no signal to the caller.

Suggested fix
 		if opts.Language != "" {
-			_ = svc.SetLanguage(opts.Language)
+			if err := svc.SetLanguage(opts.Language); err != nil {
+				return nil, err
+			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if opts.Language != "" {
_ = svc.SetLanguage(opts.Language)
}
if opts.Language != "" {
if err := svc.SetLanguage(opts.Language); err != nil {
return nil, err
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core_service.go` around lines 67 - 69, The call to
svc.SetLanguage(opts.Language) currently ignores its error, allowing invalid
ServiceOptions.Language values to silently fall back to auto-detection; update
the code path where SetLanguage is invoked (the svc.SetLanguage(opts.Language)
call) to capture and return (or propagate) the error instead of discarding it so
that invalid/unsupported language overrides result in a visible error to the
caller.

Comment on lines 135 to 143
defaultOnce.Do(func() {
// If SetDefault was already called, don't overwrite
if defaultService.Load() != nil {
return
}
svc, err := New()
if err == nil {
defaultService.Store(svc)
loadRegisteredLocales(svc)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Make the Init/SetDefault hand-off atomic.

The check at Lines 137-138 and the store at Line 142 are a TOCTOU pair. A concurrent SetDefault(custom) can still land between them and then be overwritten by the service initialised here, which defeats the new “do not overwrite” behaviour. Use a CompareAndSwap(nil, svc) or a dedicated mutex around the check/store path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service.go` around lines 135 - 143, The current default init uses
defaultOnce.Do with a separate defaultService.Load() check and
defaultService.Store(svc), which is a TOCTOU race against concurrent SetDefault
calls; fix by making the hand-off atomic: after creating svc via New() inside
the defaultOnce.Do, use defaultService.CompareAndSwap(nil, svc) to store only if
still nil (or wrap the check/store with the same mutex used by SetDefault), and
if CompareAndSwap fails, close/cleanup svc if necessary to avoid leaking
resources; update any SetDefault/Init logic to rely on the atomic CompareAndSwap
or the shared mutex.

Comment on lines +167 to +179
// AddLoader loads translations from a Loader into the default service.
// Call this from init() in packages that ship their own locale files:
//
// //go:embed *.json
// var localeFS embed.FS
// func init() { i18n.AddLoader(i18n.NewFSLoader(localeFS, ".")) }
func AddLoader(loader Loader) {
svc := Default()
if svc == nil {
return
}
_ = svc.AddLoader(loader)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Package-level AddLoader registrations are lost after a later SetDefault.

This helper only patches whichever service Default() returns at that moment. NewCoreService in core_service.go builds a fresh Service with New() and then publishes it via SetDefault(svc), so any package locales loaded earlier from init() disappear at that point. The package-level API needs a persistent loader registry, or the replacement service needs to be seeded from the existing default before it is published.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service.go` around lines 167 - 179, AddLoader currently mutates only the
service returned by Default() at call time, so loaders registered during package
init are lost when NewCoreService creates a fresh Service via New() and
publishes it with SetDefault(svc). Fix by introducing a persistent registry or
by seeding new services from the existing default: maintain a package-level
slice/map of Loader entries (e.g., loadersRegistry) that AddLoader(appends to)
and ensure SetDefault or NewCoreService copies/applies all entries from that
registry into the new Service (or have New/Service constructor consult the
registry), referencing AddLoader, Default, SetDefault, NewCoreService and New to
locate where to add the registry and apply the loaders.

Comment on lines +479 to +481
// Merge grammar data into the global grammar store
if grammar != nil && (len(grammar.Verbs) > 0 || len(grammar.Nouns) > 0 || len(grammar.Words) > 0) {
SetGrammarData(lang, grammar)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Merge grammar data here instead of replacing it.

SetGrammarData in grammar.go overwrites the whole cache entry for lang. If an extra loader only contributes package-specific verbs, nouns, or words, this call wipes the base grammar loaded by New() instead of augmenting it, which breaks existing grammar-aware translations for that language.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service.go` around lines 479 - 481, The call to SetGrammarData(lang, grammar)
replaces the entire cached grammar and should instead merge package-specific
entries into the existing grammar; retrieve the current grammar for lang (e.g.,
via GetGrammarData(lang) or the cache accessor), create/ensure a non-nil target
object, merge grammar.Verbs, grammar.Nouns and grammar.Words into the existing
maps/slices (appending or copying keys/values as appropriate while avoiding
overwrites if desired), then call SetGrammarData(lang, mergedGrammar) (or update
the cache in place) so you augment rather than overwrite the base grammar loaded
by New; use the local variables lang and grammar and the SetGrammarData function
name to locate and apply the change.

@Snider Snider merged commit f67b4a0 into main Mar 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant