feat(packages): i18n (#1708)

This commit is contained in:
Sam Potts
2026-07-10 12:58:47 +10:00
committed by GitHub
parent 5f48fcc6d6
commit 028dadb385
561 changed files with 13059 additions and 1320 deletions
@@ -0,0 +1,43 @@
'use client';
import type { AlertDialogCore } from '@videojs/core';
import { getErrorDialogDismissLabel } from '@videojs/core';
import { resolveTranslation } from '@videojs/core/i18n';
import { forwardRef, type ReactNode, useCallback } from 'react';
import { useTranslator } from '../../i18n/context';
import type { UIComponentProps } from '../../utils/types';
import { renderElement } from '../../utils/use-render';
import { useAlertDialogContext } from '../alert-dialog/context';
export interface ErrorDialogCloseProps extends UIComponentProps<'button', AlertDialogCore.State> {}
export const ErrorDialogClose = forwardRef<HTMLButtonElement, ErrorDialogCloseProps>(function ErrorDialogClose(
{ render, className, style, disabled, children, ...elementProps },
forwardedRef
) {
const t = useTranslator();
const { dialog, state, stateAttrMap } = useAlertDialogContext();
const handleClick = useCallback(() => {
if (disabled) return;
dialog.close();
}, [dialog, disabled]);
const content: ReactNode = children ?? resolveTranslation(t, getErrorDialogDismissLabel());
return renderElement(
'button',
{ render, className, style },
{
state,
stateAttrMap,
ref: forwardedRef,
props: [{ type: 'button' as const, disabled, onClick: handleClick, children: content }, elementProps],
}
);
});
export namespace ErrorDialogClose {
export type Props = ErrorDialogCloseProps;
export type State = AlertDialogCore.State;
}
@@ -1,21 +1,24 @@
'use client';
import type { AlertDialogCore } from '@videojs/core';
import { forwardRef } from 'react';
import { resolveErrorDialogDescription } from '@videojs/core';
import { resolveTranslation } from '@videojs/core/i18n';
import { forwardRef, type ReactNode } from 'react';
import { useTranslator } from '../../i18n/context';
import type { UIComponentProps } from '../../utils/types';
import { renderElement } from '../../utils/use-render';
import { useAlertDialogContext } from '../alert-dialog/context';
import { useErrorDialogContext } from './context';
const FALLBACK_MESSAGE = 'An error occurred. Please try again.';
export interface ErrorDialogDescriptionProps extends UIComponentProps<'p', AlertDialogCore.State> {}
export const ErrorDialogDescription = forwardRef<HTMLParagraphElement, ErrorDialogDescriptionProps>(
function ErrorDialogDescription({ render, className, style, children, ...elementProps }, forwardedRef) {
const t = useTranslator();
const { state, stateAttrMap } = useAlertDialogContext();
const { lastError } = useErrorDialogContext();
const description = resolveErrorDialogDescription(lastError);
const content: ReactNode = children ?? resolveTranslation(t, description);
return renderElement(
'p',
@@ -24,10 +27,7 @@ export const ErrorDialogDescription = forwardRef<HTMLParagraphElement, ErrorDial
state,
stateAttrMap,
ref: forwardedRef,
props: [
{ id: state.descriptionId, children: children ?? lastError?.message ?? FALLBACK_MESSAGE },
elementProps,
],
props: [{ id: state.descriptionId, children: content }, elementProps],
}
);
}
@@ -0,0 +1,37 @@
'use client';
import type { AlertDialogCore } from '@videojs/core';
import { getErrorDialogTitleLabel } from '@videojs/core';
import { resolveTranslation } from '@videojs/core/i18n';
import { forwardRef, type ReactNode } from 'react';
import { useTranslator } from '../../i18n/context';
import type { UIComponentProps } from '../../utils/types';
import { renderElement } from '../../utils/use-render';
import { useAlertDialogContext } from '../alert-dialog/context';
export interface ErrorDialogTitleProps extends UIComponentProps<'h2', AlertDialogCore.State> {}
export const ErrorDialogTitle = forwardRef<HTMLHeadingElement, ErrorDialogTitleProps>(function ErrorDialogTitle(
{ render, className, style, children, ...elementProps },
forwardedRef
) {
const t = useTranslator();
const { state, stateAttrMap } = useAlertDialogContext();
const content: ReactNode = children ?? resolveTranslation(t, getErrorDialogTitleLabel());
return renderElement(
'h2',
{ render, className, style },
{
state,
stateAttrMap,
ref: forwardedRef,
props: [{ id: state.titleId, children: content }, elementProps],
}
);
});
export namespace ErrorDialogTitle {
export type Props = ErrorDialogTitleProps;
export type State = AlertDialogCore.State;
}
@@ -1,17 +1,11 @@
export {
AlertDialogClose as Close,
type AlertDialogCloseProps as CloseProps,
} from '../alert-dialog/alert-dialog-close';
export {
AlertDialogPopup as Popup,
type AlertDialogPopupProps as PopupProps,
} from '../alert-dialog/alert-dialog-popup';
export {
AlertDialogTitle as Title,
type AlertDialogTitleProps as TitleProps,
} from '../alert-dialog/alert-dialog-title';
export { ErrorDialogClose as Close, type ErrorDialogCloseProps as CloseProps } from './error-dialog-close';
export {
ErrorDialogDescription as Description,
type ErrorDialogDescriptionProps as DescriptionProps,
} from './error-dialog-description';
export { ErrorDialogRoot as Root, type ErrorDialogRootProps as RootProps } from './error-dialog-root';
export { ErrorDialogTitle as Title, type ErrorDialogTitleProps as TitleProps } from './error-dialog-title';
@@ -0,0 +1,50 @@
import { cleanup, render, screen } from '@testing-library/react';
import { registerI18n, resetI18nRegistry } from '@videojs/core/i18n';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { I18nProvider } from '../../../i18n';
import { createPlayerWrapper } from '../../../testing/mocks';
import { ErrorDialog } from '..';
afterEach(() => {
resetI18nRegistry();
cleanup();
});
describe('ErrorDialog', () => {
it('shows translated title, description, and dismiss label when locale is es', () => {
registerI18n('es', {
'Something went wrong.': 'Algo salió mal.',
OK: 'Aceptar',
'A network error caused the media download to fail.': 'Error de red.',
'An error occurred. Please try again.': 'Ocurrió un error. Inténtalo de nuevo.',
});
const error = {
code: 2,
message: 'A network error caused the media download to fail.',
};
const { Wrapper } = createPlayerWrapper({
error,
dismissError: vi.fn(),
});
render(
<Wrapper>
<I18nProvider locale="es">
<ErrorDialog.Root>
<ErrorDialog.Popup>
<ErrorDialog.Title data-testid="title" />
<ErrorDialog.Description data-testid="description" />
<ErrorDialog.Close data-testid="close" />
</ErrorDialog.Popup>
</ErrorDialog.Root>
</I18nProvider>
</Wrapper>
);
expect(screen.getByTestId('title').textContent).toBe('Algo salió mal.');
expect(screen.getByTestId('description').textContent).toBe('Error de red.');
expect(screen.getByTestId('close').textContent).toBe('Aceptar');
});
});