@iamnishankhan/opencode-kit-grok 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +46 -0
- package/kit/.opencode/AGENTS.md +392 -0
- package/kit/.opencode/README.md +71 -0
- package/kit/.opencode/USAGE.md +405 -0
- package/kit/.opencode/agents/backend-specialist.md +278 -0
- package/kit/.opencode/agents/code-archaeologist.md +114 -0
- package/kit/.opencode/agents/database-architect.md +233 -0
- package/kit/.opencode/agents/debugger.md +234 -0
- package/kit/.opencode/agents/devops-engineer.md +252 -0
- package/kit/.opencode/agents/documentation-writer.md +111 -0
- package/kit/.opencode/agents/explorer-agent.md +83 -0
- package/kit/.opencode/agents/frontend-specialist.md +606 -0
- package/kit/.opencode/agents/game-developer.md +169 -0
- package/kit/.opencode/agents/mobile-developer.md +385 -0
- package/kit/.opencode/agents/orchestrator.md +213 -0
- package/kit/.opencode/agents/penetration-tester.md +197 -0
- package/kit/.opencode/agents/performance-optimizer.md +194 -0
- package/kit/.opencode/agents/product-manager.md +120 -0
- package/kit/.opencode/agents/product-owner.md +103 -0
- package/kit/.opencode/agents/project-planner.md +418 -0
- package/kit/.opencode/agents/qa-automation-engineer.md +113 -0
- package/kit/.opencode/agents/security-auditor.md +179 -0
- package/kit/.opencode/agents/seo-specialist.md +119 -0
- package/kit/.opencode/agents/test-engineer.md +169 -0
- package/kit/.opencode/commands/brainstorm.md +113 -0
- package/kit/.opencode/commands/coordinate.md +72 -0
- package/kit/.opencode/commands/create.md +63 -0
- package/kit/.opencode/commands/debug.md +102 -0
- package/kit/.opencode/commands/deploy.md +175 -0
- package/kit/.opencode/commands/enhance.md +62 -0
- package/kit/.opencode/commands/orchestrate.md +241 -0
- package/kit/.opencode/commands/plan.md +89 -0
- package/kit/.opencode/commands/preview.md +80 -0
- package/kit/.opencode/commands/remember.md +67 -0
- package/kit/.opencode/commands/status.md +86 -0
- package/kit/.opencode/commands/test.md +143 -0
- package/kit/.opencode/commands/verify.md +71 -0
- package/kit/.opencode/memory/MEMORY.md +6 -0
- package/kit/.opencode/memory/feedback-history.md +9 -0
- package/kit/.opencode/memory/project-conventions.md +16 -0
- package/kit/.opencode/memory/tech-decisions.md +10 -0
- package/kit/.opencode/memory/user-preferences.md +9 -0
- package/kit/.opencode/opencode.json +15 -0
- package/kit/.opencode/scripts/README.md +95 -0
- package/kit/.opencode/scripts/auto_preview.py +149 -0
- package/kit/.opencode/scripts/checklist.py +75 -0
- package/kit/.opencode/scripts/component_registry.py +238 -0
- package/kit/.opencode/scripts/dependency_graph.py +73 -0
- package/kit/.opencode/scripts/generate_manifest.py +44 -0
- package/kit/.opencode/scripts/session_manager.py +120 -0
- package/kit/.opencode/scripts/tests/test_toolkit.py +238 -0
- package/kit/.opencode/scripts/validate_kit.py +397 -0
- package/kit/.opencode/scripts/validation_runner.py +216 -0
- package/kit/.opencode/scripts/verify_all.py +85 -0
- package/kit/.opencode/skills/api-patterns/SKILL.md +83 -0
- package/kit/.opencode/skills/api-patterns/api-style.md +42 -0
- package/kit/.opencode/skills/api-patterns/auth.md +24 -0
- package/kit/.opencode/skills/api-patterns/documentation.md +26 -0
- package/kit/.opencode/skills/api-patterns/graphql.md +41 -0
- package/kit/.opencode/skills/api-patterns/rate-limiting.md +31 -0
- package/kit/.opencode/skills/api-patterns/response.md +37 -0
- package/kit/.opencode/skills/api-patterns/rest.md +40 -0
- package/kit/.opencode/skills/api-patterns/scripts/api_validator.py +211 -0
- package/kit/.opencode/skills/api-patterns/security-testing.md +122 -0
- package/kit/.opencode/skills/api-patterns/trpc.md +41 -0
- package/kit/.opencode/skills/api-patterns/versioning.md +22 -0
- package/kit/.opencode/skills/app-builder/SKILL.md +78 -0
- package/kit/.opencode/skills/app-builder/agent-coordination.md +71 -0
- package/kit/.opencode/skills/app-builder/feature-building.md +53 -0
- package/kit/.opencode/skills/app-builder/project-detection.md +45 -0
- package/kit/.opencode/skills/app-builder/scaffolding.md +110 -0
- package/kit/.opencode/skills/app-builder/tech-stack.md +41 -0
- package/kit/.opencode/skills/app-builder/templates/SKILL.md +39 -0
- package/kit/.opencode/skills/app-builder/templates/astro-static/TEMPLATE.md +78 -0
- package/kit/.opencode/skills/app-builder/templates/chrome-extension/TEMPLATE.md +96 -0
- package/kit/.opencode/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
- package/kit/.opencode/skills/app-builder/templates/electron-desktop/TEMPLATE.md +97 -0
- package/kit/.opencode/skills/app-builder/templates/express-api/TEMPLATE.md +89 -0
- package/kit/.opencode/skills/app-builder/templates/flutter-app/TEMPLATE.md +93 -0
- package/kit/.opencode/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +97 -0
- package/kit/.opencode/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +126 -0
- package/kit/.opencode/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +125 -0
- package/kit/.opencode/skills/app-builder/templates/nextjs-static/TEMPLATE.md +174 -0
- package/kit/.opencode/skills/app-builder/templates/nuxt-app/TEMPLATE.md +127 -0
- package/kit/.opencode/skills/app-builder/templates/python-fastapi/TEMPLATE.md +94 -0
- package/kit/.opencode/skills/app-builder/templates/react-native-app/TEMPLATE.md +121 -0
- package/kit/.opencode/skills/architecture/SKILL.md +57 -0
- package/kit/.opencode/skills/architecture/context-discovery.md +43 -0
- package/kit/.opencode/skills/architecture/examples.md +94 -0
- package/kit/.opencode/skills/architecture/pattern-selection.md +68 -0
- package/kit/.opencode/skills/architecture/patterns-reference.md +50 -0
- package/kit/.opencode/skills/architecture/trade-off-analysis.md +77 -0
- package/kit/.opencode/skills/bash-linux/SKILL.md +201 -0
- package/kit/.opencode/skills/batch-operations/SKILL.md +105 -0
- package/kit/.opencode/skills/behavioral-modes/SKILL.md +253 -0
- package/kit/.opencode/skills/brainstorming/SKILL.md +178 -0
- package/kit/.opencode/skills/brainstorming/dynamic-questioning.md +350 -0
- package/kit/.opencode/skills/clean-code/SKILL.md +201 -0
- package/kit/.opencode/skills/code-review-checklist/SKILL.md +111 -0
- package/kit/.opencode/skills/code-review-graph/SKILL.md +305 -0
- package/kit/.opencode/skills/context-compression/SKILL.md +145 -0
- package/kit/.opencode/skills/coordinator-mode/SKILL.md +208 -0
- package/kit/.opencode/skills/database-design/SKILL.md +54 -0
- package/kit/.opencode/skills/database-design/database-selection.md +43 -0
- package/kit/.opencode/skills/database-design/indexing.md +39 -0
- package/kit/.opencode/skills/database-design/migrations.md +48 -0
- package/kit/.opencode/skills/database-design/optimization.md +36 -0
- package/kit/.opencode/skills/database-design/orm-selection.md +30 -0
- package/kit/.opencode/skills/database-design/schema-design.md +56 -0
- package/kit/.opencode/skills/database-design/scripts/schema_validator.py +172 -0
- package/kit/.opencode/skills/deployment-procedures/SKILL.md +243 -0
- package/kit/.opencode/skills/design-spec/SKILL.md +185 -0
- package/kit/.opencode/skills/design-spec/collection.md +106 -0
- package/kit/.opencode/skills/documentation-templates/SKILL.md +196 -0
- package/kit/.opencode/skills/frontend-architecture/SKILL.md +245 -0
- package/kit/.opencode/skills/frontend-design/SKILL.md +1222 -0
- package/kit/.opencode/skills/frontend-design/redesign.md +178 -0
- package/kit/.opencode/skills/frontend-design/scripts/accessibility_checker.py +145 -0
- package/kit/.opencode/skills/frontend-design/scripts/ux_audit.py +722 -0
- package/kit/.opencode/skills/frontend-design/style-brutalist.md +92 -0
- package/kit/.opencode/skills/frontend-design/style-minimalist.md +85 -0
- package/kit/.opencode/skills/game-development/2d-games/SKILL.md +118 -0
- package/kit/.opencode/skills/game-development/3d-games/SKILL.md +134 -0
- package/kit/.opencode/skills/game-development/SKILL.md +169 -0
- package/kit/.opencode/skills/game-development/game-art/SKILL.md +184 -0
- package/kit/.opencode/skills/game-development/game-audio/SKILL.md +189 -0
- package/kit/.opencode/skills/game-development/game-design/SKILL.md +128 -0
- package/kit/.opencode/skills/game-development/mobile-games/SKILL.md +107 -0
- package/kit/.opencode/skills/game-development/multiplayer/SKILL.md +131 -0
- package/kit/.opencode/skills/game-development/pc-games/SKILL.md +143 -0
- package/kit/.opencode/skills/game-development/vr-ar/SKILL.md +122 -0
- package/kit/.opencode/skills/game-development/web-games/SKILL.md +149 -0
- package/kit/.opencode/skills/geo-fundamentals/SKILL.md +158 -0
- package/kit/.opencode/skills/geo-fundamentals/scripts/geo_checker.py +273 -0
- package/kit/.opencode/skills/i18n-localization/SKILL.md +156 -0
- package/kit/.opencode/skills/i18n-localization/scripts/i18n_checker.py +223 -0
- package/kit/.opencode/skills/intelligent-routing/SKILL.md +337 -0
- package/kit/.opencode/skills/lint-and-validate/SKILL.md +47 -0
- package/kit/.opencode/skills/lint-and-validate/scripts/lint_runner.py +184 -0
- package/kit/.opencode/skills/lint-and-validate/scripts/type_coverage.py +245 -0
- package/kit/.opencode/skills/mcp-builder/SKILL.md +205 -0
- package/kit/.opencode/skills/memory-system/SKILL.md +181 -0
- package/kit/.opencode/skills/mobile-design/SKILL.md +396 -0
- package/kit/.opencode/skills/mobile-design/decision-trees.md +516 -0
- package/kit/.opencode/skills/mobile-design/mobile-backend.md +491 -0
- package/kit/.opencode/skills/mobile-design/mobile-color-system.md +420 -0
- package/kit/.opencode/skills/mobile-design/mobile-debugging.md +122 -0
- package/kit/.opencode/skills/mobile-design/mobile-design-thinking.md +357 -0
- package/kit/.opencode/skills/mobile-design/mobile-navigation.md +458 -0
- package/kit/.opencode/skills/mobile-design/mobile-performance.md +767 -0
- package/kit/.opencode/skills/mobile-design/mobile-testing.md +356 -0
- package/kit/.opencode/skills/mobile-design/mobile-typography.md +433 -0
- package/kit/.opencode/skills/mobile-design/platform-android.md +666 -0
- package/kit/.opencode/skills/mobile-design/platform-ios.md +561 -0
- package/kit/.opencode/skills/mobile-design/scripts/mobile_audit.py +670 -0
- package/kit/.opencode/skills/mobile-design/touch-psychology.md +537 -0
- package/kit/.opencode/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +351 -0
- package/kit/.opencode/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
- package/kit/.opencode/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
- package/kit/.opencode/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
- package/kit/.opencode/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
- package/kit/.opencode/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
- package/kit/.opencode/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
- package/kit/.opencode/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
- package/kit/.opencode/skills/nextjs-react-expert/9-cache-components.md +103 -0
- package/kit/.opencode/skills/nextjs-react-expert/SKILL.md +293 -0
- package/kit/.opencode/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
- package/kit/.opencode/skills/nextjs-react-expert/scripts/react_performance_checker.py +281 -0
- package/kit/.opencode/skills/nodejs-best-practices/SKILL.md +336 -0
- package/kit/.opencode/skills/parallel-agents/SKILL.md +192 -0
- package/kit/.opencode/skills/performance-profiling/SKILL.md +145 -0
- package/kit/.opencode/skills/performance-profiling/scripts/bundle_analyzer.py +120 -0
- package/kit/.opencode/skills/performance-profiling/scripts/lighthouse_audit.py +108 -0
- package/kit/.opencode/skills/plan-writing/SKILL.md +153 -0
- package/kit/.opencode/skills/powershell-windows/SKILL.md +169 -0
- package/kit/.opencode/skills/python-patterns/SKILL.md +443 -0
- package/kit/.opencode/skills/red-team-tactics/SKILL.md +201 -0
- package/kit/.opencode/skills/rust-pro/SKILL.md +177 -0
- package/kit/.opencode/skills/seo-fundamentals/SKILL.md +131 -0
- package/kit/.opencode/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
- package/kit/.opencode/skills/server-management/SKILL.md +163 -0
- package/kit/.opencode/skills/simplify-code/SKILL.md +131 -0
- package/kit/.opencode/skills/skillify/SKILL.md +115 -0
- package/kit/.opencode/skills/systematic-debugging/SKILL.md +111 -0
- package/kit/.opencode/skills/tailwind-patterns/SKILL.md +271 -0
- package/kit/.opencode/skills/tdd-workflow/SKILL.md +151 -0
- package/kit/.opencode/skills/testing-patterns/SKILL.md +180 -0
- package/kit/.opencode/skills/testing-patterns/scripts/test_runner.py +219 -0
- package/kit/.opencode/skills/verify-changes/SKILL.md +127 -0
- package/kit/.opencode/skills/vulnerability-scanner/SKILL.md +278 -0
- package/kit/.opencode/skills/vulnerability-scanner/checklists.md +124 -0
- package/kit/.opencode/skills/vulnerability-scanner/scripts/dependency_analyzer.py +144 -0
- package/kit/.opencode/skills/vulnerability-scanner/scripts/security_scan.py +410 -0
- package/kit/.opencode/skills/web-design-guidelines/SKILL.md +56 -0
- package/kit/.opencode/skills/webapp-testing/SKILL.md +189 -0
- package/kit/.opencode/skills/webapp-testing/scripts/playwright_runner.py +123 -0
- package/lib/init.js +61 -0
- package/package.json +21 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
GEO Checker - Generative Engine Optimization Audit
|
|
4
|
+
Checks PUBLIC WEB CONTENT for AI citation readiness.
|
|
5
|
+
|
|
6
|
+
PURPOSE:
|
|
7
|
+
- Analyze pages that will be INDEXED by AI engines (ChatGPT, Perplexity, etc.)
|
|
8
|
+
- Check for structured data, author info, dates, FAQ sections
|
|
9
|
+
- Help content rank in AI-generated answers
|
|
10
|
+
|
|
11
|
+
WHAT IT CHECKS:
|
|
12
|
+
- HTML files (actual web pages)
|
|
13
|
+
- JSX/TSX files (React page components)
|
|
14
|
+
- NOT markdown files (those are developer docs, not public content)
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python geo_checker.py <project_path>
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
import sys
|
|
21
|
+
import re
|
|
22
|
+
import json
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# Fix Windows console encoding
|
|
26
|
+
try:
|
|
27
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
28
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
29
|
+
except AttributeError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Directories that never represent public source content.
|
|
34
|
+
SKIP_DIRS = {
|
|
35
|
+
"node_modules", ".next", "dist", "build", ".git", ".github",
|
|
36
|
+
".agents", "__pycache__", ".vscode", ".idea", "coverage",
|
|
37
|
+
"test", "tests", "__tests__", "spec", "public", "components",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
PAGE_SUFFIXES = {".html", ".htm", ".jsx", ".tsx"}
|
|
41
|
+
ARTICLE_SEGMENTS = {"blog", "blogs", "article", "articles", "post", "posts", "news"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _lower_parts(file_path: Path) -> list[str]:
|
|
45
|
+
return [part.lower() for part in file_path.parts]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_page_file(file_path: Path) -> bool:
|
|
49
|
+
"""Return True only for route entry points, not layouts or components."""
|
|
50
|
+
if file_path.suffix.lower() not in PAGE_SUFFIXES:
|
|
51
|
+
return False
|
|
52
|
+
parts = _lower_parts(file_path)
|
|
53
|
+
if any(part in SKIP_DIRS for part in parts):
|
|
54
|
+
return False
|
|
55
|
+
if file_path.suffix.lower() in {".html", ".htm"}:
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
name = file_path.stem.lower()
|
|
59
|
+
if name.endswith((".test", ".spec")) or name.startswith(("test_", "spec_")):
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
# Next.js App Router: only page.tsx/page.jsx is a public route entry.
|
|
63
|
+
if "app" in parts:
|
|
64
|
+
return name == "page"
|
|
65
|
+
|
|
66
|
+
# Next.js Pages Router: each source file is a route except framework/API files.
|
|
67
|
+
if "pages" in parts:
|
|
68
|
+
pages_index = parts.index("pages")
|
|
69
|
+
route_parts = parts[pages_index + 1 :]
|
|
70
|
+
if "api" in route_parts or name.startswith("_"):
|
|
71
|
+
return False
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
# Other routers commonly use explicit page/index route entries.
|
|
75
|
+
return "routes" in parts and name in {"page", "index"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_web_pages(project_path: Path) -> list[Path]:
|
|
79
|
+
"""Find public-facing route source files deterministically."""
|
|
80
|
+
files = []
|
|
81
|
+
for suffix in PAGE_SUFFIXES:
|
|
82
|
+
for file_path in project_path.rglob(f"*{suffix}"):
|
|
83
|
+
if is_page_file(file_path):
|
|
84
|
+
files.append(file_path)
|
|
85
|
+
return sorted(set(files), key=lambda item: item.as_posix())[:100]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_route_content(file_path: Path) -> str:
|
|
89
|
+
"""Read a route and its directly imported local content modules.
|
|
90
|
+
|
|
91
|
+
Next.js route entries often delegate all visible markup to content.tsx or
|
|
92
|
+
localized MDX files. Static analysis must follow those imports or it scores
|
|
93
|
+
a thin wrapper instead of the page users and crawlers receive.
|
|
94
|
+
"""
|
|
95
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
96
|
+
import_paths = re.findall(r"(?:import|from)\s+(?:[^'\"]+?\s+from\s+)?['\"](\.{1,2}/[^'\"]+)['\"]", content)
|
|
97
|
+
extensions = ("", ".tsx", ".ts", ".jsx", ".js", ".mdx", ".md")
|
|
98
|
+
seen = {file_path.resolve()}
|
|
99
|
+
resolved_imports = []
|
|
100
|
+
for import_path in import_paths:
|
|
101
|
+
base = (file_path.parent / import_path).resolve()
|
|
102
|
+
candidates = [Path(f"{base}{extension}") for extension in extensions]
|
|
103
|
+
candidates.extend(base / f"index{extension}" for extension in extensions[1:])
|
|
104
|
+
for candidate in candidates:
|
|
105
|
+
if not candidate.is_file() or candidate.resolve() in seen:
|
|
106
|
+
continue
|
|
107
|
+
seen.add(candidate.resolve())
|
|
108
|
+
resolved_imports.append(candidate)
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
# A localized wrapper renders one locale at a time. Analyze the canonical
|
|
112
|
+
# English source rather than concatenating four mutually exclusive H1s.
|
|
113
|
+
canonical_locale = [item for item in resolved_imports if ".en." in item.name.lower()]
|
|
114
|
+
selected = canonical_locale or resolved_imports
|
|
115
|
+
chunks = [content]
|
|
116
|
+
chunks.extend(item.read_text(encoding="utf-8", errors="ignore") for item in selected)
|
|
117
|
+
return "\n".join(chunks)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def check_page(file_path: Path, project_path: Path | None = None) -> dict:
|
|
121
|
+
"""Score a route source file without requiring article metadata on product docs."""
|
|
122
|
+
try:
|
|
123
|
+
content = _read_route_content(file_path)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
return {"file": str(file_path), "passed": [], "issues": [f"Error: {exc}"], "score": 0}
|
|
126
|
+
|
|
127
|
+
issues = []
|
|
128
|
+
passed = []
|
|
129
|
+
optional_points = 0
|
|
130
|
+
lower_content = content.lower()
|
|
131
|
+
parts = set(_lower_parts(file_path))
|
|
132
|
+
is_article = bool(parts & ARTICLE_SEGMENTS)
|
|
133
|
+
is_root_landing = file_path.parent.name.lower() == "app"
|
|
134
|
+
|
|
135
|
+
heading_content = re.sub(r"```.*?```", "", content, flags=re.S)
|
|
136
|
+
heading_content = re.sub(r"`(?:\.|[^`])*`", "", heading_content, flags=re.S)
|
|
137
|
+
h1_count = len(re.findall(r"<h1[^>]*>", heading_content, re.I))
|
|
138
|
+
h1_count += len(re.findall(r"^#\s+\S", heading_content, re.M))
|
|
139
|
+
h2_count = len(re.findall(r"<h2[^>]*>", heading_content, re.I))
|
|
140
|
+
h2_count += len(re.findall(r"^##\s+\S", heading_content, re.M))
|
|
141
|
+
required_total = 1 if is_root_landing else 2
|
|
142
|
+
required_passed = 0
|
|
143
|
+
|
|
144
|
+
if h1_count == 1:
|
|
145
|
+
passed.append("Single H1 heading (clear topic)")
|
|
146
|
+
required_passed += 1
|
|
147
|
+
elif h1_count == 0:
|
|
148
|
+
issues.append("No H1 heading - page topic unclear")
|
|
149
|
+
else:
|
|
150
|
+
issues.append(f"Multiple H1 headings ({h1_count}) - confusing for AI")
|
|
151
|
+
|
|
152
|
+
if not is_root_landing:
|
|
153
|
+
repeated_heading = h2_count >= 1 and ".map(" in content
|
|
154
|
+
if h2_count >= 2 or repeated_heading:
|
|
155
|
+
detail = "dynamic repeated" if repeated_heading and h2_count < 2 else str(h2_count)
|
|
156
|
+
passed.append(f"{detail} H2 subheadings (good structure)")
|
|
157
|
+
required_passed += 1
|
|
158
|
+
else:
|
|
159
|
+
issues.append("Add at least two H2 subheadings for scannable content")
|
|
160
|
+
|
|
161
|
+
if "application/ld+json" in lower_content:
|
|
162
|
+
passed.append("JSON-LD structured data found")
|
|
163
|
+
optional_points += 10
|
|
164
|
+
if re.search(r'"@type"\s*:\s*"(Organization|Person|Brand|Article|FAQPage)"', content, re.I):
|
|
165
|
+
passed.append("Recognizable schema entity found")
|
|
166
|
+
optional_points += 5
|
|
167
|
+
|
|
168
|
+
author_patterns = ("author", "byline", "written-by", "contributor", 'rel="author"')
|
|
169
|
+
has_author = any(pattern in lower_content for pattern in author_patterns)
|
|
170
|
+
date_patterns = ("datepublished", "datemodified", "datetime=", "pubdate", "article:published")
|
|
171
|
+
has_date = any(pattern in lower_content for pattern in date_patterns)
|
|
172
|
+
if is_article:
|
|
173
|
+
required_total += 2
|
|
174
|
+
if has_author:
|
|
175
|
+
passed.append("Author attribution found")
|
|
176
|
+
required_passed += 1
|
|
177
|
+
else:
|
|
178
|
+
issues.append("Article has no author attribution")
|
|
179
|
+
if has_date:
|
|
180
|
+
passed.append("Publication date found")
|
|
181
|
+
required_passed += 1
|
|
182
|
+
else:
|
|
183
|
+
issues.append("Article has no publication date")
|
|
184
|
+
else:
|
|
185
|
+
optional_points += 3 if has_author else 0
|
|
186
|
+
optional_points += 3 if has_date else 0
|
|
187
|
+
|
|
188
|
+
optional_checks = (
|
|
189
|
+
(r"<details|faq|frequently.?asked|\"FAQPage\"", "FAQ section detected", 5),
|
|
190
|
+
(r"<(ul|ol)[^>]*>", "Structured list content found", 5),
|
|
191
|
+
(r"<table[^>]*>", "Comparison table found", 5),
|
|
192
|
+
(r"\d+%|according to|data\s+(shows|reveals)|\d+x\s+(faster|better|more)", "Data-backed claims found", 5),
|
|
193
|
+
(r"is defined as|refers to|means that|in short,|simply put,|<dfn", "Direct-answer phrasing found", 5),
|
|
194
|
+
)
|
|
195
|
+
for pattern, label, points in optional_checks:
|
|
196
|
+
if re.search(pattern, content, re.I):
|
|
197
|
+
passed.append(label)
|
|
198
|
+
optional_points += points
|
|
199
|
+
|
|
200
|
+
base_score = (required_passed / required_total * 70) if required_total else 70
|
|
201
|
+
score = min(100, round(base_score + min(optional_points, 30)))
|
|
202
|
+
display = file_path.relative_to(project_path).as_posix() if project_path else file_path.as_posix()
|
|
203
|
+
return {"file": display, "passed": passed, "issues": issues, "score": score}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main():
|
|
207
|
+
target = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
208
|
+
target_path = Path(target).resolve()
|
|
209
|
+
|
|
210
|
+
print("\n" + "=" * 60)
|
|
211
|
+
print(" GEO CHECKER - AI Citation Readiness Audit")
|
|
212
|
+
print("=" * 60)
|
|
213
|
+
print(f"Project: {target_path}")
|
|
214
|
+
print("-" * 60)
|
|
215
|
+
|
|
216
|
+
# Find web pages only
|
|
217
|
+
pages = find_web_pages(target_path)
|
|
218
|
+
|
|
219
|
+
if not pages:
|
|
220
|
+
print("\n[!] No public web pages found.")
|
|
221
|
+
print(" Looking for: HTML, JSX, TSX files in pages/app directories")
|
|
222
|
+
print(" Skipping: docs, tests, config files, node_modules")
|
|
223
|
+
output = {"script": "geo_checker", "pages_found": 0, "passed": True}
|
|
224
|
+
print("\n" + json.dumps(output, indent=2))
|
|
225
|
+
sys.exit(0)
|
|
226
|
+
|
|
227
|
+
print(f"Found {len(pages)} public pages to analyze\n")
|
|
228
|
+
|
|
229
|
+
# Check each page
|
|
230
|
+
results = []
|
|
231
|
+
for page in pages:
|
|
232
|
+
result = check_page(page, target_path)
|
|
233
|
+
results.append(result)
|
|
234
|
+
|
|
235
|
+
# Print results
|
|
236
|
+
for result in results:
|
|
237
|
+
status = "[OK]" if result['score'] >= 60 else "[!]"
|
|
238
|
+
print(f"{status} {result['file']}: {result['score']}%")
|
|
239
|
+
if result['issues'] and result['score'] < 60:
|
|
240
|
+
for issue in result['issues'][:2]: # Show max 2 issues
|
|
241
|
+
print(f" - {issue}")
|
|
242
|
+
|
|
243
|
+
# Average score
|
|
244
|
+
avg_score = sum(r['score'] for r in results) / len(results) if results else 0
|
|
245
|
+
|
|
246
|
+
print("\n" + "=" * 60)
|
|
247
|
+
print(f"AVERAGE GEO SCORE: {avg_score:.0f}%")
|
|
248
|
+
print("=" * 60)
|
|
249
|
+
|
|
250
|
+
if avg_score >= 80:
|
|
251
|
+
print("[OK] Excellent - Content well-optimized for AI citations")
|
|
252
|
+
elif avg_score >= 60:
|
|
253
|
+
print("[OK] Good - Some improvements recommended")
|
|
254
|
+
elif avg_score >= 40:
|
|
255
|
+
print("[!] Needs work - Add structured elements")
|
|
256
|
+
else:
|
|
257
|
+
print("[X] Poor - Content needs GEO optimization")
|
|
258
|
+
|
|
259
|
+
# JSON output
|
|
260
|
+
output = {
|
|
261
|
+
"script": "geo_checker",
|
|
262
|
+
"project": str(target_path),
|
|
263
|
+
"pages_checked": len(results),
|
|
264
|
+
"average_score": round(avg_score),
|
|
265
|
+
"passed": avg_score >= 60
|
|
266
|
+
}
|
|
267
|
+
print("\n" + json.dumps(output, indent=2))
|
|
268
|
+
|
|
269
|
+
sys.exit(0 if avg_score >= 60 else 1)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
main()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n-localization
|
|
3
|
+
description: Internationalization and localization patterns. Detecting hardcoded strings, managing translations, locale files, RTL support.
|
|
4
|
+
compatibility: opencode
|
|
5
|
+
metadata:
|
|
6
|
+
when_to_use: "\"When internationalizing an app, managing translations, detecting hardcoded strings, or adding RTL support.\""
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
---
|
|
9
|
+
# i18n & Localization
|
|
10
|
+
|
|
11
|
+
> Internationalization (i18n) and Localization (L10n) best practices.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Core Concepts
|
|
16
|
+
|
|
17
|
+
| Term | Meaning |
|
|
18
|
+
|------|---------|
|
|
19
|
+
| **i18n** | Internationalization - making app translatable |
|
|
20
|
+
| **L10n** | Localization - actual translations |
|
|
21
|
+
| **Locale** | Language + Region (en-US, tr-TR) |
|
|
22
|
+
| **RTL** | Right-to-left languages (Arabic, Hebrew) |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. When to Use i18n
|
|
27
|
+
|
|
28
|
+
| Project Type | i18n Needed? |
|
|
29
|
+
|--------------|--------------|
|
|
30
|
+
| Public web app | ✅ Yes |
|
|
31
|
+
| SaaS product | ✅ Yes |
|
|
32
|
+
| Internal tool | ⚠️ Maybe |
|
|
33
|
+
| Single-region app | ⚠️ Consider future |
|
|
34
|
+
| Personal project | ❌ Optional |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 3. Implementation Patterns
|
|
39
|
+
|
|
40
|
+
### React (react-i18next)
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { useTranslation } from 'react-i18next';
|
|
44
|
+
|
|
45
|
+
function Welcome() {
|
|
46
|
+
const { t } = useTranslation();
|
|
47
|
+
return <h1>{t('welcome.title')}</h1>;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Next.js (next-intl)
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { useTranslations } from 'next-intl';
|
|
55
|
+
|
|
56
|
+
export default function Page() {
|
|
57
|
+
const t = useTranslations('Home');
|
|
58
|
+
return <h1>{t('title')}</h1>;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Python (gettext)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from gettext import gettext as _
|
|
66
|
+
|
|
67
|
+
print(_("Welcome to our app"))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 4. File Structure
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
locales/
|
|
76
|
+
├── en/
|
|
77
|
+
│ ├── common.json
|
|
78
|
+
│ ├── auth.json
|
|
79
|
+
│ └── errors.json
|
|
80
|
+
├── tr/
|
|
81
|
+
│ ├── common.json
|
|
82
|
+
│ ├── auth.json
|
|
83
|
+
│ └── errors.json
|
|
84
|
+
└── ar/ # RTL
|
|
85
|
+
└── ...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 5. Best Practices
|
|
91
|
+
|
|
92
|
+
### DO ✅
|
|
93
|
+
|
|
94
|
+
- Use translation keys, not raw text
|
|
95
|
+
- Namespace translations by feature
|
|
96
|
+
- Support pluralization
|
|
97
|
+
- Handle date/number formats per locale
|
|
98
|
+
- Plan for RTL from the start
|
|
99
|
+
- Use ICU message format for complex strings
|
|
100
|
+
|
|
101
|
+
### DON'T ❌
|
|
102
|
+
|
|
103
|
+
- Hardcode strings in components
|
|
104
|
+
- Concatenate translated strings
|
|
105
|
+
- Assume text length (German is 30% longer)
|
|
106
|
+
- Forget about RTL layout
|
|
107
|
+
- Mix languages in same file
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 6. Common Issues
|
|
112
|
+
|
|
113
|
+
| Issue | Solution |
|
|
114
|
+
|-------|----------|
|
|
115
|
+
| Missing translation | Fallback to default language |
|
|
116
|
+
| Hardcoded strings | Use linter/checker script |
|
|
117
|
+
| Date format | Use Intl.DateTimeFormat |
|
|
118
|
+
| Number format | Use Intl.NumberFormat |
|
|
119
|
+
| Pluralization | Use ICU message format |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 7. RTL Support
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
/* CSS Logical Properties */
|
|
127
|
+
.container {
|
|
128
|
+
margin-inline-start: 1rem; /* Not margin-left */
|
|
129
|
+
padding-inline-end: 1rem; /* Not padding-right */
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
[dir="rtl"] .icon {
|
|
133
|
+
transform: scaleX(-1);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 8. Checklist
|
|
140
|
+
|
|
141
|
+
Before shipping:
|
|
142
|
+
|
|
143
|
+
- [ ] All user-facing strings use translation keys
|
|
144
|
+
- [ ] Locale files exist for all supported languages
|
|
145
|
+
- [ ] Date/number formatting uses Intl API
|
|
146
|
+
- [ ] RTL layout tested (if applicable)
|
|
147
|
+
- [ ] Fallback language configured
|
|
148
|
+
- [ ] No hardcoded strings in components
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Script
|
|
153
|
+
|
|
154
|
+
| Script | Purpose | Command |
|
|
155
|
+
|--------|---------|---------|
|
|
156
|
+
| `.opencode/scripts/i18n_checker.py` | Detect hardcoded strings & missing translations | `python .opencode/scripts/i18n_checker.py <project_path>` |
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Audit locale completeness and likely user-facing hard-coded strings."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
SKIP_DIRS = {"node_modules", ".git", ".agents", ".agent", "dist", "build", "__pycache__", ".venv", "venv", ".next", "tests", "test", "spec", "specs"}
|
|
13
|
+
CODE_TYPES = {".tsx": "jsx", ".jsx": "jsx", ".ts": "jsx", ".js": "jsx", ".vue": "vue", ".py": "python"}
|
|
14
|
+
I18N_PATTERNS = (
|
|
15
|
+
re.compile(r"\buseTranslation\s*\("),
|
|
16
|
+
re.compile(r"\buseTranslations\s*\("),
|
|
17
|
+
re.compile(r"(?<![\w$])t\s*\(\s*[\"']"),
|
|
18
|
+
re.compile(r"\$t\s*\("),
|
|
19
|
+
re.compile(r"\bgettext\s*\("),
|
|
20
|
+
re.compile(r"(?<!\w)_\s*\(\s*[\"']"),
|
|
21
|
+
re.compile(r"\bFormattedMessage\b"),
|
|
22
|
+
re.compile(r"\bi18n\."),
|
|
23
|
+
)
|
|
24
|
+
HARDCODED_PATTERNS = {
|
|
25
|
+
"jsx": (
|
|
26
|
+
re.compile(r">\s*([A-Z][A-Za-z][A-Za-z\s!?.,'-]{2,80})\s*</"),
|
|
27
|
+
re.compile(r"(?:title|placeholder|label|alt|aria-label)=[\"']([A-Z][A-Za-z\s!?.,'-]{2,80})[\"']"),
|
|
28
|
+
),
|
|
29
|
+
"vue": (
|
|
30
|
+
re.compile(r">\s*([A-Z][A-Za-z][A-Za-z\s!?.,'-]{2,80})\s*</"),
|
|
31
|
+
re.compile(r"(?:title|placeholder|label|alt|aria-label)=[\"']([A-Z][A-Za-z\s!?.,'-]{2,80})[\"']"),
|
|
32
|
+
),
|
|
33
|
+
"python": (
|
|
34
|
+
re.compile(r"\bflash\s*\(\s*[\"']([A-Z][^\"']{4,100})[\"']"),
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_skipped(path: Path, root: Path) -> bool:
|
|
40
|
+
try:
|
|
41
|
+
rel = path.relative_to(root)
|
|
42
|
+
except ValueError:
|
|
43
|
+
rel = path
|
|
44
|
+
return any(part in SKIP_DIRS for part in rel.parts)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_locale_files(root: Path) -> list[Path]:
|
|
48
|
+
patterns = (
|
|
49
|
+
"**/locales/**/*.json", "**/translations/**/*.json", "**/lang/**/*.json",
|
|
50
|
+
"**/i18n/**/*.json", "**/messages/*.json", "**/*.po",
|
|
51
|
+
)
|
|
52
|
+
found: set[Path] = set()
|
|
53
|
+
for pattern in patterns:
|
|
54
|
+
for path in root.glob(pattern):
|
|
55
|
+
if path.is_file() and not is_skipped(path, root):
|
|
56
|
+
found.add(path)
|
|
57
|
+
return sorted(found)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def flatten_keys(value: Any, prefix: str = "") -> set[str]:
|
|
61
|
+
keys: set[str] = set()
|
|
62
|
+
if not isinstance(value, dict):
|
|
63
|
+
return keys
|
|
64
|
+
for key, child in value.items():
|
|
65
|
+
name = f"{prefix}.{key}" if prefix else str(key)
|
|
66
|
+
if isinstance(child, dict):
|
|
67
|
+
keys.update(flatten_keys(child, name))
|
|
68
|
+
else:
|
|
69
|
+
keys.add(name)
|
|
70
|
+
return keys
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_locale_completeness(locale_files: list[Path], root: Path) -> dict[str, Any]:
|
|
74
|
+
if not locale_files:
|
|
75
|
+
return {"status": "not_applicable", "files": 0, "languages": [], "issues": [], "notes": ["No locale files found."]}
|
|
76
|
+
|
|
77
|
+
locales: dict[str, dict[str, set[str]]] = {}
|
|
78
|
+
issues: list[dict[str, str]] = []
|
|
79
|
+
for path in locale_files:
|
|
80
|
+
if path.suffix == ".po":
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
content = json.loads(path.read_text("utf-8"))
|
|
84
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
85
|
+
issues.append({"severity": "high", "file": path.relative_to(root).as_posix(), "issue": f"Invalid locale JSON: {exc}"})
|
|
86
|
+
continue
|
|
87
|
+
language = path.parent.name
|
|
88
|
+
locales.setdefault(language, {})[path.stem] = flatten_keys(content)
|
|
89
|
+
|
|
90
|
+
languages = sorted(locales)
|
|
91
|
+
if len(languages) >= 2:
|
|
92
|
+
base = languages[0]
|
|
93
|
+
namespaces = set().union(*(set(locales[lang]) for lang in languages))
|
|
94
|
+
for namespace in sorted(namespaces):
|
|
95
|
+
base_keys = locales[base].get(namespace, set())
|
|
96
|
+
for language in languages[1:]:
|
|
97
|
+
keys = locales[language].get(namespace, set())
|
|
98
|
+
missing = sorted(base_keys - keys)
|
|
99
|
+
extra = sorted(keys - base_keys)
|
|
100
|
+
if missing:
|
|
101
|
+
issues.append({"severity": "high", "file": f"{language}/{namespace}", "issue": f"Missing {len(missing)} key(s): {', '.join(missing[:8])}"})
|
|
102
|
+
if extra:
|
|
103
|
+
issues.append({"severity": "medium", "file": f"{language}/{namespace}", "issue": f"Has {len(extra)} extra key(s)"})
|
|
104
|
+
|
|
105
|
+
return {"status": "checked", "files": len(locale_files), "languages": languages, "issues": issues, "notes": []}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def iter_code_files(root: Path):
|
|
109
|
+
for path in root.rglob("*"):
|
|
110
|
+
if path.is_file() and path.suffix.lower() in CODE_TYPES and not is_skipped(path, root):
|
|
111
|
+
yield path
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def project_uses_i18n(root: Path, code_files: list[Path], locale_files: list[Path]) -> bool:
|
|
115
|
+
if locale_files:
|
|
116
|
+
return True
|
|
117
|
+
package = root / "package.json"
|
|
118
|
+
if package.is_file():
|
|
119
|
+
try:
|
|
120
|
+
text = package.read_text("utf-8").lower()
|
|
121
|
+
if any(name in text for name in ("i18next", "next-intl", "react-intl", "vue-i18n", "@lingui")):
|
|
122
|
+
return True
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
for path in code_files[:200]:
|
|
126
|
+
try:
|
|
127
|
+
content = path.read_text("utf-8", errors="ignore")
|
|
128
|
+
except OSError:
|
|
129
|
+
continue
|
|
130
|
+
if any(pattern.search(content) for pattern in I18N_PATTERNS):
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def check_hardcoded_strings(root: Path, code_files: list[Path], enforce: bool) -> dict[str, Any]:
|
|
136
|
+
if not enforce:
|
|
137
|
+
return {"status": "not_applicable", "files_checked": 0, "files_using_i18n": 0, "issues": [], "notes": ["No i18n framework or locale files detected."]}
|
|
138
|
+
|
|
139
|
+
issues: list[dict[str, Any]] = []
|
|
140
|
+
files_using_i18n = 0
|
|
141
|
+
checked = 0
|
|
142
|
+
for path in code_files[:500]:
|
|
143
|
+
try:
|
|
144
|
+
content = path.read_text("utf-8", errors="ignore")
|
|
145
|
+
except OSError:
|
|
146
|
+
continue
|
|
147
|
+
checked += 1
|
|
148
|
+
has_i18n = any(pattern.search(content) for pattern in I18N_PATTERNS)
|
|
149
|
+
if has_i18n:
|
|
150
|
+
files_using_i18n += 1
|
|
151
|
+
if has_i18n:
|
|
152
|
+
continue
|
|
153
|
+
file_type = CODE_TYPES[path.suffix.lower()]
|
|
154
|
+
for pattern in HARDCODED_PATTERNS[file_type]:
|
|
155
|
+
for match in pattern.finditer(content):
|
|
156
|
+
value = match.group(1).strip()
|
|
157
|
+
if value.lower().startswith(("http", "error", "warning")):
|
|
158
|
+
continue
|
|
159
|
+
line = content.count("\n", 0, match.start()) + 1
|
|
160
|
+
issues.append({
|
|
161
|
+
"severity": "medium",
|
|
162
|
+
"file": path.relative_to(root).as_posix(),
|
|
163
|
+
"line": line,
|
|
164
|
+
"issue": "Likely user-facing hard-coded string",
|
|
165
|
+
"text": value[:100],
|
|
166
|
+
})
|
|
167
|
+
if len(issues) >= 100:
|
|
168
|
+
break
|
|
169
|
+
if len(issues) >= 100:
|
|
170
|
+
break
|
|
171
|
+
if len(issues) >= 100:
|
|
172
|
+
break
|
|
173
|
+
return {"status": "checked", "files_checked": checked, "files_using_i18n": files_using_i18n, "issues": issues, "notes": []}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def audit(root: Path, strict: bool = False) -> dict[str, Any]:
|
|
177
|
+
locale_files = find_locale_files(root)
|
|
178
|
+
code_files = list(iter_code_files(root))
|
|
179
|
+
enforce = strict or project_uses_i18n(root, code_files, locale_files)
|
|
180
|
+
locale = check_locale_completeness(locale_files, root)
|
|
181
|
+
code = check_hardcoded_strings(root, code_files, enforce)
|
|
182
|
+
findings = locale["issues"] + code["issues"]
|
|
183
|
+
high = sum(item["severity"] == "high" for item in findings)
|
|
184
|
+
medium = sum(item["severity"] == "medium" for item in findings)
|
|
185
|
+
return {
|
|
186
|
+
"project": str(root),
|
|
187
|
+
"applicable": enforce,
|
|
188
|
+
"locale_check": locale,
|
|
189
|
+
"code_check": code,
|
|
190
|
+
"summary": {"total": len(findings), "high": high, "medium": medium, "passed": high == 0},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def main() -> int:
|
|
195
|
+
parser = argparse.ArgumentParser(description="Check locale completeness and likely hard-coded UI strings")
|
|
196
|
+
parser.add_argument("project", nargs="?", default=".")
|
|
197
|
+
parser.add_argument("--strict", action="store_true", help="Scan hard-coded strings even when no i18n setup is detected")
|
|
198
|
+
parser.add_argument("--json", action="store_true", dest="as_json")
|
|
199
|
+
args = parser.parse_args()
|
|
200
|
+
root = Path(args.project).resolve()
|
|
201
|
+
if not root.is_dir():
|
|
202
|
+
parser.error(f"Project directory does not exist: {root}")
|
|
203
|
+
report = audit(root, args.strict)
|
|
204
|
+
if args.as_json:
|
|
205
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
206
|
+
else:
|
|
207
|
+
print(f"i18n Audit: {root}")
|
|
208
|
+
if not report["applicable"]:
|
|
209
|
+
print("[SKIP] No i18n framework or locale files detected.")
|
|
210
|
+
else:
|
|
211
|
+
locale = report["locale_check"]
|
|
212
|
+
code = report["code_check"]
|
|
213
|
+
print(f"Locale files: {locale['files']} | Languages: {', '.join(locale['languages']) or 'unknown'}")
|
|
214
|
+
print(f"Code files checked: {code['files_checked']} | Files using i18n: {code['files_using_i18n']}")
|
|
215
|
+
for item in (locale["issues"] + code["issues"])[:30]:
|
|
216
|
+
line = f":{item['line']}" if "line" in item else ""
|
|
217
|
+
print(f"[{item['severity'].upper()}] {item['file']}{line} - {item['issue']}")
|
|
218
|
+
print("[PASS] No blocking i18n issues." if report["summary"]["passed"] else "[FAIL] Blocking i18n issues found.")
|
|
219
|
+
return 0 if report["summary"]["passed"] else 1
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
raise SystemExit(main())
|