Files
SnapOtter/apps/docs/.vitepress/theme/DocsHome.vue
T
SnapOtterandGitHub 9247947704 fix(docs): localize the docs homepage, nav, and sidebar chrome across 20 languages (#547)
Localize the docs homepage (DocsHome.vue), top nav, and sidebar structural labels into all 20 non-English locales; individual tool names stay English by design. Also derive the homepage tool-count chips from the shared catalog via toolSection() so they no longer drift.
2026-07-17 09:18:26 +08:00

183 lines
10 KiB
Vue

<script setup lang="ts">
import { useData } from "vitepress";
import { computed, ref } from "vue";
import { normalizeLocale, t } from "../i18n/ui.mjs";
import { data as sectionCounts } from "./section-counts.data.ts";
const { lang } = useData();
const locale = computed(() => normalizeLocale(lang.value));
const tt = (key: string) => t(locale.value, key);
const command =
"docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest";
const selfLinks = [
{ key: "home.link.quickStart", href: "/guide/getting-started#quick-start" },
{ key: "home.link.configuration", href: "/guide/configuration" },
{ key: "home.link.hardwareSizing", href: "/guide/deployment#hardware-requirements" },
{ key: "home.link.databaseBackups", href: "/guide/database" },
{ key: "home.link.dockerTagsGpu", href: "/guide/docker-tags" },
{ key: "home.link.supportedFormats", href: "/guide/supported-formats" },
];
const entLinks = [
{ key: "home.link.architecture", href: "/guide/architecture" },
{ key: "home.link.securityHardening", href: "/guide/security" },
{ key: "home.link.ssoSamlOidc", href: "/guide/oidc" },
{ key: "home.link.scimProvisioning", href: "/guide/scim" },
{ key: "home.link.usersRolesAudit", href: "/guide/users-roles" },
{ key: "home.link.complianceSbom", href: "/guide/security#compliance-artifacts" },
];
const modalities = [
{ key: "home.mod.image", count: sectionCounts.image, href: "/tools/image/resize" },
{ key: "home.mod.video", count: sectionCounts.video, href: "/tools/video/convert-video" },
{ key: "home.mod.audio", count: sectionCounts.audio, href: "/tools/audio/convert-audio" },
{ key: "home.mod.pdf", count: sectionCounts.pdf, href: "/tools/pdf/merge-pdf" },
{ key: "home.mod.files", count: sectionCounts.files, href: "/tools/files/chart-maker" },
];
const shared = [
{ key: "home.card.restApi", subKey: "home.card.restApiSub", href: "/api/rest" },
{ key: "home.card.changelog", subKey: "home.card.changelogSub", href: "/changelog" },
{ key: "home.card.llmsTxt", subKey: "home.card.llmsTxtSub", href: "/llms.txt" },
];
const copyLabel = ref(t(locale.value, "home.copy"));
function copyCommand() {
navigator.clipboard?.writeText(command);
copyLabel.value = t(locale.value, "home.copied");
setTimeout(() => {
copyLabel.value = t(locale.value, "home.copy");
}, 1500);
}
</script>
<template>
<div class="so-home">
<section class="hero">
<h1 class="hero-title">{{ tt("home.title") }}</h1>
<p class="hero-sub">{{ tt("home.heroSub") }}</p>
<div class="cmd" :title="tt('home.clickToCopy')" @click="copyCommand">
<code>$ {{ command }}</code>
<button class="copy" type="button" :aria-label="tt('home.copyCommandAria')" @click.stop="copyCommand">{{ copyLabel }}</button>
</div>
<p class="hero-meta">
<a href="/guide/getting-started">{{ tt("home.fullInstallGuide") }}</a> ·
<a href="/guide/getting-started#docker-compose">{{ tt("home.gpuComposeSetup") }}</a> ·
<a href="https://demo.snapotter.com">{{ tt("home.tryDemo") }}</a>
</p>
</section>
<section class="doors">
<div class="door self">
<div class="door-head">
<span class="door-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/></svg>
</span>
<h2>{{ tt("home.selfHosting") }}</h2>
</div>
<p class="door-sub">{{ tt("home.selfHostingSub") }}</p>
<ul class="door-links">
<li v-for="l in selfLinks" :key="l.href"><a :href="l.href">{{ tt(l.key) }}</a></li>
</ul>
<a class="door-cta" href="/guide/getting-started">{{ tt("home.startSelfHosting") }}</a>
</div>
<div class="door ent">
<div class="door-head">
<span class="door-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M5 21V7l8-4v18M19 21V11l-6-4"/></svg>
</span>
<h2>{{ tt("home.enterprise") }}</h2>
</div>
<p class="door-sub">{{ tt("home.enterpriseSub") }}</p>
<ul class="door-links">
<li v-for="l in entLinks" :key="l.href"><a :href="l.href">{{ tt(l.key) }}</a></li>
</ul>
<a class="door-cta" href="https://snapotter.com/enterprise">{{ tt("home.evaluate") }}</a>
</div>
</section>
<section class="mod">
<p class="mod-head"><strong>{{ tt("home.modalities") }}</strong> <span>{{ tt("home.browseByType") }}</span></p>
<div class="chips">
<a v-for="m in modalities" :key="m.href" class="chip" :href="m.href">
<span class="chip-label">{{ tt(m.key) }}</span>
<span class="chip-count">{{ m.count }} {{ tt("home.toolsSuffix") }}</span>
</a>
</div>
</section>
<section class="shared">
<a v-for="s in shared" :key="s.href" class="scard" :href="s.href">
<strong>{{ tt(s.key) }}</strong>
<span>{{ tt(s.subKey) }}</span>
</a>
</section>
</div>
</template>
<style scoped>
.so-home { max-width: 1080px; margin: 0 auto; padding: 16px 24px 64px; }
.hero { text-align: center; padding: 48px 16px 28px; }
.hero-title { font-family: var(--so-font-heading); font-size: 42px; font-weight: 800; letter-spacing: -.03em; margin-bottom: 12px; }
.hero-sub { color: var(--vp-c-text-2); font-size: 17px; max-width: 60ch; margin: 0 auto 20px; }
.cmd { display: flex; gap: 14px; align-items: center; max-width: 860px; margin: 0 auto; background: #15100B; border: 1px solid #3A2A1E; border-radius: 10px; padding: 13px 16px; text-align: left; cursor: pointer; transition: border-color .15s, background-color .15s; }
.cmd:hover { border-color: #5A4636; background: #1B140D; }
.cmd code { flex: 1; min-width: 0; font: 600 13px/1.5 var(--vp-font-family-mono); color: #FFD9B0; white-space: nowrap; overflow-x: auto; scrollbar-width: none; }
.cmd code::-webkit-scrollbar { display: none; }
.copy { flex: none; min-width: 62px; text-align: center; font: 600 10.5px/1 var(--vp-font-family-mono); color: #cbb9a6; border: 1px solid #4a3a2c; border-radius: 6px; padding: 8px 11px; background: transparent; cursor: pointer; transition: border-color .15s, color .15s; }
.copy:hover { border-color: #6a5340; color: #FFD9B0; }
.hero-meta { margin-top: 12px; font-size: 13px; color: var(--vp-c-text-2); }
.hero-meta a { color: #A85518; font-weight: 600; }
.doors { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.door { border: 1px solid var(--vp-c-border); border-radius: 16px; padding: 24px; transition: transform .15s, box-shadow .15s; }
.door:hover { transform: translateY(-3px); box-shadow: 0 22px 44px -26px rgba(20,12,4,.5); }
.door.self { background: linear-gradient(180deg, #FFF5ED, #fff); }
.door.ent { background: linear-gradient(165deg, #241A13, #15100B); color: #F0EBE4; border-color: #3A2A1E; }
.door-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.door-icon { width: 40px; height: 40px; border-radius: 11px; display: flex; align-items: center; justify-content: center; flex: none; }
.door.self .door-icon { background: #E07832; color: #fff; }
.door.ent .door-icon { background: #3A2A1E; color: #F09550; }
.door-head h2 { font-family: var(--so-font-heading); font-size: 22px; font-weight: 700; border: 0; padding: 0; margin: 0; }
.door-sub { font-size: 13.5px; margin-bottom: 18px; }
.door.self .door-sub { color: var(--vp-c-text-2); }
.door.ent .door-sub { color: #C9BCAE; }
.door-links { display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; list-style: none; margin: 0 0 20px; padding: 0; }
.door-links li { border-bottom: 1px solid; padding: 0; }
.door.self .door-links li { border-color: #EBDDCC; }
.door.ent .door-links li { border-color: #3A2C20; }
.door-links a { display: block; padding: 10px 2px; font-size: 13.5px; font-weight: 500; text-decoration: none; color: inherit; }
.door.self .door-links a:hover { color: #A85518; }
.door.ent .door-links a:hover { color: #F09550; }
.door-cta { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; font-size: 13.5px; padding: 11px 18px; border-radius: 10px; text-decoration: none; }
.door.self .door-cta { background: #A85518; color: #fff; }
.door.ent .door-cta { background: #F09550; color: #231A12; }
.mod { padding: 28px 0 6px; }
.mod-head { margin-bottom: 13px; }
.mod-head strong { font-family: var(--so-font-heading); font-size: 15px; }
.mod-head span { font-size: 12.5px; color: var(--vp-c-text-2); margin-left: 8px; }
.chips { display: grid; grid-template-columns: repeat(5, 1fr); gap: 11px; }
.chip { display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--vp-c-border); border-radius: 11px; padding: 12px 13px; text-decoration: none; transition: border-color .15s, transform .15s; }
.chip:hover { border-color: #E07832; transform: translateY(-2px); }
.chip-label { font-size: 12.5px; font-weight: 600; color: var(--vp-c-text-1); }
.chip-count { font: 700 12px/1 var(--vp-font-family-mono); color: #A85518; }
.shared { display: grid; grid-template-columns: repeat(3, 1fr); gap: 13px; padding: 18px 0 0; }
.scard { display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--vp-c-border); border-radius: 12px; padding: 14px 15px; text-decoration: none; transition: border-color .15s, transform .15s; }
.scard:hover { border-color: #E07832; transform: translateY(-2px); }
.scard strong { font-family: var(--so-font-heading); font-size: 13.5px; color: var(--vp-c-text-1); }
.scard span { font-size: 11.5px; color: var(--vp-c-text-2); }
:root.dark .chip-label, :root.dark .scard strong { color: var(--vp-c-text-1); }
:root.dark .door.self { background: linear-gradient(180deg, #2A1F16, #221A13); border-color: #3A2C20; color: #F0EBE4; }
:root.dark .door.self .door-links li { border-color: #3A2C20; }
:root.dark .chip-count { color: #F0A766; }
:root.dark .hero-meta a { color: #F0A766; }
:root.dark .door.self .door-sub { color: #C9BCAE; }
:root.dark .door.self .door-links a:hover { color: #F09550; }
:root.dark .door.self .door-cta { background: #A85518; }
@media (max-width: 840px) {
.doors, .shared { grid-template-columns: 1fr; }
.chips { grid-template-columns: repeat(2, 1fr); }
}
</style>