Skip to content

Commit e898c1e

Browse files
authored
feat(compiler): fallback to source locale (#1119)
For cases: - no locale - invalid locale - unsupported locale Remove "en" as default locale. Use source locale as default.
1 parent 05d5fc3 commit e898c1e

16 files changed

Lines changed: 140 additions & 60 deletions

File tree

.changeset/big-panthers-push.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@lingo.dev/_react": patch
3+
"lingo.dev": patch
4+
---
5+
6+
compiler fallback to source locale

demo/react-router-app/app/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export async function loader(args: LoaderFunctionArgs) {
3636
export function Layout(props: { children: React.ReactNode }) {
3737
const loaderData = useLoaderData<typeof loader>();
3838
return (
39-
<LingoProvider dictionary={loaderData.lingoDictionary}>
39+
<LingoProvider dictionary={loaderData?.lingoDictionary}>
4040
<html lang="en">
4141
<head>
4242
<meta charSet="utf-8" />

packages/react/src/client/loader.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getDictionary } from "../core";
2+
13
/**
24
* A placeholder function for loading dictionaries that contain localized content.
35
*
@@ -27,18 +29,13 @@
2729
* );
2830
* ```
2931
*/
30-
export const loadDictionary = async (locale: string): Promise<any> => {
32+
export const loadDictionary = async (locale: string | null): Promise<any> => {
3133
return {};
3234
};
3335

3436
export const loadDictionary_internal = async (
35-
locale: string,
36-
loaders: Record<string, () => Promise<any>> = {},
37+
locale: string | null,
38+
dictionaryLoaders: Record<string, () => Promise<any>> = {},
3739
): Promise<any> => {
38-
const loader = loaders[locale];
39-
if (!loader) {
40-
throw new Error(`No loader found for locale: ${locale}`);
41-
}
42-
43-
return loader().then((m) => m.default);
40+
return getDictionary(locale, dictionaryLoaders);
4441
};

packages/react/src/client/locale-switcher.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ export type LocaleSwitcherProps = {
4545
*/
4646
export function LocaleSwitcher(props: LocaleSwitcherProps) {
4747
const { locales } = props;
48-
const [locale, setLocale] = useState<string>("");
48+
const [locale, setLocale] = useState<string | undefined>(undefined);
4949

5050
useEffect(() => {
5151
const currentLocale = getLocaleFromCookies();
52-
setLocale(currentLocale);
52+
const isValidLocale = currentLocale && locales.includes(currentLocale);
53+
setLocale(isValidLocale ? currentLocale : locales[0]);
5354
}, [locales]);
5455

55-
if (!locale) {
56+
if (locale === undefined) {
5657
return null;
5758
}
5859

packages/react/src/client/provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export type LingoProviderWrapperProps<D> = {
102102
*
103103
* @returns The dictionary object containing localized content.
104104
*/
105-
loadDictionary: (locale: string) => Promise<D>;
105+
loadDictionary: (locale: string | null) => Promise<D>;
106106
/**
107107
* The child components containing localizable content.
108108
*/

packages/react/src/client/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { LOCALE_COOKIE_NAME, DEFAULT_LOCALE } from "../core";
3+
import { LOCALE_COOKIE_NAME } from "../core";
44
import Cookies from "js-cookie";
55

66
/**
@@ -23,10 +23,10 @@ import Cookies from "js-cookie";
2323
* }
2424
* ```
2525
*/
26-
export function getLocaleFromCookies(): string {
27-
if (typeof document === "undefined") return DEFAULT_LOCALE;
26+
export function getLocaleFromCookies(): string | null {
27+
if (typeof document === "undefined") return null;
2828

29-
return Cookies.get(LOCALE_COOKIE_NAME) || DEFAULT_LOCALE;
29+
return Cookies.get(LOCALE_COOKIE_NAME) ?? null;
3030
}
3131

3232
/**

packages/react/src/core/const.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
/**
2-
* The default locale.
3-
*/
4-
export const DEFAULT_LOCALE = "en";
5-
61
/**
72
* The name of the cookie that stores the current locale.
83
*/
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { getDictionary } from "./get-dictionary";
3+
4+
describe("get-dictionary", () => {
5+
const mockLoaderEn = vi.fn().mockResolvedValue(
6+
Promise.resolve({
7+
default: { hello: "Hello", goodbye: "Goodbye" },
8+
otherExport: "ignored",
9+
}),
10+
);
11+
const mockLoaderEs = vi.fn().mockResolvedValue(
12+
Promise.resolve({
13+
default: { hello: "Hola", goodbye: "Adiós" },
14+
}),
15+
);
16+
const loaders = {
17+
en: mockLoaderEn,
18+
es: mockLoaderEs,
19+
};
20+
21+
beforeEach(() => {
22+
vi.clearAllMocks();
23+
});
24+
25+
describe("getDictionary", () => {
26+
it("should load dictionary for specific locale using correct async loader", async () => {
27+
const result = await getDictionary("es", loaders);
28+
expect(mockLoaderEs).toHaveBeenCalledTimes(1);
29+
expect(result).toEqual({ hello: "Hola", goodbye: "Adiós" });
30+
});
31+
32+
it("should fallback to first available loader when specific locale not found", async () => {
33+
const result = await getDictionary("fr", loaders);
34+
35+
expect(mockLoaderEn).toHaveBeenCalledTimes(1);
36+
expect(result).toEqual({ hello: "Hello", goodbye: "Goodbye" });
37+
});
38+
39+
it("should throw error when no loaders are provided", async () => {
40+
expect(() => getDictionary("en", {})).toThrow(
41+
"No available dictionary loaders found",
42+
);
43+
expect(() => getDictionary("en")).toThrow(
44+
"No available dictionary loaders found",
45+
);
46+
});
47+
});
48+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Loads a dictionary for the specified locale.
3+
*
4+
* This function attempts to load a dictionary using the provided loaders. If the specified
5+
* locale is not available, it falls back to the first available loader. The function
6+
* expects the loader to return a promise that resolves to an object with a `default` property
7+
* containing the dictionary data (the default export from dictionary file).
8+
*
9+
* @param locale - The locale to load the dictionary for. Can be null to use the first available loader.
10+
* @param loaders - A record of locale keys to loader functions. Each loader should return a Promise
11+
* that resolves to an object with a `default` property containing the dictionary.
12+
* @returns A Promise that resolves to the dictionary data (the `default` export from the loader).
13+
* @throws {Error} When no loaders are provided or available.
14+
*
15+
* @example
16+
* ```typescript
17+
* const loaders = {
18+
* 'en': () => import('./en.json'),
19+
* 'es': () => import('./es.json')
20+
* };
21+
*
22+
* const dictionary = await loadDictionary('en', loaders);
23+
* // Returns the default export from the English dictionary
24+
* ```
25+
*/
26+
export function getDictionary(
27+
locale: string | null,
28+
loaders: Record<string, () => Promise<any>> = {},
29+
) {
30+
const loader = getDictionaryLoader(locale, loaders);
31+
if (!loader) {
32+
throw new Error("No available dictionary loaders found");
33+
}
34+
return loader().then((value) => value.default);
35+
}
36+
37+
function getDictionaryLoader(
38+
locale: string | null,
39+
loaders: Record<string, () => Promise<any>> = {},
40+
) {
41+
if (locale && loaders[locale]) {
42+
return loaders[locale];
43+
}
44+
return Object.values(loaders)[0];
45+
}

packages/react/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./component";
22
export * from "./const";
33
export * from "./attribute-component";
4+
export * from "./get-dictionary";

0 commit comments

Comments
 (0)