- HackMeridian 2026, rebuilt from the chain.
+ {t('home.heroTitle')}
- The submission list is not public, so we rebuilt it from Stellar testnet. Every project
- below is matched to on-chain activity you can check yourself.
+ {t('home.heroSubtitle')}
{data ? (
- {formatDateTime(data.start)} to {formatDateTime(data.end)}
+ {formatDateTime(data.start)} {t('home.windowTo')} {formatDateTime(data.end)}
) : (
diff --git a/apps/web/src/pages/project.tsx b/apps/web/src/pages/project.tsx
index 4036891..061f8d0 100644
--- a/apps/web/src/pages/project.tsx
+++ b/apps/web/src/pages/project.tsx
@@ -1,6 +1,8 @@
-import { ExternalLinkIcon, StarIcon } from 'lucide-react'
+import { ExternalLinkIcon } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router'
+import { AwardBadge } from '@/components/award-badge'
import { Contributors } from '@/components/contributors'
import { DataTable } from '@/components/data-table'
import { ActivitySections } from '@/components/explorer/activity-sections'
@@ -8,7 +10,6 @@ import { integrationColumns, linkColumns } from '@/components/explorer/columns'
import { ProjectSkeleton } from '@/components/page-skeletons'
import { DetailError } from '@/components/query-state'
import { Section } from '@/components/section'
-import { AwardBadge } from '@/components/award-badge'
import { TierBadge } from '@/components/tier-badge'
import { TrackBadge } from '@/components/track-badge'
import { Accordion } from '@/components/ui/accordion'
@@ -16,13 +17,14 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { useHackWindow } from '@/hooks/use-hack-window'
import { useProject } from '@/hooks/use-project'
-import { flagEmoji, footprintLabel, formatDateTime, isWithin, projectName } from '@/lib/format'
+import { flagEmoji, footprintLabel, isWithin, projectName } from '@/lib/format'
import { githubUrl } from '@/lib/links'
export function ProjectPage() {
const projectId = useParams()['*'] ?? ''
const { data, isPending, error } = useProject(projectId)
const { data: hackWindow } = useHackWindow()
+ const { t, i18n } = useTranslation()
if (isPending) return
if (error) return
@@ -34,12 +36,13 @@ export function ProjectPage() {
const derived = links.length - direct.length
const packages = Object.entries(project.dependencies?.packages ?? {})
const reasons = [
- isWithin(project.created_at, hackWindow) && 'was created during the hack',
- project.stellar_added_in_window && 'added Stellar during the hack',
+ isWithin(project.created_at, hackWindow) && t('project.createdDuringHack'),
+ project.stellar_added_in_window && t('project.addedStellarDuringHack'),
].filter(Boolean) as string[]
+ const locale = i18n.language.startsWith('pt') ? 'pt' : 'en'
const inclusion = reasons.length
- ? new Intl.ListFormat('en', { type: 'conjunction' }).format(reasons)
- : 'has a Stellar dependency and commits during the hack'
+ ? new Intl.ListFormat(locale, { type: 'conjunction' }).format(reasons)
+ : t('project.defaultInclusion')
return (
@@ -56,32 +59,23 @@ export function ProjectPage() {
-
{footprintLabel[project.footprint]}
+
{t(`footprint.${project.footprint}`, { defaultValue: footprintLabel[project.footprint] })}
+
{project.description && (
- {project.description}
+ {project.description}
)}
+
{project.tier !== 'verified' && (
- Included because it {inclusion}.
+ {t('project.includedBecause', { inclusion })}
)}
{packages.length > 0 && (
@@ -138,7 +120,7 @@ export function ProjectPage() {
{integration_uses.length > 0 && (
-
+
)}
{contributors.length > 0 && (
-
+
)}
{direct.length > 0 && (
-
+
{derived > 0 && (
- {derived} more items are attributed through the same deploy accounts.
+ {t('project.derivedNotice', { count: derived })}
)}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index de60ba8..125be77 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -34,6 +34,9 @@ importers:
cn:
specifier: ^0.5.0
version: 0.5.0
+ i18next:
+ specifier: ^26.5.12
+ version: 26.5.12(typescript@6.0.3)
lucide-react:
specifier: ^1.61.0
version: 1.61.0(@types/react@19.3.0)(react@19.3.1)
@@ -52,6 +55,9 @@ importers:
react-dom:
specifier: ^19.3.1
version: 19.3.1(react@19.3.1)
+ react-i18next:
+ specifier: ^17.1.9
+ version: 17.1.9(i18next@26.5.12(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
react-router:
specifier: ^8.5.0
version: 8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
@@ -252,6 +258,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/runtime@7.29.10':
+ resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==}
+ engines: {node: '>=6.9.0'}
+
'@babel/runtime@8.0.8':
resolution: {integrity: sha512-dEu8nGlRqjsJsvviUUoSMfcRHpwomCiRlXsc2mPfJ79+ffTpDm+JtfUtTMzUrqCApHQnCsVPD3gT1ZT8RgBRCQ==}
@@ -1440,6 +1450,9 @@ packages:
resolution: {integrity: sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==}
engines: {node: '>=16.9.0'}
+ html-parse-stringify@4.0.1:
+ resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@@ -1458,6 +1471,14 @@ packages:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
+ i18next@26.5.12:
+ resolution: {integrity: sha512-vTt8tc3y5axoU7wwTYg8vjpILLyP33a5GyGJAVI7FDRx1XueYxZVIkcVeStoftrVnAM/o28hKvK0sslucDI9aQ==}
+ peerDependencies:
+ typescript: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
iconv-lite@0.7.3:
resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==}
engines: {node: '>=0.10.0'}
@@ -2235,6 +2256,22 @@ packages:
peerDependencies:
react: ^19.3.1
+ react-i18next@17.1.9:
+ resolution: {integrity: sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==}
+ peerDependencies:
+ i18next: '>= 26.2.0'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ typescript: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+
react-is@19.3.1:
resolution: {integrity: sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q==}
@@ -2933,6 +2970,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/runtime@7.29.10': {}
+
'@babel/runtime@8.0.8': {}
'@babel/template@7.29.7':
@@ -4000,6 +4039,8 @@ snapshots:
hono@4.13.14: {}
+ html-parse-stringify@4.0.1: {}
+
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
@@ -4016,6 +4057,10 @@ snapshots:
human-signals@8.0.1: {}
+ i18next@26.5.12(typescript@6.0.3):
+ optionalDependencies:
+ typescript: 6.0.3
+
iconv-lite@0.7.3:
dependencies:
safer-buffer: 2.1.2
@@ -4877,6 +4922,17 @@ snapshots:
react: 19.3.1
scheduler: 0.28.0
+ react-i18next@17.1.9(i18next@26.5.12(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
+ dependencies:
+ '@babel/runtime': 7.29.10
+ html-parse-stringify: 4.0.1
+ i18next: 26.5.12(typescript@6.0.3)
+ react: 19.3.1
+ use-sync-external-store: 1.7.0(react@19.3.1)
+ optionalDependencies:
+ react-dom: 19.3.1(react@19.3.1)
+ typescript: 6.0.3
+
react-is@19.3.1: {}
react-redux@9.3.0(@types/react@19.3.0)(react@19.3.1)(redux@5.0.1):