Feat(footer): fold the product links into a four-column footer

The Raycast / curl / Lite badges sat in an unlabelled image row of their
own under the Advanced Tools. The footer now has four columns aligned
with that card grid — brand, Project, Docs & Support, Clients — so the
product entries get a heading and show on every route; the curl
commands keep their dialog. Docs links share one locale-aware helper.
The footer never repeats on-page content, and the fixed footer height
in style.css goes, as the columns size themselves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
jason5ng32andClaude Fable 5.1 committed 2026-10-20 18:45:09 +08:00
1 parent d28fe239ae
commit 7a608cb8ce
16 files changed
+343 -210

No files matched your search

+5
View File
@@ -179,6 +179,11 @@ Every "business state → color" mapping goes through `use-status-tone.js`
no section is highlighted off Home.
- The link row collapses to the hamburger + Sheet when `use-nav-overflow.js`
measures it doesn't fit (no breakpoint); `isMobile` only drives phone behaviour.
- **Footer** — four columns on the Advanced Tools grid: brand / Project / Docs
& Support / Clients (only the CLI entry is gated, on the curl domains);
never repeats on-page content (tools, sections); About / Changelog /
Acknowledgement open in its About sheet, the CLI in its own dialog
(`Footer.vue`).
- **Page breadcrumb** — every page but Home opens its body with
`<PageBreadcrumb :items />` above the `<h1>` (← Home / … / current page):
real links, Home via `resolveBackTarget()`, section crumbs via the Nav's
-108
View File
@@ -1,108 +0,0 @@
<template>
<!-- Curl Dialog -->
<Dialog :open="isOpen" @update:open="isOpen = $event">
<DialogContent :title="t('curl.Title')">
<DialogHeader :icon="Terminal" :title="t('curl.Title')" />
<div v-if="curlDomainsHadSet" class="space-y-3">
<!-- Description -->
<div class="space-y-1 text-xs font-mono">
<p class="jn-comment"><span class="text-muted-foreground">{{ t('curl.Note1') }}</span></p>
<p class="jn-comment">
<span class="text-muted-foreground">{{ t('curl.Note2_1') }}
<Badge variant="outline" class="text-success">curl</Badge> {{ t('curl.Note2_2') }}</span>
</p>
<p class="jn-comment">
<span class="text-muted-foreground"><Badge variant="outline" class="text-success">geo</Badge> {{ t('curl.Note3') }}</span>
</p>
</div>
<!-- 3 curl command blocks -->
<div class="space-y-3">
<div>
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv4') }}</p>
<pre class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ ipv4Domain }}<span class="text-success">/geo</span></pre>
</div>
<div>
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv6') }}</p>
<pre class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ ipv6Domain }}<span class="text-success">/geo</span></pre>
</div>
<div>
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.get6and4') }}</p>
<pre class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ ipv64Domain }}<span class="text-success">/geo</span></pre>
</div>
</div>
</div>
<div v-else class="py-6 text-center">
<p class="text-sm text-muted-foreground">{{ t('curl.notAvailable') }}</p>
</div>
</DialogContent>
</Dialog>
<!-- Additional Tools: external product link bar -->
<div class="mx-auto text-center max-w-[98%]">
<div id="morefromipchecking" class="flex justify-center items-center gap-2 sm:gap-4 flex-wrap">
<a href="https://www.raycast.com/jason5ng32/ipcheck-ing" target="_blank" rel="noopener"
@click="trackEvent('Additional', 'AdditionalClick', 'Raycast')">
<img src="/additional/raycast.webp" alt="IPCheck.ing on Raycast"
class="w-[108px] sm:w-[180px] h-auto">
</a>
<button type="button" @click="openCurlModal"
aria-label="IPCheck.ing for Curl"
class="cursor-pointer bg-transparent border-0 p-0">
<img src="/additional/curl.webp" alt="IPCheck.ing for Curl"
class="w-[108px] sm:w-[180px] h-auto">
</button>
<a href="https://lite.ipcheck.ing" target="_blank" rel="noopener"
@click="trackEvent('Additional', 'AdditionalClick', 'Lite')">
<img src="/additional/lite.webp" alt="IPCheck.ing lite"
class="w-[108px] sm:w-[180px] h-auto">
</a>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
import { Terminal } from '@lucide/vue';
import { Badge } from '@/components/ui/badge';
const { t } = useI18n();
const store = useMainStore();
const ipv4Domain = computed(() => store.curl.ipv4Domain);
const ipv6Domain = computed(() => store.curl.ipv6Domain);
const ipv64Domain = computed(() => store.curl.ipv64Domain);
const curlDomainsHadSet = computed(() => store.curlDomainsHadSet);
const isOpen = ref(false);
const openCurlModal = () => {
isOpen.value = true;
trackEvent('Additional', 'AdditionalClick', 'Curl');
};
</script>
<style scoped>
.jn-curl::before {
content: '$ ';
color: var(--muted-foreground);
font-weight: 500;
margin-right: 0.25rem;
opacity: 0.7;
}
.jn-comment::before {
content: '// ';
color: var(--muted-foreground);
font-weight: 500;
opacity: 0.7;
}
</style>
+205 -58
View File
@@ -1,65 +1,141 @@
<!-- Site footer on every page: brand / Project / Docs & Support / Clients
columns, plus a signature bar (with the copyright on self-hosted
deployments). About / Changelog / Acknowledgement open in its About
sheet, the CLI in its own dialog; nothing already on the page (tools,
sections) repeats here. -->
<template>
<footer class="pb-6 text-sm text-muted-foreground" :class="!isInHomePage ? 'border-t border-secondary' : ''">
<!-- Primary footer links: Sponsor / Privacy / About / Changelog / Special
Thanks. The last three open the same About sheet on their own tab.
Text-only (no per-button icons) to keep the row calm. -->
<div class="flex flex-wrap items-center justify-center gap-x-3 mb-2 mx-auto">
<Button variant="link" size="default" as-child class="text-[#d63384] hover:text-[#d63384]">
<a href="https://github.com/sponsors/jason5ng32" target="_blank" rel="noopener">
{{ t('about.Sponsor') }}
</a>
</Button>
<Button variant="link" size="default" as-child class="cursor-pointer">
<RouterLink to="/privacy" @click="trackEvent('Footer', 'FooterClick', 'Privacy')">
{{ t('about.Privacy') }}
</RouterLink>
</Button>
<Button variant="link" size="default" as-child class="cursor-pointer">
<a :href="'https://docs.ipcheck.ing/knowledge-base/'+(locale === 'en' ? '' : locale)" target="_blank"
rel="noopener" @click="trackEvent('Footer', 'FooterClick', 'HelpCenter')">
{{ t('about.HelpCenter') }}
</a>
</Button>
<Button variant="link" size="default" as-child class="cursor-pointer">
<a :href="'https://docs.ipcheck.ing/developer/'+(locale === 'en' ? '' : locale)" target="_blank" rel="noopener"
@click="trackEvent('Footer', 'FooterClick', 'Contribute')">
{{ t('about.Contribute') }}
</a>
</Button>
<Button variant="link" size="default" @click="openAboutTab('changelog', 'Changelog')" class="cursor-pointer">
{{ t('changelog.Title') }}
</Button>
<Button variant="link" size="default" @click="openAboutTab('acknowledgement', 'Acknowledgement')"
class="cursor-pointer">
{{ t('acknowledgement.Title') }}
</Button>
<Button variant="link" size="default" @click="openAbout" class="cursor-pointer">
{{ t('about.Title') }}
</Button>
</div>
<!-- Author + GitHub on one line, copyright dimmed beneath — low-emphasis signature block -->
<div class="flex flex-col items-center justify-center gap-y-1.5 text-xs opacity-70">
<div class="flex items-center justify-center gap-x-2">
<span>Created by Jason Ng with love</span>
<JnTooltip :text="t('Tooltips.GithubLink')" side="top">
<Button variant="ghost" size="icon" as-child class="size-5 text-foreground/70 hover:text-foreground">
<a :href="t('page.footerLink')" target="_blank" rel="noopener" aria-label="Github"
<footer class="border-t border-secondary text-sm text-muted-foreground">
<div class="mx-auto w-full px-4 jn-container">
<!-- Link columns: four tracks on md+, matching the Advanced Tools grid;
on phones the brand spans the top and the rest flow two per row. -->
<div class="grid grid-cols-2 gap-8 py-12 md:grid-cols-4">
<!-- Brand -->
<div class="col-span-2 md:col-span-1">
<div class="inline-flex items-center gap-1.5 text-xl font-semibold text-foreground">
<brandIcon />
<span class="tracking-tight">
<span class="font-bold">IP</span><span class="font-extralight">Check.ing</span>
</span>
</div>
<p class="mt-2">{{ t('footer.Tagline') }}</p>
<div class="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2">
<a :href="t('page.footerLink')" target="_blank" rel="noopener" :class="linkClass"
@click="trackEvent('Footer', 'FooterClick', 'Github')">
<Icon icon="ri:github-line" />
<Icon icon="ri:github-line" class="size-4" />
<span>GitHub</span>
</a>
</Button>
</JnTooltip>
<a href="https://github.com/sponsors/jason5ng32" target="_blank" rel="noopener"
class="inline-flex items-center gap-1 text-sm text-[#d63384] no-underline transition-opacity hover:opacity-80">
<Icon icon="ri:heart-line" class="size-4" />
{{ t('about.Sponsor') }}
</a>
</div>
</div>
<!-- Project: the About sheet's tabs, privacy and license -->
<nav :aria-label="t('footer.Project')">
<h3 :class="headingClass">{{ t('footer.Project') }}</h3>
<ul class="space-y-2">
<li>
<button type="button" :class="linkClass" @click="openAbout">
{{ t('about.Title') }}
</button>
</li>
<li>
<button type="button" :class="linkClass" @click="openAboutTab('changelog', 'Changelog')">
{{ t('changelog.Title') }}
</button>
</li>
<li>
<button type="button" :class="linkClass" @click="openAboutTab('acknowledgement', 'Acknowledgement')">
{{ t('acknowledgement.Title') }}
</button>
</li>
<li>
<RouterLink to="/privacy" :class="linkClass" @click="trackEvent('Footer', 'FooterClick', 'Privacy')">
{{ t('about.Privacy') }}
</RouterLink>
</li>
<li>
<a :href="`${t('page.footerLink')}/blob/main/LICENSE`" target="_blank" rel="noopener" :class="linkClass"
@click="trackEvent('Footer', 'FooterClick', 'License')">
{{ t('footer.License') }}
</a>
</li>
</ul>
</nav>
<!-- Docs & Support: locale-aware docs.ipcheck.ing links + issue tracker -->
<nav :aria-label="t('footer.Support')">
<h3 :class="headingClass">{{ t('footer.Support') }}</h3>
<ul class="space-y-2">
<li>
<a :href="docsUrl('knowledge-base', locale)" target="_blank" rel="noopener" :class="linkClass"
@click="trackEvent('Footer', 'FooterClick', 'HelpCenter')">
{{ t('about.HelpCenter') }}
</a>
</li>
<li>
<a :href="docsUrl('developer', locale)" target="_blank" rel="noopener" :class="linkClass"
@click="trackEvent('Footer', 'FooterClick', 'DeveloperDocs')">
{{ t('footer.DeveloperDocs') }}
</a>
</li>
<li>
<a :href="docsUrl('developer', locale, 'getting-started/')" target="_blank" rel="noopener"
:class="linkClass" @click="trackEvent('Footer', 'FooterClick', 'SelfHosting')">
{{ t('footer.SelfHosting') }}
</a>
</li>
<li>
<a :href="`${t('page.footerLink')}/issues`" target="_blank" rel="noopener" :class="linkClass"
@click="trackEvent('Footer', 'FooterClick', 'ReportIssue')">
{{ t('footer.ReportIssue') }}
</a>
</li>
</ul>
</nav>
<!-- Clients: Raycast / Lite always; the CLI entry needs the curl
domains configured. -->
<nav :aria-label="t('footer.Clients')">
<h3 :class="headingClass">{{ t('footer.Clients') }}</h3>
<ul class="space-y-2">
<li>
<a href="https://www.raycast.com/jason5ng32/ipcheck-ing" target="_blank" rel="noopener" :class="linkClass"
@click="trackEvent('Footer', 'FooterClick', 'Raycast')">
<Icon icon="simple-icons:raycast" class="size-4 shrink-0" />
{{ t('footer.Raycast') }}
</a>
</li>
<li v-if="curlDomainsHadSet">
<button type="button" :class="linkClass" @click="openCurl">
<Terminal class="size-4 shrink-0" />
{{ t('curl.Title') }}
</button>
</li>
<li>
<a href="https://lite.ipcheck.ing" target="_blank" rel="noopener" :class="linkClass"
@click="trackEvent('Footer', 'FooterClick', 'Lite')">
<Feather class="size-4 shrink-0" />
{{ t('footer.Lite') }}
</a>
</li>
</ul>
</nav>
</div>
<template v-if="!configs.originalSite">
<span>
<!-- Signature bar: every deployment; the copyright only when self-hosted -->
<div class="flex flex-col items-center gap-y-1.5 border-t border-secondary py-6 text-center text-xs opacity-70">
<span>Created by Jason Ng with love</span>
<span v-if="!configs.originalSite">
{{ t('page.copyRightName') }}
<a :href="t('page.copyRightLink')" target="_blank" rel="noopener"
class="text-foreground/80 hover:text-foreground hover:underline">
{{ t('page.copyRightLinkName') }}
</a>
</span>
</template>
</div>
</div>
<!-- About Sheet -->
@@ -151,10 +227,55 @@
</li>
</ul>
</TabsContent>
</div>
</Tabs>
</SheetContent>
</Sheet>
<!-- Command Line API dialog: only opened from the Clients link, which
is listed only when the curl domains are configured. -->
<Dialog :open="isCurlOpen" @update:open="isCurlOpen = $event">
<DialogContent :title="t('curl.Title')">
<DialogHeader :icon="Terminal" :title="t('curl.Title')" />
<div class="space-y-3">
<!-- Description -->
<div class="space-y-1 text-xs font-mono">
<p class="jn-comment"><span class="text-muted-foreground">{{ t('curl.Note1') }}</span></p>
<p class="jn-comment">
<span class="text-muted-foreground">{{ t('curl.Note2_1') }}
<Badge variant="outline" class="text-success">curl</Badge> {{ t('curl.Note2_2') }}
</span>
</p>
<p class="jn-comment">
<span class="text-muted-foreground">
<Badge variant="outline" class="text-success">geo</Badge> {{ t('curl.Note3') }}
</span>
</p>
</div>
<!-- 3 curl command blocks -->
<div class="space-y-3">
<div>
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv4') }}</p>
<pre
class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ curl.ipv4Domain }}<span class="text-success">/geo</span></pre>
</div>
<div>
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv6') }}</p>
<pre
class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ curl.ipv6Domain }}<span class="text-success">/geo</span></pre>
</div>
<div>
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.get6and4') }}</p>
<pre
class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ curl.ipv64Domain }}<span class="text-success">/geo</span></pre>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</footer>
</template>
@@ -164,28 +285,31 @@ import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import changelogData from '@/data/changelog.json';
import { trackEvent } from '@/utils/analytics';
import { docsUrl } from '@/utils/docs-url.js';
import { formatIsoDate } from '@/utils/time-utils';
import { fallbackChain } from '@/utils/locale-registry.js';
import { Sheet, SheetContent, SheetClose } from '@/components/ui/sheet';
import { JnTooltip } from '@/components/ui/tooltip';
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Compass, ExternalLink, CircleFadingArrowUp, CirclePlus, BugOff, Smile } from '@lucide/vue';
import { Compass, ExternalLink, CircleFadingArrowUp, CirclePlus, BugOff, Smile, Terminal, Feather } from '@lucide/vue';
import brandIcon from './svgicons/Brand.vue';
import { Icon } from '@iconify/vue';
import { useRoute } from 'vue-router';
import { useRouteActive } from '@/composables/use-route-active.js';
const { t, tm, rt, locale } = useI18n();
const store = useMainStore();
const isMobile = computed(() => store.isMobile);
const configs = computed(() => store.configs);
const route = useRoute();
const isInHomePage = computed(() => route.path === '/');
const curl = computed(() => store.curl);
const curlDomainsHadSet = computed(() => store.curlDomainsHadSet);
const tabs = ['about', 'changelog', 'acknowledgement'];
const content = ref('about');
const headingClass = 'mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground';
const linkClass = 'inline-flex items-center gap-1 text-sm text-muted-foreground no-underline transition-colors hover:text-foreground cursor-pointer';
// Static data from JSON — reverse once via computed so the template stays tidy.
const changelogReversed = computed(() => changelogData.slice().reverse());
@@ -266,6 +390,12 @@ const openAboutTab = (tab, trackLabel) => {
trackEvent('Footer', 'FooterClick', trackLabel);
};
const isCurlOpen = ref(false);
const openCurl = () => {
isCurlOpen.value = true;
trackEvent('Footer', 'FooterClick', 'Curl');
};
watch(content, () => {
nextTick(() => {
if (sheetBody.value) sheetBody.value.scrollTop = 0;
@@ -273,3 +403,20 @@ watch(content, () => {
});
</script>
<style scoped>
.jn-curl::before {
content: '$ ';
color: var(--muted-foreground);
font-weight: 500;
margin-right: 0.25rem;
opacity: 0.7;
}
.jn-comment::before {
content: '// ';
color: var(--muted-foreground);
font-weight: 500;
opacity: 0.7;
}
</style>
+1 -3
View File
@@ -18,7 +18,6 @@
<QueryIP ref="queryIPRef" />
</FloatingDock>
<HelpModal ref="helpModalRef" />
<Additional />
<Footer />
</template>
@@ -28,7 +27,7 @@
//
// Components — the test sections and the always-visible chrome load
// synchronously; everything the first paint can't show (dialogs, the
// below-fold Additional/Footer) is an async component so its code stays
// below-fold Footer) is an async component so its code stays
// out of the route chunk and out of the mount's critical path. Their
// template refs are null until the chunk lands — consumers (use-shortcuts)
// must optional-chain.
@@ -45,7 +44,6 @@ import FloatingDock from './widgets/FloatingDock.vue';
import { ref, computed, onMounted, defineAsyncComponent } from 'vue';
// Async (off-critical-path) components
const Additional = defineAsyncComponent(() => import('./Additional.vue'));
const Footer = defineAsyncComponent(() => import('./Footer.vue'));
const Achievements = defineAsyncComponent(() => import('./Achievements.vue'));
const QueryIP = defineAsyncComponent(() => import('./widgets/QueryIP.vue'));
+14 -5
View File
@@ -384,11 +384,9 @@
"Note2_1": "You can use",
"Note2_2": "command, get the IP address of your machine in terminal",
"Note3": "is an optional path, adding it will obtain the geographical information of the IP",
"Note4": "is your IPCheck.ing API key",
"getIPv4": "Get local machine's IPv4 address",
"getIPv6": "Get local machine's IPv6 address",
"get6and4": "Get the preferred network exit IP address of the local machine",
"notAvailable": "This feature is not available yet"
"get6and4": "Get the preferred network exit IP address of the local machine"
},
"browserinfo": {
"Title": "Browser Information",
@@ -2077,13 +2075,25 @@
"helpModal": {
"Title": "Keyboard Shortcuts"
},
"footer": {
"Tagline": "A free, open-source, all-in-one privacy toolbox.",
"Project": "Project",
"Support": "Docs & Support",
"DeveloperDocs": "Developer Docs",
"SelfHosting": "Self-hosting Guide",
"ReportIssue": "Report an Issue",
"License": "MIT License",
"Clients": "Clients",
"Raycast": "Raycast Extension",
"Lite": "Lite Edition"
},
"about": {
"Title": "About",
"project": [
"IPCheck.ing is a free all-in-one IP toolbox that helps you check your own IP information, test website availability, examine DNS exit information, and run speed tests, global latency tests, MTR tests and more.",
"This is an open-source project — click the GitHub icon at the bottom of the page to browse the source code.",
"If you are unsure how a feature works, open the \"Help Center\" link at the bottom of the page. The documentation is thorough: beyond describing each feature, it also explains networking concepts and how to troubleshoot network problems.",
"If you would like to take part in the open-source project, click the \"Contribute Code\" button at the bottom of the page to see how."
"If you would like to take part in the open-source project, open the \"Developer Docs\" link at the bottom of the page to see how."
],
"founderTitle": "About the Founder",
"founder": [
@@ -2104,7 +2114,6 @@
"Sponsor": "Sponsor Us",
"Privacy": "Privacy",
"HelpCenter": "Help Center",
"Contribute": "Contribute Code",
"Close": "Close"
},
"acknowledgement": {
+14 -5
View File
@@ -384,11 +384,9 @@
"Note2_1": "Tu peux, dans le terminal, via",
"Note2_2": "commande, obtenir l'adresse IP de cette machine",
"Note3": "est un chemin optionnel, son ajout permettra d'obtenir les informations géographiques de l'IP",
"Note4": "est votre clé API IPCheck.ing",
"getIPv4": "Obtenir l'adresse IPv4 de la machine locale",
"getIPv6": "Obtenir l'adresse IPv6 de la machine locale",
"get6and4": "Obtenir l'adresse IP de sortie de réseau préférée de la machine locale",
"notAvailable": "Cette fonctionnalité n'est pas encore disponible"
"get6and4": "Obtenir l'adresse IP de sortie de réseau préférée de la machine locale"
},
"browserinfo": {
"Title": "Informations du Navigateur",
@@ -2077,13 +2075,25 @@
"helpModal": {
"Title": "Raccourcis"
},
"footer": {
"Tagline": "Une boîte à outils de confidentialité tout-en-un, gratuite et open source.",
"Project": "Projet",
"Support": "Documentation et support",
"DeveloperDocs": "Documentation développeur",
"SelfHosting": "Guide d'auto-hébergement",
"ReportIssue": "Signaler un problème",
"License": "Licence MIT",
"Clients": "Clients",
"Raycast": "Extension Raycast",
"Lite": "Édition Lite"
},
"about": {
"Title": "À propos",
"project": [
"IPCheck.ing est une boîte à outils IP tout-en-un et gratuite : elle vous aide à consulter vos informations IP, à tester la disponibilité des sites web, à examiner les informations de sortie DNS, à mesurer votre débit, à tester la latence mondiale, à lancer des tests MTR, et bien plus encore.",
"Il s'agit d'un projet open source : cliquez sur l'icône GitHub en bas de la page pour consulter le code source.",
"Si vous ne comprenez pas comment utiliser une fonctionnalité, ouvrez le « Centre d'aide » en bas de la page. Notre documentation est très détaillée : en plus de présenter chaque fonctionnalité, elle vous aide à comprendre les différents concepts réseau et à diagnostiquer vos problèmes de connexion.",
"Si vous souhaitez participer au projet open source, cliquez sur le bouton « Contribuer au code » en bas de la page pour découvrir comment faire."
"Si vous souhaitez participer au projet open source, ouvrez le lien « Documentation développeur » en bas de la page pour découvrir comment faire."
],
"founderTitle": "À propos du fondateur",
"founder": [
@@ -2104,7 +2114,6 @@
"Sponsor": "Nous soutenir",
"Privacy": "Confidentialité",
"HelpCenter": "Centre d'aide",
"Contribute": "Contribuer au code",
"Close": "Fermer"
},
"acknowledgement": {
+14 -5
View File
@@ -384,11 +384,9 @@
"Note2_1": "Você pode usar o comando",
"Note2_2": "para obter o IP da sua máquina no terminal",
"Note3": "é um caminho opcional; adicioná-lo obtém as informações geográficas do IP",
"Note4": "é sua chave de API do IPCheck.ing",
"getIPv4": "Obter o endereço IPv4 da máquina local",
"getIPv6": "Obter o endereço IPv6 da máquina local",
"get6and4": "Obter o IP de saída de rede preferencial da máquina local",
"notAvailable": "Este recurso ainda não está disponível"
"get6and4": "Obter o IP de saída de rede preferencial da máquina local"
},
"browserinfo": {
"Title": "Informações do navegador",
@@ -2077,13 +2075,25 @@
"helpModal": {
"Title": "Atalhos de teclado"
},
"footer": {
"Tagline": "Uma caixa de ferramentas de privacidade tudo em um, gratuita e de código aberto.",
"Project": "Projeto",
"Support": "Documentação e suporte",
"DeveloperDocs": "Documentação para desenvolvedores",
"SelfHosting": "Guia de auto-hospedagem",
"ReportIssue": "Relatar um problema",
"License": "Licença MIT",
"Clients": "Clientes",
"Raycast": "Extensão para Raycast",
"Lite": "Edição Lite"
},
"about": {
"Title": "Sobre",
"project": [
"IPCheck.ing é uma caixa de ferramentas de IP gratuita e tudo em um que ajuda você a verificar suas próprias informações de IP, testar disponibilidade de sites, examinar informações de saída DNS e executar testes de velocidade, latência global, MTR e muito mais.",
"Este é um projeto de código aberto — clique no ícone do GitHub no rodapé para navegar pelo código-fonte.",
"Se você não tiver certeza de como um recurso funciona, abra o link \"Central de ajuda\" no rodapé. A documentação é completa: além de descrever cada recurso, ela também explica conceitos de rede e como solucionar problemas de rede.",
"Se quiser participar do projeto de código aberto, clique no botão \"Contribuir com código\" no rodapé para ver como."
"Se quiser participar do projeto de código aberto, abra o link \"Documentação para desenvolvedores\" no rodapé para ver como."
],
"founderTitle": "Sobre o fundador",
"founder": [
@@ -2104,7 +2114,6 @@
"Sponsor": "Patrocine-nos",
"Privacy": "Privacidade",
"HelpCenter": "Central de ajuda",
"Contribute": "Contribuir com código",
"Close": "Fechar"
},
"acknowledgement": {
+14 -5
View File
@@ -384,11 +384,9 @@
"Note2_1": "Можно использовать",
"Note2_2": "команду, чтобы получить IP-адрес этого устройства в терминале",
"Note3": "— необязательный путь; добавьте его, чтобы получить географические сведения об IP",
"Note4": "— ваш ключ API IPCheck.ing",
"getIPv4": "Получить IPv4-адрес этого устройства",
"getIPv6": "Получить IPv6-адрес этого устройства",
"get6and4": "Получить предпочтительный внешний IP-адрес этого устройства",
"notAvailable": "Эта функция пока недоступна"
"get6and4": "Получить предпочтительный внешний IP-адрес этого устройства"
},
"browserinfo": {
"Title": "Сведения о браузере",
@@ -2077,13 +2075,25 @@
"helpModal": {
"Title": "Сочетания клавиш"
},
"footer": {
"Tagline": "Бесплатный универсальный набор инструментов для приватности с открытым исходным кодом.",
"Project": "Проект",
"Support": "Документация и поддержка",
"DeveloperDocs": "Документация для разработчиков",
"SelfHosting": "Руководство по самостоятельному размещению",
"ReportIssue": "Сообщить о проблеме",
"License": "Лицензия MIT",
"Clients": "Клиенты",
"Raycast": "Расширение для Raycast",
"Lite": "Версия Lite"
},
"about": {
"Title": "О проекте",
"project": [
"IPCheck.ing — бесплатный универсальный набор IP-инструментов: он помогает посмотреть сведения о своём IP, проверить доступность сайтов и выход DNS, измерить скорость соединения, задержку до серверов по всему миру, выполнить MTR-тест и многое другое.",
"Это проект с открытым исходным кодом — нажмите значок GitHub внизу страницы, чтобы посмотреть исходный код.",
"Если вам непонятно, как пользоваться той или иной функцией, откройте «Справочный центр» внизу страницы. Документация очень подробная: помимо описания функций, она помогает разобраться в сетевых понятиях и в том, как диагностировать проблемы с сетью.",
"Если вы хотите поучаствовать в проекте с открытым исходным кодом, нажмите внизу страницы кнопку «Стать разработчиком» и посмотрите, как это сделать."
"Если вы хотите поучаствовать в проекте с открытым исходным кодом, откройте внизу страницы ссылку «Документация для разработчиков» и посмотрите, как это сделать."
],
"founderTitle": "Об основателе проекта",
"founder": [
@@ -2104,7 +2114,6 @@
"Sponsor": "Поддержать",
"Privacy": "Приватность",
"HelpCenter": "Справочный центр",
"Contribute": "Стать разработчиком",
"Close": "Закрыть"
},
"acknowledgement": {
+14 -5
View File
@@ -384,11 +384,9 @@
"Note2_1": "你可以在終端裡,透過",
"Note2_2": "命令取得本機的 IP 位址",
"Note3": "是可選路徑,新增後可取得到 IP 的地理資訊",
"Note4": "請使用你申請的 IPCheck.ing API 金鑰",
"getIPv4": "取得本機 IPv4 位址",
"getIPv6": "取得本機 IPv6 位址",
"get6and4": "取得本機優先網路出口的 IP 位址",
"notAvailable": "此功能暫未開放,敬請期待"
"get6and4": "取得本機優先網路出口的 IP 位址"
},
"browserinfo": {
"Title": "瀏覽器資訊",
@@ -2077,13 +2075,25 @@
"helpModal": {
"Title": "鍵盤快速鍵"
},
"footer": {
"Tagline": "免費、開源、一站式的隱私工具箱",
"Project": "專案",
"Support": "文件與支援",
"DeveloperDocs": "開發者文件",
"SelfHosting": "自行架設指南",
"ReportIssue": "回報問題",
"License": "MIT 授權條款",
"Clients": "用戶端",
"Raycast": "Raycast 擴充功能",
"Lite": "Lite 版"
},
"about": {
"Title": "關於",
"project": [
"IPCheck.ing 是一個免費的多合一 IP 工具箱,可以協助你檢視自己的 IP 資訊,檢測網站的可用性,檢視 DNS 出口資訊,進行網速測試,全球延遲測試,MTR 測試等。",
"這是一個開源專案,你可以點選頁面底部的 GitHub 圖示檢視原始碼。",
"如果你對於產品中功能使用不理解,可以透過底部的「說明中心」取得協助,我們已經寫了非常詳細的協助文件,除了功能介紹,也能協助你理解各種網路概念和如何排查網路問題。",
"如果你有興趣參與到開源專案中,可以點選頁面底部的「貢獻程式碼」按鈕,檢視如何貢獻程式碼。"
"如果你有興趣參與到開源專案中,可以點選頁面底部的「開發者文件」,檢視如何貢獻程式碼。"
],
"founderTitle": "關於專案發起人",
"founder": [
@@ -2104,7 +2114,6 @@
"Sponsor": "贊助我們",
"Privacy": "隱私政策",
"HelpCenter": "說明中心",
"Contribute": "貢獻程式碼",
"Close": "關閉"
},
"acknowledgement": {
+14 -5
View File
@@ -384,11 +384,9 @@
"Note2_1": "你可以在终端里,通过",
"Note2_2": "命令获取本机的 IP 地址",
"Note3": "是可选路径,添加后可获取到 IP 的地理信息",
"Note4": "请使用你申请的 IPCheck.ing API 密钥",
"getIPv4": "获取本机 IPv4 地址",
"getIPv6": "获取本机 IPv6 地址",
"get6and4": "获取本机优先网络出口的 IP 地址",
"notAvailable": "此功能暂未开放,敬请期待"
"get6and4": "获取本机优先网络出口的 IP 地址"
},
"browserinfo": {
"Title": "浏览器信息",
@@ -2077,13 +2075,25 @@
"helpModal": {
"Title": "快捷键"
},
"footer": {
"Tagline": "免费、开源、一站式的隐私工具箱",
"Project": "项目",
"Support": "文档与支持",
"DeveloperDocs": "开发者文档",
"SelfHosting": "自托管指南",
"ReportIssue": "报告问题",
"License": "MIT 许可证",
"Clients": "客户端",
"Raycast": "Raycast 扩展",
"Lite": "Lite 版"
},
"about": {
"Title": "关于",
"project": [
"IPCheck.ing 是一个免费的多合一 IP 工具箱,可以帮助你查看自己的 IP 信息,检测网站的可用性,查看 DNS 出口信息,进行网速测试,全球延迟测试,MTR 测试等。",
"这是一个开源项目,你可以点击页面底部的 GitHub 图标查看源代码。",
"如果你对于产品中功能使用不理解,可以通过底部的「帮助中心」获取帮助,我们已经写了非常详细的帮助文档,除了功能介绍,也能帮助你理解各种网络概念和如何排查网络问题。",
"如果你有兴趣参与到开源项目中,可以点击页面底部的「贡献代码」按钮,查看如何贡献代码。"
"如果你有兴趣参与到开源项目中,可以点击页面底部的「开发者文档」,查看如何贡献代码。"
],
"founderTitle": "关于项目发起人",
"founder": [
@@ -2104,7 +2114,6 @@
"Sponsor": "赞助我们",
"Privacy": "隐私政策",
"HelpCenter": "帮助中心",
"Contribute": "贡献代码",
"Close": "关闭"
},
"acknowledgement": {
-11
View File
@@ -218,17 +218,6 @@
max-width: 1400px;
}
footer {
height: 100pt;
padding-top: 20pt;
}
@media only screen and (max-width: 768px) {
footer {
height: 140pt;
}
}
.body-dark-mode {
background-color: rgb(19, 21, 21) !important;
color: #e3e3e3 !important;
+12
View File
@@ -0,0 +1,12 @@
// Builds a docs.ipcheck.ing link: English lives at the section root, every
// other locale under `/<section>/<locale>/`, with `path` appended after that.
const DOCS_ORIGIN = 'https://docs.ipcheck.ing';
const trimSlashes = (value) => String(value ?? '').replace(/^\/+|\/+$/g, '');
export const docsUrl = (section, locale, path = '') => {
const base = `${DOCS_ORIGIN}/${trimSlashes(section)}/`;
const lang = !locale || locale === 'en' ? '' : `${trimSlashes(locale)}/`;
return base + lang + String(path ?? '').replace(/^\/+/, '');
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

+36
View File
@@ -0,0 +1,36 @@
// docsUrl() — locale-aware links into docs.ipcheck.ing.
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { docsUrl } from '../frontend/utils/docs-url.js';
describe('docsUrl()', () => {
it('puts English at the section root', () => {
assert.equal(docsUrl('knowledge-base', 'en'), 'https://docs.ipcheck.ing/knowledge-base/');
assert.equal(docsUrl('developer', 'en'), 'https://docs.ipcheck.ing/developer/');
});
it('puts any other locale under the section', () => {
assert.equal(docsUrl('knowledge-base', 'zh'), 'https://docs.ipcheck.ing/knowledge-base/zh/');
assert.equal(docsUrl('developer', 'pt-BR'), 'https://docs.ipcheck.ing/developer/pt-BR/');
});
it('appends the path after the locale segment', () => {
assert.equal(docsUrl('developer', 'en', 'getting-started/'), 'https://docs.ipcheck.ing/developer/getting-started/');
assert.equal(docsUrl('developer', 'zh', 'getting-started/'), 'https://docs.ipcheck.ing/developer/zh/getting-started/');
});
it('never produces a double slash', () => {
const urls = [
docsUrl('/developer/', 'zh', '/getting-started/'),
docsUrl('developer', 'en', '/getting-started/'),
docsUrl('developer/', 'fr'),
];
for (const url of urls) assert.doesNotMatch(url.replace('https://', ''), /\/\//);
assert.equal(urls[0], 'https://docs.ipcheck.ing/developer/zh/getting-started/');
});
it('treats a missing locale as English', () => {
assert.equal(docsUrl('developer', undefined), 'https://docs.ipcheck.ing/developer/');
});
});