Skip to content

Commit 047f5c6

Browse files
authored
feat: add fallback locale (#14)
* feat: add fallback locale * chore(docs): add example about fallback locale * test: add tests for fallback locales
1 parent 908ea5f commit 047f5c6

10 files changed

Lines changed: 104 additions & 4 deletions

File tree

examples/next/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export default {
88
'scope.more.test': 'A scope',
99
'scope.more.param': 'A scope with {param}',
1010
'scope.more.and.more.test': 'A scope',
11+
'missing.translation.in.fr': 'This should work',
1112
} as const;

examples/next/locales/fr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export default defineLocale({
1010
'scope.more.test': 'Un scope',
1111
'scope.more.param': 'Un scope avec un {param}',
1212
'scope.more.and.more.test': 'Un scope',
13+
'missing.translation.in.fr': '', // Comment to test locale fallback
1314
});

examples/next/pages/_app.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
22
import { AppProps } from 'next/app';
33
import { I18nProvider } from '../locales';
4+
import en from '../locales/en';
45

56
const App = ({ Component, pageProps }: AppProps) => {
67
return (
7-
<I18nProvider locale={pageProps.locale} fallback={<p>Loading initial locale client-side</p>}>
8+
<I18nProvider locale={pageProps.locale} fallback={<p>Loading initial locale client-side</p>} fallbackLocale={en}>
89
<Component {...pageProps} />
910
</I18nProvider>
1011
);

examples/next/pages/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const Home = () => {
3030
})}
3131
</p>
3232
<p>{t2('and.more.test')}</p>
33+
<p>{t('missing.translation.in.fr')}</p>
3334
<button type="button" onClick={() => changeLocale('en')}>
3435
EN
3536
</button>

packages/next-international/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Examples](#examples)
1616
- [Scoped translations](#scoped-translations)
1717
- [Change current locale](#change-current-locale)
18+
- [Fallback locale for missing translations](#fallback-locale-for-missing-translations)
1819
- [Use JSON files instead of TS for locales](#use-json-files-instead-of-ts-for-locales)
1920
- [Explicitly typing the locales](#explicitly-typing-the-locales)
2021
- [Load initial locales client-side](#load-initial-locales-client-side)
@@ -163,6 +164,22 @@ function App() {
163164
}
164165
```
165166

167+
### Fallback locale for missing translations
168+
169+
It's common to have missing translations in an application. By default, next-international outputs the key when no translation is found for the current locale, to avoid sending to users uncessary data.
170+
171+
You can provide a fallback locale that will be used for all missing translations:
172+
173+
```tsx
174+
// pages/_app.tsx
175+
import { I18nProvider } from '../locales'
176+
import en from '../locales/en'
177+
178+
<I18nProvider locale={pageProps.locale} fallbackLocale={en}>
179+
...
180+
</I18nProvider>
181+
```
182+
166183
### Use JSON files instead of TS for locales
167184

168185
Currently, this breaks the parameters type-safety, so we recommend using the TS syntax. See this issue: https://github.com/microsoft/TypeScript/issues/32063.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { createI18n } from '../src';
4+
import { render, screen } from './utils';
5+
import en from './utils/en';
6+
import fr from './utils/fr';
7+
8+
beforeEach(() => {
9+
vi.mock('next/router', () => ({
10+
useRouter: vi.fn().mockImplementation(() => ({
11+
locale: 'fr',
12+
defaultLocale: 'fr',
13+
locales: ['en', 'fr'],
14+
})),
15+
}));
16+
});
17+
18+
afterEach(() => {
19+
vi.clearAllMocks();
20+
});
21+
22+
describe('fallbackLocale', () => {
23+
it('should output the key when no fallback locale is configured', async () => {
24+
const { useI18n, I18nProvider } = createI18n<typeof import('./utils/en').default>({
25+
en: () => import('./utils/en'),
26+
fr: () => import('./utils/fr'),
27+
});
28+
29+
function App() {
30+
const { t } = useI18n();
31+
32+
return <p>{t('only.exists.in.en')}</p>;
33+
}
34+
35+
render(
36+
// @ts-expect-error missing key
37+
<I18nProvider locale={fr}>
38+
<App />
39+
</I18nProvider>,
40+
);
41+
42+
expect(screen.getByText('only.exists.in.en')).toBeInTheDocument();
43+
});
44+
45+
it('should output the key when no fallback locale is configured', async () => {
46+
const { useI18n, I18nProvider } = createI18n<typeof import('./utils/en').default>({
47+
en: () => import('./utils/en'),
48+
fr: () => import('./utils/fr'),
49+
});
50+
51+
function App() {
52+
const { t } = useI18n();
53+
54+
return <p>{t('only.exists.in.en')}</p>;
55+
}
56+
57+
render(
58+
// @ts-expect-error missing key
59+
<I18nProvider locale={fr} fallbackLocale={en}>
60+
<App />
61+
</I18nProvider>,
62+
);
63+
64+
expect(screen.getByText('EN locale')).toBeInTheDocument();
65+
});
66+
});

packages/next-international/__tests__/utils/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export default {
88
'namespace.subnamespace.hello.world': 'Hello World!',
99
'namespace.subnamespace.weather': "Today's weather is {weather}",
1010
'namespace.subnamespace.user.description': '{name} is {years} years old',
11+
'only.exists.in.en': 'EN locale',
1112
} as const;

packages/next-international/src/i18n/create-i18n-provider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@ import { error, warn } from '../helpers/log';
66
type I18nProviderProps<Locale extends BaseLocale> = {
77
locale: Locale;
88
fallback?: ReactElement | null;
9+
fallbackLocale?: Locale;
910
children: ReactNode;
1011
};
1112

1213
export function createI18nProvider<Locale extends BaseLocale>(
1314
I18nContext: Context<LocaleContext<Locale> | null>,
1415
locales: Locales,
1516
) {
16-
return function I18nProvider({ locale: baseLocale, fallback = null, children }: I18nProviderProps<Locale>) {
17+
return function I18nProvider({
18+
locale: baseLocale,
19+
fallback = null,
20+
fallbackLocale,
21+
children,
22+
}: I18nProviderProps<Locale>) {
1723
const { locale, defaultLocale, locales: nextLocales } = useRouter();
1824
const [clientLocale, setClientLocale] = useState<Locale>();
1925

@@ -63,6 +69,7 @@ export function createI18nProvider<Locale extends BaseLocale>(
6369
<I18nContext.Provider
6470
value={{
6571
localeContent: clientLocale || baseLocale,
72+
fallbackLocale,
6673
}}
6774
>
6875
{children}

packages/next-international/src/i18n/create-use-i18n.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ export function createUsei18n<Locale extends BaseLocale>(I18nContext: Context<Lo
2323
Key extends LocaleKeys<Locale, Scope>,
2424
Value extends LocaleValue = ScopedValue<Locale, Scope, Key>,
2525
>(key: Key, ...params: Params<Value>['length'] extends 0 ? [] : [ParamsObject<Value>]) {
26-
const { localeContent } = context as LocaleContext<Locale>;
26+
const { localeContent, fallbackLocale } = context as LocaleContext<Locale>;
2727

28-
let value = ((scope ? localeContent[`${scope}.${key}`] : localeContent[key]) || key).toString();
28+
let value = (
29+
(scope ? localeContent[`${scope}.${key}`] : localeContent[key]) ||
30+
(scope ? fallbackLocale?.[`${scope}.${key}`] : fallbackLocale?.[key]) ||
31+
key
32+
).toString();
2933
const paramObject = params[0];
3034

3135
if (!paramObject) {

packages/next-international/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type Locales = Record<string, () => Promise<any>>;
1111

1212
export type LocaleContext<Locale extends BaseLocale> = {
1313
localeContent: Locale;
14+
fallbackLocale?: Locale;
1415
};
1516

1617
export type Params<Value extends LocaleValue> = Value extends ''

0 commit comments

Comments
 (0)