Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fa9f9bf
fix(currency): warn users when bundled conversion rates are older tha…
appdevelopsk Oct 25, 2026
0623323
i18n(currency): warn de readers when exchange rates are stale
appdevelopsk Oct 25, 2026
2d790bc
i18n(currency): warn en readers when exchange rates are stale
appdevelopsk Oct 25, 2026
7e3f3b5
i18n(currency): warn fr readers when exchange rates are stale
appdevelopsk Oct 25, 2026
b02dd5b
i18n(currency): warn es readers when exchange rates are stale
appdevelopsk Oct 25, 2026
0a10098
i18n(currency): warn ja readers when exchange rates are stale
appdevelopsk Oct 25, 2026
2551328
i18n(currency): warn pt-BR readers when exchange rates are stale
appdevelopsk Oct 25, 2026
701e4b1
i18n(currency): warn it readers when exchange rates are stale
appdevelopsk Oct 25, 2026
7e7cd9f
i18n(currency): warn ru readers when exchange rates are stale
appdevelopsk Oct 25, 2026
eb1f22d
i18n(currency): explain stale exchange rate risk in ar
appdevelopsk Oct 25, 2026
8ddef69
i18n(currency): explain stale exchange rate risk in id
appdevelopsk Oct 25, 2026
5568525
i18n(currency): explain stale exchange rate risk in ko
appdevelopsk Oct 25, 2026
54c2259
i18n(currency): explain stale exchange rate risk in th
appdevelopsk Oct 25, 2026
37e9952
i18n(currency): explain stale exchange rate risk in hi
appdevelopsk Oct 25, 2026
85148c3
i18n(currency): explain stale exchange rate risk in zh-CN
appdevelopsk Oct 25, 2026
9e8cf4e
i18n(currency): explain stale exchange rate risk in tr
appdevelopsk Oct 25, 2026
580e173
i18n(currency): explain stale exchange rate risk in zh-TW
appdevelopsk Oct 25, 2026
94be372
i18n(currency): explain stale exchange rate risk in vi
appdevelopsk Oct 25, 2026
7d1aee9
test: verify currency fallback date, validity and all 17 stale-rate d…
appdevelopsk Oct 25, 2026
9615821
build: audit all currency locale warnings and stale fallback health b…
appdevelopsk Oct 25, 2026
7ba42c5
docs: document stale fallback disclosure and production QA
appdevelopsk Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions site/docs/CURRENCY_RATE_FRESHNESS_2026-10-25.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Toolify365 FX rate freshness (2026-10-25)

**Observed source risk:** `site/src/data/rates.json` fallback is dated 2026-09-03. `scripts/fetch-rates.ts` fetches live reference rates at prebuild and intentionally retains fallback on upstream network failure. The GitHub fallback date **does not prove** production currently serves stale rates; live builds may have refreshed successfully.

## Fixed
- The 31-currency calculator now shows a prominent, translated warning when embedded reference rates are >=7 days old (or malformed/future-dated), rather than relying solely on a small ordinary disclaimer.
- All 17 locale JSON files have `note.staleWarning` with a date placeholder; no bank account details or payment data collected.
- Prebuild `node scripts/check-currency-rate-freshness.mjs` validates USD base, finite positive rates, date and all localized warning strings. Old fallback rates produce a **warning**, not a failed global website build. `npm run check:currency` provides an independent check.
- Calculator formulas/amount handling, SEO indexability, robots, sitemaps and conversion rates were not rewritten. No unverified "live" conversion claim is made.

## Release checks
1. From `site/`: `npm run fetch:rates && npm run check:currency && npm run build`.
2. If the rate endpoint is unavailable and the fallback is older than 7 days, the UI must show the stale warning in all locales (test date 2026-09-03).
3. If the prebuild fetch succeeds with a current date, warning should not appear. The ordinary reference-rate date disclaimer remains.
4. Verify on real production `/en/tools/currency-converter` and `/ja/tools/currency-converter`; changes merged to GitHub do not prove live deployment.
5. Rates are indicative and bank/exchange spread differs; never make direct financial decisions on stale numbers.
5 changes: 3 additions & 2 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint": "next lint",
"typecheck": "tsc --noEmit",
"fetch:rates": "tsx scripts/fetch-rates.ts",
"prebuild": "tsx scripts/fetch-rates.ts && node scripts/check-pressure-reference.mjs",
"prebuild": "tsx scripts/fetch-rates.ts && node scripts/check-pressure-reference.mjs && node scripts/check-currency-rate-freshness.mjs",
"sitemap": "tsx scripts/generate-sitemap.ts",
"validate": "tsx scripts/validate.ts",
"audit:seo": "tsx scripts/audit-seo.ts",
Expand All @@ -20,7 +20,8 @@
"indexnow": "tsx scripts/indexnow.ts",
"report:calculate": "node scripts/calculate-ranking.mjs",
"postbuild": "node scripts/check-built-titles.mjs && node scripts/check-built-canonical.mjs && node scripts/check-built-descriptions.mjs",
"check:pressure": "node scripts/check-pressure-reference.mjs"
"check:pressure": "node scripts/check-pressure-reference.mjs",
"check:currency": "node scripts/check-currency-rate-freshness.mjs"
},
"dependencies": {
"@types/qrcode": "^1.5.6",
Expand Down
30 changes: 30 additions & 0 deletions site/scripts/check-currency-rate-freshness.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** Verify that stale FX fallback cannot ship without visible localized disclosure. */
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const rates = JSON.parse(readFileSync(join(root, "src/data/rates.json"), "utf8"));
const component = readFileSync(join(root, "src/tools/currency-converter/Component.tsx"), "utf8");
const localeDir = join(root, "src/tools/currency-converter/messages");
const names = readdirSync(localeDir).filter(x => x.endsWith(".json"));
const errors = [];
const updated = Date.parse(String(rates.updatedAt) + "T00:00:00Z");
if (!Number.isFinite(updated) || !/^\d{4}-\d{2}-\d{2}$/.test(rates.updatedAt)) errors.push("Invalid rates date");
if (rates.base !== "USD" || rates.rates?.USD !== 1) errors.push("Unexpected rate base");
if (!rates.rates || Object.values(rates.rates).some(v=>typeof v !== "number" || !Number.isFinite(v) || v<=0)) errors.push("Invalid rate entries");
if (!component.includes('role="alert"') || !component.includes('note.staleWarning') || !component.includes('ageMs >= 7 * 86400000')) errors.push("Missing client stale-rate alert");
for (const file of names) {
try {
const msg=JSON.parse(readFileSync(join(localeDir,file),"utf8"));
const v=msg.note?.staleWarning;
if (typeof v!=="string" || !v.includes("{date}") || v.length<25) errors.push(file+": missing localized stale warning");
} catch(e) { errors.push(file+": unreadable translation "+String(e)); }
}
if (errors.length) {
console.error("[currency-rates] FAIL: "+errors.join(" | "));
process.exitCode=1;
} else {
const age = Math.floor((Date.now() - updated)/86400000);
if (age >= 7 || age < -1) console.warn("[currency-rates] WARNING: cached rate is "+age+" days old. Live page must display warning until refreshed.");
console.log("[currency-rates] PASS: validated "+names.length+" localized stale warnings and currency base");
}
15 changes: 14 additions & 1 deletion site/src/tools/currency-converter/Component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useLocale, useTranslations } from "next-intl";

import ratesData from "@/data/rates.json";
Expand Down Expand Up @@ -60,6 +60,14 @@ export default function CurrencyConverter() {
const [amount, setAmount] = useState("100");
const [from, setFrom] = useState("USD");
const [to, setTo] = useState("JPY");
// The bundled fallback file may be old if a rate API fetch failed during
// prebuild. Compute age only after hydration to avoid static-HTML mismatch.
const [rateIsStale, setRateIsStale] = useState(false);
useEffect(() => {
const updated = Date.parse(`${RATES_UPDATED_AT}T00:00:00Z`);
const ageMs = Date.now() - updated;
setRateIsStale(!Number.isFinite(updated) || ageMs >= 7 * 86400000 || ageMs < -86400000);
}, []);

function handleSwap() {
setFrom(to);
Expand Down Expand Up @@ -104,6 +112,11 @@ export default function CurrencyConverter() {

return (
<div>
{rateIsStale && (
<p role="alert" className="mb-4 rounded-lg border border-amber-600 bg-amber-50 px-4 py-3 text-sm font-semibold text-amber-950 dark:bg-amber-950/30 dark:text-amber-100">
{t("note.staleWarning", { date: RATES_UPDATED_AT })}
</p>
)}
<div className="mb-4 flex flex-wrap gap-1">
{QUICK_PAIRS.map((p) => (
<button
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "السعر العكسي"
},
"note": {
"disclaimer": "أسعار محدَّثة في {date}. للأسعار الفورية، استخدم بنكاً أو خدمة صرف العملات. هذه أسعار مرجعية للسوق المتوسطة — الأسعار الفعلية تشمل هامشاً."
"disclaimer": "أسعار محدَّثة في {date}. للأسعار الفورية، استخدم بنكاً أو خدمة صرف العملات. هذه أسعار مرجعية للسوق المتوسطة — الأسعار الفعلية تشمل هامشاً.",
"staleWarning": "تنبيه: أسعار الصرف المرجعية مؤرخة في {date} وقد تكون قديمة. تحقق من الأسعار الحالية قبل الدفع أو صرف العملات أو التداول."
},
"button": {
"swap": "تبديل العملات"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Umgekehrter Kurs"
},
"note": {
"disclaimer": "Kurse aktualisiert am {date}. Für Live-Kurse nutzen Sie eine Bank oder einen Währungsumtausch-Dienst. Dies sind Mittelkurse als Referenz — die tatsächlichen Kurse enthalten eine Handelsspanne."
"disclaimer": "Kurse aktualisiert am {date}. Für Live-Kurse nutzen Sie eine Bank oder einen Währungsumtausch-Dienst. Dies sind Mittelkurse als Referenz — die tatsächlichen Kurse enthalten eine Handelsspanne.",
"staleWarning": "Achtung: Die Referenzwechselkurse stammen vom {date} und können veraltet sein. Prüfen Sie vor Zahlungen, Währungsumtausch oder Handel aktuelle Kurse."
},
"button": {
"swap": "Währungen tauschen"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Reverse rate"
},
"note": {
"disclaimer": "Rates updated {date}. For live rates, use a bank or currency exchange service. These are mid-market reference rates — actual rates include a spread."
"disclaimer": "Rates updated {date}. For live rates, use a bank or currency exchange service. These are mid-market reference rates — actual rates include a spread.",
"staleWarning": "Warning: reference exchange rates are dated {date} and may be out of date. Check a current provider before any payment, currency purchase or trade."
},
"button": {
"swap": "Swap currencies"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Tipo inverso"
},
"note": {
"disclaimer": "Tasas actualizadas el {date}. Para tasas en tiempo real, usa un banco o servicio de cambio de divisas. Estas son tasas de referencia de mercado medio — las tasas reales incluyen un diferencial."
"disclaimer": "Tasas actualizadas el {date}. Para tasas en tiempo real, usa un banco o servicio de cambio de divisas. Estas son tasas de referencia de mercado medio — las tasas reales incluyen un diferencial.",
"staleWarning": "Aviso: los tipos de cambio de referencia son del {date} y pueden estar desactualizados. Consulta tasas actuales antes de pagar, cambiar divisas u operar."
},
"button": {
"swap": "Intercambiar monedas"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Taux inverse"
},
"note": {
"disclaimer": "Taux mis à jour le {date}. Pour les taux en temps réel, utilisez une banque ou un service de change. Ce sont des taux de référence de marché médian — les taux réels incluent un écart."
"disclaimer": "Taux mis à jour le {date}. Pour les taux en temps réel, utilisez une banque ou un service de change. Ce sont des taux de référence de marché médian — les taux réels incluent un écart.",
"staleWarning": "Attention : les taux de change de référence datent du {date} et peuvent être obsolètes. Vérifiez les taux actuels avant tout paiement, change ou transaction."
},
"button": {
"swap": "Inverser les devises"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "विपरीत दर"
},
"note": {
"disclaimer": "दरें {date} को अपडेट की गई हैं। लाइव दरों के लिए बैंक या मनी ट्रांसफर सेवा का उपयोग करें। ये मध्य-बाज़ार संदर्भ दरें हैं — वास्तविक दरों में स्प्रेड शामिल होता है।"
"disclaimer": "दरें {date} को अपडेट की गई हैं। लाइव दरों के लिए बैंक या मनी ट्रांसफर सेवा का उपयोग करें। ये मध्य-बाज़ार संदर्भ दरें हैं — वास्तविक दरों में स्प्रेड शामिल होता है।",
"staleWarning": "चेतावनी: संदर्भ विनिमय दरें {date} की हैं और पुरानी हो सकती हैं। भुगतान, मुद्रा विनिमय या ट्रेड से पहले वर्तमान दरें जाँचें।"
},
"button": {
"swap": "मुद्राएँ बदलें"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Kurs kebalikan"
},
"note": {
"disclaimer": "Kurs diperbarui {date}. Untuk kurs langsung, gunakan bank atau layanan penukaran mata uang. Ini adalah kurs referensi pasar tengah — kurs aktual mencakup spread."
"disclaimer": "Kurs diperbarui {date}. Untuk kurs langsung, gunakan bank atau layanan penukaran mata uang. Ini adalah kurs referensi pasar tengah — kurs aktual mencakup spread.",
"staleWarning": "Peringatan: kurs referensi berasal dari {date} dan mungkin sudah kedaluwarsa. Periksa kurs terbaru sebelum pembayaran, penukaran mata uang, atau perdagangan."
},
"button": {
"swap": "Tukar mata uang"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Tasso inverso"
},
"note": {
"disclaimer": "Tassi aggiornati il {date}. Per tassi in tempo reale, utilizza una banca o un servizio di cambio. Si tratta di tassi di riferimento del mercato medio — i tassi effettivi includono uno spread."
"disclaimer": "Tassi aggiornati il {date}. Per tassi in tempo reale, utilizza una banca o un servizio di cambio. Si tratta di tassi di riferimento del mercato medio — i tassi effettivi includono uno spread.",
"staleWarning": "Attenzione: i tassi di cambio di riferimento risalgono al {date} e potrebbero non essere aggiornati. Verifica i tassi attuali prima di pagamenti, cambi o operazioni."
},
"button": {
"swap": "Inverti valute"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "逆レート"
},
"note": {
"disclaimer": "レートは{date}時点の参考値です。実際の取引レートは銀行や両替所にご確認ください。表示レートは仲値(中間レート)であり、実際の取引には手数料・スプレッドが加算されます。"
"disclaimer": "レートは{date}時点の参考値です。実際の取引レートは銀行や両替所にご確認ください。表示レートは仲値(中間レート)であり、実際の取引には手数料・スプレッドが加算されます。",
"staleWarning": "注意:基準為替レートの更新日は{date}です。古い可能性があります。送金・両替・取引の前に最新のレートを確認してください。"
},
"button": {
"swap": "通貨を入れ替え"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "역환율"
},
"note": {
"disclaimer": "환율은 {date} 기준입니다. 실시간 환율은 은행이나 환전 서비스를 이용하세요. 이 환율은 중간 시장 참조 환율이며 실제 환율에는 스프레드가 포함됩니다."
"disclaimer": "환율은 {date} 기준입니다. 실시간 환율은 은행이나 환전 서비스를 이용하세요. 이 환율은 중간 시장 참조 환율이며 실제 환율에는 스프레드가 포함됩니다.",
"staleWarning": "주의: 기준 환율은 {date} 기준이므로 오래되었을 수 있습니다. 결제, 환전 또는 거래 전에 최신 환율을 확인하세요."
},
"button": {
"swap": "통화 교체"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Taxa inversa"
},
"note": {
"disclaimer": "Taxas atualizadas em {date}. Para taxas em tempo real, use um banco ou serviço de câmbio. Estas são taxas de referência de mercado médio — as taxas reais incluem um spread."
"disclaimer": "Taxas atualizadas em {date}. Para taxas em tempo real, use um banco ou serviço de câmbio. Estas são taxas de referência de mercado médio — as taxas reais incluem um spread.",
"staleWarning": "Atenção: as taxas de câmbio de referência são de {date} e podem estar desatualizadas. Confira taxas atuais antes de pagamentos, câmbio ou operações."
},
"button": {
"swap": "Inverter moedas"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Обратный курс"
},
"note": {
"disclaimer": "Курсы обновлены {date}. Для актуальных курсов обращайтесь в банк или обменный сервис. Указаны справочные курсы среднего рынка — реальные курсы включают спред."
"disclaimer": "Курсы обновлены {date}. Для актуальных курсов обращайтесь в банк или обменный сервис. Указаны справочные курсы среднего рынка — реальные курсы включают спред.",
"staleWarning": "Внимание: справочные курсы валют датированы {date} и могут быть устаревшими. Проверяйте актуальные курсы перед платежами, обменом валюты или сделками."
},
"button": {
"swap": "Поменять валюты"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"reverseRate": "อัตราย้อนกลับ"
},
"note": {
"disclaimer": "อัตราอัปเดตเมื่อ {date} สำหรับอัตราแบบเรียลไทม์ให้ใช้ธนาคารหรือบริการแลกเปลี่ยนเงินตรา นี่คืออัตราอ้างอิงตลาดกลาง — อัตราจริงมีส่วนต่าง (spread) รวมอยู่ด้วย"
"disclaimer": "อัตราอัปเดตเมื่อ {date} สำหรับอัตราแบบเรียลไทม์ให้ใช้ธนาคารหรือบริการแลกเปลี่ยนเงินตรา นี่คืออัตราอ้างอิงตลาดกลาง — อัตราจริงมีส่วนต่าง (spread) รวมอยู่ด้วย",
"staleWarning": "คำเตือน: อัตราแลกเปลี่ยนอ้างอิงเป็นข้อมูลวันที่ {date} และอาจล้าสมัย โปรดตรวจสอบอัตราล่าสุดก่อนชำระเงิน แลกเงิน หรือซื้อขาย"
},
"button": {
"swap": "สลับสกุลเงิน"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Ters kur"
},
"note": {
"disclaimer": "Kurlar {date} tarihinde güncellendi. Anlık kurlar için bir banka veya döviz bürosu kullanın. Bunlar orta piyasa referans kurlarıdır — gerçek kurlar alış-satış farkını içerir."
"disclaimer": "Kurlar {date} tarihinde güncellendi. Anlık kurlar için bir banka veya döviz bürosu kullanın. Bunlar orta piyasa referans kurlarıdır — gerçek kurlar alış-satış farkını içerir.",
"staleWarning": "Uyarı: Referans döviz kurları {date} tarihine aittir ve güncel olmayabilir. Ödeme, döviz bozdurma veya işlem yapmadan önce güncel kurları kontrol edin."
},
"button": {
"swap": "Para birimlerini değiştir"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "Tỷ giá ngược"
},
"note": {
"disclaimer": "Tỷ giá cập nhật ngày {date}. Để có tỷ giá thời gian thực, hãy dùng ngân hàng hoặc dịch vụ đổi tiền. Đây là tỷ giá tham chiếu thị trường trung bình — tỷ giá thực tế bao gồm phí chênh lệch."
"disclaimer": "Tỷ giá cập nhật ngày {date}. Để có tỷ giá thời gian thực, hãy dùng ngân hàng hoặc dịch vụ đổi tiền. Đây là tỷ giá tham chiếu thị trường trung bình — tỷ giá thực tế bao gồm phí chênh lệch.",
"staleWarning": "Cảnh báo: tỷ giá tham khảo được cập nhật ngày {date} và có thể đã cũ. Hãy kiểm tra tỷ giá mới nhất trước khi thanh toán, đổi tiền hoặc giao dịch."
},
"button": {
"swap": "Hoán đổi tiền tệ"
Expand Down
3 changes: 2 additions & 1 deletion site/src/tools/currency-converter/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"reverseRate": "反向汇率"
},
"note": {
"disclaimer": "汇率更新于{date}。实时汇率请使用银行或货币兑换服务。此处为中间市场参考汇率,实际汇率包含买卖差价。"
"disclaimer": "汇率更新于{date}。实时汇率请使用银行或货币兑换服务。此处为中间市场参考汇率,实际汇率包含买卖差价。",
"staleWarning": "警告:参考汇率日期为 {date},可能已经过时。付款、换汇或交易前请核实最新汇率。"
},
"button": {
"swap": "互换货币"
Expand Down
Loading
Loading