diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d965445..fac20e09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,10 @@ jobs: run: npm ci - name: Run TypeScript type check - run: npx tsc --noEmit + run: npm run type-check - tests-validate: - name: Tests (Validate) + test: + name: Test runs-on: ubuntu-latest steps: - name: Checkout code @@ -65,8 +65,8 @@ jobs: - name: Install dependencies run: npm ci - - name: Run validation tests - run: npm run test:validate + - name: Run tests + run: npm run test:ci spell-check: name: Spell Check @@ -120,7 +120,7 @@ jobs: ci-success: name: CI Success runs-on: ubuntu-latest - needs: [lint, type-check, tests-validate, spell-check, build] + needs: [lint, type-check, test, spell-check, build] if: always() steps: - name: Check all jobs diff --git a/.github/workflows/scheduled-checks.yml b/.github/workflows/scheduled-checks.yml index aa45a637..2c1ab7e3 100644 --- a/.github/workflows/scheduled-checks.yml +++ b/.github/workflows/scheduled-checks.yml @@ -97,7 +97,7 @@ jobs: # TODO: Configure GITHUB_TOKEN with appropriate permissions # The fetch-github-stars script may need authentication - name: Fetch GitHub stars - run: node scripts/fetch-github-stars.mjs + run: npm run fetch:github-stars env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} continue-on-error: true diff --git a/CLAUDE.md b/CLAUDE.md index 945161ec..3c155166 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,8 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Internationalization (i18n) +**Translation Resources Location:** All translation files are located in the `translations/` directory at the project root, organized by locale code (e.g., `translations/en/`, `translations/zh-Hans/`). + When creating or modifying any page, module, or data: -- **MUST support all configured languages (18 total):** +- **MUST support all configured languages (12 total):** - English (en) - German (de) - Spanish (es) diff --git a/cspell.json b/cspell.json index 3cb37cef..e80b2df2 100644 --- a/cspell.json +++ b/cspell.json @@ -43,6 +43,15 @@ "useGitignore": true, "ignorePaths": ["cloudflare-env.d.ts", "docs", ".claude"], "words": [ + "Autorisierungsaufträgen", + "Entwicklungsframework", + "İçgörüleri", + "Versionierungsschema", + "interoperáveis", + "mengkomunikasikan", + "önekiyle", + "sağlayıcılı", + "ccstatusline", "API'lerle", "aracidir", "Anthropics", @@ -120,6 +129,7 @@ "Tasarlanmistir", "tasarlandi", "Türkçe", + "TSESTree", "Vervollständigungstool", "agentco", "agentico", @@ -193,6 +203,8 @@ "kwaikatonai", "kwaikatonai", "Kwaikatonai", + "kwaikat", + "KwaiKAT", "laboratuvari", "linkedin", "mengdebug", diff --git a/manifests/collections.json b/manifests/collections.json index 329b2c82..a94f8a38 100644 --- a/manifests/collections.json +++ b/manifests/collections.json @@ -4,25 +4,146 @@ "title": "Specifications & Protocols", "description": "Essential standards and protocols for modern software development", "translations": { + "en": { + "title": "Specifications & Protocols", + "description": "Essential standards and protocols for modern software development" + }, + "de": { + "title": "Spezifikationen & Protokolle", + "description": "Wichtige Standards und Protokolle für die moderne Softwareentwicklung" + }, + "es": { + "title": "Especificaciones y Protocolos", + "description": "Estándares y protocolos esenciales para el desarrollo de software moderno" + }, + "fr": { + "title": "Spécifications et Protocoles", + "description": "Normes et protocoles essentiels pour le développement logiciel moderne" + }, + "id": { + "title": "Spesifikasi & Protokol", + "description": "Standar dan protokol penting untuk pengembangan perangkat lunak modern" + }, + "ja": { + "title": "仕様とプロトコル", + "description": "現代のソフトウェア開発に不可欠な標準とプロトコル" + }, + "ko": { + "title": "사양 및 프로토콜", + "description": "현대 소프트웨어 개발을 위한 필수 표준 및 프로토콜" + }, + "pt": { + "title": "Especificações e Protocolos", + "description": "Padrões e protocolos essenciais para desenvolvimento de software moderno" + }, + "ru": { + "title": "Спецификации и протоколы", + "description": "Основные стандарты и протоколы для современной разработки программного обеспечения" + }, + "tr": { + "title": "Spesifikasyonlar ve Protokoller", + "description": "Modern yazılım geliştirme için temel standartlar ve protokoller" + }, "zh-Hans": { "title": "规范与协议", "description": "现代软件开发的基本标准和协议" + }, + "zh-Hant": { + "title": "規範與協議", + "description": "現代軟體開發的基本標準和協議" } }, "sections": [ { "title": "Development Standards", "translations": { + "en": { + "title": "Development Standards" + }, + "de": { + "title": "Entwicklungsstandards" + }, + "es": { + "title": "Estándares de Desarrollo" + }, + "fr": { + "title": "Normes de Développement" + }, + "id": { + "title": "Standar Pengembangan" + }, + "ja": { + "title": "開発標準" + }, + "ko": { + "title": "개발 표준" + }, + "pt": { + "title": "Padrões de Desenvolvimento" + }, + "ru": { + "title": "Стандарты разработки" + }, + "tr": { + "title": "Geliştirme Standartları" + }, "zh-Hans": { "title": "开发标准" + }, + "zh-Hant": { + "title": "開發標準" } }, "items": [ { "translations": { + "en": { + "name": "Conventional Commits", + "description": "Lightweight convention for structured commit messages that enable automated changelog generation and semantic versioning" + }, + "de": { + "name": "Conventional Commits", + "description": "Leichtgewichtige Konvention für strukturierte Commit-Nachrichten, die automatische Changelog-Generierung und semantisches Versioning ermöglichen" + }, + "es": { + "name": "Conventional Commits", + "description": "Convención ligera para mensajes de confirmación estructurados que permite la generación automática de changelog y versionado semántico" + }, + "fr": { + "name": "Conventional Commits", + "description": "Convention légère pour les messages de commit structurés permettant la génération automatique de changelog et le versionnage sémantique" + }, + "id": { + "name": "Conventional Commits", + "description": "Konvensi ringan untuk pesan commit terstruktur yang memungkinkan pembuatan changelog otomatis dan versioning semantik" + }, + "ja": { + "name": "Conventional Commits", + "description": "自動変更履歴生成とセマンティックバージョニングを可能にする構造化コミットメッセージの軽量規約" + }, + "ko": { + "name": "Conventional Commits", + "description": "자동 변경 로그 생성 및 시맨틱 버전 관리를 가능하게 하는 구조화된 커밋 메시지를 위한 경량 규칙" + }, + "pt": { + "name": "Conventional Commits", + "description": "Convenção leve para mensagens de commit estruturadas que permitem geração automatizada de changelog e versionamento semântico" + }, + "ru": { + "name": "Conventional Commits", + "description": "Легковесная конвенция для структурированных сообщений коммитов, позволяющая автоматически генерировать журнал изменений и использовать семантическое версионирование" + }, + "tr": { + "name": "Conventional Commits", + "description": "Otomatik değişiklik kaydı oluşturma ve anlamsal sürüm oluşturmayı sağlayan yapılandırılmış commit mesajları için hafif bir kural" + }, "zh-Hans": { "name": "约定式提交", "description": "结构化提交消息的轻量级约定,支持自动化变更日志生成和语义化版本控制" + }, + "zh-Hant": { + "name": "約定式提交", + "description": "結構化提交訊息的輕量級約定,支援自動化變更日誌生成和語義化版本控制" } }, "name": "Conventional Commits", @@ -31,9 +152,53 @@ }, { "translations": { + "en": { + "name": "Semantic Versioning", + "description": "Versioning scheme using MAJOR.MINOR.PATCH format to communicate compatibility and impact of updates" + }, + "de": { + "name": "Semantic Versioning", + "description": "Versionierungsschema im Format MAJOR.MINOR.PATCH zur Kommunikation von Kompatibilität und Auswirkungen von Updates" + }, + "es": { + "name": "Semantic Versioning", + "description": "Esquema de versionado usando formato MAJOR.MINOR.PATCH para comunicar compatibilidad e impacto de actualizaciones" + }, + "fr": { + "name": "Semantic Versioning", + "description": "Schéma de versionnage utilisant le format MAJOR.MINOR.PATCH pour communiquer la compatibilité et l'impact des mises à jour" + }, + "id": { + "name": "Semantic Versioning", + "description": "Skema versioning menggunakan format MAJOR.MINOR.PATCH untuk mengkomunikasikan kompatibilitas dan dampak pembaruan" + }, + "ja": { + "name": "Semantic Versioning", + "description": "互換性と更新の影響を伝えるためのMAJOR.MINOR.PATCH形式のバージョニングスキーム" + }, + "ko": { + "name": "Semantic Versioning", + "description": "호환성 및 업데이트 영향을 전달하기 위해 MAJOR.MINOR.PATCH 형식을 사용하는 버전 관리 체계" + }, + "pt": { + "name": "Semantic Versioning", + "description": "Esquema de versionamento usando formato MAJOR.MINOR.PATCH para comunicar compatibilidade e impacto de atualizações" + }, + "ru": { + "name": "Semantic Versioning", + "description": "Схема версионирования, использующая формат MAJOR.MINOR.PATCH для обозначения совместимости и влияния обновлений" + }, + "tr": { + "name": "Semantic Versioning", + "description": "MAJOR.MINOR.PATCH formatını kullanarak uyumluluk ve güncelleme etkilerini ileten sürüm oluşturma şeması" + }, "zh-Hans": { "name": "语义化版本", "description": "使用 MAJOR.MINOR.PATCH 格式的版本控制方案,传达兼容性和更新影响" + }, + "zh-Hant": { + "name": "語義化版本", + "description": "使用 MAJOR.MINOR.PATCH 格式的版本控制方案,傳達相容性和更新影響" } }, "name": "Semantic Versioning", @@ -45,16 +210,93 @@ { "title": "Agent Protocols", "translations": { + "en": { + "title": "Agent Protocols" + }, + "de": { + "title": "Agent-Protokolle" + }, + "es": { + "title": "Protocolos de Agentes" + }, + "fr": { + "title": "Protocoles d'Agents" + }, + "id": { + "title": "Protokol Agent" + }, + "ja": { + "title": "エージェントプロトコル" + }, + "ko": { + "title": "에이전트 프로토콜" + }, + "pt": { + "title": "Protocolos de Agentes" + }, + "ru": { + "title": "Протоколы агентов" + }, + "tr": { + "title": "Ajan Protokolleri" + }, "zh-Hans": { "title": "Agent 协议" + }, + "zh-Hant": { + "title": "Agent 協議" } }, "items": [ { "translations": { + "en": { + "name": "Agent2Agent (A2A)", + "description": "Open protocol enabling secure communication and collaboration between AI agents across different platforms" + }, + "de": { + "name": "Agent2Agent (A2A)", + "description": "Offenes Protokoll für sichere Kommunikation und Zusammenarbeit zwischen KI-Agenten über verschiedene Plattformen hinweg" + }, + "es": { + "name": "Agent2Agent (A2A)", + "description": "Protocolo abierto que permite comunicación y colaboración segura entre agentes de IA en diferentes plataformas" + }, + "fr": { + "name": "Agent2Agent (A2A)", + "description": "Protocole ouvert permettant une communication et une collaboration sécurisées entre les agents IA sur différentes plateformes" + }, + "id": { + "name": "Agent2Agent (A2A)", + "description": "Protokol terbuka yang memungkinkan komunikasi dan kolaborasi aman antar agen AI di berbagai platform" + }, + "ja": { + "name": "Agent2Agent (A2A)", + "description": "異なるプラットフォーム間でAIエージェント間の安全な通信とコラボレーションを可能にするオープンプロトコル" + }, + "ko": { + "name": "Agent2Agent (A2A)", + "description": "다양한 플랫폼에서 AI 에이전트 간의 안전한 통신과 협업을 가능하게 하는 개방형 프로토콜" + }, + "pt": { + "name": "Agent2Agent (A2A)", + "description": "Protocolo aberto que permite comunicação e colaboração segura entre agentes de IA em diferentes plataformas" + }, + "ru": { + "name": "Agent2Agent (A2A)", + "description": "Открытый протокол, обеспечивающий безопасную связь и взаимодействие между ИИ-агентами на разных платформах" + }, + "tr": { + "name": "Agent2Agent (A2A)", + "description": "Farklı platformlardaki AI ajanları arasında güvenli iletişim ve işbirliği sağlayan açık protokol" + }, "zh-Hans": { "name": "Agent 间通信协议 (A2A)", "description": "支持不同平台之间 AI Agent 安全通信和协作的开放协议" + }, + "zh-Hant": { + "name": "Agent 間通訊協議 (A2A)", + "description": "支援不同平台之間 AI Agent 安全通訊和協作的開放協議" } }, "name": "Agent2Agent (A2A)", @@ -63,20 +305,53 @@ }, { "translations": { - "zh-Hans": { - "name": "Agent 客户端协议 (ACP)", - "description": "标准化代码编辑器与 Agent 之间通信的开放协议,用于自主代码修改" - } - }, - "name": "Agent Client Protocol (ACP)", - "url": "https://agentclientprotocol.com", - "description": "Open protocol standardizing communication between code editors and coding agents for autonomous code modification" - }, - { - "translations": { + "en": { + "name": "Agent Network Protocol (ANP)", + "description": "Decentralized protocol for trustless agent communication with DID-based identity and automatic protocol negotiation" + }, + "de": { + "name": "Agent Network Protocol (ANP)", + "description": "Dezentralisiertes Protokoll für vertrauenslose Agentenkommunikation mit DID-basierter Identität und automatischer Protokollverhandlung" + }, + "es": { + "name": "Agent Network Protocol (ANP)", + "description": "Protocolo descentralizado para comunicación sin confianza entre agentes con identidad basada en DID y negociación automática de protocolos" + }, + "fr": { + "name": "Agent Network Protocol (ANP)", + "description": "Protocole décentralisé pour la communication sans confiance entre agents avec identité basée sur DID et négociation automatique de protocoles" + }, + "id": { + "name": "Agent Network Protocol (ANP)", + "description": "Protokol terdesentralisasi untuk komunikasi agen tanpa kepercayaan dengan identitas berbasis DID dan negosiasi protok otomatis" + }, + "ja": { + "name": "Agent Network Protocol (ANP)", + "description": "DIDベースのIDと自動プロトコルネゴシエーションを備えた、信頼不要のエージェント通信のための分散型プロトコル" + }, + "ko": { + "name": "Agent Network Protocol (ANP)", + "description": "DID 기반 신원 및 자동 프로토콜 협상을 갖춘 신뢰 없는 에이전트 통신을 위한 분산형 프로토콜" + }, + "pt": { + "name": "Agent Network Protocol (ANP)", + "description": "Protocolo descentralizado para comunicação sem confiança entre agentes com identidade baseada em DID e negociação automática de protocolos" + }, + "ru": { + "name": "Agent Network Protocol (ANP)", + "description": "Децентрализованный протокол для бесправерочного взаимодействия агентов с идентификацией на основе DID и автоматическим согласованием протоколов" + }, + "tr": { + "name": "Agent Network Protocol (ANP)", + "description": "DID tabanlı kimlik ve otomatik protokol görüşmesi ile güven gerektirmeyen ajan iletişimi için merkeziyetsiz protokol" + }, "zh-Hans": { "name": "Agent 网络协议 (ANP)", "description": "基于 DID 身份和自动协议协商的去中心化无信任 Agent 通信协议" + }, + "zh-Hant": { + "name": "Agent 網路協議 (ANP)", + "description": "基於 DID 身份和自動協議協商的去中心化無信任 Agent 通訊協議" } }, "name": "Agent Network Protocol (ANP)", @@ -85,14 +360,266 @@ }, { "translations": { + "en": { + "name": "Agent Payments Protocol (AP2)", + "description": "Open protocol for secure agent-led payments using verifiable digital credentials and authorization mandates" + }, + "de": { + "name": "Agent Payments Protocol (AP2)", + "description": "Offenes Protokoll für sichere, agentengesteuerte Zahlungen mit verifizierbaren digitalen Nachweisen und Autorisierungsaufträgen" + }, + "es": { + "name": "Agent Payments Protocol (AP2)", + "description": "Protocolo abierto para pagos seguros dirigidos por agentes utilizando credenciales digitales verificables y mandatos de autorización" + }, + "fr": { + "name": "Agent Payments Protocol (AP2)", + "description": "Protocole ouvert pour les paiements sécurisés dirigés par des agents utilisant des informations d'identification numériques vérifiables et des mandats d'autorisation" + }, + "id": { + "name": "Agent Payments Protocol (AP2)", + "description": "Protokol terbuka untuk pembayaran aman yang dipimpin agen menggunakan kredensial digital yang dapat diverifikasi dan mandat otorisasi" + }, + "ja": { + "name": "Agent Payments Protocol (AP2)", + "description": "検証可能なデジタル認証情報と委任状を使用した、エージェント主導の安全な決済のためのオープンプロトコル" + }, + "ko": { + "name": "Agent Payments Protocol (AP2)", + "description": "검증 가능한 디지털 자격 증명 및 인증 위임을 사용하는 에이전트 주도의 보안 결제를 위한 개방형 프로토콜" + }, + "pt": { + "name": "Agent Payments Protocol (AP2)", + "description": "Protocolo aberto para pagamentos seguros liderados por agentes usando credenciais digitais verificáveis e mandatos de autorização" + }, + "ru": { + "name": "Agent Payments Protocol (AP2)", + "description": "Открытый протокол для безопасных платежей, инициируемых агентами, с использованием проверяемых цифровых учетных данных и полномочий на авторизацию" + }, + "tr": { + "name": "Agent Payments Protocol (AP2)", + "description": "Doğrulanabilir dijital kimlik bilgileri ve yetkilendirme mandaları kullanarak ajan liderliğinde güvenli ödemeler için açık protokol" + }, "zh-Hans": { "name": "Agent 支付协议 (AP2)", "description": "使用可验证数字凭证和授权委托的安全 Agent 支付开放协议" + }, + "zh-Hant": { + "name": "Agent 支付協議 (AP2)", + "description": "使用可驗證數字憑證和授權委託的安全 Agent 支付開放協議" } }, "name": "Agent Payments Protocol (AP2)", "url": "https://ap2-protocol.org", "description": "Open protocol for secure agent-led payments using verifiable digital credentials and authorization mandates" + }, + { + "translations": { + "en": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "Open standard for programmatic commerce flows between AI agents and businesses, enabling secure transactions and payments" + }, + "de": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "Offener Standard für programmgesteuerte Commerce-Flows zwischen KI-Agenten und Unternehmen, enabling sichere Transaktionen und Zahlungen" + }, + "es": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "Estándar abierto para flujos de comercio programáticos entre agentes de IA y empresas, habilitando transacciones y pagos seguros" + }, + "fr": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "Standard ouvert pour les flux commerciaux programmatiques entre les agents IA et les entreprises, permettant des transactions et paiements sécurisés" + }, + "id": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "Standar terbuka untuk alur perdagangan terprogram antara agen AI dan bisnis, memungkinkan transaksi dan pembayaran aman" + }, + "ja": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "AIエージェントと企業間のプログラマティックコマースフローのためのオープン標準、安全なトランザクションと支払いを実現" + }, + "ko": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "AI 에이전트와 기업 간의 프로그래매틱 상거래 흐름을 위한 개방형 표준, 안전한 거래 및 지불 가능" + }, + "pt": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "Padrão aberto para fluxos de comércio programático entre agentes de IA e empresas, permitindo transações e pagamentos seguros" + }, + "ru": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "Открытый стандарт для программных торговых потоков между ИИ-агентами и предприятиями, обеспечивающий безопасные транзакции и платежи" + }, + "tr": { + "name": "Agentic Commerce Protocol (ACP)", + "description": "AI ajanları ve işletmeler arasındaki programatik ticaret akışları için açık standart, güvenli işlemleri ve ödemeleri mümkün kılar" + }, + "zh-Hans": { + "name": "Agent 商务协议 (ACP)", + "description": "AI Agent 与企业之间程序化商业流程的开放标准,实现安全交易和支付" + }, + "zh-Hant": { + "name": "Agent 商務協議 (ACP)", + "description": "AI Agent 與企業之間程序化商業流程的開放標準,實現安全交易和支付" + } + }, + "name": "Agentic Commerce Protocol (ACP)", + "url": "https://www.agenticcommerce.dev", + "description": "Open standard for programmatic commerce flows between AI agents and businesses, enabling secure transactions and payments" + } + ] + }, + { + "title": "Others", + "translations": { + "en": { + "title": "Others" + }, + "de": { + "title": "Andere" + }, + "es": { + "title": "Otros" + }, + "fr": { + "title": "Autres" + }, + "id": { + "title": "Lainnya" + }, + "ja": { + "title": "その他" + }, + "ko": { + "title": "기타" + }, + "pt": { + "title": "Outros" + }, + "ru": { + "title": "Другие" + }, + "tr": { + "title": "Diğerleri" + }, + "zh-Hans": { + "title": "其他" + }, + "zh-Hant": { + "title": "其他" + } + }, + "items": [ + { + "translations": { + "en": { + "name": "Agent Client Protocol (ACP)", + "description": "Open protocol standardizing communication between code editors and coding agents for autonomous code modification" + }, + "de": { + "name": "Agent Client Protocol (ACP)", + "description": "Offenes Protokoll zur Standardisierung der Kommunikation zwischen Code-Editoren und Coding-Agents für autonome Codeänderungen" + }, + "es": { + "name": "Agent Client Protocol (ACP)", + "description": "Protocolo abierto que estandariza la comunicación entre editores de código y agentes de codificación para modificación autónoma de código" + }, + "fr": { + "name": "Agent Client Protocol (ACP)", + "description": "Protocole ouvert normalisant la communication entre les éditeurs de code et les agents de codage pour la modification autonome du code" + }, + "id": { + "name": "Agent Client Protocol (ACP)", + "description": "Protokol terbuka yang menstandarkan komunikasi antara editor kode dan agen pengoding untuk modifikasi kode otonom" + }, + "ja": { + "name": "Agent Client Protocol (ACP)", + "description": "自律的なコード変更のためのコードエディタとコーディングエージェント間の通信を標準化するオープンプロトコル" + }, + "ko": { + "name": "Agent Client Protocol (ACP)", + "description": "자율적인 코드 수정을 위한 코드 편집기와 코딩 에이전트 간의 통신을 표준화하는 개방형 프로토콜" + }, + "pt": { + "name": "Agent Client Protocol (ACP)", + "description": "Protocolo aberto que padroniza a comunicação entre editores de código e agentes de codificação para modificação autônoma de código" + }, + "ru": { + "name": "Agent Client Protocol (ACP)", + "description": "Открытый протокол, стандартизирующий взаимодействие между редакторами кода и агентами программирования для автономного изменения кода" + }, + "tr": { + "name": "Agent Client Protocol (ACP)", + "description": "Otonom kod değişikliği için kod editörleri ve kodlama ajanları arasındaki iletişimi standartlaştıran açık protokol" + }, + "zh-Hans": { + "name": "Agent 客户端协议 (ACP)", + "description": "标准化代码编辑器与 Agent 之间通信的开放协议,用于自主代码修改" + }, + "zh-Hant": { + "name": "Agent 客戶端協議 (ACP)", + "description": "標準化代碼編輯器與 Agent 之間通訊的開放協議,用於自主代碼修改" + } + }, + "name": "Agent Client Protocol (ACP)", + "url": "https://agentclientprotocol.com", + "description": "Open protocol standardizing communication between code editors and coding agents for autonomous code modification" + }, + { + "translations": { + "en": { + "name": "Open Responses", + "description": "Open specification for multi-provider, interoperable LLM interfaces based on OpenAI Responses API" + }, + "de": { + "name": "Open Responses", + "description": "Offene Spezifikation für Multi-Provider-, interoperable LLM-Schnittstellen basierend auf der OpenAI Responses API" + }, + "es": { + "name": "Open Responses", + "description": "Especificación abierta para interfaces LLM interoperables y multiproveedor basada en la API de OpenAI Responses" + }, + "fr": { + "name": "Open Responses", + "description": "Spécification ouverte pour les interfaces LLM interopérables et multi-fournisseurs basée sur l'API OpenAI Responses" + }, + "id": { + "name": "Open Responses", + "description": "Spesifikasi terbuka untuk antarmuka LLM multi-penyedia yang dapat dioperasikan berdasarkan OpenAI Responses API" + }, + "ja": { + "name": "Open Responses", + "description": "OpenAI Responses APIに基づくマルチプロバイダー、相互運用可能なLLMインターフェースのオープン仕様" + }, + "ko": { + "name": "Open Responses", + "description": "OpenAI Responses API를 기반으로 하는 다중 공급업체 상호 운용 가능 LLM 인터페이스의 개방형 사양" + }, + "pt": { + "name": "Open Responses", + "description": "Especificação aberta para interfaces LLM interoperáveis e multi-fornecedor baseada na OpenAI Responses API" + }, + "ru": { + "name": "Open Responses", + "description": "Открытая спецификация для многоязычных, совместимых интерфейсов LLM на основе OpenAI Responses API" + }, + "tr": { + "name": "Open Responses", + "description": "OpenAI Responses API tabanlı çok sağlayıcılı, birlikte çalışabilir LLM arayüzleri için açık spesifikasyon" + }, + "zh-Hans": { + "name": "Open Responses", + "description": "基于 OpenAI Responses API 的多提供商、可互操作 LLM 接口的开放规范" + }, + "zh-Hant": { + "name": "Open Responses", + "description": "基於 OpenAI Responses API 的多提供商、可互操作 LLM 介面的開放規範" + } + }, + "name": "Open Responses", + "url": "https://www.openresponses.org", + "description": "Open specification for multi-provider, interoperable LLM interfaces based on OpenAI Responses API" } ] } @@ -102,25 +629,146 @@ "title": "Featured Articles", "description": "Must-read articles about AI coding from industry leaders", "translations": { + "en": { + "title": "Featured Articles", + "description": "Must-read articles about AI coding from industry leaders" + }, + "de": { + "title": "Empfohlene Artikel", + "description": "Unverzichtbare Artikel über AI-Coding von Branchenführern" + }, + "es": { + "title": "Artículos Destacados", + "description": "Artículos indispensables sobre codificación AI de líderes de la industria" + }, + "fr": { + "title": "Articles en Vedette", + "description": "Articles incontournables sur le codage IA par des leaders de l'industrie" + }, + "id": { + "title": "Artikel Pilihan", + "description": "Artikel wajib baca tentang AI coding dari pemimpin industri" + }, + "ja": { + "title": "注目記事", + "description": "業界リーダーによるAIコーディングに関する必読記事" + }, + "ko": { + "title": "추천 기사", + "description": "업계 리더들의 AI 코딩에 대한 필독 기사" + }, + "pt": { + "title": "Artigos em Destaque", + "description": "Artigos obrigatórios sobre codificação AI de líderes da indústria" + }, + "ru": { + "title": "Рекомендуемые статьи", + "description": "Обязательные статьи об AI-программировании от лидеров отрасли" + }, + "tr": { + "title": "Öne Çıkan Makaleler", + "description": "Sektör liderlerinden AI kodlama hakkında okunması gereken makaleler" + }, "zh-Hans": { "title": "精选文章", "description": "来自行业领袖的 AI 编码必读文章" + }, + "zh-Hant": { + "title": "精選文章", + "description": "來自行業領袖的 AI 編碼必讀文章" } }, "sections": [ { "title": "OpenAI on Coding", "translations": { + "en": { + "title": "OpenAI on Coding" + }, + "de": { + "title": "OpenAI über Codierung" + }, + "es": { + "title": "OpenAI sobre Codificación" + }, + "fr": { + "title": "OpenAI sur le Codage" + }, + "id": { + "title": "OpenAI tentang Coding" + }, + "ja": { + "title": "コーディングに関するOpenAI" + }, + "ko": { + "title": "코딩에 관한 OpenAI" + }, + "pt": { + "title": "OpenAI sobre Codificação" + }, + "ru": { + "title": "OpenAI о программировании" + }, + "tr": { + "title": "Kodlama Hakkında OpenAI" + }, "zh-Hans": { "title": "OpenAI 关于编码" + }, + "zh-Hant": { + "title": "OpenAI 關於編碼" } }, "items": [ { "translations": { + "en": { + "name": "Building AI-Powered Applications", + "description": "Best practices for deploying AI applications in production" + }, + "de": { + "name": "KI-gestützte Anwendungen erstellen", + "description": "Bewährte Methoden für die Bereitstellung von KI-Anwendungen in der Produktion" + }, + "es": { + "name": "Construcción de Aplicaciones Impulsadas por IA", + "description": "Mejores prácticas para desplegar aplicaciones de IA en producción" + }, + "fr": { + "name": "Créer des Applications Alimentées par l'IA", + "description": "Meilleures pratiques pour déployer des applications IA en production" + }, + "id": { + "name": "Membangun Aplikasi Bertenaga AI", + "description": "Praktik terbaik untuk men-deploy aplikasi AI di lingkungan produksi" + }, + "ja": { + "name": "AI搭載アプリケーションの構築", + "description": "本番環境でAIアプリケーションを展開するためのベストプラクティス" + }, + "ko": { + "name": "AI 기반 애플리케이션 구축", + "description": "프로덕션 환경에서 AI 애플리케이션 배포를 위한 모범 사례" + }, + "pt": { + "name": "Construindo Aplicações Alimentadas por AI", + "description": "Melhores práticas para implantar aplicações de AI em produção" + }, + "ru": { + "name": "Создание приложений на базе ИИ", + "description": "Лучшие практики развертывания приложений ИИ в производственной среде" + }, + "tr": { + "name": "AI Destekli Uygulamalar Oluşturma", + "description": "AI uygulamalarını prodüksiyonda dağıtmak için en iyi uygulamalar" + }, "zh-Hans": { "name": "构建 AI 驱动的应用", "description": "在生产环境中部署 AI 应用的最佳实践" + }, + "zh-Hant": { + "name": "構建 AI 驅動的應用", + "description": "在生產環境中部署 AI 應用的最佳實踐" } }, "name": "Building AI-Powered Applications", @@ -129,9 +777,53 @@ }, { "translations": { + "en": { + "name": "Prompt Engineering Guide", + "description": "Strategies for crafting effective prompts for code generation" + }, + "de": { + "name": "Prompt-Engineering-Leitfaden", + "description": "Strategien für die Erstellung effektiver Prompts für Codegenerierung" + }, + "es": { + "name": "Guía de Ingeniería de Prompts", + "description": "Estrategias para crear prompts efectivos para generación de código" + }, + "fr": { + "name": "Guide de l'Ingénierie des Prompts", + "description": "Stratégies pour crafting des prompts efficaces pour la génération de code" + }, + "id": { + "name": "Panduan Prompt Engineering", + "description": "Strategi untuk crafting prompt efektif untuk generasi kode" + }, + "ja": { + "name": "プロンプトエンジニアリングガイド", + "description": "コード生成のための効果的なプロンプトを作成する戦略" + }, + "ko": { + "name": "프롬프트 엔지니어링 가이드", + "description": "코드 생성을 위한 효과적인 프롬프트 작성 전략" + }, + "pt": { + "name": "Guia de Engenharia de Prompts", + "description": "Estratégias para criar prompts eficazes para geração de código" + }, + "ru": { + "name": "Руководство по prompt engineering", + "description": "Стратегии создания эффективных промптов для генерации кода" + }, + "tr": { + "name": "Prompt Mühendisliği Kılavuzu", + "description": "Kod üretimi için etkili istemler oluşturma stratejileri" + }, "zh-Hans": { "name": "提示工程指南", "description": "制作有效代码生成提示的策略" + }, + "zh-Hant": { + "name": "提示工程指南", + "description": "製作有效代碼生成提示的策略" } }, "name": "Prompt Engineering Guide", @@ -143,16 +835,93 @@ { "title": "Cursor Insights", "translations": { + "en": { + "title": "Cursor Insights" + }, + "de": { + "title": "Cursor-Erkenntnisse" + }, + "es": { + "title": "Informaciones de Cursor" + }, + "fr": { + "title": "Aperçus de Cursor" + }, + "id": { + "title": "Wawasan Cursor" + }, + "ja": { + "title": "Cursor の洞察" + }, + "ko": { + "title": "Cursor 인사이트" + }, + "pt": { + "title": "Insights do Cursor" + }, + "ru": { + "title": "Инсайты Cursor" + }, + "tr": { + "title": "Cursor İçgörüleri" + }, "zh-Hans": { "title": "Cursor 见解" + }, + "zh-Hant": { + "title": "Cursor 見解" } }, "items": [ { "translations": { + "en": { + "name": "The Future of Programming", + "description": "How AI is reshaping the development workflow" + }, + "de": { + "name": "Die Zukunft des Programmierens", + "description": "Wie KI den Entwicklungs-Workflow neu gestaltet" + }, + "es": { + "name": "El Futuro de la Programación", + "description": "Cómo la IA está reconfigurando el flujo de trabajo de desarrollo" + }, + "fr": { + "name": "L'Avenir de la Programmation", + "description": "Comment l'IA redéfinit le flux de travail de développement" + }, + "id": { + "name": "Masa Depang Programming", + "description": "Bagaimana AI membentuk kembali workflow pengembangan" + }, + "ja": { + "name": "プログラミングの未来", + "description": "AIが開発ワークフローをどのように再構築しているか" + }, + "ko": { + "name": "프로그래밍의 미래", + "description": "AI가 개발 워크플로우를 어떻게 재구성하는가" + }, + "pt": { + "name": "O Futuro da Programação", + "description": "Como AI está remodelando o fluxo de trabalho de desenvolvimento" + }, + "ru": { + "name": "Будущее программирования", + "description": "Как ИИ меняет рабочий процесс разработки" + }, + "tr": { + "name": "Programlamanın Geleceği", + "description": "AI geliştirme iş akışını nasıl yeniden şekillendiriyor" + }, "zh-Hans": { "name": "编程的未来", "description": "AI 如何重塑开发工作流" + }, + "zh-Hant": { + "name": "程式設計的未來", + "description": "AI 如何重塑開發工作流" } }, "name": "The Future of Programming", @@ -161,9 +930,53 @@ }, { "translations": { + "en": { + "name": "AI-First IDE Design", + "description": "Designing IDEs for seamless AI collaboration" + }, + "de": { + "name": "KI-First-IDE-Design", + "description": "IDEs für nahtlose KI-Zusammenarbeit entwerfen" + }, + "es": { + "name": "Diseño de IDE con IA en Primero", + "description": "Diseñando IDEs para colaboración IA fluida" + }, + "fr": { + "name": "Conception IDE IA en Premier", + "description": "Concevoir des IDE pour une collaboration IA fluide" + }, + "id": { + "name": "Desain IDE AI-First", + "description": "Merancang IDE untuk kolaborasi AI yang mulus" + }, + "ja": { + "name": "AIファーストIDE設計", + "description": "シームレスなAIコラボレーションのためのIDE設計" + }, + "ko": { + "name": "AI 우선 IDE 설계", + "description": "매끄러운 AI 협업을 위한 IDE 설계" + }, + "pt": { + "name": "Design de IDE AI em Primeiro", + "description": "Projetando IDEs para colaboração AI perfeita" + }, + "ru": { + "name": "Дизайн IDE с ИИ в первую очередь", + "description": "Проектирование IDE для бесшовного сотрудничества с ИИ" + }, + "tr": { + "name": "AI Öncelikli IDE Tasarımı", + "description": "Kusursuz AI işbirliği için IDE'ler tasarlama" + }, "zh-Hans": { "name": "AI 优先的 IDE 设计", "description": "设计无缝 AI 协作的 IDE" + }, + "zh-Hant": { + "name": "AI 優先的 IDE 設計", + "description": "設計無縫 AI 協作的 IDE" } }, "name": "AI-First IDE Design", @@ -175,16 +988,93 @@ { "title": "Claude on AI Coding", "translations": { + "en": { + "title": "Claude on AI Coding" + }, + "de": { + "title": "Claude über AI-Coding" + }, + "es": { + "title": "Claude sobre Codificación AI" + }, + "fr": { + "title": "Claude sur le Codage IA" + }, + "id": { + "title": "Claude tentang AI Coding" + }, + "ja": { + "title": "AIコーディングに関するClaude" + }, + "ko": { + "title": "AI 코딩에 관한 Claude" + }, + "pt": { + "title": "Claude sobre Codificação AI" + }, + "ru": { + "title": "Claude о AI-программировании" + }, + "tr": { + "title": "AI Kodlama Hakkında Claude" + }, "zh-Hans": { "title": "Claude 关于 AI 编码" + }, + "zh-Hant": { + "title": "Claude 關於 AI 編碼" } }, "items": [ { "translations": { + "en": { + "name": "Building with Claude Code", + "description": "Introduction to Claude Code and its capabilities" + }, + "de": { + "name": "Entwicklung mit Claude Code", + "description": "Einführung in Claude Code und seine Möglichkeiten" + }, + "es": { + "name": "Construcción con Claude Code", + "description": "Introducción a Claude Code y sus capacidades" + }, + "fr": { + "name": "Développement avec Claude Code", + "description": "Introduction à Claude Code et ses capacités" + }, + "id": { + "name": "Membangun dengan Claude Code", + "description": "Pengenalan Claude Code dan kemampuannya" + }, + "ja": { + "name": "Claude Code で構築する", + "description": "Claude Codeとその機能の紹介" + }, + "ko": { + "name": "Claude Code로 구축하기", + "description": "Claude Code 및 해당 기능 소개" + }, + "pt": { + "name": "Construindo com Claude Code", + "description": "Introdução ao Claude Code e suas capacidades" + }, + "ru": { + "name": "Разработка с Claude Code", + "description": "Введение в Claude Code и его возможности" + }, + "tr": { + "name": "Claude Code ile Geliştirme", + "description": "Claude Code ve yeteneklerine giriş" + }, "zh-Hans": { "name": "使用 Claude Code 构建", "description": "Claude Code 及其功能介绍" + }, + "zh-Hant": { + "name": "使用 Claude Code 構建", + "description": "Claude Code 及其功能介紹" } }, "name": "Building with Claude Code", @@ -193,9 +1083,53 @@ }, { "translations": { + "en": { + "name": "MCP: A New Standard", + "description": "How Model Context Protocol enables better AI integrations" + }, + "de": { + "name": "MCP: Ein Neuer Standard", + "description": "Wie das Model Context Protocol bessere KI-Integrationen ermöglicht" + }, + "es": { + "name": "MCP: Un Nuevo Estándar", + "description": "Cómo el Protocolo de Contexto de Modelo permite mejores integraciones IA" + }, + "fr": { + "name": "MCP: Un Nouveau Standard", + "description": "Comment le Protocole de Contexte de Modèle permet de meilleures intégrations IA" + }, + "id": { + "name": "MCP: Standar Baru", + "description": "Bagaimana Model Context Protocol memungkinkan integrasi AI yang lebih baik" + }, + "ja": { + "name": "MCP:新標準", + "description": "モデルコンテキストプロトコルがどのようにより良いAI統合を可能にするか" + }, + "ko": { + "name": "MCP: 새로운 표준", + "description": "모델 컨텍스트 프로토콜이 더 나은 AI 통합을 가능하게 하는 방법" + }, + "pt": { + "name": "MCP: Um Novo Padrão", + "description": "Como o Protocolo de Contexto de Modelo permite melhores integrações AI" + }, + "ru": { + "name": "MCP: Новый стандарт", + "description": "Как протокол контекста модели обеспечивает лучшую интеграцию ИИ" + }, + "tr": { + "name": "MCP: Yeni Bir Standard", + "description": "Model Bağlam Protokolü daha iyi AI entegrasyonları nasıl sağlar" + }, "zh-Hans": { "name": "MCP:新标准", "description": "模型上下文协议如何实现更好的 AI 集成" + }, + "zh-Hant": { + "name": "MCP:新標準", + "description": "模型上下文協議如何實現更好的 AI 集成" } }, "name": "MCP: A New Standard", @@ -207,16 +1141,93 @@ { "title": "Lovable Engineering", "translations": { + "en": { + "title": "Lovable Engineering" + }, + "de": { + "title": "Lovable Engineering" + }, + "es": { + "title": "Ingeniería Lovable" + }, + "fr": { + "title": "Ingénierie Lovable" + }, + "id": { + "title": "Lovable Engineering" + }, + "ja": { + "title": "Lovable Engineering" + }, + "ko": { + "title": "Lovable Engineering" + }, + "pt": { + "title": "Lovable Engineering" + }, + "ru": { + "title": "Lovable Engineering" + }, + "tr": { + "title": "Lovable Engineering" + }, "zh-Hans": { "title": "Lovable 工程" + }, + "zh-Hant": { + "title": "Lovable 工程" } }, "items": [ { "translations": { + "en": { + "name": "AI-Native Development", + "description": "Building applications designed for AI-first workflows" + }, + "de": { + "name": "KI-native Entwicklung", + "description": "Entwicklung von Anwendungen für KI-First-Workflows" + }, + "es": { + "name": "Desarrollo Nativo de IA", + "description": "Construcción de aplicaciones diseñadas para flujos de trabajo IA-first" + }, + "fr": { + "name": "Développement Natif IA", + "description": "Créer des applications conçues pour les flux de travail IA en premier" + }, + "id": { + "name": "Pengembangan AI-Native", + "description": "Membangun aplikasi yang dirancang untuk workflow AI-first" + }, + "ja": { + "name": "AIネイティブ開発", + "description": "AIファーストワークフロー向けに設計されたアプリケーションを構築する" + }, + "ko": { + "name": "AI 네이티브 개발", + "description": "AI 우선 워크플로우를 위해 설계된 애플리케이션 구축" + }, + "pt": { + "name": "Desenvolvimento AI-Nativo", + "description": "Construindo aplicações projetadas para workflows AI-first" + }, + "ru": { + "name": "AI-Native разработка", + "description": "Создание приложений, спроектированных для AI-first рабочих процессов" + }, + "tr": { + "name": "AI-Native Geliştirme", + "description": "AI öncelikli iş akışları için tasarlanmış uygulamalar oluşturma" + }, "zh-Hans": { "name": "AI 原生开发", "description": "构建为 AI 优先工作流设计的应用" + }, + "zh-Hant": { + "name": "AI 原生開發", + "description": "構建為 AI 優先工作流設計的應用" } }, "name": "AI-Native Development", @@ -225,9 +1236,53 @@ }, { "translations": { + "en": { + "name": "The Coding Copilot Pattern", + "description": "Common patterns for effective AI pair programming" + }, + "de": { + "name": "Das Coding-Copilot-Muster", + "description": "Gemeinsame Muster für effektives KI-Pair-Programming" + }, + "es": { + "name": "El Patrón de Copiloto de Codificación", + "description": "Patrones comunes para programación en pareja IA efectiva" + }, + "fr": { + "name": "Le Pattern Copilote de Codage", + "description": "Motifs courants pour la programmation en paire IA efficace" + }, + "id": { + "name": "Pola Coding Copilot", + "description": "Pola umum untuk pair programming AI yang efektif" + }, + "ja": { + "name": "コーディングコパイロットパターン", + "description": "効果的なAIペアプログラミングのための一般的なパターン" + }, + "ko": { + "name": "코딩 코파일럿 패턴", + "description": "효과적인 AI 페어 프로그래밍을 위한 일반적인 패턴" + }, + "pt": { + "name": "O Padrão Copiloto de Codificação", + "description": "Padrões comuns para programação em par AI eficaz" + }, + "ru": { + "name": "Паттерн coding-копилота", + "description": "Общие паттерны для эффективной pair-программирования с ИИ" + }, + "tr": { + "name": "Kodlama Copilot Pattern", + "description": "Etkili AI pair programming için yaygın patternlar" + }, "zh-Hans": { "name": "编码副驾驶模式", "description": "有效 AI 结对编程的常见模式" + }, + "zh-Hant": { + "name": "編碼副駕駛員模式", + "description": "有效 AI 結對編程的常見模式" } }, "name": "The Coding Copilot Pattern", @@ -242,25 +1297,146 @@ "title": "Ecosystem Tools", "description": "Curated tools and utilities for AI coding workflows", "translations": { + "en": { + "title": "Ecosystem Tools", + "description": "Curated tools and utilities for AI coding workflows" + }, + "de": { + "title": "Ökosystem-Tools", + "description": "Kuratierte Tools und Utilities für AI-Coding-Workflows" + }, + "es": { + "title": "Herramientas del Ecosistema", + "description": "Herramientas y utilidades curadas para flujos de trabajo de codificación AI" + }, + "fr": { + "title": "Outils de l'Écosystème", + "description": "Outils et utilitaires sélectionnés pour les flux de travail de codage IA" + }, + "id": { + "title": "Alat Ekosistem", + "description": "Tools dan utilitas terkurasi untuk workflow AI coding" + }, + "ja": { + "title": "エコシステムツール", + "description": "AIコーディングワークフロー向けの厳選ツールとユーティリティ" + }, + "ko": { + "title": "에코시스템 도구", + "description": "AI 코딩 워크플로우를 위한 큐레이팅된 도구 및 유틸리티" + }, + "pt": { + "title": "Ferramentas do Ecossistema", + "description": "Ferramentas e utilitários selecionados para workflows de codificação AI" + }, + "ru": { + "title": "Инструменты экосистемы", + "description": "Отобранные инструменты и утилиты для AI-программирования" + }, + "tr": { + "title": "Ekosistem Araçları", + "description": "AI kodlama iş akışları için küratörlü araçlar ve yardımcı programlar" + }, "zh-Hans": { "title": "生态系统工具", "description": "AI 编码工作流的精选工具和实用程序" + }, + "zh-Hant": { + "title": "生態系統工具", + "description": "AI 編碼工作流的精選工具和實用程序" } }, "sections": [ { "title": "Development Tools", "translations": { + "en": { + "title": "Development Tools" + }, + "de": { + "title": "Entwicklungstools" + }, + "es": { + "title": "Herramientas de Desarrollo" + }, + "fr": { + "title": "Outils de Développement" + }, + "id": { + "title": "Alat Pengembangan" + }, + "ja": { + "title": "開発ツール" + }, + "ko": { + "title": "개발 도구" + }, + "pt": { + "title": "Ferramentas de Desenvolvimento" + }, + "ru": { + "title": "Инструменты разработки" + }, + "tr": { + "title": "Geliştirme Araçları" + }, "zh-Hans": { "title": "开发工具" + }, + "zh-Hant": { + "title": "開發工具" } }, "items": [ { "translations": { + "en": { + "name": "OpenSpec", + "description": "Spec-driven development framework for AI coding assistants with organized change management and living documentation" + }, + "de": { + "name": "OpenSpec", + "description": "Spec-gesteuertes Entwicklungsframework für AI-Coding-Assistants mit organisiertem Change-Management und lebender Dokumentation" + }, + "es": { + "name": "OpenSpec", + "description": "Framework de desarrollo guiado por especificaciones para asistentes de codificación AI con gestión de cambios organizada y documentación viva" + }, + "fr": { + "name": "OpenSpec", + "description": "Framework de développement piloté par spec pour les assistants de codage IA avec gestion des changements organisée et documentation vivante" + }, + "id": { + "name": "OpenSpec", + "description": "Framework development spec-driven untuk asisten AI coding dengan manajemen perubahan terorganisir dan dokumentasi hidup" + }, + "ja": { + "name": "OpenSpec", + "description": " organized change managementとliving documentationを備えたAIコーディングアシスタント向けのspec-driven開発フレームワーク" + }, + "ko": { + "name": "OpenSpec", + "description": " organized change management과 living documentation을 갖춘 AI 코딩 어시스턴트를 위한 spec-driven development framework" + }, + "pt": { + "name": "OpenSpec", + "description": "Framework de desenvolvimento orientado a especificações para assistentes de codificação AI com gerenciamento de mudanças organizado e documentação viva" + }, + "ru": { + "name": "OpenSpec", + "description": "Фреймворк spec-driven разработки для AI-ассистентов программирования с организованным управлением изменениями и живой документацией" + }, + "tr": { + "name": "OpenSpec", + "description": "Organize change management ve living documentation ile AI kodlama asistanları için spec-driven development framework" + }, "zh-Hans": { "name": "OpenSpec", "description": "为 AI 编码助手设计的规范驱动开发框架,提供组织化的变更管理和活文档" + }, + "zh-Hant": { + "name": "OpenSpec", + "description": "為 AI 編碼助手設計的規範驅動開發框架,提供組織化的變更管理和活文檔" } }, "name": "OpenSpec", @@ -269,9 +1445,53 @@ }, { "translations": { + "en": { + "name": "Spec Kit", + "description": "Toolkit for Spec-Driven Development that generates working implementations from executable specifications" + }, + "de": { + "name": "Spec Kit", + "description": "Toolkit für Spec-Driven Development, das funktionierende Implementierungen aus ausführbaren Spezifikationen generiert" + }, + "es": { + "name": "Spec Kit", + "description": "Toolkit para Desarrollo Guiado por Especificaciones que genera implementaciones funcionales desde especificaciones ejecutables" + }, + "fr": { + "name": "Spec Kit", + "description": "Toolkit pour le Développement Piloté par Spec qui génère des implémentations fonctionnelles à partir de spécifications exécutables" + }, + "id": { + "name": "Spec Kit", + "description": "Toolkit untuk Spec-Driven Development yang menghasilkan implementasi yang berfungsi dari spesifikasi yang dapat dieksekusi" + }, + "ja": { + "name": "Spec Kit", + "description": "実行可能な仕様から動作する実装を生成するSpec-Driven Development用ツールキット" + }, + "ko": { + "name": "Spec Kit", + "description": "실행 가능한 사양에서 작동하는 구현을 생성하는 Spec-Driven Development용 툴킷" + }, + "pt": { + "name": "Spec Kit", + "description": "Toolkit para Desenvolvimento Orientado a Especificações que gera implementações funcionais a partir de especificações executáveis" + }, + "ru": { + "name": "Spec Kit", + "description": "Набор инструментов для Spec-Driven разработки, генерирующий работающие реализации из исполняемых спецификаций" + }, + "tr": { + "name": "Spec Kit", + "description": "Çalışan uygulamaları executable specifications'dan üreten Spec-Driven Development için araç seti" + }, "zh-Hans": { "name": "Spec Kit", "description": "从可执行规范生成工作实现的规范驱动开发工具包" + }, + "zh-Hant": { + "name": "Spec Kit", + "description": "從可執行規範生成工作實現的規範驅動開發工具包" } }, "name": "Spec Kit", @@ -280,9 +1500,53 @@ }, { "translations": { + "en": { + "name": "BMAD-METHOD", + "description": "Breakthrough Method for Agile AI-Driven Development with specialized AI agents for planning, architecture, and implementation" + }, + "de": { + "name": "BMAD-METHOD", + "description": "Durchbruchsmethode für agiles KI-getriebenes Entwickeln mit spezialisierten KI-Agenten für Planung, Architektur und Implementierung" + }, + "es": { + "name": "BMAD-METHOD", + "description": "Método innovador para Desarrollo Ágil Impulsado por IA con agentes IA especializados para planificación, arquitectura e implementación" + }, + "fr": { + "name": "BMAD-METHOD", + "description": "Méthode innovante pour le Développement Agile Piloté par l'IA avec des agents IA spécialisés pour la planification, l'architecture et l'implémentation" + }, + "id": { + "name": "BMAD-METHOD", + "description": "Metode terobosan untuk Agile AI-Driven Development dengan agen AI khusus untuk perencanaan, arsitektur, dan implementasi" + }, + "ja": { + "name": "BMAD-METHOD", + "description": "計画、アーキテクチャ、実装のために特化したAIエージェントを備えたアジャイルAI主導開発のための画期的な方法" + }, + "ko": { + "name": "BMAD-METHOD", + "description": "계획, 아키텍처, 구현을 위한 특화된 AI 에이전트를 갖춘 애자일 AI 주도 개발을 위한 획기적인 방법" + }, + "pt": { + "name": "BMAD-METHOD", + "description": "Método inovador para Desenvolvimento Ágil Impulsionado por AI com agentes AI especializados para planejamento, arquitetura e implementação" + }, + "ru": { + "name": "BMAD-METHOD", + "description": "Прорывной метод для Agile AI-Driven разработки со специализированными ИИ-агентами для планирования, архитектуры и реализации" + }, + "tr": { + "name": "BMAD-METHOD", + "description": "Planlama, mimari ve implementasyon için uzmanlaşmış AI ajanları ile Agile AI-Driven Development için çığır açan yöntem" + }, "zh-Hans": { "name": "BMAD-METHOD", "description": "敏捷 AI 驱动开发的突破性方法,配备专门的 AI Agent 用于规划、架构和实现" + }, + "zh-Hant": { + "name": "BMAD-METHOD", + "description": "敏捷 AI 驅動開發的突破性方法,配備專門的 AI Agent 用於規劃、架構和實現" } }, "name": "BMAD-METHOD", @@ -294,21 +1558,153 @@ { "title": "Productivity Utilities", "translations": { + "en": { + "title": "Productivity Utilities" + }, + "de": { + "title": "Produktivitäts-Utilities" + }, + "es": { + "title": "Utilidades de Productividad" + }, + "fr": { + "title": "Utilitaires de Productivité" + }, + "id": { + "title": "Utilitas Produktivitas" + }, + "ja": { + "title": "生産性ユーティリティ" + }, + "ko": { + "title": "생산성 유틸리티" + }, + "pt": { + "title": "Utilitários de Produtividade" + }, + "ru": { + "title": "Утилиты производительности" + }, + "tr": { + "title": "Verimlilik Araçları" + }, "zh-Hans": { "title": "生产力工具" + }, + "zh-Hant": { + "title": "生產力工具" } }, "items": [ { "translations": { + "en": { + "name": "ccusage", + "description": "Claude Code usage tracking and analytics tool" + }, + "de": { + "name": "ccusage", + "description": "Claude-Code-Nutzungsverfolgungs- und Analysetool" + }, + "es": { + "name": "ccusage", + "description": "Herramienta de seguimiento y análisis de uso de Claude Code" + }, + "fr": { + "name": "ccusage", + "description": "Outil de suivi et d'analyse d'utilisation de Claude Code" + }, + "id": { + "name": "ccusage", + "description": "Tool pelacakan dan analisis penggunaan Claude Code" + }, + "ja": { + "name": "ccusage", + "description": "Claude Codeの使用追跡・分析ツール" + }, + "ko": { + "name": "ccusage", + "description": "Claude Code 사용 추적 및 분석 도구" + }, + "pt": { + "name": "ccusage", + "description": "Ferramenta de rastreamento e análise de uso do Claude Code" + }, + "ru": { + "name": "ccusage", + "description": "Инструмент отслеживания и аналитики использования Claude Code" + }, + "tr": { + "name": "ccusage", + "description": "Claude Code kullanım takibi ve analiz aracı" + }, "zh-Hans": { "name": "ccusage", "description": "Claude Code 使用跟踪和分析工具" + }, + "zh-Hant": { + "name": "ccusage", + "description": "Claude Code 使用追蹤和分析工具" } }, "name": "ccusage", "url": "https://github.com/ryoppippi/ccusage", "description": "Claude Code usage tracking and analytics tool" + }, + { + "translations": { + "en": { + "name": "ccstatusline", + "description": "Status line tool for Claude Code displaying real-time usage statistics and quota information" + }, + "de": { + "name": "ccstatusline", + "description": "Statuszeilen-Tool für Claude Code mit Echtzeit-Nutzungsstatistiken und Kontingentinformationen" + }, + "es": { + "name": "ccstatusline", + "description": "Herramienta de línea de estado para Claude Code que muestra estadísticas de uso en tiempo real e información de cuota" + }, + "fr": { + "name": "ccstatusline", + "description": "Outil de ligne d'état pour Claude Code affichant les statistiques d'utilisation en temps réel et les informations de quota" + }, + "id": { + "name": "ccstatusline", + "description": "Tool status bar untuk Claude Code menampilkan statistik penggunaan real-time dan informasi kuota" + }, + "ja": { + "name": "ccstatusline", + "description": "リアルタイムの使用統計とクォータ情報を表示するClaude Codeのステータスラインツール" + }, + "ko": { + "name": "ccstatusline", + "description": "실시간 사용 통계 및 할당량 정보를 표시하는 Claude Code용 상태 표시줄 도구" + }, + "pt": { + "name": "ccstatusline", + "description": "Ferramenta de linha de status para Claude Code exibindo estatísticas de uso em tempo real e informações de quota" + }, + "ru": { + "name": "ccstatusline", + "description": "Инструмент строки состояния для Claude Code, отображающий статистику использования в реальном времени и информацию о квоте" + }, + "tr": { + "name": "ccstatusline", + "description": "Gerçek zamanlı kullanım istatistikleri ve kota bilgisi görüntüleyen Claude Code için durum çubuğu aracı" + }, + "zh-Hans": { + "name": "ccstatusline", + "description": "Claude Code 状态栏工具,显示实时使用统计和配额信息" + }, + "zh-Hant": { + "name": "ccstatusline", + "description": "Claude Code 狀態欄工具,顯示即時使用統計和配額資訊" + } + }, + "name": "ccstatusline", + "url": "https://github.com/sirmalloc/ccstatusline", + "description": "Status line tool for Claude Code displaying real-time usage statistics and quota information" } ] } @@ -318,84 +1714,414 @@ "title": "Coding Features", "description": "Must-try features for AI coding workflows", "translations": { + "en": { + "title": "Coding Features", + "description": "Must-try features for AI coding workflows" + }, + "de": { + "title": "Coding-Features", + "description": "Unverzichtbare Features für AI-Coding-Workflows" + }, + "es": { + "title": "Características de Codificación", + "description": "Características indispensables para flujos de trabajo de codificación AI" + }, + "fr": { + "title": "Fonctionnalités de Codage", + "description": "Fonctionnalités indispensables pour les flux de travail de codage IA" + }, + "id": { + "title": "Fitur Coding", + "description": "Fitur yang harus dicoba untuk workflow AI coding" + }, + "ja": { + "title": "コーディング機能", + "description": "AIコーディングワークフロー向けの必須機能" + }, + "ko": { + "title": "코딩 기능", + "description": "AI 코딩 워크플로우를 위한 필수 기능" + }, + "pt": { + "title": "Funcionalidades de Codificação", + "description": "Funcionalidades obrigatórias para workflows de codificação AI" + }, + "ru": { + "title": "Функции программирования", + "description": "Обязательные функции для AI-программирования" + }, + "tr": { + "title": "Kodlama Özellikleri", + "description": "AI kodlama iş akışları için mutlaka denenmesi gereken özellikler" + }, "zh-Hans": { "title": "编码功能", "description": "AI 编码工作流的必备功能" + }, + "zh-Hant": { + "title": "編碼功能", + "description": "AI 編碼工作流的必備功能" } }, "sections": [ { "title": "Open Standards", "translations": { + "en": { + "title": "Open Standards" + }, + "de": { + "title": "Offene Standards" + }, + "es": { + "title": "Estándares Abiertos" + }, + "fr": { + "title": "Standards Ouverts" + }, + "id": { + "title": "Standar Terbuka" + }, + "ja": { + "title": "オープン標準" + }, + "ko": { + "title": "개방형 표준" + }, + "pt": { + "title": "Padrões Abertos" + }, + "ru": { + "title": "Открытые стандарты" + }, + "tr": { + "title": "Açık Standartlar" + }, "zh-Hans": { "title": "开放标准" + }, + "zh-Hant": { + "title": "開放標準" } }, "items": [ { - "name": "Agent Skills", - "url": "https://agentskills.io", - "description": "Modular capabilities that extend Agent's functionality through filesystem-based resources containing instructions, metadata, and optional scripts, enabling domain-specific expertise and workflows", "translations": { + "en": { + "name": "Agent Skills", + "description": "Modular capabilities that extend Agent's functionality through filesystem-based resources containing instructions, metadata, and optional scripts, enabling domain-specific expertise and workflows" + }, + "de": { + "name": "Agent Skills", + "description": "Modulare Fähigkeiten, die Agenten durch dateisystembasierte Ressourcen mit Anweisungen und Skripten erweitern, domänenspezifische Expertise und Workflows ermöglichen" + }, + "es": { + "name": "Habilidades de Agente", + "description": "Capacidades modulares que extienden la funcionalidad del Agente a través de recursos del sistema con instrucciones y scripts opcionales, permitiendo experiencia específica del dominio y workflows" + }, + "fr": { + "name": "Compétences d'Agent", + "description": "Capacités modulaires qui étendent les fonctionnalités de l'Agent via des ressources du système avec instructions et scripts optionnels, permettant une expertise spécifique au domaine et workflows" + }, + "id": { + "name": "Keterampilan Agent", + "description": "Kemampuan modular yang memperluas fungsionalitas Agent melalui sumber daya berbasis filesystem yang berisi instruksi, metadata, dan skrip opsional, memungkinkan keahlian domain-specific dan workflow" + }, + "ja": { + "name": "Agent スキル", + "description": "命令、メタデータ、オプションスクリプトを含むファイルシステムベースのリソースを通じてAgentの機能を拡張するモジュラー機能で、ドメイン固有の専門知識とワークフローを実現します" + }, + "ko": { + "name": "에이전트 스킬", + "description": "명령, 메타데이터, 선택적 스크립트를 포함하는 파일 시스템 기반 리소스를 통해 에이전트의 기능을 확장하는 모듈형 기능으로, 도메인별 전문성 및 워크플로우를 가능하게 합니다" + }, + "pt": { + "name": "Habilidades de Agente", + "description": "Capacidades modulares que estendem a funcionalidade do Agente através de recursos do sistema de arquivos com instruções e scripts opcionais, permitindo expertise específica do domínio e workflows" + }, + "ru": { + "name": "Навыки агента", + "description": "Модульные возможности, расширяющие функциональность агента через ресурсы файловой системы с инструкциями и скриптами, обеспечивающие предметную экспертизу и рабочие процессы" + }, + "tr": { + "name": "Ajan Becerileri", + "description": "Dosya sistemi tabanlı kaynaklar aracılığıyla ajanın işlevselliğini genişleten modüler yetenekler, alanına özgü uzmanlık ve iş akışları sağlar" + }, "zh-Hans": { "name": "Agent 技能", "description": "模块化能力,通过基于文件系统的资源(包含指令、元数据和可选脚本)扩展 Agent 的功能,实现领域专业知识和工作流" + }, + "zh-Hant": { + "name": "Agent 技能", + "description": "模組化能力,通過基於文件系統的資源(包含指令、元數據和可選腳本)擴展 Agent 的功能,實現領域專業知識和工作流" } - } + }, + "name": "Agent Skills", + "url": "https://agentskills.io", + "description": "Modular capabilities that extend Agent's functionality through filesystem-based resources containing instructions, metadata, and optional scripts, enabling domain-specific expertise and workflows" }, { - "name": "Model Context Protocol (MCP)", - "url": "https://modelcontextprotocol.io", - "description": "Open standard connecting AI applications to external systems like data sources, tools, and workflows", "translations": { + "en": { + "name": "Model Context Protocol (MCP)", + "description": "Open standard connecting AI applications to external systems like data sources, tools, and workflows" + }, + "de": { + "name": "Model Context Protocol (MCP)", + "description": "Offener Standard zur Verbindung von KI-Anwendungen mit externen Systemen wie Datenquellen, Tools und Workflows" + }, + "es": { + "name": "Model Context Protocol (MCP)", + "description": "Estándar abierto que conecta aplicaciones de IA con sistemas externos como fuentes de datos, herramientas y flujos de trabajo" + }, + "fr": { + "name": "Model Context Protocol (MCP)", + "description": "Standard ouvert connectant les applications IA aux systèmes externes tels que les sources de données, les outils et les flux de travail" + }, + "id": { + "name": "Model Context Protocol (MCP)", + "description": "Standar terbuka yang menghubungkan aplikasi AI dengan sistem eksternal seperti sumber data, tools, dan workflow" + }, + "ja": { + "name": "Model Context Protocol (MCP)", + "description": "データソース、ツール、ワークフローなどの外部システムにAIアプリケーションを接続するオープン標準" + }, + "ko": { + "name": "Model Context Protocol (MCP)", + "description": "데이터 소스, 도구, 워크플로우와 같은 외부 시스템에 AI 애플리케이션을 연결하는 개방형 표준" + }, + "pt": { + "name": "Model Context Protocol (MCP)", + "description": "Padrão aberto conectando aplicações AI a sistemas externos como fontes de dados, ferramentas e workflows" + }, + "ru": { + "name": "Model Context Protocol (MCP)", + "description": "Открытый стандарт, соединяющий ИИ-приложения с внешними системами, такими как источники данных, инструменты и рабочие процессы" + }, + "tr": { + "name": "Model Context Protocol (MCP)", + "description": "AI uygulamalarını veri kaynakları, araçlar ve iş akışları gibi harici sistemlere bağlayan açık standart" + }, "zh-Hans": { "name": "模型上下文协议 (MCP)", "description": "将 AI 应用连接到外部系统(如数据源、工具和工作流)的开放标准" + }, + "zh-Hant": { + "name": "模型上下文協議 (MCP)", + "description": "將 AI 應用連接到外部系統(如數據源、工具和工作流)的開放標準" } - } + }, + "name": "Model Context Protocol (MCP)", + "url": "https://modelcontextprotocol.io", + "description": "Open standard connecting AI applications to external systems like data sources, tools, and workflows" }, { - "name": "AGENTS.md", - "url": "https://agents.md", - "description": "Open-format markdown file providing coding agents with project-specific instructions and context", "translations": { + "en": { + "name": "AGENTS.md", + "description": "Open-format markdown file providing coding agents with project-specific instructions and context" + }, + "de": { + "name": "AGENTS.md", + "description": "Markdown-Datei im offenen Format, die Coding-Agents mit projektspezifischen Anweisungen und Kontext versorgt" + }, + "es": { + "name": "AGENTS.md", + "description": "Archivo markdown de formato abierto que proporciona a los agentes de codificación instrucciones y contexto específicos del proyecto" + }, + "fr": { + "name": "AGENTS.md", + "description": "Fichier markdown au format ouvert fournissant aux agents de codage des instructions et un contexte spécifiques au projet" + }, + "id": { + "name": "AGENTS.md", + "description": "File markdown format terbuka yang menyediakan instruksi dan konteks spesifik proyek untuk agen coding" + }, + "ja": { + "name": "AGENTS.md", + "description": "コーディングエージェントにプロジェクト固有の命令とコンテキストを提供するオープンフォーマットのMarkdownファイル" + }, + "ko": { + "name": "AGENTS.md", + "description": "코딩 에이전트에게 프로젝트별 지침과 컨텍스트를 제공하는 개방형 형식의 마크다운 파일" + }, + "pt": { + "name": "AGENTS.md", + "description": "Arquivo markdown de formato aberto fornecendo aos agentes de codificação instruções e contexto específicos do projeto" + }, + "ru": { + "name": "AGENTS.md", + "description": "Файл markdown в открытом формате, предоставляющий кодирующим агентам проектные инструкции и контекст" + }, + "tr": { + "name": "AGENTS.md", + "description": "Kodlama ajanlarına proje özel talimatlar ve bağlam sağlayan açık formatlı markdown dosyası" + }, "zh-Hans": { "name": "AGENTS.md", "description": "为编码 Agent 提供项目特定指令和上下文的开放格式 Markdown 文件" + }, + "zh-Hant": { + "name": "AGENTS.md", + "description": "為編碼 Agent 提供項目特定指令和上下文的開放格式 Markdown 文件" } - } + }, + "name": "AGENTS.md", + "url": "https://agents.md", + "description": "Open-format markdown file providing coding agents with project-specific instructions and context" } ] }, { "title": "Vendor Standards", "translations": { + "en": { + "title": "Vendor Standards" + }, + "de": { + "title": "Anbieter-Standards" + }, + "es": { + "title": "Estándares de Proveedores" + }, + "fr": { + "title": "Normes des Fournisseurs" + }, + "id": { + "title": "Standar Vendor" + }, + "ja": { + "title": "ベンダー標準" + }, + "ko": { + "title": "공급업체 표준" + }, + "pt": { + "title": "Padrões de Fornecedores" + }, + "ru": { + "title": "Стандарты поставщиков" + }, + "tr": { + "title": "Satıcı Standartları" + }, "zh-Hans": { "title": "厂商标准" + }, + "zh-Hant": { + "title": "廠商標準" } }, "items": [ { - "name": "Slash Commands", - "url": "https://cursor.com/docs/agent/chat/commands", - "description": "Custom commands that create reusable workflows triggered with a `/` prefix in the chat input, stored as Markdown files in project, global, or team locations", "translations": { + "en": { + "name": "Slash Commands", + "description": "Custom commands that create reusable workflows triggered with a `/` prefix in the chat input, stored as Markdown files in project, global, or team locations" + }, + "de": { + "name": "Slash Commands", + "description": "Benutzerdefinierte Befehle, die wiederverwendbare Workflows erstellen, die mit einem `/`-Präfix im Chat ausgelöst werden, als Markdown-Dateien in Projekt-, globalen oder Team-Speicherorten gespeichert" + }, + "es": { + "name": "Comandos Slash", + "description": "Comandos personalizados que crean flujos de trabajo reutilizables activados con un prefijo `/` en el chat, almacenados como archivos Markdown en ubicaciones de proyecto, globales o de equipo" + }, + "fr": { + "name": "Commandes Slash", + "description": "Commandes personnalisés créant des flux de travail réutilisables déclenchés avec un préfixe `/` dans le chat, stockés en fichiers Markdown dans les emplacements de projet, globaux ou d'équipe" + }, + "id": { + "name": "Slash Commands", + "description": "Perintah kustom yang membuat workflow reusable yang dipicu dengan prefix `/` di input chat, disimpan sebagai file Markdown di lokasi proyek, global, atau tim" + }, + "ja": { + "name": "スラッシュコマンド", + "description": "チャット入力で`/`プレフィックスでトリガーされる再利用可能なワークフローを作成するカスタムコマンドで、プロジェクト、グローバル、チームの場所にMarkdownファイルとして保存されます" + }, + "ko": { + "name": "슬래시 명령어", + "description": "채팅 입력에서 `/` 접두사로 트리거되는 재사용 가능한 워크플로우를 만드는 사용자 정의 명령으로, 프로젝트, 전역 또는 팀 위치에 Markdown 파일로 저장됩니다" + }, + "pt": { + "name": "Comandos Slash", + "description": "Comandos personalizados que criam workflows reutilizáveis acionados com um prefixo `/` na entrada do chat, armazenados como arquivos Markdown em locais de projeto, global ou equipe" + }, + "ru": { + "name": "Слэш-команды", + "description": "Пользовательские команды, создающие повторно используемые рабочие процессы, запускаемые префиксом `/` в чате, сохраняемые в виде файлов Markdown в проектных, глобальных или командных расположениях" + }, + "tr": { + "name": "Slash Komutları", + "description": "Sohbet girdisinde `/` önekiyle tetiklenen yeniden kullanılabilir iş akışları oluşturan özel komutlar, proje, genel veya konumlarında Markdown dosyaları olarak saklanır" + }, "zh-Hans": { "name": "命令", "description": "自定义命令,可通过聊天输入框中的 `/` 前缀触发可重用工作流,以 Markdown 文件形式存储在项目、全局或团队位置" + }, + "zh-Hant": { + "name": "命令", + "description": "自訂命令,可通過聊天輸入框中的 `/` 前綴觸發可重用工作流,以 Markdown 文件形式存儲在項目、全局或團隊位置" } - } + }, + "name": "Slash Commands", + "url": "https://cursor.com/docs/agent/chat/commands", + "description": "Custom commands that create reusable workflows triggered with a `/` prefix in the chat input, stored as Markdown files in project, global, or team locations" }, { - "name": "Rules", - "url": "https://cursor.com/docs/context/rules", - "description": "System-level instructions for Agent that provide persistent, reusable context through Project Rules, User Rules, Team Rules, and AGENTS.md files", "translations": { + "en": { + "name": "Rules", + "description": "System-level instructions for Agent that provide persistent, reusable context through Project Rules, User Rules, Team Rules, and AGENTS.md files" + }, + "de": { + "name": "Regeln", + "description": "Systemweite Anweisungen für Agent, die beständigen, wiederverwendbaren Kontext durch Project Rules, User Rules, Team Rules und AGENTS.md-Dateien bereitstellen" + }, + "es": { + "name": "Reglas", + "description": "Instrucciones de nivel de sistema para el Agente que proporcionan contexto persistente y reutilizable a través de Project Rules, User Rules, Team Rules y archivos AGENTS.md" + }, + "fr": { + "name": "Règles", + "description": "Instructions au niveau du système pour l'Agent fournissant un contexte persistant et réutilisable via les Project Rules, User Rules, Team Rules et fichiers AGENTS.md" + }, + "id": { + "name": "Aturan", + "description": "Instruksi level sistem untuk Agent yang menyediakan context persisten, reusable melalui Project Rules, User Rules, Team Rules, dan file AGENTS.md" + }, + "ja": { + "name": "ルール", + "description": "プロジェクトルール、ユーザールール、チームルール、AGENTS.mdファイルを通じて永続的で再利用可能なコンテキストを提供するAgent向けのシステムレベル命令" + }, + "ko": { + "name": "규칙", + "description": "프로젝트 규칙, 사용자 규칙, 팀 규칙 및 AGENTS.md 파일을 통해 지속적이고 재사용 가능한 컨텍스트를 제공하는 에이전트용 시스템 수준 지침" + }, + "pt": { + "name": "Regras", + "description": "Instruções de nível de sistema para o Agente que fornecem contexto persistente e reutilizável através de Project Rules, User Rules, Team Rules e arquivos AGENTS.md" + }, + "ru": { + "name": "Правила", + "description": "Системные инструкции для агента, обеспечивающие постоянный, повторно используемый контекст через правила проекта, пользовательские правила, правила команды и файлы AGENTS.md" + }, + "tr": { + "name": "Kurallar", + "description": "Proje Kuralları, Kullanıcı Kuralları, Takım Kuralları ve AGENTS.md dosyaları aracılığıyla kalıcı, yeniden kullanılabilir bağlam sağlayan Agent için sistem düzeyi talimatlar" + }, "zh-Hans": { "name": "规则", "description": "为 Agent 提供系统级指令,通过项目规则、用户规则、团队规则和 AGENTS.md 文件提供持久、可重用的上下文" + }, + "zh-Hant": { + "name": "規則", + "description": "為 Agent 提供系統級指令,通過項目規則、用戶規則、團隊規則和 AGENTS.md 文件提供持久、可重用的上下文" } - } + }, + "name": "Rules", + "url": "https://cursor.com/docs/context/rules", + "description": "System-level instructions for Agent that provide persistent, reusable context through Project Rules, User Rules, Team Rules, and AGENTS.md files" } ] } diff --git a/manifests/extensions/gemini-code-assist.json b/manifests/extensions/gemini-code-assist.json index a0dc2d8c..2a9aa29c 100644 --- a/manifests/extensions/gemini-code-assist.json +++ b/manifests/extensions/gemini-code-assist.json @@ -3,8 +3,6 @@ "id": "gemini-code-assist", "name": "Gemini Code Assist", "description": "Google's AI coding assistant with code completion, generation, and optimization for IDEs.", - "type": "extension", - "productId": "gemini-code-assist", "translations": { "zh-Hans": { "description": "Google 的 AI 代码辅助工具,提供 IDE 中的代码补全、生成和优化功能。" @@ -98,5 +96,7 @@ "marketplaceUrl": "https://plugins.jetbrains.com/plugin/24513-gemini-code-assist", "installUri": null } - ] + ], + "type": "extension", + "productId": "gemini-code-assist" } diff --git a/manifests/models/claude-haiku-4-5.json b/manifests/models/claude-haiku-4-5.json index a408858a..02231ef9 100644 --- a/manifests/models/claude-haiku-4-5.json +++ b/manifests/models/claude-haiku-4-5.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": 0.355, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/claude-opus-4-1.json b/manifests/models/claude-opus-4-1.json index fd07c94e..99194de2 100644 --- a/manifests/models/claude-opus-4-1.json +++ b/manifests/models/claude-opus-4-1.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 74.4, "terminalBench": 0.631, - "sciCode": null, - "liveCodeBench": 46.9, "mmmu": 77.1, "mmmuPro": null, - "webDevArena": 147.9 + "webDevArena": 147.9, + "sciCode": null, + "liveCodeBench": 46.9 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/claude-opus-4-5.json b/manifests/models/claude-opus-4-5.json index 32e4ef0e..a7950ef6 100644 --- a/manifests/models/claude-opus-4-5.json +++ b/manifests/models/claude-opus-4-5.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/claude-opus-4.json b/manifests/models/claude-opus-4.json index f0ab05d8..a050dfc7 100644 --- a/manifests/models/claude-opus-4.json +++ b/manifests/models/claude-opus-4.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 67.6, "terminalBench": 0.351, - "sciCode": 1.5, - "liveCodeBench": 46.9, "mmmu": 76.5, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": 1.5, + "liveCodeBench": 46.9 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/claude-sonnet-4-5.json b/manifests/models/claude-sonnet-4-5.json index 9b165714..579760fc 100644 --- a/manifests/models/claude-sonnet-4-5.json +++ b/manifests/models/claude-sonnet-4-5.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 70.6, "terminalBench": 0.42, - "sciCode": null, - "liveCodeBench": 46.9, "mmmu": 77.8, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": 46.9 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/claude-sonnet-4.json b/manifests/models/claude-sonnet-4.json index 3c247807..d9ef64bf 100644 --- a/manifests/models/claude-sonnet-4.json +++ b/manifests/models/claude-sonnet-4.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 64.93, "terminalBench": 0.428, - "sciCode": null, - "liveCodeBench": 55.9, "mmmu": 74.4, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": 55.9 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/composer.json b/manifests/models/composer.json index 5301d3b1..5f8ebd61 100644 --- a/manifests/models/composer.json +++ b/manifests/models/composer.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/deepseek-3-2.json b/manifests/models/deepseek-3-2.json index 0dfd67a1..0058daee 100644 --- a/manifests/models/deepseek-3-2.json +++ b/manifests/models/deepseek-3-2.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/deepseek-ai/DeepSeek-V3.2", diff --git a/manifests/models/deepseek-r1.json b/manifests/models/deepseek-r1.json index 84065c86..0d5b6e61 100644 --- a/manifests/models/deepseek-r1.json +++ b/manifests/models/deepseek-r1.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": 4.6, - "liveCodeBench": 73.1, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": 4.6, + "liveCodeBench": 73.1 }, "platformUrls": { "huggingface": "https://huggingface.co/deepseek-ai/DeepSeek-R1", diff --git a/manifests/models/deepseek-v3-terminus.json b/manifests/models/deepseek-v3-terminus.json index ee1ea9d9..67c19a6f 100644 --- a/manifests/models/deepseek-v3-terminus.json +++ b/manifests/models/deepseek-v3-terminus.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": 3.1, - "liveCodeBench": 27.2, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": 3.1, + "liveCodeBench": 27.2 }, "platformUrls": { "huggingface": "https://huggingface.co/deepseek-ai/DeepSeek-V3.1-Terminus", diff --git a/manifests/models/gemini-2-5-flash.json b/manifests/models/gemini-2-5-flash.json index 01d3933d..b841ac69 100644 --- a/manifests/models/gemini-2-5-flash.json +++ b/manifests/models/gemini-2-5-flash.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 28.73, "terminalBench": 0.171, - "sciCode": null, - "liveCodeBench": 60.6, "mmmu": 79.7, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": 60.6 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gemini-2-5-pro.json b/manifests/models/gemini-2-5-pro.json index fe77781e..a24fbd16 100644 --- a/manifests/models/gemini-2-5-pro.json +++ b/manifests/models/gemini-2-5-pro.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 53.6, "terminalBench": 0.326, - "sciCode": 1.5, - "liveCodeBench": 71.8, "mmmu": 79.6, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": 1.5, + "liveCodeBench": 71.8 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gemini-3-flash.json b/manifests/models/gemini-3-flash.json index bc19171e..b9880f4c 100644 --- a/manifests/models/gemini-3-flash.json +++ b/manifests/models/gemini-3-flash.json @@ -47,8 +47,8 @@ "maxOutput": 65536, "tokenPricing": { "input": 0.5, - "cache": 0.05, - "output": 3 + "output": 3, + "cache": 0.05 }, "releaseDate": "2025-12-17", "lifecycle": "latest", @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gemini-3-pro.json b/manifests/models/gemini-3-pro.json index ef39639a..0b67dad9 100644 --- a/manifests/models/gemini-3-pro.json +++ b/manifests/models/gemini-3-pro.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 74.2, "terminalBench": 0.589, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/glm-4-6.json b/manifests/models/glm-4-6.json index 757f48b9..a915fb0a 100644 --- a/manifests/models/glm-4-6.json +++ b/manifests/models/glm-4-6.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 55.4, "terminalBench": 0.245, - "sciCode": null, - "liveCodeBench": null, "mmmu": 68, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/zai-org/GLM-4.6", diff --git a/manifests/models/glm-4-6v.json b/manifests/models/glm-4-6v.json index 6c2882e3..c092c835 100644 --- a/manifests/models/glm-4-6v.json +++ b/manifests/models/glm-4-6v.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": 0.245, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/zai-org/GLM-4.6V", diff --git a/manifests/models/glm-4-7.json b/manifests/models/glm-4-7.json index 8ae68c35..75c98602 100644 --- a/manifests/models/glm-4-7.json +++ b/manifests/models/glm-4-7.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/zai-org/GLM-4.7", diff --git a/manifests/models/gpt-4-1.json b/manifests/models/gpt-4-1.json index 7cf4e1bf..6a014e10 100644 --- a/manifests/models/gpt-4-1.json +++ b/manifests/models/gpt-4-1.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 39.58, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gpt-4o.json b/manifests/models/gpt-4o.json index 5c72bfbc..2b403d98 100644 --- a/manifests/models/gpt-4o.json +++ b/manifests/models/gpt-4o.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 21.62, "terminalBench": 0.401, - "sciCode": 1.5, - "liveCodeBench": 29.5, "mmmu": 70.7, "mmmuPro": null, - "webDevArena": 146.7 + "webDevArena": 146.7, + "sciCode": 1.5, + "liveCodeBench": 29.5 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gpt-5-1-codex.json b/manifests/models/gpt-5-1-codex.json index e090d362..58ea1622 100644 --- a/manifests/models/gpt-5-1-codex.json +++ b/manifests/models/gpt-5-1-codex.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": 0.604, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gpt-5-1.json b/manifests/models/gpt-5-1.json index 977617e0..77676bc8 100644 --- a/manifests/models/gpt-5-1.json +++ b/manifests/models/gpt-5-1.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 66, "terminalBench": 0.476, - "sciCode": null, - "liveCodeBench": null, "mmmu": 76, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gpt-5-2.json b/manifests/models/gpt-5-2.json index c447cb5e..b1bfab57 100644 --- a/manifests/models/gpt-5-2.json +++ b/manifests/models/gpt-5-2.json @@ -47,8 +47,8 @@ "maxOutput": 128000, "tokenPricing": { "input": 1.75, - "cache": 0.175, - "output": 14 + "output": 14, + "cache": 0.175 }, "releaseDate": "2025-12-11", "lifecycle": "latest", @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gpt-5-codex.json b/manifests/models/gpt-5-codex.json index 3aa43c44..f80ef440 100644 --- a/manifests/models/gpt-5-codex.json +++ b/manifests/models/gpt-5-codex.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": 0.496, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/gpt-5.json b/manifests/models/gpt-5.json index d265e836..eba62690 100644 --- a/manifests/models/gpt-5.json +++ b/manifests/models/gpt-5.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 71.8, "terminalBench": 0.54, - "sciCode": 1.5, - "liveCodeBench": 28.7, "mmmu": 74.4, "mmmuPro": null, - "webDevArena": 148 + "webDevArena": 148, + "sciCode": 1.5, + "liveCodeBench": 28.7 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/grok-code-fast-1.json b/manifests/models/grok-code-fast-1.json index 17ee947a..4b4bba03 100644 --- a/manifests/models/grok-code-fast-1.json +++ b/manifests/models/grok-code-fast-1.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": 0.258, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/kat-coder-pro-v1.json b/manifests/models/kat-coder-pro-v1.json index 6543c2a7..70b025e8 100644 --- a/manifests/models/kat-coder-pro-v1.json +++ b/manifests/models/kat-coder-pro-v1.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": 8.5, - "sciCode": 36.6, - "liveCodeBench": 74.7, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": 36.6, + "liveCodeBench": 74.7 }, "platformUrls": { "huggingface": null, diff --git a/manifests/models/kimi-k2-0905.json b/manifests/models/kimi-k2-0905.json index 711ee2c8..c26ca3b2 100644 --- a/manifests/models/kimi-k2-0905.json +++ b/manifests/models/kimi-k2-0905.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 43.8, "terminalBench": 0.278, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/moonshotai/Kimi-K2-Instruct-0905", diff --git a/manifests/models/kimi-k2-thinking.json b/manifests/models/kimi-k2-thinking.json index d04c2e9a..1e12a948 100644 --- a/manifests/models/kimi-k2-thinking.json +++ b/manifests/models/kimi-k2-thinking.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/moonshotai/Kimi-K2-Instruct-Thinking", diff --git a/manifests/models/minimax-m2-1.json b/manifests/models/minimax-m2-1.json index 95600485..a6366b2d 100644 --- a/manifests/models/minimax-m2-1.json +++ b/manifests/models/minimax-m2-1.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/MiniMaxAI/MiniMax-M2.1", diff --git a/manifests/models/minimax-m2.json b/manifests/models/minimax-m2.json index 4764bc9a..4fe5bdfd 100644 --- a/manifests/models/minimax-m2.json +++ b/manifests/models/minimax-m2.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": 61, "terminalBench": 0.3, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/MiniMaxAI/MiniMax-M2", diff --git a/manifests/models/qwen3-coder-30b-a3b.json b/manifests/models/qwen3-coder-30b-a3b.json index 1f4d3b24..e20bc15c 100644 --- a/manifests/models/qwen3-coder-30b-a3b.json +++ b/manifests/models/qwen3-coder-30b-a3b.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct", diff --git a/manifests/models/qwen3-coder-480b-a35b.json b/manifests/models/qwen3-coder-480b-a35b.json index d4353876..803dbd12 100644 --- a/manifests/models/qwen3-coder-480b-a35b.json +++ b/manifests/models/qwen3-coder-480b-a35b.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": 0.254, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": "https://huggingface.co/Qwen/Qwen3-Coder-480B-A35B-Instruct", diff --git a/manifests/models/qwen3-coder-plus.json b/manifests/models/qwen3-coder-plus.json index 356e8f3d..cecccad4 100644 --- a/manifests/models/qwen3-coder-plus.json +++ b/manifests/models/qwen3-coder-plus.json @@ -59,11 +59,11 @@ "benchmarks": { "sweBench": null, "terminalBench": null, - "sciCode": null, - "liveCodeBench": null, "mmmu": null, "mmmuPro": null, - "webDevArena": null + "webDevArena": null, + "sciCode": null, + "liveCodeBench": null }, "platformUrls": { "huggingface": null, diff --git a/manifests/vendors/cline-bot.json b/manifests/vendors/cline-bot.json new file mode 100644 index 00000000..207f1cd9 --- /dev/null +++ b/manifests/vendors/cline-bot.json @@ -0,0 +1,51 @@ +{ + "id": "cline-bot", + "name": "Cline Bot", + "description": "Cline Bot is a vendor.", + "translations": { + "zh-Hans": { + "description": "Cline CLI 支持从终端无头执行任务并将 Cline 集成到任何地方。使用它通过协作 AI 驱动 GitHub Actions、Discord 机器人和 Linear 工单。" + }, + "de": { + "description": "Cline CLI ermöglicht headless Aufgabenausführung vom Terminal und integriert Cline überall. Verwenden Sie es, um GitHub Actions, Discord-Bots und Linear-Tickets mit kollaborativer AI zu betreiben." + }, + "ko": { + "description": "Cline CLI는 터미널에서 헤드리스 작업 실행을 지원하고 Cline을 어디서든 통합할 수 있습니다. 협업 AI로 GitHub Actions, Discord 봇 및 Linear 티켓을 구동하는 데 사용하세요." + }, + "es": { + "description": "Cline CLI permite ejecucion de tareas sin interfaz desde el terminal e integra Cline en cualquier lugar. Usalo para GitHub Actions, bots Discord y tickets Linear." + }, + "fr": { + "description": "Cline CLI permet l'execution de taches sans interface desde le terminal et integre Cline partout. Utilisez-le pour alimenter GitHub Actions, bots Discord et tickets Linear avec l'IA collaborative." + }, + "id": { + "description": "Cline CLI memungkinkan eksekusi tugas tanpa antarmuka dari terminal dan mengintegrasikan Cline di mana saja. Gunakan untuk GitHub Actions, bot Discord, dan tiket Linear." + }, + "ja": { + "description": "Cline CLIは、ターミナルからのヘッドレスタスク実行を可能にし、どこでもClineを統合できます。協調型AIでGitHub Actions、Discord bot、Linearチケットを駆動するために使用します。" + }, + "pt": { + "description": "Cline CLI permite execucao de tarefas sem cabeca do terminal e integra o Cline em qualquer lugar. Use-o para GitHub Actions, bots do Discord e tickets do Linear." + }, + "ru": { + "description": "Cline CLI позволяет выполнять задачи без интерфейса из терминала и интегрирует Cline везде. Используйте его для питания GitHub Actions, ботов Discord и билетов Linear с помощью коллаборативного ИИ." + }, + "tr": { + "description": "Cline CLI, terminalden kafasiz gorev yurutme enable eder ve Cline'i her yere entegre eder. Isbirlikci AI ile GitHub Actions, Discord botlari ve Linear biletlerini guclendirmek icin kullanin." + }, + "zh-Hant": { + "description": "Cline CLI 支援從終端無介面執行任務並將 Cline 整合到任何地方。使用它透過協作 AI 驅動 GitHub Actions、Discord 機器人和 Linear 工單。" + } + }, + "verified": false, + "websiteUrl": "https://cline.bot/cline-cli", + "communityUrls": { + "linkedin": "https://www.linkedin.com/company/clinebot", + "twitter": "https://x.com/clinebot", + "github": null, + "youtube": null, + "discord": null, + "reddit": "https://www.reddit.com/r/cline", + "blog": "https://cline.bot/blog" + } +} diff --git a/manifests/vendors/continue-dev.json b/manifests/vendors/continue-dev.json new file mode 100644 index 00000000..cc959204 --- /dev/null +++ b/manifests/vendors/continue-dev.json @@ -0,0 +1,51 @@ +{ + "id": "continue-dev", + "name": "Continue Dev", + "description": "Continue Dev is a vendor.", + "translations": { + "zh-Hans": { + "description": "开源 CLI Agent,将持续 AI 带到您的终端。使用任何模型构建和运行自定义 Agent,定义您自己的规则,并集成 MCP 工具,无供应商锁定。" + }, + "de": { + "description": "Open-Source CLI-Agent, der kontinuierliche AI in Ihr Terminal bringt. Erstellen Sie Agenten mit jedem Modell, definieren Sie Regeln und integrieren Sie MCP-Tools ohne Vendor-Lock-in." + }, + "ko": { + "description": "지속적인 AI를 터미널로 가져오는 오픈 소스 CLI 에이전트입니다. 모든 모델로 사용자 지정 에이전트를 구축하고 실행하며, 규칙을 정의하고 MCP 도구를 통합하여 공급업체 종속성을 피할 수 있습니다." + }, + "es": { + "description": "Agente CLI open source que trae IA continua a terminal. Construye agentes personalizados con modelo, define reglas y herramientas MCP sin bloqueo." + }, + "fr": { + "description": "Agent CLI open source qui apporte une IA continue a terminal. Construisez et executez des agents personnalises, definissez regles et integrez outils MCP." + }, + "id": { + "description": "Agen CLI open source yang membawa AI berkelanjutan ke terminal. Membangun agen kustom dengan model, menentukan aturan dan mengintegrasikan alat MCP tanpa vendor." + }, + "ja": { + "description": "継続的なAIをターミナルにもたらすオープンソースCLIエージェント。任意のモデルでカスタムエージェントを構築・実行し、独自のルールを定義し、ベンダーロックインなしでMCPツールを統合します。" + }, + "pt": { + "description": "Agente CLI open source que traz IA continua ao terminal. Construa agentes personalizados com modelo, defina regras e integre ferramentas MCP sem bloqueio." + }, + "ru": { + "description": "Открытый агент CLI приносит непрерывный ИИ в терминал. Создавайте пользовательские агенты с моделью, определяйте правила и интегрируйте MCP без блокировки." + }, + "tr": { + "description": "Surekli AI getiren acik kaynakli CLI aracidir. Herhangi bir modelle ozel aracilar insa edin ve calistirin, kendi kurallarinizi belirleyin, satci kilitlenmesi olmadan MCP entegre edin." + }, + "zh-Hant": { + "description": "開源 CLI Agent,將持續 AI 帶到您的終端。使用任何模型建構和執行自訂 Agent,定義您自己的規則,並整合 MCP 工具,無供應商鎖定。" + } + }, + "verified": false, + "websiteUrl": "https://www.continue.dev", + "communityUrls": { + "linkedin": null, + "twitter": null, + "github": "https://github.com/continuedev/continue", + "youtube": null, + "discord": "https://discord.gg/continue", + "reddit": null, + "blog": "https://www.continue.dev/blog" + } +} diff --git a/manifests/vendors/continue.json b/manifests/vendors/continue.json index c5b6f104..9b078394 100644 --- a/manifests/vendors/continue.json +++ b/manifests/vendors/continue.json @@ -40,7 +40,6 @@ }, "verified": false, "websiteUrl": "https://continue.dev", - "docsUrl": "https://docs.continue.dev/intro", "communityUrls": { "linkedin": "https://www.linkedin.com/company/continuedev", "twitter": "https://x.com/continuedev", diff --git a/manifests/vendors/kwai.json b/manifests/vendors/kwai.json new file mode 100644 index 00000000..49c4950c --- /dev/null +++ b/manifests/vendors/kwai.json @@ -0,0 +1,51 @@ +{ + "id": "kwai", + "name": "Kwai", + "description": "Kwai is a vendor.", + "translations": { + "zh-Hans": { + "description": "快手的 AI 研究实验室,开发先进的编码模型和 AI 助手。" + }, + "de": { + "description": "Kwais KI-Forschungslabor für die Entwicklung fortschrittlicher Codierungsmodelle und KI-Assistenten." + }, + "ko": { + "description": "Kwai의 AI 연구소로 고급 코딩 모델과 AI 어시스턴트를 개발합니다." + }, + "es": { + "description": "Laboratorio de investigación de IA de Kwai que desarrolla modelos de codificación avanzados y asistentes de IA." + }, + "fr": { + "description": "Le laboratoire de recherche IA de Kwai développant des modèles de codage avancés et des assistants IA." + }, + "id": { + "description": "Laboratorium penelitian AI Kwai yang mengembangkan model coding canggih dan asisten AI." + }, + "ja": { + "description": "高度なコーディングモデルとAIアシスタントを開発するKwaiのAI研究ラボ。" + }, + "pt": { + "description": "Laboratorio de pesquisa de IA da Kwai desenvolvendo modelos de codificacao avancados e assistentes de IA." + }, + "ru": { + "description": "Исследовательская лаборатория ИИ Kwai, разрабатывающая передовые модели кодирования и ИИ-ассистенты." + }, + "tr": { + "description": "Kwai'nin ileri duzey kodlama modelleri ve yardimci sise gelistirilen AI arastirma laboratuvari." + }, + "zh-Hant": { + "description": "快手的 AI 研究實驗室,開發先進的編碼模型和 AI 助手。" + } + }, + "verified": false, + "websiteUrl": "https://kwaikatonai.com", + "communityUrls": { + "linkedin": null, + "twitter": null, + "github": null, + "youtube": null, + "discord": null, + "reddit": null, + "blog": null + } +} diff --git a/manifests/vendors/kwaikat.json b/manifests/vendors/kwaikat.json new file mode 100644 index 00000000..3e0c581a --- /dev/null +++ b/manifests/vendors/kwaikat.json @@ -0,0 +1,51 @@ +{ + "id": "kwaikat", + "name": "KwaiKAT", + "description": "KwaiKAT is a vendor.", + "translations": { + "zh-Hans": { + "description": "KwaiKAT 最先进的代理编码模型,属于 KAT-Coder 系列。专为自主编程设计,具备工具调用、多轮交互和指令遵循能力。" + }, + "de": { + "description": "KwaiKATs modernstes agentic Coding-Modell aus der KAT-Coder-Serie. Speziell fur autonomes Programmierung entwickelt mit Tool-Calling, Multi-Turn-Interaktion und Instruction-Following-Fahigkeiten." + }, + "ko": { + "description": "KwaiKAT의 KAT-Coder 시리즈에서 가장 진보된 에이전트 코딩 모델입니다. 도구 호출, 다중 턴 상호작용 및 명령 따르기 기능을 갖춘 자율 프로그래밍을 위해 설계되었습니다." + }, + "es": { + "description": "Modelo de codificacion agentico mas avanzado de KwaiKAT en la serie KAT-Coder. Diseñado para programacion autonoma con llamadas a herramientas, interaccion multi-turno y seguimiento de instrucciones." + }, + "fr": { + "description": "Le modele de codage agentique le plus avance de KwaiKAT dans la serie KAT-Coder. Conçu pour la programmation autonome avec appel d'outils, interaction multi-tours et suivi d'instructions." + }, + "id": { + "description": "Model coding agentik paling canggih dari KwaiKAT dalam seri KAT-Coder. Dirancang khusus untuk pemrograman otonom dengan kemampuan pemanggilan alat, interaksi multi-turn, dan pengikutan instruksi." + }, + "ja": { + "description": "KwaiKATのKAT-Coderシリーズで最も高度なエージェント型コーディングモデル。ツール呼び出し、マルチターン対話、命令追従機能を備えた自律プログラミング向けに設計されています。" + }, + "pt": { + "description": "Modelo de codificacao agentico mais avancado da KwaiKAT na serie KAT-Coder. Projetado para programacao autonoma com chamada de ferramentas, interacao multi-turno e seguimento de instrucoes." + }, + "ru": { + "description": "Самая продвинутая агентная модель кодирования KwaiKAT в серии KAT-Coder. Разработана для автономного программирования с вызовом инструментов, многоходового взаимодействия и выполнения инструкций." + }, + "tr": { + "description": "KwaiKAT'in KAT-Coder serisindeki en gelismis agentik kodlama modeli. Ara cagirma, cok turlu etkilesim ve talimat takip etme yetenekleri ile otonom programlama icin tasarlanmistir." + }, + "zh-Hant": { + "description": "KwaiKAT 最先進的代理編碼模型,屬於 KAT-Coder 系列。專為自主編程設計,具備工具調用、多輪交互和指令遵循能力。" + } + }, + "verified": false, + "websiteUrl": "https://openrouter.ai/kwaipilot", + "communityUrls": { + "linkedin": null, + "twitter": null, + "github": null, + "youtube": null, + "discord": null, + "reddit": null, + "blog": null + } +} diff --git a/package-lock.json b/package-lock.json index 98b1b579..73654640 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,9 @@ "@types/node": "^22.19.2", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", + "@typescript-eslint/parser": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "agent-browser": "^0.5.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "cspell": "^9.4.0", @@ -47,6 +50,7 @@ "remark-gfm": "^4.0.1", "remark-mdx-frontmatter": "^5.2.0", "tailwindcss": "^4.1.17", + "tsx": "^4.21.0", "typescript": "^5.9.3", "user-agents": "^1.1.669", "vitest": "^4.0.16", @@ -9594,6 +9598,260 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0", + "peer": true + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@formatjs/ecma402-abstract": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", @@ -9654,6 +9912,62 @@ "tslib": "2" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -13865,6 +14179,14 @@ "@types/unist": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -13939,101 +14261,259 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vitest/expect": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", - "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==", + "node_modules/@typescript-eslint/parser": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", + "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.17", - "@vitest/utils": "4.0.17", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@vitest/mocker": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz", - "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", + "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.17", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "@typescript-eslint/tsconfig-utils": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz", - "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", + "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/runner": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz", - "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", + "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.17", - "pathe": "^2.0.3" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@vitest/snapshot": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz", - "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==", + "node_modules/@typescript-eslint/types": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", + "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.17", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/spy": { + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", + "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.0", + "@typescript-eslint/tsconfig-utils": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", + "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/expect": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", + "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.17", + "@vitest/utils": "4.0.17", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz", + "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.17", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz", + "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz", + "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz", + "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.17", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { "version": "4.0.17", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz", "integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==", @@ -14117,6 +14597,44 @@ "node": ">=0.4.0" } }, + "node_modules/agent-browser": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.5.0.tgz", + "integrity": "sha512-i0NGFBMwLMk7Q47vkyI8/DNxBmf3t5O+meq25rCJgxl4UbphwbnGamMmB/ZZBLLifXGYcU/joUnFEOxmV691zw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "^1.57.0", + "ws": "^8.19.0", + "zod": "^3.22.4" + }, + "bin": { + "agent-browser": "bin/agent-browser" + } + }, + "node_modules/agent-browser/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -14812,6 +15330,14 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -15265,6 +15791,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -15624,88 +16158,353 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "peer": true, "dependencies": { - "@types/estree": "^1.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://opencollective.com/eslint" } }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://opencollective.com/eslint" } }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -15749,6 +16548,17 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -15902,6 +16712,14 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -15972,6 +16790,20 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -16006,6 +16838,39 @@ "url": "https://opencollective.com/express" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", @@ -16242,6 +17107,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", @@ -16265,6 +17143,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -16281,6 +17173,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -16331,6 +17237,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -16629,6 +17546,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -16876,13 +17804,40 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-schema-traverse": { + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -16902,6 +17857,21 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -17275,6 +18245,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -17282,6 +18269,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -18775,6 +19770,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -19092,6 +20095,59 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -19147,6 +20203,17 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -19215,6 +20282,19 @@ "node": ">=0.10" } }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/po-parser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", @@ -19250,6 +20330,17 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -19274,6 +20365,17 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -19729,6 +20831,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -20365,6 +21477,20 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strnum": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", @@ -20489,128 +21615,642 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-tqdm": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/ts-tqdm/-/ts-tqdm-0.8.6.tgz", + "integrity": "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { "node": ">=18" } }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=0.6" + "node": ">=18" } }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, - "node_modules/ts-tqdm": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/ts-tqdm/-/ts-tqdm-0.8.6.tgz", - "integrity": "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } }, "node_modules/type-is": { "version": "2.0.1", @@ -20814,6 +22454,17 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/urlpattern-polyfill": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", @@ -21712,6 +23363,17 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/workerd": { "version": "1.20260114.0", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260114.0.tgz", @@ -22446,6 +24108,20 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/package.json b/package.json index 55185069..031dd920 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "AI Coding Stack Website", "scripts": { - "dev": "npm run test:validate && npm run generate && next dev", + "dev": "npm run test:ci && npm run generate && next dev", "build:next": "npm run test:validate && npm run generate && BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) next build", "build": "npm run build:next && opennextjs-cloudflare build --skipBuild", "start": "next start", @@ -12,8 +12,8 @@ "biome": "biome check --write .", "biome:check": "biome check .", "biome:unsafe": "biome check --write --unsafe .", - "check": "biome check . && npm run spell", - "type-check": "tsc --noEmit", + "check": "npm run format && npm run biome && biome check . && npm run spell && npm run type-check", + "type-check": "tsc --noEmit --incremental", "deps:check-latest": "npm outdated", "deps:update": "npm update", "spell": "cspell '**/*.{ts,tsx,js,jsx,json,md,mdx}'", @@ -22,14 +22,20 @@ "test:ci": "vitest run --reporter=verbose", "test:validate": "vitest run tests/validate --reporter=verbose", "test:urls": "RUN_URL_TESTS=1 vitest run tests/validate/urls.accessibility.test.ts --reporter=verbose", - "generate": "node scripts/generate/index.mjs", - "generate:manifests": "node scripts/generate/index.mjs manifest-indexes", - "generate:metadata": "node scripts/generate/index.mjs metadata", - "refactor": "node scripts/refactor/index.mjs", - "refactor:sort-fields": "node scripts/refactor/index.mjs sort-manifest-fields", - "fetch": "node scripts/fetch/index.mjs", - "fetch:github-stars": "node scripts/fetch/index.mjs github-stars", - "fetch:benchmarks": "node scripts/fetch/index.mjs benchmarks", + "validate:i18n": "npx tsx scripts/validate/validate-i18n.ts", + "validate:i18n-usage": "npx tsx scripts/validate/validate-i18n-usage.ts", + "validate:i18n-duplicates": "npx tsx scripts/validate/validate-i18n-duplicates.ts", + "validate:urls": "npx tsx scripts/validate/visit-urls.ts", + "validate:urls:all": "npx tsx scripts/validate/visit-urls.ts --locales all --slugs all", + "validate:urls:quick": "npx tsx scripts/validate/visit-urls.ts", + "generate": "npx tsx scripts/generate/index.ts", + "generate:manifests": "npx tsx scripts/generate/index.ts manifest-indexes", + "generate:metadata": "npx tsx scripts/generate/index.ts metadata", + "refactor": "npx tsx scripts/refactor/index.ts", + "refactor:sort-fields": "npx tsx scripts/refactor/index.ts sort-manifest-fields", + "fetch": "npx tsx scripts/fetch/index.ts", + "fetch:github-stars": "npx tsx scripts/fetch/index.ts github-stars", + "fetch:benchmarks": "npx tsx scripts/fetch/index.ts benchmarks", "deploy": "npm run build:next && opennextjs-cloudflare build --skipBuild && opennextjs-cloudflare deploy", "preview": "npm run build:next && opennextjs-cloudflare build --skipBuild && opennextjs-cloudflare preview", "cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts", @@ -64,6 +70,9 @@ "@types/node": "^22.19.2", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", + "@typescript-eslint/parser": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "agent-browser": "^0.5.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "cspell": "^9.4.0", @@ -75,6 +84,7 @@ "remark-gfm": "^4.0.1", "remark-mdx-frontmatter": "^5.2.0", "tailwindcss": "^4.1.17", + "tsx": "^4.21.0", "typescript": "^5.9.3", "user-agents": "^1.1.669", "vitest": "^4.0.16", diff --git a/scripts/_shared/runner.mjs b/scripts/_shared/runner.ts similarity index 62% rename from scripts/_shared/runner.mjs rename to scripts/_shared/runner.ts index 41de6adf..c7394edc 100644 --- a/scripts/_shared/runner.mjs +++ b/scripts/_shared/runner.ts @@ -5,7 +5,7 @@ * Provides common functionality for running scripts in each category */ -import { spawn } from 'node:child_process' +import { type ChildProcess, spawn } from 'node:child_process' import fs from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -13,14 +13,23 @@ import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) +interface ScriptMap { + [key: string]: string +} + +export interface CategoryConfig { + categoryName: string + includes?: string[] + excludes?: string[] +} + /** * Run a script file - * @param {string} scriptPath - Path to the script file - * @returns {Promise} + * @param scriptPath - Path to the script file */ -export function runScript(scriptPath) { +export function runScript(scriptPath: string): Promise { return new Promise((resolve, reject) => { - const child = spawn('node', [scriptPath], { + const child: ChildProcess = spawn('npx', ['tsx', scriptPath], { stdio: 'inherit', shell: false, }) @@ -41,39 +50,39 @@ export function runScript(scriptPath) { /** * Get the directory path for a category - * @param {string} categoryDir - The category directory (e.g., 'generate', 'validate', 'fetch') - * @returns {string} - Full path to the category directory + * @param categoryDir - The category directory (e.g., 'generate', 'validate', 'fetch') + * @returns - Full path to the category directory */ -export function getCategoryDir(categoryDir) { +export function getCategoryDir(categoryDir: string): string { return path.join(__dirname, '..', categoryDir) } /** * Discover all scripts in a category directory - * @param {string} categoryDir - The category directory path - * @returns {Promise} - Map of script names to script files + * @param categoryDir - The category directory path + * @returns - Map of script names to script files * * Script names are generated by: - * 1. Removing .mjs extension + * 1. Removing .ts extension * 2. If filename starts with category prefix (e.g., "generate-", "fetch-"), remove it * 3. Otherwise, use the full filename without extension * * Examples: - * - "generate-manifest-indexes.mjs" in generate/ -> "manifest-indexes" - * - "sort-manifest-fields.mjs" in refactor/ -> "sort-manifest-fields" (no prefix) - * - "refactor-sort-fields.mjs" in refactor/ -> "sort-fields" (prefix removed) + * - "generate-manifest-indexes.ts" in generate/ -> "manifest-indexes" + * - "sort-manifest-fields.ts" in refactor/ -> "sort-manifest-fields" (no prefix) + * - "refactor-sort-fields.ts" in refactor/ -> "sort-fields" (prefix removed) */ -async function discoverScripts(categoryDir) { - const scripts = {} +async function discoverScripts(categoryDir: string): Promise { + const scripts: ScriptMap = {} try { const entries = await fs.readdir(categoryDir, { withFileTypes: true }) for (const entry of entries) { - // Skip index.mjs and non-.mjs files - if (entry.isFile() && entry.name.endsWith('.mjs') && entry.name !== 'index.mjs') { - // Generate script name from filename (without .mjs extension) - const baseName = entry.name.replace(/\.mjs$/, '') + // Skip index.ts and non-.ts files + if (entry.isFile() && entry.name.endsWith('.ts') && entry.name !== 'index.ts') { + // Generate script name from filename (without .ts extension) + const baseName = entry.name.replace(/\.ts$/, '') // Remove category prefix if present (e.g., "generate-", "fetch-") // If filename doesn't start with prefix, use full name without extension @@ -86,7 +95,7 @@ async function discoverScripts(categoryDir) { } } } catch (error) { - console.error(`Error discovering scripts in ${categoryDir}:`, error.message) + console.error(`Error discovering scripts in ${categoryDir}:`, (error as Error).message) } return scripts @@ -94,14 +103,14 @@ async function discoverScripts(categoryDir) { /** * Filter scripts based on includes/excludes - * @param {Object} scripts - Map of script names to script files - * @param {string[]} [includes] - Script names to include (if specified, only these will run) - * Use script name without .mjs extension (e.g., 'sort-manifest-fields', 'manifests') - * @param {string[]} [excludes] - Script names to exclude - * Use script name without .mjs extension (e.g., 'sort-manifest-fields', 'github-stars') - * @returns {Object} - Filtered scripts map + * @param scripts - Map of script names to script files + * @param includes - Script names to include (if specified, only these will run) + * Use script name without .ts extension (e.g., 'sort-manifest-fields', 'manifests') + * @param excludes - Script names to exclude + * Use script name without .ts extension (e.g., 'sort-manifest-fields', 'github-stars') + * @returns - Filtered scripts map */ -function filterScripts(scripts, includes, excludes) { +function filterScripts(scripts: ScriptMap, includes?: string[], excludes?: string[]): ScriptMap { let filtered = { ...scripts } // Apply includes filter @@ -130,12 +139,12 @@ function filterScripts(scripts, includes, excludes) { /** * Main runner function - * @param {Object} config - Configuration object - * @param {string} config.categoryName - Category name (e.g., 'generate', 'validate', 'fetch') - * @param {string[]} [config.includes] - Script names to include (without .mjs suffix, e.g., 'manifests', 'urls') - * @param {string[]} [config.excludes] - Script names to exclude (without .mjs suffix, e.g., 'github-stars') + * @param config - Configuration object + * @param config.categoryName - Category name (e.g., 'generate', 'validate', 'fetch') + * @param config.includes - Script names to include (without .ts suffix, e.g., 'manifests', 'urls') + * @param config.excludes - Script names to exclude (without .ts suffix, e.g., 'github-stars') */ -export async function runCategoryScripts(config) { +export async function runCategoryScripts(config: CategoryConfig): Promise { const { categoryName, includes, excludes } = config const categoryDir = getCategoryDir(categoryName) @@ -176,7 +185,7 @@ export async function runCategoryScripts(config) { await runScript(path.join(categoryDir, scriptFile)) console.log(`\n✅ ${scriptName} completed successfully`) } catch (error) { - console.error(`\n❌ ${scriptName} failed:`, error.message) + console.error(`\n❌ ${scriptName} failed:`, (error as Error).message) process.exit(1) } } else { @@ -187,6 +196,10 @@ export async function runCategoryScripts(config) { for (const name of order) { const scriptFile = scripts[name] + if (!scriptFile) { + console.error(`\n❌ ${name} has no associated script file`) + process.exit(1) + } console.log(`\n${'='.repeat(60)}`) console.log(`Running ${name}...`) @@ -194,7 +207,7 @@ export async function runCategoryScripts(config) { try { await runScript(path.join(categoryDir, scriptFile)) } catch (error) { - console.error(`\n❌ ${name} failed:`, error.message) + console.error(`\n❌ ${name} failed:`, (error as Error).message) process.exit(1) } } diff --git a/scripts/fetch/compare-models.mjs b/scripts/fetch/compare-models.ts similarity index 69% rename from scripts/fetch/compare-models.mjs rename to scripts/fetch/compare-models.ts index bff1c96f..a0090eaf 100644 --- a/scripts/fetch/compare-models.mjs +++ b/scripts/fetch/compare-models.ts @@ -9,50 +9,152 @@ const manifestsDir = join(__dirname, '../../manifests/models') const apiDataFile = join(__dirname, '../../tmp/models-dev-api.json') const mappingFile = join(__dirname, '../../manifests/mapping.json') +interface ComparisonResult { + match: boolean + skip: boolean + manifest?: unknown + api?: unknown +} + +interface FieldComparison { + field: string + manifestKey: string + apiKey: string + match: boolean + skip: boolean + manifest?: unknown + api?: unknown +} + +interface NormalizedApiModel { + name: string + releaseDate: string | null + contextWindow: number | null + maxOutput: number | null + inputModalities: string[] + tokenPricing: { + input: number | null + output: number | null + cache: number | null + } + capabilities: string[] +} + +interface ModelResult { + modelId: string + apiModelId: string + vendor: string + vendorKey: string + vendorExists: boolean + modelExists: boolean + comparisons: FieldComparison[] +} + +interface ApiData { + [vendorKey: string]: { + models?: { + [modelId: string]: { + name: string + release_date?: string | null + limit?: { + context?: number | null + output?: number | null + } + modalities?: { + input?: string[] + } + cost?: { + input?: number | null + output?: number | null + cache_read?: number | null + } + tool_call?: boolean + reasoning?: boolean + } + } + } +} + +interface MappingData { + vendors: Record + models: Record +} + // Helper to compare values and return match status -function compare(manifestValue, apiValue, _manifestKey, _apiKey) { +function compare( + manifestValue: unknown, + apiValue: unknown, + _manifestKey: string, + _apiKey: string +): ComparisonResult { if (manifestValue === null && apiValue === undefined) return { match: true, skip: true } if (manifestValue === null && apiValue === null) return { match: true, skip: false } if (manifestValue === null && apiValue !== undefined) - return { match: false, manifest: null, api: apiValue } + return { match: false, skip: false, manifest: null, api: apiValue } if (manifestValue !== null && apiValue === undefined) - return { match: false, manifest: manifestValue, api: null } + return { match: false, skip: false, manifest: manifestValue, api: null } if (manifestValue === apiValue) return { match: true, skip: false } - return { match: false, manifest: manifestValue, api: apiValue } + return { match: false, skip: false, manifest: manifestValue, api: apiValue } } // Convert API model data to manifest-compatible format for comparison -function normalizeApiModel(apiModel) { +function normalizeApiModel(apiModel: Record | undefined): NormalizedApiModel { + if (!apiModel) { + return { + name: '', + releaseDate: null, + contextWindow: null, + maxOutput: null, + inputModalities: [], + tokenPricing: { + input: null, + output: null, + cache: null, + }, + capabilities: [], + } + } + + const model = apiModel as { + name: string + release_date?: string | null + limit?: { context?: number | null; output?: number | null } | null + modalities?: { input?: string[] } | null + cost?: { input?: number | null; output?: number | null; cache_read?: number | null } | null + tool_call?: boolean + reasoning?: boolean + } + return { - name: apiModel.name, - releaseDate: apiModel.release_date || null, - contextWindow: apiModel.limit?.context || null, - maxOutput: apiModel.limit?.output || null, - inputModalities: apiModel.modalities?.input || [], + name: model.name, + releaseDate: model.release_date || null, + contextWindow: model.limit?.context || null, + maxOutput: model.limit?.output || null, + inputModalities: model.modalities?.input || [], tokenPricing: { - input: apiModel.cost?.input || null, - output: apiModel.cost?.output || null, - cache: apiModel.cost?.cache_read || null, + input: model.cost?.input || null, + output: model.cost?.output || null, + cache: model.cost?.cache_read || null, }, capabilities: [ - ...(apiModel.tool_call ? ['function-calling', 'tool-choice', 'structured-outputs'] : []), - ...(apiModel.reasoning ? ['reasoning'] : []), + ...(model.tool_call ? ['function-calling', 'tool-choice', 'structured-outputs'] : []), + ...(model.reasoning ? ['reasoning'] : []), ].sort(), } } -async function main() { +async function main(): Promise { // Read API reference data and mapping - const apiData = JSON.parse(await readFile(apiDataFile, 'utf-8')) + const apiData = JSON.parse(await readFile(apiDataFile, 'utf-8')) as ApiData const { vendors: vendorMapping, models: modelMapping } = JSON.parse( await readFile(mappingFile, 'utf-8') - ) + ) as MappingData // Read all model manifests const files = await readdir(manifestsDir) const manifestFiles = files.filter(f => f.endsWith('.json')) - const results = [] + const results: ModelResult[] = [] for (const file of manifestFiles) { const manifest = JSON.parse(await readFile(join(manifestsDir, file), 'utf-8')) @@ -72,8 +174,8 @@ async function main() { const apiModel = vendorData?.models?.[apiModelId] const modelExists = !!apiModel - const comparisons = [] - if (modelExists) { + const comparisons: FieldComparison[] = [] + if (modelExists && apiModel) { const normalizedApi = normalizeApiModel(apiModel) // Compare releaseDate @@ -81,7 +183,7 @@ async function main() { field: 'releaseDate', manifestKey: 'releaseDate', apiKey: 'release_date', - ...compare(manifest.releaseDate, normalizedApi.releaseDate), + ...compare(manifest.releaseDate, normalizedApi.releaseDate, 'releaseDate', 'release_date'), }) // Compare contextWindow @@ -89,7 +191,12 @@ async function main() { field: 'contextWindow', manifestKey: 'contextWindow', apiKey: 'limit.context', - ...compare(manifest.contextWindow, normalizedApi.contextWindow), + ...compare( + manifest.contextWindow, + normalizedApi.contextWindow, + 'contextWindow', + 'limit.context' + ), }) // Compare maxOutput @@ -97,7 +204,7 @@ async function main() { field: 'maxOutput', manifestKey: 'maxOutput', apiKey: 'limit.output', - ...compare(manifest.maxOutput, normalizedApi.maxOutput), + ...compare(manifest.maxOutput, normalizedApi.maxOutput, 'maxOutput', 'limit.output'), }) // Compare inputModalities diff --git a/scripts/fetch/fetch-github-stars.mjs b/scripts/fetch/fetch-github-stars.ts similarity index 79% rename from scripts/fetch/fetch-github-stars.mjs rename to scripts/fetch/fetch-github-stars.ts index 9feb9a96..70834714 100644 --- a/scripts/fetch/fetch-github-stars.mjs +++ b/scripts/fetch/fetch-github-stars.ts @@ -8,14 +8,19 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) // GitHub API token (optional but recommended to avoid rate limits) -// Set via environment variable: GITHUB_TOKEN=your_token_here node fetch-github-stars.mjs +// Set via environment variable: GITHUB_TOKEN=your_token_here node fetch-github-stars.ts const GITHUB_TOKEN = process.env.GITHUB_TOKEN // Path to the centralized GitHub stars data file const GITHUB_STARS_FILE = path.join(__dirname, '..', '..', 'data', 'github-stars.json') +interface DirConfig { + directory: string + category: string +} + // Directories configuration - mapping manifest directories to categories -const dirsConfig = [ +const dirsConfig: DirConfig[] = [ { directory: 'manifests/extensions', category: 'extensions', @@ -30,21 +35,50 @@ const dirsConfig = [ }, ] +interface GithubRepo { + owner: string + repo: string +} + +interface ProcessResult { + fileId: string + stars: number | null + updated: boolean + skipped: boolean + error: boolean +} + +interface DirectoryResult { + categoryData: Record + stats: { + updated: number + skipped: number + errors: number + } +} + +interface StarsData { + extensions: Record + clis: Record + ides: Record + [key: string]: Record +} + // Extract owner and repo from GitHub URL -function parseGithubUrl(url) { +function parseGithubUrl(url: string): GithubRepo | null { if (!url) return null const match = url.match(/github\.com\/([^/]+)\/([^/]+)/) - if (!match) return null + if (!match || match.length < 3) return null return { - owner: match[1], - repo: match[2], + owner: match[1] ?? '', + repo: match[2] ?? '', } } // Fetch stars from GitHub API -function fetchStars(owner, repo) { +function fetchStars(owner: string, repo: string): Promise { return new Promise((resolve, reject) => { - const options = { + const options: https.RequestOptions = { hostname: 'api.github.com', path: `/repos/${owner}/${repo}`, method: 'GET', @@ -55,7 +89,10 @@ function fetchStars(owner, repo) { } if (GITHUB_TOKEN) { - options.headers.Authorization = `token ${GITHUB_TOKEN}` + options.headers = { + ...options.headers, + Authorization: `token ${GITHUB_TOKEN}`, + } } const req = https.request(options, res => { @@ -74,7 +111,7 @@ function fetchStars(owner, repo) { const starsInK = parseFloat((stars / 1000).toFixed(1)) resolve(starsInK) } catch (e) { - reject(new Error(`Failed to parse response: ${e.message}`)) + reject(new Error(`Failed to parse response: ${(e as Error).message}`)) } } else if (res.statusCode === 403) { reject(new Error('Rate limit exceeded. Please set GITHUB_TOKEN environment variable.')) @@ -95,18 +132,18 @@ function fetchStars(owner, repo) { } // Extract file ID from filename (remove .json extension) -function getFileId(fileName) { +function getFileId(fileName: string): string { return fileName.replace(/\.json$/, '') } // Sleep function to avoid rate limiting -function sleep(ms) { +function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } // Process a single JSON file // Returns the file ID (from filename) and stars count (or null if no githubUrl or error) -async function processFile(filePath, fileName) { +async function processFile(filePath: string, fileName: string): Promise { const fileId = getFileId(fileName) const content = fs.readFileSync(filePath, 'utf8') const item = JSON.parse(content) @@ -135,14 +172,14 @@ async function processFile(filePath, fileName) { await sleep(1000) return { fileId, stars, updated: true, skipped: false, error: false } } catch (error) { - console.log(` ❌ ${fileId}: Error fetching stars: ${error.message}`) + console.log(` ❌ ${fileId}: Error fetching stars:`, (error as Error).message) return { fileId, stars: null, updated: false, skipped: false, error: true } } } // Process all files in a directory // Maps file names (without .json) to stars data based on githubUrl field -async function processDirectory(dirConfig) { +async function processDirectory(dirConfig: DirConfig): Promise { const dirPath = path.join(__dirname, '..', '..', dirConfig.directory) console.log(`\n📁 Processing ${dirConfig.directory}...`) @@ -162,7 +199,7 @@ async function processDirectory(dirConfig) { let updated = 0 let skipped = 0 let errors = 0 - const categoryData = {} + const categoryData: Record = {} // Process each file and map by filename (without .json extension) for (const file of files) { @@ -184,23 +221,23 @@ async function processDirectory(dirConfig) { } // Main function -async function main() { +async function main(): Promise { console.log('🚀 Starting GitHub stars fetcher...\n') console.log('📝 Note: Updating centralized github-stars.json file\n') if (!GITHUB_TOKEN) { console.log('⚠️ Warning: No GITHUB_TOKEN set. You may hit rate limits (60 requests/hour).') - console.log(' Set it with: GITHUB_TOKEN=your_token node fetch-github-stars.mjs\n') + console.log(' Set it with: GITHUB_TOKEN=your_token node fetch-github-stars.ts\n') } else { console.log('✅ Using GitHub token for authentication\n') } // Load existing stars data or create new structure - let starsData = { extensions: {}, clis: {}, ides: {} } + let starsData: StarsData = { extensions: {}, clis: {}, ides: {} } if (fs.existsSync(GITHUB_STARS_FILE)) { try { const content = fs.readFileSync(GITHUB_STARS_FILE, 'utf8') - starsData = JSON.parse(content) + starsData = JSON.parse(content) as StarsData console.log('📂 Loaded existing github-stars.json\n') } catch { console.log('⚠️ Failed to parse existing github-stars.json, creating new one\n') @@ -220,8 +257,8 @@ async function main() { // Sort the category data by key (alphabetically) const sortedCategoryData = Object.keys(categoryData) .sort() - .reduce((acc, key) => { - acc[key] = categoryData[key] + .reduce>((acc, key) => { + acc[key] = categoryData[key] ?? null return acc }, {}) @@ -233,7 +270,7 @@ async function main() { totalSkipped += stats.skipped totalErrors += stats.errors } catch (error) { - console.error(`❌ Failed to process ${dirConfig.directory}:`, error.message) + console.error(`❌ Failed to process ${dirConfig.directory}:`, (error as Error).message) totalErrors++ } } @@ -243,7 +280,7 @@ async function main() { fs.writeFileSync(GITHUB_STARS_FILE, `${JSON.stringify(starsData, null, 2)}\n`, 'utf8') console.log('\n📝 Successfully updated data/github-stars.json') } catch (error) { - console.error('\n❌ Failed to write github-stars.json:', error.message) + console.error('\n❌ Failed to write github-stars.json:', (error as Error).message) process.exit(1) } diff --git a/scripts/fetch/index.mjs b/scripts/fetch/index.ts similarity index 75% rename from scripts/fetch/index.mjs rename to scripts/fetch/index.ts index 239d7514..d3269053 100644 --- a/scripts/fetch/index.mjs +++ b/scripts/fetch/index.ts @@ -7,12 +7,12 @@ * Can be called from CI or manually. * * Usage: - * node scripts/fetch/index.mjs [script-name] + * node scripts/fetch/index.ts [script-name] * * If no script name is provided, runs all scripts. */ -import { runCategoryScripts } from '../_shared/runner.mjs' +import { runCategoryScripts } from '../_shared/runner' runCategoryScripts({ categoryName: 'fetch', diff --git a/scripts/generate/generate-manifest-indexes.mjs b/scripts/generate/generate-manifest-indexes.ts similarity index 81% rename from scripts/generate/generate-manifest-indexes.mjs rename to scripts/generate/generate-manifest-indexes.ts index 94a91194..e4470c44 100644 --- a/scripts/generate/generate-manifest-indexes.mjs +++ b/scripts/generate/generate-manifest-indexes.ts @@ -4,9 +4,9 @@ * Generate Manifest Index Files * * This script automatically generates TypeScript index files for each manifest type. - * It scans the manifests directory and creates aggregated exports in src/lib/manifests/ + * It scans the manifests directory and creates aggregated exports in src/lib/generated/ * - * Usage: node scripts/generate-manifest-indexes.mjs + * Usage: node scripts/generate-manifest-indexes.ts */ import { execSync } from 'node:child_process' @@ -21,12 +21,19 @@ const MANIFESTS_DIR = path.join(__dirname, '../../manifests') const OUTPUT_DIR = path.join(__dirname, '../../src/lib/generated') // Manifest types to process -const MANIFEST_TYPES = ['ides', 'clis', 'models', 'providers', 'extensions', 'vendors'] +const MANIFEST_TYPES = ['ides', 'clis', 'models', 'providers', 'extensions', 'vendors'] as const + +type ManifestType = (typeof MANIFEST_TYPES)[number] + +interface ImportData { + varName: string + statement: string +} /** * Ensure directory exists */ -function ensureDir(dirPath) { +function ensureDir(dirPath: string): void { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }) console.log(`✓ Created directory: ${dirPath}`) @@ -36,7 +43,7 @@ function ensureDir(dirPath) { /** * Convert kebab-case id to PascalCase variable name */ -function toPascalCase(str) { +function toPascalCase(str: string): string { return str .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) @@ -46,7 +53,7 @@ function toPascalCase(str) { /** * Get all JSON files in a directory */ -function getJsonFiles(dirPath) { +function getJsonFiles(dirPath: string): string[] { if (!fs.existsSync(dirPath)) { return [] } @@ -59,7 +66,7 @@ function getJsonFiles(dirPath) { /** * Generate index file for a manifest type */ -function generateIndexFile(typeName) { +function generateIndexFile(typeName: ManifestType): void { const typeDir = path.join(MANIFESTS_DIR, typeName) const files = getJsonFiles(typeDir) @@ -69,7 +76,7 @@ function generateIndexFile(typeName) { } // Generate import statements - sort alphabetically by variable name - const importData = files.map(file => { + const importData: ImportData[] = files.map(file => { const id = file.replace('.json', '') const varName = toPascalCase(id) const relativePath = `../../../manifests/${typeName}/${file}` @@ -88,10 +95,14 @@ function generateIndexFile(typeName) { const TypeName = toPascalCase(typeSingular) // Get the first file (alphabetically) to extract type - const firstVarName = importData[0].varName + const firstVarName = importData[0]?.varName + if (!firstVarName) { + console.log(`⚠ No files found in ${typeName}/`) + return + } // Add appropriate type import based on manifest type - const typeImportMap = { + const typeImportMap: Record = { ides: 'ManifestIDE', clis: 'ManifestCLI', models: 'ManifestModel', @@ -107,7 +118,7 @@ function generateIndexFile(typeName) { const content = `/** * Auto-generated manifest index for ${typeName} - * Generated by scripts/generate-manifest-indexes.mjs + * Generated by scripts/generate-manifest-indexes.ts * Do not edit manually - run the script to regenerate */ @@ -130,9 +141,9 @@ export default ${typeName}Data /** * Generate main index.ts file that exports all manifests */ -function generateMainIndex() { +function generateMainIndex(): void { // Generate all export statements - const exportStatements = [] + const exportStatements: { type: string; data: string }[] = [] for (const typeName of MANIFEST_TYPES) { const typeSingular = toPascalCase(typeName.replace(/s$/, '')) exportStatements.push({ @@ -144,8 +155,8 @@ function generateMainIndex() { // Sort exports alphabetically by type name and maintain type-before-data order const sortedExports = exportStatements .sort((a, b) => { - const typeA = a.type.match(/\{ (\w+) \}/)[1] - const typeB = b.type.match(/\{ (\w+) \}/)[1] + const typeA = a.type.match(/\{ (\w+) \}/)?.[1] || '' + const typeB = b.type.match(/\{ (\w+) \}/)?.[1] || '' return typeA.localeCompare(typeB) }) .flatMap(item => [item.type, item.data]) @@ -153,7 +164,7 @@ function generateMainIndex() { const content = `/** * Auto-generated main manifest index - * Generated by scripts/generate-manifest-indexes.mjs + * Generated by scripts/generate-manifest-indexes.ts * Do not edit manually - run the script to regenerate */ @@ -168,7 +179,7 @@ ${sortedExports} /** * Generate GitHub stars TypeScript file from centralized JSON */ -function generateGithubStarsFile() { +function generateGithubStarsFile(): void { const githubStarsPath = path.join(__dirname, '..', '..', 'data', 'github-stars.json') if (!fs.existsSync(githubStarsPath)) { @@ -180,7 +191,7 @@ function generateGithubStarsFile() { const content = `/** * Auto-generated GitHub stars data - * Generated by scripts/generate-manifest-indexes.mjs + * Generated by scripts/generate-manifest-indexes.ts * Do not edit manually - run the script to regenerate */ @@ -207,8 +218,8 @@ export default githubStarsData fs.writeFileSync(outputPath, content, 'utf8') // Count total entries - const totalEntries = Object.values(starsData).reduce((sum, category) => { - return sum + Object.keys(category).length + const totalEntries = Object.values(starsData).reduce((sum: number, category: unknown) => { + return sum + Object.keys(category as Record).length }, 0) console.log(`✓ Generated github-stars.ts (${totalEntries} entries)`) @@ -217,7 +228,7 @@ export default githubStarsData /** * Main execution */ -function main() { +function main(): void { console.log('='.repeat(60)) console.log('Generate Manifest Index Files') console.log('='.repeat(60)) @@ -253,7 +264,7 @@ function main() { }) console.log(`✅ Formatting complete`) } catch (error) { - console.error(`⚠️ Biome formatting failed:`, error.message) + console.error(`⚠️ Biome formatting failed:`, (error as Error).message) } } @@ -261,7 +272,7 @@ function main() { try { main() } catch (error) { - console.error('\n❌ Error generating index files:', error.message) - console.error(error.stack) + console.error('\n❌ Error generating index files:', (error as Error).message) + console.error((error as Error).stack) process.exit(1) } diff --git a/scripts/generate/generate-metadata.mjs b/scripts/generate/generate-metadata.ts similarity index 85% rename from scripts/generate/generate-metadata.mjs rename to scripts/generate/generate-metadata.ts index 9efc7c9d..138c33d1 100644 --- a/scripts/generate/generate-metadata.mjs +++ b/scripts/generate/generate-metadata.ts @@ -15,8 +15,30 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const rootDir = path.join(__dirname, '../..') +interface ArticleMetadata { + slug: string + title: string + description: string + date: string +} + +interface DocSection { + id: string + slug: string + title: string +} + +interface FaqItem { + title: string + content: string +} + +interface CollectionSection { + [key: string]: unknown +} + // Read supported locales from i18n config -function getSupportedLocales() { +function getSupportedLocales(): string[] { const configPath = path.join(rootDir, 'src/i18n/config.ts') const configContent = fs.readFileSync(configPath, 'utf8') // Extract the content between "export const locales = [" and "] as const" @@ -25,44 +47,48 @@ function getSupportedLocales() { throw new Error('Could not find locales export in src/i18n/config.ts') } // Extract all quoted strings from the array content - const stringMatches = arrayMatch[1].matchAll(/'([^']+)'/g) - return Array.from(stringMatches, m => m[1]) + const stringMatches = arrayMatch[1]?.matchAll(/'([^']+)'/g) + if (!stringMatches) return [] + return Array.from(stringMatches, m => m[1] ?? '').filter(Boolean) } const SUPPORTED_LOCALES = getSupportedLocales() -function getMDXFiles(directory) { +function getMDXFiles(directory: string): string[] { return fs.readdirSync(directory).filter(file => file.endsWith('.mdx')) } -function parseMDXFrontmatter(filePath) { +function parseMDXFrontmatter(filePath: string): Record { const fileContents = fs.readFileSync(filePath, 'utf8') const { data } = matter(fileContents) return data } -function getSlugFromFilename(fileName) { +function getSlugFromFilename(fileName: string): string { return fileName.replace(/\.mdx$/, '') } // Parse FAQ sections from a unified index.mdx file // Each H1 heading becomes a FAQ item with its title and content -function parseFaqSections(content) { +function parseFaqSections(content: string): FaqItem[] { // Remove frontmatter if present const withoutFrontmatter = content.replace(/^---[\s\S]*?---\n/, '') // Split content by H1 headings (# Title) - const sections = [] + const sections: FaqItem[] = [] const h1Regex = /^# (.+)$/gm - const matches = [] + const matches: { title: string; startIndex: number; endIndex: number }[] = [] // Find all H1 headings and their positions let match = h1Regex.exec(withoutFrontmatter) while (match !== null) { + const title = match[1]?.trim() ?? '' + const index = match.index ?? 0 + const matchLength = match[0]?.length ?? 0 matches.push({ - title: match[1].trim(), - startIndex: match.index, - endIndex: match.index + match[0].length, + title, + startIndex: index, + endIndex: index + matchLength, }) match = h1Regex.exec(withoutFrontmatter) } @@ -70,6 +96,7 @@ function parseFaqSections(content) { // Extract content for each section for (let i = 0; i < matches.length; i++) { const currentMatch = matches[i] + if (!currentMatch) continue const nextMatch = matches[i + 1] // Content is everything after the H1 until the next H1 (or end of file) @@ -87,7 +114,7 @@ function parseFaqSections(content) { } // Generate articles metadata for a specific locale -function generateArticlesMetadataForLocale(locale) { +function generateArticlesMetadataForLocale(locale: string): ArticleMetadata[] { const articlesDirectory = path.join(rootDir, `content/articles/${locale}`) // Return empty array if locale directory doesn't exist @@ -104,9 +131,9 @@ function generateArticlesMetadataForLocale(locale) { return { slug, - title: frontmatter.title, - description: frontmatter.description, - date: frontmatter.date, + title: frontmatter.title as string, + description: frontmatter.description as string, + date: frontmatter.date as string, } }) @@ -117,8 +144,8 @@ function generateArticlesMetadataForLocale(locale) { } // Generate articles metadata for all locales -function generateArticlesMetadata() { - const articlesMetadata = {} +function generateArticlesMetadata(): Record { + const articlesMetadata: Record = {} for (const locale of SUPPORTED_LOCALES) { articlesMetadata[locale] = generateArticlesMetadataForLocale(locale) @@ -128,7 +155,7 @@ function generateArticlesMetadata() { } // Generate docs metadata for a specific locale -function generateDocsMetadataForLocale(locale) { +function generateDocsMetadataForLocale(locale: string): DocSection[] { const docsDirectory = path.join(rootDir, `content/docs/${locale}`) // Return empty array if locale directory doesn't exist @@ -146,7 +173,7 @@ function generateDocsMetadataForLocale(locale) { return { id: slug, slug, - title: frontmatter.title, + title: frontmatter.title as string, } }) @@ -155,8 +182,8 @@ function generateDocsMetadataForLocale(locale) { } // Generate docs metadata for all locales -function generateDocsMetadata() { - const docsMetadata = {} +function generateDocsMetadata(): Record { + const docsMetadata: Record = {} for (const locale of SUPPORTED_LOCALES) { docsMetadata[locale] = generateDocsMetadataForLocale(locale) @@ -166,7 +193,7 @@ function generateDocsMetadata() { } // Generate collections metadata from JSON file -function generateCollectionsMetadata() { +function generateCollectionsMetadata(): Record { const collectionsFile = path.join(rootDir, 'manifests/collections.json') if (!fs.existsSync(collectionsFile)) { @@ -178,12 +205,13 @@ function generateCollectionsMetadata() { const collectionsData = JSON.parse(fileContents) // Remove $schema property as it's not part of the CollectionSection type - const { $schema: _$schema, ...collections } = collectionsData - return collections + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $schema: _$schema, ...collections } = collectionsData as Record + return collections as Record } // Generate FAQ metadata for a specific locale -function generateFaqMetadataForLocale(locale) { +function generateFaqMetadataForLocale(locale: string): FaqItem[] { const faqIndexPath = path.join(rootDir, `content/faq/${locale}/index.mdx`) // Return empty array if index file doesn't exist @@ -199,8 +227,8 @@ function generateFaqMetadataForLocale(locale) { } // Generate FAQ metadata for all locales -function generateFaqMetadata() { - const faqMetadata = {} +function generateFaqMetadata(): Record { + const faqMetadata: Record = {} for (const locale of SUPPORTED_LOCALES) { faqMetadata[locale] = generateFaqMetadataForLocale(locale) @@ -210,9 +238,9 @@ function generateFaqMetadata() { } // Generate stack counts from manifest files -function generateStackCounts() { +function generateStackCounts(): Record { // Directories containing individual JSON files (new structure) - const manifestDirectories = { + const manifestDirectories: Record = { ides: 'ides', clis: 'clis', extensions: 'extensions', @@ -221,7 +249,7 @@ function generateStackCounts() { vendors: 'vendors', } - const stackCounts = {} + const stackCounts: Record = {} // Count files in directories for (const [stackId, dirName] of Object.entries(manifestDirectories)) { @@ -237,7 +265,7 @@ function generateStackCounts() { const files = fs.readdirSync(manifestDir).filter(file => file.endsWith('.json')) stackCounts[stackId] = files.length } catch (error) { - console.error(`❌ Error reading directory ${dirName}:`, error.message) + console.error(`❌ Error reading directory ${dirName}:`, (error as Error).message) stackCounts[stackId] = 0 } } @@ -246,12 +274,13 @@ function generateStackCounts() { } // Generate component imports for articles -function generateArticleComponentsCode(articles) { +function generateArticleComponentsCode(articles: Record): string { const locales = Object.keys(articles) - const componentLines = [] + const componentLines: string[] = [] for (const locale of locales) { const localeArticles = articles[locale] + if (!localeArticles) continue const componentEntries = localeArticles .map(article => { const importLine = ` '${article.slug}': () => import('@content/articles/${locale}/${article.slug}.mdx'),` @@ -277,12 +306,13 @@ function generateArticleComponentsCode(articles) { } // Generate component imports for docs -function generateDocComponentsCode(docs) { +function generateDocComponentsCode(docs: Record): string { const locales = Object.keys(docs) - const componentLines = [] + const componentLines: string[] = [] for (const locale of locales) { const localeDocs = docs[locale] + if (!localeDocs) continue const componentEntries = localeDocs .map(doc => { // Use unquoted keys for simple identifiers @@ -305,8 +335,8 @@ function generateDocComponentsCode(docs) { } // Generate component import for manifesto (single index.mdx per locale) -function generateManifestoComponentsCode() { - const componentLines = [] +function generateManifestoComponentsCode(): string { + const componentLines: string[] = [] for (const locale of SUPPORTED_LOCALES) { const manifestoIndex = path.join(rootDir, `content/manifesto/${locale}/index.mdx`) @@ -323,7 +353,7 @@ function generateManifestoComponentsCode() { } // Main execution -function main() { +function main(): void { console.log('Generating MDX metadata...') const articles = generateArticlesMetadata() @@ -341,10 +371,10 @@ function main() { fs.mkdirSync(outputDir, { recursive: true }) } - const header = `// This file is auto-generated by scripts/generate-metadata.mjs\n// DO NOT EDIT MANUALLY` + const header = `// This file is auto-generated by scripts/generate-metadata.ts\n// DO NOT EDIT MANUALLY` // Custom JSON stringify that uses single quotes and unquoted keys - function formatObject(obj, indent = 0) { + function formatObject(obj: unknown, indent = 0): string { const spaces = ' '.repeat(indent) const innerSpaces = ' '.repeat(indent + 1) @@ -361,10 +391,10 @@ function main() { .map(key => { const quotedKey = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) && !key.includes('-') ? key : `'${key}'` - const value = formatObject(obj[key], indent + 1) + const value = formatObject((obj as Record)[key], indent + 1) // Check if the line would be too long (>80 chars) and split if needed const fullLine = `${innerSpaces}${quotedKey}: ${value}` - if (fullLine.length > 80 && typeof obj[key] === 'string') { + if (fullLine.length > 80 && typeof (obj as Record)[key] === 'string') { return `${innerSpaces}${quotedKey}:\n${innerSpaces} ${value}` } return fullLine @@ -603,7 +633,7 @@ ${manifestoComponentsCode} } // Validate manifesto existence for all locales - const manifestoLocales = [] + const manifestoLocales: string[] = [] for (const locale of SUPPORTED_LOCALES) { const manifestoIndex = path.join(rootDir, `content/manifesto/${locale}/index.mdx`) if (fs.existsSync(manifestoIndex)) { @@ -634,7 +664,7 @@ ${manifestoComponentsCode} }) console.log(`✅ Formatting complete`) } catch (error) { - console.error(`⚠️ Biome formatting failed:`, error.message) + console.error(`⚠️ Biome formatting failed:`, (error as Error).message) } } diff --git a/scripts/generate/index.mjs b/scripts/generate/index.ts similarity index 66% rename from scripts/generate/index.mjs rename to scripts/generate/index.ts index e9b1f8e8..9ac4503b 100644 --- a/scripts/generate/index.mjs +++ b/scripts/generate/index.ts @@ -7,16 +7,16 @@ * Can be called from CI or manually. * * Usage: - * node scripts/generate/index.mjs [script-name] + * node scripts/generate/index.ts [script-name] * * If no script name is provided, runs all scripts. */ -import { runCategoryScripts } from '../_shared/runner.mjs' +import { type CategoryConfig, runCategoryScripts } from '../_shared/runner' runCategoryScripts({ categoryName: 'generate', -}).catch(error => { +} as CategoryConfig).catch(error => { console.error('Fatal error:', error) process.exit(1) }) diff --git a/scripts/refactor/export-vendors.mjs b/scripts/refactor/export-vendors.ts similarity index 55% rename from scripts/refactor/export-vendors.mjs rename to scripts/refactor/export-vendors.ts index 6db41ef2..e0df5697 100644 --- a/scripts/refactor/export-vendors.mjs +++ b/scripts/refactor/export-vendors.ts @@ -16,13 +16,41 @@ const ROOT_DIR = path.resolve(__dirname, '../..') const MANIFESTS_DIR = path.join(ROOT_DIR, 'manifests') const VENDORS_DIR = path.join(MANIFESTS_DIR, 'vendors') +interface VendorData { + name: string + websiteUrl: string | null + verified: boolean + communityUrls: CommunityUrls | null + translations: Record | null +} + +interface CommunityUrls { + linkedin: string | null + twitter: string | null + github: string | null + youtube: string | null + discord: string | null + reddit: string | null + blog: string | null +} + +interface VendorObject { + id: string + name: string + description: string + translations: Record + verified: boolean + websiteUrl: string | null + communityUrls: CommunityUrls +} + /** * Convert vendor name to vendor id * Rules: lowercase, replace spaces and dots with hyphens - * @param {string} vendorName - The vendor name - * @returns {string} The vendor id + * @param vendorName - The vendor name + * @returns The vendor id */ -function vendorNameToId(vendorName) { +function vendorNameToId(vendorName: string): string { return vendorName .toLowerCase() .replace(/\s+/g, '-') @@ -33,20 +61,20 @@ function vendorNameToId(vendorName) { /** * Load and parse a JSON file - * @param {string} filePath - Path to the JSON file - * @returns {Promise} Parsed JSON object + * @param filePath - Path to the JSON file + * @returns Parsed JSON object */ -async function loadJSON(filePath) { - const content = await fs.readFile(filePath, 'utf-8') - return JSON.parse(content) +async function loadJSON(filePath: string): Promise> { + const content = await fs.readFile(filePath, 'utf8') + return JSON.parse(content) as Record } /** * Check if a file exists - * @param {string} filePath - Path to the file - * @returns {Promise} True if file exists + * @param filePath - Path to the file + * @returns True if file exists */ -async function fileExists(filePath) { +async function fileExists(filePath: string): Promise { try { await fs.access(filePath) return true @@ -57,13 +85,13 @@ async function fileExists(filePath) { /** * Get all JSON files in a directory - * @param {string} dirPath - Directory path - * @returns {Promise} Array of JSON file paths + * @param dirPath - Directory path + * @returns Array of JSON file paths */ -async function getJsonFiles(dirPath) { +async function getJsonFiles(dirPath: string): Promise { try { const entries = await fs.readdir(dirPath, { withFileTypes: true }) - const jsonFiles = [] + const jsonFiles: string[] = [] for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.json')) { @@ -80,12 +108,15 @@ async function getJsonFiles(dirPath) { /** * Merge community URLs objects * Priority: existing value > new value (if existing is null/undefined, use new) - * @param {Object|null} existing - Existing communityUrls object - * @param {Object|null} newUrls - New communityUrls object - * @returns {Object} Merged communityUrls object + * @param existing - Existing communityUrls object + * @param newUrls - New communityUrls object + * @returns Merged communityUrls object */ -function mergeCommunityUrls(existing, newUrls) { - const result = { +function mergeCommunityUrls( + existing: CommunityUrls | null, + newUrls: CommunityUrls | null +): CommunityUrls { + const result: CommunityUrls = { linkedin: null, twitter: null, github: null, @@ -97,18 +128,20 @@ function mergeCommunityUrls(existing, newUrls) { // Start with existing values if (existing) { - Object.keys(result).forEach(key => { + const keys = Object.keys(result) as Array + for (const key of keys) { result[key] = existing[key] || null - }) + } } // Override with new values if existing is null if (newUrls) { - Object.keys(result).forEach(key => { + const keys = Object.keys(result) as Array + for (const key of keys) { if (!result[key] && newUrls[key]) { result[key] = newUrls[key] } - }) + } } return result @@ -117,22 +150,18 @@ function mergeCommunityUrls(existing, newUrls) { /** * Merge vendor information from multiple manifests * Priority: existing value > new value (if existing is null/undefined, use new) - * @param {Object} existing - Existing vendor data - * @param {Object} newData - New vendor data from manifest - * @returns {Object} Merged vendor data + * @param existing - Existing vendor data + * @param newData - New vendor data from manifest + * @returns Merged vendor data */ -function mergeVendorData(existing, newData) { - const merged = { ...existing } +function mergeVendorData(existing: VendorData, newData: VendorData): VendorData { + const merged: VendorData = { ...existing } // Merge basic fields if (!merged.websiteUrl && newData.websiteUrl) { merged.websiteUrl = newData.websiteUrl } - if (!merged.docsUrl && newData.docsUrl) { - merged.docsUrl = newData.docsUrl - } - if (merged.verified === undefined && newData.verified !== undefined) { merged.verified = newData.verified } @@ -140,20 +169,20 @@ function mergeVendorData(existing, newData) { // Merge communityUrls merged.communityUrls = mergeCommunityUrls(merged.communityUrls, newData.communityUrls) - // Merge i18n if both exist - if (newData.i18n) { - if (!merged.i18n) { - merged.i18n = {} + // Merge translations if both exist + if (newData.translations) { + if (!merged.translations) { + merged.translations = {} } - // Merge i18n descriptions for each locale - Object.keys(newData.i18n).forEach(locale => { - if (!merged.i18n[locale]) { - merged.i18n[locale] = {} + // Merge translations descriptions for each locale + for (const locale of Object.keys(newData.translations)) { + if (!merged.translations[locale]) { + merged.translations[locale] = {} } - if (!merged.i18n[locale].description && newData.i18n[locale]?.description) { - merged.i18n[locale].description = newData.i18n[locale].description + if (!merged.translations[locale].description && newData.translations[locale]?.description) { + merged.translations[locale].description = newData.translations[locale].description } - }) + } } return merged @@ -161,21 +190,24 @@ function mergeVendorData(existing, newData) { /** * Extract vendor information from a manifest file - * @param {Object} manifest - The manifest object - * @returns {Object|null} Extracted vendor data or null if no vendor field + * @param manifest - The manifest object + * @returns Extracted vendor data or null if no vendor field */ -function extractVendorData(manifest) { +function extractVendorData(manifest: Record): VendorData | null { if (!manifest.vendor) { return null } - const vendorData = { - name: manifest.vendor, - websiteUrl: manifest.websiteUrl || null, - docsUrl: manifest.docsUrl || null, - verified: manifest.verified !== undefined ? manifest.verified : false, - communityUrls: manifest.communityUrls || null, - i18n: manifest.i18n || null, + const vendorData: VendorData = { + name: manifest.vendor as string, + websiteUrl: (manifest.websiteUrl as string | null) || null, + verified: + (manifest.verified as boolean | undefined) !== undefined + ? (manifest.verified as boolean) + : false, + communityUrls: (manifest.communityUrls as CommunityUrls | null) || null, + translations: + (manifest.translations as Record | null) || null, } return vendorData @@ -183,22 +215,21 @@ function extractVendorData(manifest) { /** * Create a vendor file from vendor data - * @param {string} vendorId - The vendor id - * @param {Object} vendorData - The vendor data - * @returns {Object} Complete vendor object + * @param vendorId - The vendor id + * @param vendorData - The vendor data + * @returns Complete vendor object */ -function createVendorObject(vendorId, vendorData) { +function createVendorObject(vendorId: string, vendorData: VendorData): VendorObject { // Default description if not provided const defaultDescription = `${vendorData.name} is a vendor.` - const vendor = { + const vendor: VendorObject = { id: vendorId, name: vendorData.name, - description: vendorData.description || defaultDescription, - i18n: vendorData.i18n || {}, - websiteUrl: vendorData.websiteUrl || null, - docsUrl: vendorData.docsUrl || null, + description: (vendorData as { description?: string }).description || defaultDescription, + translations: vendorData.translations || {}, verified: vendorData.verified !== undefined ? vendorData.verified : false, + websiteUrl: vendorData.websiteUrl || null, communityUrls: mergeCommunityUrls(null, vendorData.communityUrls), } @@ -207,11 +238,13 @@ function createVendorObject(vendorId, vendorData) { /** * Process a single manifest file and extract vendor information - * @param {string} manifestPath - Path to the manifest file - * @param {Map} vendorsMap - Map of vendor id to vendor data - * @returns {Promise} + * @param manifestPath - Path to the manifest file + * @param vendorsMap - Map of vendor id to vendor data */ -async function processManifest(manifestPath, vendorsMap) { +async function processManifest( + manifestPath: string, + vendorsMap: Map +): Promise { try { const manifest = await loadJSON(manifestPath) const vendorData = extractVendorData(manifest) @@ -224,23 +257,25 @@ async function processManifest(manifestPath, vendorsMap) { // If vendor already exists in map, merge the data if (vendorsMap.has(vendorId)) { - const existing = vendorsMap.get(vendorId) + const existing = vendorsMap.get(vendorId)! vendorsMap.set(vendorId, mergeVendorData(existing, vendorData)) } else { vendorsMap.set(vendorId, vendorData) } } catch (error) { - console.error(` ⚠️ Error processing ${manifestPath}:`, error.message) + console.error(` ⚠️ Error processing ${manifestPath}:`, (error as Error).message) } } /** * Process all manifest files in a directory - * @param {string} categoryDir - Directory path - * @param {Map} vendorsMap - Map of vendor id to vendor data - * @returns {Promise} + * @param categoryDir - Directory path + * @param vendorsMap - Map of vendor id to vendor data */ -async function processCategory(categoryDir, vendorsMap) { +async function processCategory( + categoryDir: string, + vendorsMap: Map +): Promise { const jsonFiles = await getJsonFiles(categoryDir) if (jsonFiles.length === 0) { @@ -255,14 +290,14 @@ async function processCategory(categoryDir, vendorsMap) { /** * Main function */ -async function main() { +async function main(): Promise { console.log('🔄 Exporting vendors from manifest files...\n') // Categories to process const categories = ['ides', 'clis', 'extensions', 'models', 'providers'] // Map to store vendor data (vendorId -> vendorData) - const vendorsMap = new Map() + const vendorsMap = new Map() // Process all categories for (const category of categories) { diff --git a/scripts/refactor/index.mjs b/scripts/refactor/index.ts similarity index 67% rename from scripts/refactor/index.mjs rename to scripts/refactor/index.ts index 85c44fa7..19f4ec4c 100644 --- a/scripts/refactor/index.mjs +++ b/scripts/refactor/index.ts @@ -7,21 +7,21 @@ * Can be called from CI or manually. * * Usage: - * node scripts/refactor/index.mjs [script-name] + * node scripts/refactor/index.ts [script-name] * * If no script name is provided, runs all scripts. * - * Note: For includes/excludes, use the script name without .mjs extension. - * For example, 'sort-manifest-fields' (not 'sort-manifest-fields.mjs') + * Note: For includes/excludes, use the script name without .ts extension. + * For example, 'sort-manifest-fields' (not 'sort-manifest-fields.ts') */ -import { runCategoryScripts } from '../_shared/runner.mjs' +import { type CategoryConfig, runCategoryScripts } from '../_shared/runner' runCategoryScripts({ categoryName: 'refactor', // Example: excludes: ['sort-manifest-fields'] to exclude it // Example: includes: ['sort-manifest-fields'] to only run it -}).catch(error => { +} as CategoryConfig).catch(error => { console.error('Fatal error:', error) process.exit(1) }) diff --git a/scripts/refactor/sort-locales-fields.mjs b/scripts/refactor/sort-locales-fields.ts old mode 100755 new mode 100644 similarity index 69% rename from scripts/refactor/sort-locales-fields.mjs rename to scripts/refactor/sort-locales-fields.ts index f1968d6d..e7298204 --- a/scripts/refactor/sort-locales-fields.mjs +++ b/scripts/refactor/sort-locales-fields.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node /** - * Sort fields in all locales JSON files alphabetically - * This script recursively processes all JSON files in the locales directory + * Sort fields in all translation JSON files alphabetically + * This script recursively processes all JSON files in the translations directory * and sorts their object keys alphabetically (including nested objects). */ @@ -13,23 +13,28 @@ import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const ROOT_DIR = path.resolve(__dirname, '../..') -const LOCALES_DIR = path.join(ROOT_DIR, 'locales') +const LOCALES_DIR = path.join(ROOT_DIR, 'translations') + +interface JsonFile { + fullPath: string + relativePath: string +} /** * Recursively sort object keys alphabetically * Arrays are preserved as-is, only object keys are sorted */ -function sortObjectKeysRecursively(obj) { +function sortObjectKeysRecursively(obj: unknown): unknown { if (Array.isArray(obj)) { return obj.map(item => sortObjectKeysRecursively(item)) } if (obj && typeof obj === 'object') { - const sorted = {} + const sorted: Record = {} const keys = Object.keys(obj).sort() for (const key of keys) { - sorted[key] = sortObjectKeysRecursively(obj[key]) + sorted[key] = sortObjectKeysRecursively((obj as Record)[key]) } return sorted @@ -41,7 +46,7 @@ function sortObjectKeysRecursively(obj) { /** * Process a single JSON file */ -async function processJsonFile(filePath, relativePath) { +async function processJsonFile(filePath: string, relativePath: string): Promise { try { // Read the file const content = await fs.readFile(filePath, 'utf-8') @@ -56,15 +61,15 @@ async function processJsonFile(filePath, relativePath) { console.log(` ✅ ${relativePath}`) } catch (error) { - console.error(` ❌ Error processing ${relativePath}:`, error.message) + console.error(` ❌ Error processing ${relativePath}:`, (error as Error).message) } } /** * Recursively find all JSON files in a directory */ -async function findJsonFiles(dirPath, basePath = dirPath) { - const jsonFiles = [] +async function findJsonFiles(dirPath: string, basePath = dirPath): Promise { + const jsonFiles: JsonFile[] = [] try { const entries = await fs.readdir(dirPath, { withFileTypes: true }) @@ -83,7 +88,7 @@ async function findJsonFiles(dirPath, basePath = dirPath) { } } } catch (error) { - console.error(` ⚠️ Error reading directory ${dirPath}:`, error.message) + console.error(` ⚠️ Error reading directory ${dirPath}:`, (error as Error).message) } return jsonFiles @@ -92,14 +97,14 @@ async function findJsonFiles(dirPath, basePath = dirPath) { /** * Main function */ -async function main() { - console.log('🔄 Sorting locales JSON files alphabetically...\n') +async function main(): Promise { + console.log('🔄 Sorting translations JSON files alphabetically...\n') - // Find all JSON files in locales directory + // Find all JSON files in translations directory const jsonFiles = await findJsonFiles(LOCALES_DIR) if (jsonFiles.length === 0) { - console.log('⚠️ No JSON files found in locales directory') + console.log('⚠️ No JSON files found in translations directory') return } diff --git a/scripts/refactor/sort-manifest-fields.mjs b/scripts/refactor/sort-manifest-fields.ts old mode 100755 new mode 100644 similarity index 84% rename from scripts/refactor/sort-manifest-fields.mjs rename to scripts/refactor/sort-manifest-fields.ts index 5c374409..02ea0688 --- a/scripts/refactor/sort-manifest-fields.mjs +++ b/scripts/refactor/sort-manifest-fields.ts @@ -16,15 +16,53 @@ const ROOT_DIR = path.resolve(__dirname, '../..') const MANIFESTS_DIR = path.join(ROOT_DIR, 'manifests') const SCHEMAS_DIR = path.join(MANIFESTS_DIR, '$schemas') +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } + +interface Schema { + $ref?: string + allOf?: Schema[] + properties?: Record + items?: Schema + type?: string + patternProperties?: Record + $defs?: Record +} + +interface ManifestCategory { + dir: string + schema: string +} + +interface CategoryStats { + total: number + processed: number + errors: number + validationErrors: number +} + +interface Stats { + totalFiles: number + processedFiles: number + errorFiles: number + totalErrors: number + categories: Record +} + +interface ValidationError { + field: string + location: string + path: (string | number)[] +} + // Cache for loaded schemas to avoid redundant reads -const schemaCache = new Map() +const schemaCache = new Map() /** * Load and parse a JSON file */ -async function loadJSON(filePath) { +async function loadJSON(filePath: string): Promise { try { - const content = await fs.readFile(filePath, 'utf-8') + const content = await fs.readFile(filePath, 'utf8') // Remove BOM if present const cleanContent = content.replace(/^\uFEFF/, '') // Trim whitespace @@ -52,7 +90,7 @@ async function loadJSON(filePath) { * Resolve a $ref path to an absolute file path or return the path for internal references * Returns null for internal references (handled separately), or the resolved file path */ -function resolveRefPath(refPath, baseSchemaPath) { +function resolveRefPath(refPath: string, baseSchemaPath: string): string | null { if (refPath.startsWith('#/')) { // Internal reference within the same schema (e.g., #/$defs/collectionSection) // Return null to indicate it should be resolved from the same schema @@ -66,32 +104,34 @@ function resolveRefPath(refPath, baseSchemaPath) { /** * Resolve an internal reference (e.g., #/$defs/collectionSection) from a schema */ -function resolveInternalRef(refPath, schema) { +function resolveInternalRef(refPath: string, schema: Schema): Schema | null { if (!refPath.startsWith('#/')) { return null } // Remove the #/ prefix const pathParts = refPath.slice(2).split('/') - let current = schema + let current: JsonValue = schema as JsonValue for (const part of pathParts) { - if (current && typeof current === 'object' && part in current) { - current = current[part] + if (current && typeof current === 'object' && !Array.isArray(current) && part in current) { + const next = (current as Record)[part] + if (next === undefined) return null + current = next } else { return null } } - return current + return current as Schema } /** * Extract property order from a schema definition * Returns an array of property names in the order they appear in the schema */ -async function extractPropertyOrder(schema, schemaPath) { - const propertyOrder = [] +async function extractPropertyOrder(schema: Schema, schemaPath: string): Promise { + const propertyOrder: string[] = [] // Handle $ref at the root level if (schema.$ref) { @@ -155,12 +195,12 @@ async function extractPropertyOrder(schema, schemaPath) { /** * Load a schema file with caching */ -async function loadSchema(schemaPath) { +async function loadSchema(schemaPath: string): Promise { if (schemaCache.has(schemaPath)) { - return schemaCache.get(schemaPath) + return schemaCache.get(schemaPath) as Schema } - const schema = await loadJSON(schemaPath) + const schema = (await loadJSON(schemaPath)) as Schema schemaCache.set(schemaPath, schema) return schema } @@ -168,8 +208,12 @@ async function loadSchema(schemaPath) { /** * Find all nested property orders from a schema */ -async function findAllNestedOrders(schema, schemaPath, parentPath = []) { - const nestedOrders = new Map() +async function findAllNestedOrders( + schema: Schema, + schemaPath: string, + parentPath: string[] = [] +): Promise> { + const nestedOrders = new Map() // Handle allOf if (schema.allOf) { @@ -178,7 +222,7 @@ async function findAllNestedOrders(schema, schemaPath, parentPath = []) { // Handle $ref in allOf if (subSchema.$ref) { const refPath = resolveRefPath(subSchema.$ref, schemaPath) - let refSchema = null + let refSchema: Schema | null = null let refSchemaPath = schemaPath if (refPath) { @@ -209,13 +253,14 @@ async function findAllNestedOrders(schema, schemaPath, parentPath = []) { // Handle properties if (schema.properties) { for (const [propName, propSchema] of Object.entries(schema.properties)) { - const currentPath = [...parentPath, propName].join('.') + const currentPath = [...parentPath, propName] + const pathKey = currentPath.join('.') // Handle $ref if (propSchema.$ref) { const refPath = resolveRefPath(propSchema.$ref, schemaPath) const currentSchema = await loadSchema(schemaPath) - let refSchema = null + let refSchema: Schema | null = null let refSchemaPath = schemaPath if (refPath) { @@ -230,13 +275,13 @@ async function findAllNestedOrders(schema, schemaPath, parentPath = []) { if (refSchema) { const order = await extractPropertyOrder(refSchema, refSchemaPath) if (order.length > 0) { - nestedOrders.set(currentPath, order) + nestedOrders.set(pathKey, order) } // Recursively find nested orders in the referenced schema const refNestedOrders = await findAllNestedOrders(refSchema, refSchemaPath, []) for (const [nestedPath, nestedOrder] of refNestedOrders) { - nestedOrders.set(`${currentPath}.${nestedPath}`, nestedOrder) + nestedOrders.set(`${pathKey}.${nestedPath}`, nestedOrder) } } } @@ -244,7 +289,7 @@ async function findAllNestedOrders(schema, schemaPath, parentPath = []) { // Handle direct object properties if (propSchema.properties) { const order = Object.keys(propSchema.properties) - nestedOrders.set(currentPath, order) + nestedOrders.set(pathKey, order) // Recursively handle nested objects const deepOrders = await findAllNestedOrders(propSchema, schemaPath, [ @@ -258,11 +303,11 @@ async function findAllNestedOrders(schema, schemaPath, parentPath = []) { // Handle array items if (propSchema.items) { - const itemsPath = `${currentPath}.items` + const itemsPath = `${pathKey}.items` if (propSchema.items.$ref) { const refPath = resolveRefPath(propSchema.items.$ref, schemaPath) const currentSchema = await loadSchema(schemaPath) - let refSchema = null + let refSchema: Schema | null = null let refSchemaPath = schemaPath if (refPath) { @@ -316,30 +361,39 @@ async function findAllNestedOrders(schema, schemaPath, parentPath = []) { * Sort object keys according to a specified order * $schema field is always placed first */ -function sortObjectKeys(obj, keyOrder) { +function sortObjectKeys(obj: JsonValue, keyOrder: string[]): JsonValue { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { return obj } - const sorted = {} + const sorted: Record = {} const objKeys = Object.keys(obj) // Always place $schema first if it exists if ('$schema' in obj) { - sorted.$schema = obj.$schema + const value = (obj as Record).$schema + if (value !== undefined) { + sorted.$schema = value + } } // Then, add keys in the specified order (excluding $schema) for (const key of keyOrder) { if (key in obj && key !== '$schema') { - sorted[key] = obj[key] + const value = (obj as Record)[key] + if (value !== undefined) { + sorted[key] = value + } } } // Then, add any remaining keys that weren't in the order (excluding $schema) for (const key of objKeys) { if (!(key in sorted) && key !== '$schema') { - sorted[key] = obj[key] + const value = (obj as Record)[key] + if (value !== undefined) { + sorted[key] = value + } } } @@ -351,12 +405,21 @@ function sortObjectKeys(obj, keyOrder) { * Also collects patternProperties information for dynamic keys */ async function collectAllValidProperties( - schema, - schemaPath, - parentPath = [], - validProps = new Set(), - patternProps = new Map() -) { + schema: Schema, + schemaPath: string, + parentPath: string[] = [], + validProps = new Set(), + patternProps = new Map< + string, + { pattern: string; schema: Schema; schemaPath: string; properties: Set } + >() +): Promise<{ + validProps: Set + patternProps: Map< + string, + { pattern: string; schema: Schema; schemaPath: string; properties: Set } + > +}> { const currentSchema = await loadSchema(schemaPath) // Handle $ref at root level @@ -460,7 +523,7 @@ async function collectAllValidProperties( // Recursively collect nested properties if (propSchema.$ref) { const refPath = resolveRefPath(propSchema.$ref, schemaPath) - let refSchema = null + let refSchema: Schema | null = null let refSchemaPath = schemaPath if (refPath) { @@ -571,7 +634,7 @@ async function collectAllValidProperties( /** * Check if a key matches a pattern (for patternProperties) */ -function matchesPattern(key, pattern) { +function matchesPattern(key: string, pattern: string): boolean { try { const regex = new RegExp(pattern) return regex.test(key) @@ -583,8 +646,11 @@ function matchesPattern(key, pattern) { /** * Extract property names from a schema (for patternProperties validation) */ -async function extractPropertiesFromSchema(schema, schemaPath) { - const props = new Set() +async function extractPropertiesFromSchema( + schema: Schema, + schemaPath: string +): Promise> { + const props = new Set() const currentSchema = await loadSchema(schemaPath) // Handle $ref @@ -650,12 +716,15 @@ async function extractPropertiesFromSchema(schema, schemaPath) { * Validate manifest fields against schema */ function validateManifestFields( - manifest, - validProps, - path = [], - errors = [], - patternProps = new Map() -) { + manifest: JsonValue, + validProps: Set, + path: (string | number)[] = [], + errors: ValidationError[] = [], + patternProps = new Map< + string, + { pattern: string; schema: Schema; schemaPath: string; properties: Set } + >() +): ValidationError[] { if (Array.isArray(manifest)) { manifest.forEach(item => { // For arrays, we don't validate the index itself, just the items @@ -680,9 +749,9 @@ function validateManifestFields( // If parent uses patternProperties, check if current key matches if (patternInfo && path.length > 0) { - const currentKey = path[path.length - 1] + const currentKey = String(path[path.length - 1]) if (!matchesPattern(currentKey, patternInfo.pattern)) { - patternInfo = null + patternInfo = undefined } } } @@ -762,7 +831,11 @@ function validateManifestFields( /** * Recursively sort all nested objects in a data structure */ -function sortNestedObjects(data, nestedOrders, path = []) { +function sortNestedObjects( + data: JsonValue, + nestedOrders: Map, + path: string[] = [] +): JsonValue { if (Array.isArray(data)) { return data.map(item => sortNestedObjects(item, nestedOrders, path)) } @@ -773,11 +846,16 @@ function sortNestedObjects(data, nestedOrders, path = []) { const order = nestedOrders.get(currentPath) // Sort current level - const sorted = order ? sortObjectKeys(data, order) : { ...data } + const sorted = order ? sortObjectKeys(data, order) : (data as Record) // Recursively sort nested objects - for (const [key, value] of Object.entries(sorted)) { - sorted[key] = sortNestedObjects(value, nestedOrders, [...path, key]) + if (sorted && typeof sorted === 'object' && !Array.isArray(sorted)) { + for (const [key, value] of Object.entries(sorted)) { + ;(sorted as Record)[key] = sortNestedObjects(value, nestedOrders, [ + ...path, + key, + ]) + } } return sorted @@ -790,7 +868,11 @@ function sortNestedObjects(data, nestedOrders, path = []) { * Process a single manifest file * Returns validation errors count */ -async function processManifest(manifestPath, schemaPath, relativePath) { +async function processManifest( + manifestPath: string, + schemaPath: string, + relativePath: string +): Promise<{ errorCount: number; hasError: boolean }> { // Check if schema exists try { await fs.access(schemaPath) @@ -826,7 +908,7 @@ async function processManifest(manifestPath, schemaPath, relativePath) { validProps.add('$schema') // Validate manifest fields - const validationErrors = [] + const validationErrors: ValidationError[] = [] if (Array.isArray(manifest)) { manifest.forEach((item, index) => { const errors = validateManifestFields(item, validProps, [index], [], patternProps) @@ -850,7 +932,7 @@ async function processManifest(manifestPath, schemaPath, relativePath) { // Sort the manifest data // If schema is array type but manifest is object, treat it as a single item - let sortedManifest + let sortedManifest: JsonValue if (Array.isArray(manifest)) { sortedManifest = manifest.map(item => { const sorted = sortObjectKeys(item, propertyOrder) @@ -871,10 +953,10 @@ async function processManifest(manifestPath, schemaPath, relativePath) { /** * Get all JSON files in a directory */ -async function getJsonFiles(dirPath) { +async function getJsonFiles(dirPath: string): Promise { try { const entries = await fs.readdir(dirPath, { withFileTypes: true }) - const jsonFiles = [] + const jsonFiles: string[] = [] for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.json')) { @@ -892,11 +974,11 @@ async function getJsonFiles(dirPath) { /** * Main function */ -async function main() { +async function main(): Promise { console.log('🔄 Sorting manifest fields according to schemas...\n') // Statistics - const stats = { + const stats: Stats = { totalFiles: 0, processedFiles: 0, errorFiles: 0, @@ -905,7 +987,7 @@ async function main() { } // Mapping of subdirectories to their schema files - const manifestCategories = [ + const manifestCategories: ManifestCategory[] = [ { dir: 'vendors', schema: 'vendor.schema.json' }, { dir: 'providers', schema: 'provider.schema.json' }, { dir: 'models', schema: 'model.schema.json' }, @@ -940,19 +1022,19 @@ async function main() { const result = await processManifest(manifestPath, schemaPath, relativePath) if (result.hasError) { stats.errorFiles++ - stats.categories[category.dir].errors++ + stats.categories[category.dir]!.errors++ } else { stats.processedFiles++ - stats.categories[category.dir].processed++ + stats.categories[category.dir]!.processed++ if (result.errorCount > 0) { stats.totalErrors += result.errorCount - stats.categories[category.dir].validationErrors += result.errorCount + stats.categories[category.dir]!.validationErrors += result.errorCount } } } catch (error) { - console.error(` ❌ Error processing ${relativePath}:`, error.message) + console.error(` ❌ Error processing ${relativePath}:`, (error as Error).message) stats.errorFiles++ - stats.categories[category.dir].errors++ + stats.categories[category.dir]!.errors++ } } } @@ -973,7 +1055,7 @@ async function main() { } } } catch (error) { - console.error(` ❌ Error processing collections.json:`, error.message) + console.error(` ❌ Error processing collections.json:`, (error as Error).message) stats.totalFiles++ stats.errorFiles++ } @@ -998,7 +1080,7 @@ async function main() { if (categoriesWithErrors.length > 0) { console.log('\nCategory breakdown:') for (const [category, catStats] of categoriesWithErrors) { - const parts = [`${category}: ${catStats.processed}/${catStats.total} processed`] + const parts: string[] = [`${category}: ${catStats.processed}/${catStats.total} processed`] if (catStats.errors > 0) { parts.push(`${catStats.errors} file error(s)`) } diff --git a/scripts/validate/lib/ast-parser.ts b/scripts/validate/lib/ast-parser.ts new file mode 100644 index 00000000..8910912a --- /dev/null +++ b/scripts/validate/lib/ast-parser.ts @@ -0,0 +1,337 @@ +/** + * AST Parser for extracting translation usage from TSX/TS files + */ + +import fs from 'node:fs' +import path from 'node:path' +import { parse } from '@typescript-eslint/parser' +import type { TSESTree } from '@typescript-eslint/types' +import type { ParsedFile, SourceLocation, TranslationCall, TranslationUsage } from './types.js' +import { FileType } from './types.js' + +/** + * Determine file type based on path + */ +export function getFileType(filePath: string): FileType { + const normalizedPath = path.normalize(filePath) + + // Pages: src/app/[locale]/**/page*.tsx + if (normalizedPath.includes('src/app/[locale]')) { + return FileType.PAGE + } + + // Components: src/components/**/*.tsx + if (normalizedPath.includes('src/components')) { + return FileType.COMPONENT + } + + return FileType.UNKNOWN +} + +/** + * Extract string literal value from a node + */ +function extractStringLiteral(node: TSESTree.Node): string | null { + if (node.type === 'Literal') { + if (typeof node.value === 'string') { + return node.value + } + } + return null +} + +/** + * Check if a node is a template literal with expressions + */ +function isTemplateLiteralWithExpressions(node: TSESTree.Node): boolean { + return node.type === 'TemplateLiteral' && node.expressions.length > 0 +} + +/** + * Check if a node is a binary expression (string concatenation) + */ +function isBinaryExpression(node: TSESTree.Node): boolean { + if (node.type === 'BinaryExpression') { + return true + } + return false +} + +/** + * Determine the key type based on the AST node + */ +function getKeyType(node: TSESTree.CallExpressionArgument): TranslationCall['keyType'] { + if (isTemplateLiteralWithExpressions(node)) { + return 'dynamic' + } + if (isBinaryExpression(node)) { + return 'dynamic' + } + if (node.type === 'Identifier' || node.type === 'MemberExpression') { + return 'dynamic' + } + if (extractStringLiteral(node) !== null) { + return 'static' + } + return 'partial' +} + +/** + * Extract translation key from a call expression argument + */ +function extractTranslationKey(node: TSESTree.CallExpressionArgument): { + key: string + keyType: TranslationCall['keyType'] +} { + const keyType = getKeyType(node) + + if (keyType === 'static') { + return { key: extractStringLiteral(node)!, keyType } + } + + if (keyType === 'dynamic') { + return { key: '', keyType } + } + + return { key: '', keyType } +} + +/** + * Visit all nodes in the AST + */ +function visit(ast: TSESTree.Program, visitor: (node: TSESTree.Node) => void): void { + const stack: TSESTree.Node[] = [ast] + + while (stack.length > 0) { + const node = stack.pop()! + visitor(node) + + // Push children onto stack + for (const key of Object.keys(node)) { + const child = node[key as keyof TSESTree.Node] + if (typeof child === 'object' && child !== null) { + if (Array.isArray(child)) { + for (const item of child) { + if (typeof item === 'object' && item !== null && 'type' in item) { + stack.push(item as TSESTree.Node) + } + } + } else if ('type' in child) { + stack.push(child as TSESTree.Node) + } + } + } + } +} + +/** + * Find all useTranslations declarations in the AST + */ +function findTranslationsDeclarations( + ast: TSESTree.Program +): Map { + const translationsMap = new Map() + + visit(ast, node => { + // Look for: const tX = useTranslations('namespace') + if (node.type === 'VariableDeclarator') { + const declarator = node as TSESTree.VariableDeclarator + + if (declarator.id.type === 'Identifier' && declarator.init) { + const varName = declarator.id.name + + // Check if it's a call expression + if (declarator.init.type === 'CallExpression') { + const call = declarator.init + + // Check if callee is useTranslations + if (call.callee.type === 'Identifier' && call.callee.name === 'useTranslations') { + // Extract namespace argument + if (call.arguments.length > 0) { + const namespaceArg = call.arguments[0] + if (namespaceArg) { + const namespace = extractStringLiteral(namespaceArg) + + if (namespace) { + translationsMap.set(varName, { + namespace, + location: { + file: '', + line: declarator.loc?.start.line ?? 0, + column: declarator.loc?.start.column ?? 0, + }, + }) + } + } + } + } + } + } + } + }) + + return translationsMap +} + +/** + * Find all translation function calls in the AST + */ +function findTranslationCalls( + ast: TSESTree.Program, + translationsVars: Set +): Array<{ + varName: string + key: string + keyType: TranslationCall['keyType'] + location: SourceLocation +}> { + const calls: Array<{ + varName: string + key: string + keyType: TranslationCall['keyType'] + location: SourceLocation + }> = [] + + visit(ast, node => { + // Look for: tX('key') or tX('key', options) + if (node.type === 'CallExpression') { + const call = node as TSESTree.CallExpression + + // Check if callee is a translation variable + if (call.callee.type === 'Identifier' && translationsVars.has(call.callee.name)) { + const varName = call.callee.name + + // Extract key argument + if (call.arguments.length > 0) { + const keyArg = call.arguments[0] + if (keyArg) { + const { key, keyType } = extractTranslationKey(keyArg) + + calls.push({ + varName, + key, + keyType, + location: { + file: '', + line: call.loc?.start.line ?? 0, + column: call.loc?.start.column ?? 0, + }, + }) + } + } + } + } + }) + + return calls +} + +/** + * Parse a single file and extract translation usage + */ +export function parseFile(filePath: string): ParsedFile { + const fileType = getFileType(filePath) + + try { + const content = fs.readFileSync(filePath, 'utf-8') + + const ast = parse(content, { + sourceType: 'module', + ecmaVersion: 'latest', + ecmaFeatures: { + jsx: true, + }, + filePath, + }) + + // Find all useTranslations declarations + const translationsDeclarations = findTranslationsDeclarations(ast) + const translationsVars = new Set(translationsDeclarations.keys()) + + // Find all translation calls + const calls = findTranslationCalls(ast, translationsVars) + + // Group calls by variable name + const usages: TranslationUsage[] = [] + for (const [varName, { namespace, location }] of translationsDeclarations) { + const varCalls = calls.filter(c => c.varName === varName) + + usages.push({ + variableName: varName, + namespace, + calls: varCalls.map(c => ({ + key: c.key, + keyType: c.keyType, + location: { + file: filePath, + line: c.location.line, + column: c.location.column, + }, + })), + location: { + file: filePath, + line: location.line, + column: location.column, + }, + }) + } + + return { + path: filePath, + fileType, + usages, + } + } catch (error) { + return { + path: filePath, + fileType, + usages: [], + error: error instanceof Error ? error.message : String(error), + } + } +} + +/** + * Find all TSX/TS files in a directory recursively + */ +export function findSourceFiles(dir: string, extensions: string[] = ['.tsx', '.ts']): string[] { + const files: string[] = [] + + const traverse = (currentPath: string): void => { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name) + + if (entry.isDirectory()) { + // Skip node_modules and other common directories + if ( + entry.name === 'node_modules' || + entry.name === '.next' || + entry.name === 'dist' || + entry.name === 'build' || + entry.name === '.git' + ) { + continue + } + traverse(fullPath) + } else if (entry.isFile()) { + const ext = path.extname(entry.name) + if (extensions.includes(ext)) { + files.push(fullPath) + } + } + } + } + + traverse(dir) + + return files +} + +/** + * Filter files by type (pages or components) + */ +export function filterFilesByType(files: string[], fileType: FileType): string[] { + return files.filter(file => getFileType(file) === fileType) +} diff --git a/scripts/validate/lib/config.mjs b/scripts/validate/lib/config.ts similarity index 61% rename from scripts/validate/lib/config.mjs rename to scripts/validate/lib/config.ts index b55f6b91..eb2d6555 100644 --- a/scripts/validate/lib/config.mjs +++ b/scripts/validate/lib/config.ts @@ -4,37 +4,17 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' +import { locales } from '@/i18n/config' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) export const ROOT_DIR = path.resolve(__dirname, '../../..') -// Locales configuration -export const LOCALES = [ - 'en', - 'de', - 'es', - 'fr', - 'id', - 'ja', - 'ko', - 'pt', - 'ru', - 'tr', - 'zh-Hans', - 'zh-Hant', -] +// Re-export from central i18n config +export const LOCALES = locales as readonly string[] // Base URL - can be overridden via BASE_URL environment variable export const BASE_URL = process.env.BASE_URL || 'http://localhost:3000' // Delay between requests in milliseconds - can be overridden via REQUEST_DELAY environment variable -// Default: 100ms (0.1 second) between requests to avoid overwhelming the server export const REQUEST_DELAY = Number.parseInt(process.env.REQUEST_DELAY || '100', 10) - -/** - * Get locale prefix for URL - */ -export function getLocalePrefix(locale) { - return locale === 'en' ? '' : `/${locale}` -} diff --git a/scripts/validate/lib/key-validator.ts b/scripts/validate/lib/key-validator.ts new file mode 100644 index 00000000..5e7c9d37 --- /dev/null +++ b/scripts/validate/lib/key-validator.ts @@ -0,0 +1,191 @@ +/** + * Translation key validator - checks if used keys exist in translation files + */ + +import { keyExists } from './translation-loader.js' +import type { + DynamicKeyWarning, + MissingKeyViolation, + MissingTranslationFileWarning, + ParsedFile, + SyntaxErrorWarning, + TranslationIndex, + Warning, +} from './types.js' +import { ViolationType, WarningType } from './types.js' + +/** + * Validate translation keys for a single parsed file + */ +export function validateKeys( + parsedFile: ParsedFile, + translationIndex: TranslationIndex +): { violations: MissingKeyViolation[]; warnings: Warning[] } { + const violations: MissingKeyViolation[] = [] + const warnings: Warning[] = [] + + // Handle files with parsing errors + if (parsedFile.error) { + warnings.push({ + type: 'syntax_error', + file: parsedFile.path, + error: parsedFile.error, + } as SyntaxErrorWarning) + return { violations, warnings } + } + + // Check each usage + for (const usage of parsedFile.usages) { + const { variableName, namespace, calls } = usage + + // Check if namespace exists in translations + const namespaceExists = translationIndex.byNamespace.has(namespace) + + // If the exact namespace doesn't exist, check if it might be a sub-namespace + // (e.g., 'components.common.header' where 'header' is a nested key in 'components.common') + if (!namespaceExists) { + const parts = namespace.split('.') + // Only warn if there's no possible parent namespace either + // For 'components.common.header', check if 'components.common' exists + let hasPotentialParentNamespace = false + if (parts.length >= 2) { + const parentNamespace = parts.slice(0, -1).join('.') + if (translationIndex.byNamespace.has(parentNamespace)) { + hasPotentialParentNamespace = true + } + } + + if (!hasPotentialParentNamespace) { + warnings.push({ + type: 'missing_translation_file', + file: parsedFile.path, + namespace, + locale: 'en', // Default to English for missing file warning + } as MissingTranslationFileWarning) + } + } + + // Check each call + for (const call of calls) { + const { key, keyType, location } = call + + if (keyType === 'dynamic') { + // Dynamic keys cannot be validated - add warning + warnings.push({ + type: WarningType.DYNAMIC_KEY, + file: parsedFile.path, + line: location.line, + column: location.column, + variableName, + namespace, + description: `Dynamic key detected at ${parsedFile.path}:${location.line}:${location.column}`, + }) + continue + } + + if (keyType === 'partial') { + // Unknown key type - skip validation + continue + } + + // Check if key exists + if (!keyExists(translationIndex, key, namespace)) { + violations.push({ + type: ViolationType.MISSING_KEY, + file: parsedFile.path, + line: location.line, + column: location.column, + variableName, + namespace, + key, + }) + } + } + } + + return { violations, warnings } +} + +/** + * Validate translation keys across multiple parsed files + */ +export function validateKeysAll( + parsedFiles: ParsedFile[], + translationIndex: TranslationIndex +): { violations: MissingKeyViolation[]; warnings: Warning[] } { + const allViolations: MissingKeyViolation[] = [] + const allWarnings: Warning[] = [] + + for (const parsedFile of parsedFiles) { + const { violations, warnings } = validateKeys(parsedFile, translationIndex) + allViolations.push(...violations) + allWarnings.push(...warnings) + } + + return { violations: allViolations, warnings: allWarnings } +} + +/** + * Get summary of missing key violations + */ +export function getMissingKeySummary(violations: MissingKeyViolation[]): { + byNamespace: Record + byVariable: Record +} { + const byNamespace: Record = {} + const byVariable: Record = {} + + for (const violation of violations) { + byNamespace[violation.namespace] = (byNamespace[violation.namespace] || 0) + 1 + byVariable[violation.variableName] = (byVariable[violation.variableName] || 0) + 1 + } + + return { byNamespace, byVariable } +} + +/** + * Get summary of warnings + */ +export function getWarningSummary(warnings: Warning[]): { + byType: Record + dynamicKeyWarnings: DynamicKeyWarning[] + syntaxErrorWarnings: SyntaxErrorWarning[] + missingFileWarnings: MissingTranslationFileWarning[] +} { + const byType: Record = { + dynamic_key: 0, + syntax_error: 0, + missing_translation_file: 0, + } + + const dynamicKeyWarnings: DynamicKeyWarning[] = [] + const syntaxErrorWarnings: SyntaxErrorWarning[] = [] + const missingFileWarnings: MissingTranslationFileWarning[] = [] + + for (const warning of warnings) { + const typeKey = String(warning.type) + if (!(typeKey in byType)) { + byType[typeKey] = 0 + } + byType[typeKey] = (byType[typeKey] ?? 0) + 1 + + switch (warning.type) { + case WarningType.DYNAMIC_KEY: + dynamicKeyWarnings.push(warning) + break + case WarningType.SYNTAX_ERROR: + syntaxErrorWarnings.push(warning) + break + case WarningType.MISSING_TRANSLATION_FILE: + missingFileWarnings.push(warning) + break + } + } + + return { + byType, + dynamicKeyWarnings, + syntaxErrorWarnings, + missingFileWarnings, + } +} diff --git a/scripts/validate/lib/namespace-validator.ts b/scripts/validate/lib/namespace-validator.ts new file mode 100644 index 00000000..3b219754 --- /dev/null +++ b/scripts/validate/lib/namespace-validator.ts @@ -0,0 +1,136 @@ +/** + * Namespace validator - checks correct usage of translation namespaces + */ + +import type { NamespaceViolation, ParsedFile, Violation } from './types.js' +import { FileType, ViolationType } from './types.js' + +/** + * Check if a variable name matches the pattern for page translations + * Allowed: tPage, tShared, and anything ending with Page + */ +function isPageTranslationVar(varName: string): boolean { + return varName === 'tPage' || varName === 'tShared' || varName.endsWith('Page') +} + +/** + * Check if a variable name matches the pattern for component translations + * Allowed: tComponent, tShared, and anything ending with Component + */ +function isComponentTranslationVar(varName: string): boolean { + return varName === 'tComponent' || varName === 'tShared' || varName.endsWith('Component') +} + +/** + * Check if a namespace is allowed for a file type + */ +function isAllowedNamespace(fileType: FileType, namespace: string): boolean { + const ns = namespace.toLowerCase() + + if (fileType === FileType.PAGE) { + // Pages can use 'pages.*', 'shared' + return ns.startsWith('pages.') || ns === 'shared' + } + + if (fileType === FileType.COMPONENT) { + // Components can use 'components.*', 'shared' + return ns.startsWith('components.') || ns === 'shared' + } + + return true +} + +/** + * Validate namespace usage in a parsed file + */ +export function validateNamespaces(parsedFile: ParsedFile): Violation[] { + const violations: Violation[] = [] + + // Skip files with errors + if (parsedFile.error) { + return violations + } + + // Skip unknown file types + if (parsedFile.fileType === FileType.UNKNOWN) { + return violations + } + + for (const usage of parsedFile.usages) { + const { variableName, namespace, location } = usage + + // Check if variable name follows the correct pattern for the file type + const isVarNameAllowed = + parsedFile.fileType === FileType.PAGE + ? isPageTranslationVar(variableName) + : isComponentTranslationVar(variableName) + + if (!isVarNameAllowed) { + violations.push({ + type: ViolationType.NAMESPACE, + file: parsedFile.path, + line: location.line, + column: location.column, + fileType: parsedFile.fileType, + variableName, + namespace, + }) + continue + } + + // Also check if namespace is appropriate for the file type + if (!isAllowedNamespace(parsedFile.fileType, namespace)) { + violations.push({ + type: ViolationType.NAMESPACE, + file: parsedFile.path, + line: location.line, + column: location.column, + fileType: parsedFile.fileType, + variableName, + namespace, + }) + } + } + + return violations +} + +/** + * Validate namespace usage across multiple parsed files + */ +export function validateNamespacesAll(parsedFiles: ParsedFile[]): NamespaceViolation[] { + const allViolations: NamespaceViolation[] = [] + + for (const parsedFile of parsedFiles) { + const violations = validateNamespaces(parsedFile) + for (const violation of violations) { + if (violation.type === 'namespace') { + allViolations.push(violation as NamespaceViolation) + } + } + } + + return allViolations +} + +/** + * Get summary of namespace violations + */ +export function getNamespaceViolationSummary(violations: NamespaceViolation[]): { + byFileType: Record + byVariable: Record +} { + const byFileType: Record = { + [FileType.PAGE]: 0, + [FileType.COMPONENT]: 0, + [FileType.UNKNOWN]: 0, + } + const byVariable: Record = {} + + for (const violation of violations) { + byFileType[violation.fileType]++ + byVariable[violation.variableName] = (byVariable[violation.variableName] || 0) + 1 + } + + return { byFileType, byVariable } +} diff --git a/scripts/validate/lib/reporters/console-reporter.ts b/scripts/validate/lib/reporters/console-reporter.ts new file mode 100644 index 00000000..7719bcca --- /dev/null +++ b/scripts/validate/lib/reporters/console-reporter.ts @@ -0,0 +1,218 @@ +/** + * Console reporter - outputs validation results to console + */ + +import path from 'node:path' +import type { ValidationReport } from '../types.js' + +/** + * Format a file path for display + */ +function formatFilePath(filePath: string, rootDir: string): string { + const relativePath = path.relative(rootDir, filePath) + return relativePath +} + +/** + * Print a header box + */ +function printHeader(title: string): void { + const width = 64 + const padding = Math.floor((width - title.length - 2) / 2) + const leftPad = ' '.repeat(padding) + const rightPad = ' '.repeat(width - title.length - 2 - padding) + + console.log() + console.log(`╔${'═'.repeat(width)}╗`) + console.log(`║${leftPad}${title}${rightPad}║`) + console.log(`╚${'═'.repeat(width)}╝`) + console.log() +} + +/** + * Print a section header + */ +function printSectionHeader(title: string): void { + console.log() + console.log(title) + console.log('━'.repeat(title.length)) +} + +/** + * Print summary section + */ +function printSummary(report: ValidationReport, _rootDir: string): void { + printSectionHeader('SUMMARY') + + const { summary } = report + + console.log(`Files scanned: ${summary.filesScanned}`) + console.log(`Pages: ${summary.pages}`) + console.log(`Components: ${summary.components}`) + console.log(`Translation keys: ${summary.translationKeys}`) + console.log() +} + +/** + * Print violations section + */ +function printViolations(report: ValidationReport, rootDir: string): void { + printSectionHeader('VIOLATIONS') + + const { violations } = report + + if (violations.length === 0) { + console.log('No violations found.') + return + } + + // Group by type + const namespaceViolations = violations.filter(v => v.type === 'namespace') + const missingKeyViolations = violations.filter(v => v.type === 'missing_key') + + console.log(`1. NAMESPACE VIOLATIONS: ${namespaceViolations.length}`) + + if (namespaceViolations.length > 0) { + for (const violation of namespaceViolations) { + if (violation.type === 'namespace') { + const file = formatFilePath(violation.file, rootDir) + console.log() + console.log(` ${file}:${violation.line}`) + console.log(` Variable: ${violation.variableName}`) + console.log(` Namespace: ${violation.namespace}`) + console.log(` File Type: ${violation.fileType}`) + console.log( + ` Expected: ${ + violation.fileType === 'page' + ? 'tPage, tShared (or names ending with Page/Shared)' + : 'tComponent, tShared (or names ending with Component/Shared)' + }` + ) + } + } + } + + console.log() + console.log(`2. MISSING TRANSLATION KEYS: ${missingKeyViolations.length}`) + + if (missingKeyViolations.length > 0) { + for (const violation of missingKeyViolations) { + if (violation.type === 'missing_key') { + const file = formatFilePath(violation.file, rootDir) + console.log() + console.log(` ${file}:${violation.line}`) + console.log(` Variable: ${violation.variableName}`) + console.log(` Namespace: ${violation.namespace}`) + console.log(` Key: ${violation.key}`) + } + } + } +} + +/** + * Print warnings section + */ +function printWarnings(report: ValidationReport, rootDir: string): void { + const { warnings } = report + + if (warnings.length === 0) { + return + } + + printSectionHeader('WARNINGS') + + // Group by type + const dynamicKeyWarnings = warnings.filter(w => w.type === 'dynamic_key') + const syntaxErrorWarnings = warnings.filter(w => w.type === 'syntax_error') + const missingFileWarnings = warnings.filter(w => w.type === 'missing_translation_file') + + console.log(`1. DYNAMIC KEYS (cannot be validated): ${dynamicKeyWarnings.length}`) + + if (dynamicKeyWarnings.length > 0 && dynamicKeyWarnings.length <= 10) { + for (const warning of dynamicKeyWarnings) { + if (warning.type === 'dynamic_key') { + const file = formatFilePath(warning.file, rootDir) + console.log(` ${file}:${warning.line} - ${warning.variableName}('${warning.namespace}')`) + } + } + } else if (dynamicKeyWarnings.length > 10) { + console.log(` (showing first 10 of ${dynamicKeyWarnings.length})`) + for (const warning of dynamicKeyWarnings.slice(0, 10)) { + if (warning.type === 'dynamic_key') { + const file = formatFilePath(warning.file, rootDir) + console.log(` ${file}:${warning.line} - ${warning.variableName}('${warning.namespace}')`) + } + } + } + + console.log() + console.log(`2. SYNTAX ERRORS: ${syntaxErrorWarnings.length}`) + + if (syntaxErrorWarnings.length > 0) { + for (const warning of syntaxErrorWarnings) { + if (warning.type === 'syntax_error') { + const file = formatFilePath(warning.file, rootDir) + console.log(` ${file}`) + console.log(` Error: ${warning.error}`) + } + } + } + + console.log() + console.log(`3. MISSING TRANSLATION FILES: ${missingFileWarnings.length}`) + + if (missingFileWarnings.length > 0) { + for (const warning of missingFileWarnings) { + if (warning.type === 'missing_translation_file') { + console.log(` Namespace: ${warning.namespace} (locale: ${warning.locale})`) + } + } + } +} + +/** + * Print @: references statistics + */ +function printAtReferences(report: ValidationReport): void { + printSectionHeader('@: REFERENCES STATISTICS') + + const { summary } = report + + console.log(`Total @: references: ${summary.atReferences.total}`) + console.log(`Files with @: references: ${summary.atReferences.filesWithReferences}`) +} + +/** + * Print final result + */ +function printResult(report: ValidationReport): void { + console.log() + console.log('─'.repeat(64)) + console.log() + + if (report.summary.passed) { + console.log('%s VALIDATION PASSED %s', '\x1b[42m', '\x1b[0m') + console.log() + console.log('All i18n usage is compliant with the project guidelines.') + } else { + console.log('%s VALIDATION FAILED %s', '\x1b[41m', '\x1b[0m') + console.log() + console.log( + `Found ${report.summary.violations.total} violation(s). Please fix the issues above.` + ) + } + + console.log() +} + +/** + * Print the complete validation report to console + */ +export function printReport(report: ValidationReport, rootDir: string): void { + printHeader('I18N VALIDATION REPORT') + printSummary(report, rootDir) + printViolations(report, rootDir) + printWarnings(report, rootDir) + printAtReferences(report) + printResult(report) +} diff --git a/scripts/validate/lib/reporters/json-reporter.ts b/scripts/validate/lib/reporters/json-reporter.ts new file mode 100644 index 00000000..998d76c2 --- /dev/null +++ b/scripts/validate/lib/reporters/json-reporter.ts @@ -0,0 +1,99 @@ +/** + * JSON reporter - outputs validation results as JSON + */ + +import fs from 'node:fs' +import type { ValidationReport } from '../types.js' + +/** + * Convert validation report to JSON-compatible object + */ +export function reportToJSON(report: ValidationReport): object { + return { + summary: { + filesScanned: report.summary.filesScanned, + pages: report.summary.pages, + components: report.summary.components, + translationKeys: report.summary.translationKeys, + violations: { + total: report.summary.violations.total, + namespaceViolations: report.summary.violations.namespaceViolations, + missingKeys: report.summary.violations.missingKeys, + }, + atReferences: { + total: report.summary.atReferences.total, + filesWithReferences: report.summary.atReferences.filesWithReferences, + }, + warnings: { + total: report.summary.warnings.total, + }, + passed: report.summary.passed, + }, + violations: report.violations.map(v => { + if (v.type === 'namespace') { + return { + type: 'namespace', + file: v.file, + line: v.line, + column: v.column, + fileType: v.fileType, + variableName: v.variableName, + namespace: v.namespace, + } + } + // missing_key + return { + type: 'missing_key', + file: v.file, + line: v.line, + column: v.column, + variableName: v.variableName, + namespace: v.namespace, + key: v.key, + } + }), + warnings: report.warnings.map(w => { + if (w.type === 'dynamic_key') { + return { + type: 'dynamic_key', + file: w.file, + line: w.line, + column: w.column, + variableName: w.variableName, + namespace: w.namespace, + description: w.description, + } + } + if (w.type === 'syntax_error') { + return { + type: 'syntax_error', + file: w.file, + error: w.error, + } + } + // missing_translation_file + return { + type: 'missing_translation_file', + file: w.file, + namespace: w.namespace, + locale: w.locale, + } + }), + } +} + +/** + * Print validation report as JSON to console + */ +export function printJSON(report: ValidationReport): void { + const json = reportToJSON(report) + console.log(JSON.stringify(json, null, 2)) +} + +/** + * Write validation report to a JSON file + */ +export function writeJSON(report: ValidationReport, outputPath: string): void { + const json = reportToJSON(report) + fs.writeFileSync(outputPath, JSON.stringify(json, null, 2)) +} diff --git a/scripts/validate/lib/routes.mjs b/scripts/validate/lib/routes.ts similarity index 70% rename from scripts/validate/lib/routes.mjs rename to scripts/validate/lib/routes.ts index 18e7f8e1..fbeb4534 100644 --- a/scripts/validate/lib/routes.mjs +++ b/scripts/validate/lib/routes.ts @@ -4,20 +4,20 @@ import fs from 'node:fs' import path from 'node:path' -import { ROOT_DIR } from './config.mjs' +import { ROOT_DIR } from './config' /** * Read JSON file */ -export function readJsonFile(filePath) { +export function readJsonFile(filePath: string): Record { const content = fs.readFileSync(filePath, 'utf8') - return JSON.parse(content) + return JSON.parse(content) as Record } /** * Get all static routes */ -export function getStaticRoutes() { +export function getStaticRoutes(): string[] { return [ '/', '/ides', @@ -44,24 +44,25 @@ export function getStaticRoutes() { /** * Get all slugs from manifests directory */ -export function getSlugsFromManifests(category) { +export function getSlugsFromManifests(category: string): string[] { const manifestsDir = path.join(ROOT_DIR, 'manifests', category) if (!fs.existsSync(manifestsDir)) { return [] } const files = fs.readdirSync(manifestsDir).filter(f => f.endsWith('.json')) - const slugs = [] + const slugs: string[] = [] for (const file of files) { const filePath = path.join(manifestsDir, file) try { const data = readJsonFile(filePath) if (data && typeof data === 'object' && data.id) { - slugs.push(data.id) + slugs.push(data.id as string) } } catch (error) { - console.warn(`Warning: Failed to read ${filePath}: ${error.message}`) + const err = error as Error + console.warn(`Warning: Failed to read ${filePath}: ${err.message}`) } } @@ -71,9 +72,8 @@ export function getSlugsFromManifests(category) { /** * Get article slugs from content directory */ -export function getArticleSlugs() { +export function getArticleSlugs(): string[] { try { - // Read from content directory const articlesDir = path.join(ROOT_DIR, 'content/articles/en') if (!fs.existsSync(articlesDir)) { return [] @@ -82,7 +82,8 @@ export function getArticleSlugs() { const files = fs.readdirSync(articlesDir).filter(f => f.endsWith('.mdx')) return files.map(file => file.replace('.mdx', '')) } catch (error) { - console.warn(`Warning: Failed to get article slugs: ${error.message}`) + const err = error as Error + console.warn(`Warning: Failed to get article slugs: ${err.message}`) return [] } } @@ -90,7 +91,7 @@ export function getArticleSlugs() { /** * Get doc slugs from content directory */ -export function getDocSlugs() { +export function getDocSlugs(): string[] { try { const docsDir = path.join(ROOT_DIR, 'content/docs/en') if (!fs.existsSync(docsDir)) { @@ -100,15 +101,24 @@ export function getDocSlugs() { const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.mdx')) return files.map(file => file.replace('.mdx', '')) } catch (error) { - console.warn(`Warning: Failed to get doc slugs: ${error.message}`) + const err = error as Error + console.warn(`Warning: Failed to get doc slugs: ${err.message}`) return [] } } +/** + * Dynamic route configuration + */ +export interface DynamicRoute { + path: string + category: string +} + /** * Get dynamic route configurations */ -export function getDynamicRoutes() { +export function getDynamicRoutes(): DynamicRoute[] { return [ { path: '/ides', category: 'ides' }, { path: '/clis', category: 'clis' }, diff --git a/scripts/validate/lib/translation-loader.ts b/scripts/validate/lib/translation-loader.ts new file mode 100644 index 00000000..41351f08 --- /dev/null +++ b/scripts/validate/lib/translation-loader.ts @@ -0,0 +1,311 @@ +/** + * Translation file loader and indexer + */ + +import fs from 'node:fs' +import path from 'node:path' +import type { + AtReference, + TranslationFile, + TranslationIndex, + TranslationLocation, +} from './types.js' + +/** + * Convert kebab-case to camelCase + * Examples: model-providers -> modelProviders, open-source-rank -> openSourceRank + */ +function kebabToCamel(str: string): string { + return str.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()) +} + +/** + * Get all translation keys from a nested object + */ +function getKeysFromObject(obj: unknown, prefix = ''): string[] { + const keys: string[] = [] + + if (typeof obj !== 'object' || obj === null) { + return keys + } + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + keys.push(...getKeysFromObject(value, fullKey)) + } else { + keys.push(fullKey) + } + } + + return keys +} + +/** + * Count @: references in a translation object + */ +function countAtReferences(obj: unknown, file: string, locale: string, prefix = ''): AtReference[] { + const refs: AtReference[] = [] + + if (typeof obj !== 'object' || obj === null) { + return refs + } + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'string' && value.startsWith('@:')) { + refs.push({ + sourceFile: file, + sourceKey: fullKey, + targetKey: value.slice(2), // Remove '@:' prefix + locale, + }) + } + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + refs.push(...countAtReferences(value, file, locale, fullKey)) + } + } + + return refs +} + +/** + * Load a single translation file + */ +function loadTranslationFile(filePath: string, locale: string): TranslationFile | null { + try { + const content = fs.readFileSync(filePath, 'utf-8') + const raw = JSON.parse(content) as unknown + const keys = new Set(getKeysFromObject(raw)) + + // Extract namespace from path + // translations/en/pages/models.json -> pages.models + // translations/en/pages/model-providers.json -> pages.modelProviders + const relativePath = path.relative('translations', filePath) + const parts = relativePath.split(path.sep) + // ['en', 'pages', 'models.json'] or ['en', 'pages', 'model-providers.json'] + const namespaceParts = parts.slice(1, -1) // ['pages'] + const fileName = parts[parts.length - 1] // 'models.json' or 'model-providers.json' + if (!fileName) return null + const fileNameWithoutExt = fileName.replace('.json', '') // 'models' or 'model-providers' + const fileNameCamelCase = kebabToCamel(fileNameWithoutExt) // 'models' or 'modelProviders' + + const namespace = [...namespaceParts, fileNameCamelCase].join('.') + + return { + locale, + namespace, + path: filePath, + keys, + raw, + } + } catch { + return null + } +} + +/** + * Load all translation files for a given locale + */ +function loadTranslationFilesForLocale(translationsDir: string, locale: string): TranslationFile[] { + const files: TranslationFile[] = [] + const localeDir = path.join(translationsDir, locale) + + if (!fs.existsSync(localeDir)) { + return files + } + + const traverse = (currentPath: string, namespacePrefix = ''): void => { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name) + + if (entry.isDirectory()) { + const newPrefix = namespacePrefix ? `${namespacePrefix}.${entry.name}` : entry.name + traverse(fullPath, newPrefix) + } else if (entry.isFile() && entry.name.endsWith('.json')) { + const translationFile = loadTranslationFile(fullPath, locale) + if (translationFile) { + files.push(translationFile) + } + } + } + } + + traverse(localeDir) + + return files +} + +/** + * Build index of all translation keys + */ +function buildKeyIndex(files: TranslationFile[]): Map { + const index = new Map() + + for (const file of files) { + for (const key of file.keys) { + if (!index.has(key)) { + index.set(key, []) + } + index.get(key)?.push({ + locale: file.locale, + namespace: file.namespace, + file: file.path, + }) + } + } + + return index +} + +/** + * Build index of @: references + */ +function buildAtReferenceIndex(files: TranslationFile[]): AtReference[] { + const refs: AtReference[] = [] + + for (const file of files) { + const fileRefs = countAtReferences(file.raw, file.path, file.locale) + refs.push(...fileRefs) + } + + return refs +} + +/** + * Load all translations and build index + */ +export function loadTranslations( + translationsDir: string, + locales: readonly string[] +): TranslationIndex { + const allFiles: TranslationFile[] = [] + + for (const locale of locales) { + const files = loadTranslationFilesForLocale(translationsDir, locale) + allFiles.push(...files) + } + + // Build by namespace index + const byNamespace = new Map() + for (const file of allFiles) { + if (!byNamespace.has(file.namespace)) { + byNamespace.set(file.namespace, []) + } + byNamespace.get(file.namespace)?.push(file) + } + + // Build by key index + const byKey = buildKeyIndex(allFiles) + + // Build @: references index + const atReferences = buildAtReferenceIndex(allFiles) + + return { + byNamespace, + byKey, + allFiles, + atReferences, + } +} + +/** + * Check if a namespace exists in translations + */ +export function namespaceExists(index: TranslationIndex, namespace: string): boolean { + return index.byNamespace.has(namespace) +} + +/** + * Check if a translation key exists + * Supports looking up keys from parent namespaces for nested translation file structures + * Example: namespace='components.controls.copyButton', key='copied' + * will check 'components.controls' for 'copyButton.copied' + */ +export function keyExists(index: TranslationIndex, key: string, namespace: string): boolean { + // First, check if the exact key exists in the given namespace + const namespaceFiles = index.byNamespace.get(namespace) + if (namespaceFiles && namespaceFiles.length > 0) { + // Check if any file in this namespace has the key + for (const file of namespaceFiles) { + if (file.keys.has(key)) { + return true + } + } + } + + // Check the global key index with full namespace path + if (index.byKey.has(`${namespace}.${key}`)) { + return true + } + + // If namespace has multiple parts (e.g., 'components.controls.copyButton'), + // try looking in parent namespace with compound key + // Example: 'components.controls.copyButton' + 'copied' -> 'components.controls' + 'copyButton.copied' + const parts = namespace.split('.') + if (parts.length > 2) { + // Get the last part of namespace (e.g., 'copyButton' from 'components.controls.copyButton') + const lastPart = parts[parts.length - 1] // 'copyButton' + const parentNamespace = parts.slice(0, -1).join('.') // 'components.controls' + const compoundKey = `${lastPart}.${key}` // 'copyButton.copied' + + // Check if parent namespace exists and has the compound key + const parentFiles = index.byNamespace.get(parentNamespace) + if (parentFiles && parentFiles.length > 0) { + for (const file of parentFiles) { + if (file.keys.has(compoundKey)) { + return true + } + } + } + + // Also check global index with parent namespace + if (index.byKey.has(`${parentNamespace}.${compoundKey}`)) { + return true + } + } + + return false +} + +/** + * Get all keys for a namespace + */ +export function getKeysForNamespace(index: TranslationIndex, namespace: string): Set { + const keys = new Set() + + const namespaceFiles = index.byNamespace.get(namespace) + if (namespaceFiles) { + for (const file of namespaceFiles) { + for (const key of file.keys) { + keys.add(key) + } + } + } + + return keys +} + +/** + * Get total count of @: references + */ +export function getAtReferencesStats(index: TranslationIndex): { + total: number + filesWithReferences: number +} { + const filesWithRefs = new Set() + + for (const ref of index.atReferences) { + filesWithRefs.add(ref.sourceFile) + } + + return { + total: index.atReferences.length, + filesWithReferences: filesWithRefs.size, + } +} diff --git a/scripts/validate/lib/types.ts b/scripts/validate/lib/types.ts new file mode 100644 index 00000000..897ae88f --- /dev/null +++ b/scripts/validate/lib/types.ts @@ -0,0 +1,233 @@ +/** + * Type definitions for i18n validation script + */ + +/** + * File type classification + */ +export enum FileType { + PAGE = 'page', + COMPONENT = 'component', + UNKNOWN = 'unknown', +} + +/** + * Violation types + */ +export enum ViolationType { + NAMESPACE = 'namespace', + MISSING_KEY = 'missing_key', +} + +/** + * Source location in a file + */ +export interface SourceLocation { + file: string + line: number + column: number +} + +/** + * A single translation function call + */ +export interface TranslationCall { + key: string + keyType: 'static' | 'dynamic' | 'partial' + location: SourceLocation +} + +/** + * Translation usage detected in code + */ +export interface TranslationUsage { + variableName: string + namespace: string + calls: TranslationCall[] + location: SourceLocation +} + +/** + * Parsed file result + */ +export interface ParsedFile { + path: string + fileType: FileType + usages: TranslationUsage[] + error?: string +} + +/** + * Translation file info + */ +export interface TranslationFile { + locale: string + namespace: string + path: string + keys: Set + raw: unknown +} + +/** + * Location of a translation key + */ +export interface TranslationLocation { + locale: string + namespace: string + file: string +} + +/** + * Index of all translation files + */ +export interface TranslationIndex { + byNamespace: Map + byKey: Map + allFiles: TranslationFile[] + atReferences: AtReference[] +} + +/** + * An @: reference in translation files + */ +export interface AtReference { + sourceFile: string + sourceKey: string + targetKey: string + locale: string +} + +/** + * Base violation + */ +export interface BaseViolation { + type: ViolationType + file: string + line: number + column: number +} + +/** + * Namespace violation + */ +export interface NamespaceViolation extends BaseViolation { + type: ViolationType.NAMESPACE + fileType: FileType + variableName: string + namespace: string +} + +/** + * Missing translation key violation + */ +export interface MissingKeyViolation extends BaseViolation { + type: ViolationType.MISSING_KEY + variableName: string + namespace: string + key: string +} + +/** + * Union type of all violations + */ +export type Violation = NamespaceViolation | MissingKeyViolation + +/** + * Warning type + */ +export enum WarningType { + DYNAMIC_KEY = 'dynamic_key', + SYNTAX_ERROR = 'syntax_error', + MISSING_TRANSLATION_FILE = 'missing_translation_file', +} + +/** + * Base warning + */ +export interface BaseWarning { + type: WarningType + file: string +} + +/** + * Dynamic key warning + */ +export interface DynamicKeyWarning extends BaseWarning { + type: WarningType.DYNAMIC_KEY + line: number + column: number + variableName: string + namespace: string + description: string +} + +/** + * Syntax error warning + */ +export interface SyntaxErrorWarning extends BaseWarning { + type: WarningType.SYNTAX_ERROR + error: string +} + +/** + * Missing translation file warning + */ +export interface MissingTranslationFileWarning extends BaseWarning { + type: WarningType.MISSING_TRANSLATION_FILE + namespace: string + locale: string +} + +/** + * Union type of all warnings + */ +export type Warning = DynamicKeyWarning | SyntaxErrorWarning | MissingTranslationFileWarning + +/** + * Validation summary + */ +export interface ValidationSummary { + filesScanned: number + pages: number + components: number + translationKeys: number + violations: { + total: number + namespaceViolations: number + missingKeys: number + } + atReferences: { + total: number + filesWithReferences: number + } + warnings: { + total: number + } + passed: boolean +} + +/** + * Complete validation report + */ +export interface ValidationReport { + summary: ValidationSummary + violations: Violation[] + warnings: Warning[] +} + +/** + * Reporter output format + */ +export enum OutputFormat { + CONSOLE = 'console', + JSON = 'json', +} + +/** + * Validation options + */ +export interface ValidationOptions { + locale?: string + format: OutputFormat + output?: string +} diff --git a/scripts/validate/lib/url-builder.mjs b/scripts/validate/lib/url-builder.ts similarity index 71% rename from scripts/validate/lib/url-builder.mjs rename to scripts/validate/lib/url-builder.ts index 02ffda81..00ece140 100644 --- a/scripts/validate/lib/url-builder.mjs +++ b/scripts/validate/lib/url-builder.ts @@ -2,24 +2,50 @@ * URL building utilities */ -import { BASE_URL, getLocalePrefix, LOCALES } from './config.mjs' +import { BASE_URL, LOCALES } from './config' import { getArticleSlugs, getDocSlugs, getDynamicRoutes, getSlugsFromManifests, getStaticRoutes, -} from './routes.mjs' +} from './routes' + +/** + * Get locale prefix for URL building + * Returns '' for 'en' (default locale) and '/{locale}' for others + */ +function getLocalePrefix(locale: string): string { + return locale === 'en' ? '' : `/${locale}` +} + +/** + * URL info interface + */ +export interface UrlInfo { + url: string + route: string + locale: string + type: 'static' | 'dynamic' + slug?: string +} + +/** + * Build URLs options + */ +export interface BuildUrlsOptions { + allLocales?: boolean + allSlugs?: boolean +} /** * Build URLs based on configuration - * @param {Object} options - Configuration options - * @param {boolean} options.allLocales - Whether to visit all locales or just English - * @param {boolean} options.allSlugs - Whether to visit all slugs or just one per route type - * @returns {Array} Array of URL info objects */ -export function buildUrls({ allLocales = false, allSlugs = false }) { - const urls = [] +export function buildUrls({ + allLocales = false, + allSlugs = false, +}: BuildUrlsOptions = {}): UrlInfo[] { + const urls: UrlInfo[] = [] const localesToUse = allLocales ? LOCALES : ['en'] // Static routes @@ -37,7 +63,7 @@ export function buildUrls({ allLocales = false, allSlugs = false }) { for (const { path: routePath, category } of dynamicRoutes) { const slugs = getSlugsFromManifests(category) - const slugsToVisit = allSlugs ? slugs : slugs.slice(0, 1) // Only first slug if not all slugs + const slugsToVisit = allSlugs ? slugs : slugs.slice(0, 1) for (const slug of slugsToVisit) { for (const locale of localesToUse) { diff --git a/scripts/validate/lib/visitor.mjs b/scripts/validate/lib/visitor.ts similarity index 53% rename from scripts/validate/lib/visitor.mjs rename to scripts/validate/lib/visitor.ts index 21c416fd..d8861713 100644 --- a/scripts/validate/lib/visitor.mjs +++ b/scripts/validate/lib/visitor.ts @@ -2,12 +2,22 @@ * URL visiting utilities */ -import { REQUEST_DELAY } from './config.mjs' +import { REQUEST_DELAY } from './config' +import type { UrlInfo } from './url-builder' + +/** + * Visit result interface + */ +export interface VisitResult extends UrlInfo { + status: number | null + success: boolean + error: string | null +} /** * Visit a URL with timeout and retries */ -export async function visitUrl(urlInfo, retries = 2) { +export async function visitUrl(urlInfo: UrlInfo, retries = 2): Promise { const REQUEST_TIMEOUT = 10000 for (let attempt = 0; attempt <= retries; attempt++) { @@ -39,11 +49,12 @@ export async function visitUrl(urlInfo, retries = 2) { continue } + const err = error as Error return { ...urlInfo, status: null, success: false, - error: error.name === 'AbortError' ? 'Request timeout' : error.message, + error: err.name === 'AbortError' ? 'Request timeout' : err.message, } } } @@ -57,38 +68,57 @@ export async function visitUrl(urlInfo, retries = 2) { } /** - * Visit all URLs sequentially (one by one) + * Visit all URLs with concurrency control */ -export async function visitAllUrls(urls) { - const results = [] +export async function visitAllUrlsConcurrent( + urls: UrlInfo[], + maxConcurrency = 1 +): Promise { + const results: VisitResult[] = [] + const queue = [...urls] + let processed = 0 const total = urls.length - for (let i = 0; i < urls.length; i++) { - const urlInfo = urls[i] - const result = await visitUrl(urlInfo) - results.push(result) + async function worker(): Promise { + while (queue.length > 0) { + const next = queue.shift() + if (!next) return - const processed = i + 1 + const result = await visitUrl(next) + results.push(result) + processed++ - if (result.success) { - console.log(`✓ [${processed}/${total}] ${result.url} (${result.status})`) - } else { - console.error(`✗ [${processed}/${total}] ${result.url} - ${result.error || result.status}`) - } + if (result.success) { + console.log(`✓ [${processed}/${total}] ${result.url} (${result.status})`) + } else { + console.error(`✗ [${processed}/${total}] ${result.url} - ${result.error || result.status}`) + } - // Add delay between requests to avoid overwhelming the server - if (i < urls.length - 1 && REQUEST_DELAY > 0) { - await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY)) + // Add delay between requests to avoid overwhelming the server + if (queue.length > 0 && REQUEST_DELAY > 0) { + await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY)) + } } } + const workers = Array.from({ length: Math.max(1, maxConcurrency) }, () => worker()) + await Promise.all(workers) + return results } +/** + * Visit all URLs sequentially (one by one) + * Alias for visitAllUrlsConcurrent with concurrency=1 + */ +export async function visitAllUrls(urls: UrlInfo[]): Promise { + return visitAllUrlsConcurrent(urls, 1) +} + /** * Print summary of visit results */ -export function printSummary(results, startTime) { +export function printSummary(results: VisitResult[], startTime: number): boolean { const endTime = Date.now() const successful = results.filter(r => r.success) const failed = results.filter(r => !r.success) diff --git a/scripts/validate/analyze-translation-duplicates.mjs b/scripts/validate/validate-i18n-duplicates.ts old mode 100755 new mode 100644 similarity index 72% rename from scripts/validate/analyze-translation-duplicates.mjs rename to scripts/validate/validate-i18n-duplicates.ts index 8f051132..1d89ee4b --- a/scripts/validate/analyze-translation-duplicates.mjs +++ b/scripts/validate/validate-i18n-duplicates.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env tsx /** * Script to analyze duplicate keys and values in translation files * Scans all JSON files in translations/en/ directory and reports: @@ -15,24 +15,33 @@ const __dirname = path.dirname(__filename) const ROOT_DIR = path.resolve(__dirname, '../..') const TRANSLATIONS_DIR = path.join(ROOT_DIR, 'translations', 'en') +interface FileInfo { + filePath: string + relativePath: string +} + +interface KeyLocation { + file: string + fullKey: string +} + +interface FlattenedObject { + [key: string]: string | number | boolean | null +} + /** * Flatten a nested object into dot-notation keys - * @param {object} obj - The object to flatten - * @param {string} prefix - The prefix for keys - * @returns {object} - Flattened object with dot-notation keys */ -function flattenObject(obj, prefix = '') { - const flattened = {} +function flattenObject(obj: Record, prefix = ''): FlattenedObject { + const flattened: FlattenedObject = {} for (const [key, value] of Object.entries(obj)) { const newKey = prefix ? `${prefix}.${key}` : key if (value !== null && typeof value === 'object' && !Array.isArray(value)) { - // Recursively flatten nested objects - Object.assign(flattened, flattenObject(value, newKey)) + Object.assign(flattened, flattenObject(value as Record, newKey)) } else { - // Store the value - flattened[newKey] = value + flattened[newKey] = value as string | number | boolean | null } } @@ -41,27 +50,23 @@ function flattenObject(obj, prefix = '') { /** * Read and parse a JSON file - * @param {string} filePath - Path to the JSON file - * @returns {object|null} - Parsed JSON object or null if error */ -function readJsonFile(filePath) { +function readJsonFile(filePath: string): Record | null { try { const content = fs.readFileSync(filePath, 'utf8') - return JSON.parse(content) + return JSON.parse(content) as Record } catch (error) { - console.warn(`Warning: Failed to read ${filePath}: ${error.message}`) + const err = error as Error + console.warn(`Warning: Failed to read ${filePath}: ${err.message}`) return null } } /** * Get all JSON files recursively from a directory - * @param {string} dir - Directory to search - * @param {string} baseDir - Base directory for relative paths - * @returns {Array<{filePath: string, relativePath: string}>} - Array of file info */ -function getAllJsonFiles(dir, baseDir = dir) { - const files = [] +function getAllJsonFiles(dir: string, baseDir = dir): FileInfo[] { + const files: FileInfo[] = [] if (!fs.existsSync(dir)) { return files @@ -74,12 +79,11 @@ function getAllJsonFiles(dir, baseDir = dir) { const relativePath = path.relative(baseDir, fullPath) if (entry.isDirectory()) { - // Recursively search subdirectories files.push(...getAllJsonFiles(fullPath, baseDir)) } else if (entry.isFile() && entry.name.endsWith('.json')) { files.push({ filePath: fullPath, - relativePath: relativePath, + relativePath, }) } } @@ -87,52 +91,55 @@ function getAllJsonFiles(dir, baseDir = dir) { return files } +/** + * Analysis results + */ +interface AnalysisResults { + totalFiles: number + totalKeys: number + duplicateKeys: Array<{ key: string; locations: KeyLocation[] }> + duplicateValues: Array<{ value: string; locations: KeyLocation[] }> +} + /** * Analyze all translation files - * @returns {object} - Analysis results */ -function analyzeTranslations() { +function analyzeTranslations(): AnalysisResults { const files = getAllJsonFiles(TRANSLATIONS_DIR) - const keyMap = new Map() // key -> Array of {file, fullKey} - const valueMap = new Map() // value -> Array of {file, fullKey} + const keyMap = new Map() + const valueMap = new Map() console.log(`Scanning ${files.length} translation files...\n`) - // Process each file for (const { filePath, relativePath } of files) { const data = readJsonFile(filePath) if (!data) continue const flattened = flattenObject(data) - // Track keys for (const [fullKey, value] of Object.entries(flattened)) { - // Track duplicate keys if (!keyMap.has(fullKey)) { keyMap.set(fullKey, []) } - keyMap.get(fullKey).push({ file: relativePath, fullKey }) + keyMap.get(fullKey)!.push({ file: relativePath, fullKey }) - // Track duplicate values (only for string values) if (typeof value === 'string') { if (!valueMap.has(value)) { valueMap.set(value, []) } - valueMap.get(value).push({ file: relativePath, fullKey }) + valueMap.get(value)!.push({ file: relativePath, fullKey }) } } } - // Find duplicate keys (keys that appear in multiple files) - const duplicateKeys = [] + const duplicateKeys: Array<{ key: string; locations: KeyLocation[] }> = [] for (const [key, locations] of keyMap.entries()) { if (locations.length > 1) { duplicateKeys.push({ key, locations }) } } - // Find duplicate values (values used by multiple keys) - const duplicateValues = [] + const duplicateValues: Array<{ value: string; locations: KeyLocation[] }> = [] for (const [value, locations] of valueMap.entries()) { if (locations.length > 1) { duplicateValues.push({ value, locations }) @@ -150,7 +157,7 @@ function analyzeTranslations() { /** * Generate and print report */ -function printReport(results) { +function printReport(results: AnalysisResults): void { const { totalFiles, totalKeys, duplicateKeys, duplicateValues } = results console.log('='.repeat(80)) @@ -158,7 +165,6 @@ function printReport(results) { console.log('='.repeat(80)) console.log() - // Summary console.log('SUMMARY') console.log('-'.repeat(80)) console.log(`Total files scanned: ${totalFiles}`) @@ -167,7 +173,6 @@ function printReport(results) { console.log(`Duplicate values (same value for different keys): ${duplicateValues.length}`) console.log() - // Duplicate keys report if (duplicateKeys.length > 0) { console.log('='.repeat(80)) console.log('DUPLICATE KEYS') @@ -175,7 +180,6 @@ function printReport(results) { console.log('The following keys appear in multiple files:') console.log() - // Sort by key name for better readability duplicateKeys.sort((a, b) => a.key.localeCompare(b.key)) for (const { key, locations } of duplicateKeys) { @@ -194,7 +198,6 @@ function printReport(results) { console.log() } - // Duplicate values report if (duplicateValues.length > 0) { console.log('='.repeat(80)) console.log('DUPLICATE VALUES') @@ -202,11 +205,9 @@ function printReport(results) { console.log('The following values are used by multiple keys:') console.log() - // Sort by number of occurrences (descending) for better readability duplicateValues.sort((a, b) => b.locations.length - a.locations.length) for (const { value, locations } of duplicateValues) { - // Truncate long values for display const displayValue = value.length > 60 ? `${value.substring(0, 60)}...` : value console.log(`Value: "${displayValue}"`) console.log(` Used by ${locations.length} key(s):`) @@ -231,7 +232,7 @@ function printReport(results) { /** * Main function */ -function main() { +function main(): void { if (!fs.existsSync(TRANSLATIONS_DIR)) { console.error(`Error: Translations directory not found: ${TRANSLATIONS_DIR}`) process.exit(1) @@ -240,11 +241,9 @@ function main() { const results = analyzeTranslations() printReport(results) - // Exit with non-zero code if duplicates found if (results.duplicateKeys.length > 0 || results.duplicateValues.length > 0) { process.exit(1) } } -// Run the script main() diff --git a/scripts/validate/validate-i18n-usage.ts b/scripts/validate/validate-i18n-usage.ts new file mode 100644 index 00000000..3e251aac --- /dev/null +++ b/scripts/validate/validate-i18n-usage.ts @@ -0,0 +1,185 @@ +#!/usr/bin/env -S npx tsx + +/** + * I18N Usage Validation Script + * + * This script validates that i18n translations are used correctly according to + * the project's i18n architecture rules: + * + * 1. Namespace rules: + * - Pages: should use tPage, tShared (or names ending with Page/Shared) + * - Components: should use tComponent, tShared (or names ending with Component/Shared) + * + * 2. Translation key existence: + * - All used translation keys must exist in the corresponding translation files + * + * 3. @: references: + * - Statistics are reported (not considered errors) + * + * Usage: + * npx tsx scripts/validate/validate-i18n-usage.ts + * npx tsx scripts/validate/validate-i18n-usage.ts --locale de + * npx tsx scripts/validate/validate-i18n-usage.ts --format json --output report.json + */ + +import path from 'node:path' +import { parseArgs } from 'node:util' +import { findSourceFiles, parseFile } from './lib/ast-parser.js' +import { LOCALES, ROOT_DIR } from './lib/config.js' +import { validateKeysAll } from './lib/key-validator.js' +import { validateNamespacesAll } from './lib/namespace-validator.js' +import { printReport as printConsoleReport } from './lib/reporters/console-reporter.js' +import { printJSON, writeJSON } from './lib/reporters/json-reporter.js' +import { getAtReferencesStats, loadTranslations } from './lib/translation-loader.js' +import type { + OutputFormat, + SyntaxErrorWarning, + ValidationOptions, + ValidationReport, + Warning, +} from './lib/types.js' +import { WarningType } from './lib/types.js' + +/** + * Parse command line arguments + */ +function parseOptions(): ValidationOptions { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + locale: { + type: 'string', + short: 'l', + }, + format: { + type: 'string', + short: 'f', + default: 'console', + }, + output: { + type: 'string', + short: 'o', + }, + }, + }) + + // Validate locale if provided + if (values.locale && !LOCALES.includes(values.locale)) { + console.error(`Error: Invalid locale "${values.locale}"`) + console.error(`Valid locales: ${LOCALES.join(', ')}`) + process.exit(1) + } + + // Validate format + const format = values.format?.toLowerCase() + if (format !== 'console' && format !== 'json') { + console.error(`Error: Invalid format "${format}"`) + console.error('Valid formats: console, json') + process.exit(1) + } + + return { + locale: values.locale, + format: (format ?? 'console') as OutputFormat, + output: values.output, + } +} + +/** + * Main validation function + */ +function main(): void { + const options = parseOptions() + + console.log(`Scanning project files in ${ROOT_DIR}...`) + console.log() + + // Determine which locales to validate + const localesToValidate = options.locale ? ([options.locale] as string[]) : (LOCALES as string[]) + + // Find all source files + const srcDir = path.join(ROOT_DIR, 'src') + const sourceFiles = findSourceFiles(srcDir) + + // Parse all files + const parsedFiles = sourceFiles.map(parseFile) + + // Count file types + const pagesCount = parsedFiles.filter(f => f.fileType === 'page').length + const componentsCount = parsedFiles.filter(f => f.fileType === 'component').length + + // Load translations + const translationsDir = path.join(ROOT_DIR, 'translations') + const translationIndex = loadTranslations(translationsDir, localesToValidate) + + // Count total translation keys + let totalKeys = 0 + for (const files of translationIndex.byNamespace.values()) { + for (const file of files) { + totalKeys += file.keys.size + } + } + + // Validate namespaces + const namespaceViolations = validateNamespacesAll(parsedFiles) + + // Validate translation keys + const { violations: missingKeyViolations, warnings: keyWarnings } = validateKeysAll( + parsedFiles, + translationIndex + ) + + // Collect all warnings (syntax errors from parsed files + key validation warnings) + const syntaxErrorWarnings: SyntaxErrorWarning[] = parsedFiles + .filter(f => f.error) + .map(f => ({ + type: WarningType.SYNTAX_ERROR, + file: f.path, + error: f.error!, + })) + + const allWarnings: Warning[] = [...syntaxErrorWarnings, ...keyWarnings] + + // Get @: references stats + const atRefsStats = getAtReferencesStats(translationIndex) + + // Build report + const report: ValidationReport = { + summary: { + filesScanned: parsedFiles.length, + pages: pagesCount, + components: componentsCount, + translationKeys: totalKeys, + violations: { + total: namespaceViolations.length + missingKeyViolations.length, + namespaceViolations: namespaceViolations.length, + missingKeys: missingKeyViolations.length, + }, + atReferences: atRefsStats, + warnings: { + total: allWarnings.length, + }, + passed: namespaceViolations.length === 0 && missingKeyViolations.length === 0, + }, + violations: [...namespaceViolations, ...missingKeyViolations], + warnings: allWarnings, + } + + // Output report + if (options.format === 'json') { + if (options.output) { + writeJSON(report, options.output) + console.log(`Report written to ${options.output}`) + } else { + printJSON(report) + } + } else { + printConsoleReport(report, ROOT_DIR) + } + + // Exit with appropriate code + process.exit(report.summary.passed ? 0 : 1) +} + +// Run main function +main() diff --git a/scripts/validate/visit-all-urls.mjs b/scripts/validate/visit-all-urls.mjs deleted file mode 100755 index 1ce3ecbd..00000000 --- a/scripts/validate/visit-all-urls.mjs +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env node -/** - * Script to visit all URLs on the website - * For dynamic routes with [slug], only visits once per unique slug - */ - -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const ROOT_DIR = path.resolve(__dirname, '../..') - -// Locales configuration -const LOCALES = ['en', 'de', 'es', 'fr', 'id', 'ja', 'ko', 'pt', 'ru', 'tr', 'zh-Hans', 'zh-Hant'] - -// Base URL - can be overridden via BASE_URL environment variable -const BASE_URL = process.env.BASE_URL || 'http://localhost:3000' - -/** - * Get locale prefix for URL - */ -function getLocalePrefix(locale) { - return locale === 'en' ? '' : `/${locale}` -} - -/** - * Read JSON file - */ -function readJsonFile(filePath) { - const content = fs.readFileSync(filePath, 'utf8') - return JSON.parse(content) -} - -/** - * Get all static routes - */ -function getStaticRoutes() { - return [ - '/', - '/ides', - '/clis', - '/extensions', - '/models', - '/model-providers', - '/vendors', - '/articles', - '/ai-coding-stack', - '/docs', - '/curated-collections', - '/manifesto', - '/ai-coding-landscape', - '/open-source-rank', - '/search', - '/clis/comparison', - '/extensions/comparison', - '/ides/comparison', - '/models/comparison', - ] -} - -/** - * Get all slugs from manifests directory - */ -function getSlugsFromManifests(category) { - const manifestsDir = path.join(ROOT_DIR, 'manifests', category) - if (!fs.existsSync(manifestsDir)) { - return [] - } - - const files = fs.readdirSync(manifestsDir).filter(f => f.endsWith('.json')) - const slugs = [] - - for (const file of files) { - const filePath = path.join(manifestsDir, file) - try { - const data = readJsonFile(filePath) - if (data && typeof data === 'object' && data.id) { - slugs.push(data.id) - } - } catch (error) { - console.warn(`Warning: Failed to read ${filePath}: ${error.message}`) - } - } - - return slugs -} - -/** - * Get article slugs from content directory - */ -function getArticleSlugs() { - try { - // Read from content directory - const articlesDir = path.join(ROOT_DIR, 'content/articles/en') - if (!fs.existsSync(articlesDir)) { - return [] - } - - const files = fs.readdirSync(articlesDir).filter(f => f.endsWith('.mdx')) - return files.map(file => file.replace('.mdx', '')) - } catch (error) { - console.warn(`Warning: Failed to get article slugs: ${error.message}`) - return [] - } -} - -/** - * Get doc slugs from content directory - */ -function getDocSlugs() { - try { - const docsDir = path.join(ROOT_DIR, 'content/docs/en') - if (!fs.existsSync(docsDir)) { - return [] - } - - const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.mdx')) - return files.map(file => file.replace('.mdx', '')) - } catch (error) { - console.warn(`Warning: Failed to get doc slugs: ${error.message}`) - return [] - } -} - -/** - * Build all URLs to visit - */ -function buildAllUrls() { - const urls = [] - const visitedSlugs = new Set() // Track visited slugs for dynamic routes - - // Static routes - visit for all locales - const staticRoutes = getStaticRoutes() - for (const route of staticRoutes) { - for (const locale of LOCALES) { - const localePrefix = getLocalePrefix(locale) - const url = `${BASE_URL}${localePrefix}${route}` - urls.push({ url, route, locale, type: 'static' }) - } - } - - // Dynamic routes with [slug] - const dynamicRoutes = [ - { path: '/ides', category: 'ides' }, - { path: '/clis', category: 'clis' }, - { path: '/extensions', category: 'extensions' }, - { path: '/models', category: 'models' }, - { path: '/model-providers', category: 'providers' }, - { path: '/vendors', category: 'vendors' }, - ] - - for (const { path: routePath, category } of dynamicRoutes) { - const slugs = getSlugsFromManifests(category) - for (const slug of slugs) { - // Only visit once per unique slug (use first locale) - if (!visitedSlugs.has(`${routePath}/${slug}`)) { - visitedSlugs.add(`${routePath}/${slug}`) - const url = `${BASE_URL}${routePath}/${slug}` - urls.push({ url, route: `${routePath}/${slug}`, locale: 'en', type: 'dynamic', slug }) - } - } - } - - // Articles - visit once per slug - const articleSlugs = getArticleSlugs() - for (const slug of articleSlugs) { - if (!visitedSlugs.has(`/articles/${slug}`)) { - visitedSlugs.add(`/articles/${slug}`) - const url = `${BASE_URL}/articles/${slug}` - urls.push({ url, route: `/articles/${slug}`, locale: 'en', type: 'dynamic', slug }) - } - } - - // Docs - visit once per slug - const docSlugs = getDocSlugs() - for (const slug of docSlugs) { - if (!visitedSlugs.has(`/docs/${slug}`)) { - visitedSlugs.add(`/docs/${slug}`) - const url = `${BASE_URL}/docs/${slug}` - urls.push({ url, route: `/docs/${slug}`, locale: 'en', type: 'dynamic', slug }) - } - } - - return urls -} - -/** - * Visit a URL with timeout and retries - */ -async function visitUrl(urlInfo, retries = 2) { - const REQUEST_TIMEOUT = 10000 - - for (let attempt = 0; attempt <= retries; attempt++) { - try { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT) - - const response = await fetch(urlInfo.url, { - method: 'HEAD', - signal: controller.signal, - redirect: 'follow', - }) - - clearTimeout(timeoutId) - - return { - ...urlInfo, - status: response.status, - success: response.ok || (response.status >= 300 && response.status < 400), - error: null, - } - } catch (error) { - if (attempt < retries) { - const delay = 2 ** attempt * 1000 - await new Promise(resolve => setTimeout(resolve, delay)) - continue - } - - return { - ...urlInfo, - status: null, - success: false, - error: error.name === 'AbortError' ? 'Request timeout' : error.message, - } - } - } - - return { - ...urlInfo, - status: null, - success: false, - error: 'Unknown error', - } -} - -/** - * Visit all URLs with concurrency control - */ -async function visitAllUrls(urls, maxConcurrency = 5) { - const results = [] - const queue = [...urls] - let processed = 0 - const total = urls.length - - async function worker() { - while (queue.length > 0) { - const next = queue.shift() - if (!next) return - - const result = await visitUrl(next) - results.push(result) - processed++ - - if (result.success) { - console.log(`✓ [${processed}/${total}] ${result.url} (${result.status})`) - } else { - console.error(`✗ [${processed}/${total}] ${result.url} - ${result.error || result.status}`) - } - } - } - - const workers = Array.from({ length: Math.max(1, maxConcurrency) }, () => worker()) - await Promise.all(workers) - - return results -} - -/** - * Main function - */ -async function main() { - console.log(`Building URL list...`) - const urls = buildAllUrls() - console.log(`Found ${urls.length} URLs to visit`) - console.log(`Base URL: ${BASE_URL}`) - console.log(`\nStarting to visit URLs...\n`) - - const startTime = Date.now() - const results = await visitAllUrls(urls, 5) - const endTime = Date.now() - - const successful = results.filter(r => r.success) - const failed = results.filter(r => !r.success) - - console.log(`\n${'='.repeat(60)}`) - console.log(`Summary:`) - console.log(` Total URLs: ${results.length}`) - console.log(` Successful: ${successful.length}`) - console.log(` Failed: ${failed.length}`) - console.log(` Time: ${((endTime - startTime) / 1000).toFixed(2)}s`) - - if (failed.length > 0) { - console.log(`\nFailed URLs:`) - for (const result of failed) { - console.log(` - ${result.url} (${result.error || result.status})`) - } - process.exit(1) - } - - console.log(`\nAll URLs visited successfully!`) -} - -// Run the script -main().catch(error => { - console.error('Fatal error:', error) - process.exit(1) -}) diff --git a/scripts/validate/visit-urls-all-locales-static-all-slugs.mjs b/scripts/validate/visit-urls-all-locales-static-all-slugs.mjs deleted file mode 100644 index 73501ac4..00000000 --- a/scripts/validate/visit-urls-all-locales-static-all-slugs.mjs +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -/** - * Script 4: Visit URLs - * - All locales - * - All static pages - * - All slug pages (all slugs for each route type) - */ - -import { BASE_URL } from './lib/config.mjs' -import { buildUrls } from './lib/url-builder.mjs' -import { printSummary, visitAllUrls } from './lib/visitor.mjs' - -async function main() { - console.log(`Building URL list...`) - console.log(`Configuration: All locales, all static pages, all slugs per route type`) - const urls = buildUrls({ allLocales: true, allSlugs: true }) - console.log(`Found ${urls.length} URLs to visit`) - console.log(`Base URL: ${BASE_URL}`) - console.log(`\nStarting to visit URLs...\n`) - - const startTime = Date.now() - const results = await visitAllUrls(urls) - const success = printSummary(results, startTime) - - process.exit(success ? 0 : 1) -} - -main().catch(error => { - console.error('Fatal error:', error) - process.exit(1) -}) diff --git a/scripts/validate/visit-urls-all-locales-static-one-slug.mjs b/scripts/validate/visit-urls-all-locales-static-one-slug.mjs deleted file mode 100644 index ae4f5fb3..00000000 --- a/scripts/validate/visit-urls-all-locales-static-one-slug.mjs +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -/** - * Script 3: Visit URLs - * - All locales - * - All static pages - * - All slug pages (only one slug per route type) - */ - -import { BASE_URL } from './lib/config.mjs' -import { buildUrls } from './lib/url-builder.mjs' -import { printSummary, visitAllUrls } from './lib/visitor.mjs' - -async function main() { - console.log(`Building URL list...`) - console.log(`Configuration: All locales, all static pages, one slug per route type`) - const urls = buildUrls({ allLocales: true, allSlugs: false }) - console.log(`Found ${urls.length} URLs to visit`) - console.log(`Base URL: ${BASE_URL}`) - console.log(`\nStarting to visit URLs...\n`) - - const startTime = Date.now() - const results = await visitAllUrls(urls) - const success = printSummary(results, startTime) - - process.exit(success ? 0 : 1) -} - -main().catch(error => { - console.error('Fatal error:', error) - process.exit(1) -}) diff --git a/scripts/validate/visit-urls-en-static-all-slugs.mjs b/scripts/validate/visit-urls-en-static-all-slugs.mjs deleted file mode 100644 index 7e3354d5..00000000 --- a/scripts/validate/visit-urls-en-static-all-slugs.mjs +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -/** - * Script 2: Visit URLs - * - Only English locale - * - All static pages - * - All slug pages (all slugs for each route type) - */ - -import { BASE_URL } from './lib/config.mjs' -import { buildUrls } from './lib/url-builder.mjs' -import { printSummary, visitAllUrls } from './lib/visitor.mjs' - -async function main() { - console.log(`Building URL list...`) - console.log(`Configuration: English only, all static pages, all slugs per route type`) - const urls = buildUrls({ allLocales: false, allSlugs: true }) - console.log(`Found ${urls.length} URLs to visit`) - console.log(`Base URL: ${BASE_URL}`) - console.log(`\nStarting to visit URLs...\n`) - - const startTime = Date.now() - const results = await visitAllUrls(urls) - const success = printSummary(results, startTime) - - process.exit(success ? 0 : 1) -} - -main().catch(error => { - console.error('Fatal error:', error) - process.exit(1) -}) diff --git a/scripts/validate/visit-urls-en-static-one-slug.mjs b/scripts/validate/visit-urls-en-static-one-slug.mjs deleted file mode 100644 index 2f8c1272..00000000 --- a/scripts/validate/visit-urls-en-static-one-slug.mjs +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -/** - * Script 1: Visit URLs - * - Only English locale - * - All static pages - * - All slug pages (only one slug per route type) - */ - -import { BASE_URL } from './lib/config.mjs' -import { buildUrls } from './lib/url-builder.mjs' -import { printSummary, visitAllUrls } from './lib/visitor.mjs' - -async function main() { - console.log(`Building URL list...`) - console.log(`Configuration: English only, all static pages, one slug per route type`) - const urls = buildUrls({ allLocales: false, allSlugs: false }) - console.log(`Found ${urls.length} URLs to visit`) - console.log(`Base URL: ${BASE_URL}`) - console.log(`\nStarting to visit URLs...\n`) - - const startTime = Date.now() - const results = await visitAllUrls(urls) - const success = printSummary(results, startTime) - - process.exit(success ? 0 : 1) -} - -main().catch(error => { - console.error('Fatal error:', error) - process.exit(1) -}) diff --git a/scripts/validate/visit-urls.ts b/scripts/validate/visit-urls.ts new file mode 100644 index 00000000..97f6c27e --- /dev/null +++ b/scripts/validate/visit-urls.ts @@ -0,0 +1,139 @@ +#!/usr/bin/env tsx +/** + * Unified URL validation script + * + * Usage: + * visit-urls.ts [options] + * + * Options: + * --locales Locale strategy: "en" (default) or "all" + * --slugs Slug strategy: "one" (default) or "all" + * --concurrency Max concurrent requests (default: 1) + * + * Examples: + * visit-urls.ts # English only, one slug per route + * visit-urls.ts --locales all # All locales, one slug per route + * visit-urls.ts --slugs all # English only, all slugs + * visit-urls.ts --locales all --slugs all # All locales, all slugs + * visit-urls.ts --concurrency 5 # With 5 concurrent requests + */ + +import { BASE_URL } from './lib/config' +import { buildUrls } from './lib/url-builder' +import { printSummary, visitAllUrlsConcurrent } from './lib/visitor' + +/** + * CLI options interface + */ +interface CliOptions { + locales: 'en' | 'all' + slugs: 'one' | 'all' + concurrency: number +} + +/** + * Parse command line arguments + */ +function parseArgs(args: string[]): CliOptions { + const options: CliOptions = { + locales: 'en', + slugs: 'one', + concurrency: 1, + } + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (!arg) continue + const nextArg = args[i + 1] + + switch (arg) { + case '--locales': + options.locales = nextArg === 'all' ? 'all' : 'en' + i++ + break + case '--slugs': + options.slugs = nextArg === 'all' ? 'all' : 'one' + i++ + break + case '--concurrency': + options.concurrency = Math.max(1, Number.parseInt(nextArg || '1', 10)) + i++ + break + case '--help': + // biome-ignore lint/suspicious/noFallthroughSwitchClause: process.exit terminates + case '-h': + printHelp() + process.exit(0) + default: + if (arg.startsWith('-')) { + console.error(`Unknown option: ${arg}`) + console.error('Run --help for usage information') + process.exit(1) + } + // If not a flag, silently ignore and continue + break + } + } + + return options +} + +/** + * Print help message + */ +function printHelp(): void { + console.log(` +URL Validation Script + +Usage: + visit-urls.ts [options] + +Options: + --locales Locale strategy: "en" (default) or "all" + --slugs Slug strategy: "one" (default) or "all" + --concurrency Max concurrent requests (default: 1) + --help, -h Show this help message + +Examples: + visit-urls.ts # English only, one slug per route + visit-urls.ts --locales all # All locales, one slug per route + visit-urls.ts --slugs all # English only, all slugs + visit-urls.ts --locales all --slugs all # All locales, all slugs + visit-urls.ts --concurrency 5 # With 5 concurrent requests + +Environment Variables: + BASE_URL Base URL to test (default: http://localhost:3000) + REQUEST_DELAY Delay between requests in ms (default: 100) +`) +} + +/** + * Main function + */ +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)) + + console.log(`Building URL list...`) + console.log( + `Configuration: ${options.locales === 'all' ? 'All locales' : 'English only'}, ${options.slugs === 'all' ? 'all slugs' : 'one slug per route type'}` + ) + const urls = buildUrls({ + allLocales: options.locales === 'all', + allSlugs: options.slugs === 'all', + }) + console.log(`Found ${urls.length} URLs to visit`) + console.log(`Base URL: ${BASE_URL}`) + console.log(`Concurrency: ${options.concurrency}`) + console.log(`\nStarting to visit URLs...\n`) + + const startTime = Date.now() + const results = await visitAllUrlsConcurrent(urls, options.concurrency) + const success = printSummary(results, startTime) + + process.exit(success ? 0 : 1) +} + +main().catch(error => { + console.error('Fatal error:', error) + process.exit(1) +}) diff --git a/src/app/[locale]/ai-coding-landscape/page.tsx b/src/app/[locale]/ai-coding-landscape/page.tsx index 53f429ab..f3f52d68 100644 --- a/src/app/[locale]/ai-coding-landscape/page.tsx +++ b/src/app/[locale]/ai-coding-landscape/page.tsx @@ -10,10 +10,10 @@ import { buildTitle, generateStaticPageMetadata } from '@/lib/metadata' export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params - const tPage = await getTranslations({ locale, namespace: 'pages.landscape' }) + const tShared = await getTranslations({ locale, namespace: 'shared' }) - const title = buildTitle({ title: tPage('title') }) - const description = tPage('description') + const title = buildTitle({ title: tShared('terms.landscapeTitle') }) + const description = tShared('terms.landscapeDescription') return generateStaticPageMetadata({ locale: locale as Locale, @@ -32,7 +32,7 @@ type Props = { export default async function Page({ params }: Props) { const { locale } = await params - const tPage = await getTranslations({ locale, namespace: 'pages.landscape' }) + const tShared = await getTranslations({ locale, namespace: 'shared' }) // Build vendor matrix data const matrixData = buildVendorMatrix() @@ -41,13 +41,16 @@ export default async function Page({ params }: Props) { <>
- + {/* Vendor Matrix */} {/* Back to Overview */} - +
diff --git a/src/app/[locale]/ai-coding-stack/page.tsx b/src/app/[locale]/ai-coding-stack/page.tsx index f84de971..715b5e2f 100644 --- a/src/app/[locale]/ai-coding-stack/page.tsx +++ b/src/app/[locale]/ai-coding-stack/page.tsx @@ -9,11 +9,10 @@ import type { LocalePageProps } from '@/types/locale' export async function generateMetadata({ params }: LocalePageProps) { const { locale } = await params - const tPage = await getTranslations({ locale, namespace: 'pages.stacksOverview' }) const tShared = await getTranslations({ locale, namespace: 'shared' }) const title = buildTitle({ title: tShared('terms.aiCodingStack') }) - const description = tPage('subtitle') + const description = tShared('terms.ecosystemSubtitle') return generateStaticPageMetadata({ locale: locale as Locale, @@ -36,7 +35,10 @@ export default async function AICodingStackPage({ params }: LocalePageProps) {
- + {/* Stacks Grid */}
diff --git a/src/app/[locale]/clis/comparison/page.client.tsx b/src/app/[locale]/clis/comparison/page.client.tsx index ef011068..d7d337e5 100644 --- a/src/app/[locale]/clis/comparison/page.client.tsx +++ b/src/app/[locale]/clis/comparison/page.client.tsx @@ -304,7 +304,7 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) { items={clis as unknown as Record[]} columns={columns} itemLinkPrefix={`/clis`} - nameColumnLabel={tPage('columns.name')} + nameColumnLabel={tShared('labels.name')} />
diff --git a/src/app/[locale]/extensions/comparison/page.client.tsx b/src/app/[locale]/extensions/comparison/page.client.tsx index 6a9bf6ac..da27e20f 100644 --- a/src/app/[locale]/extensions/comparison/page.client.tsx +++ b/src/app/[locale]/extensions/comparison/page.client.tsx @@ -290,7 +290,7 @@ export default function ExtensionComparisonPageClient({ locale: _locale }: Props items={extensions as unknown as Record[]} columns={columns} itemLinkPrefix={`/extensions`} - nameColumnLabel={tPage('columns.name')} + nameColumnLabel={tShared('labels.name')} /> diff --git a/src/app/[locale]/ides/comparison/page.client.tsx b/src/app/[locale]/ides/comparison/page.client.tsx index 5ac66402..2e64fa1f 100644 --- a/src/app/[locale]/ides/comparison/page.client.tsx +++ b/src/app/[locale]/ides/comparison/page.client.tsx @@ -304,7 +304,7 @@ export default function IDEComparisonPageClient({ locale: _locale }: Props) { items={ides as unknown as Record[]} columns={columns} itemLinkPrefix={`/ides`} - nameColumnLabel={tPage('columns.name')} + nameColumnLabel={tShared('labels.name')} /> diff --git a/src/app/[locale]/manifesto/page.tsx b/src/app/[locale]/manifesto/page.tsx index 9e75369d..8c73c3d9 100644 --- a/src/app/[locale]/manifesto/page.tsx +++ b/src/app/[locale]/manifesto/page.tsx @@ -52,7 +52,7 @@ export default async function ManifestoPage({ params }: LocalePageProps) { {tShared('terms.aiCodingStack')}

- {tPage('exploreStack.subtitle')} + {tShared('terms.ecosystemSubtitle')}

diff --git a/src/app/[locale]/model-providers/page.client.tsx b/src/app/[locale]/model-providers/page.client.tsx index 0132afd8..1e338dad 100644 --- a/src/app/[locale]/model-providers/page.client.tsx +++ b/src/app/[locale]/model-providers/page.client.tsx @@ -80,7 +80,7 @@ export default function ModelProvidersPageClient({ locale }: Props) { type="text" value={searchQuery} onChange={e => setSearchQuery(e.target.value)} - placeholder={tPage('search') || 'Search by name...'} + placeholder={tShared('labels.searchByName')} className="w-full max-w-2xs px-[var(--spacing-sm)] py-1 text-sm border border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-border-strong)] transition-colors" /> diff --git a/src/app/[locale]/models/[slug]/page.tsx b/src/app/[locale]/models/[slug]/page.tsx index ba435d74..014280d2 100644 --- a/src/app/[locale]/models/[slug]/page.tsx +++ b/src/app/[locale]/models/[slug]/page.tsx @@ -66,7 +66,6 @@ export default async function ModelPage({ notFound() } - const tPage = await getTranslations({ locale, namespace: 'pages.modelDetail' }) const tShared = await getTranslations({ locale, namespace: 'shared' }) // Generate JSON-LD schema @@ -90,7 +89,10 @@ export default async function ModelPage({ // Build additional info for ProductHero const additionalInfo = [ model.size && { label: tShared('terms.modelSize'), value: model.size }, - { label: tPage('contextWindow'), value: `${model.contextWindow.toLocaleString()} tokens` }, + { + label: tShared('terms.contextWindow'), + value: `${model.contextWindow.toLocaleString()} tokens`, + }, { label: tShared('terms.maxOutput'), value: `${model.maxOutput.toLocaleString()} tokens` }, ].filter(Boolean) as { label: string; value: string }[] diff --git a/src/app/[locale]/models/compare/[models]/page.tsx b/src/app/[locale]/models/compare/[models]/page.tsx index 7fa7279a..174a8622 100644 --- a/src/app/[locale]/models/compare/[models]/page.tsx +++ b/src/app/[locale]/models/compare/[models]/page.tsx @@ -124,8 +124,8 @@ export default async function ComparePage({ const { locale, models } = await params const modelIds = parseModelsParam(models) - const tPage = await getTranslations({ locale, namespace: 'pages.modelCompare' }) const tComparison = await getTranslations({ locale, namespace: 'pages.comparison' }) + const tShared = await getTranslations({ locale, namespace: 'shared' }) const comparisonModels: ManifestModel[] = [] let pageTitle = tComparison('models.title') @@ -169,7 +169,7 @@ export default async function ComparePage({ locale={locale as Locale} /> - +
) diff --git a/src/app/[locale]/models/compare/page.tsx b/src/app/[locale]/models/compare/page.tsx index 66f1394d..8986a7df 100644 --- a/src/app/[locale]/models/compare/page.tsx +++ b/src/app/[locale]/models/compare/page.tsx @@ -31,8 +31,8 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s export default async function ComparePage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params - const tPage = await getTranslations({ locale, namespace: 'pages.modelCompare' }) const tComparison = await getTranslations({ locale, namespace: 'pages.comparison' }) + const tShared = await getTranslations({ locale, namespace: 'shared' }) const groups = getComparisonGroups() const allModelsList = allModels.map(m => ({ id: m.id, name: m.name, vendor: m.vendor })) @@ -54,7 +54,7 @@ export default async function ComparePage({ params }: { params: Promise<{ locale locale={locale as Locale} /> - + ) diff --git a/src/app/[locale]/models/comparison/page.client.tsx b/src/app/[locale]/models/comparison/page.client.tsx index eff8d13d..bfd6e678 100644 --- a/src/app/[locale]/models/comparison/page.client.tsx +++ b/src/app/[locale]/models/comparison/page.client.tsx @@ -19,10 +19,6 @@ type Props = { locale: string } -function camelToKebab(str: string): string { - return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) -} - // Helper to wrap content in a span with alignment function wrapWithAlign( content: string | React.ReactNode, @@ -213,17 +209,17 @@ export default function ModelComparisonPageClient({ locale: _locale }: Props) { })), { key: 'inputModalities', - label: tPage('columns.inputModalities'), + label: tShared('capabilities.inputModalities'), render: createAbbreviationsRenderer(MODEL_INPUT_MODALITIES), }, { key: 'capabilities', - label: tPage('columns.capabilities'), + label: tShared('capabilities.capabilities'), render: createAbbreviationsRenderer(MODEL_CAPABILITIES), }, ...BENCHMARK_KEYS.map(key => ({ key, - label: tPage(`columns.${camelToKebab(key)}`), + label: tShared(`benchmarks.${key}`), render: (_: unknown, item: Record) => { const benchmarks = item.benchmarks as Record | undefined const value = benchmarks?.[key] @@ -264,7 +260,7 @@ export default function ModelComparisonPageClient({ locale: _locale }: Props) { items={models as unknown as Record[]} columns={columns} itemLinkPrefix={`/models`} - nameColumnLabel={tPage('columns.name')} + nameColumnLabel={tShared('labels.name')} /> diff --git a/src/app/[locale]/models/page.client.tsx b/src/app/[locale]/models/page.client.tsx index 47ca085a..2649835c 100644 --- a/src/app/[locale]/models/page.client.tsx +++ b/src/app/[locale]/models/page.client.tsx @@ -101,7 +101,7 @@ export default function ModelsPageClient({ locale }: Props) { type="text" value={searchQuery} onChange={e => setSearchQuery(e.target.value)} - placeholder={tPage('search') || 'Search by name...'} + placeholder={tShared('labels.searchByName')} className="w-full max-w-2xs px-[var(--spacing-sm)] py-1 text-sm border border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-border-strong)] transition-colors" /> diff --git a/src/app/[locale]/open-source-rank/page.client.tsx b/src/app/[locale]/open-source-rank/page.client.tsx index f6b04170..27bba89b 100644 --- a/src/app/[locale]/open-source-rank/page.client.tsx +++ b/src/app/[locale]/open-source-rank/page.client.tsx @@ -243,7 +243,7 @@ export function OpenSourceRankPage() { : 'border-[var(--color-border)] hover:bg-[var(--color-hover)]' }`} > - {tPage('productType.ide')} ( + {tShared('categories.plural.ides')} ( {openSourceProjects.filter(p => p.type === 'ide').length + proprietaryProjects.filter(p => p.type === 'ide').length} ) @@ -257,7 +257,7 @@ export function OpenSourceRankPage() { : 'border-[var(--color-border)] hover:bg-[var(--color-hover)]' }`} > - {tPage('productType.cli')} ( + {tShared('categories.plural.clis')} ( {openSourceProjects.filter(p => p.type === 'cli').length + proprietaryProjects.filter(p => p.type === 'cli').length} ) @@ -271,7 +271,7 @@ export function OpenSourceRankPage() { : 'border-[var(--color-border)] hover:bg-[var(--color-hover)]' }`} > - {tPage('productType.extension')} ( + {tShared('categories.plural.extensions')} ( {openSourceProjects.filter(p => p.type === 'extension').length + proprietaryProjects.filter(p => p.type === 'extension').length} ) @@ -291,7 +291,7 @@ export function OpenSourceRankPage() { {tPage('table.rank')} - {tPage('table.name')} + {tShared('labels.name')} {tShared('terms.type')} diff --git a/src/app/[locale]/vendors/page.client.tsx b/src/app/[locale]/vendors/page.client.tsx index 78982ffe..12015882 100644 --- a/src/app/[locale]/vendors/page.client.tsx +++ b/src/app/[locale]/vendors/page.client.tsx @@ -19,6 +19,7 @@ type Props = { export default function VendorsPageClient({ locale }: Props) { const tPage = useTranslations('pages.vendors') + const tShared = useTranslations('shared') const [searchQuery, setSearchQuery] = useState('') // Localize vendors @@ -74,7 +75,7 @@ export default function VendorsPageClient({ locale }: Props) { type="text" value={searchQuery} onChange={e => setSearchQuery(e.target.value)} - placeholder={tPage('search') || 'Search by name...'} + placeholder={tShared('labels.searchByName')} className="w-full max-w-2xs px-[var(--spacing-sm)] py-1 text-sm border border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-border-strong)] transition-colors" /> diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 43c8780e..48964c41 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -35,7 +35,7 @@ function FooterLinkList({ title, links }: FooterLinkListProps) { } export default function Footer() { - const tComponent = useTranslations('components.common.footer') + const tComponent = useTranslations('components.common') const tShared = useTranslations('shared') // Define link arrays (static hrefs, only labels depend on translations) @@ -81,8 +81,8 @@ export default function Footer() { {tShared('terms.aiCodingStack')}

- {tComponent('tagline')} - {tComponent('openSource')} + {tComponent('footer.tagline')} + {tComponent('footer.openSource')}

@@ -96,7 +96,7 @@ export default function Footer() {
- {tComponent('copyright')} + {tComponent('footer.copyright')}
) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b9baf9cc..3ec3c945 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -32,7 +32,7 @@ function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false) const [activeMegaMenu, setActiveMegaMenu] = useState<'aiCodingStack' | 'ranking' | null>(null) const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(false) - const tComponent = useTranslations('components.common.header') + const tComponent = useTranslations('components.common') const tShared = useTranslations('shared') // Menu items configuration - memoized to avoid recreation on each render @@ -46,10 +46,10 @@ function Header() { hasMegaMenu: true, megaMenuType: 'aiCodingStack', }, - { href: '/ai-coding-landscape', translationKey: 'landscape', namespace: 'header' }, + { href: '/ai-coding-landscape', translationKey: 'header.landscape', namespace: 'header' }, { href: '#', - translationKey: 'ranking', + translationKey: 'header.ranking', namespace: 'header', hasMegaMenu: true, megaMenuType: 'ranking', @@ -172,7 +172,7 @@ function Header() { // Memoized menu button label const menuButtonLabel = useMemo( - () => (isMenuOpen ? tComponent('closeMenu') : tComponent('openMenu')), + () => (isMenuOpen ? tComponent('header.closeMenu') : tComponent('header.openMenu')), [isMenuOpen, tComponent] ) @@ -222,7 +222,7 @@ function Header() { d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> - {tComponent('searchPlaceholder')} + {tShared('actions.searchPlaceholder')} K @@ -260,7 +260,7 @@ function Header() { type="button" onClick={handleMenuToggle} className="p-[var(--spacing-xs)] hover:bg-[var(--color-hover)] transition-colors" - aria-label={tComponent('toggleMenu')} + aria-label={tComponent('header.toggleMenu')} > { @@ -26,8 +26,10 @@ export default function CopyButton({ text }: { text: string }) { ? 'text-green-600' : 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]' }`} - title={copied ? tComponent('copied') : tComponent('copyToClipboard')} - aria-label={copied ? tComponent('copied') : tComponent('copyToClipboard')} + title={copied ? tComponent('copyButton.copied') : tComponent('copyButton.copyToClipboard')} + aria-label={ + copied ? tComponent('copyButton.copied') : tComponent('copyButton.copyToClipboard') + } > {copied ? ( - {tComponent('copied')} + {tComponent('copyButton.copied')} ) : ( @@ -56,7 +58,7 @@ export default function CopyButton({ text }: { text: string }) { strokeLinejoin="round" role="img" > - {tComponent('copyToClipboard')} + {tComponent('copyButton.copyToClipboard')} diff --git a/src/components/controls/FilterSortBar.tsx b/src/components/controls/FilterSortBar.tsx index 0a248045..53b1e59d 100644 --- a/src/components/controls/FilterSortBar.tsx +++ b/src/components/controls/FilterSortBar.tsx @@ -28,19 +28,20 @@ export default function FilterSortBar({ searchQuery = '', onSearchChange, }: FilterSortBarProps) { - const tComponent = useTranslations('components.controls.filterSortBar') + const tComponent = useTranslations('components.controls') const tShared = useTranslations('shared') const [isSortOpen, setIsSortOpen] = useState(false) const sortRef = useRef(null) const sortOptions = [ - { value: 'default', label: tComponent('sortDefault') }, - { value: 'name-asc', label: tComponent('sortNameAsc') }, - { value: 'name-desc', label: tComponent('sortNameDesc') }, + { value: 'default', label: tComponent('filterSortBar.sortDefault') }, + { value: 'name-asc', label: tComponent('filterSortBar.sortNameAsc') }, + { value: 'name-desc', label: tComponent('filterSortBar.sortNameDesc') }, ] const currentSortLabel = - sortOptions.find(opt => opt.value === sortOrder)?.label || tComponent('sortDefault') + sortOptions.find(opt => opt.value === sortOrder)?.label || + tComponent('filterSortBar.sortDefault') // Close dropdown when clicking outside useEffect(() => { @@ -99,7 +100,9 @@ export default function FilterSortBar({ {/* Sort Custom Dropdown */}
- {tComponent('sort')} + + {tComponent('filterSortBar.sort')} +
)}
diff --git a/src/components/controls/LanguageSwitcher.tsx b/src/components/controls/LanguageSwitcher.tsx index 119be2bc..769ddf25 100644 --- a/src/components/controls/LanguageSwitcher.tsx +++ b/src/components/controls/LanguageSwitcher.tsx @@ -58,7 +58,7 @@ export default function LanguageSwitcher() { const locale = useLocale() as Locale const router = useRouter() const pathname = usePathname() - const tComponent = useTranslations('components.common.footer') + const tComponent = useTranslations('components.common') const [isOpen, setIsOpen] = useState(false) const dropdownRef = useRef(null) @@ -120,8 +120,8 @@ export default function LanguageSwitcher() { type="button" onClick={() => setIsOpen(!isOpen)} className="footer-control-button" - title={tComponent('selectLanguage')} - aria-label={tComponent('selectLanguage')} + title={tComponent('footer.selectLanguage')} + aria-label={tComponent('footer.selectLanguage')} aria-expanded={isOpen} > diff --git a/src/components/controls/SearchDialog.tsx b/src/components/controls/SearchDialog.tsx index b96128ee..f464e745 100644 --- a/src/components/controls/SearchDialog.tsx +++ b/src/components/controls/SearchDialog.tsx @@ -17,7 +17,7 @@ export interface SearchDialogProps { export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogProps) { const tShared = useTranslations('shared') - const tComponent = useTranslations('components.controls.searchDialog') + const tComponent = useTranslations('components.controls') const router = useRouter() const [query, setQuery] = useState('') const [suggestions, setSuggestions] = useState([]) @@ -104,7 +104,7 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr @@ -118,7 +118,9 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr {/* Empty State */} {query.trim() === '' ? ( -
{tComponent('empty')}
+
+ {tComponent('searchDialog.empty')} +
) : (

- {tComponent('noResultsFor', { query })} + {tComponent('searchDialog.noResultsFor', { query })}

)} @@ -180,7 +182,7 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr className="px-4 py-3 cursor-pointer transition-colors border-t border-[var(--color-border)] data-[selected=true]:bg-[var(--color-hover)] aria-selected:bg-[var(--color-hover)] text-[var(--color-text-secondary)]" >
- {tComponent('viewAllResults', { query })} + {tComponent('searchDialog.viewAllResults', { query })}
@@ -199,13 +201,13 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr - {tComponent('navigate')} + {tComponent('searchDialog.navigate')}
- {tComponent('select')} + {tComponent('searchDialog.select')}
)} diff --git a/src/components/controls/SearchInput.tsx b/src/components/controls/SearchInput.tsx index 7cb388f3..e57e7f62 100644 --- a/src/components/controls/SearchInput.tsx +++ b/src/components/controls/SearchInput.tsx @@ -18,7 +18,6 @@ export default function SearchInput({ onSearch, }: SearchInputProps) { const tShared = useTranslations('shared') - const tComponent = useTranslations('components.controls.searchInput') const router = useRouter() const [query, setQuery] = useState(initialQuery) const [suggestions, setSuggestions] = useState([]) @@ -29,7 +28,7 @@ export default function SearchInput({ const debounceTimerRef = useRef(null) const hasUserInteracted = useRef(false) - const placeholderText = placeholder || tComponent('placeholder') + const placeholderText = placeholder || tShared('actions.searchPlaceholder') // Debounce search suggestions useEffect(() => { diff --git a/src/components/controls/ThemeSwitcher.tsx b/src/components/controls/ThemeSwitcher.tsx index 9ed7c780..2425dbe5 100644 --- a/src/components/controls/ThemeSwitcher.tsx +++ b/src/components/controls/ThemeSwitcher.tsx @@ -10,15 +10,15 @@ import { useTheme } from '../ThemeProvider' */ export default function ThemeSwitcher() { const { theme, toggleTheme } = useTheme() - const tComponent = useTranslations('components.common.footer') + const tComponent = useTranslations('components.common') return ( )} @@ -286,7 +294,7 @@ export default function VendorMatrix({ matrixData }: VendorMatrixProps) { {/* Sort Controls */}
- {tComponent('controls.sortByLabel')} + {tComponent('vendorMatrix.controls.sortByLabel')}