Skip to content

Codex Desktop falls back to English UI when started offline - i18n gated behind server config enable_i18n (default false) #52471

Open

Description

@zhangdagen666

What version of the Codex App are you using?

26.1002.52244 (package OpenAI.Codex_26.1002.7124.0_x64__2p2nqsd0c76g0, prod channel, installed from Microsoft Store)

What platform is your computer?

Microsoft Windows 11 家庭版 中文版 (10.0.26200, Build 26200) x64

System region: zh-CN, preferred UI language: zh-Hans-CN

What issue are you seeing?

Codex Desktop renders its UI in English when the app is launched without a working connection to OpenAI, even though:

  • the UI language is set to 中文(中国)/ zh-CN,
  • the locale pack is already bundled inside the app (app.asar contains zh-CN-b05cfd55c534.js ~2.5 MB and zh-Hans-6d9213662c0b.js),
  • the local config correctly stores the override:
[desktop]
localeOverride = "zh-CN"

As soon as the app is launched with a working proxy/VPN (so that it can reach OpenAI at startup), the same installation renders in Chinese. Turning the proxy off and restarting brings the English UI back. No restart-with-network is needed after the initial launch: the language stays Chinese for the lifetime of the process.

What steps can reproduce the bug?

  1. Settings > General > Language: select 中文(中国)(zh-CN), confirm config.toml gets [desktop] localeOverride = "zh-CN".
  2. Fully quit Codex (tray > Quit).
  3. Start Codex with a network path that cannot reach OpenAI (e.g. direct connection without proxy).
  4. The UI renders in English. Log shows the backend is unreachable, e.g.:
warning [electron-message-handler] sa_server_request_failed ... errorMessage="Workspace routing is unavailable"
warning [AppServerConnection] app_server_connection.transport_connect_failed errorMessage="Sign in to ChatGPT to start a durable thread."
  1. Quit Codex, enable the proxy/VPN, start it again — the UI is now Chinese.

Expected behavior

The UI should be rendered in the selected language as soon as it is selected, including offline starts, since both the preference and the locale pack are available locally.

Root cause (from inspecting the shipped bundle)

The i18n path is gated behind a dynamically fetched config, and the gate defaults to disabled when the config is unavailable. In app.asar (minified):

function RYs(e){
  ...
  s = En(`72216192`),                       // server-provided dynamic config
  c = s?.get(`enable_i18n`, false);         // missing config => false => English UI
  ...
}

The locale resolution logic directly below it would happily use the local override or the system locale, but it never runs when enable_i18n is false.

Workaround

Start the app once with a working proxy/VPN, then keep the process running (e.g. disable the system proxy afterwards). The language persists until the app is restarted.

Suggested fix

Either default enable_i18n to true on desktop, or honor a non-empty localeOverride with the bundled locale packs and no network dependency.


中文摘要(可贴在 issue 末尾或作为补充)

界面语言被一个在线开关 enable_i18n 控制,取不到配置时默认 false,于是离线启动一律回退英文;但语言包本来就在本地(app.asar 内的 zh-CN-*.js),config.toml 里的 localeOverride = "zh-CN" 也已生效。开代理启动一次即恢复中文,说明只差这一个开关。建议桌面端把 enable_i18n 默认设为 true,或在 localeOverride 非空时直接用本地语言包。


Related open issues with the same symptom: #19239, #23815. This report adds the offline/online correlation and the code-level cause (enable_i18n defaulting to false); feel free to consolidate it into those threads if you prefer.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    connectivityIssues involving networking or endpoint connectivity problems (disconnections)
    on Oct 9, 2026
  2. gehai46 commented on Oct 10, 2026

    @gehai46

    Follow-up with code-level confirmation. I filed the symptom-level report in #51074 before
    isolating the cause; the same machine and the same config file produce the A/B below.

    Environment

    • Windows 10 Pro 26H2, build 26300.9550, x64; system UI language zh-CN
    • Package OpenAI.Codex_26.1007.2314.0_x64__2p2nqsd0c76g0; release reported in logs 26.1007.21434
    • ~/.codex/config.toml contains [desktop] localeOverride = "zh-CN", and that line is never
      removed or rewritten by either run below

    Decisive observation

    One installation, one config file, only the network path changes:

    Cold start (2026-10-10, UTC+08:00) Path browser_use_availability_resolved UI language
    15:55 through a local proxy available=true ... reason=available release=26.1007.21434 Simplified Chinese
    17:01 direct, no proxy available=false ... reason=statsig-disabled release=26.1007.21434 English

    Nothing else differs. The saved localeOverride is present and identical in both runs, so the
    symptom is network-triggered rather than version-triggered.

    Bundle review of this exact build

    The locale provider gates the loader on a server-delivered dynamic config whose default is
    false:

    s = n0e(`72216192`),
    c = s?.get(`enable_i18n`, false),        // config unavailable => false
    ...
    k = () => { let e = !1; return l && (async () => {
            if (y) try { let t = await y7i(y); ... }
            catch { gr.error(`Failed to load locale messages`, { safe: { locale: y.locale }, sensitive: {} }) }
          })(), () => { e = !0 } }

    The same config key defaults to true in the settings picker:

    // Settings > General > Language
    r = Pn(`72216192`)?.get(`enable_i18n`, true)

    Two different defaults for one key reproduce the symptom in the original report exactly: the
    dropdown keeps listing 中文(中国) because the picker defaults to enabled, while the loader that
    would actually apply it defaults to disabled. Selection appears to save and never takes effect.

    Pointers that may save triage time

    • The failing call is POST https://chatgpt.com/backend-api/wham/statsig/bootstrap, issued as
      safePost('/wham/statsig/bootstrap', { requestBody: e, retry: false, signal: t }). It is
      single-shot, so one slow body read degrades the session with no retry.
    • 432 "Workspace routing is unavailable" on /settings/user and /wham/usage is a red herring
      for this bug. It is present in the proxied session too, where statsig succeeded and the UI was
      Chinese. It does not discriminate between the two states.
    • Locale loading never fails in the logs: there is no Failed to load locale messages entry, and
      app.asar ships .vite/build/locales/zh-CN.json plus the webview zh-CN-*.js chunk. Missing
      resources are not involved.
    • Main-process locale resolution also works correctly; it resolves zh-CN while native strings
      stay English, which is the "value resolved, value unused" signature reported here.

    Separate defect, worth tracking independently

    AppxManifest.xml in 26.1007.2314.0 still declares a single resource language:

    <Resources>
      <Resource Language="en-US" />
    </Resources>

    That is a real packaging issue (see #51926 and #52800) affecting the system-locale path. It cannot
    explain the A/B above, since it does not change with the network path.

    Timeline

    Logs on this machine only go back to 2026-10-04, so I cannot speak to earlier builds. Across
    26.930.31730, 26.930.41038, 26.930.61225, 26.1002.51308, 26.1002.52244 and
    26.1007.21434, both reason=available and reason=statsig-disabled appear, including different
    outcomes on the same build within the same hour. The outcome is not pinned to any one build.
    The report in #50377, observed 2026-10-02 to 10-03, is the earliest occurrence I can point at.

    Workaround verified here

    Start the app once on a path that can reach chatgpt.com, then keep the process running. The UI
    stays in the selected language for the lifetime of the process, and the proxy does not need to
    stay on afterwards. Restarting without such a path brings English back.

    Requested fix

    Honour a non-empty localeOverride using the bundled catalogs with no network dependency, or
    default enable_i18n to true on desktop.


    中文摘要

    同一台机器、同一版本 26.1007.2314.0、同一份 config.toml,只更换网络路径即可复现:走代理时
    reason=available,界面为简体中文;直连时 reason=statsig-disabled,界面为英文。原因是语言加载器
    里 enable_i18n 的默认值为 false,而设置页语言选择器的同一配置默认值为 true,于是选项可见、选中无效。
    localeOverride 与本地语言包都正常,日志中也没有语言包加载失败。另有 MSIX 清单只声明 en-US 的独立打包
    缺陷(见 #51926、#52800),与本条成因不同,需分开修复。

  3. welger commented on Oct 13, 2026

    @welger

    Why "launch once online" does not survive an offline restart: the Statsig evaluations cache never persists (localStorage QuotaExceededError)

    The reports above establish that the UI is localized only when enable_i18n is available at startup. One question was still open: why doesn't a successful online launch carry over to the next offline launch? The Statsig client in the renderer has storage enabled and reads statsig.cached.evaluations.* before it hits the network, so the last-known values should be served from cache. They aren't, because the cache write always fails.

    Environment

    • Package OpenAI.Codex_26.1007.2314.0_x64__2p2nqsd0c76g0, release 26.1007.21434, Chromium 155.0.8059.40
    • Bundled Statsig JS SDK 3.34.0
    • Windows, custom model provider (API key), not signed in with ChatGPT
    • [desktop] localeOverride = "zh-CN" present; no [analytics] override, so renderer storage is enabled (disableStorage is unset on the client)

    Method

    The app was launched in its package context with --remote-debugging-port (via Invoke-CommandInDesktopPackage, so the data directory does not change). I inspected __STATSIG__.firstInstance in the primary window, wrapped Storage.prototype.setItem to record every write, and triggered a refresh with updateUserAsync(currentUser).

    Result (network reachable)

    update:  success=true, source=Network
    setItem  statsig.cached.evaluations.<hash>   5,336,996 chars   QuotaExceededError
    setItem  statsig.last_modified_time.evaluations   55 chars     ok
    setItem  statsig.cached.evaluations.<hash>   5,336,996 chars   QuotaExceededError
    ...      (5 failed attempts in total)
    setItem  statsig.last_modified_time.evaluations   2 chars ("{}")   ok
    

    After the refresh, localStorage holds only statsig.session_id.*, statsig.stable_id.* and an empty ledger statsig.last_modified_time.evaluations = {}. No evaluations are ever written to disk.

    Chromium caps localStorage at 10 MiB per origin, stored as UTF-16, which is about 5,242,880 characters. The serialized cache record is 5,336,996 characters, about 1.8% over the limit.

    This is how the SDK's _writeToCache handles it (the code is identical in the earlier 26.730 bundle; only the payload size changed):

    for (let t = 0; t < c; t++) try {
      Storage.setItem(key, value); break;              // throws QuotaExceededError every time
    } catch (t) {
      if (!isQuotaError(t) || this._cacheLimit <= 1) throw t;
      this._cacheLimit = Math.ceil(this._cacheLimit / 2);              // 10 -> 5 -> 3 -> 2 -> 1
      this._runLocalStorageCacheEviction(key, this._cacheLimit - 1);   // last pass evicts with limit 0
    }

    The last eviction pass runs with limit 0. It removes every entry in the ledger, including any previously cached value under the same key, and leaves the ledger as {}. The error is silent: nothing about it appears in the desktop logs.

    What fills the payload

    key chars
    dynamic_configs 4,544,961 (~91%)
    layer_configs 272,210
    feature_gates 154,739
    everything else < 2,000
    total data 4,973,409

    The layer behind localization (72216192 → { "enable_i18n": true, "locale_source": "IDE" }) is a single small entry inside layer_configs.

    Consequence

    Every launch without a working path to OpenAI starts with NoValues. That means enable_i18n is false (English), and every other gate falls back to its default too. On this machine that also brings back the older navigation layout and removes Approve for me from the permissions menu, which matches the symptoms in #50377.

    Control experiment: the cache-read path works; only the size breaks it

    1. Offline: I wrote a trimmed record (2,098 chars) under the same cache key. It contained the real metadata, with feature_gates and dynamic_configs emptied and layer_configs reduced to 72216192. The SDK's own _loadFromCache returned it with source: Cache.
    2. Offline cold start: the UI rendered in Chinese, and the layer reason was Cache:Recognized. The layout was still the older one, since every other gate was at its default.
    3. One online launch: the full payload hit the quota loop above, and the final eviction deleted the seeded record.
    4. Offline cold start again: English, NoValues.

    So the offline fallback is already designed to work. It is defeated only because the full evaluations payload no longer fits in localStorage.

    Relation to #50377

    The same oversized payload is probably also why the post-login bootstrap misses its fixed 10 s deadline on slower links, as reported there. I have not measured that directly.

    Possible fixes (from the outside; you will know better)

    1. Persist evaluations through a store that isn't bound by the localStorage quota. The SDK already supports a custom storageProvider (for example IndexedDB, or a file through the main process).
    2. Shrink what gets cached: dynamic_configs is about 91% of it. Or additionally persist a small allowlist of startup-critical values (locale, navigation layout, permission presets) that always fits.
    3. Honour a non-empty localeOverride from the bundled catalogs without depending on enable_i18n, as already requested above.
    4. At minimum, log the QuotaExceededError. It is currently swallowed, which makes this very hard to diagnose from user reports.

    On builds from late August we saw the evaluations cache present on disk, and the localized UI persisted offline. I did not measure the payload size on those builds, so I can't say exactly when it crossed the threshold.


    中文摘要

    「联网启动一次」之所以撑不到下一次断网启动,是因为 Statsig 的开关缓存从来没有写进磁盘。

    在 26.1007.2314.0 里实测:联网拉取成功,但缓存记录有 5,336,996 个字符,超过 Chromium localStorage 每个源约 5,242,880 字符的上限(超出约 1.8%),连续 5 次 QuotaExceededError。SDK 的减半重试最后按上限 0 淘汰,把账本清成 {},而且不留任何日志。

    数据里 dynamic_configs 占约 91%。汉化开关 72216192 只是其中很小的一项。

    对照实验:断网时只写入裁剪后 2,098 字符的记录,断网冷启动界面就是中文(Cache:Recognized);联网启动一次,这条记录就被同一个配额循环删掉,再断网又变回英文。这说明离线读缓存的路径本身是好的,只是数据太大存不下。

    建议的修法:换用不受 localStorage 配额限制的 storageProvider;精简缓存内容,或单独持久化启动所需的少量关键值;localeOverride 不依赖 enable_i18n;至少把配额错误记进日志。

  4. WU928-spec commented on Oct 15, 2026

    @WU928-spec

    Adding a macOS data point to the root-cause side of this, on 26.1007.71326 (build 20398), macOS 14.8.3 arm64, system language zh-Hans-CN:

    The UI stays English while the machine has a working route to OpenAI, so this is not only the offline case described above. Confirmed at startup through a working proxy: the app's own processes connect to chatgpt.com, chat.openai.com and ab.chatgpt.com, and api.openai.com is reachable (401 without a key). Network is not the variable here — the gate still resolves to false.

    The likely cause is a default mismatch between the two call sites in Contents/Resources/app.asar:

    // provider that injects locale/messages for the UI
    c = s?.get(`enable_i18n`, !1);                // default false
    // Settings page
    r = Pn(`72216192`)?.get(`enable_i18n`, !0);   // default true

    So unless the server explicitly returns enable_i18n: true for 72216192, the UI renders English defaultMessages and every local lever (localeOverride = "zh-CN", --lang=zh-CN, changing the picker) is a no-op. The zh-Hans strings are bundled in app.asar (verified), so it is not a missing-translation problem either.

    Full macOS repro and the things that were tried are in #19239.

  5. 4x25 commented on Oct 16, 2026

    @4x25

    Confirming the same defect on macOS, with a cache angle that lines up with @welger's Windows findings.

    Environment: Codex App 26.1013.31256 (Chromium base 155.0.8059.80), macOS Darwin 27.0.0 arm64. One installation, one config.toml; only the network path changes.

    A/B (cold starts):

    • Direct network: ab.chatgpt.com -> connection reset (curl exit 35), chatgpt.com/backend-api -> connect timeout after 8s. Every cold start renders English; changing the language in Settings writes [desktop] localeOverride but never changes the UI.
    • Through a local proxy (127.0.0.1:7890): both hosts answer (403 without auth). The UI localizes a moment after launch (brief English -> localized flash, i.e. the bundled locale chunk loads right after the gate resolves) and stays localized for the process lifetime.

    Both call sites read the same layer. In this build, general-settings-fd634ec6c16e.js imports Or from app-initial-3ccf4886957f.js as a re-export of the same getLayer helper (dWt as oDr in the bundle's export table). So the picker lists languages (?.get('enable_i18n', true)) while the provider that actually injects messages uses false, for the exact same 72216192 layer: the option is always selectable and the write always succeeds even when the value can never be applied.

    Cache state on this macOS install (matches @welger's conclusion):

    • The live Local Storage LevelDB currently holds statsig.session_id.*, statsig.stable_id.* and an empty statsig.last_modified_time.evaluations ledger ({}). There is no statsig.cached.evaluations.* for the current identity, so every launch starts from NoValues.
    • Historical tables (compacted away during 2026-10-16 16:40; the LOG shows a compaction down to 554 bytes / 9 keys) did contain cached entries, e.g. key statsig.cached.evaluations.3217092736 with source: "Network", receivedAt 2026-06-01, and layer_configs["72216192"] = {"enable_i18n": true, "locale_source": "IDE"}. The cache write path used to work on macOS; today nothing survives.
    • The post-login bootstrap path is memory-only by design: dataAdapter.setData(payload) + initializeSync(), and the SDK's setData writes to its in-memory cache only - nothing hits storage. Combined with the above, an offline cold start always falls back to the enable_i18n default.

    On the 432 noise: agreed it is orthogonal - this machine logged 477 "Workspace routing is unavailable" (status 432) events today, including while the proxy was on and the UI localized.

    The requested fix (honor a non-empty localeOverride from the bundled catalogs without the gate, and/or default enable_i18n to true on desktop) resolves every variant above; the workaround (start once on a reachable network path) keeps matching across reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appbugSomething isn't workingconnectivityIssues involving networking or endpoint connectivity problems (disconnections)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions