Skip to content

Commit 1da5a09

Browse files
committed
Use own logic for translation
1 parent 18836cd commit 1da5a09

10 files changed

+196
-135
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 & 29 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 { t } from "@/app/translate";
1919
import { GithubRelease } from "./downloads/config";
2020

2121
const InteractiveLogo = dynamic(() => import("../components/logo"), {
@@ -27,7 +27,6 @@ const Installers = dynamic(() => import("./installers"), {
2727
});
2828

2929
export default function Home() {
30-
const { t } = useTranslation();
3130
const [latest, setLatest] = React.useState<GithubRelease | null>(null);
3231

3332
React.useEffect(() => {
@@ -47,9 +46,7 @@ export default function Home() {
4746
<InteractiveLogo className={classes.logo} />
4847

4948
<Container size="md">
50-
<Title className={classes.title} suppressHydrationWarning>
51-
{t("home.title")}
52-
</Title>
49+
<Title className={classes.title}>{t("home.title")}</Title>
5350
<div className={classes.hero}>
5451
<Image
5552
className={classes.heroImage}
@@ -60,9 +57,7 @@ export default function Home() {
6057
priority
6158
/>
6259
<div className={classes.heroInner}>
63-
<Text mt="md" suppressHydrationWarning>
64-
{t("home.intro")}
65-
</Text>
60+
<Text mt="md">{t("home.intro")}</Text>
6661

6762
<List
6863
mt={30}
@@ -79,31 +74,16 @@ export default function Home() {
7974
}
8075
>
8176
<ListItem>
82-
<b className={classes.key} suppressHydrationWarning>
83-
{t("home.safe")}
84-
</b>{" "}
85-
-{" "}
86-
<span suppressHydrationWarning>
87-
{t("home.safe-description")}
88-
</span>
77+
<b className={classes.key}>{t("home.safe")}</b> -{" "}
78+
<span>{t("home.safe-description")}</span>
8979
</ListItem>
9080
<ListItem>
91-
<b className={classes.key} suppressHydrationWarning>
92-
{t("home.easy")}
93-
</b>{" "}
94-
-{" "}
95-
<span suppressHydrationWarning>
96-
{t("home.easy-description")}
97-
</span>
81+
<b className={classes.key}>{t("home.easy")}</b> -{" "}
82+
<span>{t("home.easy-description")}</span>
9883
</ListItem>
9984
<ListItem>
100-
<b className={classes.key} suppressHydrationWarning>
101-
{t("home.free")}
102-
</b>{" "}
103-
-{" "}
104-
<span suppressHydrationWarning>
105-
{t("home.free-description")}
106-
</span>
85+
<b className={classes.key}>{t("home.free")}</b> -{" "}
86+
<span>{t("home.free-description")}</span>
10787
</ListItem>
10888
</List>
10989

src/app/translate.tsx

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

src/components/footer.tsx

Lines changed: 2 additions & 4 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 { t } from "@/app/translate";
66

77
import {
88
IconBrandX,
@@ -49,10 +49,8 @@ const allSocials = [
4949
];
5050

5151
export function FooterSocial() {
52-
const { t } = useTranslation();
5352
const socials = allSocials.map((social, i) => (
5453
<ActionIcon
55-
suppressHydrationWarning
5654
key={i}
5755
size="lg"
5856
color="gray"
@@ -77,7 +75,7 @@ export function FooterSocial() {
7775
width={91}
7876
priority
7977
/>
80-
<Text size="lg" className={classes.tagline} suppressHydrationWarning>
78+
<Text size="lg" className={classes.tagline}>
8179
{t("footer.tagline")}
8280
</Text>
8381
</Container>

0 commit comments

Comments
 (0)