fix(ui): ensure i18n locale is loaded before initial render (closes #24803)#46366
fix(ui): ensure i18n locale is loaded before initial render (closes #24803)#46366Br1an67 wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR addresses the locale-flash bug (#24803) by ensuring the initial locale is fully loaded before the first render. Two targeted changes are made to Key changes:
Minor observation:
Confidence Score: 3/5
Last reviewed commit: 9fb136f |
ui/src/ui/app.ts
Outdated
| if (!this.localeReady) { | ||
| return nothing; | ||
| } |
There was a problem hiding this comment.
firstUpdated fires on blank render, breaking topbarObserver
Returning nothing on the first render means firstUpdated() is called while no DOM exists. handleFirstUpdated calls observeTopbar, which queries host.querySelector(".topbar"). Since nothing produces no DOM, it finds no element and returns early — leaving host.topbarObserver as null permanently.
When localeReady becomes true and the component re-renders with real content (including .topbar), LitElement will call updated() but not firstUpdated() again. The topbar ResizeObserver is never created, so the --topbar-height CSS variable is never set.
A straightforward fix is to call observeTopbar the first time localeReady flips to true inside updated(), e.g.:
protected updated(changed: Map<PropertyKey, unknown>) {
if (changed.has('localeReady') && this.localeReady) {
observeTopbar(this);
}
handleUpdated(this as unknown as Parameters<typeof handleUpdated>[0], changed);
}Or alternatively, call observeTopbar directly in the Promise.all callback after setting localeReady = true.
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/app.ts
Line: 729-731
Comment:
**`firstUpdated` fires on blank render, breaking `topbarObserver`**
Returning `nothing` on the first render means `firstUpdated()` is called while no DOM exists. `handleFirstUpdated` calls `observeTopbar`, which queries `host.querySelector(".topbar")`. Since `nothing` produces no DOM, it finds no element and returns early — leaving `host.topbarObserver` as `null` permanently.
When `localeReady` becomes `true` and the component re-renders with real content (including `.topbar`), LitElement will call `updated()` but **not** `firstUpdated()` again. The topbar `ResizeObserver` is never created, so the `--topbar-height` CSS variable is never set.
A straightforward fix is to call `observeTopbar` the first time `localeReady` flips to `true` inside `updated()`, e.g.:
```ts
protected updated(changed: Map<PropertyKey, unknown>) {
if (changed.has('localeReady') && this.localeReady) {
observeTopbar(this);
}
handleUpdated(this as unknown as Parameters<typeof handleUpdated>[0], changed);
}
```
Or alternatively, call `observeTopbar` directly in the `Promise.all` callback after setting `localeReady = true`.
How can I resolve this? If you propose a fix, please make it concise.| constructor() { | ||
| super(); | ||
| if (isSupportedLocale(this.settings.locale)) { | ||
| void i18n.setLocale(this.settings.locale); | ||
| } | ||
| const settingsLocale = isSupportedLocale(this.settings.locale) | ||
| ? i18n.setLocale(this.settings.locale) | ||
| : Promise.resolve(); | ||
| Promise.all([i18n.ready, settingsLocale]).then( | ||
| () => { | ||
| this.localeReady = true; | ||
| }, | ||
| () => { | ||
| // Locale load failed; render with the default locale. | ||
| this.localeReady = true; | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Race condition: concurrent setLocale calls may interleave
I18nManager is a module-level singleton instantiated at import time. Its constructor immediately calls loadLocale(), which calls setLocale(initialLocale) asynchronously. When OpenClawApp's constructor then calls i18n.setLocale(this.settings.locale), both calls can be in-flight simultaneously.
setLocale has no mutual-exclusion guard — if two calls overlap for the same locale that needs a translation download, loadLazyLocaleTranslation is called twice (wasteful but not data-corrupting). For different locales, the final value of this.locale depends on which await resolves last, not on which call was initiated last. For example: if settings.locale is "fr" but initialLocale (from navigator) is "zh-CN", the component could end up with either locale after Promise.all resolves.
Consider adding a simple "last writer wins with a generation counter" guard inside setLocale, or at minimum document that i18n.ready only reflects the startup locale — not the override applied in the OpenClawApp constructor.
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/app.ts
Line: 119-133
Comment:
**Race condition: concurrent `setLocale` calls may interleave**
`I18nManager` is a module-level singleton instantiated at import time. Its constructor immediately calls `loadLocale()`, which calls `setLocale(initialLocale)` asynchronously. When `OpenClawApp`'s constructor then calls `i18n.setLocale(this.settings.locale)`, both calls can be in-flight simultaneously.
`setLocale` has no mutual-exclusion guard — if two calls overlap for the same locale that needs a translation download, `loadLazyLocaleTranslation` is called twice (wasteful but not data-corrupting). For **different** locales, the final value of `this.locale` depends on which `await` resolves last, not on which call was initiated last. For example: if `settings.locale` is `"fr"` but `initialLocale` (from navigator) is `"zh-CN"`, the component could end up with either locale after `Promise.all` resolves.
Consider adding a simple "last writer wins with a generation counter" guard inside `setLocale`, or at minimum document that `i18n.ready` only reflects the startup locale — not the override applied in the `OpenClawApp` constructor.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c6ea8789f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
ui/src/ui/app.ts
Outdated
| if (!this.localeReady) { | ||
| return nothing; |
There was a problem hiding this comment.
Avoid blanking UI while locale chunks are still loading
Blocking render() on localeReady means the entire Control UI stays empty until i18n.ready and settingsLocale resolve; when a non-English locale chunk load is slow or stalls, users get a blank screen instead of a usable fallback UI. This is a startup availability regression introduced by this change because there is no timeout, loading state, or fallback render path while those promises are pending.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 567528bf04
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
ui/src/i18n/lib/translate.ts
Outdated
| const seq = ++this.localeSeq; | ||
| const needsTranslationLoad = locale !== DEFAULT_LOCALE && !this.translations[locale]; | ||
| if (this.locale === locale && !needsTranslationLoad) { | ||
| return; |
There was a problem hiding this comment.
Avoid canceling startup locale load on no-op setLocale calls
setLocale() now increments localeSeq before the early-return no-op check, so a concurrent no-op call (for example the startup call in ui/src/ui/app.ts:121 when the current locale is already en) can invalidate the in-flight loadLocale() request started in the constructor. Because i18n.ready only awaits that initial loadLocale() call, it can resolve after the request is canceled rather than after the active startup locale has actually loaded, which means callers that gate first render on i18n.ready can still render in the wrong language.
Useful? React with 👍 / 👎.
1f2740b to
9fb136f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9fb136f8fb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
ui/src/i18n/lib/translate.ts
Outdated
| const seq = ++this.localeSeq; | ||
| const needsTranslationLoad = locale !== DEFAULT_LOCALE && !this.translations[locale]; | ||
| if (this.locale === locale && !needsTranslationLoad) { | ||
| return; |
There was a problem hiding this comment.
Move sequence bump after no-op locale short-circuit
Incrementing localeSeq before the no-op return allows a no-op setLocale() call to cancel an in-flight locale load that should still complete. In this commit, if startup loadLocale() begins loading a saved non-English locale and a concurrent setLocale("en") returns early because the current locale is already English, the sequence mismatch aborts the startup load and leaves i18n.ready resolved with the wrong locale; I verified this by reproducing openclaw.i18n.locale=zh-CN followed by immediate setLocale("en"), which ends with getLocale() still at en.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f56dca0df
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (seq !== this.localeSeq) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Keep i18n.ready pending until winning locale load finishes
i18n.ready now awaits loadLocale(), but the stale-request guard can make that startup call return early when another setLocale() runs first, so ready can resolve before the active locale request completes. Fresh evidence: with openclaw.i18n.locale=zh-CN and an immediate setLocale("pt-BR"), await i18n.ready returns while getLocale() is still en. Any caller that gates first render on ready can still render in the wrong language.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1e204ca44
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| id: string, | ||
| factory: ContextEngineFactory, | ||
| ): ContextEngineRegistrationResult { | ||
| return registerContextEngineForOwner(id, factory, PUBLIC_CONTEXT_ENGINE_OWNER); |
There was a problem hiding this comment.
Keep core ownership when registering legacy context engine
registerContextEngine() now hard-codes PUBLIC_CONTEXT_ENGINE_OWNER, but registerContextEngineForOwner() rejects non-core owners for the default context-engine id (legacy). The built-in startup path still registers legacy through registerContextEngine("legacy", ...), so that call now returns { ok: false } and leaves no default engine installed; subsequent ensureContextEnginesInitialized() + resolveContextEngine() flows can fail with Context engine "legacy" is not registered.
Useful? React with 👍 / 👎.
Closes openclaw#24803 Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2545533e14
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| id: string, | ||
| factory: ContextEngineFactory, | ||
| ): ContextEngineRegistrationResult { | ||
| return registerContextEngineForOwner(id, factory, PUBLIC_CONTEXT_ENGINE_OWNER); |
There was a problem hiding this comment.
Register core default engine through privileged owner
registerContextEngine() now always uses the unprivileged public-sdk owner, but registerContextEngineForOwner() rejects that owner for the default context-engine slot id (legacy). The built-in bootstrap still calls registerContextEngine("legacy", ...) via registerLegacyContextEngine(), so startup silently fails to register the default engine and later resolveContextEngine() can throw Context engine "legacy" is not registered even in core-only setups.
Useful? React with 👍 / 👎.
|
Closing to manage active PR count. Will reopen when slot is available. |
Summary
Fixes #24803 — Control UI defaults to English on initialization even when user has configured a different locale.
Change Type
Bug fix (non-breaking)
Change Scope
UI i18n initialization
Security Impact
None
How to Reproduce
Evidence
I18nManagernow exposes areadypromise that resolves when the initial locale finishes loadingOpenClawAppgates its firstrender()on locale readiness — no English flashi18n.readyinstead of polling withvi.waitFor()Human Verification
Backward Compatibility
Fully backward compatible
Risks
Minimal — changes initialization order only