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>
apis/plugins
Josh Hunt 1 month ago committed by GitHub
parent e92baba748
commit e49c73533b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 24
      packages/grafana-i18n/src/i18n.tsx
  2. 31
      packages/grafana-i18n/src/languages.test.ts
  3. 8
      packages/grafana-i18n/src/languages.ts
  4. 18
      public/app/core/components/SharedPreferences/SharedPreferences.test.tsx
  5. 7
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  6. 20
      public/app/core/internationalization/constants.test.ts
  7. 8
      public/app/core/internationalization/constants.ts

@ -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>;
}

Loading…
Cancel
Save