fix: 语言切换全面修复 —— 品牌层/主题首页/详情区块/404 全量 i18n(zh 矩阵 13/13)

- 站点品牌双语:每站 brandZh(品牌名/标语/hero/导航/页脚/搜索占位)由生成器写入
  站点 messages 的 zh 覆盖;SiteChrome 改为 async getTranslations 按语言渲染
- 五套主题首页接线 siteHome/hero 命名空间(新增 wire-theme-i18n.py 一次性接线脚本,
  仅处理组件函数体、hook 自动插入、hero 用 th 命名空间)
- 详情页 Use cases/Good for/FAQ/reviewCta、用例与合集索引与详情、提交页、
  对比页、Alternatives 表头与 FAQ 全部走 next-intl
- 404 双语言:locale 内走 next-intl,根级按 Accept-Language
- 修复 zh.json 中 84 处 zhXxx 旁挂键归位 + en.json 残留清理(i18n 694 键一致)
- 回归:/zh 下 13 页全中文无英文残留、无横向溢出;五站 /zh lang=zh 全部正确
This commit is contained in:
AI Directory Platform
2026-09-09 16:59:52 +00:00
parent f4f48f7293
commit 5cc4195d68
50 changed files with 4141 additions and 235 deletions
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
# 脚本 wire-theme-i18n
# 说明:一次性把五个主题模板首页的硬编码文案接线到 next-intl(siteHome/hero 命名空间)。
# 规则:
# - import 加 useTranslations
# - 每个含 t( 调用的函数组件体开头插入 hook 声明
# - 文案字符串替换为 t("key") / {t("key")}(JSX 子节点带花括号,属性值带花括号)
# - 顶层常量(navItems/footerGroups)不改(导航/页脚由 SiteChrome i18n 提供,这些常量是死代码但无害)
# 运行:python3 scripts/wire-theme-i18n.py
"""
import re
# 每个模板的替换表:(原文案, i18n key, 是否 JSX 子节点)
COMMON = [
# classic
('>Find the best AI tools, ranked by real users<', 'heroTitle', True),
('>1,000+ AI tools across 14 categories. Compare votes, reviews and pricing to pick the right\n one.<', 'heroSubtitle', True),
('placeholder="Search 1,000+ AI tools..."', 'searchPlaceholder', False),
('<span>Popular:</span>', 'popularLabel', True),
('>\n Search\n </button>', 'searchButton', True),
('"Trending this week"', 'trendingTitle', False),
('"Ranked by votes, reviews and engagement over the last 7 days"', 'trendingSubtitle', False),
('"All rankings"', 'allRankings', False),
('"Browse by category"', 'categoriesTitle', False),
('"All tools"', 'categoriesViewAll', False),
('{category.toolCount} tools', 'toolsCount', 'ctx:category.toolCount'),
('"New AI tools"', 'newTitle', False),
('"Editor\'s Choice"', 'editorsTitle', False),
('"Tools by use case"', 'useCasesTitle', False),
('"What do you want to get done?"', 'useCasesSubtitle', False),
('"All use cases"', 'useCasesViewAll', False),
('"Best AI tool lists"', 'bestListsTitle', False),
('"Curated rankings by our editors"', 'bestListsSubtitle', False),
('"All lists"', 'bestListsViewAll', False),
('"{col.toolCount} tools"', 'toolsCount', 'ctx:col.toolCount'),
('"Latest community reviews"', 'reviewsTitle', False),
# product
('placeholder="Search AI products..."', 'searchPlaceholder', False),
('>Today\'s AI product launches<', 'heroTitle', True),
('>Fresh AI products, every day. Upvote the best ones to the top.<', 'heroSubtitle', True),
('Launch your product', 'launchButton', False),
('"Today\'s feed"', 'feedTitle', False),
('No launches yet — be the first to submit.', 'noLaunches', False),
('Browse all products', 'feedBrowse', False),
('"Fresh launches"', 'newTitle', False),
('"Leaderboard"', 'leaderboardTitle', False),
('Full leaderboard', 'leaderboardFull', False),
('"{tool.reviewCount} reviews"', 'reviewsCount', 'ctx:tool.reviewCount'),
('"Collections"', 'collectionsTitle', False),
# editorial
('Independent AI Coverage', 'mastheadLabel', False),
('Featured story', 'featuredLabel', False),
('Read the story', 'featuredRead', False),
('Editor\'s picks', 'picksEditorialLabel', False),
('"Buying guides"', 'guidesTitle', False),
('"Deep dives that help you choose"', 'guidesSubtitle', False),
('"All guides"', 'guidesViewAll', False),
('tools compared', 'toolsCompared', False),
('"Latest stories"', 'storiesTitle', False),
('"All stories"', 'storiesViewAll', False),
('"From the community"', 'communityTitle', False),
('"Browse directory"', 'communityViewAll', False),
# niche
('placeholder="Search dev tools..."', 'searchPlaceholder', False),
('>AI tools for developers<', 'heroTitle', True),
('>Code assistants, DevOps automation, testing copilots and everything else you ship with.<', 'heroSubtitle', True),
('Browse tools\n', 'browseButton', True),
('"Ship faster with AI"', 'shipTitle', False),
('"Pick your workflow — we ranked the tools for it"', 'shipSubtitle', False),
('"Most upvoted this week"', 'upvotedTitle', False),
('"View all"', 'upvotedViewAll', False),
('"Workflows & stacks"', 'workflowsTitle', False),
('"Batteries-included tool stacks for common dev tasks"', 'workflowsSubtitle', False),
('"All workflows"', 'workflowsViewAll', False),
# minimal
('AI TOOL SEARCH', 'minimalTagline', False),
('placeholder="I need to create marketing videos..."', 'minimalPlaceholder', False),
('"Trending now"', 'trendingNowTitle', False),
('See full rankings', 'seeRankings', False),
]
NEEDS = {
"heroTitle": ("hero", "title"), "heroSubtitle": ("hero", "description"),
"searchPlaceholder": ("siteHome", "searchPlaceholder"),
"popularLabel": ("siteHome", "popularLabel"),
"trendingTitle": ("siteHome", "trendingTitle"), "trendingSubtitle": ("siteHome", "trendingSubtitle"),
"allRankings": ("siteHome", "allRankings"),
"categoriesTitle": ("siteHome", "categoriesTitle"), "categoriesViewAll": ("siteHome", "categoriesViewAll"),
"newTitle": ("siteHome", "newTitle"), "editorsTitle": ("siteHome", "editorsTitle"),
"useCasesTitle": ("siteHome", "useCasesTitle"), "useCasesSubtitle": ("siteHome", "useCasesSubtitle"),
"useCasesViewAll": ("siteHome", "useCasesViewAll"),
"bestListsTitle": ("siteHome", "bestListsTitle"), "bestListsSubtitle": ("siteHome", "bestListsSubtitle"),
"bestListsViewAll": ("siteHome", "bestListsViewAll"),
"reviewsTitle": ("siteHome", "reviewsTitle"),
"ctaTitle": ("siteHome", "ctaTitle"), "ctaBody": ("siteHome", "ctaBody"), "ctaButton": ("siteHome", "ctaButton"),
"launchButton": ("siteHome", "launchButton"),
"feedTitle": ("siteHome", "feedTitle"), "noLaunches": ("siteHome", "noLaunches"),
"feedBrowse": ("siteHome", "feedBrowse"),
"leaderboardTitle": ("siteHome", "leaderboardTitle"), "leaderboardFull": ("siteHome", "leaderboardFull"),
"picksTitle": ("siteHome", "picksTitle"), "collectionsTitle": ("siteHome", "collectionsTitle"),
"mastheadLabel": ("siteHome", "mastheadLabel"),
"featuredLabel": ("siteHome", "featuredLabel"), "featuredRead": ("siteHome", "featuredRead"),
"picksEditorialLabel": ("siteHome", "picksEditorialLabel"),
"guidesTitle": ("siteHome", "guidesTitle"), "guidesSubtitle": ("siteHome", "guidesSubtitle"),
"guidesViewAll": ("siteHome", "guidesViewAll"),
"storiesTitle": ("siteHome", "storiesTitle"), "storiesViewAll": ("siteHome", "storiesViewAll"),
"communityTitle": ("siteHome", "communityTitle"), "communityViewAll": ("siteHome", "communityViewAll"),
"shipTitle": ("siteHome", "shipTitle"), "shipSubtitle": ("siteHome", "shipSubtitle"),
"upvotedTitle": ("siteHome", "upvotedTitle"), "upvotedViewAll": ("siteHome", "upvotedViewAll"),
"workflowsTitle": ("siteHome", "workflowsTitle"), "workflowsSubtitle": ("siteHome", "workflowsSubtitle"),
"workflowsViewAll": ("siteHome", "workflowsViewAll"),
"browseButton": ("siteHome", "browseButton"),
"browseCategoryTitle": ("siteHome", "browseCategoryTitle"),
"minimalTagline": ("siteHome", "minimalTagline"),
"minimalPlaceholder": ("siteHome", "minimalPlaceholder"),
"trendingNowTitle": ("siteHome", "trendingNowTitle"),
"seeRankings": ("siteHome", "seeRankings"),
"searchButton": ("siteHome", "searchButton"),
"toolsCount": ("siteHome", "toolsCount"),
"toolsCompared": ("siteHome", "toolsCompared"),
"reviewsCount": ("siteHome", "reviewsCount"),
}
TEMPLATES = [
"src/kit/site-themes/classic/index.tsx",
"src/kit/site-themes/product/index.tsx",
"src/kit/site-themes/editorial/index.tsx",
"src/kit/site-themes/niche/index.tsx",
"src/kit/site-themes/minimal/index.tsx",
]
def add_import(src):
if "useTranslations" not in src:
m = re.search(r'import \{ ([^}]+) \} from "lucide-react";\n', src)
assert m, "lucide import not found"
src = src.replace(m.group(0), m.group(0) + 'import { useTranslations } from "next-intl";\n', 1)
return src
def add_hooks(src):
"""为每个使用了 t( 的函数组件体开头插入 hook 声明"""
out = []
# 找所有 function 块开头
for m in re.finditer(r'((?:async )?function (\w+)\(\{[^}]*locale[^}]*\}(?:[^)]*)\) \{\n)', src):
header, fname = m.group(1), m.group(2)
start = m.end()
# 找函数结束(下一个顶层 function 或文件尾)
nxt = re.search(r'\n(?:async )?function ', src[start:])
end = start + (nxt.start() if nxt else len(src) - start)
body = src[start:end]
if 't("' not in body and 't(\'"' not in body:
continue
if 'const t = useTranslations' in body:
continue
hooks = ' const t = useTranslations("siteHome");\n'
if any(k in body for k in ("heroTitle", "heroSubtitle")):
hooks += ' const th = useTranslations("hero");\n'
src = src[:start] + hooks + src[start:]
# 重新计算(插入后偏移变化),直接 break 重跑
return add_hooks(src)
return src
def apply_replacements(src, fname):
used = set()
# 只在组件函数体内部替换(跳过顶层常量——导航/页脚由 SiteChrome i18n 提供)
first_fn = re.search(r'(?:async )?function \w+', src)
body_start = first_fn.start() if first_fn else len(src)
head, body = src[:body_start], src[body_start:]
for original, key, mode in COMMON:
ns, k = NEEDS[key] if key in NEEDS else (None, None)
tref = f'{ns}["{k}"]' if ns else key # hero 用 th
fn = "th" if ns == "hero" else "t"
tref = f'{fn}("{k}")'
if original.startswith('>'):
# JSX 子节点(含换行缩进的两段式原文用正则容忍空白)
pat = re.compile(re.escape(original).replace(r'\ ', r'\s+'))
if pat.search(body):
body = pat.sub(f'>{tref}<', body, count=1)
used.add(key)
elif mode == 'ctx':
ctx_expr = original.replace('{', '').replace('}', '').split()[0]
pat = re.compile(re.escape(original).replace(r'\ ', r'\s+'))
if pat.search(body):
body = pat.sub(f'{{{tref}({ctx_expr})}}', body, count=1)
used.add(key)
elif mode is False or mode is None:
# 引号内字符串 → 属性 {t("key")} 或 JSX 子节点 {t("key")}
if original in body:
body = body.replace(original, f'{{{tref}}}')
used.add(key)
return head + body, used
def main():
for p in TEMPLATES:
src = open(p).read()
src = add_import(src)
src, used = apply_replacements(src, p)
src = add_hooks(src)
open(p, 'w').write(src)
# 报告未使用的 key(该模板不需要)
unused = [k for k in NEEDS if k not in used]
print(f"{p}: wired {len(used)} keys")
if __name__ == "__main__":
main()