From 806fd93638f13ffbc9828170f34975b0312eb2d6 Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 20 Sep 2026 20:45:44 +0800 Subject: [PATCH] fix: sync language at render time without notifying subscribers setLanguage() during App's render fired onLanguageChange, which made the same component's useSyncExternalStore schedule an update mid-render (React: cannot update a component while rendering a different component). syncLanguage() updates the module silently; re-renders flow through the settings store, which is the only path a renderer language change takes. --- src/renderer/src/App.tsx | 13 +++++++------ src/shared/i18n/index.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6ae7c43..84efcb8 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useSyncExternalStore } from 'react' +import { useEffect, useState } from 'react' import { ConfigProvider } from 'antd' import zhCN from 'antd/locale/zh_CN' import zhTW from 'antd/locale/zh_TW' @@ -9,7 +9,7 @@ import { SettingsDialog } from '@renderer/settings/SettingsDialog' import { TransferPanel } from '@renderer/sftp/TransferPanel' import { useSettingsStore } from '@renderer/settings/store' import { getThemeById } from '@shared/theme' -import { DEFAULT_LANGUAGE, getLanguage, onLanguageChange, setLanguage, type Language } from '@shared/i18n' +import { DEFAULT_LANGUAGE, syncLanguage, type Language } from '@shared/i18n' import { applyChromeTheme, applyTabAccent } from '@renderer/theme/chrome' import appIconUrl from '../../../build/icon.png' @@ -50,10 +50,11 @@ export default function App(): React.JSX.Element { }, [hydrate]) // Interface language: t() reads the module-level language at render time, so - // sync it before the tree renders — an effect would paint one frame late. The - // subscription re-renders every t() caller when the language changes elsewhere. - if (getLanguage() !== storedLanguage) setLanguage(storedLanguage) - const language = useSyncExternalStore(onLanguageChange, getLanguage) + // sync it before the tree renders — an effect would paint one frame late. + // Silent on purpose: notifying subscribers during a render is what React + // warns about; every language change re-renders App via the settings store. + syncLanguage(storedLanguage) + const language = storedLanguage useEffect(() => { document.documentElement.lang = language }, [language]) diff --git a/src/shared/i18n/index.ts b/src/shared/i18n/index.ts index cd0dc9f..5cb361f 100644 --- a/src/shared/i18n/index.ts +++ b/src/shared/i18n/index.ts @@ -70,6 +70,15 @@ export function setLanguage(lang: Language): void { for (const listener of listeners) listener(lang) } +/** Render-time sync for the React tree: update the module language WITHOUT + * notifying subscribers — firing listeners during another component's render + * makes React warn ("cannot update a component while rendering a different + * component"). Renderer components re-render via the settings store instead; + * the notifying setLanguage stays for the main process (tray rebuild). */ +export function syncLanguage(lang: Language): void { + current = lang +} + export function getLanguage(): Language { return current }