From a3480227aaa5d3cecb165c22ec9bd1fdca0d618c Mon Sep 17 00:00:00 2001 From: legend4tech Date: Mon, 22 Jun 2026 11:43:38 +0100 Subject: [PATCH 1/2] feat(i18n): wire locale switcher, verify translations, and add CI check --- .github/workflows/ci.yml | 23 +++++++++ messages/es.json | 2 +- messages/tl.json | 26 +++++----- package.json | 3 +- scripts/check-locales.js | 79 ++++++++++++++++++++++++++++++ src/app/[locale]/settings/page.tsx | 26 ++++++---- 6 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 scripts/check-locales.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8286390 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + check-locales: + name: Check Locale Parity + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Check Locales Parity + run: node scripts/check-locales.js diff --git a/messages/es.json b/messages/es.json index 871397f..35f8af3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -130,7 +130,7 @@ }, "LoanCard": { "totalOwed": "Total adeudado", - "principal": "Principal", + "principal": "Capital", "accruedInterest": "Interés acumulado", "interest": "Interés", "nextPayment": "Próximo pago", diff --git a/messages/tl.json b/messages/tl.json index 5f2fb85..67e7674 100644 --- a/messages/tl.json +++ b/messages/tl.json @@ -5,10 +5,10 @@ "getStarted": "Magsimula Na", "learnMore": "Matuto Nang Higit Pa", "stats": { - "netWorth": "Net Worth", + "netWorth": "Netong Halaga", "activeLoans": "Mga aktibong loan", "totalRemitted": "Kabuuang naipadala", - "yieldApy": "Yield (APY)" + "yieldApy": "Kita (APY)" }, "activity": { "title": "Kamakailang aktibidad", @@ -23,7 +23,7 @@ "sendRemittanceDesc": "Maglipat ng pondo sa buong mundo" }, "outreach": { - "title": "Community Outreach", + "title": "Programa sa Komunidad", "description": "Ang mga bagong borrower sa Ghana ay naghahanap ng mga micro-loan para sa mga kagamitang pang-agrikultura. Tulungang palaguin ang ecosystem!", "explore": "Galugarin ang mga pagkakataon" } @@ -46,7 +46,7 @@ "completed": "Nakumpleto", "repaid": "Binayaran", "failed": "Nabigo", - "liquidated": "Liquidated", + "liquidated": "Na-liquidate", "defaulted": "Di-nakamit" }, "pagination": { @@ -60,7 +60,7 @@ "amount": "Halaga", "status": "Katayuan", "transactionHash": "Hash ng Transaksyon", - "stellarExplorerLink": "Stellar Explorer Link" + "stellarExplorerLink": "Link ng Stellar Explorer" } }, "WalletPage": { @@ -68,14 +68,14 @@ "date": "Petsa", "type": "Uri", "amount": "Halaga", - "asset": "Asset", + "asset": "Ari-arian", "status": "Katayuan", "transactionHash": "Hash ng Transaksyon", - "stellarExplorerLink": "Stellar Explorer Link" + "stellarExplorerLink": "Link ng Stellar Explorer" } }, "Navigation": { - "home": "Home", + "home": "Pangunahin", "loans": "Mga Loan", "repay": "Bayaran", "dashboard": "Dashboard", @@ -98,7 +98,7 @@ "all": "Lahat", "active": "Aktibo", "repaid": "Bayad Na", - "defaulted": "Defaulted" + "defaulted": "Hindi Nabayaran" }, "empty": { "title": "Walang nahanap na loan", @@ -110,7 +110,7 @@ "next": "Susunod", "pageOf": "Pahina {current} of {total}" }, - "loanNumber": "Loan #{id}", + "loanNumber": "Utang #{id}", "due": "Dapat bayaran sa {date}", "status": { "active": "Aktibo", @@ -118,7 +118,7 @@ } }, "Kingdom": { - "title": "Kingdom Dashboard", + "title": "Dashboard ng Kaharian", "description": "Subaybayan ang iyong pag-unlad, i-unlock ang mga tagumpay, at umangat sa mga ranggo", "welcome": "Maligayang pagdating, {kingdomTitle}!", "level": "Ngayon ay nasa Level {level} ka", @@ -130,7 +130,7 @@ }, "LoanCard": { "totalOwed": "Kabuuang Utang", - "principal": "Principal", + "principal": "Pangunahing Halaga", "accruedInterest": "Naipon na Interes", "interest": "Interes", "nextPayment": "Susunod na Pagbabayad", @@ -140,7 +140,7 @@ "onTrack": "Sa Track", "dueSoon": "Malapit nang Dapat Bayaran", "overdue": "Lintasan", - "defaulted": "Defaulted", + "defaulted": "Hindi Nabayaran", "repayNow": "Bayaran Ngayon", "payNowOverdue": "Bayaran Ngayon (Lintasan)", "contactSupport": "Kontakin ang Support", diff --git a/package.json b/package.json index 9f2c8e1..5cb4637 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "format": "prettier --write .", "test:e2e": "playwright test", "prepare": "husky", - "analyze": "ANALYZE=true next build" + "analyze": "ANALYZE=true next build", + "check-locales": "node scripts/check-locales.js" }, "dependencies": { "@sentry/nextjs": "^10.46.0", diff --git a/scripts/check-locales.js b/scripts/check-locales.js new file mode 100644 index 0000000..24936c2 --- /dev/null +++ b/scripts/check-locales.js @@ -0,0 +1,79 @@ +const fs = require('fs'); +const path = require('path'); + +const MESSAGES_DIR = path.join(__dirname, '../messages'); +const EN_FILE = path.join(MESSAGES_DIR, 'en.json'); +const OTHER_LOCALES = ['es.json', 'tl.json']; + +function flattenObject(ob) { + let toReturn = {}; + for (let i in ob) { + if (!ob.hasOwnProperty(i)) continue; + + if (typeof ob[i] == 'object' && ob[i] !== null) { + let flatObject = flattenObject(ob[i]); + for (let x in flatObject) { + if (!flatObject.hasOwnProperty(x)) continue; + toReturn[i + '.' + x] = flatObject[x]; + } + } else { + toReturn[i] = ob[i]; + } + } + return toReturn; +} + +function checkLocales() { + console.log('Checking locale drift...'); + + if (!fs.existsSync(EN_FILE)) { + console.error('Error: en.json not found'); + process.exit(1); + } + + const enContent = JSON.parse(fs.readFileSync(EN_FILE, 'utf8')); + const enKeys = Object.keys(flattenObject(enContent)).sort(); + + let hasErrors = false; + + OTHER_LOCALES.forEach(localeFile => { + const localePath = path.join(MESSAGES_DIR, localeFile); + if (!fs.existsSync(localePath)) { + console.error(`Error: ${localeFile} not found`); + hasErrors = true; + return; + } + + const localeContent = JSON.parse(fs.readFileSync(localePath, 'utf8')); + const localeKeys = Object.keys(flattenObject(localeContent)).sort(); + + const missingKeys = enKeys.filter(k => !localeKeys.includes(k)); + const extraKeys = localeKeys.filter(k => !enKeys.includes(k)); + + if (missingKeys.length > 0) { + console.error(`\n❌ [${localeFile}] Missing keys compared to en.json:`); + missingKeys.forEach(k => console.error(` - ${k}`)); + hasErrors = true; + } + + if (extraKeys.length > 0) { + console.error(`\n❌ [${localeFile}] Extra keys not found in en.json:`); + extraKeys.forEach(k => console.error(` - ${k}`)); + hasErrors = true; + } + + if (missingKeys.length === 0 && extraKeys.length === 0) { + console.log(`✅ ${localeFile} is in sync with en.json`); + } + }); + + if (hasErrors) { + console.error('\n❌ Locale drift detected! Please ensure all locale files have the exact same keys as en.json.'); + process.exit(1); + } else { + console.log('\n✨ All locales are perfectly in sync!'); + process.exit(0); + } +} + +checkLocales(); diff --git a/src/app/[locale]/settings/page.tsx b/src/app/[locale]/settings/page.tsx index 4d80f29..21a23f9 100644 --- a/src/app/[locale]/settings/page.tsx +++ b/src/app/[locale]/settings/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { useParams, useRouter, usePathname } from "next/navigation"; import { User, Wallet, @@ -423,15 +424,25 @@ function SecuritySection() { // ─── Display section ────────────────────────────────────────────────────────── function DisplaySection() { + const router = useRouter(); + const pathname = usePathname(); + const params = useParams(); + const currentLocale = (params?.locale as string) || "en"; + const LANGUAGES = [ { code: "en", label: "English" }, { code: "es", label: "Español" }, - { code: "fr", label: "Français" }, - { code: "pt", label: "Português" }, - { code: "hi", label: "हिन्दी" }, + { code: "tl", label: "Tagalog" }, ]; - const [language, setLanguage] = useState("en"); + const handleLanguageChange = (e: React.ChangeEvent) => { + const newLocale = e.target.value; + if (pathname.startsWith(`/${currentLocale}`)) { + router.push(pathname.replace(`/${currentLocale}`, `/${newLocale}`)); + } else { + router.push(`/${newLocale}${pathname}`); + } + }; const theme = useThemeStore((s) => s.theme); const hydrated = useThemeStore((s) => s.hydrated); @@ -488,8 +499,8 @@ function DisplaySection() { -

- Full i18n support is coming soon. Only English is fully translated. -

From 4c3d79c05298cf9ff6099691d49233e838f25b9e Mon Sep 17 00:00:00 2001 From: legend4tech Date: Fri, 26 Jun 2026 08:00:42 +0100 Subject: [PATCH 2/2] style: format ci workflow and locale checker with prettier --- .github/workflows/ci.yml | 18 ++++++++-------- scripts/check-locales.js | 44 +++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8286390..d3ed4ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: check-locales: @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" - - name: Check Locales Parity - run: node scripts/check-locales.js + - name: Check Locales Parity + run: node scripts/check-locales.js diff --git a/scripts/check-locales.js b/scripts/check-locales.js index 24936c2..d168e37 100644 --- a/scripts/check-locales.js +++ b/scripts/check-locales.js @@ -1,20 +1,20 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); -const MESSAGES_DIR = path.join(__dirname, '../messages'); -const EN_FILE = path.join(MESSAGES_DIR, 'en.json'); -const OTHER_LOCALES = ['es.json', 'tl.json']; +const MESSAGES_DIR = path.join(__dirname, "../messages"); +const EN_FILE = path.join(MESSAGES_DIR, "en.json"); +const OTHER_LOCALES = ["es.json", "tl.json"]; function flattenObject(ob) { let toReturn = {}; for (let i in ob) { if (!ob.hasOwnProperty(i)) continue; - if (typeof ob[i] == 'object' && ob[i] !== null) { + if (typeof ob[i] == "object" && ob[i] !== null) { let flatObject = flattenObject(ob[i]); for (let x in flatObject) { if (!flatObject.hasOwnProperty(x)) continue; - toReturn[i + '.' + x] = flatObject[x]; + toReturn[i + "." + x] = flatObject[x]; } } else { toReturn[i] = ob[i]; @@ -24,19 +24,19 @@ function flattenObject(ob) { } function checkLocales() { - console.log('Checking locale drift...'); - + console.log("Checking locale drift..."); + if (!fs.existsSync(EN_FILE)) { - console.error('Error: en.json not found'); + console.error("Error: en.json not found"); process.exit(1); } - const enContent = JSON.parse(fs.readFileSync(EN_FILE, 'utf8')); + const enContent = JSON.parse(fs.readFileSync(EN_FILE, "utf8")); const enKeys = Object.keys(flattenObject(enContent)).sort(); - + let hasErrors = false; - OTHER_LOCALES.forEach(localeFile => { + OTHER_LOCALES.forEach((localeFile) => { const localePath = path.join(MESSAGES_DIR, localeFile); if (!fs.existsSync(localePath)) { console.error(`Error: ${localeFile} not found`); @@ -44,34 +44,36 @@ function checkLocales() { return; } - const localeContent = JSON.parse(fs.readFileSync(localePath, 'utf8')); + const localeContent = JSON.parse(fs.readFileSync(localePath, "utf8")); const localeKeys = Object.keys(flattenObject(localeContent)).sort(); - const missingKeys = enKeys.filter(k => !localeKeys.includes(k)); - const extraKeys = localeKeys.filter(k => !enKeys.includes(k)); + const missingKeys = enKeys.filter((k) => !localeKeys.includes(k)); + const extraKeys = localeKeys.filter((k) => !enKeys.includes(k)); if (missingKeys.length > 0) { console.error(`\n❌ [${localeFile}] Missing keys compared to en.json:`); - missingKeys.forEach(k => console.error(` - ${k}`)); + missingKeys.forEach((k) => console.error(` - ${k}`)); hasErrors = true; } if (extraKeys.length > 0) { console.error(`\n❌ [${localeFile}] Extra keys not found in en.json:`); - extraKeys.forEach(k => console.error(` - ${k}`)); + extraKeys.forEach((k) => console.error(` - ${k}`)); hasErrors = true; } - + if (missingKeys.length === 0 && extraKeys.length === 0) { console.log(`✅ ${localeFile} is in sync with en.json`); } }); if (hasErrors) { - console.error('\n❌ Locale drift detected! Please ensure all locale files have the exact same keys as en.json.'); + console.error( + "\n❌ Locale drift detected! Please ensure all locale files have the exact same keys as en.json.", + ); process.exit(1); } else { - console.log('\n✨ All locales are perfectly in sync!'); + console.log("\n✨ All locales are perfectly in sync!"); process.exit(0); } }