#!/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), ('Popular:', 'popularLabel', True), ('>\n Search\n ', '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()