Skip to content

Commit 44976c4

Browse files
authored
Merge pull request #41 from legend4tech/i18n-locale-switcher
feat(i18n): wire locale switcher, verify translations, and add CI check
2 parents 30cd0a2 + 4c3d79c commit 44976c4

6 files changed

Lines changed: 137 additions & 24 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
check-locales:
11+
name: Check Locale Parity
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: "20"
21+
22+
- name: Check Locales Parity
23+
run: node scripts/check-locales.js

messages/es.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
},
131131
"LoanCard": {
132132
"totalOwed": "Total adeudado",
133-
"principal": "Principal",
133+
"principal": "Capital",
134134
"accruedInterest": "Interés acumulado",
135135
"interest": "Interés",
136136
"nextPayment": "Próximo pago",

messages/tl.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
"getStarted": "Magsimula Na",
66
"learnMore": "Matuto Nang Higit Pa",
77
"stats": {
8-
"netWorth": "Net Worth",
8+
"netWorth": "Netong Halaga",
99
"activeLoans": "Mga aktibong loan",
1010
"totalRemitted": "Kabuuang naipadala",
11-
"yieldApy": "Yield (APY)"
11+
"yieldApy": "Kita (APY)"
1212
},
1313
"activity": {
1414
"title": "Kamakailang aktibidad",
@@ -23,7 +23,7 @@
2323
"sendRemittanceDesc": "Maglipat ng pondo sa buong mundo"
2424
},
2525
"outreach": {
26-
"title": "Community Outreach",
26+
"title": "Programa sa Komunidad",
2727
"description": "Ang mga bagong borrower sa Ghana ay naghahanap ng mga micro-loan para sa mga kagamitang pang-agrikultura. Tulungang palaguin ang ecosystem!",
2828
"explore": "Galugarin ang mga pagkakataon"
2929
}
@@ -46,7 +46,7 @@
4646
"completed": "Nakumpleto",
4747
"repaid": "Binayaran",
4848
"failed": "Nabigo",
49-
"liquidated": "Liquidated",
49+
"liquidated": "Na-liquidate",
5050
"defaulted": "Di-nakamit"
5151
},
5252
"pagination": {
@@ -60,22 +60,22 @@
6060
"amount": "Halaga",
6161
"status": "Katayuan",
6262
"transactionHash": "Hash ng Transaksyon",
63-
"stellarExplorerLink": "Stellar Explorer Link"
63+
"stellarExplorerLink": "Link ng Stellar Explorer"
6464
}
6565
},
6666
"WalletPage": {
6767
"csv": {
6868
"date": "Petsa",
6969
"type": "Uri",
7070
"amount": "Halaga",
71-
"asset": "Asset",
71+
"asset": "Ari-arian",
7272
"status": "Katayuan",
7373
"transactionHash": "Hash ng Transaksyon",
74-
"stellarExplorerLink": "Stellar Explorer Link"
74+
"stellarExplorerLink": "Link ng Stellar Explorer"
7575
}
7676
},
7777
"Navigation": {
78-
"home": "Home",
78+
"home": "Pangunahin",
7979
"loans": "Mga Loan",
8080
"repay": "Bayaran",
8181
"dashboard": "Dashboard",
@@ -98,7 +98,7 @@
9898
"all": "Lahat",
9999
"active": "Aktibo",
100100
"repaid": "Bayad Na",
101-
"defaulted": "Defaulted"
101+
"defaulted": "Hindi Nabayaran"
102102
},
103103
"empty": {
104104
"title": "Walang nahanap na loan",
@@ -110,15 +110,15 @@
110110
"next": "Susunod",
111111
"pageOf": "Pahina {current} of {total}"
112112
},
113-
"loanNumber": "Loan #{id}",
113+
"loanNumber": "Utang #{id}",
114114
"due": "Dapat bayaran sa {date}",
115115
"status": {
116116
"active": "Aktibo",
117117
"pending": "Nakabinbin"
118118
}
119119
},
120120
"Kingdom": {
121-
"title": "Kingdom Dashboard",
121+
"title": "Dashboard ng Kaharian",
122122
"description": "Subaybayan ang iyong pag-unlad, i-unlock ang mga tagumpay, at umangat sa mga ranggo",
123123
"welcome": "Maligayang pagdating, {kingdomTitle}!",
124124
"level": "Ngayon ay nasa Level {level} ka",
@@ -130,7 +130,7 @@
130130
},
131131
"LoanCard": {
132132
"totalOwed": "Kabuuang Utang",
133-
"principal": "Principal",
133+
"principal": "Pangunahing Halaga",
134134
"accruedInterest": "Naipon na Interes",
135135
"interest": "Interes",
136136
"nextPayment": "Susunod na Pagbabayad",
@@ -140,7 +140,7 @@
140140
"onTrack": "Sa Track",
141141
"dueSoon": "Malapit nang Dapat Bayaran",
142142
"overdue": "Lintasan",
143-
"defaulted": "Defaulted",
143+
"defaulted": "Hindi Nabayaran",
144144
"repayNow": "Bayaran Ngayon",
145145
"payNowOverdue": "Bayaran Ngayon (Lintasan)",
146146
"contactSupport": "Kontakin ang Support",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"format": "prettier --write .",
1212
"test:e2e": "playwright test",
1313
"prepare": "husky",
14-
"analyze": "ANALYZE=true next build"
14+
"analyze": "ANALYZE=true next build",
15+
"check-locales": "node scripts/check-locales.js"
1516
},
1617
"dependencies": {
1718
"@sentry/nextjs": "^10.46.0",

scripts/check-locales.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
const MESSAGES_DIR = path.join(__dirname, "../messages");
5+
const EN_FILE = path.join(MESSAGES_DIR, "en.json");
6+
const OTHER_LOCALES = ["es.json", "tl.json"];
7+
8+
function flattenObject(ob) {
9+
let toReturn = {};
10+
for (let i in ob) {
11+
if (!ob.hasOwnProperty(i)) continue;
12+
13+
if (typeof ob[i] == "object" && ob[i] !== null) {
14+
let flatObject = flattenObject(ob[i]);
15+
for (let x in flatObject) {
16+
if (!flatObject.hasOwnProperty(x)) continue;
17+
toReturn[i + "." + x] = flatObject[x];
18+
}
19+
} else {
20+
toReturn[i] = ob[i];
21+
}
22+
}
23+
return toReturn;
24+
}
25+
26+
function checkLocales() {
27+
console.log("Checking locale drift...");
28+
29+
if (!fs.existsSync(EN_FILE)) {
30+
console.error("Error: en.json not found");
31+
process.exit(1);
32+
}
33+
34+
const enContent = JSON.parse(fs.readFileSync(EN_FILE, "utf8"));
35+
const enKeys = Object.keys(flattenObject(enContent)).sort();
36+
37+
let hasErrors = false;
38+
39+
OTHER_LOCALES.forEach((localeFile) => {
40+
const localePath = path.join(MESSAGES_DIR, localeFile);
41+
if (!fs.existsSync(localePath)) {
42+
console.error(`Error: ${localeFile} not found`);
43+
hasErrors = true;
44+
return;
45+
}
46+
47+
const localeContent = JSON.parse(fs.readFileSync(localePath, "utf8"));
48+
const localeKeys = Object.keys(flattenObject(localeContent)).sort();
49+
50+
const missingKeys = enKeys.filter((k) => !localeKeys.includes(k));
51+
const extraKeys = localeKeys.filter((k) => !enKeys.includes(k));
52+
53+
if (missingKeys.length > 0) {
54+
console.error(`\n❌ [${localeFile}] Missing keys compared to en.json:`);
55+
missingKeys.forEach((k) => console.error(` - ${k}`));
56+
hasErrors = true;
57+
}
58+
59+
if (extraKeys.length > 0) {
60+
console.error(`\n❌ [${localeFile}] Extra keys not found in en.json:`);
61+
extraKeys.forEach((k) => console.error(` - ${k}`));
62+
hasErrors = true;
63+
}
64+
65+
if (missingKeys.length === 0 && extraKeys.length === 0) {
66+
console.log(`✅ ${localeFile} is in sync with en.json`);
67+
}
68+
});
69+
70+
if (hasErrors) {
71+
console.error(
72+
"\n❌ Locale drift detected! Please ensure all locale files have the exact same keys as en.json.",
73+
);
74+
process.exit(1);
75+
} else {
76+
console.log("\n✨ All locales are perfectly in sync!");
77+
process.exit(0);
78+
}
79+
}
80+
81+
checkLocales();

src/app/[locale]/settings/page.tsx

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

33
import { useEffect, useState } from "react";
4+
import { useParams, useRouter, usePathname } from "next/navigation";
45
import {
56
User,
67
Wallet,
@@ -423,15 +424,25 @@ function SecuritySection() {
423424
// ─── Display section ──────────────────────────────────────────────────────────
424425

425426
function DisplaySection() {
427+
const router = useRouter();
428+
const pathname = usePathname();
429+
const params = useParams();
430+
const currentLocale = (params?.locale as string) || "en";
431+
426432
const LANGUAGES = [
427433
{ code: "en", label: "English" },
428434
{ code: "es", label: "Español" },
429-
{ code: "fr", label: "Français" },
430-
{ code: "pt", label: "Português" },
431-
{ code: "hi", label: "हिन्दी" },
435+
{ code: "tl", label: "Tagalog" },
432436
];
433437

434-
const [language, setLanguage] = useState("en");
438+
const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
439+
const newLocale = e.target.value;
440+
if (pathname.startsWith(`/${currentLocale}`)) {
441+
router.push(pathname.replace(`/${currentLocale}`, `/${newLocale}`));
442+
} else {
443+
router.push(`/${newLocale}${pathname}`);
444+
}
445+
};
435446

436447
const theme = useThemeStore((s) => s.theme);
437448
const hydrated = useThemeStore((s) => s.hydrated);
@@ -488,8 +499,8 @@ function DisplaySection() {
488499
</label>
489500
<select
490501
id="settings-language"
491-
value={language}
492-
onChange={(e) => setLanguage(e.target.value)}
502+
value={currentLocale}
503+
onChange={handleLanguageChange}
493504
className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 focus:border-indigo-500 focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-50"
494505
>
495506
{LANGUAGES.map((l) => (
@@ -498,9 +509,6 @@ function DisplaySection() {
498509
</option>
499510
))}
500511
</select>
501-
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-1.5">
502-
Full i18n support is coming soon. Only English is fully translated.
503-
</p>
504512
</div>
505513
</CardContent>
506514
</Card>

0 commit comments

Comments
 (0)