Skip to content

Commit 65f9684

Browse files
committed
i18n: Use own logic for translation
1 parent 8751c08 commit 65f9684

File tree

12 files changed

+228
-197
lines changed

12 files changed

+228
-197
lines changed

i18n/index.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

package-lock.json

Lines changed: 0 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
"feed": "^4.2.2",
3939
"gray-matter": "^4.0.3",
4040
"jsdom": "^25.0.1",
41-
"next-export-i18n": "^3.0.0",
4241
"octokit": "^4.0.2",
4342
"postcss": "^8.4.49",
4443
"postcss-preset-mantine": "^1.17.0",

src/app/layout.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { MantineProvider, ColorSchemeScript } from "@mantine/core";
55
import { cssResolver, theme } from "@/theme";
66
import { Header } from "@/components/header";
77
import { FooterSocial } from "@/components/footer";
8-
import { Suspense } from "react";
98

109
export const metadata: Metadata = {
1110
title: "Ruffle - Flash Emulator",
@@ -42,11 +41,9 @@ export default function RootLayout({
4241
</head>
4342
<body>
4443
<MantineProvider theme={theme} cssVariablesResolver={cssResolver}>
45-
<Suspense>
46-
<Header />
47-
{children}
48-
<FooterSocial />
49-
</Suspense>
44+
<Header />
45+
{children}
46+
<FooterSocial />
5047
</MantineProvider>
5148
</body>
5249
</html>

src/app/page.tsx

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

33
import dynamic from "next/dynamic";
4-
import { useTranslation } from "next-export-i18n";
54
import classes from "./index.module.css";
65
import {
76
Container,
@@ -16,6 +15,7 @@ import Image from "next/image";
1615
import { IconCheck } from "@tabler/icons-react";
1716
import React from "react";
1817
import { getLatestReleases } from "@/app/downloads/github";
18+
import { useTranslation } from "@/app/translate";
1919
import { GithubRelease } from "./downloads/config";
2020

2121
const InteractiveLogo = dynamic(() => import("../components/logo"), {
@@ -47,9 +47,7 @@ export default function Home() {
4747
<InteractiveLogo className={classes.logo} />
4848

4949
<Container size="md">
50-
<Title className={classes.title} suppressHydrationWarning>
51-
{t("home.title")}
52-
</Title>
50+
<Title className={classes.title}>{t("home.title")}</Title>
5351
<div className={classes.hero}>
5452
<Image
5553
className={classes.heroImage}
@@ -60,9 +58,7 @@ export default function Home() {
6058
priority
6159
/>
6260
<div className={classes.heroInner}>
63-
<Text mt="md" suppressHydrationWarning>
64-
{t("home.intro")}
65-
</Text>
61+
<Text mt="md">{t("home.intro")}</Text>
6662

6763
<List
6864
mt={30}
@@ -79,31 +75,16 @@ export default function Home() {
7975
}
8076
>
8177
<ListItem>
82-
<b className={classes.key} suppressHydrationWarning>
83-
{t("home.safe")}
84-
</b>{" "}
85-
-{" "}
86-
<span suppressHydrationWarning>
87-
{t("home.safe-description")}
88-
</span>
78+
<b className={classes.key}>{t("home.safe")}</b> -{" "}
79+
<span>{t("home.safe-description")}</span>
8980
</ListItem>
9081
<ListItem>
91-
<b className={classes.key} suppressHydrationWarning>
92-
{t("home.easy")}
93-
</b>{" "}
94-
-{" "}
95-
<span suppressHydrationWarning>
96-
{t("home.easy-description")}
97-
</span>
82+
<b className={classes.key}>{t("home.easy")}</b> -{" "}
83+
<span>{t("home.easy-description")}</span>
9884
</ListItem>
9985
<ListItem>
100-
<b className={classes.key} suppressHydrationWarning>
101-
{t("home.free")}
102-
</b>{" "}
103-
-{" "}
104-
<span suppressHydrationWarning>
105-
{t("home.free-description")}
106-
</span>
86+
<b className={classes.key}>{t("home.free")}</b> -{" "}
87+
<span>{t("home.free-description")}</span>
10788
</ListItem>
10889
</List>
10990

src/app/translate.tsx

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"use client";
2+
3+
import React, { useEffect, useState, useCallback } from "react";
4+
import defaultTranslations from "@/i18n/translations.en.json";
5+
import classes from "@/components/header.module.css";
6+
7+
const languages = {
8+
en: "English",
9+
es: "Español",
10+
// ...
11+
};
12+
13+
type TranslationObject = {
14+
[key: string]: string | TranslationObject;
15+
};
16+
17+
interface LanguageSelectorProps {
18+
className?: string;
19+
}
20+
21+
async function getAvailableLanguage() {
22+
const defaultLanguage = "en";
23+
const storedLanguage = window.localStorage.getItem("next-export-i18n-lang");
24+
const browserLanguage =
25+
window.navigator.language ||
26+
(window.navigator.languages && window.navigator.languages[0]);
27+
const language = storedLanguage || browserLanguage || defaultLanguage;
28+
29+
// Helper function to check if a language file exists
30+
const checkLanguageFileExists = async (lang: string) => {
31+
try {
32+
await import(`@/i18n/translations.${lang}.json`);
33+
return lang;
34+
} catch {
35+
console.warn(`Translation file for language "${lang}" not found.`);
36+
return null;
37+
}
38+
};
39+
40+
// Check for full and base language, then fallback to default language
41+
const lang =
42+
(await checkLanguageFileExists(language)) ||
43+
(await checkLanguageFileExists(language.split("-")[0])) ||
44+
defaultLanguage;
45+
return lang;
46+
}
47+
48+
const getNestedTranslation = (
49+
obj: TranslationObject,
50+
key: string,
51+
): string | undefined => {
52+
let acc: TranslationObject | string | undefined = obj;
53+
for (let i = 0; i < key.split(".").length; i++) {
54+
const part = key.split(".")[i];
55+
if (acc && typeof acc !== "string" && acc[part] !== undefined) {
56+
acc = acc[part];
57+
} else {
58+
acc = undefined; // If a part is not found, stop and return undefined
59+
break;
60+
}
61+
}
62+
return typeof acc === "string" ? acc : undefined;
63+
};
64+
65+
async function fetchTranslations(lang: string) {
66+
try {
67+
const translations = await import(`@/i18n/translations.${lang}.json`);
68+
return translations;
69+
} catch {
70+
console.warn(`Translation file for language "${lang}" not found.`);
71+
return null;
72+
}
73+
}
74+
75+
export function useTranslation() {
76+
const [translations, setTranslations] =
77+
useState<TranslationObject>(defaultTranslations);
78+
79+
useEffect(() => {
80+
const fetchLanguageAndTranslations = async () => {
81+
const lang = await getAvailableLanguage();
82+
const loadedTranslations = await fetchTranslations(lang);
83+
setTranslations(loadedTranslations || defaultTranslations);
84+
};
85+
86+
fetchLanguageAndTranslations();
87+
88+
const handleLocalStorageLangChange = () => fetchLanguageAndTranslations();
89+
90+
window.addEventListener(
91+
"localStorageLangChange",
92+
handleLocalStorageLangChange,
93+
);
94+
return () =>
95+
window.removeEventListener(
96+
"localStorageLangChange",
97+
handleLocalStorageLangChange,
98+
);
99+
}, []);
100+
101+
const t = useCallback(
102+
(translationKey: string): string => {
103+
return (
104+
getNestedTranslation(translations, translationKey) ||
105+
getNestedTranslation(defaultTranslations, translationKey) ||
106+
translationKey
107+
);
108+
},
109+
[translations],
110+
);
111+
112+
return { t };
113+
}
114+
115+
export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
116+
className,
117+
}) => {
118+
const [selectedLang, setSelectedLang] = useState<string>("");
119+
120+
useEffect(() => {
121+
// Fetch and set the selected language
122+
const fetchLanguage = async () => {
123+
const lang = await getAvailableLanguage();
124+
setSelectedLang(lang);
125+
};
126+
127+
fetchLanguage(); // Set the language initially
128+
}, []);
129+
130+
const handleLanguageChange = (
131+
event: React.ChangeEvent<HTMLSelectElement>,
132+
) => {
133+
const newLang = event.target.value;
134+
setSelectedLang(newLang);
135+
window.localStorage.setItem("next-export-i18n-lang", newLang);
136+
137+
// Dispatch an event to notify other components or contexts of the language change
138+
const langChangeEvent = new Event("localStorageLangChange");
139+
window.dispatchEvent(langChangeEvent);
140+
};
141+
142+
return (
143+
<select
144+
className={`${classes.languageSelector} ${className || ""}`}
145+
value={selectedLang}
146+
onChange={handleLanguageChange}
147+
>
148+
{Object.entries(languages).map(([langCode, langName]) => (
149+
<option key={langCode} value={langCode}>
150+
{langName}
151+
</option>
152+
))}
153+
</select>
154+
);
155+
};

src/components/footer.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Container, Group, ActionIcon, rem, Text } from "@mantine/core";
44
import Link from "next/link";
5-
import { useTranslation } from "next-export-i18n";
5+
import { useTranslation } from "@/app/translate";
66

77
import {
88
IconBrandX,
@@ -52,7 +52,6 @@ export function FooterSocial() {
5252
const { t } = useTranslation();
5353
const socials = allSocials.map((social, i) => (
5454
<ActionIcon
55-
suppressHydrationWarning
5655
key={i}
5756
size="lg"
5857
color="gray"
@@ -77,7 +76,7 @@ export function FooterSocial() {
7776
width={91}
7877
priority
7978
/>
80-
<Text size="lg" className={classes.tagline} suppressHydrationWarning>
79+
<Text size="lg" className={classes.tagline}>
8180
{t("footer.tagline")}
8281
</Text>
8382
</Container>

src/components/header.module.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,10 @@
5555
.overlay {
5656
top: rem(56px);
5757
}
58+
59+
.languageSelector {
60+
color: var(--ruffle-orange);
61+
background: var(--ruffle-blue);
62+
border: 1px solid var(--ruffle-orange);
63+
padding: 4px;
64+
}

0 commit comments

Comments
 (0)