grafana/public/app/core/internationalization/index.tsx
Josh Hunt fe24404432
I18n: Support for Enterprise translations (#86215)
* I18n: Support for Enterprise translations

* don't attempt to link to enterprise in tests

* move extract script to makefile to optionally support enterprise

* update references to old extract script

* update docs

* thank god for unit tests
2024-04-18 16:25:27 +01:00

89 lines
3.2 KiB
TypeScript

import i18n, { InitOptions, TFunction } from 'i18next';
import LanguageDetector, { DetectorOptions } from 'i18next-browser-languagedetector';
import React from 'react';
import { Trans as I18NextTrans, initReactI18next } from 'react-i18next'; // eslint-disable-line no-restricted-imports
import { DEFAULT_LANGUAGE, NAMESPACES, VALID_LANGUAGES } from './constants';
import { loadTranslations } from './loadTranslations';
let tFunc: TFunction<string[], undefined> | undefined;
export async 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', 'The code is the source of truth for English phrases. They should be updated in the components directly, and additional plurals specified in this file.');
const options: InitOptions = {
// We don't bundle any translations, we load them async
partialBundledLanguages: true,
resources: {},
// 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,
ns: NAMESPACES,
};
let i18nInstance = i18n;
if (language === 'detect') {
i18nInstance = i18nInstance.use(LanguageDetector);
const detection: DetectorOptions = { order: ['navigator'], caches: [] };
options.detection = detection;
} else {
options.lng = VALID_LANGUAGES.includes(language) ? language : undefined;
}
const loadPromise = i18nInstance
.use(loadTranslations)
.use(initReactI18next) // passes i18n down to react-i18next
.init(options);
await loadPromise;
tFunc = i18n.getFixedT(null, NAMESPACES);
return {
language: i18nInstance.resolvedLanguage,
};
}
export function changeLanguage(locale: string) {
const validLocale = VALID_LANGUAGES.includes(locale) ? locale : undefined;
return i18n.changeLanguage(validLocale);
}
export const Trans: typeof I18NextTrans = (props) => {
return <I18NextTrans shouldUnescape ns={NAMESPACES} {...props} />;
};
// Wrap t() to provide default namespaces and enforce a consistent API
export const t = (id: string, defaultMessage: string, values?: Record<string, unknown>) => {
if (!tFunc) {
if (process.env.NODE_ENV !== 'test') {
console.warn(
't() was called before i18n was initialized. This is probably caused by calling t() in the root module scope, instead of lazily on render'
);
}
if (process.env.NODE_ENV === 'development') {
throw new Error('t() was called before i18n was initialized');
}
tFunc = i18n.t;
}
return tFunc(id, defaultMessage, values);
};
export const i18nDate = (value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string => {
if (typeof value === 'string') {
return i18nDate(new Date(value), format);
}
const dateFormatter = new Intl.DateTimeFormat(i18n.language, format);
return dateFormatter.format(value);
};