I18n: Allow pseudo-locale to be enabled in production builds (#106738)
* I18n: Allow pseudo-locale to be enabled in production builds * fix tests now that pseudo is around * remove psuedo locale from i18n package * load en-us from plugin resources for pseudo * fix tests + remove 'hidden' option --------- Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
co-authored by
Ashley Harrison
parent
e92baba748
commit
e49c73533b
@@ -11,6 +11,14 @@ import { ResourceLoader, Resources, TFunction, TransProps, TransType } from './t
|
||||
let tFunc: I18NextTFunction<string[], undefined> | undefined;
|
||||
let transComponent: TransType;
|
||||
|
||||
const VALID_LANGUAGES = [
|
||||
...LANGUAGES,
|
||||
{
|
||||
name: 'Pseudo',
|
||||
code: PSEUDO_LOCALE,
|
||||
},
|
||||
];
|
||||
|
||||
function initTFuncAndTransComponent({ id, ns }: { id?: string; ns?: string[] } = {}) {
|
||||
if (id) {
|
||||
tFunc = getI18nInstance().getFixedT(null, id);
|
||||
@@ -28,13 +36,15 @@ export async function loadPluginResources(id: string, language: string, loaders?
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedLanguage = language === PSEUDO_LOCALE ? DEFAULT_LANGUAGE : language;
|
||||
|
||||
return Promise.all(
|
||||
loaders.map(async (loader) => {
|
||||
try {
|
||||
const resources = await loader(language);
|
||||
addResourceBundle(language, id, resources);
|
||||
const resources = await loader(resolvedLanguage);
|
||||
addResourceBundle(resolvedLanguage, id, resources);
|
||||
} catch (error) {
|
||||
console.error(`Error loading resources for plugin ${id} and language: ${language}`, error);
|
||||
console.error(`Error loading resources for plugin ${id} and language: ${resolvedLanguage}`, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -108,7 +118,7 @@ async function initTranslations({
|
||||
returnEmptyString: false,
|
||||
|
||||
// Required to ensure that `resolvedLanguage` is set property when an invalid language is passed (such as through 'detect')
|
||||
supportedLngs: LANGUAGES.map((language) => language.code),
|
||||
supportedLngs: VALID_LANGUAGES.map((lang) => lang.code),
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
|
||||
ns,
|
||||
@@ -123,7 +133,7 @@ async function initTranslations({
|
||||
const detection: DetectorOptions = { order: ['navigator'], caches: [] };
|
||||
options.detection = detection;
|
||||
} else {
|
||||
options.lng = LANGUAGES.find((lang) => lang.code === language)?.code ?? undefined;
|
||||
options.lng = VALID_LANGUAGES.find((lang) => lang.code === language)?.code ?? undefined;
|
||||
}
|
||||
|
||||
if (module) {
|
||||
@@ -132,7 +142,7 @@ async function initTranslations({
|
||||
getI18nInstance().use(initReactI18next); // passes i18n down to react-i18next
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (language === PSEUDO_LOCALE) {
|
||||
const { default: Pseudo } = await import('i18next-pseudo');
|
||||
getI18nInstance().use(
|
||||
new Pseudo({
|
||||
@@ -165,7 +175,7 @@ export function getNamespaces() {
|
||||
}
|
||||
|
||||
export async function changeLanguage(language?: string) {
|
||||
const validLanguage = LANGUAGES.find((lang) => lang.code === language)?.code ?? DEFAULT_LANGUAGE;
|
||||
const validLanguage = VALID_LANGUAGES.find((lang) => lang.code === language)?.code ?? DEFAULT_LANGUAGE;
|
||||
await getI18nInstance().changeLanguage(validLanguage);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,19 +27,28 @@ describe('LANGUAGES', () => {
|
||||
expect(LANGUAGES).toEqual(expectedLanguages);
|
||||
});
|
||||
|
||||
it('should include the pseudo-locale in development mode', async () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
jest.resetModules();
|
||||
const { LANGUAGES: languages } = await import('./languages');
|
||||
|
||||
expect(languages).toEqual([...expectedLanguages, { code: 'pseudo', name: 'Pseudo-locale' }]);
|
||||
it('should match a canonical locale definition', () => {
|
||||
for (const lang of LANGUAGES) {
|
||||
const resolved = Intl.getCanonicalLocales(lang.code);
|
||||
expect(lang.code).toEqual(resolved[0]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not include the pseudo-locale in production mode', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
jest.resetModules();
|
||||
const { LANGUAGES: languages } = await import('./languages');
|
||||
it('should have locale codes including the country code', () => {
|
||||
for (const lang of LANGUAGES) {
|
||||
if (lang.code === 'pseudo') {
|
||||
// special case pseudo because its not a real language
|
||||
continue;
|
||||
}
|
||||
expect(lang.code).toMatch(/^[a-z]{2}-[a-zA-Z]+$/);
|
||||
}
|
||||
});
|
||||
|
||||
expect(languages).toEqual(expectedLanguages);
|
||||
it('should not have duplicate languages codes', () => {
|
||||
for (let i = 0; i < LANGUAGES.length; i++) {
|
||||
const lang = LANGUAGES[i];
|
||||
const index = LANGUAGES.findIndex((v) => v.code === lang.code);
|
||||
expect(index).toBe(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,11 +18,13 @@ import {
|
||||
POLISH_POLAND,
|
||||
SWEDISH_SWEDEN,
|
||||
TURKISH_TURKEY,
|
||||
PSEUDO_LOCALE,
|
||||
} from './constants';
|
||||
|
||||
interface TranslationDefinition {
|
||||
/** IETF language tag */
|
||||
code: string;
|
||||
|
||||
/** The language name in its own language (e.g. "Français" for French) */
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -50,7 +52,3 @@ export const LANGUAGES: TranslationDefinition[] = [
|
||||
{ code: SWEDISH_SWEDEN, name: 'Svenska' },
|
||||
{ code: TURKISH_TURKEY, name: 'Türkçe' },
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
LANGUAGES.push({ code: PSEUDO_LOCALE, name: 'Pseudo-locale' });
|
||||
}
|
||||
|
||||
@@ -161,6 +161,24 @@ describe('SharedPreferences', () => {
|
||||
expect(langSelect).toHaveValue('Default');
|
||||
});
|
||||
|
||||
it('does not render the pseudo-locale', async () => {
|
||||
const langSelect = await screen.findByRole('combobox', { name: /language/i });
|
||||
|
||||
// Open the combobox and wait for the options to be rendered
|
||||
await userEvent.click(langSelect);
|
||||
|
||||
// TODO: The input value should be cleared when clicked, but for some reason it's not?
|
||||
// checking langSelect.value beforehand indicates that it is cleared, but after using
|
||||
// userEvent.type the default value comes back?
|
||||
await userEvent.type(
|
||||
langSelect,
|
||||
'{Backspace}{Backspace}{Backspace}{Backspace}{Backspace}{Backspace}{Backspace}Pseudo'
|
||||
);
|
||||
|
||||
const option = screen.queryByRole('option', { name: 'Pseudo-locale' });
|
||||
expect(option).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saves the users new preferences', async () => {
|
||||
await selectComboboxOptionInTest(await screen.findByRole('combobox', { name: 'Interface theme' }), 'Dark');
|
||||
await selectOptionInTest(screen.getByLabelText('Timezone'), 'Australia/Sydney');
|
||||
|
||||
@@ -57,6 +57,13 @@ function getLanguageOptions(): ComboboxOption[] {
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
languageOptions.push({
|
||||
value: PSEUDO_LOCALE,
|
||||
label: 'Pseudo-locale',
|
||||
});
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: '',
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import { LANGUAGES, VALID_LANGUAGES } from './constants';
|
||||
|
||||
describe('internationalization constants', () => {
|
||||
it('should match a canonical locale definition', () => {
|
||||
for (const lang of LANGUAGES) {
|
||||
const resolved = Intl.getCanonicalLocales(lang.code);
|
||||
expect(lang.code).toEqual(resolved[0]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have locale codes including the country code', () => {
|
||||
for (const lang of LANGUAGES) {
|
||||
expect(lang.code).toMatch(/^[a-z]{2}-[a-zA-Z]+$/);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not have duplicate languages codes', () => {
|
||||
const uniqLocales = uniqBy(LANGUAGES, (v) => v.code);
|
||||
expect(LANGUAGES).toHaveLength(uniqLocales.length);
|
||||
});
|
||||
|
||||
it('should have a correct list of valid locale codes', () => {
|
||||
expect(VALID_LANGUAGES).toEqual(LANGUAGES.map((v) => v.code));
|
||||
});
|
||||
|
||||
@@ -4,13 +4,9 @@ import { uniq } from 'lodash';
|
||||
import { DEFAULT_LANGUAGE, PSEUDO_LOCALE, LANGUAGES as SUPPORTED_LANGUAGES } from '@grafana/i18n';
|
||||
|
||||
export type LocaleFileLoader = () => Promise<ResourceKey>;
|
||||
export interface LanguageDefinition<Namespace extends string = string> {
|
||||
/** IETF language tag for the language e.g. en-US */
|
||||
code: string;
|
||||
|
||||
/** Language name to show in the UI. Should be formatted local to that language e.g. Français for French */
|
||||
name: string;
|
||||
|
||||
type BaseLanguageDefinition = (typeof SUPPORTED_LANGUAGES)[number];
|
||||
export interface LanguageDefinition<Namespace extends string = string> extends BaseLanguageDefinition {
|
||||
/** Function to load translations */
|
||||
loader: Record<Namespace, LocaleFileLoader>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user