Skip to content
Open
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
6a352ee
feat: add ISR with i18n content translation for node pages
snomiao Apr 5, 2026
e7f222a
feat: auto-translate node descriptions via OpenAI when no stored tran…
snomiao Apr 5, 2026
a2d594f
debug: add error logging to getStaticProps
snomiao Apr 5, 2026
33013ea
fix: use fetch instead of generated axios client in getStaticProps
snomiao Apr 5, 2026
bcad8fa
debug: add translation debug logging
snomiao Apr 5, 2026
ec36bca
chore: trigger redeploy for updated OPENAI_API_KEY
snomiao Apr 5, 2026
885cd0b
chore: remove debug logging from ISR translation
snomiao Apr 5, 2026
5c9cba2
fix: re-fetch getStaticProps on locale switch for correct translations
snomiao Apr 5, 2026
0203a3d
feat: add SEO meta tags with translated content to node pages
snomiao Apr 5, 2026
526571c
format: Apply prettier --fix changes
snomiao Apr 5, 2026
1951e00
fix: add 15s timeout to OpenAI translation fetch via AbortController
snomiao Apr 5, 2026
e3d8ca1
fix: validate publisherId matches node's publisher in getStaticProps
snomiao Apr 5, 2026
7597080
docs: update content i18n status to reflect current implementation
snomiao Apr 5, 2026
97dcb63
format: Apply prettier --fix changes
snomiao Apr 5, 2026
4ba73c4
refactor: export LANGUAGE_NAMES from constants to avoid duplication
snomiao Apr 5, 2026
f61c937
fix: address review — deduplicate LANGUAGE_NAMES, fix clearTimeout le…
snomiao Apr 5, 2026
fa19f0a
fix: add SEO meta tags to publisher-scoped node page
snomiao Apr 5, 2026
5b26534
docs: update implementation status in i18n docs to reflect current state
snomiao Apr 5, 2026
cb735ef
format: Apply prettier --fix changes
snomiao Apr 5, 2026
df11450
perf: move OpenAI translation to async client-side fetch
snomiao Apr 5, 2026
76de056
format: Apply prettier --fix changes
snomiao Apr 5, 2026
8021cc6
fix: add method restriction and locale allowlist to translate-node API
snomiao Apr 5, 2026
9c69300
fix: use temporary redirect for publisher mismatch
snomiao Apr 5, 2026
e4b52bf
fix: update translateNodeContent docstring to reflect actual usage
snomiao Apr 5, 2026
d1083c1
docs: clarify ISR uses stored translations only, auto-translate is async
snomiao Apr 5, 2026
6e5428b
format: Apply prettier --fix changes
snomiao Apr 5, 2026
e1a7cbf
feat: block on translation in getStaticProps for SEO
snomiao Apr 5, 2026
1115238
feat: preheat top 20 nodes × 4 locales at build time
snomiao Apr 5, 2026
91e30f1
format: Apply prettier --fix changes
snomiao Apr 5, 2026
fe50fd3
perf: only preheat on production builds, skip preview PRs
snomiao Apr 5, 2026
399f010
test: enable preheat on sno-i18n-isr branch for validation
snomiao Apr 5, 2026
991ee44
format: Apply prettier --fix changes
snomiao Apr 5, 2026
94a79f6
docs: add ISR implementation experience, metrics, and debugging lessons
snomiao Apr 5, 2026
dd03de9
fix: preserve LANGUAGE_NAMES type inference for SupportedLanguage
snomiao Apr 5, 2026
f232903
format: Apply prettier --fix changes
snomiao Apr 5, 2026
786996b
docs: clarify translateNodeContent is used by both getStaticProps and…
snomiao Apr 7, 2026
f539a28
fix: validate nodeId format/length in translate-node API to reduce ab…
snomiao Apr 7, 2026
b8b95e7
format: Apply prettier --fix changes
snomiao Apr 7, 2026
8b0d525
fix: clear stale asyncTranslation before early-return on locale/node …
snomiao Apr 7, 2026
385de79
format: Apply prettier --fix changes
snomiao Apr 7, 2026
8b1fa5b
refactor: extract shared loadNodeStaticProps helper for ISR/bot variants
snomiao Apr 7, 2026
563293f
fix: remove blocking OpenAI call from human node-page getStaticProps
snomiao Apr 7, 2026
5f09bdf
feat: add /_bot/nodes blocking ISR variants for search engine crawlers
snomiao Apr 7, 2026
7545f8c
feat: rewrite bot requests for node pages to /_bot blocking variant
snomiao Apr 7, 2026
6812768
feat: add ContentToggle to switch between translated and original des…
snomiao Apr 7, 2026
f1963aa
chore(i18n): scan new keys for ContentToggle
snomiao Apr 7, 2026
c9403dd
fix: type loadNodeStaticProps return as GetStaticPropsResult
snomiao Apr 7, 2026
68bb8a4
format: Apply prettier --fix changes
snomiao Apr 7, 2026
65e25cd
fix: make Translating… indicator reachable in ContentToggle
snomiao Apr 8, 2026
f0d4cdf
format: Apply prettier --fix changes
snomiao Apr 8, 2026
9579f8a
feat: use isbot package for bot detection in middleware
snomiao Apr 8, 2026
f61c673
format: Apply prettier --fix changes
snomiao Apr 8, 2026
107f289
fix: distinguish stored vs auto translations in ContentToggle label
snomiao Apr 9, 2026
ce44296
format: Apply prettier --fix changes
snomiao Apr 9, 2026
3147d67
format: Apply prettier --fix changes
snomiao Apr 9, 2026
eb0666e
refactor: remove /api/translate-node route
snomiao Apr 9, 2026
ace3c6d
fix: remove dead asyncTranslation/isTranslating references after tran…
snomiao Apr 9, 2026
e9d13d2
format: Apply prettier --fix changes
snomiao Apr 9, 2026
84e165c
fix: URL-encode publisher/node IDs in redirect destination
snomiao Apr 9, 2026
e04c71b
format: Apply prettier --fix changes
snomiao Apr 9, 2026
619f3b8
fix: guard auto-translation on source=original and update docstring
snomiao Apr 9, 2026
75b24d6
docs: remove stale /api/translate-node reference from nodeStaticProps
snomiao Apr 9, 2026
419a337
fix: remove hardcoded branch name from PREHEAT_ENABLED and update sta…
snomiao Apr 9, 2026
076ee68
docs: update ISR implementation doc to reflect removal of client-side…
snomiao Apr 9, 2026
af1ce53
format: Apply prettier --fix changes
snomiao Apr 9, 2026
edb59d1
docs: clarify OpenAI translation is bot-path only in architecture dia…
snomiao Apr 10, 2026
81312ac
fix: correct middleware comment about bot vs human content difference
snomiao Apr 10, 2026
3c8aa56
format: Apply prettier --fix changes
snomiao Apr 10, 2026
3e0d992
Merge remote-tracking branch 'origin/main' into sno-i18n-isr
snomiao May 11, 2026
1e1b987
fix: tighten bot canonical redirects and align ISR docs
Copilot May 11, 2026
b9f4821
fix(isr): return notFound on 404 from registry API
snomiao May 12, 2026
70b9c9a
Merge remote-tracking branch 'origin/main' into sno-i18n-isr
snomiao May 12, 2026
faef1b7
format: Apply prettier --fix changes
snomiao May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 97 additions & 0 deletions components/nodes/ContentToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useEffect, useState } from "react";
import { useNextTranslation } from "@/src/hooks/i18n";
import type { TranslationSource } from "@/src/hooks/i18n/translateNode";

const STORAGE_KEY = "comfy-registry-content-translation-mode";
type Mode = "translated" | "original";

interface ContentToggleProps {
original: string;
translated: string | null;
locale: string;
isLoadingTranslation?: boolean;
/**
* Where the translation came from. `"stored"` means a publisher-provided
* translation from the registry API; `"auto"` means OpenAI generated it on
* the fly. Used to show an accurate label (AI-translated vs. human-translated)
* so users aren't misled about provenance.
*/
translationSource?: TranslationSource;
}

/**
* Renders dynamic node content (description, etc.) with a toggle that lets
* users switch between the auto-translated version and the original English.
*
* - When `locale === "en"` or no translation is available, falls back to
* the original and hides the toggle entirely.
* - The user's preference is persisted in localStorage so toggling once
* sticks across navigations.
* - Default mode is "translated" when a translation exists, so users on
* non-English locales get the localized text by default.
*/
export default function ContentToggle({
original,
translated,
locale,
isLoadingTranslation,
translationSource,
}: ContentToggleProps) {
const { t } = useNextTranslation();
const [mode, setMode] = useState<Mode>("translated");

// Restore stored preference on mount
useEffect(() => {
if (typeof window === "undefined") return;
const saved = window.localStorage.getItem(STORAGE_KEY);
if (saved === "original" || saved === "translated") setMode(saved);
}, []);

const isNonEn = locale !== "en";
const hasTranslation = !!translated && isNonEn;
const showOriginal = !hasTranslation || mode === "original";
const displayed = showOriginal ? original : translated!;
// Show the header whenever we're on a non-en locale and either have a
// translation to toggle to or are still fetching one. This makes the
// "Translating…" indicator reachable while the async fetch is in flight.
const showHeader = isNonEn && (hasTranslation || isLoadingTranslation);

const toggle = () => {
const next: Mode = mode === "translated" ? "original" : "translated";
setMode(next);
if (typeof window !== "undefined") window.localStorage.setItem(STORAGE_KEY, next);
};

return (
<div>
{showHeader && (
<div className="mb-2 flex items-center gap-2 text-xs text-gray-400">
{hasTranslation && (
<button
type="button"
onClick={toggle}
className="rounded border border-gray-600 px-2 py-1 hover:border-blue-500 hover:text-blue-300"
>
{showOriginal ? t("Show translation") : t("Show original")}
Comment thread
snomiao marked this conversation as resolved.
</button>
)}
{hasTranslation &&
!showOriginal &&
(translationSource === "auto" ? (
<span title={t("Auto-translated by AI; original may be more accurate")}>
{t("Auto-translated")}
</span>
) : translationSource === "stored" ? (
<span title={t("Translation provided by the publisher")}>{t("Translated")}</span>
) : (
<span>{t("Translated")}</span>
))}
{isLoadingTranslation && !translated && (
<span className="italic">{t("Translating…")}</span>
)}
</div>
)}
<p className="text-base font-normal whitespace-pre-wrap text-gray-200">{displayed}</p>
Comment on lines +68 to +94
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLoadingTranslation && !translated can never render here because this block is gated by hasTranslation (which is !!translated && locale !== 'en'). As a result the “Translating…” indicator is unreachable when a translation is being fetched. Consider rendering the header when locale !== 'en' && (translated || isLoadingTranslation), and keep the toggle button conditional on translated.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8a670db — split the header gate so it shows whenever locale !== 'en' and we either have a translation or are still loading one. The toggle button stays gated on hasTranslation, but the Translating… indicator is now reachable during the async fetch.

</div>
);
}
29 changes: 27 additions & 2 deletions components/nodes/NodeDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import {
} from "@/src/api/generated";
import nodesLogo from "@/src/assets/images/nodesLogo.svg";
import { useNextTranslation } from "@/src/hooks/i18n";
import type { TranslatedNodeContent } from "@/src/hooks/i18n/translateNode";
import CopyableCodeBlock from "../CodeBlock/CodeBlock";
import ContentToggle from "./ContentToggle";
import { NodeDeleteModal } from "./NodeDeleteModal";
import { NodeEditModal } from "./NodeEditModal";
import NodeStatusBadge from "./NodeStatusBadge";
Expand Down Expand Up @@ -87,8 +89,13 @@ export function formatDownloadCount(count: number): string {
return `${cleanNum}${units[unitIndex]}`;
}

const NodeDetails = () => {
const NodeDetails = ({
translatedContent,
}: {
translatedContent?: TranslatedNodeContent | null;
Comment thread
snomiao marked this conversation as resolved.
}) => {
const { t, i18n } = useNextTranslation();

// state for drawer and modals
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [selectedVersion, setSelectedVersion] = useState<NodeVersion | null>(null);
Expand Down Expand Up @@ -489,7 +496,25 @@ const NodeDetails = () => {
</div>
<div>
<h2 className="mb-2 text-lg font-bold">{t("Description")}</h2>
<p className="text-base font-normal text-gray-200">{node.description}</p>
{(() => {
const isrTranslation =
translatedContent?.locale === i18n.language &&
translatedContent?.description &&
translatedContent.description !== node.description
? {
description: translatedContent.description,
source: translatedContent.source,
}
: null;
return (
<ContentToggle
original={node.description ?? ""}
translated={isrTranslation?.description ?? null}
translationSource={isrTranslation?.source}
locale={i18n.language}
/>
);
Comment on lines 498 to +516
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NodeDetails no longer performs any async translation fetch (no useEffect / /api/translate-node call), so on the human path users will only ever see (a) stored publisher translations or (b) the original English description. This conflicts with the PR’s stated “progressive translation enhancement” behavior and also makes the Translating…/toggle UX unreachable for nodes without stored translations. Either reintroduce the client-side translation fetch path, or update the PR/docs/UI to match the actual behavior.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — the client-side translation fallback was intentionally removed (commit 28234f9) because the /api/translate-node route exposed OpenAI cost without auth. The current design: humans see stored translations or English; bots get blocking OpenAI translation via the /_bot path. Stale docs/comments have been updated in commits 95e1ff0, d5c7017, 74e5771, 723c9de.

})()}
</div>
<div className="mt-10" hidden={isUnclaimed}>
<h2 className="mb-2 text-lg font-semibold">{t("Version history")}</h2>
Expand Down
Loading
Loading