This commit is contained in:
Shintaro Jokagi 2025-05-28 13:46:07 +12:00 committed by GitHub
commit deb5983ca4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 828 additions and 228 deletions

View file

@ -12,7 +12,7 @@ export default defineConfig({
site: 'https://zen-browser.app',
i18n: {
defaultLocale: 'en',
locales: ['en'],
locales: ['en', 'ja'],
routing: {
fallbackType: 'rewrite',
prefixDefaultLocale: false,

View file

@ -9,8 +9,9 @@
"preview": "astro preview --port 3000",
"wrangler": "wrangler",
"astro": "astro",
"lint": "biome lint ./src",
"format": "biome format ./src",
"lint": "biome lint .",
"format": "biome format .",
"check": "biome check .",
"prepare": "husky",
"test": "npx vitest run",
"test:coverage": "npx vitest --coverage",

View file

@ -9,12 +9,20 @@ const {
mods: { slug },
},
} = getUI(locale)
const { href, ...props } = Astro.props
if (!href) {
console.error('BackButton: href is required')
}
---
<button
onclick="window.history.back()"
class="mb-8 flex w-min items-center gap-2"
<a
href={href}
class="mb-8 flex w-fit items-center gap-2"
{...props}
data-testid="back-button"
>
<ArrowLeftIcon class="size-4" />
{slug.back}
</button>
</a>

View file

@ -3,7 +3,7 @@ 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
const { class: className, isPrimary, isAlert, isBordered, href, id, extra, localePath = true, ...props } = Astro.props
---
{
@ -11,7 +11,7 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
<a
id={id}
{...extra}
href={getLocalePath(href)}
href={localePath ? getLocalePath(href) : href}
class:list={[
'transition-bg flex items-center justify-center gap-2 rounded-xl px-6 py-4 transition-transform duration-150 hover:scale-[1.02] active:scale-[0.98]',
className,
@ -23,6 +23,7 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
? 'bg-subtle'
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
]}
{...props}
>
<slot />
</a>
@ -41,6 +42,7 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
? ''
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
]}
{...props}
>
<slot />
</button>

View file

@ -23,15 +23,18 @@ const {
class="relative flex w-full flex-col items-center gap-6 py-12 text-start md:text-center lg:py-36"
>
<Description class="mb-2 text-6xl font-bold">
<motion.span client:load {...getTitleAnimation(0.2)}>
{community.title[0]}
</motion.span>
<motion.span client:load {...getTitleAnimation(0.4)}>
{community.title[1]}
</motion.span>
<motion.span client:load {...getTitleAnimation(0.6)}>
{community.title[2]}
</motion.span>
{community.title.map((title, index) => (
title !== '\n' ? (
<motion.span
client:load
{...getTitleAnimation(0.2 + index * 0.2)}
>
{title}
</motion.span>
) : (
<br class="hidden md:block" />
)
))}
</Description>
<motion.p
client:load

View file

@ -19,7 +19,11 @@ const {
},
} = getUI(locale)
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
interface Props {
titles?: string[]
}
const { titles } = Astro.props
const descriptions = Object.values(features.featureTabs).map((tab) => tab.description)
---
@ -29,15 +33,18 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
class="relative flex w-full flex-col py-12 text-start lg:py-36"
>
<Description class="mb-2 text-4xl sm:text-6xl font-bold">
<motion.span client:load {...getTitleAnimation(0.2)}>
{title1}
</motion.span>
<motion.span client:load {...getTitleAnimation(0.4)}>
{title2}
</motion.span>
<motion.span client:load {...getTitleAnimation(0.6)}>
{title3}
</motion.span>
{(titles || features.titles).map((title, index) => (
title !== '\n' ? (
<motion.span
client:load
{...getTitleAnimation(0.2 + index * 0.2)}
>
{title}
</motion.span>
) : (
<br class="hidden md:block" />
)
))}
</Description>
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2">
{features.description}

View file

@ -39,30 +39,23 @@ const {
<Title
class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl"
>
<motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[0]}
</motion.span>
<motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[1]}
</motion.span>
<br class="hidden md:block" />
<motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[2]}
</motion.span>
<motion.span
client:load
{...getHeroTitleAnimation()}
className="italic text-coral"
>
{hero.title[3]}
</motion.span>
<motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[4]}
</motion.span>
{hero.title.map((title) => (
title.text !== '\n' ? (
<motion.span
client:load
{...getHeroTitleAnimation()}
className={title.highlight ? 'italic text-coral' : ''}
>
{title.text}
</motion.span>
) : (
<br class="hidden md:block" />
)
))}
</Title>
<motion.span client:load {...getHeroTitleAnimation()}>
<Description class="px-12 text-center lg:px-0">
{hero.description[0]}.
{hero.description[0]}
<br class="hidden sm:inline" />
{hero.description[1]}</Description
>

View file

@ -2,8 +2,9 @@ import { icon, library } from '@fortawesome/fontawesome-svg-core'
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'
import { useEffect, useState } from 'preact/hooks'
import { useModsSearch } from '~/hooks/useModsSearch'
import type EN_UI from '~/i18n/en/translation.json'
import type { ZenTheme } from '~/mods'
import { type Locale, getUI } from '~/utils/i18n'
import type { Locale } from '~/utils/i18n'
// Add icons to the library
library.add(faSort, faSortUp, faSortDown)
@ -16,9 +17,19 @@ const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
interface ModsListProps {
allMods: ZenTheme[]
locale: Locale
translations: typeof EN_UI.routes.mods
}
export default function ModsList({ allMods, locale }: ModsListProps) {
export const getPath = (locale?: Locale) => (path: string) => {
if (locale && locale !== 'en' && !path.startsWith(`/${locale}`)) {
return `/${locale}${path.startsWith('/') ? '' : '/'}${path}`
}
return path
}
export default function ModsList({ allMods, locale, translations }: ModsListProps) {
const getLocalePath = getPath(locale)
const {
search,
createdSort,
@ -33,7 +44,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
setPage,
setLimit,
mods: paginatedMods,
// searchParams,
searchParams,
} = useModsSearch(allMods)
const [pageInput, setPageInput] = useState(page.toString())
@ -80,10 +91,6 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
window.scrollTo(0, 0)
}
const {
routes: { mods },
} = getUI(locale)
function renderPagination() {
if (totalPages <= 1) return null
return (
@ -96,7 +103,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
&lt;
</button>
<form className="flex items-center gap-2" onSubmit={handlePageSubmit}>
{mods.pagination.pagination.split('{input}').map((value, index) => {
{translations.pagination.pagination.split('{input}').map((value, index) => {
if (index === 0) {
return (
<input
@ -134,7 +141,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
className="w-full rounded-full border-2 border-dark bg-transparent px-6 py-2 text-lg outline-none"
id="search"
onInput={handleSearch}
placeholder={mods.search}
placeholder={translations.search}
type="text"
value={search}
/>
@ -147,7 +154,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
onClick={toggleCreatedSort}
type="button"
>
{mods.sort.lastCreated}
{translations.sort.lastCreated}
<span
// biome-ignore lint/security/noDangerouslySetInnerHtml: Icons are safe
dangerouslySetInnerHTML={{
@ -163,7 +170,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
onClick={toggleUpdatedSort}
type="button"
>
{mods.sort.lastUpdated}
{translations.sort.lastUpdated}
<span
// biome-ignore lint/security/noDangerouslySetInnerHtml: Icons are safe
dangerouslySetInnerHTML={{
@ -175,10 +182,10 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
<div className="flex items-center gap-2 px-4 py-2">
<label className="font-semibold text-md" htmlFor="limit">
{mods.sort.perPage}
{translations.sort.perPage}
</label>
<select
className="rounded border border-dark bg-transparent px-2 py-1 text-sm [&>option]:text-black"
className="rounded border border-dark px-2 py-1 text-sm dark:bg-paper"
id="limit"
onInput={handleLimitChange}
value={limit}
@ -197,7 +204,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
paginatedMods.map((mod) => (
<a
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
href={`/mods/${mod.id}`}
href={getLocalePath(`/mods/${mod.id}${searchParams ? `?${searchParams}` : ''}`)}
key={mod.id}
>
<div className="relative mb-0 block aspect-[1.85/1] h-48 overflow-hidden rounded-md border-2 border-dark object-cover shadow-md">
@ -218,8 +225,8 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
))
) : (
<div className="col-span-4 grid place-items-center gap-4 place-self-center px-8 text-center">
<h2 className="font-bold text-lg">{mods.noResults}</h2>
<p className="font-thin text-sm">{mods.noResultsDescription}</p>
<h2 className="font-bold text-lg">{translations.noResults}</h2>
<p className="font-thin text-sm">{translations.noResultsDescription}</p>
</div>
)}
</div>

View file

@ -1,6 +1,7 @@
---
import InfoIcon from '~/icons/InfoIcon.astro'
import { getIntlLocale } from '~/constants/i18n'
import { releaseNotes as releaseNotesData } from '~/release-notes'
import { getLocale, getPath, getUI } from '~/utils/i18n'
import { type ReleaseNote, getReleaseNoteFirefoxVersion } from '../release-notes'
@ -177,7 +178,7 @@ generateItems(props.knownIssues, 'known')
}
</div>
<div class="text-xs opacity-80 font-bold">
{date && date.toLocaleDateString('en-US', { dateStyle: 'long' })}
{date && date.toLocaleDateString(getIntlLocale(locale), { dateStyle: 'long' })}
</div>
</div>
{

View file

@ -21,7 +21,7 @@ const {
<section id="sponsors" class:list={['py-12', !showSponsors && 'hidden']}>
<div class="mx-auto flex flex-col text-center">
<motion.span client:load {...getTitleAnimation(0.2)}>
<Description class="mb-2 text-6xl font-bold">Our Sponsors</Description>
<Description class="mb-2 text-6xl font-bold">{sponsors.title}</Description>
</motion.span>
<motion.span client:load {...getTitleAnimation(0.4)}>
<Description set:html={sponsors.description} />

View file

@ -1,4 +1,13 @@
---
import { getLocale, getUI } from '~/utils/i18n'
const locale = getLocale(Astro)
const {
routes: {
download: { buttonCard },
},
} = getUI(locale)
interface Props {
label: string
href: string
@ -42,7 +51,7 @@ const { label, href, checksum } = Astro.props
</svg>
</button>
<span class="absolute -top-10 left-1/2 z-50 hidden min-w-[120px] -translate-x-1/2 select-none whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 opacity-100 shadow transition-opacity duration-150 group-focus-within/checksum:hidden group-hover/checksum:flex group-focus-within/checksum:group-hover/checksum:hidden dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
Show SHA-256
{buttonCard.showChecksum}
</span>
<span class="checksum-tooltip popover absolute -left-14 -top-12 z-50 hidden min-w-[220px] items-center gap-2 whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 opacity-100 shadow transition-opacity duration-150 group-focus-within/checksum:flex dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
<span class="flex-1 truncate font-mono text-xs">{checksum}</span>
@ -50,7 +59,7 @@ const { label, href, checksum } = Astro.props
type="button"
class="copy-btn rounded bg-coral px-2 py-1 text-xs text-white hover:bg-coral/80 data-[twilight='true']:bg-zen-blue data-[twilight='true']:hover:bg-zen-blue/80"
>
Copy
{buttonCard.copy}
</button>
</span>
</span>
@ -62,7 +71,7 @@ const { label, href, checksum } = Astro.props
<span
class="release-type-tag rounded-full bg-coral/10 px-2 py-1 text-xs font-medium text-coral transition-colors duration-200 group-hover:bg-coral/20 data-[twilight='true']:bg-zen-blue/10 data-[twilight='true']:text-zen-blue data-[twilight='true']:group-hover:bg-zen-blue/20"
>
Beta
{buttonCard.beta}
</span>
<div
class="download-arrow-icon text-muted-foreground rounded-xl border border-subtle p-2 transition-colors duration-200 group-hover:border-coral group-hover:text-coral data-[twilight='true']:group-hover:border-zen-blue data-[twilight='true']:group-hover:text-zen-blue"

View file

@ -1,49 +1,61 @@
import { getUI } from '~/utils/i18n'
/**
* Returns the releases object, injecting checksums dynamically.
* @param locale The locale to use for labels
* @param checksums Record<string, string> mapping filenames to SHA-256 hashes
*/
export function getReleasesWithChecksums(checksums: Record<string, string>) {
return {
macos: {
universal: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
label: 'Universal',
checksum: checksums['zen.macos-universal.dmg'],
export function getReleasesWithChecksums(locale: string) {
const {
routes: {
download: {
links: { macos, windows, linux },
},
},
windows: {
x86_64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
label: '64-bit (Recommended)',
checksum: checksums['zen.installer.exe'],
},
arm64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
label: 'ARM64',
checksum: checksums['zen.installer-arm64.exe'],
},
},
linux: {
x86_64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
label: 'Tarball',
checksum: checksums['zen.linux-x86_64.tar.xz'],
} = getUI(locale)
return (checksums: Record<string, string>) => {
return {
macos: {
universal: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
label: macos.universal,
checksum: checksums['zen.macos-universal.dmg'],
},
},
aarch64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
label: 'Tarball',
checksum: checksums['zen.linux-aarch64.tar.xz'],
windows: {
x86_64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
label: windows['64bit'],
checksum: checksums['zen.installer.exe'],
},
arm64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
label: windows.ARM64,
checksum: checksums['zen.installer-arm64.exe'],
},
},
flathub: {
all: {
link: 'https://flathub.org/apps/app.zen_browser.zen',
label: 'Flathub',
linux: {
x86_64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
label: linux.x86_64,
checksum: checksums['zen.linux-x86_64.tar.xz'],
},
},
aarch64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
label: linux.aarch64,
checksum: checksums['zen.linux-aarch64.tar.xz'],
},
},
flathub: {
all: {
link: 'https://flathub.org/apps/app.zen_browser.zen',
label: linux.flathub,
},
},
},
},
}
}
}

View file

@ -1,4 +1,20 @@
export const I18N = {
const UI_EN = (await import('~/i18n/en/translation.json', { with: { type: 'json' } })).default
const UI_JA = (await import('~/i18n/ja/translation.json', { with: { type: 'json' } })).default
export const i18n = {
DEFAULT_LOCALE: 'en',
LOCALES: [{ label: 'English', value: 'en' }],
} as const
LOCALES: [
{ label: 'English', value: 'en', ui: UI_EN, intl: 'en-US' },
{ label: '日本語', value: 'ja', ui: UI_JA, intl: 'ja-JP' },
],
}
/**
* Type definition for UI translations based on the English translation
* @typedef {Object} UIProps
*/
export type UIProps = typeof UI_EN | typeof UI_JA
export const getIntlLocale = (locale: string) => {
return i18n.LOCALES.find((l) => l.value === locale)?.intl
}

View file

@ -1,5 +1,5 @@
import { CHECKSUMS } from './checksum'
import { I18N } from './i18n'
import { i18n as I18N } from './i18n'
export const CONSTANT = {
I18N,

View file

@ -26,6 +26,7 @@ export function useModsSearch(mods: ZenTheme[]) {
// Initialize search params
useEffect(() => {
const params = new URLSearchParams(window.location.search)
console.log(params)
setSearchParams(params)
setState({
search: params.get('q') || '',

View file

@ -3,7 +3,14 @@
"index": {
"title": "Zen Browser",
"hero": {
"title": ["welcome", "to", "a", "calmer", "internet"],
"title": [
{ "text": "welcome ", "highlight": false },
{ "text": "to ", "highlight": false },
{ "text": "\n", "highlight": false },
{ "text": "a ", "highlight": false },
{ "text": "calmer ", "highlight": true },
{ "text": "internet", "highlight": false }
],
"description": [
"Beautifully designed, privacy-focused, and packed with features.",
"We care about your experience, not your data."
@ -14,10 +21,8 @@
}
},
"features": {
"title1": "Productivity",
"title2": "at",
"title3": "its best",
"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.",
"titles": ["Productivity ", "at ", "its best"],
"description": "Zen 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.",
"featureTabs": {
"workspaces": {
"title": "Workspaces",
@ -48,7 +53,7 @@
}
},
"community": {
"title": ["Our", "Core", "Values"],
"title": ["Our ", "Core ", "Values"],
"description": "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.",
"lists": {
"freeAndOpenSource": {
@ -102,9 +107,8 @@
}
},
"releaseNotes": {
"title": "Release notes - Zen Browser",
"topSection": {
"title": "Release Notes",
"title": "Changelog",
"description": "Stay up to date with the latest changes to Zen! Since the <a class=\"zen-link\" href=\"#1.0.0-a.1\">first release</a> till <a class=\"zen-link\" href=\"#{latestVersion}\">{latestVersion}</a>, we've been working hard to make Zen Browser the best it can be. Thanks everyone for your feedback! ❤️"
},
"list": {
@ -256,25 +260,25 @@
}
},
"download": {
"title": "Download - Zen Browser",
"description": "Download Zen Browser for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification.",
"title": "Download Zen",
"description": "Download Zen for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification.",
"twilightInfo": "You're currently in Twilight mode, this means you're downloading the latest experimental features and updates.",
"alertInfo": {
"description": "<strong class='font-medium text-zen-blue'>Twilight Mode:</strong> You're currently in Twilight mode, this means you're downloading the latest experimental features and updates."
},
"platformSelector": {
"title": "Platform Selector",
"description": "Select your platform to download Zen Browser."
"description": "Select your platform to download Zen."
},
"additionalResources": {
"title": "Additional Resources",
"sourceCode": {
"title": "Source Code",
"description": "Explore Zen Browser's source code on GitHub. Contribute to the project or build your own version."
"description": "Explore Zen's source code on GitHub. Contribute to the project or build your own version."
},
"documentation": {
"title": "Documentation",
"description": "Access comprehensive documentation, guides, and tutorials for Zen Browser."
"description": "Access comprehensive documentation, guides, and tutorials for Zen."
}
},
"securityNotice": {
@ -293,6 +297,20 @@
"mac": "Works on both new Apple (M-Series) and older Intel Macs.<br />Requires macOS 11.0 or later.",
"windows": "Works on Windows 10 and Windows 11.<br />Not sure which version to get? Most people should choose the 64-bit installer.",
"linux": "Works with many Linux versions.<br />Pick the download that matches your system."
},
"links": {
"macos": { "universal": "Universal" },
"windows": { "64bit": "64-bit (Recommended)", "ARM64": "ARM64" },
"linux": {
"flathub": "Flathub",
"x86_64": "Tarball",
"aarch64": "Tarball"
}
},
"buttonCard": {
"copy": "Copy",
"showChecksum": "Show SHA-256",
"beta": "Beta"
}
},
"privacyPolicy": {
@ -301,90 +319,90 @@
"sections": {
"introduction": {
"title": "Introduction",
"body": "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.",
"body": "Welcome to Zen! 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.",
"summary": "We don't sell data - We don't collect data - We don't track you"
},
"noCollect": {
"title": "1. Information We Do Not Collect",
"body": "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:"
"body": "Zen is designed with privacy in mind. We do not collect, store, or share any of your personal data. Here's what that means:"
},
"noTelemetry": {
"title": "1.1. No Telemetry",
"body": "We do not collect any telemetry data or crash reports.",
"body2": "Zen Browser has stripped out telemetry built into Mozilla Firefox. We have removed all telemetry data collection and crash reports."
"body2": "Zen has stripped out telemetry built into Mozilla Firefox. We have removed all telemetry data collection and crash reports."
},
"noPersonalData": {
"title": "1.2. No Personal Data Collection",
"body": "Zen Browser does not collect any personal information such as your IP address, browsing history, search queries, or form data."
"body": "Zen does not collect any personal information such as your IP address, browsing history, search queries, or form data."
},
"noThirdParty": {
"title": "1.3. No Third-Party Tracking",
"body": "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."
"body": "We do not allow third-party trackers or analytics tools to operate within Zen. 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."
},
"externalConnections": {
"title": "1.4. External connections made at startup",
"body": "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)."
"body": "Zen 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)."
},
"localStorage": {
"title": "2. Information Stored Locally on Your Device"
},
"browsingData": {
"title": "2.1. Browsing Data",
"body": "Zen Browser stores certain data locally on your device to enhance your browsing experience. This includes:"
"body": "Zen stores certain data locally on your device to enhance your browsing experience. This includes:"
},
"cookies": {
"title": "Cookies",
"body": "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."
"body": "Cookies are stored locally on your device and are not shared with Zen or any third party. You have full control over the management of cookies through the browser's settings."
},
"cache": {
"title": "Cache and Temporary Files",
"body": "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."
"body": "Zen 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."
},
"settings": {
"title": "2.2. Settings and Preferences",
"body": "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."
"body": "Any customizations, settings, and preferences you make within Zen are stored locally on your device. We do not have access to or control over this data."
},
"sync": {
"title": "3. Sync Feature",
"body": "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.",
"body": "Zen 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.",
"link1": "Mozilla Firefox Sync",
"link2": "This is how we store your passwords"
},
"addons": {
"title": "4. Add-ons and \"Mods\"",
"body": "You can install Add-ons from addons.mozilla.org. Zen Browser periodically checks for updates to these Add-ons.\nYou 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."
"body": "You can install Add-ons from addons.mozilla.org. Zen periodically checks for updates to these Add-ons.\nYou 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."
},
"security": {
"title": "5. Data Security",
"body": "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.",
"body": "Although Zen 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": "Note that most of the security measures are taken care by Mozilla Firefox."
},
"control": {
"title": "6. Your Control",
"deletionTitle": "6.1. Data Deletion",
"deletionBody": "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."
"deletionBody": "You have full control over all data stored locally on your device by Zen. You can clear your browsing data, cookies, and cache at any time using the browser's settings."
},
"website": {
"title": "7. Our Website and Services",
"body": "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.",
"body": "Zen'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.",
"externalLinksTitle": "7.1. External links",
"externalLinksBody": "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."
"externalLinksBody": "Zen 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."
},
"changes": {
"title": "8. Changes to This Privacy Policy",
"body": "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."
"body": "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 after such changes constitutes your acceptance of the new terms."
},
"otherTelemetry": {
"title": "9. Other telemetry done by Mozilla Firefox",
"body": "We try to disable all telemetry data collection in Zen Browser. But, we may have missed some. Check the below links for more information.",
"body": "We try to disable all telemetry data collection in Zen. But, we may have missed some. Check the below links for more information.",
"firefoxPrivacyNotice": "Firefox Privacy Notice",
"forMoreInformation": "for more information."
},
"contact": {
"title": "10. Contact Us",
"body": "If you have any questions or concerns about this Privacy Policy or Zen Browser, please contact us at:",
"body": "If you have any questions or concerns about this Privacy Policy or Zen, please contact us at:",
"discord": "Discord: ",
"discordLink": "Zen Browser's Discord",
"discordLink": "Zen's Discord",
"github": "GitHub: ",
"githubLink": "Organization"
}
@ -412,11 +430,11 @@
},
"mods": {
"title": "Zen Mods",
"description": "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!"
"description": "Browse our diverse collection of Zen Mods, community-made plugins and themes for Zen. Discover a theme to match every mood, and a plugin to fulfill every requirement. Start customizing your browser experience today!"
},
"releaseNotes": {
"title": "Release notes - Zen",
"description": "Stay up to date with the latest changes to Zen Browser! Since the first release till {latestVersion}, we've been working hard to make Zen Browser the best it can be. Thanks everyone for your feedback! ❤️"
"description": "Stay up to date with the latest changes to Zen! Since the first release till {latestVersion}, we've been working hard to make Zen the best it can be. Thanks everyone for your feedback! ❤️"
},
"about": {
"title": "About Zen",
@ -428,11 +446,11 @@
},
"download": {
"title": "Download - Zen",
"description": "Download Zen Browser for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification."
"description": "Download Zen for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification."
},
"privacyPolicy": {
"title": "Privacy Policy - Zen",
"description": "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."
"description": "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."
},
"welcome": {
"title": "Welcome!",
@ -477,13 +495,13 @@
"releaseNotesDesc": "Stay up to date with the latest features and improvements.",
"discordDesc": "Join our community on Discord to chat with other Zen users!",
"donate": "Donate ❤️",
"donateDesc": "Support the development of Zen Browser with a donation.",
"donateDesc": "Support the development of Zen with a donation.",
"aboutUs": "About Us 🌟",
"aboutUsDesc": "Learn more about the team behind Zen Browser.",
"aboutUsDesc": "Learn more about the team behind Zen.",
"documentation": "Documentation",
"documentationDesc": "Learn how to use Zen Browser with our documentation.",
"documentationDesc": "Learn how to use Zen with our documentation.",
"github": "GitHub",
"githubDesc": "Contribute to the development of Zen Browser on GitHub.",
"githubDesc": "Contribute to the development of Zen on GitHub.",
"menu": "Menu"
}
}

View file

@ -0,0 +1,514 @@
{
"routes": {
"index": {
"title": "Zenブラウザー",
"hero": {
"title": [
{ "text": "ようこそ", "highlight": false },
{ "text": "\n", "highlight": false },
{ "text": "静かな", "highlight": true },
{ "text": "\n", "highlight": false },
{ "text": "インターネット", "highlight": false },
{ "text": "へ", "highlight": false }
],
"description": [
"美しいデザイン、プライバシー重視、機能満載。",
"私たちはあなたの体験を大切にし、データには関心がありません。"
],
"buttons": {
"beta": "ベータ版が利用可能です!",
"support": "サポートする ❤️"
}
},
"features": {
"titles": ["生産性", "の", "極み"],
"description": "Zenは、生産性と集中力を高める機能が満載です。ブラウザーは作業の妨げではなく、作業を助けるツールであるべきです。",
"featureTabs": {
"workspaces": {
"title": "ワークスペース",
"description": "タブをワークスペースごとに整理し、プロジェクトごとに分けて管理。簡単に切り替え可能です。"
},
"compactMode": {
"title": "コンパクトモード",
"description": "Zenのコンパクトモードは、必要ないときにタブバーを隠し、必要なときに表示して画面を広く使えます。"
},
"glance": {
"title": "覗き見",
"description": "覗き見機能で、よく使うタブを素早く切り替え。履歴をスクロールする必要はありません。"
},
"splitView": {
"title": "画面分割",
"description": "画面分割機能で、2つのタブを並べて表示。比較や切り替えが簡単です。"
}
}
},
"sponsors": {
"title": "スポンサー",
"description": "ご支援いただいているスポンサーの皆様に感謝します。<br />あなたも<a href=\"/donate\" class=\"zen-link\">直接寄付</a>でこの旅に参加できます!",
"sponsors": {
"tuta": {
"name": "Tuta",
"url": "https://tuta.com/"
}
}
},
"community": {
"title": ["私たちの", "コア", "バリュー"],
"description": "Zenは美しさ、パフォーマンス、プライバシーのバランスを最優先にしています。最高の体験を妥協せずに提供します。",
"lists": {
"freeAndOpenSource": {
"title": "無料・オープンソース",
"description": "Zenは無料でオープンソース。誰でも自由に使え、カスタマイズできます。"
},
"simpleYetPowerful": {
"title": "シンプルでパワフル",
"description": "Zenは使いやすく、日常の作業も十分にこなせます。"
},
"privateAndAlwaysUpToDate": {
"title": "プライバシー重視・常に最新",
"description": "Zenはプライバシーを守り、常に最新。無料で使え、カスタマイズも可能です。"
}
},
"images": {
"community": {
"alt": "コミュニティ"
}
}
}
},
"mods": {
"title": "Zen Mods",
"description": "Zen Browser用の多彩なModプラグイン・テーマを探そう。気分やニーズに合ったテーマやプラグインで、ブラウザー体験をカスタマイズ",
"pagination": {
"pagination": "{input} / {totalPages}(全{totalItems}件)"
},
"search": "検索ワードを入力...",
"sort": {
"lastCreated": "新着順",
"lastUpdated": "更新順",
"perPage": "表示件数"
},
"noResults": "結果が見つかりません",
"noResultsDescription": "別のキーワードで検索するか、後でもう一度お試しください。",
"slug": {
"title": "{name} - Zen Mods",
"description": "{name} Modの詳細Zen用",
"alert": {
"description": "このテーマをインストールするにはZenが必要です。",
"button": "今すぐダウンロード!"
},
"createdBy": "作成者:<a href={link} class=\"zen-link font-bold\">{author}</a> • <span class=\"font-bold\">v{version}</span>",
"creationDate": "作成日 • <b>{createdAt}</b>",
"latestUpdate": "最終更新 • <b>{updatedAt}</b>",
"visitModHomepage": "Modのホームページへ",
"installMod": "Modをインストール 🎉",
"uninstallMod": "Modをアンインストール",
"back": "戻る"
}
},
"releaseNotes": {
"topSection": {
"title": "変更履歴",
"description": "Zenの最新情報はこちら<a class=\"zen-link\" href=\"#1.0.0-a.1\">最初のリリース</a>から<a class=\"zen-link\" href=\"#{latestVersion}\">{latestVersion}</a>まで、最高のブラウザーを目指して努力しています。ご意見ありがとうございます!❤️"
},
"list": {
"support": "応援してください!",
"navigateToVersion": "バージョンへ移動..."
},
"itemType": {
"fix": "修正",
"feature": "追加",
"known": "既知",
"break": "重大",
"theme": "テーマ",
"security": "セキュリティ"
},
"backToTop": "トップへ戻る",
"chooseVersion": "バージョンを選択",
"components": {
"releaseNoteItem": {
"twilight": "Twilight",
"twilightChanges": "Twilightの変更点",
"releaseChanges": "v{version}",
"firefoxVersion": "Firefox {version}",
"githubRelease": "GitHubリリース",
"workflowRun": "ワークフロー実行",
"compareChanges": "変更を比較",
"twilightWarning": "TwilightはZen Browserのプレリリース版です。不具合や未完成の機能が含まれる場合があります。",
"reportIssues": " 問題が発生した場合は、<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https://github.com/zen-browser/desktop/issues/\" class=\"zen-link\">issueページ</a>でご報告ください。",
"learnMore": "詳細はこちら",
"viewIssue": "GitHubのIssue番号{issue}を見る"
}
},
"slug": {
"title": "リリースノート",
"redirect": "バージョン{version}のリリースノートにリダイレクト中..."
}
},
"about": {
"title": "Zenについて",
"description": "私たちは、ウェブ体験を大切にする開発者とデザイナーの集まりです。インターネットは、データ収集を心配せずに探索・学習・交流できる場所であるべきだと信じています。",
"littleHelp": "応援しませんか?",
"mainTeam": {
"title": "メインチーム",
"description": "最高のブラウジング体験を提供するために努力しているメンバーです。",
"subTitle": {
"browser": "ブラウザー",
"website": "ウェブサイト・ブランディング"
},
"members": {
"browser": {
"mauro": {
"name": "Mauro B.",
"description": "クリエイター・メイン開発者",
"link": "https://cheff.dev/"
},
"jan": {
"name": "Jan Heres",
"description": "MacOSビルド担当・貢献者",
"link": "https://janheres.eu/"
},
"bryan": {
"name": "Bryan Galdámez",
"description": "テーマ機能の大貢献者",
"link": "https://josuegalre.netlify.app/"
},
"oscar": {
"name": "Oscar Gonzalez",
"description": "SRE・コード署名担当",
"link": false
},
"daniel": {
"name": "Daniel García",
"description": "MacOS証明書・公証管理",
"link": false
},
"brhm": {
"name": "BrhmDev",
"description": "大きな貢献をしている開発者",
"link": "https://github.com/BrhmDev"
},
"kristijanribaric": {
"name": "Kristijan Ribaric",
"description": "スプリットビュー・ワークスペース担当",
"link": "https://github.com/kristijanribaric"
},
"larvey": {
"name": "Larvey",
"description": "AUR管理者",
"link": "https://github.com/LarveyOfficial/"
},
"studio": {
"name": "Studio Movie Girl",
"description": "グラデーションジェネレーターの貢献者",
"link": "https://github.com/neurokitti"
}
},
"website": {
"taroj1205": {
"name": "Shintaro Jokagi",
"description": "コアウェブサイトアーキテクト、リファクタリング・技術強化担当",
"link": "https://github.com/taroj1205"
},
"jace": {
"name": "Jace",
"description": "ウェブサイトデザイン・ブランディング担当",
"link": "https://x.com/JaceThings"
},
"canoa": {
"name": "Canoa",
"description": "活発な貢献者・ウェブサイト管理",
"link": "https://thatcanoa.org/"
},
"adam": {
"name": "Adam",
"description": "ブランディング・デザイン",
"link": "https://cybrneon.xyz/"
},
"n7itro": {
"name": "n7itro",
"description": "リリースノート執筆・貢献者",
"link": "https://github.com/n7itro"
},
"jafeth": {
"name": "Jafeth Garro",
"description": "ドキュメント執筆",
"link": "https://iamjafeth.com/"
}
}
}
},
"contributors": {
"title": "コントリビューター",
"description": "Zenの発展に貢献してくださった皆様です。",
"browser": "ブラウザー",
"website": "ウェブサイト"
}
},
"donate": {
"title": "寄付",
"description": "私たちは少人数の開発チームです。ご支援いただけると幸いです。",
"patreon": {
"title": "Patreon",
"description": "Patreonで毎月のご支援が可能です。ご自身に合った支援レベルをお選びください。",
"button": "Patreonへ"
},
"koFi": {
"title": "Ko-fi",
"description": "Ko-fiで一度きり、または毎月のご支援が可能です。ご希望の金額をお選びください。",
"button": "Ko-fiへ"
}
},
"download": {
"title": "Zenをダウンロードする",
"description": "お使いのプラットフォーム向けにZenをダウンロード。すべてのダウンロードにはSHA256チェックサムが付属しています。",
"twilightInfo": "現在Twilightモードです。最新の実験的機能とアップデートをダウンロードしています。",
"alertInfo": {
"description": "<strong class='font-medium text-zen-blue'>Twilightモード:</strong> 現在Twilightモードで、最新の実験的機能とアップデートをダウンロードしています。"
},
"platformSelector": {
"title": "プラットフォーム選択",
"description": "お使いのプラットフォームを選択してZenをダウンロード。"
},
"additionalResources": {
"title": "追加リソース",
"sourceCode": {
"title": "ソースコード",
"description": "GitHubでZenのソースコードを閲覧・貢献・ビルドできます。"
},
"documentation": {
"title": "ドキュメント",
"description": "Zenの包括的なドキュメント・ガイド・チュートリアル。"
}
},
"securityNotice": {
"title": "検証済み・安全なダウンロード",
"description": "すべてのZenダウンロードは署名・検証済みです。公式サイトまたはGitHubからのダウンロードを推奨します。ダウンロードに問題がある場合やウイルス対策で警告が出た場合は、<a href='https://github.com/zen-browser/desktop/issues/new/choose' class='zen-link ml-1'>ご報告ください</a>。"
},
"platformNames": {
"mac": "macOS",
"windows": "Windows",
"linux": "Linux",
"macDownload": "MacOSダウンロード",
"windowsDownload": "Windowsダウンロード",
"linuxDownload": "Linuxダウンロード"
},
"platformDescriptions": {
"mac": "AppleMシリーズ・Intel両対応。<br />macOS 11.0以降が必要です。",
"windows": "Windows 10・11対応。<br />どちらを選ぶか迷った場合は64ビット版を推奨します。",
"linux": "多くのLinuxディストリビューションで動作。<br />お使いのシステムに合ったものを選択してください。"
},
"links": {
"macos": {
"universal": "ユニバーサル"
},
"windows": {
"64bit": "64-ビット(推奨)",
"ARM64": "ARM64"
},
"linux": {
"flathub": "Flathub",
"x86_64": "Tarball",
"aarch64": "Tarball"
}
},
"buttonCard": {
"copy": "コピー",
"showChecksum": "SHA-256を表示",
"beta": "ベータ"
}
},
"privacyPolicy": {
"title": "プライバシーポリシー",
"lastUpdated": "最終更新: 2025-02-5",
"sections": {
"introduction": {
"title": "はじめに",
"body": "Zenへようこそあなたのプライバシーは最優先です。本ポリシーでは、収集する情報の種類、利用方法、保護手段について説明します。",
"summary": "データ販売なし - データ収集なし - トラッキングなし"
},
"noCollect": {
"title": "1. 収集しない情報",
"body": "Zenはプライバシー重視で設計されています。個人データを収集・保存・共有しません。"
},
"noTelemetry": {
"title": "1.1. テレメトリーなし",
"body": "テレメトリーデータやクラッシュレポートは収集しません。",
"body2": "Mozilla Firefoxに組み込まれているテレメトリーも削除しています。"
},
"noPersonalData": {
"title": "1.2. 個人データの収集なし",
"body": "IPアドレス、閲覧履歴、検索クエリ、フォームデータなどの個人情報は一切収集しません。"
},
"noThirdParty": {
"title": "1.3. サードパーティトラッキングなし",
"body": "サードパーティのトラッカーや解析ツールは一切許可していません。Mozillaはベースであり、サードパーティではありません。"
},
"externalConnections": {
"title": "1.4. 起動時の外部接続",
"body": "Zenは起動時にアップデート確認や接続性・ジオロケーション/プッシュ通知サービスのため外部接続を行う場合があります。これらの接続は機能上必要で、トラッキングやプロファイリング目的ではありません。about:configで無効化可能です。"
},
"localStorage": {
"title": "2. デバイスに保存される情報"
},
"browsingData": {
"title": "2.1. 閲覧データ",
"body": "Zenは体験向上のため、いくつかのデータをローカルに保存します。"
},
"cookies": {
"title": "クッキー",
"body": "クッキーはローカルに保存され、Zenや第三者と共有されません。管理はブラウザー設定から可能です。"
},
"cache": {
"title": "キャッシュ・一時ファイル",
"body": "パフォーマンス向上のためキャッシュや一時データを保存します。設定からいつでも削除可能です。"
},
"settings": {
"title": "2.2. 設定・プリファレンス",
"body": "カスタマイズや設定はすべてローカルに保存され、私たちがアクセスすることはありません。"
},
"sync": {
"title": "3. 同期機能",
"body": "ZenはMozilla FirefoxのSync機能を利用しています。データは暗号化されMozillaのサーバーに保存されます。私たちは内容を閲覧できません。",
"link1": "Mozilla Firefox Sync",
"link2": "パスワードの保存方法"
},
"addons": {
"title": "4. アドオン・Mod",
"body": "MozillaのアドオンやZen Modsをインストール可能です。Zen Modsは当社サービスでホストされ、データ収集はありません。"
},
"security": {
"title": "5. データセキュリティ",
"body": "Zenはデータを収集しませんが、ローカルやMozillaサーバー上のデータ保護に努めています。安全なパスワードやデバイス暗号化、ソフトウェアの定期更新を推奨します。",
"note": "多くのセキュリティ対策はMozilla Firefoxによって提供されています。"
},
"control": {
"title": "6. コントロール",
"deletionTitle": "6.1. データ削除",
"deletionBody": "Zenが保存するすべてのローカルデータは、設定からいつでも削除できます。"
},
"website": {
"title": "7. ウェブサイト・サービス",
"body": "Zenのウェブサイト・サービスはサードパーティの解析やCDNを使用しません。Cloudflareでホストされていますが、解析・トラッキングは無効化されています。",
"externalLinksTitle": "7.1. 外部リンク",
"externalLinksBody": "Zenには外部サイトへのリンクが含まれる場合があります。内容やプライバシーについては各サイトのポリシーをご確認ください。"
},
"changes": {
"title": "8. ポリシーの変更",
"body": "本ポリシーは必要に応じて更新されます。重要な変更時は日付を更新し、継続利用で同意したものとみなします。"
},
"otherTelemetry": {
"title": "9. Mozilla Firefoxによるその他のテレメトリー",
"body": "すべてのテレメトリー無効化に努めていますが、見落としがある場合もあります。詳細は下記リンクをご参照ください。",
"firefoxPrivacyNotice": "Firefoxプライバシー通知",
"forMoreInformation": "詳細はこちら。"
},
"contact": {
"title": "10. お問い合わせ",
"body": "本ポリシーやZenに関するご質問は下記までご連絡ください",
"discord": "Discord: ",
"discordLink": "ZenのDiscord",
"github": "GitHub: ",
"githubLink": "Organization"
}
}
},
"welcome": {
"title": ["ようこそ", "Zenへ", "!"]
},
"whatsNew": {
"title": "{latestVersion.version}の新機能!",
"reportIssue": "問題を報告する",
"joinDiscord": "Discordに参加",
"readFullReleaseNotes": "リリースノート全文を読む"
},
"notFound": {
"title": "ページが見つかりません",
"description": "お探しのページは存在しないか、移動されました。",
"button": "ホームへ戻る"
}
},
"layout": {
"index": {
"title": "Zenブラウザー",
"description": "美しいデザイン、プライバシー重視、機能満載。"
},
"mods": {
"title": "Zen Mods",
"description": "Zen用の多彩なModプラグイン・テーマを探そう。気分やニーズに合ったテーマやプラグインで、ブラウザ体験をカスタマイズ"
},
"releaseNotes": {
"title": "リリースノート - Zen",
"description": "Zenの最新情報はこちら最初のリリースから{latestVersion}まで、最高のブラウザを目指して努力しています。ご意見ありがとうございます!❤️"
},
"about": {
"title": "Zenについて",
"description": "私たちは、ウェブ体験を大切にする開発者とデザイナーの集まりです。インターネットは、データ収集を心配せずに探索・学習・交流できる場所であるべきだと信じています。"
},
"donate": {
"title": "寄付 - Zen",
"description": "私たちは少人数の開発チームです。ご支援いただけると幸いです。"
},
"download": {
"title": "Zenをダウンロードする",
"description": "お使いのプラットフォーム向けにZenをダウンロード。すべてのダウンロードにはSHA256チェックサムが付属しています。"
},
"privacyPolicy": {
"title": "プライバシーポリシー - Zen",
"description": "あなたのプライバシーは最優先です。本ポリシーでは、収集する情報の種類、利用方法、保護手段について説明します。"
},
"welcome": {
"title": "ようこそ!",
"description": "Zenへようこそ"
},
"whatsNew": {
"title": "{latestVersion.version}の新機能!"
}
},
"components": {
"footer": {
"title": "Zenブラウザー",
"description": "美しいデザイン、プライバシー重視、機能満載。私たちはあなたのデータではなく、体験を大切にします。",
"download": "ダウンロード",
"followUs": "フォローする",
"aboutUs": "私たちについて",
"teamAndContributors": "チーム・コントリビューター",
"privacyPolicy": "プライバシーポリシー",
"getStarted": "はじめに",
"documentation": "ドキュメント",
"zenMods": "Zen Mods",
"releaseNotes": "リリースノート",
"getHelp": "ヘルプ",
"discord": "Discord",
"uptimeStatus": "稼働状況",
"reportAnIssue": "問題を報告",
"twilight": "Twilight",
"madeWith": "<span aria-label='love'>❤️</span>と共に<a href='{link}' class='zen-link inline-block font-bold'>Zenチーム</a>が作りました"
},
"nav": {
"brand": "Zenブラウザー",
"menu": {
"gettingStarted": "はじめに",
"usefulLinks": "便利なリンク",
"mods": "Mods",
"download": "ダウンロード",
"discord": "Discord",
"releaseNotes": "リリースノート",
"zenMods": "Zen Mods",
"tryZenMods": "Zen Modsを試す",
"zenModsDesc": "Zen Modsでブラウザ体験をカスタマイズ。",
"releaseNotesDesc": "最新機能・改善情報はこちら。",
"discordDesc": "Discordコミュニティで他のZenユーザーと交流",
"donate": "寄付 ❤️",
"donateDesc": "Zen開発を寄付で応援。",
"aboutUs": "私たちについて 🌟",
"aboutUsDesc": "Zenのチームについて知る。",
"documentation": "ドキュメント",
"documentationDesc": "ドキュメントでZenの使い方を学ぶ。",
"github": "GitHub",
"githubDesc": "GitHubでZen開発に貢献。",
"menu": "メニュー"
}
}
}
}

View file

@ -27,7 +27,7 @@ const appleIcon = icon({ prefix: 'fab', iconName: 'apple' })
const githubIcon = icon({ prefix: 'fab', iconName: 'github' })
const checksums = await getChecksums()
const releases = getReleasesWithChecksums(checksums)
const releases = getReleasesWithChecksums(locale)(checksums)
const platformNames = download.platformNames
const platformDescriptions = download.platformDescriptions

View file

@ -6,7 +6,7 @@ import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import InfoIcon from '~/icons/InfoIcon.astro'
import Layout from '~/layouts/Layout.astro'
import { getAllMods, getAuthorLink, getLocalizedDate } from '~/mods'
import { getUI } from '~/utils/i18n'
import { getPath, getUI } from '~/utils/i18n'
import { getLocale, getOtherLocales } from '~/utils/i18n'
export async function getStaticPaths() {
@ -46,6 +46,8 @@ const dates = {
const locale = getLocale(Astro as { params: { locale?: string } })
const getLocalePath = getPath(locale)
const {
routes: {
mods: { slug },
@ -79,7 +81,7 @@ const {
<ArrowRightIcon class="size-4" />
</Button>
</div>
<BackButton />
<BackButton id="back-button" href={getLocalePath('/mods')} />
<div>
<Description class="text-6xl font-bold">{mod.name}</Description>
<Description>{mod.description}</Description>
@ -149,3 +151,11 @@ const {
<div></div>
</main></Layout
>
<script>
const backButton = document.getElementById('back-button') as HTMLAnchorElement
const search = window.location.search
const searchParams = new URLSearchParams(search.length > 0 ? search : "created=desc")
const backLink = `${backButton.href}?${searchParams.toString()}`
backButton.href = backLink
</script>

View file

@ -4,7 +4,7 @@ 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'
import { getLocale, getPath, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
const locale = getLocale(Astro)
@ -30,6 +30,7 @@ const allMods = (await getAllMods()) || []
<ModsList
allMods={allMods}
locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE}
translations={mods}
client:load
/>
</main>

View file

@ -1,9 +1,10 @@
---
import Layout from '~/layouts/Layout.astro'
import { releaseNotes } from '~/release-notes'
import { getStaticPaths as getI18nPaths, getLocale, getUI } from '~/utils/i18n'
import { getStaticPaths as getI18nPaths, getLocale, getPath, getUI } from '~/utils/i18n'
const locale = getLocale(Astro)
const getLocalePath = getPath(locale)
const {
routes: {
@ -29,7 +30,7 @@ export async function getStaticPaths() {
const release = Astro.props
---
<Layout title={slug.title} redirect={`/release-notes#${release.version}`}>
<Layout title={slug.title} redirect={getLocalePath(`/release-notes#${release.version}`)}>
<main class="flex flex-col items-center pb-52 pt-36">
{slug.redirect.replaceAll('{version}', release.version)}
</main>

View file

@ -25,7 +25,7 @@ const {
id="release-notes"
class="py-42 flex min-h-screen gap-8 w-full flex-col justify-center"
>
<Description class="mt-48 text-6xl font-bold">Changelog</Description>
<Description class="mt-48 text-6xl font-bold">{releaseNotes.topSection.title}</Description>
<p
class="text-base opacity-55"
set:html={releaseNotes.topSection.description.replaceAll(
@ -39,7 +39,7 @@ const {
<Button class="flex" isPrimary href="/donate">
{releaseNotes.list.support}
</Button>
<Button id="navigate-to-version" href="#" class="flex">
<Button id="navigate-to-version" href="#" class="flex" localePath={false}>
{releaseNotes.list.navigateToVersion}
</Button>
</div>
@ -52,7 +52,7 @@ const {
{releaseNotesData.map((notes: any) => <ReleaseNoteItem {...notes} />)}
</div>
</main>
<Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8">
<Button id="scroll-top" isPrimary class="fixed bottom-8 right-8" onclick="window.scrollTo(0, 0)">
<p class="hidden items-center gap-2 sm:flex">
{releaseNotes.backToTop}
<ArrowUpIcon aria-hidden="true" class="size-4" />

View file

@ -15,9 +15,7 @@ const {
<Layout title={layout.welcome.title} description={layout.welcome.description}>
<main class="container">
<Features
title1={welcome.title[0]}
title2={welcome.title[1]}
title3={welcome.title[2]}
titles={welcome.title}
/>
</main>
</Layout>

View file

@ -10,7 +10,7 @@ describe('getReleasesWithChecksums', () => {
'zen.linux-x86_64.tar.xz': 'linx86sum',
'zen.linux-aarch64.tar.xz': 'linaarchsum',
}
const releases = getReleasesWithChecksums(checksums)
const releases = getReleasesWithChecksums('en')(checksums)
expect(releases.macos.universal.checksum).toBe('macsum')
expect(releases.windows.x86_64.checksum).toBe('winsum')
expect(releases.windows.arm64.checksum).toBe('winarmsum')

View file

@ -75,9 +75,10 @@ test.describe('Download page platform detection and tab switching', () => {
})
test.describe('Download page download links', () => {
const releases = getReleasesWithChecksums(CONSTANT.CHECKSUMS)
const releases = getReleasesWithChecksums('en')(CONSTANT.CHECKSUMS)
function getPlatformLinks(releases: ReturnType<typeof getReleasesWithChecksums>) {
type Releases = ReturnType<ReturnType<typeof getReleasesWithChecksums>>
function getPlatformLinks(releases: Releases) {
return {
mac: [releases.macos.universal],
windows: [releases.windows.x86_64, releases.windows.arm64],

View file

@ -5,7 +5,8 @@ test('clicking back button navigates to previous page', async ({ page }) => {
const currentUrl = page.url()
const modCards = await page.locator('.mod-card').all()
await modCards[0].click()
await page.getByRole('button', { name: 'Back' }).click()
await page.waitForURL('/mods/*')
await page.getByTestId('back-button').click()
await page.waitForURL(currentUrl)
expect(page.url()).toStrictEqual(currentUrl)
})

View file

@ -1,6 +1,6 @@
import type { GetStaticPaths } from 'astro'
import { CONSTANT } from '~/constants'
import UI_EN from '~/i18n/en/translation.json'
import type { UIProps } from '~/constants/i18n'
/**
* Represents the available locales in the application
@ -44,7 +44,9 @@ export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value)
* List of locales excluding the default locale
* @type {Locale[]}
*/
const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE)
const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(
({ value }) => value,
)
/**
* Retrieves locales other than the default locale
@ -52,78 +54,72 @@ const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONST
*/
export const getOtherLocales = () => otherLocales
/**
* Type definition for UI translations based on the English translation
* @typedef {Object} UI
*/
export type UI = typeof UI_EN
/**
* Mapping of locales to their UI translation objects
* @type {Object.<Locale, UI>}
*/
export const ui = { en: UI_EN }
/**
* Retrieves UI translations for a given locale, merging with default translations
* @param {Locale} [locale] - The target locale for translations
* @returns {UI} Merged UI translations
*/
export const getUI = (locale?: Locale | string): UI => {
export const getUI = (locale?: Locale | string): UIProps => {
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]
const defaultUI = CONSTANT.I18N.LOCALES.find(({ value }) => value === CONSTANT.I18N.DEFAULT_LOCALE)?.ui
const localeUI = CONSTANT.I18N.LOCALES.find(({ value }) => value === validLocale)?.ui
/**
* Recursively merges two objects, with the override object taking precedence
* @template T
* @param {T} defaultObj - The default object to merge from
* @param {Partial<T>} overrideObj - The object to merge over the default
* @returns {T} The deeply merged object
*/
function deepMerge<T extends object>(defaultObj: T, overrideObj: Partial<T>): T {
// Handle non-object cases
if (typeof defaultObj !== 'object' || defaultObj === null) {
return (overrideObj ?? defaultObj) as T
}
if (typeof overrideObj !== 'object' || overrideObj === null) {
return (overrideObj ?? defaultObj) as T
}
// Create a new object or array based on the default object's type
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
// Merge properties from the default object
for (const key of Object.keys(defaultObj) as Array<keyof T>) {
const defaultValue = defaultObj[key]
const overrideValue = overrideObj[key]
// Recursively merge nested objects
if (
defaultValue !== null &&
overrideValue !== null &&
typeof defaultValue === 'object' &&
typeof overrideValue === 'object'
// Helper to recursively check for missing keys
function checkMismatch(defaultObj: UIProps, localeObj: Partial<UIProps> = {}, path: string[] = []): void {
if (typeof defaultObj !== 'object' || defaultObj === null) return
for (const key of Object.keys(defaultObj) as (keyof UIProps)[]) {
if (!(key in localeObj)) {
console.error(
`[i18n] Missing translation key: ${[...path, key as string].join('.')} in locale '\x1b[1m${validLocale}\x1b[0m'. See src/i18n/${validLocale}/translation.json`,
)
} else if (
typeof defaultObj[key] === 'object' &&
defaultObj[key] !== null &&
typeof localeObj[key] === 'object' &&
localeObj[key] !== null
) {
// Type assertion to handle nested merging
;(result as Record<keyof T, unknown>)[key] = deepMerge(defaultValue as object, overrideValue as Partial<object>)
} else if (overrideValue !== undefined) {
// Override with the new value if it exists
;(result as Record<keyof T, unknown>)[key] = overrideValue
// @ts-expect-error: recursive structure
checkMismatch(defaultObj[key], localeObj[key], [...path, key as string])
}
}
// Add any new properties from overrideObj
for (const key of Object.keys(overrideObj) as Array<keyof T>) {
if (!(key in defaultObj)) {
;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
}
}
return result as T
}
return deepMerge(defaultUI, localeUI)
// Deep merge: localeUI overrides defaultUI, fallback to defaultUI for missing keys
function deepMerge(defaultObj: UIProps, localeObj: Partial<UIProps> = {}): UIProps {
if (typeof defaultObj !== 'object' || defaultObj === null) return defaultObj
if (typeof localeObj !== 'object' || localeObj === null) return defaultObj
const result: any = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
for (const key of Object.keys(defaultObj) as (keyof UIProps)[]) {
if (key in localeObj) {
if (
typeof defaultObj[key] === 'object' &&
defaultObj[key] !== null &&
typeof localeObj[key] === 'object' &&
localeObj[key] !== null
) {
// @ts-expect-error: recursive structure
result[key] = deepMerge(defaultObj[key], localeObj[key])
} else {
result[key] = localeObj[key]
}
} else {
result[key] = defaultObj[key]
}
}
return result
}
if (!defaultUI) {
throw new Error('Default UI translation is missing!')
}
if (localeUI && validLocale !== CONSTANT.I18N.DEFAULT_LOCALE) {
checkMismatch(defaultUI, localeUI)
return deepMerge(defaultUI, localeUI) as UIProps
}
// If localeUI is undefined or locale is default, just return defaultUI
return defaultUI
}
/**