Skip to content

Commit

Permalink
46 Internationalization Example (#53)
Browse files Browse the repository at this point in the history
* #46 initial i18n

* #46 tests

* #46 tests

* #46 namespaces

* #46 translations modules

* #46 welcome translation

* #46 translations
  • Loading branch information
mwarman authored May 30, 2024
1 parent efced3d commit 6abb0c4
Show file tree
Hide file tree
Showing 30 changed files with 714 additions and 24 deletions.
74 changes: 71 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@
"classnames": "2.5.1",
"dayjs": "1.11.10",
"formik": "2.4.5",
"i18next": "23.11.5",
"i18next-browser-languagedetector": "8.0.0",
"lodash": "4.17.21",
"qs": "6.11.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "14.1.2",
"react-router-dom": "6.22.1",
"tailwindcss": "3.4.1",
"uuid": "9.0.1",
Expand Down
57 changes: 57 additions & 0 deletions src/components/Button/LanguageToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { PropsWithClassName } from '@leanstacks/react-common';
import { useTranslation } from 'react-i18next';

import { StorageKeys } from 'utils/constants';
import storage from 'utils/storage';
import Dropdown from 'components/Dropdown/Dropdown';
import Icon from 'components/Icon/Icon';
import DropdownContent from 'components/Dropdown/DropdownContent';
import DropdownItem from 'components/Dropdown/DropdownItem';

/**
* Properties for the `LanguageToggle` component.
* @see {@link PropsWithClassName}
*/
interface LanguageToggleProps extends PropsWithClassName {}

/**
* The `LanguageToggle` component renders a `Dropdown` which allows users
* to select the language in which they wish to view the application.
* @param {LanguageToggleProps} props - Component properties.
* @returns {JSX.Element} JSX
*/
const LanguageToggle = ({ className }: LanguageToggleProps): JSX.Element => {
const { i18n } = useTranslation();

/**
* Set the application-wide langague code used for i18n.
* @param {string} lng - A langage code, e.g. `en` or `es`.
*/
const setLanguage = (lng: string) => {
storage.setItem(StorageKeys.Language, lng);
i18n.changeLanguage(lng);
};

return (
<Dropdown
toggle={<Icon name="language" className="px-2 py-1" />}
content={
<DropdownContent className="text-sm">
<DropdownItem onClick={() => setLanguage('en')} testId="dropdown-item-en">
English
</DropdownItem>
<DropdownItem onClick={() => setLanguage('fr')} testId="dropdown-item-fr">
French
</DropdownItem>
<DropdownItem onClick={() => setLanguage('es')} testId="dropdown-item-es">
Spanish
</DropdownItem>
</DropdownContent>
}
className={className}
testId="dropdown-language"
/>
);
};

export default LanguageToggle;
79 changes: 79 additions & 0 deletions src/components/Button/__tests__/LanguageToggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';

import { StorageKeys } from 'utils/constants';
import { render, screen } from 'test/test-utils';
import storage from 'utils/storage';

import LanguageToggle from '../LanguageToggle';

// mock select functions from react-i18next
const mockChangeLanguage = vi.fn();
vi.mock('react-i18next', async () => {
const original = await vi.importActual('react-i18next');
return {
...original,
useTranslation: () => ({ i18n: { changeLanguage: mockChangeLanguage } }),
};
});

describe('LanguageToggle', () => {
const setItemSpy = vi.spyOn(storage, 'setItem');

it('should render successfully', async () => {
// ARRANGE
render(<LanguageToggle />);
await screen.findByTestId('dropdown-language');

// ASSERT
expect(screen.getByTestId('dropdown-language')).toBeDefined();
});

it('should use custom className', async () => {
// ARRANGE
render(<LanguageToggle className="custom-className" />);
await screen.findByTestId('dropdown-language');

// ASSERT
expect(screen.getByTestId('dropdown-language').classList).toContain('custom-className');
});

it('should set language to English', async () => {
// ARRANGE
render(<LanguageToggle />);
await screen.findByTestId('dropdown-language');

// ACT
await userEvent.click(screen.getByTestId('dropdown-item-en'));

// ASSERT
expect(mockChangeLanguage).toHaveBeenCalledWith('en');
expect(setItemSpy).toHaveBeenCalledWith(StorageKeys.Language, 'en');
});

it('should set language to French', async () => {
// ARRANGE
render(<LanguageToggle />);
await screen.findByTestId('dropdown-language');

// ACT
await userEvent.click(screen.getByTestId('dropdown-item-fr'));

// ASSERT
expect(mockChangeLanguage).toHaveBeenCalledWith('fr');
expect(setItemSpy).toHaveBeenCalledWith(StorageKeys.Language, 'fr');
});

it('should set language to Spanish', async () => {
// ARRANGE
render(<LanguageToggle />);
await screen.findByTestId('dropdown-language');

// ACT
await userEvent.click(screen.getByTestId('dropdown-item-es'));

// ASSERT
expect(mockChangeLanguage).toHaveBeenCalledWith('es');
expect(setItemSpy).toHaveBeenCalledWith(StorageKeys.Language, 'es');
});
});
88 changes: 88 additions & 0 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ReactNode, useState } from 'react';
import { PropsWithClassName, PropsWithTestId } from '@leanstacks/react-common';
import classNames from 'classnames';

/**
* Properties for the `Dropdown` component.
* @param {ReactNode} toggle - The content which toggles display of the dropdown content.
* @param {ReactNode} content - The dropdown content.
* @see {@link PropsWithClassName}
* @see {@link PropsWithTestId}
*/
interface DropdownProps extends PropsWithClassName, PropsWithTestId {
toggle: ReactNode;
content: ReactNode;
}

/**
* The `Dropdown` component controls the display of content which "drops down"
* from the trigger.
*
* Any `ReactNode` may be used for the `toggle` and the `content`; however,
* you may use the `DropdownContent` and `DropdownItem` components to simplify
* creation of a dropdown component content.
*
* *Example:*
* ```jsx
<Dropdown
toggle={<Icon name="language" className="px-2 py-1" />}
content={
<DropdownContent className="text-sm">
<DropdownItem onClick={() => setLanguage('en')} testId="dropdown-item-en">
English
</DropdownItem>
<DropdownItem onClick={() => setLanguage('fr')} testId="dropdown-item-fr">
French
</DropdownItem>
<DropdownItem onClick={() => setLanguage('es')} testId="dropdown-item-es">
Spanish
</DropdownItem>
</DropdownContent>
}
className={className}
testId="dropdown-language"
/>
* ```
* @param {DropdownProps} props - Component properties.
* @returns {JSX.Element} JSX
*/
const Dropdown = ({
className,
content,
testId = 'dropdown',
toggle,
}: DropdownProps): JSX.Element => {
const [hidden, setHidden] = useState<boolean>(true);

return (
<div className={className} data-testid={testId}>
<div
className={classNames('absolute left-0 top-0 z-[1000] h-screen w-screen', {
hidden: hidden,
})}
onClick={() => setHidden(true)}
data-testid={`${testId}-backdrop`}
></div>
<div className="relative">
<div
className="cursor-pointer"
onClick={() => setHidden(!hidden)}
data-testid={`${testId}-toggle`}
>
{toggle}
</div>
<div
className={classNames('absolute right-0 z-[1001]', {
hidden: hidden,
})}
onClick={() => setHidden(true)}
data-testid={`${testId}-content`}
>
{content}
</div>
</div>
</div>
);
};

export default Dropdown;
Loading

0 comments on commit 6abb0c4

Please sign in to comment.