diff --git a/scripts/translate.mjs b/scripts/translate.mjs
deleted file mode 100644
index dd04cae3..00000000
--- a/scripts/translate.mjs
+++ /dev/null
@@ -1,114 +0,0 @@
-import { readFileSync, writeFileSync } from "fs";
-import { resolve } from "path";
-import { translate } from "@vitalets/google-translate-api";
-
-const ROOT = resolve(import.meta.dirname, "..");
-const SEP = "\n␟\n";
-
-const TARGETS = [
- { code: "pt", to: "pt" }, { code: "pl", to: "pl" }, { code: "sv", to: "sv" },
- { code: "tr", to: "tr" }, { code: "ro", to: "ro" }, { code: "hu", to: "hu" },
- { code: "cs", to: "cs" }, { code: "sk", to: "sk" }, { code: "da", to: "da" },
- { code: "no", to: "no" }, { code: "el", to: "el" }, { code: "bg", to: "bg" },
- { code: "hr", to: "hr" }, { code: "sr", to: "sr" }, { code: "uk", to: "uk" },
- { code: "ru", to: "ru" },
-];
-
-function flatten(o, p) { const r={}; for(const[k,v]of Object.entries(o)){const f=p?`${p}.${k}`:k; typeof v==="object"&&v!==null&&!Array.isArray(v)?Object.assign(r,flatten(v,f)):r[f]=String(v??"");} return r; }
-function unflatten(flat){const r={};for(const[k,v]of Object.entries(flat)){const p=k.split(".");let c=r;for(let i=0;i
= 0) {
- const marker = `Ω${idx}Ω`;
- markers.push({ marker, original: text.slice(start, i + 1) });
- result += marker;
- idx++;
- start = -1;
- continue;
- }
- }
- if (depth === 0) result += ch;
- }
- return { text: result, markers };
-}
-
-function restore(text, markers) {
- let result = text;
- // Sort longest first
- const sorted = [...markers].sort((a, b) => b.marker.length - a.marker.length);
- for (const { marker, original } of sorted) {
- result = result.replaceAll(marker, original);
- }
- return result;
-}
-
-async function main() {
- const raw = JSON.parse(readFileSync(resolve(ROOT, "src/messages/en.json"), "utf-8"));
- const flat = flatten(raw);
- const keys = Object.keys(flat);
- const vals = Object.values(flat);
-
- console.log(`Source: en.json — ${keys.length} keys`);
-
- // Protect all values
- const protectedArr = vals.map(protect);
- const allTexts = protectedArr.map((p) => p.text);
- const allMarkers = protectedArr.map((p) => p.markers);
-
- const batch = allTexts.join(SEP);
- const batchLen = batch.length;
- console.log(`Batch size: ${batchLen} chars`);
-
- for (const { code, to } of TARGETS) {
- const outPath = resolve(ROOT, "src/messages", `${code}.json`);
- process.stdout.write(` ${code}... `);
-
- try {
- const { text } = await translate(batch, { to });
- let parts = text.split(SEP);
-
- if (parts.length !== keys.length) {
- process.stdout.write(`split fail (${parts.length}), individual fallback... `);
- parts = [];
- for (let i = 0; i < keys.length; i++) {
- try {
- const { text: t } = await translate(allTexts[i], { to });
- parts.push(t);
- } catch {
- parts.push(vals[i]);
- }
- await new Promise((r) => setTimeout(r, 200));
- }
- }
-
- // Restore placeholders
- const restored = parts.map((p, i) => restore(p, allMarkers[i]));
-
- const flatResult = {};
- keys.forEach((k, i) => { flatResult[k] = restored[i]; });
- writeFileSync(outPath, JSON.stringify(unflatten(flatResult), null, 2) + "\n", "utf-8");
- process.stdout.write(`✓\n`);
- } catch (err) {
- process.stdout.write(`FAIL: ${err.message.slice(0, 50)}\n`);
- }
-
- await new Promise((r) => setTimeout(r, 2000));
- }
-
- console.log("\nDone!");
-}
-
-main().catch(console.error);
diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx
index e9b1a8af..8e316716 100644
--- a/src/components/language-switcher.tsx
+++ b/src/components/language-switcher.tsx
@@ -3,7 +3,8 @@
import Image from "next/image";
import { useLocale } from "next-intl";
import { useRouter } from "next/navigation";
-import { useTransition, useState, useRef, useEffect } from "react";
+import { useTransition, useState, useRef, useEffect, useMemo } from "react";
+import { Search } from "lucide-react";
const LOCALES: { code: string; label: string; lang: string }[] = [
{ code: "nl", label: "Nederlands", lang: "nl" },
@@ -35,9 +36,21 @@ export function LanguageSwitcher() {
const router = useRouter();
const [pending, startTransition] = useTransition();
const [open, setOpen] = useState(false);
+ const [search, setSearch] = useState("");
const ref = useRef(null);
const current = LOCALES.find((l) => l.code === locale) ?? LOCALES[0];
+ const filtered = useMemo(() => {
+ if (!search) return LOCALES;
+ const q = search.toLowerCase();
+ return LOCALES.filter(
+ (l) =>
+ l.label.toLowerCase().includes(q) ||
+ l.code.toLowerCase().includes(q) ||
+ l.lang.toLowerCase().includes(q),
+ );
+ }, [search]);
+
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
@@ -46,6 +59,10 @@ export function LanguageSwitcher() {
return () => document.removeEventListener("mousedown", handleClick);
}, []);
+ useEffect(() => {
+ if (!open) setSearch("");
+ }, [open]);
+
function switchLocale(code: string) {
if (code === locale) return;
document.cookie = `NEXT_LOCALE=${code};path=/;max-age=31536000;samesite=strict;secure`;
@@ -76,42 +93,62 @@ export function LanguageSwitcher() {
{open && (
-
- {LOCALES.map((l) => (
-
)}