Skip to content
Closed
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
7 changes: 5 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lint": "oxlint",
"preview": "vite preview",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"check:i18n": "node scripts/check-i18n.mjs"
},
"dependencies": {
"@ai-sdk/react": "^4.0.158",
Expand All @@ -20,12 +21,14 @@
"ai": "^7.0.154",
"class-variance-authority": "^0.7.1",
"cn": "^0.5.0",
"i18next": "^26.5.12",
"lucide-react": "^1.61.0",
"motion": "^14.6.0",
"next-themes": "^0.4.6",
"posthog-js": "^1.440.0",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-i18next": "^17.1.9",
"react-router": "^8.5.0",
"react-useanimations": "^3.1.1",
"recharts": "3.8.0",
Expand All @@ -46,4 +49,4 @@
"typescript": "~6.0.3",
"vite": "^8.3.4"
}
}
}
93 changes: 93 additions & 0 deletions apps/web/scripts/check-i18n.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')

const enPath = path.join(root, 'src', 'i18n', 'locales', 'en.json')
const ptPath = path.join(root, 'src', 'i18n', 'locales', 'pt.json')

if (!fs.existsSync(enPath) || !fs.existsSync(ptPath)) {
console.error('Error: Translation files missing!')
process.exit(1)
}

const en = JSON.parse(fs.readFileSync(enPath, 'utf8'))
const pt = JSON.parse(fs.readFileSync(ptPath, 'utf8'))

function flattenKeys(obj, prefix = '') {
let keys = {}
for (const [k, v] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${k}` : k
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
Object.assign(keys, flattenKeys(v, fullKey))
} else {
keys[fullKey] = v
}
}
return keys
}

const enKeys = flattenKeys(en)
const ptKeys = flattenKeys(pt)

let errors = 0

// 1. Verify key parity
for (const key of Object.keys(enKeys)) {
if (!(key in ptKeys)) {
console.error(`Missing translation in pt.json: ${key}`)
errors++
} else if (!ptKeys[key] || typeof ptKeys[key] !== 'string' || ptKeys[key].trim() === '') {
console.error(`Empty translation in pt.json: ${key}`)
errors++
}
}

for (const key of Object.keys(ptKeys)) {
if (!(key in enKeys)) {
console.error(`Extraneous key in pt.json not in en.json: ${key}`)
errors++
}
}

console.log(`Verified ${Object.keys(enKeys).length} translation keys parity between EN and PT.`)

// 2. Scan critical components for leftover hardcoded strings
const filesToCheck = [
'src/components/layout/site-header.tsx',
'src/components/layout/site-footer.tsx',
'src/pages/home.tsx',
'src/pages/builder.tsx',
'src/components/overview.tsx',
]

const forbiddenSubstrings = [
'> Winners',
'>Open Source',
'Independent project, not affiliated',
'HackMeridian 2026, rebuilt from the chain.',
'Contracts deployed, per hour over the 27 hour window',
]

for (const rel of filesToCheck) {
const filePath = path.join(root, rel)
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf8')
for (const forbidden of forbiddenSubstrings) {
if (content.includes(forbidden)) {
console.error(`Found hardcoded string "${forbidden}" in ${rel}`)
errors++
}
}
}
}

if (errors > 0) {
console.error(`\nCI i18n Check FAILED with ${errors} error(s).`)
process.exit(1)
} else {
console.log('CI i18n Check PASSED: No hardcoded strings found and all keys match!')
process.exit(0)
}
11 changes: 9 additions & 2 deletions apps/web/src/components/contributors.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useTranslation } from 'react-i18next'

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { flagEmoji } from '@/lib/format'
Expand All @@ -6,6 +8,7 @@ import type { Contributor } from '@/lib/types'
const VISIBLE = 30

export function Contributors({ contributors }: { contributors: Contributor[] }) {
const { t } = useTranslation()
const hidden = contributors.length - VISIBLE

return (
Expand All @@ -24,11 +27,15 @@ export function Contributors({ contributors }: { contributors: Contributor[] })
</TooltipTrigger>
<TooltipContent>
{person.login}
{person.country && ` ${flagEmoji(person.country)}`} · {person.contributions} commits
{person.country && ` ${flagEmoji(person.country)}`} · {person.contributions} {t('project.commits')}
</TooltipContent>
</Tooltip>
))}
{hidden > 0 && <span className="text-sm text-muted-foreground">+{hidden} more</span>}
{hidden > 0 && (
<span className="text-sm text-muted-foreground">
{t('project.moreContributors', { count: hidden })}
</span>
)}
</div>
)
}
10 changes: 7 additions & 3 deletions apps/web/src/components/explorer/activity-sections.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useTranslation } from 'react-i18next'

import { DataTable } from '@/components/data-table'
import { Section } from '@/components/section'
import type { Contract, FailedTx, WasmUpload } from '@/lib/types'
Expand All @@ -11,20 +13,22 @@ interface ActivitySectionsProps {
}

export function ActivitySections({ contracts, wasmUploads, failedTxs }: ActivitySectionsProps) {
const { t } = useTranslation()

return (
<>
{contracts.length > 0 && (
<Section value="contracts" title="Contracts" count={contracts.length}>
<Section value="contracts" title={t('explorer.tabs.contracts')} count={contracts.length}>
<DataTable columns={contractColumns} rows={contracts} rowKey={(row) => row.contract_id} />
</Section>
)}
{wasmUploads.length > 0 && (
<Section value="wasm" title="Wasm uploads" count={wasmUploads.length}>
<Section value="wasm" title={t('overview.wasmUploads')} count={wasmUploads.length}>
<DataTable columns={wasmColumns} rows={wasmUploads} rowKey={(row) => row.tx_hash} />
</Section>
)}
{failedTxs.length > 0 && (
<Section value="failed" title="Failed transactions" count={failedTxs.length}>
<Section value="failed" title={t('overview.failedTxs')} count={failedTxs.length}>
<DataTable columns={failedColumns} rows={failedTxs} rowKey={(row) => row.tx_hash} />
</Section>
)}
Expand Down
18 changes: 10 additions & 8 deletions apps/web/src/components/explorer/explorer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SearchIcon } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'react-router'

import { Input } from '@/components/ui/input'
Expand All @@ -17,6 +18,7 @@ export function Explorer() {
const search = useDebouncedValue(input.trim())
const top = useRef<HTMLDivElement>(null)
const awards = params.has('award')
const { t } = useTranslation()

useEffect(() => {
if (awards) top.current?.scrollIntoView({ behavior: 'smooth' })
Expand All @@ -40,17 +42,17 @@ export function Explorer() {
className="flex scroll-mt-4 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<TabsList>
<TabsTrigger value="projects">Projects</TabsTrigger>
<TabsTrigger value="contracts">Contracts</TabsTrigger>
<TabsTrigger value="wasm">Wasm</TabsTrigger>
<TabsTrigger value="failed">Failed</TabsTrigger>
<TabsTrigger value="projects">{t('explorer.tabs.projects')}</TabsTrigger>
<TabsTrigger value="contracts">{t('explorer.tabs.contracts')}</TabsTrigger>
<TabsTrigger value="wasm">{t('explorer.tabs.wasm')}</TabsTrigger>
<TabsTrigger value="failed">{t('explorer.tabs.failed')}</TabsTrigger>
</TabsList>
<div className="relative w-full sm:max-w-xs">
<SearchIcon className="absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={input}
onChange={(event) => setInput(event.target.value)}
placeholder="Search address, hash or repo"
placeholder={t('explorer.searchPlaceholder')}
className="pl-8"
/>
</div>
Expand All @@ -65,7 +67,7 @@ export function Explorer() {
search={search}
columns={contractColumns}
rowKey={(row) => row.contract_id}
empty="No contracts match."
empty={t('explorer.noContracts')}
/>
</TabsContent>
<TabsContent value="wasm">
Expand All @@ -75,7 +77,7 @@ export function Explorer() {
search={search}
columns={wasmColumns}
rowKey={(row) => row.tx_hash}
empty="No wasm uploads match."
empty={t('explorer.noWasm')}
/>
</TabsContent>
<TabsContent value="failed">
Expand All @@ -85,7 +87,7 @@ export function Explorer() {
search={search}
columns={failedColumns}
rowKey={(row) => row.tx_hash}
empty="No failed transactions match."
empty={t('explorer.noFailed')}
/>
</TabsContent>
</Tabs>
Expand Down
Loading