feat(i18n): 21-language pipeline, landing/docs/API wiring, landing+API translations

Shared Claude Code translation pipeline (scripts/i18n, no API key) plus Astro/VitePress/Scalar i18n wiring. Landing and API reference translated into all 20 languages; docs i18n wiring + English source anchors. The translated docs markdown (apps/docs/<locale>/**, 3,620 files) follows in a companion PR because it exceeds GitHub's per-PR CI file limit.
This commit is contained in:
SnapOtter
2026-07-11 13:01:55 +08:00
committed by GitHub
parent 2e91368816
commit 00b651c9f8
353 changed files with 564607 additions and 2594 deletions
+21 -11
View File
@@ -1,5 +1,14 @@
<script setup lang="ts">
const command = "docker run -d --name SnapOtter -p 1349:1349 -v SnapOtter-data:/data snapotter/snapotter:latest";
import { useData } from "vitepress";
import { computed, ref } from "vue";
import { normalizeLocale, t } from "../i18n/ui.mjs";
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 = [
{ label: "Quick start", href: "/guide/getting-started#quick-start" },
@@ -30,19 +39,20 @@ const shared = [
{ label: "llms.txt", sub: "AI-friendly docs", href: "/llms.txt" },
];
import { ref } from "vue";
const copyLabel = ref("Copy");
const copyLabel = ref(t(locale.value, "home.copy"));
function copyCommand() {
navigator.clipboard?.writeText(command);
copyLabel.value = "Copied!";
setTimeout(() => { copyLabel.value = "Copy"; }, 1500);
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">SnapOtter Documentation</h1>
<h1 class="hero-title">{{ tt("home.title") }}</h1>
<p class="hero-sub">
Install, operate, and build on your self-hosted file-processing infrastructure. Get running
in one command:
@@ -64,13 +74,13 @@ function copyCommand() {
<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>Self-hosting</h2>
<h2>{{ tt("home.selfHosting") }}</h2>
</div>
<p class="door-sub">Get SnapOtter running and keep it healthy.</p>
<ul class="door-links">
<li v-for="l in selfLinks" :key="l.href"><a :href="l.href">{{ l.label }}</a></li>
</ul>
<a class="door-cta" href="/guide/getting-started">Start self-hosting </a>
<a class="door-cta" href="/guide/getting-started">{{ tt("home.startSelfHosting") }}</a>
</div>
<div class="door ent">
@@ -78,18 +88,18 @@ function copyCommand() {
<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>Enterprise</h2>
<h2>{{ tt("home.enterprise") }}</h2>
</div>
<p class="door-sub">Evaluate, secure &amp; govern your deployment.</p>
<ul class="door-links">
<li v-for="l in entLinks" :key="l.href"><a :href="l.href">{{ l.label }}</a></li>
</ul>
<a class="door-cta" href="https://snapotter.com/enterprise">See enterprise features </a>
<a class="door-cta" href="https://snapotter.com/enterprise">{{ tt("home.evaluate") }}</a>
</div>
</section>
<section class="mod">
<p class="mod-head"><strong>200+ tools across 5 modalities</strong> <span>browse the full reference by type</span></p>
<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">{{ m.label }}</span>
+12 -2
View File
@@ -1,14 +1,24 @@
<script setup lang="ts">
import { useData } from "vitepress";
import { computed } from "vue";
import { normalizeLocale, t } from "../i18n/ui.mjs";
const { lang } = useData();
const locale = computed(() => normalizeLocale(lang.value));
const label = computed(() => t(locale.value, "fund.label"));
</script>
<template>
<a
href="https://github.com/sponsors/snapotter-hq"
target="_blank"
rel="noopener"
class="fund-btn"
title="Fund Development"
:title="label"
>
<svg class="fund-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
<span>Fund Development</span>
<span>{{ label }}</span>
</a>
</template>
+8 -2
View File
@@ -1,5 +1,11 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useData } from "vitepress";
import { computed, onMounted, ref } from "vue";
import { normalizeLocale, t } from "../i18n/ui.mjs";
const { lang } = useData();
const locale = computed(() => normalizeLocale(lang.value));
const starLabel = computed(() => t(locale.value, "github.star"));
const stars = ref<string | null>(null);
const repo = "https://github.com/snapotter-hq/snapotter";
@@ -24,7 +30,7 @@ onMounted(async () => {
<svg class="github-icon" width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
</svg>
<span class="github-btn-label">Star</span>
<span class="github-btn-label">{{ starLabel }}</span>
</a>
<a
v-if="stars !== null"
+12 -3
View File
@@ -1,14 +1,23 @@
<script setup>
import { useData } from "vitepress";
import VPSwitchAppearance from "vitepress/dist/client/theme-default/components/VPSwitchAppearance.vue";
import DefaultTheme from "vitepress/theme";
import { computed } from "vue";
import { normalizeLocale, t } from "../i18n/ui.mjs";
import FundButton from "./FundButton.vue";
import GitHubStars from "./GitHubStars.vue";
import MachineTranslationBanner from "./MachineTranslationBanner.vue";
const { Layout } = DefaultTheme;
const { lang } = useData();
const locale = computed(() => normalizeLocale(lang.value));
</script>
<template>
<Layout>
<template #layout-top>
<MachineTranslationBanner />
</template>
<template #nav-bar-content-after>
<div class="nav-bar-right">
<VPSwitchAppearance />
@@ -19,9 +28,9 @@ const { Layout } = DefaultTheme;
<template #not-found>
<div class="not-found-page">
<span class="not-found-otter">🦦</span>
<h1 class="not-found-heading">Hello from the otter side!</h1>
<p class="not-found-text">This page swam away. Let's get you back on track.</p>
<a href="/" class="not-found-link">Back to docs</a>
<h1 class="not-found-heading">{{ t(locale, "notFound.heading") }}</h1>
<p class="not-found-text">{{ t(locale, "notFound.text") }}</p>
<a href="/" class="not-found-link">{{ t(locale, "notFound.link") }}</a>
</div>
</template>
</Layout>
@@ -0,0 +1,64 @@
<script setup lang="ts">
import { useData } from "vitepress";
import { computed, ref } from "vue";
import { normalizeLocale, t } from "../i18n/ui.mjs";
const { lang } = useData();
// Guard both existence AND callability: VitePress SSR defines a `localStorage`
// global that is not a real Storage, so `getItem` may not be a function there.
function readDismissed() {
try {
return (
typeof localStorage?.getItem === "function" && localStorage.getItem("so-mt-banner") === "1"
);
} catch {
return false;
}
}
const dismissed = ref(readDismissed());
const locale = computed(() => normalizeLocale(lang.value));
const show = computed(() => locale.value !== "en" && !dismissed.value);
const text = computed(() => t(locale.value, "banner.text"));
const cta = computed(() => t(locale.value, "banner.cta"));
function dismiss() {
dismissed.value = true;
try {
localStorage.setItem("so-mt-banner", "1");
} catch {
// ignore storage failures (private mode)
}
}
</script>
<template>
<div v-if="show" class="mt-banner" role="note">
<span>{{ text }}</span>
<a href="https://github.com/snapotter-hq/snapotter/blob/main/apps/docs/guide/translations.md">{{ cta }}</a>
<button type="button" class="mt-dismiss" aria-label="Dismiss" @click="dismiss">×</button>
</div>
</template>
<style scoped>
.mt-banner {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
font-size: 13px;
background: #FFF5ED;
border-bottom: 1px solid #EBDDCC;
color: #7A4A1E;
}
.mt-banner a { color: #A85518; font-weight: 600; }
.mt-dismiss {
margin-inline-start: auto;
border: 0;
background: transparent;
font-size: 18px;
line-height: 1;
cursor: pointer;
color: inherit;
}
:root.dark .mt-banner { background: #241A13; border-color: #3A2C20; color: #E0C6A8; }
</style>