From 193c159db55bb3f138697ebb118f2236892a73db Mon Sep 17 00:00:00 2001 From: Shintaro Jokagi Date: Wed, 14 May 2025 09:41:08 +1200 Subject: [PATCH 1/3] feat(i18n): add i18n support and restructure pages for localization - Configure i18n in astro.config.mjs with English as default locale - Add translation files and i18n utilities - Reorganize pages under [locale] subdirectory - Update components to use translated strings - Add language routing and fallback handling --- astro.config.mjs | 22 +- src/components/BackButton.astro | 13 +- src/components/Button.astro | 22 +- src/components/Circles.astro | 6 +- src/components/Community.astro | 49 +- src/components/Features.astro | 104 ++-- src/components/Footer.astro | 75 +-- src/components/Hero.astro | 58 ++- src/components/MobileMenu.astro | 53 +- src/components/ModsList.tsx | 425 ++++++++-------- src/components/NavBar.astro | 83 ++-- src/components/ReleaseNoteItem.astro | 100 ++-- src/components/SocialMediaStrip.astro | 28 +- src/components/Sponsors.astro | 37 +- src/components/ThemeSwitch.astro | 6 +- src/components/Title.astro | 2 +- src/components/Video.astro | 4 +- src/constants/i18n.ts | 4 + src/constants/index.ts | 5 + src/i18n/en/translation.json | 467 ++++++++++++++++++ src/pages/404.astro | 5 + src/pages/[...locale]/404.astro | 31 ++ src/pages/[...locale]/about.astro | 82 +++ src/pages/{ => [...locale]}/donate.astro | 40 +- src/pages/{ => [...locale]}/download.astro | 123 ++--- src/pages/[...locale]/feed.xml.ts | 174 +++++++ src/pages/[...locale]/index.astro | 41 ++ .../{ => [...locale]}/mods/[...slug].astro | 101 ++-- src/pages/[...locale]/mods/index.astro | 40 ++ src/pages/[...locale]/privacy-policy.astro | 201 ++++++++ .../[...locale]/release-notes/[...slug].astro | 36 ++ .../release-notes/index.astro | 66 +-- src/pages/[...locale]/welcome.astro | 25 + src/pages/{ => [...locale]}/whatsnew.astro | 54 +- src/pages/about.astro | 142 ------ src/pages/feed.xml.ts | 173 ------- src/pages/index.astro | 31 -- src/pages/mods/index.astro | 30 -- src/pages/privacy-policy.astro | 256 ---------- src/pages/release-notes/[...slug].astro | 25 - src/pages/welcome.astro | 12 - src/utils/i18n.ts | 85 ++++ tsconfig.json | 14 +- 43 files changed, 2039 insertions(+), 1311 deletions(-) create mode 100644 src/constants/i18n.ts create mode 100644 src/constants/index.ts create mode 100644 src/i18n/en/translation.json create mode 100644 src/pages/404.astro create mode 100644 src/pages/[...locale]/404.astro create mode 100644 src/pages/[...locale]/about.astro rename src/pages/{ => [...locale]}/donate.astro (53%) rename src/pages/{ => [...locale]}/download.astro (63%) create mode 100644 src/pages/[...locale]/feed.xml.ts create mode 100644 src/pages/[...locale]/index.astro rename src/pages/{ => [...locale]}/mods/[...slug].astro (56%) create mode 100644 src/pages/[...locale]/mods/index.astro create mode 100644 src/pages/[...locale]/privacy-policy.astro create mode 100644 src/pages/[...locale]/release-notes/[...slug].astro rename src/pages/{ => [...locale]}/release-notes/index.astro (75%) create mode 100644 src/pages/[...locale]/welcome.astro rename src/pages/{ => [...locale]}/whatsnew.astro (54%) delete mode 100644 src/pages/about.astro delete mode 100644 src/pages/feed.xml.ts delete mode 100644 src/pages/index.astro delete mode 100644 src/pages/mods/index.astro delete mode 100644 src/pages/privacy-policy.astro delete mode 100644 src/pages/release-notes/[...slug].astro delete mode 100644 src/pages/welcome.astro create mode 100644 src/utils/i18n.ts diff --git a/astro.config.mjs b/astro.config.mjs index 1b368a5..559aa08 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,13 +1,21 @@ +import tailwind from "@astrojs/tailwind"; // @ts-check -import { defineConfig } from 'astro/config' -import tailwind from '@astrojs/tailwind' +import { defineConfig } from "astro/config"; -import preact from '@astrojs/preact' +import preact from "@astrojs/preact"; -import sitemap from '@astrojs/sitemap' +import sitemap from "@astrojs/sitemap"; // https://astro.build/config export default defineConfig({ - integrations: [tailwind(), preact({ compat: true }), sitemap({})], - site: 'https://zen-browser.app', -}) + integrations: [tailwind(), preact({ compat: true }), sitemap({})], + site: "https://zen-browser.app", + i18n: { + defaultLocale: "en", + locales: ["en"], + routing: { + fallbackType: "rewrite", + prefixDefaultLocale: false, + }, + }, +}); diff --git a/src/components/BackButton.astro b/src/components/BackButton.astro index df7c2c0..992880f 100644 --- a/src/components/BackButton.astro +++ b/src/components/BackButton.astro @@ -1,5 +1,14 @@ --- -import { ArrowLeft } from 'lucide-astro' +import { ArrowLeft } from "lucide-astro"; +import { getLocale, getUI } from "~/utils/i18n"; + +const locale = getLocale(Astro); + +const { + routes: { + mods: { slug }, + }, +} = getUI(locale); --- diff --git a/src/components/Button.astro b/src/components/Button.astro index b985cdc..f13d17b 100644 --- a/src/components/Button.astro +++ b/src/components/Button.astro @@ -1,13 +1,17 @@ --- +import { getLocale, getPath } from "~/utils/i18n"; + +const locale = getLocale(Astro); +const getLocalePath = getPath(locale); const { - class: className, - isPrimary, - isAlert, - isBordered, - href, - id, - extra, -} = Astro.props + class: className, + isPrimary, + isAlert, + isBordered, + href, + id, + extra, +} = Astro.props; --- { @@ -15,7 +19,7 @@ const { - Our - Core - Values + + {community.title[0]} + + + {community.title[1]} + + + {community.title[2]} + - We make it not only a priority, but a necessity to ensure that Zen always - strikes the right balance between beauty, performance, and privacy. We are - committed to making Zen the most beautiful, productive, and - privacy-respecting browser out there — without compromising on your - experience. + {community.description}
- Simple yet powerful + {community.lists.simpleYetPowerful.title} - Private and always up-to-date + {community.lists.privateAndAlwaysUpToDate.title}
Community diff --git a/src/components/Features.astro b/src/components/Features.astro index 0da3e3c..c212935 100644 --- a/src/components/Features.astro +++ b/src/components/Features.astro @@ -1,20 +1,33 @@ --- -import Description from '../components/Description.astro' -import { motion } from 'motion/react' -import { getTitleAnimation } from '../animations' +import { motion } from "motion/react"; +import { getTitleAnimation } from "~/animations"; +import Description from "~/components/Description.astro"; -import WorkspacesVideo from '../assets/Workspaces.webm' -import GlanceVideo from '../assets/Glance.webm' -import CompactModeVideo from '../assets/CompactMode.webm' -import SplitViewsVideo from '../assets/SplitViews.webm' +import CompactModeVideo from "~/assets/CompactMode.webm"; +import GlanceVideo from "~/assets/Glance.webm"; +import SplitViewsVideo from "~/assets/SplitViews.webm"; +import WorkspacesVideo from "~/assets/Workspaces.webm"; -import Video from './Video.astro' +import { getLocale, getUI } from "~/utils/i18n"; +import Video from "./Video.astro"; + +const locale = getLocale(Astro); const { - title1 = 'Productivity', - title2 = 'at', - title3 = 'its best', -} = Astro.props + routes: { + index: { features }, + }, +} = getUI(locale); + +const { + title1 = features.title1, + title2 = features.title2, + title3 = features.title3, +} = Astro.props; + +const descriptions = Object.values(features.featureTabs).map( + (tab) => tab.description, +); ---
- Zen Browser is packed with features that help you stay productive and - focused. Browsers should be tools that help you get things done, not - distractions that keep you from your work. + {features.description}
- Workspaces + {features.featureTabs.workspaces.title} - Compact Mode + {features.featureTabs.compactMode.title} - Glance + {features.featureTabs.glance.title} - Split View + {features.featureTabs.splitView.title}
@@ -82,37 +93,45 @@ const { className="feature" data-active="true" > - Workspaces + + {features.featureTabs.workspaces.title} + - Organize your tabs into Workspaces to keep your projects separate - and organized, and switch between them with ease. + {features.featureTabs.workspaces.description} - Compact Mode + + {features.featureTabs.compactMode.title} + - Zen's Compact Mode gives you more screen real estate by hiding the - tab bar when you don't need it, and showing it when you do. + {features.featureTabs.compactMode.description} - Glance + + {features.featureTabs.glance.title} + - Zen's Glance lets you preview tabs without switching to them, so you - can quickly find the page you're looking for. + {features.featureTabs.glance.description} - Split View + + {features.featureTabs.splitView.title} + - Zen's Split View lets you view up to 4 tabs side by side, so you can - compare information or multitask easily. + {features.featureTabs.splitView.description} -
+
+
@@ -160,17 +179,16 @@ const {
diff --git a/src/pages/mods/[...slug].astro b/src/pages/[...locale]/mods/[...slug].astro similarity index 56% rename from src/pages/mods/[...slug].astro rename to src/pages/[...locale]/mods/[...slug].astro index d22e624..5a4d2b0 100644 --- a/src/pages/mods/[...slug].astro +++ b/src/pages/[...locale]/mods/[...slug].astro @@ -1,34 +1,60 @@ --- -import { getAbsoluteLocaleUrl } from 'astro:i18n' -import { getAllMods, getAuthorLink, getLocalizedDate } from '../../mods' -import Layout from '../../layouts/Layout.astro' -import Title from '../../components/Title.astro' -import Description from '../../components/Description.astro' -import Button from '../../components/Button.astro' -import BackButton from '../../components/BackButton.astro' -import { ArrowRight, Info } from 'lucide-astro' +import { ArrowRight, Info } from "lucide-astro"; +import BackButton from "~/components/BackButton.astro"; +import Button from "~/components/Button.astro"; +import Description from "~/components/Description.astro"; +import Layout from "~/layouts/Layout.astro"; +import { getAllMods, getAuthorLink, getLocalizedDate } from "~/mods"; +import { getUI } from "~/utils/i18n"; +import { getLocale, getOtherLocales } from "~/utils/i18n"; export async function getStaticPaths() { - const mods = await getAllMods() - return mods.map((mod) => ({ - params: { slug: mod.id }, - props: { ...mod }, - })) + const mods = await getAllMods(); + return mods.flatMap((mod) => [ + ...getOtherLocales().map((locale) => ({ + params: { + slug: mod.id, + locale: locale, + }, + props: { + ...mod, + locale: locale, + }, + })), + { + params: { + slug: mod.id, + locale: undefined, + }, + props: { + ...mod, + locale: undefined, + }, + }, + ]); } // https://github.com/TeaClientMC/Website/blob/7faacc9f8b2c79c74f711d413b155c84faafc00d/src/pages/news/%5B...slug%5D.astro -const mod = Astro.props +const mod = Astro.props; const dates = { - createdAt: getLocalizedDate(mod.createdAt), - updatedAt: getLocalizedDate(mod.updatedAt), -} + createdAt: getLocalizedDate(mod.createdAt), + updatedAt: getLocalizedDate(mod.updatedAt), +}; + +const locale = getLocale(Astro); + +const { + routes: { + mods: { slug }, + }, +} = getUI(locale); ---
@@ -40,7 +66,7 @@ const dates = {

- You need to have Zen Browser installed to install this theme.{' '} + {slug.alert.description}

@@ -64,18 +90,23 @@ const dates = { />
-

- Created by @{mod.author}v{mod.version} -

-

Creation date • {dates.createdAt}

+

+

{ dates.createdAt !== dates.updatedAt && ( -

- Latest update • {dates.updatedAt} -

+

) } { @@ -86,7 +117,7 @@ const dates = { rel="noopener noreferrer" class="zen-link" > - Visit mod homepage + {slug.visitModHomepage} ) } @@ -98,7 +129,7 @@ const dates = { extra={{ 'zen-theme-id': mod.id }} isPrimary > - Install Theme 🎉 + {slug.installMod}

- diff --git a/src/pages/[...locale]/mods/index.astro b/src/pages/[...locale]/mods/index.astro new file mode 100644 index 0000000..2358642 --- /dev/null +++ b/src/pages/[...locale]/mods/index.astro @@ -0,0 +1,40 @@ +--- +import Description from "~/components/Description.astro"; +import ModsList from "~/components/ModsList"; +import { CONSTANT } from "~/constants"; +import Layout from "~/layouts/Layout.astro"; +import { getAllMods } from "~/mods"; +import { getLocale, getUI } from "~/utils/i18n"; +export { getStaticPaths } from "~/utils/i18n"; + +const locale = getLocale(Astro); + +const { + routes: { mods }, + layout, +} = getUI(locale); + +const allMods = (await getAllMods()) || []; +--- + + +
+
+
+
+ {mods.title} + + {mods.description} + +
+
+
+ + + +
+
diff --git a/src/pages/[...locale]/privacy-policy.astro b/src/pages/[...locale]/privacy-policy.astro new file mode 100644 index 0000000..9d8be57 --- /dev/null +++ b/src/pages/[...locale]/privacy-policy.astro @@ -0,0 +1,201 @@ +--- +import Title from "~/components/Title.astro"; +import Layout from "~/layouts/Layout.astro"; +import { getLocale, getUI } from "~/utils/i18n"; +export { getStaticPaths } from "~/utils/i18n"; + +const locale = getLocale(Astro); + +const { + routes: { privacyPolicy }, + layout, +} = getUI(locale); +--- + + +
+ {privacyPolicy.title} +
{privacyPolicy.lastUpdated}
+ + {privacyPolicy.sections.introduction.title} + +

{privacyPolicy.sections.introduction.body}

+
+ {privacyPolicy.sections.introduction.summary} +
+ + {privacyPolicy.sections.noCollect.title} + +

{privacyPolicy.sections.noCollect.body}

+

+ {privacyPolicy.sections.noTelemetry.title} +

+

{privacyPolicy.sections.noTelemetry.body}

+

{privacyPolicy.sections.noTelemetry.body2}

+

+ {privacyPolicy.sections.noPersonalData.title} +

+

{privacyPolicy.sections.noPersonalData.body}

+

+ {privacyPolicy.sections.noThirdParty.title} +

+

{privacyPolicy.sections.noThirdParty.body}

+

+ {privacyPolicy.sections.externalConnections.title} +

+

{privacyPolicy.sections.externalConnections.body}

+ + {privacyPolicy.sections.localStorage.title} + +

+ {privacyPolicy.sections.browsingData.title} +

+

{privacyPolicy.sections.browsingData.body}

+
    +
  • + {privacyPolicy.sections.cookies.title}: {privacyPolicy.sections.cookies.body} +
  • +
  • + {privacyPolicy.sections.cache.title}: { + privacyPolicy.sections.cache.body + } +
  • +
+

+ {privacyPolicy.sections.settings.title} +

+

{privacyPolicy.sections.settings.body}

+ + {privacyPolicy.sections.sync.title} + +

{privacyPolicy.sections.sync.body}

+ + + {privacyPolicy.sections.addons.title} + +

{privacyPolicy.sections.addons.body}

+ + {privacyPolicy.sections.security.title} + +

{privacyPolicy.sections.security.body}

+
    +
  • {privacyPolicy.sections.security.note}
  • +
+ + {privacyPolicy.sections.control.title} + +

+ {privacyPolicy.sections.control.deletionTitle} +

+

{privacyPolicy.sections.control.deletionBody}

+ + {privacyPolicy.sections.website.title} + +

{privacyPolicy.sections.website.body}

+ +

{privacyPolicy.sections.website.externalLinksBody}

+ + {privacyPolicy.sections.changes.title} + +

+ {privacyPolicy.sections.changes.body} +

+ + {privacyPolicy.sections.otherTelemetry.title} + +

+ {privacyPolicy.sections.otherTelemetry.body} +

+ + + {privacyPolicy.sections.contact.title} + +

+ {privacyPolicy.sections.contact.body} +

+ +
+
+ diff --git a/src/pages/[...locale]/release-notes/[...slug].astro b/src/pages/[...locale]/release-notes/[...slug].astro new file mode 100644 index 0000000..93ae03a --- /dev/null +++ b/src/pages/[...locale]/release-notes/[...slug].astro @@ -0,0 +1,36 @@ +--- +import Layout from "~/layouts/Layout.astro"; +import { releaseNotes } from "~/release-notes"; +import { getStaticPaths as getI18nPaths, getLocale, getUI } from "~/utils/i18n"; + +const locale = getLocale(Astro); + +const { + routes: { + releaseNotes: { slug }, + }, +} = getUI(locale); + +export async function getStaticPaths() { + const i18nPaths = getI18nPaths(); + + return i18nPaths.flatMap(({ params: { locale } }) => [ + ...releaseNotes.map((release: any) => ({ + params: { slug: release.version, locale }, + props: { ...release }, + })), + { + params: { slug: "latest", locale }, + props: { ...releaseNotes[0] }, + }, + ]); +} + +const release = Astro.props; +--- + + +
+ {slug.redirect.replaceAll('{version}', release.version)} +
+
diff --git a/src/pages/release-notes/index.astro b/src/pages/[...locale]/release-notes/index.astro similarity index 75% rename from src/pages/release-notes/index.astro rename to src/pages/[...locale]/release-notes/index.astro index 5017e26..6552b2c 100644 --- a/src/pages/release-notes/index.astro +++ b/src/pages/[...locale]/release-notes/index.astro @@ -1,14 +1,26 @@ --- -import Layout from '../../layouts/Layout.astro' -import ReleaseNoteItem from '../../components/ReleaseNoteItem.astro' -import { releaseNotes, releaseNotesTwilight } from '../../release-notes' -import Description from '../../components/Description.astro' -import Button from '../../components/Button.astro' -import { Modal, ModalBody, ModalHeader } from 'free-astro-components' -import { ArrowUp } from 'lucide-astro' +import { Modal, ModalBody, ModalHeader } from "free-astro-components"; +import { ArrowUp } from "lucide-astro"; +import Button from "~/components/Button.astro"; +import Description from "~/components/Description.astro"; +import ReleaseNoteItem from "~/components/ReleaseNoteItem.astro"; +import Layout from "~/layouts/Layout.astro"; +import { + releaseNotes as releaseNotesData, + releaseNotesTwilight, +} from "~/release-notes"; +import { getLocale, getUI } from "~/utils/i18n"; +export { getStaticPaths } from "~/utils/i18n"; + +const locale = getLocale(Astro); + +const { + routes: { releaseNotes }, + layout, +} = getUI(locale); --- - +
@@ -17,26 +29,24 @@ import { ArrowUp } from 'lucide-astro' class="py-42 flex min-h-screen flex-col justify-center px-10 lg:px-10 xl:px-10 2xl:w-3/5" > Release Notes -

- Stay up to date with the latest changes to Zen Browser! Since the first release till {releaseNotes[0].version}, we've been working hard to make Zen Browser the best it can be. - Thanks everyone for your feedback! ❤️ -

+

{ @@ -45,29 +55,25 @@ import { ArrowUp } from 'lucide-astro' ) : null } - {releaseNotes.map((notes: any) => )} + {releaseNotesData.map((notes: any) => )}
-
-

Choose version

+

{releaseNotes.chooseVersion}

{ - releaseNotes.map((note) => ( + releaseNotesData.map((note) => ( diff --git a/src/pages/about.astro b/src/pages/about.astro deleted file mode 100644 index e91a4c7..0000000 --- a/src/pages/about.astro +++ /dev/null @@ -1,142 +0,0 @@ ---- -import Button from '../components/Button.astro' -import Description from '../components/Description.astro' -import Title from '../components/Title.astro' -import Layout from '../layouts/Layout.astro' ---- - - -
-
- About Us - - We are simply a group of developers and designers who care about your - experience on the web. We believe that the internet should be a place - where you can explore, learn, and connect without worrying about your - data being collected. - - -
-
-
-
Main Team
- - This list shows the main team members who are working hard to bring - you the best browsing experience. - -
-
    -
  • - Mauro B. : Creator, Main Developer -
  • -
  • - Oscar Gonzalez - : Site Reliability Engineer (SRE) and code signing. -
  • -
  • - Jan Heres - : Active contributor and helps with MacOS builds -
  • -
  • - BrhmDev - : Active contributor with great contributions -
  • -
  • - Canoa - : Active contributor, and very active in issue handling and - website management -
  • -
  • - Adam : Branding and design -
  • -
  • - kristijanribaric : Active contributor -
  • -
  • - n7itro - : Active contributor and release notes writer -
  • -
  • - Bryan Galdámez - : Huge contributor on theme functionalities -
  • -
  • - Jafeth Garro : Documentation writer -
  • -
  • - Larvey : AUR maintainer -
  • -
  • - Daniel García - : MacOS certificate and app notarization maintainer -
  • -
-
-
- -
-
Contributors
- - Here are all the wonderful people that decided to contribute to the - project! The first set of images are from the desktop repository, and - the second set is from the website repository. - - Contributors - Contributors (website) -
-
-
-
diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts deleted file mode 100644 index cb10b28..0000000 --- a/src/pages/feed.xml.ts +++ /dev/null @@ -1,173 +0,0 @@ -import rss, { type RSSOptions } from '@astrojs/rss' -import { releaseNotes } from '../release-notes' -import type { ReleaseNote } from '../release-notes' - -/** The default number of entries to include in the RSS feed. */ -const RSS_ENTRY_LIMIT = 20 - -/** - * Handles the GET request for the `feed.xml` endpoint. - * @returns The RSS feed for the Zen Browser release notes. - */ -export function GET(context: any) { - // Just in case the release notes array is empty for whatever reason. - const latestDate = - releaseNotes.length > 0 - ? formatRssDate(releaseNotes[0].date as string) - : new Date() - - const rssData: RSSOptions = { - title: 'Zen Browser Release Notes', - description: 'Release Notes for the Zen Browser', - site: context.url, - items: [], - customData: ` - en - https://www.zen-browser.app/release-notes - Zen Browser © ${new Date().getFullYear()} - Made with ❤️ by the Zen team. - ${pubDate(latestDate)} - - https://www.zen-browser.app/favicon.ico - Zen Browser - https://www.zen-browser.app - - `, - } - - for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) { - rssData.items.push({ - title: `Release notes for version ${releaseNote.version}`, - link: `https://www.zen-browser.app/release-notes/${releaseNote.version}`, - pubDate: formatRssDate(releaseNote.date as string), - description: releaseNote.extra, - content: formatReleaseNote(releaseNote), - }) - } - - return rss(rssData) -} - -/** - * Formats a date string in the format day/month/year. - * - * Note: If release notes change to ISO format, this will need to be updated. - * @param dateStr The date string to format. - * @returns The passed in date string as a Date object. - */ -function formatRssDate(dateStr: string) { - const splitDate = dateStr.split('/') - if (splitDate.length !== 3) { - throw new Error('Invalid date format') - } - - const day = Number(splitDate[0]) - const month = Number(splitDate[1]) - 1 - const year = Number(splitDate[2]) - return new Date(year, month, day) -} - -/** - * Formats the release note entry for use as the content of the RSS feed. - * @param releaseNote The release note to format. - * @returns The formatted release note as a HTML string. - */ -function formatReleaseNote(releaseNote: ReleaseNote) { - let content = `

- If you encounter any issues, please report them on the issues page. - Thanks everyone for your feedback! ❤️ -

` - - if (releaseNote.image) { - content += `Release Image for version ${releaseNote.version}` - } - - if (releaseNote.extra) { - content += `

${releaseNote.extra.replace(/(\n)/g, '
')}

` - } - - content += addReleaseNoteSection( - '⚠️ Breaking changes', - releaseNote.breakingChanges?.map(breakingChangeToReleaseNote), - ) - content += addReleaseNoteSection( - '✓ Fixes', - releaseNote.fixes?.map(fixToReleaseNote), - ) - content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges) - content += addReleaseNoteSection('⭐ Features', releaseNote.features) - - return content -} - -function addReleaseNoteSection(title: string, items?: string[]): string { - if (!items) { - return '' - } - - let content = `

${title}

` - content += `
    ` - for (const item of items) { - if (item && item.length > 0) { - content += `
  • ${item}
  • ` - } - } - content += `
` - return content -} - -function fixToReleaseNote( - fix?: Exclude[number], -) { - if (typeof fix === 'string') { - return fix - } - - if (!fix || !fix.description || fix.description.length === 0) { - return '' - } - - let note = fix.description - if (fix.issue) { - note += ` (#${fix.issue})` - } - return note -} - -function breakingChangeToReleaseNote( - breakingChange?: Exclude[number], -) { - if (typeof breakingChange === 'string') { - return breakingChange - } - - if ( - !breakingChange || - !breakingChange.description || - breakingChange.description.length === 0 - ) { - return '' - } - - return `${breakingChange.description} (Learn more)` -} - -function pubDate(date?: Date) { - date ??= new Date() - - const pieces = date.toString().split(' ') - const offsetTime = pieces[5].match(/[-+]\d{4}/) - const offset = offsetTime ? offsetTime : pieces[5] - const parts = [ - pieces[0] + ',', - pieces[2], - pieces[1], - pieces[3], - pieces[4], - offset, - ] - - return parts.join(' ') -} diff --git a/src/pages/index.astro b/src/pages/index.astro deleted file mode 100644 index a09e6a0..0000000 --- a/src/pages/index.astro +++ /dev/null @@ -1,31 +0,0 @@ ---- -import Layout from '../layouts/Layout.astro' -import Hero from '../components/Hero.astro' -import Community from '../components/Community.astro' -import Features from '../components/Features.astro' -import Sponsors from '../components/Sponsors.astro' ---- - - -
- - - - -
-
- - diff --git a/src/pages/mods/index.astro b/src/pages/mods/index.astro deleted file mode 100644 index 71b2d3e..0000000 --- a/src/pages/mods/index.astro +++ /dev/null @@ -1,30 +0,0 @@ ---- -import Description from '../../components/Description.astro' -import Title from '../../components/Title.astro' -import Layout from '../../layouts/Layout.astro' -import ModsList from '../../components/ModsList' -import { getAllMods } from '../../mods' - -const mods = (await getAllMods()) || [] ---- - - -
-
-
-
- Zen Mods - - Browse our diverse collection of Zen Mods, community-made plugins - and themes for Zen Browser. Discover a theme to match every mood, - and a plugin to fulfill every requirement. Start customizing your - browser experience today! - -
-
-
- - - -
-
diff --git a/src/pages/privacy-policy.astro b/src/pages/privacy-policy.astro deleted file mode 100644 index e54a57b..0000000 --- a/src/pages/privacy-policy.astro +++ /dev/null @@ -1,256 +0,0 @@ ---- -import Title from '../components/Title.astro' -import Layout from '../layouts/Layout.astro' ---- - - -
- Privacy Policy -
Last updated: 2025-02-5
- Introduction -

- Welcome to Zen Browser! Your privacy is our priority. This Privacy Policy - outlines the types of personal information we collect, how we use it, and - the steps we take to protect your data when you use Zen Browser. -

-
- We don't sell data - We don't collect data - We don't track you -
- 1. Information We Do Not Collect -

- Zen Browser is designed with privacy in mind. We do not collect, store, or - share any of your personal data. Here’s what that means: -

-

- 1.1. No Telemetry -

-

We do not collect any telemetry data or crash reports.

-

- Zen Browser has stripped out telemetry built into Mozilla Firefox. We have - removed all telemetry data collection and crash reports. -

-

- 1.2. No Personal Data Collection -

-

- Zen Browser does not collect any personal information such as your IP - address, browsing history, search queries, or form data. -

-

- 1.3. No Third-Party Tracking -

-

- We do not allow third-party trackers or analytics tools to operate within - Zen Browser. Your browsing activity remains entirely private and is not - shared with any third party. Mozilla is not considered a third party as it - is the base of Zen Browser. -

-

- 1.4. External connections made at startup -

-

- Zen Browser may make external connections at startup to check for updates - and ensure the browser is up to date on plugins, addons, check for - connectivity and Geolocation/push notifications services in order to - comply with web standards. We, at Zen, do not collect any data from these - connections, but they may be logged by third-party services or websites - you visit. These connections are necessary for the proper functioning of - the browser and are not used for tracking or profiling purposes. They can - be disabled through the browser flags (about:config). -

- 2. Information Stored Locally on Your Device -

- 2.1. Browsing Data -

-

- Zen Browser stores certain data locally on your device to enhance your - browsing experience. This includes: -

-
    -
  • - Cookies: Cookies are stored locally - on your device and are not shared with Zen Browser or any third party. - You have full control over the management of cookies through the - browser’s settings. -
  • -
  • - Cache and Temporary Files: Zen - Browser may store cache files and other temporary data locally to - improve performance. These files can be cleared at any time through the - browser’s settings. -
  • -
-

- 2.2. Settings and Preferences -

-

- Any customizations, settings, and preferences you make within Zen Browser - are stored locally on your device. We do not have access to or control - over this data. -

- 3. Sync Feature -

- Zen Browser offers a "Sync" feature, which is implemented using - Mozilla Firefox's Sync feature. This feature allows you to synchronize - your bookmarks, history, passwords, and other data across multiple - devices. For this feature to work, your data is encrypted and stored on - Mozilla’s servers and is treated in accordance with their Privacy Policy. - We, at Zen, cannot view any of this data. -

- - 4. Add-ons and "Mods" -

- You can install Add-ons from addons.mozilla.org. Zen Browser periodically - checks for updates to these Add-ons. -
- You can also install "Mods" from zen-browser.app/mods. These Mods - are hosted by our services and follow the same privacy policy our website. - We do not collect any data from these Mods, they are purely static content - that is downloaded to your device. -

- 5. Data Security -

- Although Zen Browser does not collect your data, we are committed to - protecting the information that is stored locally on your device and, if - you use the Sync feature, the encrypted data stored on Mozilla's - servers. We recommend that you use secure passwords, enable device - encryption, and regularly update your software to ensure your data remains - safe. -

-
    -
  • - Note that most of the security measures are taken care by Mozilla - Firefox. -
  • -
- 6. Your Control -

- 6.1. Data Deletion -

-

- You have full control over all data stored locally on your device by Zen - Browser. You can clear your browsing data, cookies, and cache at any time - using the browser’s settings. -

- 7. Our Website and Services -

- Zen Browser's website and services do not use any third-party analytics, - tracking, or CDN services. We do not collect any personal information from - users visiting our website. The website is hosted on Cloudflare but with - analytics and tracking disabled, Cloudflare may collect some analytics - data from HTTP requests in order to provide security and performance - improvements. However, this data is not linked to any personal information - and is not used for tracking purposes. -

- -

- Zen Browser may contain links to external websites or services that are - not owned or operated by us. We are not responsible for the content or - privacy practices of these sites. We recommend that you review the privacy - policies of these sites before providing them with any personal - information. -

- 8. Changes to This Privacy Policy -

- We may update this Privacy Policy from time to time to reflect changes in - our practices or legal requirements. We will notify you of any significant - changes by updating the effective date at the top of this policy. - Continued use of Zen Browser after such changes constitutes your - acceptance of the new terms. -

- 9. Other telemetry done by Mozilla Firefox -

- We try to disable all telemetry data collection in Zen Browser. But, we - may have missed some. Check the below links for more information. -

- - 10. Contact Us -

- If you have any questions or concerns about this Privacy Policy or Zen - Browser, please contact us at: -

- -
-
- diff --git a/src/pages/release-notes/[...slug].astro b/src/pages/release-notes/[...slug].astro deleted file mode 100644 index 2c66131..0000000 --- a/src/pages/release-notes/[...slug].astro +++ /dev/null @@ -1,25 +0,0 @@ ---- -import { releaseNotes } from '../../release-notes' -import Layout from '../../layouts/Layout.astro' - -export async function getStaticPaths() { - return [ - ...releaseNotes.map((release: any) => ({ - params: { slug: release.version }, - props: { ...release }, - })), - { - params: { slug: 'latest' }, - props: { ...releaseNotes[0] }, - }, - ] -} - -const release = Astro.props ---- - - -
- Redirecting to release notes for version {release.version}... -
-
diff --git a/src/pages/welcome.astro b/src/pages/welcome.astro deleted file mode 100644 index bf2c73a..0000000 --- a/src/pages/welcome.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -import Layout from '../layouts/Layout.astro' -import Features from '../components/Features.astro' ---- - - -
- -
-
diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..dc3293d --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,85 @@ +import type { GetStaticPaths } from "astro"; +import { CONSTANT } from "~/constants"; +import UI_EN from "~/i18n/en/translation.json"; + +export type Locale = (typeof locales)[number]; + +export const getPath = (locale?: Locale) => (path: string) => { + if (locale && !path.startsWith(`/${locale}`)) { + return `/${locale}${path.startsWith("/") ? "" : "/"}${path}`; + } + return path; +}; + +export const getLocale = (Astro: any) => { + if (Astro.params.locale) { + return Astro.params.locale as Locale; + } +}; + +export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value); + +const otherLocales = CONSTANT.I18N.LOCALES.filter( + ({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE, +); + +export const getOtherLocales = () => otherLocales; + +export type UI = typeof UI_EN; + +export const ui = { en: UI_EN }; + +export const getUI = (locale?: Locale | string): UI => { + const validLocale = locales.includes(locale as Locale) + ? locale + : CONSTANT.I18N.DEFAULT_LOCALE; + const defaultUI = ui[CONSTANT.I18N.DEFAULT_LOCALE]; + const localeUI = ui[validLocale as Locale]; + + function deepMerge(defaultObj: T, overrideObj: Partial): T { + if (typeof defaultObj !== "object" || defaultObj === null) + return (overrideObj ?? defaultObj) as T; + if (typeof overrideObj !== "object" || overrideObj === null) + return (overrideObj ?? defaultObj) as T; + const result: any = Array.isArray(defaultObj) + ? [...defaultObj] + : { ...defaultObj }; + for (const key in defaultObj) { + if (Object.prototype.hasOwnProperty.call(defaultObj, key)) { + result[key] = deepMerge( + (defaultObj as any)[key], + (overrideObj as any)?.[key], + ); + } + } + for (const key in overrideObj) { + if (!(key in defaultObj)) { + result[key] = (overrideObj as any)[key]; + } + } + return result as T; + } + + return deepMerge(defaultUI, localeUI); +}; + +export const getStaticPaths = (() => { + return [ + { + params: { locale: undefined }, + props: { locale: CONSTANT.I18N.DEFAULT_LOCALE }, + }, + ...CONSTANT.I18N.LOCALES.filter( + ({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE, + ).map(({ value }) => ({ + params: { locale: value }, + props: { + locale: value, + }, + })), + ]; +}) satisfies GetStaticPaths; + +export const getLocales = () => { + return [...locales, ...otherLocales]; +}; diff --git a/tsconfig.json b/tsconfig.json index e43d02f..263cf8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { - "extends": "astro/tsconfigs/strict", - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact", + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } + } } From e068816f18ef654b4e4677093e9b31815afd075c Mon Sep 17 00:00:00 2001 From: Shintaro Jokagi Date: Wed, 14 May 2025 09:43:02 +1200 Subject: [PATCH 2/3] chore(prettier): format fiels with prettier --- astro.config.mjs | 30 +- src/components/BackButton.astro | 14 +- src/components/Button.astro | 22 +- src/components/Circles.astro | 6 +- src/components/Community.astro | 26 +- src/components/Features.astro | 46 +- src/components/Footer.astro | 24 +- src/components/Hero.astro | 40 +- src/components/MobileMenu.astro | 14 +- src/components/ModsList.tsx | 434 ++++---- src/components/NavBar.astro | 30 +- src/components/ReleaseNoteItem.astro | 61 +- src/components/SocialMediaStrip.astro | 28 +- src/components/Sponsors.astro | 24 +- src/components/ThemeSwitch.astro | 6 +- src/components/Title.astro | 2 +- src/components/Video.astro | 4 +- src/constants/i18n.ts | 6 +- src/constants/index.ts | 6 +- src/i18n/en/translation.json | 930 +++++++++--------- src/pages/404.astro | 2 +- src/pages/[...locale]/404.astro | 20 +- src/pages/[...locale]/donate.astro | 20 +- src/pages/[...locale]/download.astro | 56 +- src/pages/[...locale]/feed.xml.ts | 208 ++-- src/pages/[...locale]/index.astro | 18 +- src/pages/[...locale]/mods/[...slug].astro | 82 +- src/pages/[...locale]/mods/index.astro | 24 +- src/pages/[...locale]/privacy-policy.astro | 16 +- .../[...locale]/release-notes/[...slug].astro | 40 +- .../[...locale]/release-notes/index.astro | 32 +- src/pages/[...locale]/welcome.astro | 16 +- src/pages/[...locale]/whatsnew.astro | 42 +- src/utils/githubChecksums.ts | 38 +- src/utils/i18n.ts | 132 +-- tsconfig.json | 18 +- 36 files changed, 1261 insertions(+), 1256 deletions(-) diff --git a/astro.config.mjs b/astro.config.mjs index 559aa08..fda3d82 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,21 +1,21 @@ -import tailwind from "@astrojs/tailwind"; +import tailwind from '@astrojs/tailwind' // @ts-check -import { defineConfig } from "astro/config"; +import { defineConfig } from 'astro/config' -import preact from "@astrojs/preact"; +import preact from '@astrojs/preact' -import sitemap from "@astrojs/sitemap"; +import sitemap from '@astrojs/sitemap' // https://astro.build/config export default defineConfig({ - integrations: [tailwind(), preact({ compat: true }), sitemap({})], - site: "https://zen-browser.app", - i18n: { - defaultLocale: "en", - locales: ["en"], - routing: { - fallbackType: "rewrite", - prefixDefaultLocale: false, - }, - }, -}); + integrations: [tailwind(), preact({ compat: true }), sitemap()], + site: 'https://zen-browser.app', + i18n: { + defaultLocale: 'en', + locales: ['en'], + routing: { + fallbackType: 'rewrite', + prefixDefaultLocale: false, + }, + }, +}) diff --git a/src/components/BackButton.astro b/src/components/BackButton.astro index 992880f..2397d41 100644 --- a/src/components/BackButton.astro +++ b/src/components/BackButton.astro @@ -1,14 +1,14 @@ --- -import { ArrowLeft } from "lucide-astro"; -import { getLocale, getUI } from "~/utils/i18n"; +import { ArrowLeft } from 'lucide-astro' +import { getLocale, getUI } from '~/utils/i18n' -const locale = getLocale(Astro); +const locale = getLocale(Astro) const { - routes: { - mods: { slug }, - }, -} = getUI(locale); + routes: { + mods: { slug }, + }, +} = getUI(locale) --- -
- {mods.pagination.pagination.split("{input}").map((value, index) => { - if (index === 0) { - return ( - - ); - } - return ( - - {value - .replace("{totalPages}", totalPages.toString()) - .replace("{totalItems}", totalItems.toString())} - - ); - })} -
- -
- ); - } + function renderPagination() { + if (totalPages <= 1) return null + return ( +
+ +
+ {mods.pagination.pagination.split('{input}').map((value, index) => { + if (index === 0) { + return ( + + ) + } + return ( + + {value + .replace('{totalPages}', totalPages.toString()) + .replace('{totalItems}', totalItems.toString())} + + ) + })} +
+ +
+ ) + } - return ( -
-
-
- -
+ return ( +
+
+
+ +
-
-
- -
+
+
+ +
-
- -
+
+ +
-
- - -
-
-
+
+ + +
+
+
-
- {paginatedMods.length > 0 ? ( - paginatedMods.map((mod) => ( - -
- {mod.name} -
-
-

- {mod.name}{" "} - - by @{mod.author} - -

-

{mod.description}

-
-
- )) - ) : ( -
-

{mods.noResults}

-

{mods.noResultsDescription}

-
- )} -
+
+ {paginatedMods.length > 0 ? ( + paginatedMods.map((mod) => ( + +
+ {mod.name} +
+
+

+ {mod.name}{' '} + + by @{mod.author} + +

+

{mod.description}

+
+
+ )) + ) : ( +
+

{mods.noResults}

+

{mods.noResultsDescription}

+
+ )} +
- {renderPagination()} -
- ); + {renderPagination()} +
+ ) } diff --git a/src/components/NavBar.astro b/src/components/NavBar.astro index d1ec289..bb39314 100644 --- a/src/components/NavBar.astro +++ b/src/components/NavBar.astro @@ -1,21 +1,21 @@ --- -import { Astronav, Dropdown, DropdownItems, MenuItems } from "astro-navbar"; -import { ArrowRight, ChevronDown, Download, Menu } from "lucide-astro"; -import { motion } from "motion/react"; -import Button from "~/components/Button.astro"; -import { getLocale, getPath, getUI } from "~/utils/i18n"; -import { getTitleAnimation } from "../animations.ts"; -import Logo from "./Logo.astro"; -import MobileMenu from "./MobileMenu.astro"; -import ThemeSwitch from "./ThemeSwitch.astro"; +import { Astronav, Dropdown, DropdownItems, MenuItems } from 'astro-navbar' +import { ArrowRight, ChevronDown, Download, Menu } from 'lucide-astro' +import { motion } from 'motion/react' +import Button from '~/components/Button.astro' +import { getLocale, getPath, getUI } from '~/utils/i18n' +import { getTitleAnimation } from '../animations.ts' +import Logo from './Logo.astro' +import MobileMenu from './MobileMenu.astro' +import ThemeSwitch from './ThemeSwitch.astro' -const locale = getLocale(Astro); -const getLocalePath = getPath(locale); +const locale = getLocale(Astro) +const getLocalePath = getPath(locale) const { - components: { - nav: { brand, menu }, - }, -} = getUI(locale); + components: { + nav: { brand, menu }, + }, +} = getUI(locale) --- diff --git a/src/components/ReleaseNoteItem.astro b/src/components/ReleaseNoteItem.astro index c7d509a..d4de504 100644 --- a/src/components/ReleaseNoteItem.astro +++ b/src/components/ReleaseNoteItem.astro @@ -1,41 +1,41 @@ --- -import { Accordion, AccordionItem } from "free-astro-components"; -import { Info } from "lucide-astro"; +import { Accordion, AccordionItem } from 'free-astro-components' +import { Info } from 'lucide-astro' -import { releaseNotes as releaseNotesData } from "~/release-notes"; -import { getLocale, getPath, getUI } from "~/utils/i18n"; +import { releaseNotes as releaseNotesData } from '~/release-notes' +import { getLocale, getPath, getUI } from '~/utils/i18n' import { - type BreakingChange, - type ReleaseNote, - getReleaseNoteFirefoxVersion, -} from "../release-notes"; -export type Props = ReleaseNote; -const { isTwilight, ...props } = Astro.props; + type BreakingChange, + type ReleaseNote, + getReleaseNoteFirefoxVersion, +} from '../release-notes' +export type Props = ReleaseNote +const { isTwilight, ...props } = Astro.props -const locale = getLocale(Astro); -const getLocalePath = getPath(locale); +const locale = getLocale(Astro) +const getLocalePath = getPath(locale) const { - routes: { - releaseNotes: { - components: { releaseNoteItem }, - }, - }, -} = getUI(locale); + routes: { + releaseNotes: { + components: { releaseNoteItem }, + }, + }, +} = getUI(locale) -let date; +let date if (props.date) { - const [day, month, year] = props.date.split("/"); - date = new Date(Date.parse(`${year}-${month}-${day}`)); + const [day, month, year] = props.date.split('/') + date = new Date(Date.parse(`${year}-${month}-${day}`)) } -const ffVersion = getReleaseNoteFirefoxVersion(props); +const ffVersion = getReleaseNoteFirefoxVersion(props) const currentReleaseIndex = releaseNotesData.findIndex( - (releaseNote: ReleaseNote) => releaseNote.version === props.version, -); -const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1]; -let compareLink = ""; + (releaseNote: ReleaseNote) => releaseNote.version === props.version, +) +const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1] +let compareLink = '' if (prevReleaseNote && !isTwilight) { - compareLink = `https://github.com/zen-browser/desktop/compare/${prevReleaseNote.version}...${props.version}`; + compareLink = `https://github.com/zen-browser/desktop/compare/${prevReleaseNote.version}...${props.version}` } --- @@ -62,7 +62,10 @@ if (prevReleaseNote && !isTwilight) { ) : ( <> - {releaseNoteItem.releaseChanges.replaceAll("{version}", props.version)} + {releaseNoteItem.releaseChanges.replaceAll( + '{version}', + props.version, + )} ) } @@ -152,7 +155,7 @@ if (prevReleaseNote && !isTwilight) { target="_blank" aria-label={releaseNoteItem.viewIssue.replace( '{issue}', - fix.issue + fix.issue, )} > #{fix.issue} diff --git a/src/components/SocialMediaStrip.astro b/src/components/SocialMediaStrip.astro index 188a730..ed22a5b 100644 --- a/src/components/SocialMediaStrip.astro +++ b/src/components/SocialMediaStrip.astro @@ -1,21 +1,21 @@ --- -const { gap = 4 } = Astro.props; +const { gap = 4 } = Astro.props -import { icon, library } from "@fortawesome/fontawesome-svg-core"; +import { icon, library } from '@fortawesome/fontawesome-svg-core' import { - faBluesky, - faGithub, - faMastodon, - faReddit, - faXTwitter, -} from "@fortawesome/free-brands-svg-icons"; + faBluesky, + faGithub, + faMastodon, + faReddit, + faXTwitter, +} from '@fortawesome/free-brands-svg-icons' -library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit); -const Mastodon = icon({ prefix: "fab", iconName: "mastodon" }); -const Bluesky = icon({ prefix: "fab", iconName: "bluesky" }); -const Github = icon({ prefix: "fab", iconName: "github" }); -const XTwitter = icon({ prefix: "fab", iconName: "x-twitter" }); -const Reddit = icon({ prefix: "fab", iconName: "reddit" }); +library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit) +const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' }) +const Bluesky = icon({ prefix: 'fab', iconName: 'bluesky' }) +const Github = icon({ prefix: 'fab', iconName: 'github' }) +const XTwitter = icon({ prefix: 'fab', iconName: 'x-twitter' }) +const Reddit = icon({ prefix: 'fab', iconName: 'reddit' }) ---
    diff --git a/src/components/Sponsors.astro b/src/components/Sponsors.astro index 382690b..cbc6297 100644 --- a/src/components/Sponsors.astro +++ b/src/components/Sponsors.astro @@ -1,21 +1,21 @@ --- -import { motion } from "motion/react"; -import { getTitleAnimation } from "~/animations"; -import Description from "~/components/Description.astro"; -import { getLocale, getUI } from "~/utils/i18n"; +import { motion } from 'motion/react' +import { getTitleAnimation } from '~/animations' +import Description from '~/components/Description.astro' +import { getLocale, getUI } from '~/utils/i18n' -const locale = getLocale(Astro); +const locale = getLocale(Astro) -import tutaLogo from "~/assets/tuta-logo.png"; +import tutaLogo from '~/assets/tuta-logo.png' -import Image from "astro/components/Image.astro"; -const { showSponsors = true } = Astro.props; +import Image from 'astro/components/Image.astro' +const { showSponsors = true } = Astro.props const { - routes: { - index: { sponsors }, - }, -} = getUI(locale); + routes: { + index: { sponsors }, + }, +} = getUI(locale) ---
    diff --git a/src/components/ThemeSwitch.astro b/src/components/ThemeSwitch.astro index cff1bfd..39b4025 100644 --- a/src/components/ThemeSwitch.astro +++ b/src/components/ThemeSwitch.astro @@ -1,10 +1,10 @@ --- interface Props { - label?: string; - className?: string; + label?: string + className?: string } -const { label, className = "" } = Astro.props; +const { label, className = '' } = Astro.props ---