Skip to content

Commit

Permalink
feat: url에 lang param이 있을 경우 브라우저 lang보다 우선하여 표시
Browse files Browse the repository at this point in the history
  • Loading branch information
danah-kim committed Dec 1, 2024
1 parent 7b75837 commit 11e8425
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 37 deletions.
16 changes: 11 additions & 5 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HelmetProvider } from "react-helmet-async";

import { MemoryRouter, type MemoryRouterProps } from "react-router-dom";

import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";

import ThemeProvider from "@providers/ThemeProvider";
Expand Down Expand Up @@ -30,15 +30,21 @@ describe("상단 네비게이션", () => {
const languageButton = await screen.findByTestId("language-button");
expect(languageButton).toBeInTheDocument();
await userEvent.click(languageButton);
expect(await screen.findByTestId(/language-option-korea/)).toBeInTheDocument();
expect(await screen.findByTestId(/language-option-korean/)).toBeInTheDocument();
expect(await screen.findByTestId(/language-option-english/)).toBeInTheDocument();

if (languageButton.getAttribute("aria-label") === "한국어") {
await userEvent.click(screen.getByTestId("language-option-english"));
expect(languageButton.getAttribute("aria-label")).equal("english");
await waitFor(async () => {
const languageButton = await screen.findByTestId("language-button");
expect(languageButton.getAttribute("aria-label")).equal("English");
});
} else {
await userEvent.click(screen.getByTestId("language-option-korea"));
expect(languageButton.getAttribute("aria-label")).equal("korea");
await userEvent.click(screen.getByTestId("language-option-korean"));
await waitFor(async () => {
const languageButton = await screen.findByTestId("language-button");
expect(languageButton.getAttribute("aria-label")).equal("한국어");
});
}
});

Expand Down
8 changes: 8 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Route, Routes } from "react-router-dom";

import ErrorBoundary from "@components/utils/ErrorBoundary";
import LangLayout from "@components/utils/LangLayout";
import Error404Page from "@pages/error/404/page";
import Error500Page from "@pages/error/500/page";
import HowToConnectPage from "@pages/faq/how-to-connect/page";
Expand All @@ -25,6 +26,13 @@ function App() {
/>
<Route path="/terms" element={<TermsPage />} />
<Route path="/privacy" element={<PrivacyPage />} />
<Route path="/:lang" element={<LangLayout />}>
<Route index element={<HomePage />} />
<Route path="faq" element={<FaqPage />} />
<Route path="guide" element={<GuidePage />} />
<Route path="terms" element={<TermsPage />} />
<Route path="privacy" element={<PrivacyPage />} />
</Route>
<Route path="*" element={<Error404Page />} />
</Routes>
</ErrorBoundary>
Expand Down
10 changes: 7 additions & 3 deletions src/components/molecules/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link } from "react-router-dom";
import { Link, useParams } from "react-router-dom";

import Button from "@components/atoms/Button";
import Container from "@components/atoms/Container";
Expand All @@ -7,6 +7,10 @@ import { GoogleFirebase } from "@utils/google-firebase";
import { Copyright, Divider, InfoBox, PolicyButtonGroup, StyledFooter } from "./Footer.styles";

function Footer() {
const { lang } = useParams();

const prefixUrlLang = lang ? `/${lang}` : "";

const handleLogEvent = (label: string) => {
GoogleFirebase.logEvent("click_top_nav", {
item_name: label
Expand All @@ -20,12 +24,12 @@ function Footer() {
<InfoBox>
<Copyright>Copyright © {new Date().getFullYear()} Plandy</Copyright>
<PolicyButtonGroup>
<Link to="/terms" onClick={() => handleLogEvent("terms")}>
<Link to={`${prefixUrlLang}/terms`} onClick={() => handleLogEvent("terms")}>
<Button variant="text" size="small">
Terms of service
</Button>
</Link>
<Link to="/privacy" onClick={() => handleLogEvent("privacy")}>
<Link to={`${prefixUrlLang}/privacy`} onClick={() => handleLogEvent("privacy")}>
<Button variant="text" size="small">
Privacy policy
</Button>
Expand Down
46 changes: 17 additions & 29 deletions src/components/molecules/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useState } from "react";

import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";

import Button from "@components/atoms/Button";
import Container from "@components/atoms/Container";
Expand All @@ -9,31 +8,21 @@ import Select, { Option } from "@components/atoms/Select";
import useThemeStore from "@stores/theme";
import { GoogleFirebase } from "@utils/google-firebase";

import i18n from "@utils/i18n";
import { matchSupportLanguage, SupportLanguage } from "@utils/i18n";

import { Adornment, HeaderInner, Logo, StyledHeader } from "./Header.styles";

const Language = {
en: {
name: "English",
value: "english"
},
ko: {
name: "한국어",
value: "korean"
},
ja: {
name: "日本語",
value: "japanese"
}
};

function Header() {
const { i18n } = useTranslation();
const navigate = useNavigate();
const { pathname } = useLocation();
const { lang } = useParams();

const mode = useThemeStore((state) => state.mode);
const updateTrigger = useThemeStore((state) => state.updateTrigger);
const updateMode = useThemeStore((state) => state.updateMode);

const [language, setLanguage] = useState(i18n.language);
const prefixUrlLang = lang ? `/${lang}` : "";

const handleClick = () => {
updateTrigger("manual");
Expand All @@ -42,8 +31,7 @@ function Header() {
};

const handleChangeLang = (newLang?: string) => {
i18n.changeLanguage(newLang);
setLanguage(newLang || "en");
navigate(`/${newLang}${pathname.replace(prefixUrlLang, "")}`);
};

const handleLogEvent = (label: string) => {
Expand All @@ -56,39 +44,39 @@ function Header() {
<StyledHeader id="header">
<Container>
<HeaderInner>
<Link to="/" onClick={() => handleLogEvent("logo")}>
<Link to={`${prefixUrlLang}/`} onClick={() => handleLogEvent("logo")}>
<Button variant="text" size="small">
<Logo>
<img width={30} height={30} src="/icons/apple-icon.png" alt="Plandy Logo" />
</Logo>
</Button>
</Link>
<Adornment>
<Link to="/faq" onClick={() => handleLogEvent("faq")}>
<Link to={`${prefixUrlLang}/faq`} onClick={() => handleLogEvent("faq")}>
<Button variant="text" size="small">
FAQ
</Button>
</Link>
<Link to="/guide" onClick={() => handleLogEvent("guide")}>
<Link to={`${prefixUrlLang}/guide`} onClick={() => handleLogEvent("guide")}>
<Button variant="text" size="small">
Guide
</Button>
</Link>
<Select
id="language-button"
data-testid="language-button"
aria-label={language}
aria-label={matchSupportLanguage(i18n.resolvedLanguage).name}
size="small"
onChange={handleChangeLang}
value={language}
value={i18n.resolvedLanguage}
endIcon={<Icon name="ArrowDownBold" width={14} height={14} />}
placeholder="Language"
>
{Object.entries(Language).map(([key, { name, value }]) => (
{Object.values(SupportLanguage).map(({ name, value, langCode }) => (
<Option
key={`language-option-${value}`}
data-testid={`language-option-${value}`}
value={key}
value={langCode}
>
{name}
</Option>
Expand Down
24 changes: 24 additions & 0 deletions src/components/utils/LangLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";

import { resources } from "@utils/i18n";

function LangLayout() {
const { i18n } = useTranslation();
const navigate = useNavigate();
const { pathname } = useLocation();
const { lang = LangCode.EN } = useParams();

useEffect(() => {
if (lang in resources) {
i18n.changeLanguage(lang);
} else {
navigate(pathname.replace(`/${lang}`, ""), { replace: true });
}
}, [lang]);

return <Outlet />;
}

export default LangLayout;
54 changes: 54 additions & 0 deletions src/utils/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,60 @@ export const resources = {
}
};

export enum LangCode {
EN = "en",
KO = "ko",
JA = "ja"
}
export type Language = {
name: string;
value: string;
keywords: string[];
countries: string[];
fullLangCode: string;
langCode: (typeof LangCode)[keyof typeof LangCode];
};

export const SupportLanguage: Record<"ENGLISH" | "KOREAN" | "JAPANESE", Language> = {
ENGLISH: {
name: "English",
value: "english",
keywords: ["english", "en", "en-us", "us"],
countries: ["us"],
fullLangCode: "en-US",
langCode: LangCode.EN
},
KOREAN: {
name: "한국어",
value: "korean",
keywords: ["korean", "ko", "ko-kr", "kr"],
countries: ["kr"],
fullLangCode: "ko-KR",
langCode: LangCode.KO
},
JAPANESE: {
name: "日本語",
value: "japanese",
keywords: ["japanese", "ja", "ja-jp", "jp"],
countries: ["jp"],
fullLangCode: "ja-JP",
langCode: LangCode.JA
}
};

export function hasSupportLanguage(value: string): boolean {
return value
? Object.values(SupportLanguage).some(({ keywords }) => keywords.includes(value.toLowerCase()))
: false;
}

export function matchSupportLanguage(value = SupportLanguage.ENGLISH.value): Language {
return (
Object.values(SupportLanguage).find(({ keywords }) => keywords.includes(value.toLowerCase())) ||
SupportLanguage.ENGLISH
);
}

i18n
.use(Backend)
.use(LanguageDetector)
Expand Down

0 comments on commit 11e8425

Please sign in to comment.