Skip to content

Commit 9c8febb

Browse files
committed
fix: ensure i18next initialization in client components to prevent translation keys from showing in Vercel builds
1 parent 4938be5 commit 9c8febb

File tree

7 files changed

+150
-14
lines changed

7 files changed

+150
-14
lines changed

src/components/Footer/PureFooter/PureFooter.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { useState, useEffect } from "preact/hooks"
21
import { aboutList, mediaList, resourceList } from "../helper.tsx"
32
import styles from "./PureFooter.module.css"
43
import { t } from "i18next"
4+
import { useI18n } from "~/hooks/useI18n"
55

66
const Footer = () => {
7+
// Use the hook to ensure i18next is initialized
8+
useI18n()
79
return (
810
<div className={styles.footerLayout}>
911
<a className={styles.logo} href="/">

src/components/Footer/Subscribe/EmailInput.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import styles from "./EmailInput.module.css"
33
import ArrowSvg from "~/assets/svgs/footer/arrow-right.svg?react"
44
import { clsx } from "~/lib"
55
import { t } from "i18next"
6+
import { useI18n } from "~/hooks/useI18nReact"
67

78
const EmailInput = (props) => {
9+
// Use the hook to ensure i18next is initialized
10+
useI18n()
811
const { end, onClick, onEnter, ...restProps } = props
912

1013
const handleEnter = (e) => {

src/components/Footer/Subscribe/Subscribe.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { useState, useEffect } from "preact/hooks"
33
import MailchimpSubscribe from "react-mailchimp-subscribe"
44
import SubscribeSvg from "~/assets/svgs/footer/subscribe.svg?react"
55
import { clsx } from "~/lib"
6-
import i18next, { changeLanguage, t } from "i18next"
7-
8-
6+
import { t } from "i18next"
7+
import { useI18n } from "~/hooks/useI18n"
98
import EmailInput from "./EmailInput.tsx"
109
import styles from "./Subscribe.module.css"
1110

@@ -20,8 +19,8 @@ export default function Subscribe(props) {
2019
const [email, setEmail] = useState("")
2120
const [customMessage, setCustomMessage] = useState("")
2221
const [emailValid, setEmailValid] = useState(false)
23-
24-
i18next.changeLanguage(props.lang)
22+
// Use the hook to ensure i18next is initialized and handle language changes
23+
useI18n(props.lang)
2524

2625
useEffect(() => {
2726
setCustomMessage("")
@@ -51,22 +50,28 @@ export default function Subscribe(props) {
5150
</span>
5251

5352
<div className={styles.copyBox}>
54-
<div className={styles.subscribeTitle}>{ t("landing.NewsletterCTA.title") }</div>
55-
<div className={styles.subscribeText}>
56-
{ t("landing.NewsletterCTA.text") }
57-
</div>
53+
<div className={styles.subscribeTitle}>{t("landing.NewsletterCTA.title")}</div>
54+
<div className={styles.subscribeText}>{t("landing.NewsletterCTA.text")}</div>
5855
</div>
5956
<MailchimpSubscribe
6057
url={url}
61-
render={({ subscribe, status, message }: any) => (
58+
render={({
59+
subscribe,
60+
status,
61+
message,
62+
}: {
63+
subscribe: (data: { EMAIL: string }) => void
64+
status: string
65+
message: string
66+
}) => (
6267
<div className={styles.emailBox}>
6368
<EmailInput
6469
className={styles.emailInput}
6570
value={email}
6671
onChange={handleChangeEmail}
6772
onClick={() => handleSubmit(subscribe)}
6873
onEnter={() => handleSubmit(subscribe)}
69-
placeholder= { t("landing.NewsletterCTA.placeholder") }
74+
placeholder={t("landing.NewsletterCTA.placeholder")}
7075
end={status === "success"}
7176
/>
7277
{customMessage && <div className={styles.errorMessage}>{customMessage}</div>}

src/components/RightSidebar/TableOfContents/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useStore } from "@nanostores/preact"
33
import type { FunctionalComponent } from "preact"
44
import { useState, useEffect, useRef } from "preact/hooks"
55
import { shouldUpdateToc } from "./tocStore"
6-
import i18next, { t } from "i18next"
6+
import { t } from "i18next"
7+
import { useI18n } from "~/hooks/useI18n"
78

89
export interface Heading {
910
depth: number
@@ -15,6 +16,8 @@ const TableOfContents: FunctionalComponent<{
1516
headers: Heading[]
1617
clientSideToc: boolean
1718
}> = ({ headers = [], clientSideToc = false }) => {
19+
// Use the hook to ensure i18next is initialized
20+
useI18n()
1821
// headers = [...headers].filter(({ depth }) => depth > 1 && depth < 4)
1922
const [headings, setHeadings] = useState([...headers].filter(({ depth }) => depth > 1 && depth < 4))
2023
const tableOfContents = useRef<HTMLUListElement>()

src/features/landing/components/SearchCTA.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import React, { useState, useCallback } from "react"
2-
import i18next, { t } from "i18next"
2+
import { t } from "i18next"
3+
import { useI18n } from "~/hooks/useI18nReact"
34
import "./Search.css"
45

56
import { SearchModal } from "~/components/Header/Search/SearchModal"
67
export function SearchCTA() {
8+
// Use the hook to ensure i18next is initialized
9+
useI18n()
710
const [isOpen, setIsOpen] = useState(false)
811

912
const onOpen = useCallback(() => {

src/hooks/useI18n.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useState, useEffect } from "preact/hooks"
2+
import i18next, { changeLanguage } from "i18next"
3+
4+
/**
5+
* Custom hook to ensure i18next is initialized and handle language changes
6+
* This fixes the issue where Vercel build shows translation keys instead of translated text
7+
* due to SSR/CSR timing differences
8+
*
9+
* @param lang - The language code to use (optional, defaults to current i18next language)
10+
* @returns The current language code
11+
*/
12+
export function useI18n(lang?: string): string {
13+
const [currentLang, setCurrentLang] = useState(i18next.language || "en")
14+
15+
useEffect(() => {
16+
const updateLanguage = async () => {
17+
if (lang && i18next.language !== lang) {
18+
try {
19+
await changeLanguage(lang)
20+
setCurrentLang(lang)
21+
} catch (error) {
22+
// Silently handle any errors during language change
23+
setCurrentLang(i18next.language)
24+
}
25+
} else {
26+
setCurrentLang(i18next.language)
27+
}
28+
}
29+
30+
// Wait for i18next to be initialized before changing language
31+
if (i18next.isInitialized) {
32+
updateLanguage()
33+
} else {
34+
// Listen for initialization event
35+
const handleInitialized = () => {
36+
updateLanguage()
37+
i18next.off("initialized", handleInitialized)
38+
}
39+
i18next.on("initialized", handleInitialized)
40+
41+
return () => {
42+
i18next.off("initialized", handleInitialized)
43+
}
44+
}
45+
}, [lang])
46+
47+
// Listen for language changes to trigger re-render
48+
useEffect(() => {
49+
const handleLanguageChanged = (lng: string) => {
50+
setCurrentLang(lng)
51+
}
52+
i18next.on("languageChanged", handleLanguageChanged)
53+
54+
return () => {
55+
i18next.off("languageChanged", handleLanguageChanged)
56+
}
57+
}, [])
58+
59+
return currentLang
60+
}

src/hooks/useI18nReact.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useState, useEffect } from "react"
2+
import i18next, { changeLanguage } from "i18next"
3+
4+
/**
5+
* Custom hook for React components to ensure i18next is initialized and handle language changes
6+
* This fixes the issue where Vercel build shows translation keys instead of translated text
7+
* due to SSR/CSR timing differences
8+
*
9+
* @param lang - The language code to use (optional, defaults to current i18next language)
10+
* @returns The current language code
11+
*/
12+
export function useI18n(lang?: string): string {
13+
const [currentLang, setCurrentLang] = useState(i18next.language || "en")
14+
15+
useEffect(() => {
16+
const updateLanguage = async () => {
17+
if (lang && i18next.language !== lang) {
18+
try {
19+
await changeLanguage(lang)
20+
setCurrentLang(lang)
21+
} catch (error) {
22+
// Silently handle any errors during language change
23+
setCurrentLang(i18next.language)
24+
}
25+
} else {
26+
setCurrentLang(i18next.language)
27+
}
28+
}
29+
30+
// Wait for i18next to be initialized before changing language
31+
if (i18next.isInitialized) {
32+
updateLanguage()
33+
} else {
34+
// Listen for initialization event
35+
const handleInitialized = () => {
36+
updateLanguage()
37+
i18next.off("initialized", handleInitialized)
38+
}
39+
i18next.on("initialized", handleInitialized)
40+
41+
return () => {
42+
i18next.off("initialized", handleInitialized)
43+
}
44+
}
45+
}, [lang])
46+
47+
// Listen for language changes to trigger re-render
48+
useEffect(() => {
49+
const handleLanguageChanged = (lng: string) => {
50+
setCurrentLang(lng)
51+
}
52+
i18next.on("languageChanged", handleLanguageChanged)
53+
54+
return () => {
55+
i18next.off("languageChanged", handleLanguageChanged)
56+
}
57+
}, [])
58+
59+
return currentLang
60+
}

0 commit comments

Comments
 (0)