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
50 changes: 36 additions & 14 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "jsx-a11y", "unicorn"],
"jsPlugins": ["eslint-plugin-i18next", "eslint-plugin-file-component-constraints", "eslint-plugin-unused-imports"],
"plugins": [
"react",
"typescript",
"jsx-a11y",
"unicorn"
],
"jsPlugins": [
"eslint-plugin-i18next",
"eslint-plugin-file-component-constraints",
"eslint-plugin-unused-imports"
],
"categories": {
"correctness": "warn",
"suspicious": "warn",
Expand All @@ -19,7 +28,6 @@
"eqeqeq": "error",
"no-var": "error",
"prefer-const": "warn",

"react/react-in-jsx-scope": "off",
"react/jsx-key": "error",
"react/no-direct-mutation-state": "error",
Expand All @@ -28,40 +36,43 @@
"react/jsx-no-useless-fragment": "warn",
"react/jsx-curly-brace-presence": "warn",
"react/no-array-index-key": "warn",

"typescript/no-explicit-any": "error",
"typescript/prefer-ts-expect-error": "warn",
"typescript/no-non-null-assertion": "warn",
"typescript/consistent-type-imports": "error",

"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-is-valid": "warn",

"unicorn/no-null": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/require-module-specifiers": "off",

"file-component-constraints/enforce": [
"error",
{
"rules": [
{
"fileMatch": "**/sheets/Miniapp*.tsx",
"mustUse": ["MiniappSheetHeader"],
"mustUse": [
"MiniappSheetHeader"
],
"mustImportFrom": {
"MiniappSheetHeader": ["@/components/ecosystem"]
"MiniappSheetHeader": [
"@/components/ecosystem"
]
}
}
]
}
],

"i18next/no-literal-string": [
"warn",
"error",
{
"mode": "jsx-only",
"jsx-components": {
"exclude": ["Trans", "Icon", "TablerIcon"]
"exclude": [
"Trans",
"Icon",
"TablerIcon"
]
},
"jsx-attributes": {
"exclude": [
Expand Down Expand Up @@ -275,7 +286,18 @@
"^:$",
"^:$",
"^daysAgo$",
"^yesterday$"
"^yesterday$",
"^°$",
"^9\\+$",
"two-step-error",
"two-step-secret-error",
"Chain not supported",
"^Chain:",
"^Address:",
"^loading$",
"loading...",
"^--$",
"^≈ --$"
]
}
}
Expand Down Expand Up @@ -314,4 +336,4 @@
"version": "19"
}
}
}
}
4 changes: 2 additions & 2 deletions packages/key-ui/src/qr-code/QRCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export function QRCode({
{renderFn ? (
renderFn({ value, size, level })
) : (
<div
<div
className="flex items-center justify-center bg-muted text-muted-foreground text-xs"
style={{ width: size, height: size }}
>
QR: {value.slice(0, 20)}...
{value.slice(0, 20)}...
</div>
)}
{logoUrl && (
Expand Down
59 changes: 50 additions & 9 deletions scripts/i18n-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ interface CheckResult {
locale: string
missing: string[]
extra: string[]
untranslated: string[] // Keys with [MISSING:xx] placeholder
}

// ==================== Utilities ====================
Expand All @@ -92,6 +93,25 @@ function extractKeys(obj: TranslationFile, prefix = ''): string[] {
return keys
}

/**
* Find keys with [MISSING:xx] placeholder values
*/
function findUntranslatedKeys(obj: TranslationFile, prefix = ''): string[] {
const untranslated: string[] = []

for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key

if (typeof value === 'object' && value !== null) {
untranslated.push(...findUntranslatedKeys(value as TranslationFile, fullKey))
} else if (typeof value === 'string' && value.startsWith('[MISSING:')) {
untranslated.push(fullKey)
}
}

return untranslated
}

/**
* Get value at a nested path
*/
Expand Down Expand Up @@ -206,6 +226,7 @@ function checkNamespace(namespace: string, fix: boolean, verbose: boolean): Chec
locale,
missing: [...refKeys],
extra: [],
untranslated: [],
})
continue
}
Expand All @@ -214,21 +235,23 @@ function checkNamespace(namespace: string, fix: boolean, verbose: boolean): Chec
const localeKeys = new Set(extractKeys(localeData))

const diff = compareKeys(refKeys, localeKeys)
const untranslated = findUntranslatedKeys(localeData)

if (diff.missing.length > 0 || diff.extra.length > 0) {
if (diff.missing.length > 0 || diff.extra.length > 0 || untranslated.length > 0) {
results.push({
namespace,
locale,
missing: diff.missing,
extra: diff.extra,
untranslated,
})

// Fix missing keys if requested
if (fix && diff.missing.length > 0) {
for (const key of diff.missing) {
const refValue = getNestedValue(refData, key)
const placeholder = typeof refValue === 'string'
? `[MISSING:${locale}] ${refValue}`
const placeholder = typeof refValue === 'string'
? `[MISSING:${locale}] ${refValue}`
: refValue
setNestedValue(localeData, key, placeholder as TranslationValue)
}
Expand Down Expand Up @@ -277,8 +300,9 @@ ${colors.cyan}╔═════════════════════

const hasMissingKeys = allResults.some((r) => r.missing.length > 0)
const hasExtraKeys = allResults.some((r) => r.extra.length > 0)
const hasUntranslated = allResults.some((r) => r.untranslated.length > 0)

if (!hasMissingKeys && !hasExtraKeys) {
if (!hasMissingKeys && !hasExtraKeys && !hasUntranslated) {
log.success('All translations are complete!')
console.log(`
${colors.green}✓ All ${namespaces.length} namespaces checked across ${LOCALES.length} locales${colors.reset}
Expand All @@ -297,15 +321,18 @@ ${colors.green}✓ All ${namespaces.length} namespaces checked across ${LOCALES.

let totalMissing = 0
let totalExtra = 0
let totalUntranslated = 0

for (const [locale, results] of byLocale) {
const missingCount = results.reduce((sum, r) => sum + r.missing.length, 0)
const extraCount = results.reduce((sum, r) => sum + r.extra.length, 0)
const untranslatedCount = results.reduce((sum, r) => sum + r.untranslated.length, 0)

if (missingCount === 0 && extraCount === 0) continue
if (missingCount === 0 && extraCount === 0 && untranslatedCount === 0) continue

totalMissing += missingCount
totalExtra += extraCount
totalUntranslated += untranslatedCount

console.log(`\n${colors.bold}${locale}${colors.reset}`)

Expand All @@ -329,26 +356,40 @@ ${colors.green}✓ All ${namespaces.length} namespaces checked across ${LOCALES.
log.dim(` ... and ${result.extra.length - 3} more`)
}
}

if (result.untranslated.length > 0) {
log.error(`${result.namespace}.json: ${result.untranslated.length} untranslated keys ([MISSING:] placeholders)`)
for (const key of result.untranslated.slice(0, 5)) {
log.dim(`! ${key}`)
}
if (result.untranslated.length > 5) {
log.dim(` ... and ${result.untranslated.length - 5} more`)
}
}
}
}

if (totalMissing > 0) {
if (totalMissing > 0 || totalUntranslated > 0) {
console.log(`
${colors.red}✗ Found issues:${colors.reset}
${colors.red}Missing: ${totalMissing} keys${colors.reset}
${totalMissing > 0 ? `${colors.red}Missing: ${totalMissing} keys${colors.reset}` : ''}
${totalUntranslated > 0 ? `${colors.red}Untranslated: ${totalUntranslated} keys (have [MISSING:] placeholder)${colors.reset}` : ''}
${colors.yellow}Extra: ${totalExtra} keys${colors.reset}
`)

if (!fix) {
if (!fix && totalMissing > 0) {
log.info(`Run with ${colors.cyan}--fix${colors.reset} to add missing keys with placeholder values`)
}
if (totalUntranslated > 0) {
log.info(`Fix [MISSING:xx] placeholders by providing actual translations`)
}

process.exit(1)
}

// Only extra keys - warn but don't fail
console.log(`
${colors.green}✓ No missing translations${colors.reset}
${colors.green}✓ No missing or untranslated keys${colors.reset}
${colors.yellow}Extra: ${totalExtra} keys (not in reference, can be cleaned up)${colors.reset}
`)
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/contact/contact-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
*/

import { QRCodeSVG } from 'qrcode.react';
import { useTranslation } from 'react-i18next';
import { ContactAvatar } from '@/components/common/contact-avatar';
import { generateAvatarFromAddress } from '@/lib/avatar-codec';
import { detectAddressFormat } from '@/lib/address-format';
import type { ContactAddressInfo } from '@/lib/qr-parser';
import { isBioforestChain } from '@/lib/crypto';
import { useTranslation } from 'react-i18next';

/** Address format standard colors */
const ADDRESS_FORMAT_COLORS = {
Expand Down
11 changes: 3 additions & 8 deletions src/components/ecosystem/app-stack-page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
/**
* AppStackPage - 应用堆栈页面
*
* Swiper 的第三页,作为小程序窗口的背景板
* 当没有激活应用时,此页禁用滑动
*/

import { useCallback } from 'react'
import { useStore } from '@tanstack/react-store'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import {
miniappRuntimeStore,
miniappRuntimeSelectors,
Expand All @@ -20,6 +14,7 @@ export interface AppStackPageProps {
}

export function AppStackPage({ className }: AppStackPageProps) {
const { t } = useTranslation('ecosystem')
const hasRunningApps = useStore(
miniappRuntimeStore,
miniappRuntimeSelectors.hasRunningApps
Expand Down Expand Up @@ -48,7 +43,7 @@ export function AppStackPage({ className }: AppStackPageProps) {
{/* 空状态提示(调试用,生产环境可移除) */}
{!hasRunningApps && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center opacity-0">
<p className="text-muted-foreground text-sm">应用堆栈</p>
<p className="text-muted-foreground text-sm">{t('stack.title')}</p>
</div>
)}
</div>
Expand Down
Loading