diff --git a/website/src/api/client.ts b/website/src/api/client.ts
index 16272bb..ae6d1c2 100644
--- a/website/src/api/client.ts
+++ b/website/src/api/client.ts
@@ -1,11 +1,12 @@
-import NodeCounterService from "./node-counter-service";
import type {
- NodeCounterListener,
+ NodeRpcListener,
NodeData,
- NodeCounterOptions,
- NodeCounterState,
-} from "./node-counter-service";
+ NodeRpcOptions,
+ NodeRpcState,
+} from "./node-rpc-service";
import env from "@/config";
+import NodeRpcService from "./node-rpc-service";
+import type { EthereumAddressData } from "@/interfaces/EthereumSecurity";
// Types for API responses
interface ChainStatsData {
@@ -68,111 +69,22 @@ export interface RaidLeaderboardEntrant {
last_activity: string;
}
-interface LeaderboardOptions {
- page?: number;
- pageSize?: number;
- filterByReferralCode?: string;
-}
-
-export interface LeaderboardResponse {
- data: LeaderboardEntrant[];
- meta: {
- page: number;
- page_size: number;
- total_items: number;
- total_pages: number;
- };
-}
-
-export interface RaidLeaderboardResponse {
- data: RaidLeaderboardEntrant[];
- meta: {
- page: number;
- page_size: number;
- total_items: number;
- total_pages: number;
- };
-}
-
type ApiResponse = Promise;
const createApiClient = () => {
- const nodeCounter = new NodeCounterService();
+ const nodeRpc = new NodeRpcService();
const methods = {
- fetchRaidLeaderboard: (
- options: LeaderboardOptions,
- ): ApiResponse => {
- const firstRaid = 1;
- let url = `${env.TASK_MASTER_URL}/raid-quests/leaderboards/${firstRaid}`;
-
- let queryParams = [];
- if (options.page) queryParams.push(`page=${options.page}`);
- if (options.pageSize) queryParams.push(`page_size=${options.pageSize}`);
- if (options.filterByReferralCode)
- queryParams.push(`referral_code=${options.filterByReferralCode}`);
-
- if (queryParams.length != 0) url = url + "?" + queryParams.join("&");
-
- return fetch(url, {
- headers: {
- "Content-Type": "application/json",
- },
- });
- },
- fetchLeaderboard: (
- options: LeaderboardOptions,
- ): ApiResponse => {
- let url = `${env.TASK_MASTER_URL}/addresses/leaderboard`;
-
- let queryParams = [];
- if (options.page) queryParams.push(`page=${options.page}`);
- if (options.pageSize) queryParams.push(`page_size=${options.pageSize}`);
- if (options.filterByReferralCode)
- queryParams.push(`referral_code=${options.filterByReferralCode}`);
-
- if (queryParams.length != 0) url = url + "?" + queryParams.join("&");
-
- return fetch(url, {
- headers: {
- "Content-Type": "application/json",
- },
- });
- },
-
/**
- * Fetch blockchain statistics including transaction and account counts
+ * Get Ethereum security analysis
*/
- chainStats: (): ApiResponse> => {
- const query = `
- query GetStatus {
- allTransactions: eventsConnection(
- orderBy: id_ASC,
- where: {
- OR: {
- type_in: [TRANSFER, REVERSIBLE_TRANSFER]
- }
- }
- ) {
- totalCount
- }
- allAccounts: accountsConnection(
- orderBy: id_ASC
- ) {
- totalCount
- }
- }
- `;
-
- return fetch(env.GRAPHQL_URL, {
- headers: {
- "Content-Type": "application/json",
- },
- method: "POST",
- body: JSON.stringify({
- query,
- }),
- });
+ getEthereumSecurityAnalysis: async (
+ addressOrEnsName: string,
+ ): Promise => {
+ const data = await fetch(
+ `${env.TASK_MASTER_URL}/risk-checker/${addressOrEnsName}`,
+ );
+ return (await data.json())?.data as EthereumAddressData | null;
},
/**
@@ -213,78 +125,53 @@ const createApiClient = () => {
});
},
- /**
- * Helper method to handle GraphQL responses with proper error checking
- */
- async handleGraphQLResponse(response: Response): Promise {
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data: GraphQLResponse = await response.json();
-
- if (data.errors && data.errors.length > 0) {
- throw new Error(`GraphQL error: ${data.errors[0].message}`);
- }
-
- if (!data.data) {
- throw new Error("No data returned from GraphQL query");
- }
-
- return data.data;
- },
-
- /**
- * Convenience method to get chain stats with automatic error handling
- */
- async getChainStats(): Promise {
- const response = await methods.chainStats();
- return methods.handleGraphQLResponse(response);
- },
-
/**
* Node Counter Methods
*/
- nodeCounter: {
+ nodeRpc: {
/**
- * Subscribe to node count updates
+ * Subscribe to node RPC updates
* @param listener Callback function that receives state updates
* @returns Unsubscribe function
*/
- subscribe: (listener: NodeCounterListener) =>
- nodeCounter.subscribe(listener),
+ subscribe: (listener: NodeRpcListener) => nodeRpc.subscribe(listener),
/**
* Get current node counter state
*/
- getState: () => nodeCounter.getState(),
+ getState: () => nodeRpc.getState(),
/**
* Start the WebSocket connection to track nodes
*/
- connect: () => nodeCounter.connect(),
+ connect: () => nodeRpc.connect(),
/**
* Disconnect from the node tracking WebSocket
*/
- disconnect: () => nodeCounter.disconnect(),
+ disconnect: () => nodeRpc.disconnect(),
/**
* Check if currently connected
*/
- isConnected: () => nodeCounter.isConnected(),
+ isConnected: () => nodeRpc.isConnected(),
/**
* Get just the current node count (convenience method)
*/
- getCount: () => nodeCounter.getState().count,
+ getCount: () => nodeRpc.getState().count,
+
+ /**
+ * Get the current block height
+ */
+ getBlockHeight: () => nodeRpc.getState().blockHeight,
/**
* Get current connection status
*/
getStatus: () => ({
- status: nodeCounter.getState().status,
- message: nodeCounter.getState().statusMessage,
+ status: nodeRpc.getState().status,
+ message: nodeRpc.getState().statusMessage,
}),
},
};
@@ -302,7 +189,7 @@ export type {
ContactData,
SubscribeData,
NodeData,
- NodeCounterState,
- NodeCounterOptions,
- NodeCounterListener,
+ NodeRpcState,
+ NodeRpcOptions,
+ NodeRpcListener,
};
diff --git a/website/src/api/node-counter-service.ts b/website/src/api/node-rpc-service.ts
similarity index 68%
rename from website/src/api/node-counter-service.ts
rename to website/src/api/node-rpc-service.ts
index 576fb6c..80daec7 100644
--- a/website/src/api/node-counter-service.ts
+++ b/website/src/api/node-rpc-service.ts
@@ -6,13 +6,14 @@ export interface NodeData {
timestamp: number;
}
-export interface NodeCounterState {
+export interface NodeRpcState {
count: number | null;
+ blockHeight: number | null;
status: "disconnected" | "connecting" | "connected" | "error";
statusMessage: string;
}
-export interface NodeCounterOptions {
+export interface NodeRpcOptions {
wsUrl?: string;
maxReconnectAttempts?: number;
reconnectDelay?: number;
@@ -20,20 +21,20 @@ export interface NodeCounterOptions {
nodeTimeout?: number;
}
-export type NodeCounterListener = (state: NodeCounterState) => void;
+export type NodeRpcListener = (state: NodeRpcState) => void;
/**
* Node Counter Service
* Manages WebSocket connection to substrate network and tracks connected nodes
*/
-class NodeCounterService {
+class NodeRpcService {
private ws: WebSocket | null = null;
- private listeners: Set = new Set();
+ private listeners: Set = new Set();
private reconnectAttempts = 0;
private heartbeatIntervalId: number | null = null;
private isConnecting = false;
- private options: Required = {
+ private options: Required = {
wsUrl: "wss://a1-dirac.quantus.cat",
maxReconnectAttempts: 5,
reconnectDelay: 1000,
@@ -41,20 +42,23 @@ class NodeCounterService {
nodeTimeout: 300000, // 5 minutes
};
- private state: NodeCounterState = {
+ private state: NodeRpcState = {
count: null,
+ blockHeight: null,
status: "disconnected",
statusMessage: "Not connected",
};
- constructor(options: NodeCounterOptions = {}) {
+ private blockHeightSubscriptionId: string | null = null;
+
+ constructor(options: NodeRpcOptions = {}) {
this.options = { ...this.options, ...options };
}
/**
* Subscribe to state changes
*/
- subscribe(listener: NodeCounterListener): () => void {
+ subscribe(listener: NodeRpcListener): () => void {
this.listeners.add(listener);
// Immediately call with current state
@@ -69,7 +73,7 @@ class NodeCounterService {
/**
* Get current state snapshot
*/
- getState(): NodeCounterState {
+ getState(): NodeRpcState {
return {
...this.state,
};
@@ -104,9 +108,23 @@ class NodeCounterService {
disconnect(): void {
this.cleanup();
if (this.ws) {
+ if (
+ this.blockHeightSubscriptionId &&
+ this.ws.readyState === WebSocket.OPEN
+ ) {
+ this.ws.send(
+ JSON.stringify({
+ id: 3,
+ jsonrpc: "2.0",
+ method: "chain_unsubscribeNewHeads",
+ params: [this.blockHeightSubscriptionId],
+ }),
+ );
+ }
this.ws.close();
this.ws = null;
}
+ this.blockHeightSubscriptionId = null;
this.updateState("disconnected", "Disconnected");
this.isConnecting = false;
}
@@ -126,15 +144,25 @@ class NodeCounterService {
this.reconnectAttempts = 0;
this.updateState("connected", "Connected to network");
- // Subscribe to system health (includes peer count)
- const healthRequest = {
- id: 1,
- jsonrpc: "2.0",
- method: "system_health",
- params: [],
- };
-
- this.ws?.send(JSON.stringify(healthRequest));
+ // Request system health (includes peer count)
+ this.ws?.send(
+ JSON.stringify({
+ id: 1,
+ jsonrpc: "2.0",
+ method: "system_health",
+ params: [],
+ }),
+ );
+
+ // Subscribe to new block heads for live block height
+ this.ws?.send(
+ JSON.stringify({
+ id: 2,
+ jsonrpc: "2.0",
+ method: "chain_subscribeNewHeads",
+ params: [],
+ }),
+ );
};
this.ws.onmessage = (event) => {
@@ -174,12 +202,26 @@ class NodeCounterService {
textData = data;
}
- const message = await JSON.parse(textData);
+ const message = JSON.parse(textData);
- if (message.id == 1 && message.result?.peers > 0) {
- const nodeCount = message.result.peers;
- this.state.count = nodeCount;
+ // system_health response — peer count as validator/node count
+ if (message.id === 1 && message.result != null) {
+ this.state.count = message.result.peers ?? this.state.count;
+ this.notifyListeners();
+ }
+
+ // chain_subscribeNewHeads confirmation — store subscription id
+ if (message.id === 2 && message.result != null) {
+ this.blockHeightSubscriptionId = message.result;
+ }
+ // chain_subscribeNewHeads notification — live block height
+ if (
+ message.method === "chain_newHead" &&
+ message.params?.result?.number != null
+ ) {
+ const hexNumber = message.params.result.number as string;
+ this.state.blockHeight = parseInt(hexNumber, 16);
this.notifyListeners();
}
} catch (error) {
@@ -189,7 +231,7 @@ class NodeCounterService {
}
private updateState(
- status: NodeCounterState["status"],
+ status: NodeRpcState["status"],
statusMessage: string,
): void {
this.state.status = status;
@@ -239,4 +281,4 @@ class NodeCounterService {
}
}
-export default NodeCounterService;
+export default NodeRpcService;
diff --git a/website/src/components/features/blog/BlogList.tsx b/website/src/components/features/blog/BlogList.tsx
index 82369ed..0ca0e4b 100644
--- a/website/src/components/features/blog/BlogList.tsx
+++ b/website/src/components/features/blog/BlogList.tsx
@@ -29,6 +29,7 @@ interface Props {
noPostsFoundText: string;
searchPlaceholder: string;
featuredLabel: string;
+ readLabel: string;
tagsMap: Record;
}
@@ -87,6 +88,7 @@ export const BlogList: React.FC = ({
noPostsFoundText,
searchPlaceholder,
featuredLabel,
+ readLabel,
tagsMap,
}) => {
const [query, setQuery] = useState("");
@@ -185,7 +187,7 @@ export const BlogList: React.FC = ({
/>
- READ
+ {readLabel}
@@ -251,7 +253,7 @@ export const BlogList: React.FC = ({
/>
- READ
+ {readLabel}
diff --git a/website/src/components/features/blog/BlogPost.astro b/website/src/components/features/blog/BlogPost.astro
index abdb316..ae09098 100644
--- a/website/src/components/features/blog/BlogPost.astro
+++ b/website/src/components/features/blog/BlogPost.astro
@@ -110,6 +110,7 @@ const articleSchema: WithContext = {
{
updatedDate && (
+ {t("blog.post.updated")}{" "}
{updatedDate.toLocaleDateString(currentLocale, {
year: "numeric",
month: "long",
diff --git a/website/src/components/features/blog/Card.astro b/website/src/components/features/blog/Card.astro
index 70da592..7b4bc78 100644
--- a/website/src/components/features/blog/Card.astro
+++ b/website/src/components/features/blog/Card.astro
@@ -68,7 +68,7 @@ const formattedDate = pubDate
/>
- READ
+ {t("blog.card.read")}
diff --git a/website/src/components/features/community/BlogSection.astro b/website/src/components/features/community/BlogSection.astro
index 9ac7225..9fbfd97 100644
--- a/website/src/components/features/community/BlogSection.astro
+++ b/website/src/components/features/community/BlogSection.astro
@@ -1,8 +1,45 @@
---
-import { createTranslator, getLocaleFromUrl } from "@/utils/i18n";
+import { getCollection } from "astro:content";
+import {
+ createTranslator,
+ getLocaleFromUrl,
+ getLocalizedPath,
+ DEFAULT_LOCALE,
+} from "@/utils/i18n";
const locale = getLocaleFromUrl(Astro.url.pathname);
const t = await createTranslator(locale);
+
+let localePosts = await getCollection("blog", ({ id }) => {
+ return id.toLowerCase().startsWith(`${locale.toLowerCase()}/`);
+});
+if (localePosts.length === 0) {
+ localePosts = await getCollection("blog", ({ id }) => {
+ return id.toLowerCase().startsWith(`${DEFAULT_LOCALE.toLowerCase()}/`);
+ });
+}
+
+const recentPosts = localePosts
+ .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
+ .slice(0, 3);
+
+const translations = await import(`../../../i18n/${locale}.json`);
+const tagsMap: Record = translations.default.blog?.tags ?? {};
+
+function formatDate(date: Date): string {
+ return date
+ .toLocaleDateString("en-US", { month: "short", day: "2-digit" })
+ .toUpperCase();
+}
+
+function getTagLabel(tag: string): string {
+ return (tagsMap[tag] ?? tag.replace(/-/g, " ")).toUpperCase();
+}
+
+function getBlogUrl(postId: string): string {
+ const slug = postId.split("/").slice(1).join("/");
+ return getLocalizedPath(locale, `/blog/${slug}`);
+}
---
@@ -18,71 +55,31 @@ const t = await createTranslator(locale);
{t("community.blog.sub")}
-
-
-
-
- {t("community.blog.posts.post1.date")}
-
-
- {t("community.blog.posts.post1.tag")}
-
-
-
- {t("community.blog.posts.post1.title")}
-
-
- →
-
-
-
-
-
-
- {t("community.blog.posts.post2.date")}
-
-
- {t("community.blog.posts.post2.tag")}
-
-
-
- {t("community.blog.posts.post2.title")}
-
-
- →
-
-
-
-
-
-
- {t("community.blog.posts.post3.date")}
-
-
- {t("community.blog.posts.post3.tag")}
-
-
-
- {t("community.blog.posts.post3.title")}
-
-
- →
-
+ {
+ recentPosts.map((post, i) => (
+
+
+
+
+ {formatDate(post.data.pubDate)}
+
+
+ {getTagLabel(post.data.tags[0] ?? "")}
+
+
+
{post.data.title}
+
+ →
+
+ ))
+ }
{t("community.blog.view_all")}
diff --git a/website/src/components/features/home/BlogSection.astro b/website/src/components/features/home/BlogSection.astro
index a89f9fb..fd8789e 100644
--- a/website/src/components/features/home/BlogSection.astro
+++ b/website/src/components/features/home/BlogSection.astro
@@ -1,64 +1,84 @@
+---
+import { getCollection } from "astro:content";
+import {
+ getLocaleFromUrl,
+ getLocalizedPath,
+ DEFAULT_LOCALE,
+ createTranslator,
+} from "@/utils/i18n";
+
+const locale = getLocaleFromUrl(Astro.url.pathname);
+const t = await createTranslator(locale);
+
+// Fetch posts for the current locale, fall back to DEFAULT_LOCALE if none exist
+let localePosts = await getCollection("blog", ({ id }) => {
+ return id.toLowerCase().startsWith(`${locale.toLowerCase()}/`);
+});
+if (localePosts.length === 0) {
+ localePosts = await getCollection("blog", ({ id }) => {
+ return id.toLowerCase().startsWith(`${DEFAULT_LOCALE.toLowerCase()}/`);
+ });
+}
+
+const recentPosts = localePosts
+ .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
+ .slice(0, 3);
+
+const translations = await import(`../../../i18n/${locale}.json`);
+const tagsMap: Record = translations.default.blog?.tags ?? {};
+
+function formatDate(date: Date): string {
+ return date
+ .toLocaleDateString("en-US", { month: "short", day: "2-digit" })
+ .toUpperCase();
+}
+
+function getTagLabel(tag: string): string {
+ return (tagsMap[tag] ?? tag.replace(/-/g, " ")).toUpperCase();
+}
+
+function getBlogUrl(postId: string): string {
+ const slug = postId.split("/").slice(1).join("/");
+ return getLocalizedPath(locale, `/blog/${slug}`);
+}
+---
+
-
Latest from Quantus
-
Stay ahead of the quantum shift.
+
{t("home.blog_section.eyebrow")}
+
{t("home.blog_section.headline")}
-
Blogs
+
{t("home.blog_section.blogs_label")}
-
-
- MAR 24
- UPDATE
-
-
- Second Halving: 4x Faster ZK Proofs in Two Weeks
- Another 2x cut to prover time, Poseidon hashing, mining fix, Senoti
- rate limits, and whitepaper launch.
- →
-
-
-
-
- MAR 16
- UPDATE
-
-
- Quantum-Safe Signatures, ZK Scaling, and 2x Faster Wormhole Proofs
- Weekly update on ML-DSA signature scaling, ZK aggregation, wormhole
- proof speedup, and security reviews.
- →
-
-
-
-
- MAR 07
- PROTOCOL
-
-
- Wormhole Transactions
- Weekly update covering wormhole transactions, private block rewards
- for miners, and consensus bug fixes.
- →
-
-
-
VIEW ALL POSTS →
+ {
+ recentPosts.map((post) => (
+
+
+ {formatDate(post.data.pubDate)}
+
+ {getTagLabel(post.data.tags[0] ?? "")}
+
+
+
+ {post.data.title}
+ {post.data.description}
+ →
+
+
+ ))
+ }
+
{t("home.blog_section.view_all")}
-
QDAY Podcast
+
{t("home.blog_section.podcast_label")}
@@ -83,7 +103,7 @@
WATCH ON YOUTUBE → {t("home.blog_section.watch_on_youtube")}
@@ -98,16 +118,19 @@
WATCH ON YOUTUBE → {t("home.blog_section.watch_on_youtube")}
VIEW ALL EPISODES → {t("home.blog_section.view_all_episodes")}
+
{t("home.blog_section.episodes_count", { count: 18 })}
-
18 episodes on QDay Podcast
diff --git a/website/src/components/features/home/NewsletterSection.astro b/website/src/components/features/home/NewsletterSection.astro
index 7cd53b6..dc88a06 100644
--- a/website/src/components/features/home/NewsletterSection.astro
+++ b/website/src/components/features/home/NewsletterSection.astro
@@ -1,5 +1,6 @@
---
import { createTranslator, getLocaleFromUrl } from "@/utils/i18n";
+import Button from "@/components/ui/Button.astro";
const locale = getLocaleFromUrl(Astro.url.pathname);
const t = await createTranslator(locale);
@@ -42,9 +43,9 @@ const t = await createTranslator(locale);
placeholder={t("home.newsletter.email_placeholder")}
aria-label={t("home.newsletter.email_aria")}
/>
-
+
{t("home.newsletter.submit_label")}
-
+
{t("home.newsletter.error_message")}
@@ -173,22 +174,9 @@ const t = await createTranslator(locale);
border-color: rgba(232, 230, 224, 0.5);
}
.newsletter-btn {
- background: #ff6b35;
- color: #0e0e0e;
- border: 1px solid #ff6b35;
padding: 14px 28px;
- font-family: "Geist Mono", monospace;
- font-size: 12px;
- font-weight: 500;
- letter-spacing: 0.12em;
- cursor: pointer;
- border-radius: 0;
- transition: opacity 0.2s;
white-space: nowrap;
}
- .newsletter-btn:hover {
- opacity: 0.85;
- }
.newsletter-fine {
font-family: "Geist Mono", monospace;
font-size: 12px;
@@ -321,7 +309,9 @@ const t = await createTranslator(locale);
diff --git a/website/src/components/features/home/SolutionSection.astro b/website/src/components/features/home/SolutionSection.astro
index 504471c..8a9dc78 100644
--- a/website/src/components/features/home/SolutionSection.astro
+++ b/website/src/components/features/home/SolutionSection.astro
@@ -183,7 +183,6 @@ const t = await createTranslator(locale);
color: rgba(232, 230, 224, 0.9);
line-height: 1.25;
letter-spacing: -0.02em;
- white-space: pre-wrap;
}
#tw-cursor {
display: inline;
diff --git a/website/src/components/features/qday-checker/AdviceSection.astro b/website/src/components/features/qday-checker/AdviceSection.astro
index 0f45f80..4d4e1c4 100644
--- a/website/src/components/features/qday-checker/AdviceSection.astro
+++ b/website/src/components/features/qday-checker/AdviceSection.astro
@@ -1,35 +1,42 @@
+---
+import { createTranslator, getLocaleFromUrl } from "@/utils/i18n";
+
+const locale = getLocaleFromUrl(Astro.url.pathname);
+const t = await createTranslator(locale);
+---
+
-
WHAT TO DO NEXT
+
+ {t("quantum_risk_checker.advice.eyebrow")}
+
- Got risk? Here's what to do about it.
+ {t("quantum_risk_checker.advice.title_line1")} {
+ t("quantum_risk_checker.advice.title_line2")
+ }
- If your wallet is exposed, the only real solution is moving your assets to
- a quantum-secure address. The Quantus wallet is built on a chain where
- every transaction is signed with post-quantum cryptography, meaning your
- assets are protected not just today, but when quantum computers arrive.
+ {t("quantum_risk_checker.advice.body")}
DOWNLOAD THE QUANTUS WALLET →
- {t("quantum_risk_checker.advice.cta")}
+
diff --git a/website/src/components/features/qday-checker/DefinitionSection.astro b/website/src/components/features/qday-checker/DefinitionSection.astro
index 226c2f9..d67356b 100644
--- a/website/src/components/features/qday-checker/DefinitionSection.astro
+++ b/website/src/components/features/qday-checker/DefinitionSection.astro
@@ -1,24 +1,28 @@
+---
+import { createTranslator, getLocaleFromUrl } from "@/utils/i18n";
+
+const locale = getLocaleFromUrl(Astro.url.pathname);
+const t = await createTranslator(locale);
+---
+
-
THE QUANTUM DEADLINE
+
+ {t("quantum_risk_checker.definition.eyebrow")}
+
- Q-Day is coming.
+ {t("quantum_risk_checker.definition.title")}
- Q-Day is the moment quantum computers become powerful enough to break the
- cryptography securing most of today's digital assets. Once your public key
- is exposed, a sufficiently powerful quantum computer can derive your
- private key and drain your wallet without your knowledge.
+ {t("quantum_risk_checker.definition.body_1")}
- The window to act is open. It will not stay that way.
+ {t("quantum_risk_checker.definition.body_2")}
diff --git a/website/src/components/features/qday-checker/HeroBanner.astro b/website/src/components/features/qday-checker/HeroBanner.astro
index 4e56dc8..afb136a 100644
--- a/website/src/components/features/qday-checker/HeroBanner.astro
+++ b/website/src/components/features/qday-checker/HeroBanner.astro
@@ -1,11 +1,110 @@
+---
+import { createTranslator, getLocaleFromUrl } from "@/utils/i18n";
+
+const locale = getLocaleFromUrl(Astro.url.pathname);
+const t = await createTranslator(locale);
+
+const checkerI18n = {
+ errors: {
+ empty: t("quantum_risk_checker.checker.errors.empty"),
+ invalid: t("quantum_risk_checker.checker.errors.invalid"),
+ ensFailed: t("quantum_risk_checker.checker.errors.ens_failed"),
+ },
+ buttons: {
+ checkNow: t("quantum_risk_checker.checker.buttons.check_now"),
+ scanning: t("quantum_risk_checker.checker.buttons.scanning"),
+ },
+ terminal: {
+ init: t("quantum_risk_checker.checker.terminal.init"),
+ address: t("quantum_risk_checker.checker.terminal.address"),
+ checkingPk: t("quantum_risk_checker.checker.terminal.checking_pk"),
+ analyzingBalance: t(
+ "quantum_risk_checker.checker.terminal.analyzing_balance",
+ ),
+ calculating: t("quantum_risk_checker.checker.terminal.calculating"),
+ prompt: t("quantum_risk_checker.checker.terminal.prompt"),
+ complete: t("quantum_risk_checker.checker.terminal.complete"),
+ readiness: t("quantum_risk_checker.checker.terminal.readiness"),
+ grade: t("quantum_risk_checker.checker.terminal.grade"),
+ risk: t("quantum_risk_checker.checker.terminal.risk"),
+ exposure: t("quantum_risk_checker.checker.terminal.exposure"),
+ advice: t("quantum_risk_checker.checker.terminal.advice"),
+ advice_bullet: t("quantum_risk_checker.checker.terminal.advice_bullet"),
+ },
+ riskLevel: {
+ very_low: t("quantum_risk_checker.checker.risk_level.very_low"),
+ low: t("quantum_risk_checker.checker.risk_level.low"),
+ medium: t("quantum_risk_checker.checker.risk_level.medium"),
+ high: t("quantum_risk_checker.checker.risk_level.high"),
+ very_high: t("quantum_risk_checker.checker.risk_level.very_high"),
+ },
+ exposureLabel: {
+ exposed: t("quantum_risk_checker.checker.exposure.exposed"),
+ not_yet: t("quantum_risk_checker.checker.exposure.not_yet"),
+ },
+ recommendations: {
+ smart_contract: t(
+ "quantum_risk_checker.checker.recommendations.smart_contract",
+ ),
+ public_key_exposed: t(
+ "quantum_risk_checker.checker.recommendations.public_key_exposed",
+ ),
+ public_key_safe: t(
+ "quantum_risk_checker.checker.recommendations.public_key_safe",
+ ),
+ balance_very_high: t(
+ "quantum_risk_checker.checker.recommendations.balance_very_high",
+ ),
+ balance_high: t(
+ "quantum_risk_checker.checker.recommendations.balance_high",
+ ),
+ balance_medium: t(
+ "quantum_risk_checker.checker.recommendations.balance_medium",
+ ),
+ balance_low: t("quantum_risk_checker.checker.recommendations.balance_low"),
+ receive_only: t(
+ "quantum_risk_checker.checker.recommendations.receive_only",
+ ),
+ empty_address: t(
+ "quantum_risk_checker.checker.recommendations.empty_address",
+ ),
+ },
+ exposureDuration: {
+ less_than_a_day: t(
+ "quantum_risk_checker.checker.exposure_duration.less_than_a_day",
+ ),
+ one_day: t("quantum_risk_checker.checker.exposure_duration.one_day"),
+ days: t("quantum_risk_checker.checker.exposure_duration.days"),
+ one_month: t("quantum_risk_checker.checker.exposure_duration.one_month"),
+ months: t("quantum_risk_checker.checker.exposure_duration.months"),
+ one_year: t("quantum_risk_checker.checker.exposure_duration.one_year"),
+ one_year_months: t(
+ "quantum_risk_checker.checker.exposure_duration.one_year_months",
+ ),
+ years: t("quantum_risk_checker.checker.exposure_duration.years"),
+ years_months: t(
+ "quantum_risk_checker.checker.exposure_duration.years_months",
+ ),
+ },
+};
+---
+
+
+
-
QUANTUM RISK CHECKER
-
Is your wallet exposed?
+
+ {t("quantum_risk_checker.hero_banner.eyebrow")}
+
+
+ {t("quantum_risk_checker.hero_banner.title")}
+
- Enter any Ethereum address. We will scan its transaction history and
- balance to calculate your quantum vulnerability. Know your risk before
- Q-Day arrives.
+ {t("quantum_risk_checker.hero_banner.sub")}
@@ -14,32 +113,37 @@
- QUANTUM RISK SCANNER v1.0
+
+ {t("quantum_risk_checker.hero_banner.tool_label")}
+
- ETHEREUM ADDRESS OR ENS NAME
+
+ {t("quantum_risk_checker.hero_banner.field_label")}
+
- Please enter a valid Ethereum address (0x... 42 characters) or ENS
- name (example.eth).
- CHECK NOW →
- Reads public blockchain data only. No private keys required.
+
+ {t("quantum_risk_checker.hero_banner.validation_error")}
+
+
+ {t("quantum_risk_checker.hero_banner.scan_button")}
+
+
+ {t("quantum_risk_checker.hero_banner.fine_print")}
+
-
CHECK ANOTHER ADDRESS →
+
+ {t("quantum_risk_checker.hero_banner.reset_link")}
+
@@ -373,6 +477,11 @@
white-space: pre-wrap;
}
+ .qrc-line--indent {
+ padding-left: 6ch;
+ text-indent: -6ch;
+ }
+
/* ── DIVIDER ── */
.qrc-divider {
display: block;
@@ -465,250 +574,246 @@
diff --git a/website/src/components/features/qday-checker/MethodSection.astro b/website/src/components/features/qday-checker/MethodSection.astro
index 26527e9..769b856 100644
--- a/website/src/components/features/qday-checker/MethodSection.astro
+++ b/website/src/components/features/qday-checker/MethodSection.astro
@@ -1,28 +1,38 @@
+---
+import { createTranslator, getLocaleFromUrl } from "@/utils/i18n";
+
+const locale = getLocaleFromUrl(Astro.url.pathname);
+const t = await createTranslator(locale);
+---
+
-
HOW IT WORKS
+
+ {t("quantum_risk_checker.method.eyebrow")}
+
- Two risk factors. One score.
+ {t("quantum_risk_checker.method.title")}
- The Quantum Risk Checker scans any Ethereum address or ENS name and
- calculates a Quantum Readiness Score based on two signals:
+ {t("quantum_risk_checker.method.intro")}
-
PUBLIC KEY EXPOSURE
+
+ {t("quantum_risk_checker.method.signal_1_title")}
+
- Every outgoing transaction permanently exposes your public key
- on-chain. The longer it has been exposed, the higher the risk.
+ {t("quantum_risk_checker.method.signal_1_body")}
-
BALANCE SENSITIVITY
+
+ {t("quantum_risk_checker.method.signal_2_title")}
+
- The higher the balance in an exposed wallet, the more attractive the
- target. High balance combined with an exposed key means maximum risk.
+ {t("quantum_risk_checker.method.signal_2_body")}
diff --git a/website/src/components/layout/Layout.astro b/website/src/components/layout/Layout.astro
index 8e24f07..4cc2e52 100644
--- a/website/src/components/layout/Layout.astro
+++ b/website/src/components/layout/Layout.astro
@@ -121,8 +121,7 @@ const breadcrumbs = generateBreadcrumbs({