- 站点品牌双语:每站 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 全部正确
206 lines
11 KiB
Python
206 lines
11 KiB
Python
#!/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()
|