diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 628e3a5..8d25120 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -7,15 +7,48 @@ import sitemap from '@astrojs/sitemap'; import mdx from '@astrojs/mdx'; import cloudflare from '@astrojs/cloudflare'; +import remarkLocalizeDocLinks from './src/lib/remark-localize-doc-links.mjs'; export default defineConfig({ site: 'https://shutterbridge.yarosfpv.com', compressHTML: true, - integrations: [svelte(), icon(), sitemap(), mdx()], + + markdown: { + remarkPlugins: [remarkLocalizeDocLinks], + }, + integrations: [ + svelte(), + icon(), + sitemap({ + i18n: { + defaultLocale: 'en', + locales: { en: 'en', es: 'es', uk: 'uk' }, + }, + }), + mdx(), + ], + + i18n: { + defaultLocale: 'en', + locales: ['en', 'es', 'uk'], + routing: { + prefixDefaultLocale: true, + }, + }, vite: { plugins: [tailwindcss()], - optimizeDeps: { include: ['esptool-js'] }, + // Pre-bundle the deps that would otherwise be discovered mid-session and + // trigger a full optimizer reload, which the Cloudflare workerd dev runner + // can't survive (crash: ".vite/deps_ssr/handler-*.js does not exist"). + optimizeDeps: { + include: [ + 'esptool-js', + '@astrojs/svelte/server.js', + 'astro-icon/components', + 'astro/logger/console', + ], + }, }, adapter: cloudflare(), diff --git a/site/package.json b/site/package.json index 315bb56..0ab5171 100644 --- a/site/package.json +++ b/site/package.json @@ -14,7 +14,7 @@ "generate-types": "wrangler types" }, "dependencies": { - "@astrojs/cloudflare": "^14.3.0", + "@astrojs/cloudflare": "^14.3.2", "@astrojs/mdx": "^7.0.5", "@astrojs/sitemap": "^3.7.4", "@astrojs/svelte": "^9.0.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 51acaa2..6ea85d1 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -9,17 +9,17 @@ importers: .: dependencies: '@astrojs/cloudflare': - specifier: ^14.3.0 - version: 14.3.0(@types/node@24.13.3)(astro@7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(wrangler@4.129.0) + specifier: ^14.3.2 + version: 14.3.2(@types/node@24.13.3)(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(wrangler@4.129.0) '@astrojs/mdx': specifier: ^7.0.5 - version: 7.0.8(astro@7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)) + version: 7.0.8(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)) '@astrojs/sitemap': specifier: ^3.7.4 version: 3.7.4 '@astrojs/svelte': specifier: ^9.0.1 - version: 9.0.1(@types/node@24.13.3)(astro@7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(svelte@5.57.0)(typescript@6.0.3) + version: 9.0.1(@types/node@24.13.3)(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(svelte@5.57.0)(typescript@6.0.3) '@iconify-json/fa6-brands': specifier: ^1.2.6 version: 1.2.6 @@ -34,7 +34,7 @@ importers: version: 4.3.3(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)) astro: specifier: ^7.3.1 - version: 7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) + version: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) astro-icon: specifier: ^1.2.0 version: 1.2.0 @@ -60,8 +60,8 @@ packages: '@antfu/install-pkg@2.0.1': resolution: {integrity: sha512-iCKVQcIC0e3oDxEfs3SHQGW+ovhBMZmS1TE+bTk50rVyMCBmCfClv7Qi3HQKlumYwvjb/iIMeWCW2i67q6kFfQ==} - '@astrojs/cloudflare@14.3.0': - resolution: {integrity: sha512-sJZH33jpin7IM4NW0xREPSmqzxyNtzmdywMyXah8VID/MhvNwaOiu6Wwha97QNAKZQwZoPbgAblZjxqisyfDnA==} + '@astrojs/cloudflare@14.3.2': + resolution: {integrity: sha512-4aWixXcCsxOGUH/alrJ/a/Nm5pKYBy8vqDj6YDmG+yPSLrx5QptLVCJTGyKPRRWtWG4rExPXrD2ClO7jqpYauw==} peerDependencies: astro: ^7.2.0 wrangler: ^4.125.0 @@ -140,8 +140,8 @@ packages: '@astrojs/markdown-remark@7.2.4': resolution: {integrity: sha512-MvspGMynWKAjTe4/lTUdmBPHIFKNVLTCF6UlyWGogTGzNrTvjD+D4n48k7h8swxsEPKHK2TwxkZO7uoaCv1Pow==} - '@astrojs/markdown-satteri@0.4.0': - resolution: {integrity: sha512-wykOOW9KsUVcZweOpY/CeXpdKcCKZy6fQbdcteWFuI75+sQCiqxYM7VKsGa5b+aGl3cYQscFY37rsbbyal5MRw==} + '@astrojs/markdown-satteri@0.4.1': + resolution: {integrity: sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg==} '@astrojs/mdx@7.0.8': resolution: {integrity: sha512-RNuwq2ccTSi7NX9YqlR0noaWoVdOrZuJvdfWXotAzdhMVB0b0zEMLnNU+xar7le0GwTMlTObD/QAu8jeHA/3nA==} @@ -1349,8 +1349,8 @@ packages: resolution: {integrity: sha512-0848V2WZuTPhqGSqLeZimLJZ5wmamMtWYzvA2kvg8s2DrUPZ3/nrTcHJWN0eJHt62jy7uvNxgTNiUJGIr7JNxg==} engines: {node: '>=22.12.0'} - astro@7.3.1: - resolution: {integrity: sha512-A/bJYHtc6n0UAdROY9W948fW6lX0pnUA+JWKW+IGCXRyDIGN2MsXC0OlS8onZGJjr+sv8htemwOc9W5PBRXCkw==} + astro@7.3.3: + resolution: {integrity: sha512-NF08hk3edFkVmr9avf9HW/rQyf6NrpbVDyYj2cVN3JfijDhfJYh1ivWVTY5PUd5+rRZn/Vtu4iaeyNlP0Iv6mg==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: @@ -1609,8 +1609,8 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} - find-proc@0.1.0: - resolution: {integrity: sha512-OaOpEYv2PiQ7SQ5LIrl+deA1XaWcxEjnpM6VuWXTUvn+teIXxeFTLDmu18/zDQpFmHN4o3oDBX+BT0AGwEhemg==} + find-proc@0.2.0: + resolution: {integrity: sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==} engines: {node: ^20.19.0 || >=22.12.0} flattie@1.1.1: @@ -2507,6 +2507,10 @@ packages: uploadthing: optional: true + verkit@0.4.0: + resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==} + engines: {node: '>=18.12.0'} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -2630,12 +2634,12 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.3.1 - '@astrojs/cloudflare@14.3.0(@types/node@24.13.3)(astro@7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(wrangler@4.129.0)': + '@astrojs/cloudflare@14.3.2(@types/node@24.13.3)(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(wrangler@4.129.0)': dependencies: '@astrojs/internal-helpers': 0.11.0 '@astrojs/underscore-redirects': 1.0.4 '@cloudflare/vite-plugin': 1.54.4(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0))(wrangler@4.129.0) - astro: 7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) + astro: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) piccolore: 0.1.3 vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0) wrangler: 4.129.0 @@ -2753,20 +2757,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/markdown-satteri@0.4.0': + '@astrojs/markdown-satteri@0.4.1': dependencies: '@astrojs/internal-helpers': 0.11.0 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 satteri: 0.10.5 - '@astrojs/mdx@7.0.8(astro@7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))': + '@astrojs/mdx@7.0.8(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@astrojs/internal-helpers': 0.10.4 '@astrojs/markdown-remark': 7.2.4 '@mdx-js/mdx': 3.1.1 acorn: 8.18.0 - astro: 7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) + astro: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) es-module-lexer: 2.3.2 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -2789,10 +2793,10 @@ snapshots: sitemap: 9.0.1 zod: 4.5.4 - '@astrojs/svelte@9.0.1(@types/node@24.13.3)(astro@7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(svelte@5.57.0)(typescript@6.0.3)': + '@astrojs/svelte@9.0.1(@types/node@24.13.3)(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0))(esbuild@0.28.2)(jiti@2.7.0)(svelte@5.57.0)(typescript@6.0.3)': dependencies: '@sveltejs/vite-plugin-svelte': 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)) - astro: 7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) + astro: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0) svelte: 5.57.0 svelte2tsx: 0.7.61(svelte@5.57.0)(typescript@6.0.3) typescript: 6.0.3 @@ -3678,11 +3682,11 @@ snapshots: '@iconify/types': 2.0.0 '@iconify/utils': 3.1.5 - astro@7.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0): + astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0): dependencies: '@astrojs/compiler-rs': 0.4.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) '@astrojs/internal-helpers': 0.11.0 - '@astrojs/markdown-satteri': 0.4.0 + '@astrojs/markdown-satteri': 0.4.1 '@astrojs/telemetry': 3.3.3 '@capsizecss/unpack': 4.0.1 '@clack/prompts': 1.7.0 @@ -3699,7 +3703,7 @@ snapshots: dset: 3.1.4 es-module-lexer: 2.3.2 esbuild: 0.28.2 - find-proc: 0.1.0 + find-proc: 0.2.0 flattie: 1.1.1 fontace: 0.4.1 get-tsconfig: 5.0.0-beta.4 @@ -3718,7 +3722,6 @@ snapshots: package-manager-detector: 1.8.0 piccolore: 0.1.3 picomatch: 4.0.7 - semver: 7.8.5 shiki: 4.4.3 smol-toml: 1.8.0 svgo: 4.1.0 @@ -3728,6 +3731,7 @@ snapshots: ultrahtml: 1.7.0 unifont: 0.7.5 unstorage: 1.17.5 + verkit: 0.4.0 vite: 8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0) vitefu: 1.1.3(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)) xxhash-wasm: 1.1.0 @@ -4039,7 +4043,7 @@ snapshots: fflate@0.8.3: {} - find-proc@0.1.0: {} + find-proc@0.2.0: {} flattie@1.1.1: {} @@ -5358,6 +5362,8 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.4 + verkit@0.4.0: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/site/src/components/BaseHead.astro b/site/src/components/BaseHead.astro index 447bb86..604d5b3 100644 --- a/site/src/components/BaseHead.astro +++ b/site/src/components/BaseHead.astro @@ -11,10 +11,17 @@ interface Props { title: string; description: string; image?: string; + lang?: string; } const canonicalURL = new URL(Astro.url.pathname, Astro.site); -const { title, description, image = "/og.webp" } = Astro.props; +const { title, description, image = "/og.webp", lang = "en" } = Astro.props; + +const normalizedPath = Astro.url.pathname.replace(/^\/(en|es|uk)(?=\/|$)/, ""); +const locales = ["en", "es", "uk"]; +const altHref = (code: string) => + new URL(`/${code}${normalizedPath === "" ? "/" : normalizedPath}`, Astro.site) + .href; const siteURL = new URL("/", Astro.site).href; const publisher = { @@ -106,6 +113,13 @@ const jsonLd = { +{ + locales.map((code) => ( + + )) +} + + {title} @@ -116,7 +130,10 @@ const jsonLd = { - + diff --git a/site/src/components/CameraMatrix.astro b/site/src/components/CameraMatrix.astro index 4c3a3eb..d71f0a3 100644 --- a/site/src/components/CameraMatrix.astro +++ b/site/src/components/CameraMatrix.astro @@ -1,6 +1,20 @@ --- import { Icon } from "astro-icon/components"; -import { CAMERAS, FEATURES, type Feature, type Support } from "../consts"; +import { + CAMERAS, + FEATURE_SUPPORT, + type FeatureSupport, + type Support, +} from "../consts"; +import { getLangFromUrl, useTranslations, useData } from "../i18n"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); +const d = useData(lang); + +const cameraNotes = d("compat.cameraNotes"); +const featureLabels = d("compat.featureLabels"); +const columnLabels = d("compat.columns"); const cell = (s: Support) => s === "yes" @@ -9,16 +23,21 @@ const cell = (s: Support) => ? { icon: "mdi:circle-slice-5", cls: "text-[rgb(var(--color-accent))]" } : { icon: "mdi:close-circle", cls: "text-[rgb(var(--color-danger))]" }; -const columns: { label: string; key: keyof Omit }[] = [ - { label: "Osmo (Nano)", key: "osmoNano" }, - { label: "Action / 360", key: "action" }, - { label: "GoPro", key: "gopro" }, +const columns: { label: string; key: keyof FeatureSupport }[] = [ + { label: columnLabels[0], key: "osmoNano" }, + { label: columnLabels[1], key: "action" }, + { label: columnLabels[2], key: "gopro" }, ]; + +const features = FEATURE_SUPPORT.map((support, i) => ({ + label: featureLabels[i], + support, +})); ---
{ - CAMERAS.map((cam) => ( + CAMERAS.map((cam, i) => (
@@ -26,16 +45,16 @@ const columns: { label: string; key: keyof Omit }[] = [ {cam.tested ? ( - Tested + {t("compat.tested")} ) : ( - Untested + {t("compat.untested")} )}

{cam.name}

-

{cam.notes}

+

{cameraNotes[i]}

)) } @@ -47,7 +66,7 @@ const columns: { label: string; key: keyof Omit }[] = [ - + { columns.map((c) => ( @@ -57,14 +76,14 @@ const columns: { label: string; key: keyof Omit }[] = [ { - FEATURES.map((f, i) => ( + features.map((f, i) => ( {columns.map((c) => ( ))} @@ -77,15 +96,15 @@ const columns: { label: string; key: keyof Omit }[] = [
{ - FEATURES.map((f) => ( + features.map((f) => (

{f.label}

{columns.map((c) => (
{c.label} @@ -102,18 +121,18 @@ const columns: { label: string; key: keyof Omit }[] = [ class="mt-4 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-xs text-[rgb(var(--color-text-muted))]" > Supported {t("compat.legendSupported")} Partial / conditional {t("compat.legendPartial")} Unsupported {t("compat.legendUnsupported")}
diff --git a/site/src/components/HeaderLink.astro b/site/src/components/HeaderLink.astro index 47453c1..19c64c2 100644 --- a/site/src/components/HeaderLink.astro +++ b/site/src/components/HeaderLink.astro @@ -1,5 +1,6 @@ --- import type { HTMLAttributes } from "astro/types"; +import { getRouteFromUrl } from "../i18n"; type Props = HTMLAttributes<"a">; @@ -10,8 +11,10 @@ const normalize = (route: string) => { return route.replace(/\/$/, ""); }; -const current = normalize(Astro.url.pathname); -const target = normalize(new URL(href || "/", Astro.url.origin).pathname); +const current = normalize(getRouteFromUrl(Astro.url)); +const target = normalize( + getRouteFromUrl(new URL(href || "/", Astro.url.origin)), +); const isActive = current === target || (target !== "/" && current.startsWith(target + "/")); diff --git a/site/src/components/LanguageSelector.svelte b/site/src/components/LanguageSelector.svelte new file mode 100644 index 0000000..647d4e8 --- /dev/null +++ b/site/src/components/LanguageSelector.svelte @@ -0,0 +1,84 @@ + + +
+ + + {#if showDropdown} + + +
(showDropdown = false)} + > + {#each Object.entries(languages) as [langCode, langName]} + + {/each} +
+ {/if} +
+ + { + const target = e.target as Element; + if (!target.closest(".relative")) showDropdown = false; + }} +/> diff --git a/site/src/components/Navbar.astro b/site/src/components/Navbar.astro index 0585f47..6620256 100644 --- a/site/src/components/Navbar.astro +++ b/site/src/components/Navbar.astro @@ -1,13 +1,19 @@ --- import HeaderLink from "./HeaderLink.astro"; import ThemeToggle from "./ThemeToggle.svelte"; +import LanguageSelector from "./LanguageSelector.svelte"; import Socials from "./Socials.astro"; import { Icon } from "astro-icon/components"; +import { getLangFromUrl, useTranslations, getLocalizedUrl } from "../i18n"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); +const homeUrl = getLocalizedUrl(lang, "/"); const links = [ - { href: "/", label: "Home", icon: "mdi:home-variant" }, - { href: "/flash", label: "Flasher", icon: "mdi:flash" }, - { href: "/docs", label: "Documentation", icon: "mdi:book-open-variant" }, + { href: getLocalizedUrl(lang, "/"), label: t("nav.home"), icon: "mdi:home-variant" }, + { href: getLocalizedUrl(lang, "/flash"), label: t("nav.flasher"), icon: "mdi:flash" }, + { href: getLocalizedUrl(lang, "/docs"), label: t("nav.docs"), icon: "mdi:book-open-variant" }, ]; --- @@ -17,7 +23,7 @@ const links = [
Feature{t("compat.featureHeader")}{c.label}
{f.label}