Skip to content

Commit 04522a2

Browse files
committed
feat: Add i18n Support (EN/AR)
1 parent 8ad550c commit 04522a2

14 files changed

Lines changed: 419 additions & 118 deletions

app/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CompareForm } from "../components/compare-form";
55
import { ResultDashboard } from "../components/result-dashboard";
66
import { DashboardSkeleton } from "../components/skeletons";
77
import { UserResult } from "@/types/user-result";
8+
import { LanguageSwitcher } from "@/components/language-switcher";
89

910
type ApiResponse = {
1011
success: boolean;
@@ -74,7 +75,10 @@ export default function HomePage() {
7475
DevImpact
7576
</span>
7677
</div>
77-
78+
79+
<div className="flex gap-4">
80+
<LanguageSwitcher />
81+
</div>
7882
</div>
7983
</header>
8084
<div className="max-w-6xl mx-auto px-4 py-10 space-y-6">

app/providers.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"use client";
22

3+
import { LanguageProvider } from "@/components/language-provider";
34
import { TooltipProvider } from "@/components/ui/tooltip";
45

56
export default function Providers({ children }: { children: React.ReactNode }) {
67
return (
7-
<TooltipProvider>
8-
{children}
9-
</TooltipProvider>
8+
<LanguageProvider>
9+
<TooltipProvider>{children}</TooltipProvider>{" "}
10+
</LanguageProvider>
1011
);
1112
}

components/breakdown-bars.tsx

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { UserResult } from "@/types/user-result";
2-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardHeader,
7+
CardTitle,
8+
} from "./ui/card";
39
import { Progress } from "./ui/progress";
4-
10+
import { useTranslation } from "./language-provider";
511

612
type Props = {
713
user1: UserResult;
@@ -15,45 +21,64 @@ const items = [
1521
];
1622

1723
export function BreakdownBars({ user1, user2 }: Props) {
18-
const getMaxScore = (score1: number, score2: number) => Math.max(score1, score2, 1)
19-
24+
const getMaxScore = (score1: number, score2: number) =>
25+
Math.max(score1, score2, 1);
26+
const { t,dir } = useTranslation();
2027

2128
return (
22-
<Card>
23-
<CardHeader>
24-
<CardTitle>Detailed Breakdown</CardTitle>
25-
<CardDescription>Progress bars showing relative performance</CardDescription>
26-
</CardHeader>
27-
<CardContent className="space-y-6">
28-
{["repoScore", "prScore", "contributionScore"].map((metric) => {
29-
const user1Value = user1[metric as keyof UserResult] as number
30-
const user2Value = user2[metric as keyof UserResult] as number
31-
const maxVal = getMaxScore(user1Value, user2Value)
32-
const metricLabel = metric === "repoScore" ? "Repository Score" : metric === "prScore" ? "Pull Request Score" : "Contribution Score"
33-
return (
34-
<div key={metric} className="space-y-2 pe-2">
35-
<div className="flex justify-between text-sm">
36-
<span>{metricLabel}</span>
37-
<span className="text-muted-foreground">
38-
{user1.username}: {user1Value} | {user2.username}: {user2Value}
39-
</span>
40-
</div>
41-
<div className="space-y-1 ">
42-
<div className="flex items-center gap-2">
43-
<span className="text-xs w-24 truncate">{user1.username}</span>
44-
<Progress value={(user1Value / maxVal) * 100} className="flex-1 h-2" />
45-
<span className="text-xs w-8">{user1Value}</span>
46-
</div>
47-
<div className="flex items-center gap-2">
48-
<span className="text-xs w-24 truncate">{user2.username}</span>
49-
<Progress value={(user2Value / maxVal) * 100} className="flex-1 h-2" />
50-
<span className="text-xs w-8">{user2Value}</span>
51-
</div>
52-
</div>
53-
</div>
54-
)
55-
})}
56-
</CardContent>
57-
</Card>
29+
<Card>
30+
<CardHeader>
31+
<CardTitle>{t('breakdown.title')}</CardTitle>
32+
<CardDescription>
33+
{t('breakdown.description')}
34+
</CardDescription>
35+
</CardHeader>
36+
<CardContent className="space-y-6">
37+
{["repoScore", "prScore", "contributionScore"].map((metric) => {
38+
const user1Value = user1[metric as keyof UserResult] as number;
39+
const user2Value = user2[metric as keyof UserResult] as number;
40+
const maxVal = getMaxScore(user1Value, user2Value);
41+
const metricLabel =
42+
metric === "repoScore"
43+
? "breakdown.repo"
44+
: metric === "prScore"
45+
? "breakdown.pr"
46+
: "breakdown.contribution";
47+
return (
48+
<div key={metric} className="space-y-2 pe-2">
49+
<div className="flex justify-between text-sm">
50+
<span>{t(metricLabel)}</span>
51+
<span className="text-muted-foreground">
52+
{user1.username}: {user1Value} | {user2.username}:{" "}
53+
{user2Value}
54+
</span>
55+
</div>
56+
<div className="space-y-1 ">
57+
<div className="flex items-center gap-2">
58+
<span className="text-xs w-24 truncate">
59+
{user1.username}
60+
</span>
61+
<Progress
62+
value={(user1Value / maxVal) * 100}
63+
className="flex-1 h-2"
64+
/>
65+
<span className="text-xs w-8">{user1Value}</span>
66+
</div>
67+
<div className="flex items-center gap-2">
68+
<span className="text-xs w-24 truncate">
69+
{user2.username}
70+
</span>
71+
<Progress
72+
value={(user2Value / maxVal) * 100}
73+
className="flex-1 h-2"
74+
/>
75+
<span className="text-xs w-8">{user2Value}</span>
76+
</div>
77+
</div>
78+
</div>
79+
);
80+
})}
81+
</CardContent>
82+
</Card>
5883
);
5984
}

components/compare-form.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CardTitle,
1010
} from "./ui/card";
1111
import { Alert, AlertDescription } from "./ui/alert";
12+
import { useTranslation } from "./language-provider";
1213

1314
type CompareFormProps = {
1415
data?: any;
@@ -27,6 +28,7 @@ export function CompareForm({
2728
reset,
2829
error,
2930
}: CompareFormProps) {
31+
const { t } = useTranslation();
3032
const [username1, setUsername1] = useState("pbiggar");
3133
const [username2, setUsername2] = useState("CoralineAda");
3234

@@ -54,22 +56,20 @@ export function CompareForm({
5456
<form onSubmit={submit}>
5557
<Card className="border-0 shadow-lg p-6 backdrop-blur-sm">
5658
<CardHeader>
57-
<CardTitle>Compare GitHub Developers</CardTitle>
58-
<CardDescription>
59-
Enter two GitHub usernames to compare their developer metrics
60-
</CardDescription>
59+
<CardTitle>{t("app.title")}</CardTitle>
60+
<CardDescription>{t("app.subtitle")}</CardDescription>
6161
</CardHeader>
6262
<CardContent>
6363
<div className="grid gap-3 md:grid-cols-2">
6464
<input
6565
className="h-11 rounded-lg border border-slate-200 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent bg-white"
66-
placeholder={"Username 1 (e.g., torvalds)"}
66+
placeholder={t("form.username1") }
6767
value={username1}
6868
onChange={(e) => setUsername1(e.target.value)}
6969
/>
7070
<input
7171
className="h-11 rounded-lg border border-slate-200 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent bg-white"
72-
placeholder={"Username 2 (e.g., torvalds)"}
72+
placeholder={t("form.username2") }
7373
value={username2}
7474
onChange={(e) => setUsername2(e.target.value)}
7575
/>
@@ -81,22 +81,22 @@ export function CompareForm({
8181
disabled={!canSubmit}
8282
className="min-w-[140px] shadow-sm transition-transform hover:-translate-y-0.5"
8383
>
84-
{loading ? "Comparing..." : "Compare"}
84+
{loading ? t("form.compare.ing") : t("form.compare")}
8585
</Button>
8686
{data && (
8787
<>
8888
<Button
8989
onClick={handleSwap}
9090
disabled={loading}
9191
type="button"
92-
title={"Swap users"}
92+
title={t("form.swap")}
9393
>
9494
<ArrowLeftRight className="h-4 w-4" />
9595
</Button>
9696
<Button
9797
onClick={handleReset}
9898
disabled={loading}
99-
title={"Reset"}
99+
title={t("form.reset")}
100100
type="button"
101101
>
102102
<RefreshCw className="h-4 w-4" />

components/comparison-chart.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,38 @@ import {
1717
} from "./ui/card";
1818
import { BarChart3 } from "lucide-react";
1919
import { UserResult } from "@/types/user-result";
20+
import { useTranslation } from "./language-provider";
2021

2122
type Props = {
2223
user1: UserResult;
2324
user2: UserResult;
2425
};
2526

2627
const metrics = [
27-
{ key: "repoScore", label: "Repos" },
28-
{ key: "prScore", label: "PRs" },
29-
{ key: "contributionScore", label: "Activity" },
28+
{ key: "repoScore", label: "comparsion.repo.score" },
29+
{ key: "prScore", label: "comparsion.pr.score" },
30+
{ key: "contributionScore", label: "comparsion.activity.score" },
3031
];
3132

3233
export function ComparisonChart({ user1, user2 }: Props) {
34+
const {t} = useTranslation();
3335

3436
const data = metrics.map((m) => ({
35-
name: m.label,
37+
name: t(m.label),
3638
[user1.username]: user1[m.key as keyof UserResult] ?? 0,
3739
[user2.username]: user2[m.key as keyof UserResult] ?? 0,
3840
}));
39-
41+
const renderLegendText = (value: string) => {
42+
return <span className="ms-2">{value}</span>;
43+
};
4044
return (
4145
<Card>
4246
<CardHeader>
4347
<CardTitle className="flex items-center gap-2">
4448
<BarChart3 className="h-5 w-5" />
45-
Score Comparison
49+
{t('barchart.title')}
4650
</CardTitle>
47-
<CardDescription>Visual breakdown of key metrics</CardDescription>
51+
<CardDescription>{t('barchart.desc')}</CardDescription>
4852
</CardHeader>
4953
<CardContent>
5054
<div className="h-80">
@@ -59,7 +63,7 @@ export function ComparisonChart({ user1, user2 }: Props) {
5963
<Tooltip
6064
contentStyle={{ borderRadius: 12, border: "1px solid #e2e8f0" }}
6165
/>
62-
<Legend />
66+
<Legend formatter={renderLegendText} />
6367
<Bar
6468
dataKey={user1.username}
6569
fill={user1.isWinner ? "#3b82f6" : "#22D3EE"}

components/comparison-table.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { UserResult } from "@/types/user-result";
22
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
3+
import { useTranslation } from "./language-provider";
34

45
type ScoreRow = {
56
label: string;
@@ -19,7 +20,7 @@ type ComparisonTableProps = {
1920
};
2021

2122
export function ComparisonTable({ user1, user2 }: ComparisonTableProps) {
22-
23+
const {t} = useTranslation();
2324
return (
2425
<div className="grid md:grid-cols-2 gap-6">
2526
{[user1, user2].map((user, idx) => (
@@ -34,23 +35,23 @@ export function ComparisonTable({ user1, user2 }: ComparisonTableProps) {
3435
</CardHeader>
3536
<CardContent className="pt-6 space-y-4">
3637
<div className="flex justify-between items-center border-b pb-2">
37-
<span className="text-muted-foreground">Final Score</span>
38+
<span className="text-muted-foreground">{t("comparsion.final.score")}</span>
3839
<span className="text-2xl font-bold">{user.finalScore}</span>
3940
</div>
4041
<div className="flex justify-between items-center">
41-
<span className="text-muted-foreground">Repo Score</span>
42+
<span className="text-muted-foreground">{t("comparsion.repo.score")}</span>
4243
<span className={`font-semibold ${user.repoScore > (idx === 0 ? user2.repoScore : user1.repoScore) ? "text-primary" : ""}`}>
4344
{user.repoScore}
4445
</span>
4546
</div>
4647
<div className="flex justify-between items-center">
47-
<span className="text-muted-foreground">PR Score</span>
48+
<span className="text-muted-foreground">{t("comparsion.pr.score")}</span>
4849
<span className={`font-semibold ${user.prScore > (idx === 0 ? user2.prScore : user1.prScore) ? "text-primary" : ""}`}>
4950
{user.prScore}
5051
</span>
5152
</div>
5253
<div className="flex justify-between items-center">
53-
<span className="text-muted-foreground">Contribution Score</span>
54+
<span className="text-muted-foreground">{t("comparsion.contribution.score")}</span>
5455
<span className={`font-semibold ${user.contributionScore > (idx === 0 ? user2.contributionScore : user1.contributionScore) ? "text-primary" : ""}`}>
5556
{user.contributionScore}
5657
</span>

components/insights-list.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { TrendingUp } from "lucide-react";
22
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
3+
import { useTranslation } from "./language-provider";
34

45
type Props = {
56
insights: string[];
67
};
78

89
export function InsightsList({ insights }: Props) {
9-
10+
const { t } = useTranslation();
1011
return (
1112
<Card className="bg-gradient-to-r from-primary/5 to-transparent">
1213
<CardHeader>
1314
<CardTitle className="flex items-center gap-2">
1415
<TrendingUp className="h-5 w-5" />
15-
Key Insights
16+
{t('insights.title')}
1617
</CardTitle>
1718
</CardHeader>
1819
<CardContent>

components/language-provider.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client";
2+
3+
import { createContext, useContext } from "react";
4+
import { useI18nProvider, type Locale } from "../lib/i18n";
5+
6+
type I18nContextValue = {
7+
locale: Locale;
8+
setLocale: (l: Locale) => void;
9+
t: (key: string, params?: Record<string, string | number>) => string;
10+
dir: "ltr" | "rtl";
11+
locales: { value: Locale; label: string }[];
12+
ready: boolean;
13+
};
14+
15+
const I18nContext = createContext<I18nContextValue | null>(null);
16+
17+
export function LanguageProvider({ children }: { children: React.ReactNode }) {
18+
const value = useI18nProvider();
19+
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
20+
}
21+
22+
export function useTranslation() {
23+
const ctx = useContext(I18nContext);
24+
if (!ctx) throw new Error("useTranslation must be used inside LanguageProvider");
25+
return ctx;
26+
}

components/language-switcher.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use client";
2+
3+
import { useTranslation } from "./language-provider";
4+
import { cn } from "../lib/utils";
5+
6+
export function LanguageSwitcher() {
7+
const { locale, setLocale, locales, dir } = useTranslation();
8+
return (
9+
<div className={cn("flex items-center gap-2 text-sm", dir === "rtl" && "flex-row-reverse")}>
10+
<select
11+
className="h-9 rounded-lg border border-slate-200 bg-white px-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/60"
12+
value={locale}
13+
onChange={(e) => setLocale(e.target.value as any)}
14+
>
15+
{locales.map((l) => (
16+
<option key={l.value} value={l.value}>
17+
{l.label}
18+
</option>
19+
))}
20+
</select>
21+
</div>
22+
);
23+
}

0 commit comments

Comments
 (0)