-
Notifications
You must be signed in to change notification settings - Fork 10
feat: ISR with auto-translated content i18n for node pages #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 66 commits
6a352ee
e7f222a
a2d594f
33013ea
bcad8fa
ec36bca
885cd0b
5c9cba2
0203a3d
526571c
1951e00
e3d8ca1
7597080
97dcb63
4ba73c4
f61c937
fa19f0a
5b26534
cb735ef
df11450
76de056
8021cc6
9c69300
e4b52bf
d1083c1
6e5428b
e1a7cbf
1115238
91e30f1
fe50fd3
399f010
991ee44
94a79f6
dd03de9
f232903
786996b
f539a28
b8b95e7
8b0d525
385de79
8b1fa5b
563293f
5f09bdf
7545f8c
6812768
f1963aa
c9403dd
68bb8a4
65e25cd
f0d4cdf
9579f8a
f61c673
107f289
ce44296
3147d67
eb0666e
ace3c6d
e9d13d2
84e165c
e04c71b
619f3b8
75b24d6
419a337
076ee68
af1ce53
edb59d1
81312ac
3c8aa56
3e0d992
1e1b987
b9f4821
70b9c9a
faef1b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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")} | ||
| </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
|
||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -87,8 +89,13 @@ export function formatDownloadCount(count: number): string { | |
| return `${cleanNum}${units[unitIndex]}`; | ||
| } | ||
|
|
||
| const NodeDetails = () => { | ||
| const NodeDetails = ({ | ||
| translatedContent, | ||
| }: { | ||
| translatedContent?: TranslatedNodeContent | null; | ||
|
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); | ||
|
|
@@ -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
|
||
| })()} | ||
| </div> | ||
| <div className="mt-10" hidden={isUnclaimed}> | ||
| <h2 className="mb-2 text-lg font-semibold">{t("Version history")}</h2> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.