From db68359fb23f618c65a77f247c5027526c2e434f Mon Sep 17 00:00:00 2001 From: huiquanyun <80934379+huiquanyun@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:39:35 +0800 Subject: [PATCH] fix(fireedge): preload locale catalog server-side to eliminate translation flash TranslationProvider initialises with an empty messages object, causing a flash of untranslated (English) text on every page load before the async locale script resolves. This is particularly impactful for non-English locales (zh_CN, fr_FR, etc.) where untranslated text is completely foreign. Changes:/n- App.js: Read the default locale JSON at request time and inject as window.locale in the HTML template (alongside existing preloads) - translationProvider.js: Initialise useState with root.locale ?? {} instead of {}, so the first render uses preloaded translations The async loadMessages path is unchanged and still runs to ensure the full catalog is loaded. Falls back gracefully if the JSON file is missing. Tested on OpenNebula 7.2.0 with zh_CN locale. --- .../modules/providers/translationProvider.js | 19 +++++-- .../src/server/routes/entrypoints/App.js | 53 ++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/fireedge/src/modules/providers/translationProvider.js b/src/fireedge/src/modules/providers/translationProvider.js index a952b1bc89c..02a8fd2b4dc 100644 --- a/src/fireedge/src/modules/providers/translationProvider.js +++ b/src/fireedge/src/modules/providers/translationProvider.js @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * - * limitations under the License. * + * limitations under the License. * * ------------------------------------------------------------------------- */ import { Settings } from 'luxon' import PropTypes from 'prop-types' @@ -99,6 +99,13 @@ const loadMessages = (locale) => { /** * Provides the active locale and translation function. * + * When the server preloads a locale catalog into `window.locale` (via the + * `preload-locale` script tag in App.js), the provider initialises with + * those messages immediately, eliminating the first-render flash of + * untranslated text. The async `loadMessages` call still runs to ensure + * the full catalog is loaded; if the preloaded catalog is already complete, + * the state simply updates to the same value. + * * @param {object} props - Provider props * @param {any} props.children - Application tree * @returns {ReactElement} Translation context provider @@ -109,11 +116,17 @@ export const TranslationProvider = ({ children }) => { settings.LANG ?? settings.FIREEDGE?.LANG ?? DEFAULT_LANGUAGE const fallbackLocale = LANGUAGES[DEFAULT_LANGUAGE] ? DEFAULT_LANGUAGE : 'en' const locale = LANGUAGES[requestedLocale] ? requestedLocale : fallbackLocale + + // Use server-side preloaded translations if available to eliminate + // the first-render flash of untranslated (English) text. + const preloadedMessages = root.locale ?? {} + const hasPreloaded = Object.keys(preloadedMessages).length > 0 + const [state, setState] = useState({ error: null, - isLoading: true, + isLoading: !hasPreloaded, locale, - messages: {}, + messages: preloadedMessages, }) useEffect(() => { diff --git a/src/fireedge/src/server/routes/entrypoints/App.js b/src/fireedge/src/server/routes/entrypoints/App.js index 87e82b6915e..fd49c2233c0 100644 --- a/src/fireedge/src/server/routes/entrypoints/App.js +++ b/src/fireedge/src/server/routes/entrypoints/App.js @@ -11,11 +11,12 @@ * distributed under the License is distributed on an "AS IS" BASIS, * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * - * limitations under the License. * + * limitations under the License. * * ------------------------------------------------------------------------- */ -// eslint-disable-next-line node/no-deprecated-api const { parse } = require('url') const { Router } = require('express') +const path = require('path') +const fs = require('fs') // server const { getSunstoneConfig, getFireedgeConfig } = require('server/utils/yml') @@ -43,6 +44,38 @@ const globalApiTimeout = (config) => ? config.api_timeout : defaultApiTimeout +/** + * Loads the locale JSON catalog for server-side preloading. + * + * The catalog is read from `client/assets/languages/.json`. + * If the file does not exist or cannot be parsed, an empty object is + * returned so that the client falls back to the async loading path. + * + * @param {string} locale - Target locale (e.g. "en", "zh_CN") + * @returns {object} Parsed locale catalog or empty object + */ +const loadLocaleCatalog = (locale) => { + if (!locale) return {} + + const localeDir = path.join( + __dirname, + '..', + '..', + 'client', + 'assets', + 'languages' + ) + + // Prefer JSON catalog; fall back to empty if not found + const jsonPath = path.join(localeDir, `${locale}.json`) + try { + const content = fs.readFileSync(jsonPath, 'utf-8') + return JSON.parse(content) + } catch { + return {} + } +} + const router = Router() const defaultConfig = { @@ -85,6 +118,13 @@ router.get('*', async (req, res) => { } } + // Preload the default locale catalog so the client can render + // translated text on first paint without waiting for the async + // locale script to load. This eliminates the flash of untranslated + // (English) text on page load. + const defaultLang = appConfig?.default_lang ?? 'en' + const preloadedLocale = loadLocaleCatalog(defaultLang) + const faviconLink = encodedFavIcon && encodedFavIcon?.b64 !== null ? `` @@ -121,6 +161,14 @@ router.get('*', async (req, res) => { window.__PRELOADED_STATE__ = ${ensuredScriptValue(PRELOAD_STATE)} ` + // Preload the default locale catalog into window.locale so that + // TranslationProvider can initialise with translated messages + // instead of an empty object, eliminating first-render flash. + const localePreload = ` + ` + const html = ` @@ -136,6 +184,7 @@ router.get('*', async (req, res) => { ${storeRender} ${config} ${requestTimeOut} + ${localePreload} ${remoteModules} ${forecastConf}