-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
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,21 @@ | ||
import { format as fnsFormat } from "date-fns"; | ||
import { useCurrentLocale } from "../utils/useCurrentLocale"; | ||
|
||
export function format( | ||
date: Date | number, | ||
formatStr = "PP", | ||
options: { | ||
locale?: Locale; | ||
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; | ||
firstWeekContainsDate?: number; | ||
useAdditionalWeekYearTokens?: boolean; | ||
useAdditionalDayOfYearTokens?: boolean; | ||
} = {} | ||
) { | ||
const locale = useCurrentLocale(); | ||
|
||
return fnsFormat(date, formatStr, { | ||
locale, | ||
...options, | ||
}); | ||
} |
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,19 @@ | ||
import { formatDistance as fnsFormatDistance } from "date-fns"; | ||
import { useCurrentLocale } from "../utils/useCurrentLocale"; | ||
|
||
export function formatDistance( | ||
date: Date | number, | ||
baseDate: Date | number, | ||
options: { | ||
includeSeconds?: boolean; | ||
addSuffix?: boolean; | ||
locale?: Locale; | ||
} = {} | ||
) { | ||
const locale = useCurrentLocale(); | ||
|
||
return fnsFormatDistance(date, baseDate, { | ||
locale, | ||
...options, | ||
}); | ||
} |
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,20 @@ | ||
import { formatDistanceStrict as fnsFormatDistanceStrict } from "date-fns"; | ||
import { useCurrentLocale } from "../utils/useCurrentLocale"; | ||
|
||
export function formatDistanceStrict( | ||
date: Date | number, | ||
baseDate: Date | number, | ||
options: { | ||
addSuffix?: boolean; | ||
unit?: "second" | "minute" | "hour" | "day" | "month" | "year"; | ||
roundingMethod?: "floor" | "ceil" | "round"; | ||
locale?: Locale; | ||
} = {} | ||
) { | ||
const locale = useCurrentLocale(); | ||
|
||
return fnsFormatDistanceStrict(date, baseDate, { | ||
locale, | ||
...options, | ||
}); | ||
} |
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,18 @@ | ||
import { formatRelative as fnsFormatRelative } from "date-fns"; | ||
import { useCurrentLocale } from "../utils/useCurrentLocale"; | ||
|
||
export function formatRelative( | ||
date: Date | number, | ||
baseDate: Date | number, | ||
options: { | ||
locale?: Locale; | ||
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; | ||
} = {} | ||
) { | ||
const locale = useCurrentLocale(); | ||
|
||
return fnsFormatRelative(date, baseDate, { | ||
locale, | ||
...options, | ||
}); | ||
} |
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,102 @@ | ||
import { ReactElement } from "react"; | ||
import i18n from "i18next"; | ||
import { cleanup, render, screen, waitFor } from "@testing-library/react"; | ||
import userEvent from "@testing-library/user-event"; | ||
import { initReactI18next } from "react-i18next"; | ||
|
||
import { I18nProvider, useCurrentLocale, useI18n } from "./i18n"; | ||
|
||
const EN = "en"; | ||
const PL = "pl"; | ||
const availableLanguages = { | ||
[EN]: "en-US", | ||
[PL]: "pl", | ||
}; | ||
const resources = { | ||
[EN]: { translation: { hello: "hello" } }, | ||
[PL]: { translation: { hello: "cześć" } }, | ||
}; | ||
|
||
const renderProvider = (ui: ReactElement) => render(ui, { wrapper: Provider }); | ||
|
||
describe("i18n()", () => { | ||
it("should check default language", async () => { | ||
// arrange | ||
renderProvider(<LanguageSwitcher />); | ||
|
||
// act - noop | ||
|
||
// assert | ||
await waitFor(() => { | ||
expect(screen.getByRole("locale")).toHaveTextContent( | ||
availableLanguages[EN] | ||
); | ||
}); | ||
}); | ||
|
||
it("should change language and translation", async () => { | ||
// arrange | ||
renderProvider(<LanguageSwitcher />); | ||
|
||
// act | ||
userEvent.click(screen.getByRole(PL)); | ||
|
||
// assert | ||
await waitFor(() => { | ||
expect(screen.getByRole("locale")).toHaveTextContent( | ||
availableLanguages[PL] | ||
); | ||
expect(screen.getByRole("translation")).toHaveTextContent( | ||
resources[PL].translation.hello | ||
); | ||
}); | ||
}); | ||
|
||
afterEach(cleanup); | ||
}); | ||
|
||
function Provider({ children }: { children: ReactElement }) { | ||
const i18nInstance = i18n.createInstance(); | ||
|
||
i18nInstance.use(initReactI18next).init({ | ||
fallbackLng: EN, | ||
debug: false, | ||
load: "languageOnly", | ||
supportedLngs: Object.keys(availableLanguages), | ||
resources, | ||
}); | ||
|
||
return ( | ||
<I18nProvider | ||
i18n={i18nInstance} | ||
availableLanguages={availableLanguages} | ||
> | ||
{children} | ||
</I18nProvider> | ||
); | ||
} | ||
|
||
function LanguageSwitcher() { | ||
const { i18n, supportedLanguages, t } = useI18n(); | ||
const locale = useCurrentLocale(); | ||
|
||
return ( | ||
<> | ||
{locale && ( | ||
<> | ||
<div role="locale">{locale.code}</div> | ||
<div role="translation">{t("hello")}</div> | ||
</> | ||
)} | ||
{supportedLanguages.map((locale) => ( | ||
<button | ||
role={locale} | ||
onClick={() => i18n.changeLanguage(locale)} | ||
key={locale} | ||
> | ||
{locale} | ||
</button> | ||
))} | ||
</> | ||
); | ||
} |
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,5 @@ | ||
import { useCurrentLocale } from "./utils/useCurrentLocale"; | ||
import { useI18n } from "./utils/useI18n"; | ||
import { I18nProvider } from "./i18nContext"; | ||
|
||
export { I18nProvider, useI18n, useCurrentLocale }; |
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,95 @@ | ||
import { | ||
createContext, | ||
ReactNode, | ||
useCallback, | ||
useEffect, | ||
useState, | ||
} from "react"; | ||
import { Locale } from "date-fns"; | ||
import { I18nextProvider } from "react-i18next"; | ||
import { i18n } from "i18next"; | ||
|
||
type I18nContextData = ReturnType<typeof useProviderI18n>; | ||
type I18nState = { | ||
locales: { [key: string]: Locale }; | ||
}; | ||
|
||
// i18n context | ||
export const I18nContext = createContext<I18nContextData | null>(null); | ||
|
||
// i18n provider | ||
export function I18nProvider({ | ||
i18n: i18nInstance, | ||
availableLanguages = {}, | ||
children, | ||
}: { | ||
i18n: i18n; | ||
availableLanguages: { [key: string]: string }; | ||
children: ReactNode; | ||
}) { | ||
const value = useProviderI18n(); | ||
const { setLocale, locales } = value; | ||
|
||
const loadLocale = useCallback(async (language: string) => { | ||
const localeName = availableLanguages[language]; | ||
|
||
if (!localeName) { | ||
throw new Error(`Locale "${localeName}" not supported`); | ||
} | ||
|
||
return (await import(`date-fns/locale/${localeName}`)).default; | ||
}, []); | ||
|
||
const onLoaded = useCallback(async () => { | ||
const loadedLocales: { [key: string]: Locale } = {}; | ||
|
||
await Promise.all( | ||
(i18nInstance.languages || []).map((language) => | ||
loadLocale(language).then( | ||
(locale) => (loadedLocales[language] = locale) | ||
) | ||
) | ||
); | ||
|
||
setLocale(loadedLocales); | ||
}, []); | ||
|
||
const onLanguageChanged = useCallback(async () => { | ||
if (!locales[i18nInstance.resolvedLanguage]) { | ||
const locale = await loadLocale(i18nInstance.resolvedLanguage); | ||
|
||
setLocale({ | ||
...locales, | ||
[i18nInstance.resolvedLanguage]: locale, | ||
}); | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
// only for loading "fallbackLng" | ||
onLoaded(); | ||
|
||
i18nInstance.on("loaded", onLoaded); | ||
i18nInstance.on("languageChanged", onLanguageChanged); | ||
|
||
return () => { | ||
i18nInstance.off("loaded", onLoaded); | ||
i18nInstance.off("languageChanged", onLanguageChanged); | ||
}; | ||
}, [i18nInstance]); | ||
|
||
return ( | ||
<I18nContext.Provider value={value}> | ||
<I18nextProvider i18n={i18nInstance}>{children}</I18nextProvider> | ||
</I18nContext.Provider> | ||
); | ||
} | ||
|
||
export function useProviderI18n() { | ||
const [locales, setLocale] = useState<I18nState["locales"]>({}); | ||
|
||
return { | ||
locales, | ||
setLocale, | ||
}; | ||
} |
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,22 @@ | ||
import { isArray } from "lodash"; | ||
|
||
import { useI18n } from "./useI18n"; | ||
|
||
export function useCurrentLocale() { | ||
const { | ||
i18n: { | ||
language, | ||
options: { fallbackLng }, | ||
}, | ||
locales, | ||
} = useI18n(); | ||
let fallback = ""; | ||
|
||
if (typeof fallbackLng === "string") { | ||
fallback = fallbackLng; | ||
} else if (isArray(fallbackLng) && typeof fallbackLng[0] === "string") { | ||
fallback = fallbackLng[0]; | ||
} | ||
|
||
return locales[language] || locales[fallback]; | ||
} |
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,16 @@ | ||
import { useContext } from "react"; | ||
import { useTranslation } from "react-i18next"; | ||
|
||
import { I18nContext } from "../i18nContext"; | ||
|
||
export function useI18n() { | ||
const i18nContext = useContext(I18nContext); | ||
const { i18n, t } = useTranslation(); | ||
const supportedLanguages = i18n.options.supportedLngs || []; | ||
|
||
if (!i18nContext) { | ||
throw new Error("`useI18n` must be used inside I18nProvider"); | ||
} | ||
|
||
return { ...i18nContext, supportedLanguages, i18n, t }; | ||
} |
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
Oops, something went wrong.