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}