Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ See [docs/chromatic-setup.md](docs/chromatic-setup.md) for more details on our C
#### Update PR Branches

We provide a GitHub Actions workflow to automatically update open PR branches with the latest changes from `main`. This is useful for:

- Keeping long-running PRs up-to-date
- Reducing merge conflicts
- Repository maintenance
Expand Down
416 changes: 199 additions & 217 deletions bun.lock

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion components/common/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Link, { LinkProps } from 'next/link'
import { useRouter } from 'next/router'
import React, { useEffect, useMemo, useState } from 'react'
import { SUPPORTED_LANGUAGES } from '@/src/constants'
import { useNextTranslation } from '@/src/hooks/i18n'
import { useDynamicTranslate, useNextTranslation } from '@/src/hooks/i18n'
import { LocalizationContributeModal } from './LocalizationContributeModal'

export default function LanguageSwitcher({
Expand All @@ -13,6 +13,11 @@ export default function LanguageSwitcher({
className?: string
} = {}) {
const { t, i18n, changeLanguage, currentLanguage } = useNextTranslation()
const {
available: dynamicTranslateAvailable,
enabled: dynamicTranslateEnabled,
setEnabled: setDynamicTranslateEnabled,
} = useDynamicTranslate()
const router = useRouter()
const [showContributeModal, setShowContributeModal] = useState(false)

Expand Down Expand Up @@ -131,6 +136,31 @@ export default function LanguageSwitcher({
</DropdownItem>
)
})}
{dynamicTranslateAvailable && (
<DropdownItem
className="border-t border-gray-200 mt-2 pt-2"
onClick={() => {
setDynamicTranslateEnabled(!dynamicTranslateEnabled)
}}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<span className="mr-2">🔄</span>
<span>{t('Dynamic Translation')}</span>
<span className="ml-1 text-xs text-gray-500">(Beta)</span>
</div>
<input
type="checkbox"
checked={dynamicTranslateEnabled}
onChange={() =>
setDynamicTranslateEnabled(!dynamicTranslateEnabled)
}
Comment on lines +155 to +157
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

The onChange handler duplicates the onClick handler from the parent DropdownItem (lines 142-144). Consider removing the onChange handler from the checkbox since the parent already handles the toggle, or remove the parent's onClick to avoid duplicate event handling.

Suggested change
onChange={() =>
setDynamicTranslateEnabled(!dynamicTranslateEnabled)
}
// onChange handler removed to avoid duplicate event handling

Copilot uses AI. Check for mistakes.
className="form-checkbox h-4 w-4 text-blue-600"
onClick={(e) => e.stopPropagation()}
/>
</div>
</DropdownItem>
)}
<DropdownItem
className="border-t border-gray-600 mt-2 pt-2"
onClick={() => setShowContributeModal(true)}
Expand Down
8 changes: 5 additions & 3 deletions components/nodes/NodeDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
useListPublishersForUser,
} from '@/src/api/generated'
import nodesLogo from '@/src/assets/images/nodesLogo.svg'
import { useNextTranslation } from '@/src/hooks/i18n'
import { useDynamicTranslate, useNextTranslation } from '@/src/hooks/i18n'
import CopyableCodeBlock from '../CodeBlock/CodeBlock'
import { NodeDeleteModal } from './NodeDeleteModal'
import { NodeEditModal } from './NodeEditModal'
Expand Down Expand Up @@ -90,6 +90,8 @@ export function formatDownloadCount(count: number): string {

const NodeDetails = () => {
const { t, i18n } = useNextTranslation()
const { dt } = useDynamicTranslate()

// state for drawer and modals
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [selectedVersion, setSelectedVersion] = useState<NodeVersion | null>(
Expand Down Expand Up @@ -443,7 +445,7 @@ const NodeDetails = () => {
<div>
<h2 className="mb-2 text-lg font-bold">{t('Description')}</h2>
<p className="text-base font-normal text-gray-200">
{node.description}
{dt(node.description)}
</p>
</div>
<div className="mt-10" hidden={isUnclaimed}>
Expand All @@ -463,7 +465,7 @@ const NodeDetails = () => {
<FormatRelativeDate date={version.createdAt || ''} />
</p>
<div className="flex-grow mt-3 text-base font-normal text-gray-200 line-clamp-2">
{version.changelog}
{dt(version.changelog)}
</div>
<div
className="text-sm font-normal text-blue-500 cursor-pointer"
Expand Down
124 changes: 124 additions & 0 deletions docs/node-metadata-translation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Node Metadata Dynamic Translation

## Problem Statement

The ComfyUI Registry displays custom nodes with metadata (descriptions, changelogs, etc.) that node authors write in various languages, primarily English. Users who speak other languages need to be able to read node descriptions and changelogs in their preferred language.

### Challenges

1. **User-Generated Content**: Node descriptions and changelogs are submitted by node authors, not part of the application's static content. Traditional i18n approaches that work with pre-translated static keys don't apply here.

2. **Dynamic and Unpredictable**: New nodes and versions are constantly added to the registry. We cannot pre-translate all possible content.

3. **Translation Quality**: We need accurate, contextual translations that preserve technical terminology specific to ComfyUI.

4. **Cost and Performance**: Server-side translation APIs (like Google Translate, OpenAI) can be expensive at scale and add latency.

5. **Availability**: Some translation services may not be accessible in certain regions (e.g., China).

## Solution: Browser-Based Dynamic Translation

This PR implements an experimental feature using Chrome's built-in Translator API to provide dynamic, on-demand translation of node metadata.

### How It Works

1. **Browser Translator API**: Uses Chrome 138+'s experimental `window.Translator` API, which provides local, on-device translation.

2. **Opt-in Feature**: Users can enable dynamic translation via a toggle in the language dropdown menu.

3. **Translation Hook**: The `useDynamicTranslate()` hook provides a `dt()` function that wraps i18next:
- When disabled or unavailable: Returns the original text (English)
- When enabled: Translates text on-demand using the browser's translation service

4. **Missing Key Handler**: When i18next encounters a missing translation key (user-generated content), it automatically attempts translation via the Translator API and caches the result.

5. **Selective Usage**: The `dt()` function is applied only to user-generated content:
- Node descriptions (`dt(node.description)`)
- Version changelogs (`dt(version.changelog)`)
- Static UI text continues using regular `t()` function

### Implementation Details

#### Core Components

**`src/hooks/i18n/index.tsx`**

- TypeScript type definitions for Chrome's Translator API
- `useDynamicTranslateEnabled()` - LocalStorage-based toggle hook
- `useDynamicTranslate()` - Main hook providing:
- `available` - Whether browser supports the Translator API
- `enabled` - User preference for dynamic translation
- `setEnabled` - Toggle function
- `dt()` - Dynamic translation function
- `missingKeyHandler` - i18next callback that auto-translates missing keys

**`components/nodes/NodeDetails.tsx`**

- Uses `dt()` for node descriptions and version changelogs
- Displays English text when feature is disabled/unavailable

**`components/common/LanguageSwitcher.tsx`**

- Shows dynamic translation toggle when browser supports it
- Toggle appears as a checkbox option in the language dropdown
- Marked as "(Beta)" to indicate experimental status

### Advantages

1. **No Server Costs**: Translation happens locally in the browser
2. **Fast**: Local processing with no network round-trips
3. **Privacy**: Content never leaves the user's device
4. **Offline Capable**: Works without internet connection (after model download)
5. **Quality**: Uses Google's neural translation models
6. **Caching**: i18next automatically caches translated content

### Limitations

1. **Browser Support**: Only works in Chrome 138+ with experimental features enabled
2. **Not SSR-Compatible**: Cannot be used for server-side rendering
3. **Regional Availability**: Not available in all regions (notably China)
4. **Opt-in**: Users must manually enable the feature
5. **Initial Load**: First translation may be slower while browser downloads language models

### Dependencies

- `use-async@^1.2.0` - For managing async Translator API state
- `react-use` - For LocalStorage persistence of user preference

## Future Considerations

### Alternative Approaches

1. **OpenAI GPT Translation**: Server-side translation using ChatGPT API
- Pros: Better quality, context-aware, available everywhere
- Cons: API costs, latency, privacy concerns

2. **Crowdsourced Translations**: Allow community to submit translations
- Pros: Free, potentially high quality for popular nodes
- Cons: Requires moderation, incomplete coverage

3. **Publisher-Provided Translations**: Encourage node authors to provide translations
- Pros: Most accurate, free
- Cons: Low adoption, maintenance burden on publishers

### Potential Improvements

1. **Fallback to Server**: When browser API unavailable, use server-side translation
2. **Pre-translation**: Batch-translate popular nodes server-side
3. **Translation Memory**: Share translations across users via backend
4. **Quality Indicators**: Show confidence scores or allow users to report bad translations

## Testing

To test this feature:

1. Use Chrome 138+ with experimental features enabled
2. Visit a node detail page
3. Switch to a non-English language
4. Enable "Dynamic Translation" in the language dropdown
5. Observe descriptions and changelogs being translated

## References

- Chrome Translator API: [Built-in AI Early Preview Program](https://developer.chrome.com/docs/ai/built-in)
- i18next missing key handling: [i18next Events](https://www.i18next.com/overview/api#events)
19 changes: 14 additions & 5 deletions docs/update-pr-branches.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ The "Update PR Branches to Latest Main" workflow allows maintainers to automatic
2. Click on "Update PR Branches to Latest Main" workflow
3. Click the "Run workflow" button
4. Configure options:
- **PR numbers**: Leave empty to update all open PRs, or specify comma-separated PR numbers (e.g., `220,224,226`)
- **Dry run**: Check this to see what would be updated without making changes
- **PR numbers**: Leave empty to update all open PRs, or specify comma-separated PR numbers (e.g., `220,224,226`)
- **Dry run**: Check this to see what would be updated without making changes
5. Click "Run workflow"

### Via GitHub CLI

Update all open PRs:

```bash
gh workflow run update-pr-branches.yml
```

Update specific PRs:

```bash
gh workflow run update-pr-branches.yml -f pr_numbers="220,224,226"
```

Dry run mode:

```bash
gh workflow run update-pr-branches.yml -f dry_run=true
```
Expand All @@ -58,9 +61,9 @@ If the workflow detects merge conflicts:
- The merge is aborted automatically
- A comment is added to the PR noting manual intervention is needed
- The PR author should:
1. Pull latest changes from main
2. Resolve conflicts locally
3. Push the resolved changes
1. Pull latest changes from main
2. Resolve conflicts locally
3. Push the resolved changes

## Limitations

Expand All @@ -80,16 +83,19 @@ If the workflow detects merge conflicts:
## Troubleshooting

### Workflow fails with permission error

- Check that `GITHUB_TOKEN` has write permissions
- Verify branch protection rules allow GitHub Actions to push

### PR not updated

- Check workflow logs for specific error messages
- Verify PR is in OPEN state
- Ensure branch exists and is accessible
- Check for merge conflicts reported in comments

### Push fails

- Branch may have protection rules preventing push
- PR might be from a forked repository
- Network issues or rate limiting
Expand All @@ -104,16 +110,19 @@ If the workflow detects merge conflicts:
## Examples

### Example 1: Update all open PRs

```bash
gh workflow run update-pr-branches.yml
```

### Example 2: Update specific PRs (220, 224, 226)

```bash
gh workflow run update-pr-branches.yml -f pr_numbers="220,224,226"
```

### Example 3: Dry run to see what would be updated

```bash
gh workflow run update-pr-branches.yml -f dry_run=true
```
Expand Down
7 changes: 4 additions & 3 deletions locales/ar/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
"Approved by admin": "تمت الموافقة بواسطة المشرف",
"Are you sure you want to delete this node? This action cannot be undone.": "هل أنت متأكد أنك تريد حذف هذه العقدة؟ لا يمكن التراجع عن هذا الإجراء.",
"Are you sure you want to delete this version? This action cannot be undone.": "هل أنت متأكد أنك تريد حذف هذا الإصدار؟ لا يمكن التراجع عن هذا الإجراء.",
"Are you sure you want to unclaim this node? This will remove the publisher association, allowing the original author to claim it under a different publisher.": "هل أنت متأكد أنك تريد إلغاء المطالبة بهذه العقدة؟ هذا سيزيل ارتباط الناشر، مما يسمح للمؤلف الأصلي بالمطالبة بها تحت ناشر مختلف.",
"Author": "المؤلف",
"Back to Node Details": "العودة إلى تفاصيل العقدة",
"Back to node details": "العودة إلى تفاصيل العقدة",
"Are you sure you want to unclaim this node? This will remove the publisher association, allowing the original author to claim it under a different publisher.": "هل أنت متأكد أنك تريد إلغاء المطالبة بهذه العقدة؟ هذا سيزيل ارتباط الناشر، مما يسمح للمؤلف الأصلي بالمطالبة بها تحت ناشر مختلف.",
"Back to social login": "العودة إلى تسجيل الدخول الاجتماعي",
"Back to your nodes": "العودة إلى عقدك",
"Banned": "محظور",
Expand Down Expand Up @@ -117,6 +117,7 @@
"Download Version {{version}}": "تنزيل الإصدار {{version}}",
"Downloads": "التنزيلات",
"Duplicated node: ": "عقدة مكررة:",
"Dynamic Translation": "ترجمة ديناميكية",
"E.g. Jane Doe": "مثال: Jane Doe",
"E.g. janedoe55": "مثال: janedoe55",
"Edit": "تحرير",
Expand Down Expand Up @@ -196,9 +197,9 @@
"Installs": "التثبيتات",
"Invalid GitHub repository URL format.": "صيغة عنوان URL لمستودع GitHub غير صالحة.",
"Invalid batch action: {{action}}": "إجراء دفعة غير صالح: {{action}}",
"Invalid email format": "تنسيق البريد الإلكتروني غير صالح",
"Invalid repository URL format": "تنسيق عنوان URL للمستودع غير صالح",
"Issue with Node Version {{nodeId}}@{{version}}": "مشكلة في إصدار العقدة {{nodeId}}@{{version}}",
"Invalid email format": "تنسيق البريد الإلكتروني غير صالح",
"Join our Discord community": "انضم إلى مجتمعنا على Discord",
"Latest Version": "أحدث إصدار",
"Latest Version Compatibility Reference": "مرجع توافق أحدث إصدار",
Expand Down Expand Up @@ -378,8 +379,8 @@
"Unable to claim the node. Please verify your GitHub repository ownership and try again.": "غير قادر على المطالبة بالعقدة. يرجى التحقق من ملكية مستودع GitHub الخاص بك والمحاولة مرة أخرى.",
"Unable to get user email, please reload and try again": "غير قادر على الحصول على بريد المستخدم الإلكتروني، يرجى إعادة التحميل والمحاولة مرة أخرى.",
"Unable to save: missing node or publisher information": "غير قادر على الحفظ: معلومات العقدة أو الناشر مفقودة",
"Unable to unclaim: missing node information": "غير قادر على إلغاء المطالبة: معلومات العقدة مفقودة",
"Unable to verify repository permissions. Please try again.": "غير قادر على التحقق من أذونات المستودع. يرجى المحاولة مرة أخرى.",
"Unable to unclaim: missing node information": "غير قادر على إلغاء المطالبة: معلومات العقدة مفقودة",
"Unclaim Node": "إلغاء المطالبة بالعقدة",
"Unclaim node": "إلغاء المطالبة بالعقدة",
"Unclaimed": "غير مُطالب بها",
Expand Down
1 change: 1 addition & 0 deletions locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"Download Version {{version}}": "Download Version {{version}}",
"Downloads": "Downloads",
"Duplicated node: ": "Duplicated node: ",
"Dynamic Translation": "Dynamic Translation",
"E.g. Jane Doe": "E.g. Jane Doe",
"E.g. janedoe55": "E.g. janedoe55",
"Edit": "Edit",
Expand Down
7 changes: 4 additions & 3 deletions locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
"Approved by admin": "Aprobado por el administrador",
"Are you sure you want to delete this node? This action cannot be undone.": "¿Estás seguro de que quieres eliminar este nodo? Esta acción no se puede deshacer.",
"Are you sure you want to delete this version? This action cannot be undone.": "¿Estás seguro de que quieres eliminar esta versión? Esta acción no puede deshacerse.",
"Are you sure you want to unclaim this node? This will remove the publisher association, allowing the original author to claim it under a different publisher.": "¿Está seguro de que desea cancelar la reclamación de este nodo? Esto eliminará la asociación con el editor, permitiendo al autor original reclamarlo bajo un editor diferente.",
"Author": "Autor",
"Back to Node Details": "Volver a los detalles del nodo",
"Back to node details": "Volver a los detalles del nodo",
"Are you sure you want to unclaim this node? This will remove the publisher association, allowing the original author to claim it under a different publisher.": "¿Está seguro de que desea cancelar la reclamación de este nodo? Esto eliminará la asociación con el editor, permitiendo al autor original reclamarlo bajo un editor diferente.",
"Back to social login": "Volver a inicio de sesión social",
"Back to your nodes": "Volver a tus nodos",
"Banned": "Prohibido",
Expand Down Expand Up @@ -117,6 +117,7 @@
"Download Version {{version}}": "Descargar Versión {{version}}",
"Downloads": "Descargas",
"Duplicated node: ": "Nodo duplicado: ",
"Dynamic Translation": "Traducción dinámica",
"E.g. Jane Doe": "Por ejemplo: Jane Doe",
"E.g. janedoe55": "Por ejemplo: janedoe55",
"Edit": "Editar",
Expand Down Expand Up @@ -196,9 +197,9 @@
"Installs": "Instalaciones",
"Invalid GitHub repository URL format.": "Formato de URL de repositorio de GitHub no válido.",
"Invalid batch action: {{action}}": "Acción de lote inválida: {{action}}",
"Invalid email format": "Formato de correo electrónico no válido",
"Invalid repository URL format": "Formato de URL de repositorio no válido",
"Issue with Node Version {{nodeId}}@{{version}}": "Problema con la versión del nodo {{nodeId}}@{{version}}",
"Invalid email format": "Formato de correo electrónico no válido",
"Join our Discord community": "Únete a nuestra comunidad de Discord",
"Latest Version": "Última Versión",
"Latest Version Compatibility Reference": "Última Referencia de Compatibilidad de Versión",
Expand Down Expand Up @@ -378,8 +379,8 @@
"Unable to claim the node. Please verify your GitHub repository ownership and try again.": "No se puede reclamar el nodo. Por favor verifique la propiedad de su repositorio de GitHub y vuelva a intentarlo.",
"Unable to get user email, please reload and try again": "No se puede obtener el correo electrónico del usuario, por favor recargue e intente de nuevo",
"Unable to save: missing node or publisher information": "No se puede guardar: falta información del nodo o del editor.",
"Unable to unclaim: missing node information": "No se puede cancelar la reclamación: falta información del nodo.",
"Unable to verify repository permissions. Please try again.": "No se pueden verificar los permisos del repositorio. Por favor, inténtelo de nuevo.",
"Unable to unclaim: missing node information": "No se puede cancelar la reclamación: falta información del nodo.",
"Unclaim Node": "Cancelar reclamación de nodo",
"Unclaim node": "Cancelar reclamación de nodo",
"Unclaimed": "No reclamado",
Expand Down
Loading