-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
46 Internationalization Example (#53)
* #46 initial i18n * #46 tests * #46 tests * #46 namespaces * #46 translations modules * #46 welcome translation * #46 translations
- Loading branch information
Showing
30 changed files
with
714 additions
and
24 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.