diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index a606825d820..def3242e7c5 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -230,6 +230,12 @@ export interface GrafanaConfig { // The namespace to use for kubernetes apiserver requests namespace: string; + + /** + * Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of + * Grafana's supported language. + */ + language: string | undefined; } export interface SqlConnectionLimits { diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 3d25e0d5b2f..8a7dae06941 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -176,6 +176,12 @@ export class GrafanaBootConfig implements GrafanaConfig { localFileSystemAvailable: boolean | undefined; cloudMigrationIsTarget: boolean | undefined; + /** + * Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of + * Grafana's supported language. + */ + language: string | undefined; + constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; diff --git a/public/app/app.ts b/public/app/app.ts index 0de34f8fe68..613b40d560c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -42,7 +42,7 @@ import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelData import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; import { getScrollbarWidth } from '@grafana/ui'; -import config from 'app/core/config'; +import config, { updateConfig } from 'app/core/config'; import { arrayMove } from 'app/core/utils/arrayMove'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -126,6 +126,7 @@ export class GrafanaApp { parent.postMessage('GrafanaAppInit', '*'); const initI18nPromise = initializeI18n(config.bootData.user.language); + initI18nPromise.then(({ language }) => updateConfig({ language })); setBackendSrv(backendSrv); initEchoSrv(); diff --git a/public/app/core/internationalization/index.tsx b/public/app/core/internationalization/index.tsx index 3265397ce26..1b9d850c6d0 100644 --- a/public/app/core/internationalization/index.tsx +++ b/public/app/core/internationalization/index.tsx @@ -3,7 +3,7 @@ import LanguageDetector, { DetectorOptions } from 'i18next-browser-languagedetec import React from 'react'; import { Trans as I18NextTrans, initReactI18next } from 'react-i18next'; // eslint-disable-line no-restricted-imports -import { LANGUAGES, VALID_LANGUAGES } from './constants'; +import { DEFAULT_LANGUAGE, LANGUAGES, VALID_LANGUAGES } from './constants'; const getLanguagePartFromCode = (code: string) => code.split('-')[0].toLowerCase(); @@ -23,7 +23,7 @@ const loadTranslations: BackendModule = { }, }; -export function initializeI18n(language: string) { +export function initializeI18n(language: string): Promise<{ language: string | undefined }> { // This is a placeholder so we can put a 'comment' in the message json files. // Starts with an underscore so it's sorted to the top of the file. Even though it is in a comment the following line is still extracted // t('_comment', 'This file is the source of truth for English strings. Edit this to change plurals and other phrases for the UI.'); @@ -35,19 +35,30 @@ export function initializeI18n(language: string) { // If translations are empty strings (no translation), fall back to the default value in source code returnEmptyString: false, + + // Required to ensure that `resolvedLanguage` is set property when an invalid language is passed (such as through 'detect') + supportedLngs: VALID_LANGUAGES, + fallbackLng: DEFAULT_LANGUAGE, }; - let init = i18n; + let i18nInstance = i18n; if (language === 'detect') { - init = init.use(LanguageDetector); + i18nInstance = i18nInstance.use(LanguageDetector); const detection: DetectorOptions = { order: ['navigator'], caches: [] }; options.detection = detection; } else { options.lng = VALID_LANGUAGES.includes(language) ? language : undefined; } - return init + + const loadPromise = i18nInstance .use(loadTranslations) .use(initReactI18next) // passes i18n down to react-i18next .init(options); + + return loadPromise.then(() => { + return { + language: i18nInstance.resolvedLanguage, + }; + }); } export function changeLanguage(locale: string) {