diff --git a/app/about/page.tsx b/app/about/page.tsx
index 212cf25..ff98f8d 100644
--- a/app/about/page.tsx
+++ b/app/about/page.tsx
@@ -1,45 +1,49 @@
+import { getI18n } from "@/lib/i18n";
import { Header, Footer } from "@/components/Shell";
import Link from "next/link";
-export default function About() {
+export default async function About() {
+ const { t } = await getI18n();
return (
<>
- THE EXPLORER’S FIELD GUIDE
+ {t("THE EXPLORER’S FIELD GUIDE")}
- You know this place.
+ {t("You know this place.")}
- Probably.
+ {t("Probably.")}
- MineGeo drops you into a panorama from a community Minecraft world.
- Find out where you are before the clock runs out.
+ {t(
+ "MineGeo drops you into a panorama from a community Minecraft world. Find out where you are before the clock runs out.",
+ )}
-
Look for the little things
+
{t("Look for the little things")}
- Drag to look around. Mountains, shorelines, roofs, and landmarks all
- help. You stay in one place throughout a round.
+ {t(
+ "Drag to look around. Mountains, shorelines, roofs, and landmarks all help. You stay in one place throughout a round.",
+ )}
-
Make your guess
+
{t("Make your guess")}
- Expand the map in the bottom-right corner and click to place a pin.
- North is up, X increases east, and Z increases south. Move your pin
- until you’re ready, then submit.
+ {t(
+ "Expand the map in the bottom-right corner and click to place a pin. North is up, X increases east, and Z increases south. Move your pin until you’re ready, then submit.",
+ )}
-
Five chances to find your way
+
{t("Five chances to find your way")}
- Easy gives you 180 seconds, Normal 90, and Hard 30. Hard disables
- zoom. A timed-out round scores zero; you can still finish the
- expedition.
+ {t(
+ "Easy gives you 180 seconds, Normal 90, and Hard 30. Hard disables zoom. A timed-out round scores zero; you can still finish the expedition.",
+ )}
-
Closer means better
+
{t("Closer means better")}
- Each round awards up to 5,000 points based on horizontal distance. A
- guess within one block earns a perfect score. Compare all five guesses
- at the end.
+ {t(
+ "Each round awards up to 5,000 points based on horizontal distance. A guess within one block earns a perfect score. Compare all five guesses at the end.",
+ )}
- Let’s find a world →
+ {t("Let’s find a world →")}
diff --git a/app/auth/reset-password/page.tsx b/app/auth/reset-password/page.tsx
index 7b389d3..3a0b6de 100644
--- a/app/auth/reset-password/page.tsx
+++ b/app/auth/reset-password/page.tsx
@@ -1,11 +1,16 @@
+import { getI18n } from "@/lib/i18n";
import { cookies } from "next/headers";
import Link from "next/link";
import { Header, Footer } from "@/components/Shell";
import { PasswordForm } from "@/components/PasswordForm";
import { authClient, user } from "@/lib/supabase";
import { recoveryCookie, verifyRecoveryGrant } from "@/lib/password-recovery";
-export const metadata = { title: "Reset password" };
+export async function generateMetadata() {
+ const { t } = await getI18n();
+ return { title: t("Reset password") };
+}
export default async function ResetPassword() {
+ const { t } = await getI18n();
const [account, jar, client] = await Promise.all([
user(),
cookies(),
@@ -27,24 +32,27 @@ export default async function ResetPassword() {
- ACCOUNT RECOVERY
-
A fresh start.
+ {t("ACCOUNT RECOVERY")}
+
{t("A fresh start.")}
{valid ? (
<>
-
Choose a new password for your account.
+
+ {t("Choose a new password for your account.")}
+
>
) : (
<>
- This reset link has expired or has already been used. Request a
- fresh link to continue.
+ {t(
+ "This reset link has expired or has already been used. Request a fresh link to continue.",
+ )}
- Request a new reset link →
+ {t("Request a new reset link →")}
>
)}
diff --git a/app/dashboard/[id]/page.tsx b/app/dashboard/[id]/page.tsx
index 4ce1236..6e4c4ed 100644
--- a/app/dashboard/[id]/page.tsx
+++ b/app/dashboard/[id]/page.tsx
@@ -1,3 +1,4 @@
+import { getI18n } from "@/lib/i18n";
import { Header, Footer } from "@/components/Shell";
import { DraftEditor } from "@/components/DraftEditor";
import { user } from "@/lib/supabase";
@@ -7,6 +8,7 @@ export default async function Draft({
}: {
params: Promise<{ id: string }>;
}) {
+ const { t } = await getI18n();
const { id } = await params;
if (!(await user()))
redirect(`/log-in?next=${encodeURIComponent("/dashboard/" + id)}`);
@@ -14,8 +16,8 @@ export default async function Draft({
<>
- YOUR WORLD · YOUR LOCATIONS
-
))}
diff --git a/app/downloads/page.tsx b/app/downloads/page.tsx
index 90a7c50..b731e16 100644
--- a/app/downloads/page.tsx
+++ b/app/downloads/page.tsx
@@ -1,9 +1,11 @@
+import { getI18n } from "@/lib/i18n";
import { Header, Footer } from "@/components/Shell";
import { Blocks, Server, Download, ExternalLink } from "lucide-react";
import { existsSync } from "node:fs";
import Link from "next/link";
export const dynamic = "force-dynamic";
-export default function Downloads() {
+export default async function Downloads() {
+ const { t } = await getI18n();
const fabric = existsSync("public/tools/mod/minegeo-fabric-1.21.11.jar"),
plugin = existsSync("public/tools/plugin/minegeo-plugin-1.21.11.jar");
return (
@@ -11,32 +13,40 @@ export default function Downloads() {
- MADE FOR THE PEOPLE WHO KNOW THEIR WORLDS
+ {t("MADE FOR THE PEOPLE WHO KNOW THEIR WORLDS")}
-
Become a cartographer.
+
{t("Become a cartographer.")}
- Two ways to bring your world to MineGeo. Both create private drafts
- for you to preview before submitting for review.
+ {t(
+ "Two ways to bring your world to MineGeo. Both create private drafts for you to preview before submitting for review.",
+ )}
- FOR PLAYERS
-
Fabric capture mod.
+ {t("FOR PLAYERS")}
+
{t("Fabric capture mod.")}
- Explore normally and capture the places worth remembering. Your
- server doesn’t need a plugin.
+ {t(
+ "Explore normally and capture the places worth remembering. Your server doesn’t need a plugin.",
+ )}
-
Minecraft Java 1.21.11 + Fabric Loader + Fabric API
-
Open the capture panel with G
-
Link your account through a browser pairing code
-
Capture 5–24 viewpoints for a new map, then upload
- Paste a map ID into “Add to map” to contribute 1–12 panoramas
+ {t("Minecraft Java 1.21.11 + Fabric Loader + Fabric API")}
-
Only terrain received by your client enters the map
-
Contributions check the recorded server address
+
{t("Open the capture panel with G")}
+
{t("Link your account through a browser pairing code")}
+
{t("Capture 5–24 viewpoints for a new map, then upload")}
+
+ {t(
+ "Paste a map ID into “Add to map” to contribute 1–12 panoramas",
+ )}
+
+
+ {t("Only terrain received by your client enters the map")}
+
+
{t("Contributions check the recorded server address")}
- Install the JAR in your Minecraft mods folder alongside Fabric
- API.
+ {t(
+ "Install the JAR in your Minecraft mods folder alongside Fabric API.",
+ )}
- FOR SERVER ADMINISTRATORS
-
Server export plugin.
+ {t("FOR SERVER ADMINISTRATORS")}
+
{t("Server export plugin.")}
- Export generated Overworld terrain. MineGeo renders panoramas
- without a connected player or BlueMap.
+ {t(
+ "Export generated Overworld terrain. MineGeo renders panoramas without a connected player or BlueMap.",
+ )}
-
Bukkit/Paper 1.21.11 · Java 21
-
Select a bounded area and export existing chunks
-
Choose up to 24 random, safe surface viewpoints
+
{t("Bukkit/Paper 1.21.11 · Java 21")}
+
{t("Select a bounded area and export existing chunks")}
+
{t("Choose up to 24 random, safe surface viewpoints")}
Use /minegeo contribute <map ID> to join an existing map
-
Link a contributor token and upload resumably
-
Set the public Minecraft server address before exporting
-
Vanilla blocks/fluids, simplified lighting
+
{t("Link a contributor token and upload resumably")}
+
+ {t("Set the public Minecraft server address before exporting")}
+
- Start with a small, explored area around your builds. Exports are
- saved in plugins/MineGeo/exports/export-…/. The
- manifest appears when export finishes. Upload sends the latest
- completed export; it does not capture the world.
+ {t(
+ "Start with a small, explored area around your builds. Exports are saved in",
+ )}{" "}
+ plugins/MineGeo/exports/export-…/
+ {t(
+ ". The manifest appears when export finishes. Upload sends the latest completed export; it does not capture the world.",
+ )}
Put the JAR in plugins/ and restart. Set website, server-name and
@@ -105,15 +126,15 @@ export default function Downloads() {
-
Your map, your credit.
+
{t("Your map, your credit.")}
- Map owners can allow player contributions. Each player may add up
- to 12 panoramas per map. Owners approve the selection, then
- moderators review the map update before it becomes playable.
+ {t(
+ "Map owners can allow player contributions. Each player may add up to 12 panoramas per map. Owners approve the selection, then moderators review the map update before it becomes playable.",
+ )}
- We couldn’t load this part of the world. Check your connection and try
- again.
+ {t(
+ "We couldn’t load this part of the world. Check your connection and try again.",
+ )}
);
diff --git a/app/globals.css b/app/globals.css
index ce956ed..2a5bbfa 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -982,7 +982,9 @@ pre {
position: absolute;
inset: 0;
touch-action: none;
+ cursor: grab;
}
+.panorama:active { cursor: grabbing; }
.panorama canvas {
width: 100%;
height: 100%;
diff --git a/app/layout.tsx b/app/layout.tsx
index 00b0b5d..e7289db 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -9,24 +9,45 @@ import "./home-map.css";
import "./rounded-theme.css";
import "./cursor.css";
import { SiteCursor } from "@/components/SiteCursor";
-export const metadata: Metadata = {
- title: {
- default: "MineGeo — Get lost in a familiar world",
- template: "%s · MineGeo",
- },
- description:
- "Guess where you are in Minecraft. Explore community servers through 360° panoramas and place your pin on the map.",
-};
-export default function RootLayout({
+import { I18nProvider } from "@/components/I18nProvider";
+import { getI18n } from "@/lib/i18n";
+import "./polish.css";
+import { SiteSettingsProvider } from "@/components/SiteSettingsProvider";
+import { initialSiteSettings } from "@/lib/site-settings";
+export async function generateMetadata(): Promise {
+ const { t } = await getI18n();
+ return {
+ title: {
+ default: t("MineGeo — Get lost in a familiar world"),
+ template: "%s · MineGeo",
+ },
+ description: t(
+ "Guess where you are in Minecraft. Explore community servers through 360° panoramas and place your pin on the map.",
+ ),
+ };
+}
+export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
+ const [{ locale, preference, detectedLocale }, settings] = await Promise.all([
+ getI18n(),
+ initialSiteSettings(),
+ ]);
return (
-
+
- {children}
-
+
+
+ {children}
+
+
+
);
diff --git a/app/leaderboard/loading.tsx b/app/leaderboard/loading.tsx
index d430ca3..0cd8cc9 100644
--- a/app/leaderboard/loading.tsx
+++ b/app/leaderboard/loading.tsx
@@ -1,14 +1,16 @@
-export default function LoadingLeaderboard() {
+import { getI18n } from "@/lib/i18n";
+export default async function LoadingLeaderboard() {
+ const { t } = await getI18n();
return (
- THE EXPLORERS’ BOARD
-
Every block counts.
+ {t("THE EXPLORERS’ BOARD")}
+
{t("Every block counts.")}
- Fetching the explorers’ scores…
+ {t("Fetching the explorers’ scores…")}
{Array.from({ length: 6 }, (_, index) => (
diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx
index fcc6810..06d425e 100644
--- a/app/leaderboard/page.tsx
+++ b/app/leaderboard/page.tsx
@@ -1,3 +1,4 @@
+import { getI18n } from "@/lib/i18n";
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowLeft, ArrowRight, Search } from "lucide-react";
@@ -11,24 +12,28 @@ import { PixelTrophy } from "@/components/PixelArt";
import { leaderboard } from "@/lib/leaderboard";
import { user } from "@/lib/supabase";
import {
- formatScore,
leaderboardDifficulties,
leaderboardFilters,
leaderboardHref,
} from "@/shared/leaderboard";
export const dynamic = "force-dynamic";
-export const metadata: Metadata = {
- title: "Leaderboard",
- description:
- "Meet MineGeo’s top explorers. Compare the best five-round scores across easy, normal, and hard Minecraft expeditions.",
-};
+export async function generateMetadata(): Promise {
+ const { t } = await getI18n();
+ return {
+ title: t("Leaderboard"),
+ description: t(
+ "Meet MineGeo’s top explorers. Compare the best five-round scores across easy, normal, and hard Minecraft expeditions.",
+ ),
+ };
+}
export default async function LeaderboardPage({
searchParams,
}: {
searchParams: Promise>;
}) {
+ const { t, formatNumber } = await getI18n();
const { difficulty, search, page } = leaderboardFilters(await searchParams);
const [data, account] = await Promise.all([
leaderboard({ difficulty, search, page }),
@@ -39,16 +44,16 @@ export default async function LeaderboardPage({
- Back to home
+ {t("Back to home")}
- THE EXPLORERS’ BOARD
-
Every block counts.
+ {t("THE EXPLORERS’ BOARD")}
+
{t("Every block counts.")}
- The sharpest eyes in the Overworld.
+ {t("The sharpest eyes in the Overworld.")}
- Five rounds. 25,000 possible points. Where do you stand?
+ {t("Five rounds. 25,000 possible points. Where do you stand?")}
@@ -56,7 +61,7 @@ export default async function LeaderboardPage({
@@ -116,24 +123,25 @@ export function AccountSettings({
-
Password & security.
-
Keep your account yours.
+
{t("Password & security.")}
+
{t("Keep your account yours.")}
{passwordUpdated && (
- Your password has been updated.
+ {t("Your password has been updated.")}
)}
-
Signed up with an email link?
+
{t("Signed up with an email link?")}
- Set your first password, or recover a forgotten one, using a link
- sent to your account email.
+ {t(
+ "Set your first password, or recover a forgotten one, using a link sent to your account email.",
+ )}
- {sending ? "Sending…" : "Email me a password reset link"}
+ {sending ? t("Sending…") : t("Email me a password reset link")}
{recoveryMessage && (
diff --git a/components/Brand.tsx b/components/Brand.tsx
index 61f208c..3637784 100644
--- a/components/Brand.tsx
+++ b/components/Brand.tsx
@@ -1,13 +1,18 @@
+"use client";
+import { useI18n } from "@/components/I18nProvider";
import Link from "next/link";
+import { useSiteSettings } from "./SiteSettingsProvider";
export function Brand() {
+ const { t } = useI18n();
+ const { settings } = useSiteSettings();
return (
-
+
MineGeo
- BETA
+ {settings.betaBadgeEnabled && BETA}
);
}
diff --git a/components/CatalogBrowser.tsx b/components/CatalogBrowser.tsx
index b522a78..f9f1371 100644
--- a/components/CatalogBrowser.tsx
+++ b/components/CatalogBrowser.tsx
@@ -1,8 +1,11 @@
"use client";
+import { useI18n } from "@/components/I18nProvider";
+
import { useState } from "react";
import { Search } from "lucide-react";
import { MapCard, type CardMap } from "./MapCard";
export function CatalogBrowser({ maps }: { maps: CardMap[] }) {
+ const { t } = useI18n();
const [query, setQuery] = useState(""),
[source, setSource] = useState("all");
const shown = maps.filter(
@@ -18,17 +21,17 @@ export function CatalogBrowser({ maps }: { maps: CardMap[] }) {
-
)}
>
);
diff --git a/components/ContributionInstructions.tsx b/components/ContributionInstructions.tsx
index edc5d4d..94ecc3f 100644
--- a/components/ContributionInstructions.tsx
+++ b/components/ContributionInstructions.tsx
@@ -1,3 +1,5 @@
+"use client";
+import { useI18n } from "@/components/I18nProvider";
import Link from "next/link";
export function ContributionInstructions({
id,
@@ -10,57 +12,63 @@ export function ContributionInstructions({
serverAddress: string | null;
isOwner?: boolean;
}) {
+ const { t } = useI18n();
return (
-
Add panoramas to this map
+
{t("Add panoramas to this map")}
{serverAddress ? (
-
Contribution server:
+
{t("Contribution server:")}
{serverAddress}
- Capture on this server. Uploads from a different address are
- blocked.
+ {t(
+ "Capture on this server. Uploads from a different address are blocked.",
+ )}
) : (
- Server check is off. The owner has not set a server address; captures
- from other servers can be submitted. Only submit panoramas from this
- map’s world.
+ {t(
+ "Server check is off. The owner has not set a server address; captures from other servers can be submitted. Only submit panoramas from this map’s world.",
+ )}
)}
- Capture in the same Minecraft world, within X {bounds.minX} to{" "}
- {bounds.maxX} and Z {bounds.minZ} to {bounds.maxZ}.{" "}
+ {t("Capture in the same Minecraft world, within X")} {bounds.minX}{" "}
+ {t("to")} {bounds.maxX} {t("and Z")} {bounds.minZ} {t("to")}{" "}
+ {bounds.maxZ}.{" "}
{isOwner
- ? "As the owner, you can fill the map’s 24 panorama slots."
- : "Each guest contributor may add up to 12 panoramas in total."}{" "}
- The map holds at most 24, including pending uploads and unselected
- captures.
+ ? t("As the owner, you can fill the map’s 24 panorama slots.")
+ : t(
+ "Each guest contributor may add up to 12 panoramas in total.",
+ )}{" "}
+ {t(
+ "The map holds at most 24, including pending uploads and unselected captures.",
+ )}
- In the Fabric mod, paste this map ID into “Add to map”. For the server
- plugin, export the area and run:
+ {t(
+ "In the Fabric mod, paste this map ID into “Add to map”. For the server plugin, export the area and run:",
+ )}
/minegeo contribute {id}
{isOwner && (
- This command also adds your own panoramas. Open the resulting
- contribution in your dashboard, submit it, then accept it in this
- map’s submissions below. Finally, submit the map update for moderator
- review.
+ {t(
+ "This command also adds your own panoramas. Open the resulting contribution in your dashboard, submit it, then accept it in this map’s submissions below. Finally, submit the map update for moderator review.",
+ )}
)}
- Map ID: {id}
+ {t("Map ID:")} {id}
- Open your dashboard after processing, inspect your panoramas, and submit
- them to the owner. The owner approves them first, then moderators review
- the map update.
+ {t(
+ "Open your dashboard after processing, inspect your panoramas, and submit them to the owner. The owner approves them first, then moderators review the map update.",
+ )}
- Get the capture tools →
+ {t("Get the capture tools →")}
);
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx
index 54ce598..59c5575 100644
--- a/components/Dashboard.tsx
+++ b/components/Dashboard.tsx
@@ -1,4 +1,7 @@
"use client";
+import { statusMessage } from "@/shared/i18n";
+import { useI18n } from "@/components/I18nProvider";
+
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { createBrowserClient } from "@supabase/ssr";
@@ -11,6 +14,7 @@ type Draft = MapListing & {
upload_jobs: ProcessingJob | null;
};
export function Dashboard() {
+ const { t } = useI18n();
const [maps, setMaps] = useState([]),
[error, setError] = useState(""),
[tokens, setTokens] = useState<
@@ -77,33 +81,34 @@ export function Dashboard() {
<>
- Your maps start privately. You decide when they’re ready for review.
- Contributions are hidden here after five days. Their panoramas and
- direct links stay available.
+ {t(
+ "Your maps start privately. You decide when they’re ready for review. Contributions are hidden here after five days. Their panoramas and direct links stay available.",
+ )}
- Install the Fabric mod to capture while you explore, or the server
- plugin to export your world.
+ {t(
+ "Install the Fabric mod to capture while you explore, or the server plugin to export your world.",
+ )}
- Get the capture tools →
+ {t("Get the capture tools →")}
)}
@@ -111,24 +116,34 @@ export function Dashboard() {
- Map banned after three confirmed reports. Open your map to
- appeal.
+ {t(
+ "Map banned after three confirmed reports. Open your map to appeal.",
+ )}
- Create a contributor token, then run{" "}
- /minegeo link <token> in your server console.
- Fabric uses browser pairing instead.
+ {t("Create a contributor token, then run")}{" "}
+ /minegeo link <token>{" "}
+ {t("in your server console. Fabric uses browser pairing instead.")}
- Copy this token now. It is shown once. Keep it out of public chat.
+ {t(
+ "Copy this token now. It is shown once. Keep it out of public chat.",
+ )}
- Contribution: {draft.contribution_status}.{" "}
+ {t("Contribution:")} {t(statusMessage(draft.contribution_status))}.{" "}
{draft.state === "failed"
- ? "This contribution no longer reserves panorama slots. Upload a fresh capture to try again."
+ ? t(
+ "This contribution no longer reserves panorama slots. Upload a fresh capture to try again.",
+ )
: draft.contribution_status === "accepted"
- ? "The owner included your panoramas in the map draft. Map updates become playable after moderator approval."
+ ? t(
+ "The owner included your panoramas in the map draft. Map updates become playable after moderator approval.",
+ )
: draft.contribution_status === "submitted"
- ? "Waiting for the map owner's approval."
+ ? t("Waiting for the map owner's approval.")
: draft.contribution_status === "rejected"
- ? "Upload a fresh capture if you want to try again."
- : "Choose your locations, then submit them to the map owner."}
+ ? t("Upload a fresh capture if you want to try again.")
+ : t(
+ "Choose your locations, then submit them to the map owner.",
+ )}
)}
{draft.state === "published" && (
- The approved version stays playable while you edit and moderators
- review your update.
+ {t(
+ "The approved version stays playable while you edit and moderators review your update.",
+ )}
- Map refresh failed: {draft.upload_jobs.error}. Your previous map is
- still available. Try refreshing again below.
+ {t("Map refresh failed:")} {draft.upload_jobs.error}
+ {t(
+ ". Your previous map is still available. Try refreshing again below.",
+ )}
)}
{refreshing && (
- Refreshing your guessing map. This page updates automatically; the
- published version stays playable.
+ {t(
+ "Refreshing your guessing map. This page updates automatically; the published version stays playable.",
+ )}
{draft.state === "uploading"
- ? "Waiting for the capture tool to finish uploading. Retry upload in the tool if the connection was interrupted."
- : "Your map and panoramas are being rendered. This page updates automatically."}
+ ? t(
+ "Waiting for the capture tool to finish uploading. Retry upload in the tool if the connection was interrupted.",
+ )
+ : t(
+ "Your map and panoramas are being rendered. This page updates automatically.",
+ )}
)}
{draft.rejection_reason && (
@@ -205,8 +227,12 @@ export function DraftEditor({ id }: { id: string }) {
)}
{draft.source === "plugin"
- ? "Server-generated views use vanilla terrain and simplified lighting. Entities, shaders, custom packs and dynamic sign text are omitted."
- : "Player captures show the terrain received by the client. Unexplored map coverage remains blank."}
+ ? t(
+ "Server-generated views use vanilla terrain and simplified lighting. Entities, shaders, custom packs and dynamic sign text are omitted.",
+ )
+ : t(
+ "Player captures show the terrain received by the client. Unexplored map coverage remains blank.",
+ )}
- ↑ N · PLACE YOUR GUESS
+ {t("↑ N · PLACE YOUR GUESS")} setExpanded((v) => !v)}
>
{expanded ? : }
@@ -249,7 +252,7 @@ export function Game({ id }: { id: string }) {
{pin
? `X ${Math.round(pin.x)} · Z ${Math.round(pin.z)}`
- : "Click the map to drop a pin"}
+ : t("Click the map to drop a pin")}
guess()}
>
- {busy ? "Checking your guess…" : "Submit guess"}
+ {busy ? t("Checking your guess…") : t("Submit guess")}
)}
- DRAG TO LOOK AROUND
+ {t("DRAG TO LOOK AROUND")}
{" "}
- {session?.difficulty === "hard" ? "NO ZOOM" : "SCROLL TO ZOOM"}
+ {session?.difficulty === "hard" ? t("NO ZOOM") : t("SCROLL TO ZOOM")}
{result.timedOut
- ? "Time slipped away."
+ ? t("Time slipped away.")
: result.score === 5000
- ? "Right on the block!"
+ ? t("Right on the block!")
: result.score > 3000
- ? "You know your way around."
- : "A little off the beaten block."}
+ ? t("You know your way around.")
+ : t("A little off the beaten block.")}
{result.timedOut
- ? "The clock ran out. This round scores zero."
- : `${Math.round(result.distance || 0).toLocaleString()} blocks from the actual location.`}
+ ? t("The clock ran out. This round scores zero.")
+ : t("{count} blocks from the actual location.", {
+ count: formatNumber(Math.round(result.distance || 0)),
+ })}
)}
diff --git a/components/GuessMap.tsx b/components/GuessMap.tsx
index db6c0c7..f04e14c 100644
--- a/components/GuessMap.tsx
+++ b/components/GuessMap.tsx
@@ -1,4 +1,6 @@
"use client";
+import { useI18n } from "@/components/I18nProvider";
+
import { useEffect, useRef, useState } from "react";
import type * as Leaflet from "leaflet";
import "leaflet/dist/leaflet.css";
@@ -23,6 +25,7 @@ export function GuessMap({
onError?: (message: string) => void;
disabled?: boolean;
}) {
+ const { t } = useI18n();
const root = useRef(null),
markers = useRef(null),
lib = useRef(null),
@@ -177,18 +180,20 @@ export function GuessMap({
role="application"
aria-label={
disabled
- ? "Minecraft map. North is up."
- : "Minecraft map. North is up. Click to place a guess, or use arrow keys to pan and Enter to pin the center."
+ ? t("Minecraft map. North is up.")
+ : t(
+ "Minecraft map. North is up. Click to place a guess, or use arrow keys to pan and Enter to pin the center.",
+ )
}
/>
{error && (
{unavailable
- ? "The scoreboard is taking a break."
+ ? t("The scoreboard is taking a break.")
: search
- ? "No explorers found."
- : "The first place is still open."}
+ ? t("No explorers found.")
+ : t("The first place is still open.")}
{unavailable
- ? "We couldn’t load the scores. Try again in a moment."
+ ? t("We couldn’t load the scores. Try again in a moment.")
: search
- ? "Try another player name or clear your search."
- : "Log in and finish five rounds to put your name on the board."}
+ ? t("Try another player name or clear your search.")
+ : t("Log in and finish five rounds to put your name on the board.")}
- Players ranked by best five-round score. Ties are broken by average
- score, then games played.
+ {t(
+ "Players ranked by best five-round score. Ties are broken by average score, then games played.",
+ )}
- {strikes}/3 confirmed reports.{" "}
+ {strikes}
+ {t("/3 confirmed reports.")}{" "}
{bannedAt
- ? "This map cannot be published until a moderator approves your appeal."
- : "Three confirmed reports automatically unpublish this map."}
+ ? t(
+ "This map cannot be published until a moderator approves your appeal.",
+ )
+ : t("Three confirmed reports automatically unpublish this map.")}
- Rebuild the overhead map from a processed capture. To show new
- buildings, export the same area again and upload it first. Select that
- capture here; its boundaries must match this map exactly.
+ {t(
+ "Rebuild the overhead map from a processed capture. To show new buildings, export the same area again and upload it first. Select that capture here; its boundaries must match this map exactly.",
+ )}
- Confirming a valid report adds one strike to the map. Three strikes
- automatically unpublish it. Dismissed reports add no strikes.
+ {t(
+ "Confirming a valid report adds one strike to the map. Three strikes automatically unpublish it. Dismissed reports add no strikes.",
+ )}