Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/fireedge/src/modules/providers/translationProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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(() => {
Expand Down
53 changes: 51 additions & 2 deletions src/fireedge/src/server/routes/entrypoints/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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/<locale>.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 = {
Expand Down Expand Up @@ -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
? `<link rel="icon" href="${encodedFavIcon.b64}">`
Expand Down Expand Up @@ -121,6 +161,14 @@ router.get('*', async (req, res) => {
window.__PRELOADED_STATE__ = ${ensuredScriptValue(PRELOAD_STATE)}
</script>`

// 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 = `
<script id="preload-locale">
window.locale = ${ensuredScriptValue(preloadedLocale)}
</script>`

const html = `
<!DOCTYPE html>
<html lang="en">
Expand All @@ -136,6 +184,7 @@ router.get('*', async (req, res) => {
${storeRender}
${config}
${requestTimeOut}
${localePreload}
${remoteModules}
${forecastConf}
<script src='${APP_URL}/client/bundle.${appName}.js'></script>
Expand Down