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: 13 additions & 6 deletions datahub-web-react/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import { BADGE, defaultBadgesConfig } from '@geometricpanda/storybook-addon-badges';
import { ConfigProvider } from 'antd';
// FYI: import of antd styles required to show components based on it correctly
import 'antd/dist/antd.css';
import React, { useEffect } from 'react';
import { I18nextProvider } from 'react-i18next';
import { ThemeProvider } from 'styled-components';

import { LOCALE_MAP } from '../src/app/i18n/constants';
import { isSupportedLanguage } from '../src/app/i18n/utils';
import themes from '../src/conf/theme/themes';
import dayjs from '../src/utils/dayjs';
import DocTemplate from './DocTemplate.mdx';
import i18n from './i18n';
import './storybook-theme.css';

// Drives the shared i18next instance from the toolbar's selected locale. Done in an
// effect so we mutate the singleton after render rather than during it.
// Drives i18next, antd, and dayjs from the toolbar's selected locale — mirroring the app's
// `I18nProvider`/`useLanguageSync` so antd components (e.g. DatePicker calendar labels) and
// dayjs-formatted dates localize too, not just `t()` strings. i18next is mutated in an effect
// so we touch the singleton after render rather than during it.
const LocaleProvider = ({ locale, children }: { locale: string; children: React.ReactNode }) => {
const localeConfig = isSupportedLanguage(locale) ? LOCALE_MAP[locale] : LOCALE_MAP.en;
useEffect(() => {
if (i18n.language !== locale) {
i18n.changeLanguage(locale);
if (i18n.language !== localeConfig.lang) {
i18n.changeLanguage(localeConfig.lang);
}
}, [locale]);
return <>{children}</>;
dayjs.locale(localeConfig.dayjs);
}, [localeConfig.lang, localeConfig.dayjs]);
return <ConfigProvider locale={localeConfig.antd}>{children}</ConfigProvider>;
};

const preview = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { i18n as remirrorI18n } from '@remirror/i18n';
import * as remirrorPlurals from '@remirror/i18n/plurals';
import { I18nProvider } from '@remirror/react';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { REMIRROR_LOCALE_MESSAGES } from '@src/i18n/remirror';
import { REMIRROR_LOCALE_LOADERS } from '@src/i18n/remirror';

// Languages whose Remirror built-in labels we localize. Any other app language falls back
// to English (Remirror's built-in) so its labels never render as raw message ids.
const REMIRROR_SUPPORTED_LOCALES = ['en', ...Object.keys(REMIRROR_LOCALE_LOADERS)];

// Remirror's built-in labels use its own Lingui i18n, which ships English only. Load our
// supplementary locale bundles (e.g. German) into that shared instance once — values may
// be plain strings or ICU messages, which Lingui compiles at render time. Plural messages
// also need the locale's plural rules.
Object.entries(REMIRROR_LOCALE_MESSAGES).forEach(([locale, messages]) => {
// supplementary locale bundle (e.g. German) into that shared instance the first time its
// language is activated — values may be plain strings or ICU messages, which Lingui compiles
// at render time. Plural messages also need the locale's plural rules.
async function loadRemirrorLocale(locale: string): Promise<void> {
const loader = REMIRROR_LOCALE_LOADERS[locale];
// `en` has no loader (built in); a bundle is loaded at most once.
if (!loader || remirrorI18n.messages[locale]) {
return;
}
const { default: messages } = await loader();
const plurals = (remirrorPlurals as Record<string, ((n: number, ord?: boolean) => string) | undefined>)[locale];
if (plurals) {
remirrorI18n.loadLocaleData(locale, { plurals });
}
remirrorI18n.load(locale, messages);
});

// Languages whose Remirror built-in labels we localize. Any other app language falls back
// to English (Remirror's built-in) so its labels never render as raw message ids.
const REMIRROR_SUPPORTED_LOCALES = ['en', ...Object.keys(REMIRROR_LOCALE_MESSAGES)];
}

type Props = {
children: React.ReactNode;
Expand All @@ -35,12 +41,26 @@ export default function RemirrorLocaleProvider({ children }: Props) {
const { i18n: appI18n } = useTranslation();
const appLanguage = (appI18n.resolvedLanguage || appI18n.language || 'en').split('-')[0];
const locale = REMIRROR_SUPPORTED_LOCALES.includes(appLanguage) ? appLanguage : 'en';
// Only activate once the bundle is loaded, so labels never flash raw message ids. Until then
// the provider stays on English (Remirror's built-in).
const [activeLocale, setActiveLocale] = useState('en');

useEffect(() => {
remirrorI18n.activate(locale);
let cancelled = false;
loadRemirrorLocale(locale).then(() => {
if (cancelled) {
return;
}
remirrorI18n.activate(locale);
setActiveLocale(locale);
});
return () => {
cancelled = true;
};
}, [locale]);

return (
<I18nProvider i18n={remirrorI18n} locale={locale}>
<I18nProvider i18n={remirrorI18n} locale={activeLocale}>
{children}
</I18nProvider>
);
Expand Down
1 change: 1 addition & 0 deletions datahub-web-react/src/app/i18n/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('isSupportedLanguage', () => {
it('returns true for supported languages', () => {
expect(isSupportedLanguage('en')).toBe(true);
expect(isSupportedLanguage('de')).toBe(true);
expect(isSupportedLanguage('es')).toBe(true);
});

it('returns false for unsupported languages', () => {
Expand Down
24 changes: 18 additions & 6 deletions datahub-web-react/src/app/i18n/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SelectOption } from '@components';
import deDE from 'antd/lib/locale/de_DE';
import enUS from 'antd/lib/locale/en_US';
import esES from 'antd/lib/locale/es_ES';
import ptBR from 'antd/lib/locale/pt_BR';

import { LocaleConfig, SupportedLanguage } from '@app/i18n/types';
Expand All @@ -19,6 +20,13 @@ export const DE_LOCALE_CONFIG: LocaleConfig = {
label: 'Deutsch',
};

export const ES_LOCALE_CONFIG: LocaleConfig = {
lang: 'es',
antd: esES,
dayjs: 'es',
label: 'Español (Beta)',
};

export const PT_BR_LOCALE_CONFIG: LocaleConfig = {
lang: 'pt-BR',
antd: ptBR,
Expand All @@ -29,14 +37,18 @@ export const PT_BR_LOCALE_CONFIG: LocaleConfig = {
export const LOCALE_MAP: Record<SupportedLanguage, LocaleConfig> = {
en: EN_LOCALE_CONFIG,
de: DE_LOCALE_CONFIG,
es: ES_LOCALE_CONFIG,
'pt-BR': PT_BR_LOCALE_CONFIG,
};

export const LANGUAGE_OPTIONS: SelectOption[] = [EN_LOCALE_CONFIG, DE_LOCALE_CONFIG, PT_BR_LOCALE_CONFIG].map(
(localeConfig) => ({
value: localeConfig.lang,
label: localeConfig.label,
}),
);
export const LANGUAGE_OPTIONS: SelectOption[] = [
EN_LOCALE_CONFIG,
DE_LOCALE_CONFIG,
ES_LOCALE_CONFIG,
PT_BR_LOCALE_CONFIG,
].map((localeConfig) => ({
value: localeConfig.lang,
label: localeConfig.label,
}));

export const DEFAULT_LANGUAGE: SupportedLanguage = 'en';
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_LANGUAGE } from '@app/i18n/constants';
import { useChangeLocale } from '@app/i18n/hooks/useChangeLocale';
import { useUpdateUserLocaleSettings } from '@app/i18n/hooks/useUpdateUserLocaleSettings';
import dayjs from '@utils/dayjs';
import { setDayjsLocale } from '@utils/dayjs';

vi.mock('@app/i18n/hooks/useUpdateUserLocaleSettings');
vi.mock('@utils/dayjs', () => ({ default: { locale: vi.fn() } }));
vi.mock('@utils/dayjs', () => ({ setDayjsLocale: vi.fn().mockResolvedValue(undefined) }));

const mockUpdateUserLocaleSettings = vi.fn().mockResolvedValue({});
vi.mocked(useUpdateUserLocaleSettings).mockReturnValue(mockUpdateUserLocaleSettings);
Expand All @@ -29,7 +29,7 @@ describe('useChangeLocale', () => {

expect(mockUpdateUserLocaleSettings).toHaveBeenCalledWith('en');
expect(i18next.changeLanguage).toHaveBeenCalledWith('en');
expect(dayjs.locale).toHaveBeenCalledWith('en');
expect(setDayjsLocale).toHaveBeenCalledWith('en');
});

it('falls back to DEFAULT_LANGUAGE for unsupported language', async () => {
Expand All @@ -41,7 +41,7 @@ describe('useChangeLocale', () => {

expect(mockUpdateUserLocaleSettings).toHaveBeenCalledWith('unsupported');
expect(i18next.changeLanguage).toHaveBeenCalledWith(DEFAULT_LANGUAGE);
expect(dayjs.locale).toHaveBeenCalledWith(DEFAULT_LANGUAGE);
expect(setDayjsLocale).toHaveBeenCalledWith(DEFAULT_LANGUAGE);
});

it('falls back to DEFAULT_LANGUAGE for null', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { LOCALE_MAP } from '@app/i18n/constants';
import { useLanguageSync } from '@app/i18n/hooks/useLanguageSync';
import { useLocaleConfig } from '@app/i18n/hooks/useLocaleConfig';
import dayjs from '@utils/dayjs';
import { setDayjsLocale } from '@utils/dayjs';

vi.mock('@app/i18n/hooks/useLocaleConfig');
vi.mock('i18next', () => ({ default: { changeLanguage: vi.fn() } }));
vi.mock('@utils/dayjs', () => ({ default: { locale: vi.fn() } }));
vi.mock('@utils/dayjs', () => ({ setDayjsLocale: vi.fn().mockResolvedValue(undefined) }));

const mockUseLocaleConfig = vi.mocked(useLocaleConfig);

Expand All @@ -24,7 +24,7 @@ describe('useLanguageSync', () => {
renderHook(() => useLanguageSync());

expect(i18next.changeLanguage).toHaveBeenCalledWith('en');
expect(dayjs.locale).toHaveBeenCalledWith('en');
expect(setDayjsLocale).toHaveBeenCalledWith('en');
});

it('re-syncs when locale config changes', () => {
Expand All @@ -37,6 +37,6 @@ describe('useLanguageSync', () => {
rerender();

expect(i18next.changeLanguage).toHaveBeenCalledWith('de');
expect(dayjs.locale).toHaveBeenCalledWith('de');
expect(setDayjsLocale).toHaveBeenCalledWith('de');
});
});
4 changes: 2 additions & 2 deletions datahub-web-react/src/app/i18n/hooks/useChangeLocale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DEFAULT_LANGUAGE, LOCALE_MAP } from '@app/i18n/constants';
import { useUpdateUserLocaleSettings } from '@app/i18n/hooks/useUpdateUserLocaleSettings';
import { SupportedLanguage } from '@app/i18n/types';
import { isSupportedLanguage } from '@app/i18n/utils';
import dayjs from '@utils/dayjs';
import { setDayjsLocale } from '@utils/dayjs';

export function useChangeLocale() {
const updateUserLocaleSettings = useUpdateUserLocaleSettings();
Expand All @@ -19,7 +19,7 @@ export function useChangeLocale() {
await i18next.loadLanguages(localeConfig.lang);
await updateUserLocaleSettings(language);
i18next.changeLanguage(localeConfig.lang);
dayjs.locale(localeConfig.dayjs);
await setDayjsLocale(localeConfig.dayjs);
},
[updateUserLocaleSettings],
);
Expand Down
6 changes: 4 additions & 2 deletions datahub-web-react/src/app/i18n/hooks/useLanguageSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import i18next from 'i18next';
import { useEffect } from 'react';

import { useLocaleConfig } from '@app/i18n/hooks/useLocaleConfig';
import dayjs from '@utils/dayjs';
import { setDayjsLocale } from '@utils/dayjs';

export function useLanguageSync(): void {
const localeConfig = useLocaleConfig();

useEffect(() => {
i18next.changeLanguage(localeConfig.lang);
dayjs.locale(localeConfig.dayjs);
// setDayjsLocale resolves after the locale chunk loads; ignore the promise — dayjs falls
// back to its current locale until then, and a later language change supersedes this one.
setDayjsLocale(localeConfig.dayjs);
}, [localeConfig.lang, localeConfig.dayjs]);
}
2 changes: 1 addition & 1 deletion datahub-web-react/src/app/i18n/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Locale } from 'antd/lib/locale-provider';

export type SupportedLanguage = 'en' | 'de' | 'pt-BR';
export type SupportedLanguage = 'en' | 'de' | 'es' | 'pt-BR';

export type LocaleConfig = {
lang: SupportedLanguage;
Expand Down
80 changes: 80 additions & 0 deletions datahub-web-react/src/i18n/locales/es/alchemy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"avatar.defaultName": "Nombre de usuario",
"calendarChart.weekday.fri": "Vie",
"calendarChart.weekday.mon": "Lun",
"calendarChart.weekday.sat": "Sáb",
"calendarChart.weekday.sun": "Dom",
"calendarChart.weekday.thu": "Jue",
"calendarChart.weekday.tue": "Mar",
"calendarChart.weekday.wed": "Mié",
"colorPicker.invalidHex.error": "Introduzca un código de color hexadecimal válido",
"datePicker.placeholder": "Seleccionar fecha",
"editor.addImage.altLabel": "Texto alternativo",
"editor.addImage.embed": "Insertar imagen",
"editor.addImage.title": "Añadir imagen",
"editor.addImage.urlLabel": "URL de la imagen",
"editor.command.editLink": "Editar enlace",
"editor.command.insertImage": "Imagen",
"editor.command.insertLink": "Enlace",
"editor.command.toggleBulletList": "Lista con viñetas",
"editor.command.toggleLink": "Quitar enlace",
"editor.command.updateLink": "Actualizar enlace",
"editor.heading.h1": "Encabezado 1",
"editor.heading.h2": "Encabezado 2",
"editor.heading.h3": "Encabezado 3",
"editor.heading.h4": "Encabezado 4",
"editor.heading.h5": "Encabezado 5",
"editor.heading.normal": "Normal",
"editor.link.textLabel": "Texto",
"editor.link.title": "Añadir enlace",
"editor.link.urlLabel": "URL del enlace",
"editor.mentions.notFound": "No se pudo encontrar la entidad",
"editor.table.deleteColumn": "Eliminar columna",
"editor.table.deleteRow": "Eliminar fila",
"editor.table.deleteTable": "Eliminar tabla",
"editor.table.insertColumnLeft": "Insertar columna a la izquierda",
"editor.table.insertColumnRight": "Insertar columna a la derecha",
"editor.table.insertRowAbove": "Insertar fila arriba",
"editor.table.insertRowBelow": "Insertar fila abajo",
"editor.upload.chooseFile": "Elegir archivo",
"editor.upload.failedTitle": "Error en la carga",
"editor.upload.notSupported": "La carga de archivos no está disponible actualmente en este contexto",
"editor.upload.tabUrl": "URL",
"editor.upload.uploadFile": "Cargar archivo",
"fileNode.uploading": "Cargando {{fileName}}...",
"fileUpload.dragDropPrompt": "Arrastre un archivo o <uploadButton>haga clic para cargar</uploadButton>",
"fileUpload.maxSize": "Tamaño máximo: 2 GB",
"graphCard.moreInfo": "Más información",
"graphCard.noStats": "No se han recopilado estadísticas para este activo por el momento.",
"incidentStage.fixed": "Resuelta",
"incidentStage.inProgress": "En curso",
"incidentStage.investigation": "Investigación",
"incidentStage.noAction": "Sin acción",
"incidentStage.triage": "Triaje",
"input.placeholder": "Marcador de posición",
"loadedImage.error": "Imagen no encontrada",
"loadedVideo.error": "No se pudo cargar el vídeo",
"loadedVideo.unsupportedFallback": "Su navegador no admite vídeo HTML5. Aquí tiene un <anchor>enlace al vídeo</anchor> en su lugar.",
"noData": "Sin datos",
"radio.label": "Etiqueta",
"resizablePills.moreHiddenCount_one": "{{count}} elemento más oculto",
"resizablePills.moreHiddenCount_other": "{{count}} elementos más ocultos",
"search.placeholder": "Buscar...",
"select.placeholder": "Seleccione una opción",
"selectItems.addMoreSection": "Añadir más",
"selectItems.entitiesLabel": "entidades",
"selectItems.itemsLabel": "elementos",
"selectItems.noEntityFound": "No se encontró {{entityName}}",
"selectItems.removeAll": "Quitar todo",
"selectItems.searchForEntity": "Buscar {{entityName}}...",
"selectItems.selectedSection": "Seleccionado",
"switch.label": "Etiqueta",
"table.loading": "Cargando datos...",
"textArea.placeholder": "Marcador de posición",
"tooltipHeader.imageAlt": "Encabezado del tooltip",
"whiskerChart.metric.firstQuartile": "Primer cuartil",
"whiskerChart.metric.max": "Máximo",
"whiskerChart.metric.median": "Mediana",
"whiskerChart.metric.min": "Mínimo",
"whiskerChart.metric.thirdQuartile": "Tercer cuartil"
}
11 changes: 11 additions & 0 deletions datahub-web-react/src/i18n/locales/es/analytics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"allDomains": "Todos los dominios",
"dataLandscapeSummary": "Resumen del panorama de datos",
"domainLandscapeSummary": "Resumen del panorama de dominios",
"failedToLoadCharts": "No se pudieron cargar los gráficos",
"failedToLoadDomains": "No se pudieron cargar los dominios",
"failedToLoadHighlights": "No se pudieron cargar los aspectos destacados",
"noDomainData": "No hay datos de analítica para este dominio",
"selectDomain": "Seleccionar dominio",
"usageAnalytics": "Analítica de uso"
}
30 changes: 30 additions & 0 deletions datahub-web-react/src/i18n/locales/es/auth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"confirmPasswordLabel": "Confirmar contraseña",
"confirmPasswordRequired": "Confirme su contraseña",
"emailLabel": "Correo electrónico",
"emailPlaceholder": "nombre@empresa.com",
"emailRequired": "Introduzca su correo electrónico",
"fullNameLabel": "Nombre completo",
"fullNamePlaceholder": "Nombre Apellido",
"fullNameRequired": "Introduzca su nombre",
"login.failed": "Error al iniciar sesión. {{error}}",
"login.loading": "Iniciando sesión...",
"login.ssoButton": "Iniciar sesión con SSO",
"login.submitButton": "Iniciar sesión",
"passwordHint": "Debe tener 8 caracteres; distingue entre mayúsculas y minúsculas",
"passwordLabel": "Contraseña",
"passwordRequired": "Introduzca su contraseña",
"passwordsDoNotMatch": "Las contraseñas no coinciden",
"reset.failed": "Error al iniciar sesión.",
"reset.loading": "Restableciendo credenciales...",
"reset.submitButton": "Restablecer credenciales",
"signup.acceptInviteFailed": "No se pudo aceptar la invitación: {{error}}",
"signup.acceptedInvite": "Invitación aceptada.",
"signup.loginFailed": "Error al iniciar sesión. Se ha producido un error inesperado.",
"signup.subHeading": "Antes de empezar, solo le haremos unas preguntas",
"signup.submitButton": "Empezar",
"usernameLabel": "Nombre de usuario",
"usernamePlaceholder": "Introduzca el nombre de usuario",
"usernameRequired": "Introduzca su nombre de usuario",
"welcomeToDataHub": "Le damos la bienvenida a DataHub"
}
Loading
Loading