Skip to content

Commit 2d67369

Browse files
davidturnbulldevin-ai-integration[bot]maxprilutskiy
authored
fix: fix loadDictionary for React Router (#1054)
* fix: fix loadDictionary for React Router * fix: fix loadDictionary for React Router * fix: fix loadDictionary for React Router * fix: add changeset for loadLocaleFromCookies bug fix Co-Authored-By: Max Prilutskiy <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Max Prilutskiy <[email protected]>
1 parent 1ff847b commit 2d67369

File tree

3 files changed

+157
-13
lines changed

3 files changed

+157
-13
lines changed

.changeset/five-bugs-confess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lingo.dev/_react": patch
3+
---
4+
5+
Fix loadLocaleFromCookies to return default locale instead of null when no cookie is found
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { LOCALE_COOKIE_NAME } from "../core";
3+
import { loadDictionary_internal } from "./loader";
4+
5+
describe("loadDictionary_internal", () => {
6+
function createMockRequest(cookieHeader?: string): Request {
7+
const headers = new Headers();
8+
if (cookieHeader) {
9+
headers.set("Cookie", cookieHeader);
10+
}
11+
return new Request("http://localhost", { headers });
12+
}
13+
14+
const mockDictionaryLoader = {
15+
en: vi.fn().mockResolvedValue({ default: { hello: "Hello" } }),
16+
es: vi.fn().mockResolvedValue({ default: { hello: "Hola" } }),
17+
fr: vi.fn().mockResolvedValue({ default: { hello: "Bonjour" } }),
18+
};
19+
20+
it("should return null when no Cookie header is present", async () => {
21+
const request = createMockRequest();
22+
const result = await loadDictionary_internal(request, mockDictionaryLoader);
23+
24+
expect(mockDictionaryLoader.en).toHaveBeenCalled();
25+
expect(result).toEqual({ hello: "Hello" });
26+
});
27+
28+
it("should return null when Cookie header exists but no lingo-locale cookie", async () => {
29+
const request = createMockRequest("session=abc123; other-cookie=value");
30+
const result = await loadDictionary_internal(request, mockDictionaryLoader);
31+
32+
expect(mockDictionaryLoader.en).toHaveBeenCalled();
33+
expect(result).toEqual({ hello: "Hello" });
34+
});
35+
36+
it("should parse locale from lingo-locale cookie", async () => {
37+
const request = createMockRequest(`${LOCALE_COOKIE_NAME}=es`);
38+
const result = await loadDictionary_internal(request, mockDictionaryLoader);
39+
40+
expect(mockDictionaryLoader.es).toHaveBeenCalled();
41+
expect(result).toEqual({ hello: "Hola" });
42+
});
43+
44+
it("should handle lingo-locale cookie with other cookies", async () => {
45+
const request = createMockRequest(
46+
`session=abc; ${LOCALE_COOKIE_NAME}=fr; other=value`,
47+
);
48+
const result = await loadDictionary_internal(request, mockDictionaryLoader);
49+
50+
expect(mockDictionaryLoader.fr).toHaveBeenCalled();
51+
expect(result).toEqual({ hello: "Bonjour" });
52+
});
53+
54+
it("should handle lingo-locale cookie with spaces", async () => {
55+
const request = createMockRequest(
56+
`session=abc; ${LOCALE_COOKIE_NAME}=es ; other=value`,
57+
);
58+
const result = await loadDictionary_internal(request, mockDictionaryLoader);
59+
60+
expect(mockDictionaryLoader.es).toHaveBeenCalled();
61+
expect(result).toEqual({ hello: "Hola" });
62+
});
63+
64+
it("should use explicit locale string when provided", async () => {
65+
const result = await loadDictionary_internal("fr", mockDictionaryLoader);
66+
67+
expect(mockDictionaryLoader.fr).toHaveBeenCalled();
68+
expect(result).toEqual({ hello: "Bonjour" });
69+
});
70+
71+
it("should return null when locale is not available in dictionary", async () => {
72+
const request = createMockRequest(`${LOCALE_COOKIE_NAME}=de`);
73+
const result = await loadDictionary_internal(request, mockDictionaryLoader);
74+
75+
expect(result).toBeNull();
76+
});
77+
78+
it("should return null when explicit locale is not available", async () => {
79+
const result = await loadDictionary_internal("de", mockDictionaryLoader);
80+
81+
expect(result).toBeNull();
82+
});
83+
84+
it("should handle malformed cookie values gracefully", async () => {
85+
const request = createMockRequest(`${LOCALE_COOKIE_NAME}=`);
86+
const result = await loadDictionary_internal(request, mockDictionaryLoader);
87+
88+
expect(result).toBeNull();
89+
});
90+
91+
it("should handle cookie with equals sign in value", async () => {
92+
const request = createMockRequest(`${LOCALE_COOKIE_NAME}=en=US`);
93+
const mockLoader = {
94+
"en=US": vi.fn().mockResolvedValue({ default: { hello: "Hello US" } }),
95+
};
96+
const result = await loadDictionary_internal(request, mockLoader);
97+
98+
expect(mockLoader["en=US"]).toHaveBeenCalled();
99+
expect(result).toEqual({ hello: "Hello US" });
100+
});
101+
102+
it("should handle empty string locale", async () => {
103+
const result = await loadDictionary_internal("", mockDictionaryLoader);
104+
105+
expect(result).toBeNull();
106+
});
107+
108+
it("should extract default export from loader result", async () => {
109+
const customLoader = {
110+
custom: vi.fn().mockResolvedValue({
111+
default: { test: "value" },
112+
other: { ignored: "data" },
113+
}),
114+
};
115+
const result = await loadDictionary_internal("custom", customLoader);
116+
117+
expect(result).toEqual({ test: "value" });
118+
});
119+
});

packages/react/src/react-router/loader.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LOCALE_COOKIE_NAME } from "../core";
1+
import { DEFAULT_LOCALE, LOCALE_COOKIE_NAME } from "../core";
22

33
/**
44
* A placeholder function for loading dictionaries that contain localized content.
@@ -67,28 +67,48 @@ export const loadDictionary = async (
6767
return null;
6868
};
6969

70-
async function loadLocaleFromCookies(request: Request) {
71-
const cookieHeaderValue = request.headers.get("Cookie") || "";
72-
const cookieValue = cookieHeaderValue
70+
function loadLocaleFromCookies(request: Request) {
71+
// it's a Request, so get the Cookie header
72+
const cookieHeaderValue = request.headers.get("Cookie");
73+
74+
// there's no Cookie header, so return default
75+
if (!cookieHeaderValue) {
76+
return DEFAULT_LOCALE;
77+
}
78+
79+
// get the lingo-locale cookie
80+
const cookiePrefix = `${LOCALE_COOKIE_NAME}=`;
81+
const cookie = cookieHeaderValue
7382
.split(";")
74-
.find((cookie) => cookie.trim().startsWith(`${LOCALE_COOKIE_NAME}=`));
75-
const locale = cookieValue ? cookieValue.split("=")[1] : null;
76-
return locale;
83+
.find((cookie) => cookie.trim().startsWith(cookiePrefix));
84+
85+
// there's no lingo-locale cookie, so return default
86+
if (!cookie) {
87+
return DEFAULT_LOCALE;
88+
}
89+
90+
// extract the locale value from the cookie
91+
return cookie.trim().substring(cookiePrefix.length);
7792
}
7893

7994
export async function loadDictionary_internal(
8095
requestOrExplicitLocale: Request | string,
8196
dictionaryLoader: Record<string, () => Promise<any>>,
8297
) {
98+
// gets the locale (falls back to "en")
8399
const locale =
84100
typeof requestOrExplicitLocale === "string"
85101
? requestOrExplicitLocale
86-
: await loadLocaleFromCookies(requestOrExplicitLocale);
102+
: loadLocaleFromCookies(requestOrExplicitLocale);
103+
104+
// get dictionary loader for the locale
105+
const loader = dictionaryLoader[locale];
87106

88-
if (locale && dictionaryLoader[locale]) {
89-
return dictionaryLoader[locale]().then((value) => {
90-
return value.default;
91-
});
107+
// locale is not available in the dictionary
108+
if (!loader) {
109+
// TODO: throw a clear error message
110+
return null;
92111
}
93-
return null;
112+
113+
return loader().then((value) => value.default);
94114
}

0 commit comments

Comments
 (0)