Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/web/content/docs/dev/i18n/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,29 @@ export const vitNodeApiConfig = buildApiConfig({

---

### Your app's own strings

Copy that belongs to no package - your landing page, say - gets a namespace of its own. Pick an id no package uses, put the tree under it, and register it like any other file:

```json title="apps/web/src/locales/site/en.json"
{
"site": {
"home": { "title": "A home for your people" }
}
}
```

```ts title="apps/web/src/locales/app.ts"
export const appMessages: AppMessagesMap = {
en: { site: async () => await import('./site/en.json') },
pl: { site: async () => await import('./site/pl.json') },
}
```

`i18n:check` treats it like a package: your default-locale file is the source of truth, and every other language is checked against it for missing and leftover keys. No default-locale file at all is an error - there would be nothing to translate from.

---

## Translation Architecture

| Source | Role | Order |
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/locales/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import type { AppMessagesMap } from '@vitnode/core/lib/i18n/types'
* API registers the same file through `vitnode.api.config.ts`.
*/
export const appMessages: AppMessagesMap = {
en: {
site: async () => await import('./site/en.json'),
},
pl: {
'@vitnode/blog': async () => await import('./@vitnode/blog/pl.json'),
'@vitnode/core': async () => await import('./@vitnode/core/pl.json'),
site: async () => await import('./site/pl.json'),
},
}
24 changes: 24 additions & 0 deletions apps/web/src/locales/site/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"site": {
"home": {
"hero": {
"canary": "VitNode 2.0 is an early canary",
"description": "Publish content, bring members together and build the features your community needs on an open-source platform you control.",
"explore": "Explore what you can build",
"preview": {
"alt": "The AdminCP dashboard with a sidebar listing Core, Blog and Example plugin sections, a private notes widget and a send-notification widget.",
"members": "{count, plural, one {<strong>#</strong> member} other {<strong>#</strong> members}}",
"toast_meta": "Live toast · just now",
"toast_title": "Welcome aboard, {name}!"
},
"see_product": "See the product",
"title": "A home for <highlight>your people</highlight>, built around your idea.",
"trust": {
"agents": "Made for AI coding agents",
"license": "MIT licence",
"open_source": "Open source"
}
}
}
}
}
24 changes: 24 additions & 0 deletions apps/web/src/locales/site/pl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"site": {
"home": {
"hero": {
"canary": "VitNode 2.0 to wczesna wersja canary",
"description": "Publikuj treści, łącz ludzi i twórz funkcje, których potrzebuje Twoja społeczność, na platformie open source, nad którą masz pełną kontrolę.",
"explore": "Odkryj, co możesz zbudować",
"preview": {
"alt": "Pulpit AdminCP z paskiem bocznym z sekcjami Core, Blog i wtyczki Example, widżetem prywatnych notatek i widżetem wysyłania powiadomień.",
"members": "{count, plural, one {<strong>#</strong> członek} few {<strong>#</strong> członkowie} many {<strong>#</strong> członków} other {<strong>#</strong> członka}}",
"toast_meta": "Komunikat na żywo · przed chwilą",
"toast_title": "Witaj na pokładzie, {name}!"
},
"see_product": "Zobacz produkt",
"title": "Dom dla <highlight>Twojej społeczności</highlight>, zbudowany wokół Twojego pomysłu.",
"trust": {
"agents": "Stworzony dla agentów AI do kodowania",
"license": "Licencja MIT",
"open_source": "Open source"
}
}
}
}
}
11 changes: 11 additions & 0 deletions apps/web/src/routes/_main/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { createFileRoute } from '@tanstack/react-router'
import { intlQueryOptions } from '@vitnode/core/tanstack/i18n'

import { HomeRouteContent } from '@/site/home/home-content'
import { HOME_NAMESPACES } from '@/site/home/namespaces'
import { MARKETING_PAGES, marketingHead } from '@/site/marketing/metadata'

export const Route = createFileRoute('/_main/')({
loader: async ({ context }) => {
await context.queryClient.query({
...intlQueryOptions({
locale: context.locale,
namespaces: HOME_NAMESPACES,
}),
Comment on lines +10 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the home query out of the route file

Keep this file topology-only by exposing a home route loader/query contract from the TanStack feature module instead of constructing intlQueryOptions here and importing an app-local namespace list. The current arrangement couples the host route to the screen's query and namespace implementation, contrary to the repository's route-module boundary.

AGENTS.md reference: AGENTS.md:L26-L28

Useful? React with 👍 / 👎.

staleTime: 'static',
})
},
head: () => marketingHead(MARKETING_PAGES.home),
component: HomeRouteContent,
})
45 changes: 25 additions & 20 deletions apps/web/src/site/home/home-content.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { RouteMessages } from '@vitnode/core/tanstack/i18n'

import { HOME_NAMESPACES } from '@/site/home/namespaces'
import { AgentsSection } from '@/site/home/sections/agents'
import { CommunitySection } from '@/site/home/sections/community'
import { DevelopersSection } from '@/site/home/sections/developers'
import { FaqSection } from '@/site/home/sections/faq'
import { FeaturesBentoSection } from '@/site/home/sections/features-bento'
import { FinalCtaSection } from '@/site/home/sections/final-cta'
import { HeroSection } from '@/site/home/sections/hero'
// import { MakerSection } from '@/site/home/sections/maker'
import { IntegrationsSection } from '@/site/home/sections/integrations'
import { OutcomesSection } from '@/site/home/sections/outcomes'
import { PluginsSection } from '@/site/home/sections/plugins'
import { PoweringBySection } from '@/site/home/sections/powering-by'
Expand All @@ -16,23 +19,25 @@ import { MarketingPage } from '@/site/marketing/marketing-page'
import { CanaryNotice } from '@/site/marketing/shared'

export const HomeRouteContent = () => (
<MarketingPage>
<HeroSection />
<PoweringBySection />
<div className="container mx-auto px-4 pt-12 sm:px-6">
<CanaryNotice />
</div>
<OutcomesSection />
<FeaturesBentoSection />
<PluginsSection />
<ShowcaseSection />
<CommunitySection />
<AgentsSection />
<SecuritySection />
<DevelopersSection />
{/* <MakerSection /> */}
<PricingSection />
<FaqSection />
<FinalCtaSection />
</MarketingPage>
<RouteMessages namespaces={HOME_NAMESPACES}>
<MarketingPage>
<HeroSection />
<PoweringBySection />
<div className="container mx-auto px-4 pt-12 sm:px-6">
<CanaryNotice />
</div>
<OutcomesSection />
<FeaturesBentoSection />
<PluginsSection />
<ShowcaseSection />
<CommunitySection />
<AgentsSection />
<SecuritySection />
<DevelopersSection />
<PricingSection />
<FaqSection />
<IntegrationsSection />
<FinalCtaSection />
</MarketingPage>
</RouteMessages>
)
74 changes: 74 additions & 0 deletions apps/web/src/site/home/illustrations/hero-icon-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { LucideIcon } from 'lucide-react'

import {
Bell,
Blocks,
Bot,
CalendarDays,
Code,
Database,
FileText,
Globe,
Image as ImageIcon,
Languages,
LayoutDashboard,
Lock,
Mail,
Plug,
Search,
ShieldCheck,
Sparkles,
Timer,
Users,
Zap,
} from 'lucide-react'

const ICONS: LucideIcon[] = [
Bell,
Blocks,
Bot,
CalendarDays,
Code,
Database,
FileText,
Globe,
ImageIcon,
Languages,
LayoutDashboard,
Lock,
Mail,
Plug,
Search,
ShieldCheck,
Sparkles,
Timer,
Users,
Zap,
]

const FIELD = Array.from({ length: 26 }, (_, index) => ({
Icon: ICONS[index % ICONS.length],
rotate: ((index * 47) % 30) - 15,
x: (index * 29.7 + 7) % 96,
y: (index * 53.3 + 11) % 92,
}))

export const HeroIconField = () => (
<div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 hidden lg:block"
style={{
maskImage:
'radial-gradient(ellipse 45% 60% at 25% 50%, transparent 45%, black 90%)',
}}
>
{FIELD.map(({ Icon, rotate, x, y }) => (
<Icon
className="text-foreground/15 absolute size-5"
key={`${x}-${y}`}
strokeWidth={1.5}
style={{ left: `${x}%`, rotate: `${rotate}deg`, top: `${y}%` }}
/>
))}
</div>
)
Loading
Loading