mirror of https://github.com/langgenius/dify.git
Merge branch 'feat/credit-pool' into deploy/dev
This commit is contained in:
commit
c3d0c80ca1
|
|
@ -1,94 +0,0 @@
|
|||
name: Translate i18n Files Based on English
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/i18n/en-US/*.json'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-and-update:
|
||||
if: github.repository == 'langgenius/dify'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
# Keep use old checkout action version for https://github.com/peter-evans/create-pull-request/issues/4272
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for file changes in i18n/en-US
|
||||
id: check_files
|
||||
run: |
|
||||
# Skip check for manual trigger, translate all files
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "FILES_CHANGED=true" >> $GITHUB_ENV
|
||||
echo "FILE_ARGS=" >> $GITHUB_ENV
|
||||
echo "Manual trigger: translating all files"
|
||||
else
|
||||
git fetch origin "${{ github.event.before }}" || true
|
||||
git fetch origin "${{ github.sha }}" || true
|
||||
changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'i18n/en-US/*.json')
|
||||
echo "Changed files: $changed_files"
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "FILES_CHANGED=true" >> $GITHUB_ENV
|
||||
file_args=""
|
||||
for file in $changed_files; do
|
||||
filename=$(basename "$file" .json)
|
||||
file_args="$file_args --file $filename"
|
||||
done
|
||||
echo "FILE_ARGS=$file_args" >> $GITHUB_ENV
|
||||
echo "File arguments: $file_args"
|
||||
else
|
||||
echo "FILES_CHANGED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node.js
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate i18n translations
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm run i18n:gen ${{ env.FILE_ARGS }}
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore(i18n): update translations based on en-US changes'
|
||||
title: 'chore(i18n): translate i18n files based on en-US changes'
|
||||
body: |
|
||||
This PR was automatically created to update i18n translation files based on changes in en-US locale.
|
||||
|
||||
**Triggered by:** ${{ github.sha }}
|
||||
|
||||
**Changes included:**
|
||||
- Updated translation files for all locales
|
||||
branch: chore/automated-i18n-updates-${{ github.sha }}
|
||||
delete-branch: true
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
name: Translate i18n Files with Claude Code
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/i18n/en-US/*.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
files:
|
||||
description: 'Specific files to translate (space-separated, e.g., "app common"). Leave empty for all files.'
|
||||
required: false
|
||||
type: string
|
||||
languages:
|
||||
description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.'
|
||||
required: false
|
||||
type: string
|
||||
mode:
|
||||
description: 'Sync mode: incremental (only changes) or full (re-check all keys)'
|
||||
required: false
|
||||
default: 'incremental'
|
||||
type: choice
|
||||
options:
|
||||
- incremental
|
||||
- full
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
if: github.repository == 'langgenius/dify'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
- name: Detect changed files and generate diff
|
||||
id: detect_changes
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
# Manual trigger
|
||||
if [ -n "${{ github.event.inputs.files }}" ]; then
|
||||
echo "CHANGED_FILES=${{ github.event.inputs.files }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Get all JSON files in en-US directory
|
||||
files=$(ls web/i18n/en-US/*.json 2>/dev/null | xargs -n1 basename | sed 's/.json$//' | tr '\n' ' ')
|
||||
echo "CHANGED_FILES=$files" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "TARGET_LANGS=${{ github.event.inputs.languages }}" >> $GITHUB_OUTPUT
|
||||
echo "SYNC_MODE=${{ github.event.inputs.mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# For manual trigger with incremental mode, get diff from last commit
|
||||
# For full mode, we'll do a complete check anyway
|
||||
if [ "${{ github.event.inputs.mode }}" == "full" ]; then
|
||||
echo "Full mode: will check all keys" > /tmp/i18n-diff.txt
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
git diff HEAD~1..HEAD -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
||||
if [ -s /tmp/i18n-diff.txt ]; then
|
||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Push trigger - detect changed files from the push
|
||||
BEFORE_SHA="${{ github.event.before }}"
|
||||
# Handle edge case: first push or force push may have null/zero SHA
|
||||
if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
# Fallback to comparing with parent commit
|
||||
BEFORE_SHA="HEAD~1"
|
||||
fi
|
||||
changed=$(git diff --name-only "$BEFORE_SHA" ${{ github.sha }} -- 'web/i18n/en-US/*.json' 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/.json$//' | tr '\n' ' ' || echo "")
|
||||
echo "CHANGED_FILES=$changed" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_LANGS=" >> $GITHUB_OUTPUT
|
||||
echo "SYNC_MODE=incremental" >> $GITHUB_OUTPUT
|
||||
|
||||
# Generate detailed diff for the push
|
||||
git diff "$BEFORE_SHA"..${{ github.sha }} -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
||||
if [ -s /tmp/i18n-diff.txt ]; then
|
||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
# Truncate diff if too large (keep first 50KB)
|
||||
if [ -f /tmp/i18n-diff.txt ]; then
|
||||
head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
|
||||
mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
|
||||
fi
|
||||
|
||||
echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"
|
||||
|
||||
- name: Run Claude Code for Translation Sync
|
||||
if: steps.detect_changes.outputs.CHANGED_FILES != ''
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prompt: |
|
||||
You are a professional i18n synchronization engineer for the Dify project.
|
||||
Your task is to keep all language translations in sync with the English source (en-US).
|
||||
|
||||
## CRITICAL TOOL RESTRICTIONS
|
||||
- Use **Read** tool to read files (NOT cat or bash)
|
||||
- Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts)
|
||||
- Use **Bash** ONLY for: git commands, gh commands, pnpm commands
|
||||
- Run bash commands ONE BY ONE, never combine with && or ||
|
||||
- NEVER use `$()` command substitution - it's not supported. Split into separate commands instead.
|
||||
|
||||
## WORKING DIRECTORY & ABSOLUTE PATHS
|
||||
Claude Code sandbox working directory may vary. Always use absolute paths:
|
||||
- For pnpm: `pnpm --dir ${{ github.workspace }}/web <command>`
|
||||
- For git: `git -C ${{ github.workspace }} <command>`
|
||||
- For gh: `gh --repo ${{ github.repository }} <command>`
|
||||
- For file paths: `${{ github.workspace }}/web/i18n/`
|
||||
|
||||
## EFFICIENCY RULES
|
||||
- **ONE Edit per language file** - batch all key additions into a single Edit
|
||||
- Insert new keys at the beginning of JSON (after `{`), lint:fix will sort them
|
||||
- Translate ALL keys for a language mentally first, then do ONE Edit
|
||||
|
||||
## Context
|
||||
- Changed/target files: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
||||
- Target languages (empty means all supported): ${{ steps.detect_changes.outputs.TARGET_LANGS }}
|
||||
- Sync mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
||||
- Translation files are located in: ${{ github.workspace }}/web/i18n/{locale}/{filename}.json
|
||||
- Language configuration is in: ${{ github.workspace }}/web/i18n-config/languages.ts
|
||||
- Git diff is available: ${{ steps.detect_changes.outputs.DIFF_AVAILABLE }}
|
||||
|
||||
## CRITICAL DESIGN: Verify First, Then Sync
|
||||
|
||||
You MUST follow this three-phase approach:
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 1: VERIFY - Analyze and Generate Change Report ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
### Step 1.1: Analyze Git Diff (for incremental mode)
|
||||
Use the Read tool to read `/tmp/i18n-diff.txt` to see the git diff.
|
||||
|
||||
Parse the diff to categorize changes:
|
||||
- Lines with `+` (not `+++`): Added or modified values
|
||||
- Lines with `-` (not `---`): Removed or old values
|
||||
- Identify specific keys for each category:
|
||||
* ADD: Keys that appear only in `+` lines (new keys)
|
||||
* UPDATE: Keys that appear in both `-` and `+` lines (value changed)
|
||||
* DELETE: Keys that appear only in `-` lines (removed keys)
|
||||
|
||||
### Step 1.2: Read Language Configuration
|
||||
Use the Read tool to read `${{ github.workspace }}/web/i18n-config/languages.ts`.
|
||||
Extract all languages with `supported: true`.
|
||||
|
||||
### Step 1.3: Run i18n:check for Each Language
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web install --frozen-lockfile
|
||||
```
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
||||
```
|
||||
|
||||
This will report:
|
||||
- Missing keys (need to ADD)
|
||||
- Extra keys (need to DELETE)
|
||||
|
||||
### Step 1.4: Generate Change Report
|
||||
|
||||
Create a structured report identifying:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ I18N SYNC CHANGE REPORT ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Files to process: [list] ║
|
||||
║ Languages to sync: [list] ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ ADD (New Keys): ║
|
||||
║ - [filename].[key]: "English value" ║
|
||||
║ ... ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ UPDATE (Modified Keys - MUST re-translate): ║
|
||||
║ - [filename].[key]: "Old value" → "New value" ║
|
||||
║ ... ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ DELETE (Extra Keys): ║
|
||||
║ - [language]/[filename].[key] ║
|
||||
║ ... ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**IMPORTANT**: For UPDATE detection, compare git diff to find keys where
|
||||
the English value changed. These MUST be re-translated even if target
|
||||
language already has a translation (it's now stale!).
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 2: SYNC - Execute Changes Based on Report ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
### Step 2.1: Process ADD Operations (BATCH per language file)
|
||||
|
||||
**CRITICAL WORKFLOW for efficiency:**
|
||||
1. First, translate ALL new keys for ALL languages mentally
|
||||
2. Then, for EACH language file, do ONE Edit operation:
|
||||
- Read the file once
|
||||
- Insert ALL new keys at the beginning (right after the opening `{`)
|
||||
- Don't worry about alphabetical order - lint:fix will sort them later
|
||||
|
||||
Example Edit (adding 3 keys to zh-Hans/app.json):
|
||||
```
|
||||
old_string: '{\n "accessControl"'
|
||||
new_string: '{\n "newKey1": "translation1",\n "newKey2": "translation2",\n "newKey3": "translation3",\n "accessControl"'
|
||||
```
|
||||
|
||||
**IMPORTANT**:
|
||||
- ONE Edit per language file (not one Edit per key!)
|
||||
- Always use the Edit tool. NEVER use bash scripts, node, or jq.
|
||||
|
||||
### Step 2.2: Process UPDATE Operations
|
||||
|
||||
**IMPORTANT: Special handling for zh-Hans and ja-JP**
|
||||
If zh-Hans or ja-JP files were ALSO modified in the same push:
|
||||
- Run: `git -C ${{ github.workspace }} diff HEAD~1 --name-only` and check for zh-Hans or ja-JP files
|
||||
- If found, it means someone manually translated them. Apply these rules:
|
||||
|
||||
1. **Missing keys**: Still ADD them (completeness required)
|
||||
2. **Existing translations**: Compare with the NEW English value:
|
||||
- If translation is **completely wrong** or **unrelated** → Update it
|
||||
- If translation is **roughly correct** (captures the meaning) → Keep it, respect manual work
|
||||
- When in doubt, **keep the manual translation**
|
||||
|
||||
Example:
|
||||
- English changed: "Save" → "Save Changes"
|
||||
- Manual translation: "保存更改" → Keep it (correct meaning)
|
||||
- Manual translation: "删除" → Update it (completely wrong)
|
||||
|
||||
For other languages:
|
||||
Use Edit tool to replace the old value with the new translation.
|
||||
You can batch multiple updates in one Edit if they are adjacent.
|
||||
|
||||
### Step 2.3: Process DELETE Operations
|
||||
For extra keys reported by i18n:check:
|
||||
- Run: `pnpm --dir ${{ github.workspace }}/web run i18n:check --auto-remove`
|
||||
- Or manually remove from target language JSON files
|
||||
|
||||
## Translation Guidelines
|
||||
|
||||
- PRESERVE all placeholders exactly as-is:
|
||||
- `{{variable}}` - Mustache interpolation
|
||||
- `${variable}` - Template literal
|
||||
- `<tag>content</tag>` - HTML tags
|
||||
- `_one`, `_other` - Pluralization suffixes (these are KEY suffixes, not values)
|
||||
- Use appropriate language register (formal/informal) based on existing translations
|
||||
- Match existing translation style in each language
|
||||
- Technical terms: check existing conventions per language
|
||||
- For CJK languages: no spaces between characters unless necessary
|
||||
- For RTL languages (ar-TN, fa-IR): ensure proper text handling
|
||||
|
||||
## Output Format Requirements
|
||||
- Alphabetical key ordering (if original file uses it)
|
||||
- 2-space indentation
|
||||
- Trailing newline at end of file
|
||||
- Valid JSON (use proper escaping for special characters)
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 3: RE-VERIFY - Confirm All Issues Resolved ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
### Step 3.1: Run Lint Fix (IMPORTANT!)
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web lint:fix --quiet -- 'i18n/**/*.json'
|
||||
```
|
||||
This ensures:
|
||||
- JSON keys are sorted alphabetically (jsonc/sort-keys rule)
|
||||
- Valid i18n keys (dify-i18n/valid-i18n-keys rule)
|
||||
- No extra keys (dify-i18n/no-extra-keys rule)
|
||||
|
||||
### Step 3.2: Run Final i18n Check
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
||||
```
|
||||
|
||||
### Step 3.3: Fix Any Remaining Issues
|
||||
If check reports issues:
|
||||
- Go back to PHASE 2 for unresolved items
|
||||
- Repeat until check passes
|
||||
|
||||
### Step 3.4: Generate Final Summary
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ SYNC COMPLETED SUMMARY ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Language │ Added │ Updated │ Deleted │ Status ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ zh-Hans │ 5 │ 2 │ 1 │ ✓ Complete ║
|
||||
║ ja-JP │ 5 │ 2 │ 1 │ ✓ Complete ║
|
||||
║ ... │ ... │ ... │ ... │ ... ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ i18n:check │ PASSED - All keys in sync ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Mode-Specific Behavior
|
||||
|
||||
**SYNC_MODE = "incremental"** (default):
|
||||
- Focus on keys identified from git diff
|
||||
- Also check i18n:check output for any missing/extra keys
|
||||
- Efficient for small changes
|
||||
|
||||
**SYNC_MODE = "full"**:
|
||||
- Compare ALL keys between en-US and each language
|
||||
- Run i18n:check to identify all discrepancies
|
||||
- Use for first-time sync or fixing historical issues
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. Always run i18n:check BEFORE and AFTER making changes
|
||||
2. The check script is the source of truth for missing/extra keys
|
||||
3. For UPDATE scenario: git diff is the source of truth for changed values
|
||||
4. Create a single commit with all translation changes
|
||||
5. If any translation fails, continue with others and report failures
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 4: COMMIT AND CREATE PR ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
After all translations are complete and verified:
|
||||
|
||||
### Step 4.1: Check for changes
|
||||
```bash
|
||||
git -C ${{ github.workspace }} status --porcelain
|
||||
```
|
||||
|
||||
If there are changes:
|
||||
|
||||
### Step 4.2: Create a new branch and commit
|
||||
Run these git commands ONE BY ONE (not combined with &&).
|
||||
**IMPORTANT**: Do NOT use `$()` command substitution. Use two separate commands:
|
||||
|
||||
1. First, get the timestamp:
|
||||
```bash
|
||||
date +%Y%m%d-%H%M%S
|
||||
```
|
||||
(Note the output, e.g., "20260115-143052")
|
||||
|
||||
2. Then create branch using the timestamp value:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} checkout -b chore/i18n-sync-20260115-143052
|
||||
```
|
||||
(Replace "20260115-143052" with the actual timestamp from step 1)
|
||||
|
||||
3. Stage changes:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} add web/i18n/
|
||||
```
|
||||
|
||||
4. Commit:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}"
|
||||
```
|
||||
|
||||
5. Push:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} push origin HEAD
|
||||
```
|
||||
|
||||
### Step 4.3: Create Pull Request
|
||||
```bash
|
||||
gh pr create --repo ${{ github.repository }} --title "chore(i18n): sync translations with en-US" --body "## Summary
|
||||
|
||||
This PR was automatically generated to sync i18n translation files.
|
||||
|
||||
### Changes
|
||||
- Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
||||
- Files processed: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
||||
|
||||
### Verification
|
||||
- [x] \`i18n:check\` passed
|
||||
- [x] \`lint:fix\` applied
|
||||
|
||||
🤖 Generated with Claude Code GitHub Action" --base main
|
||||
```
|
||||
|
||||
claude_args: |
|
||||
--max-turns 150
|
||||
--allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),Glob,Grep"
|
||||
|
|
@ -270,7 +270,7 @@ class HostedDeepseekConfig(BaseSettings):
|
|||
)
|
||||
|
||||
HOSTED_DEEPSEEK_PAID_ENABLED: bool = Field(
|
||||
description="Enable paid access to hosted XAI service",
|
||||
description="Enable paid access to hosted Deepseek service",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
|
@ -367,7 +367,7 @@ class HostedTongyiConfig(BaseSettings):
|
|||
)
|
||||
|
||||
HOSTED_TONGYI_TRIAL_ENABLED: bool = Field(
|
||||
description="Enable trial access to hosted Anthropic service",
|
||||
description="Enable trial access to hosted Tongyi service",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from flask import make_response, redirect, request
|
||||
from flask_restx import Resource, reqparse
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, model_validator
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.web.error import NotFoundError
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
|
|
@ -35,35 +35,38 @@ from ..wraps import (
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerSubscriptionUpdateRequest(BaseModel):
|
||||
"""Request payload for updating a trigger subscription"""
|
||||
class TriggerSubscriptionBuilderCreatePayload(BaseModel):
|
||||
credential_type: str = CredentialType.UNAUTHORIZED
|
||||
|
||||
name: str | None = Field(default=None, description="The name for the subscription")
|
||||
credentials: Mapping[str, Any] | None = Field(default=None, description="The credentials for the subscription")
|
||||
parameters: Mapping[str, Any] | None = Field(default=None, description="The parameters for the subscription")
|
||||
properties: Mapping[str, Any] | None = Field(default=None, description="The properties for the subscription")
|
||||
|
||||
class TriggerSubscriptionBuilderVerifyPayload(BaseModel):
|
||||
credentials: dict[str, Any]
|
||||
|
||||
|
||||
class TriggerSubscriptionBuilderUpdatePayload(BaseModel):
|
||||
name: str | None = None
|
||||
parameters: dict[str, Any] | None = None
|
||||
properties: dict[str, Any] | None = None
|
||||
credentials: dict[str, Any] | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_at_least_one_field(self):
|
||||
if all(v is None for v in (self.name, self.credentials, self.parameters, self.properties)):
|
||||
if all(v is None for v in self.model_dump().values()):
|
||||
raise ValueError("At least one of name, credentials, parameters, or properties must be provided")
|
||||
return self
|
||||
|
||||
|
||||
class TriggerSubscriptionVerifyRequest(BaseModel):
|
||||
"""Request payload for verifying subscription credentials."""
|
||||
|
||||
credentials: Mapping[str, Any] = Field(description="The credentials to verify")
|
||||
class TriggerOAuthClientPayload(BaseModel):
|
||||
client_params: dict[str, Any] | None = None
|
||||
enabled: bool | None = None
|
||||
|
||||
|
||||
console_ns.schema_model(
|
||||
TriggerSubscriptionUpdateRequest.__name__,
|
||||
TriggerSubscriptionUpdateRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
)
|
||||
|
||||
console_ns.schema_model(
|
||||
TriggerSubscriptionVerifyRequest.__name__,
|
||||
TriggerSubscriptionVerifyRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
TriggerSubscriptionBuilderCreatePayload,
|
||||
TriggerSubscriptionBuilderVerifyPayload,
|
||||
TriggerSubscriptionBuilderUpdatePayload,
|
||||
TriggerOAuthClientPayload,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -132,16 +135,11 @@ class TriggerSubscriptionListApi(Resource):
|
|||
raise
|
||||
|
||||
|
||||
parser = reqparse.RequestParser().add_argument(
|
||||
"credential_type", type=str, required=False, nullable=True, location="json"
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/create",
|
||||
)
|
||||
class TriggerSubscriptionBuilderCreateApi(Resource):
|
||||
@console_ns.expect(parser)
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderCreatePayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
|
|
@ -151,10 +149,10 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
|
|||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
args = parser.parse_args()
|
||||
payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
credential_type = CredentialType.of(args.get("credential_type") or CredentialType.UNAUTHORIZED.value)
|
||||
credential_type = CredentialType.of(payload.credential_type)
|
||||
subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder(
|
||||
tenant_id=user.current_tenant_id,
|
||||
user_id=user.id,
|
||||
|
|
@ -182,18 +180,11 @@ class TriggerSubscriptionBuilderGetApi(Resource):
|
|||
)
|
||||
|
||||
|
||||
parser_api = (
|
||||
reqparse.RequestParser()
|
||||
# The credentials of the subscription builder
|
||||
.add_argument("credentials", type=dict, required=False, nullable=True, location="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify-and-update/<path:subscription_builder_id>",
|
||||
)
|
||||
class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource):
|
||||
@console_ns.expect(parser_api)
|
||||
class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
|
|
@ -203,7 +194,7 @@ class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource):
|
|||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
args = parser_api.parse_args()
|
||||
payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
# Use atomic update_and_verify to prevent race conditions
|
||||
|
|
@ -213,7 +204,7 @@ class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource):
|
|||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
credentials=args.get("credentials", None),
|
||||
credentials=payload.credentials,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
@ -221,24 +212,11 @@ class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource):
|
|||
raise ValueError(str(e)) from e
|
||||
|
||||
|
||||
parser_update_api = (
|
||||
reqparse.RequestParser()
|
||||
# The name of the subscription builder
|
||||
.add_argument("name", type=str, required=False, nullable=True, location="json")
|
||||
# The parameters of the subscription builder
|
||||
.add_argument("parameters", type=dict, required=False, nullable=True, location="json")
|
||||
# The properties of the subscription builder
|
||||
.add_argument("properties", type=dict, required=False, nullable=True, location="json")
|
||||
# The credentials of the subscription builder
|
||||
.add_argument("credentials", type=dict, required=False, nullable=True, location="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/update/<path:subscription_builder_id>",
|
||||
)
|
||||
class TriggerSubscriptionBuilderUpdateApi(Resource):
|
||||
@console_ns.expect(parser_update_api)
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
|
|
@ -249,7 +227,7 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
|
|||
assert isinstance(user, Account)
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
args = parser_update_api.parse_args()
|
||||
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
try:
|
||||
return jsonable_encoder(
|
||||
TriggerSubscriptionBuilderService.update_trigger_subscription_builder(
|
||||
|
|
@ -257,10 +235,10 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
|
|||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
name=args.get("name", None),
|
||||
parameters=args.get("parameters", None),
|
||||
properties=args.get("properties", None),
|
||||
credentials=args.get("credentials", None),
|
||||
name=payload.name,
|
||||
parameters=payload.parameters,
|
||||
properties=payload.properties,
|
||||
credentials=payload.credentials,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
@ -295,7 +273,7 @@ class TriggerSubscriptionBuilderLogsApi(Resource):
|
|||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/build/<path:subscription_builder_id>",
|
||||
)
|
||||
class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
@console_ns.expect(parser_update_api)
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
|
|
@ -304,7 +282,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
|||
"""Build a subscription instance for a trigger provider"""
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
args = parser_update_api.parse_args()
|
||||
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
try:
|
||||
# Use atomic update_and_build to prevent race conditions
|
||||
TriggerSubscriptionBuilderService.update_and_build_builder(
|
||||
|
|
@ -313,9 +291,9 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
|||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
name=args.get("name", None),
|
||||
parameters=args.get("parameters", None),
|
||||
properties=args.get("properties", None),
|
||||
name=payload.name,
|
||||
parameters=payload.parameters,
|
||||
properties=payload.properties,
|
||||
),
|
||||
)
|
||||
return 200
|
||||
|
|
@ -328,7 +306,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
|||
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/update",
|
||||
)
|
||||
class TriggerSubscriptionUpdateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionUpdateRequest.__name__])
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
|
|
@ -338,7 +316,7 @@ class TriggerSubscriptionUpdateApi(Resource):
|
|||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
request = TriggerSubscriptionUpdateRequest.model_validate(console_ns.payload)
|
||||
request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
subscription = TriggerProviderService.get_subscription_by_id(
|
||||
tenant_id=user.current_tenant_id,
|
||||
|
|
@ -568,13 +546,6 @@ class TriggerOAuthCallbackApi(Resource):
|
|||
return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback")
|
||||
|
||||
|
||||
parser_oauth_client = (
|
||||
reqparse.RequestParser()
|
||||
.add_argument("client_params", type=dict, required=False, nullable=True, location="json")
|
||||
.add_argument("enabled", type=bool, required=False, nullable=True, location="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/oauth/client")
|
||||
class TriggerOAuthClientManageApi(Resource):
|
||||
@setup_required
|
||||
|
|
@ -622,7 +593,7 @@ class TriggerOAuthClientManageApi(Resource):
|
|||
logger.exception("Error getting OAuth client", exc_info=e)
|
||||
raise
|
||||
|
||||
@console_ns.expect(parser_oauth_client)
|
||||
@console_ns.expect(console_ns.models[TriggerOAuthClientPayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
|
|
@ -632,15 +603,15 @@ class TriggerOAuthClientManageApi(Resource):
|
|||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
args = parser_oauth_client.parse_args()
|
||||
payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
provider_id = TriggerProviderID(provider)
|
||||
return TriggerProviderService.save_custom_oauth_client_params(
|
||||
tenant_id=user.current_tenant_id,
|
||||
provider_id=provider_id,
|
||||
client_params=args.get("client_params"),
|
||||
enabled=args.get("enabled"),
|
||||
client_params=payload.client_params,
|
||||
enabled=payload.enabled,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
|
|
@ -676,7 +647,7 @@ class TriggerOAuthClientManageApi(Resource):
|
|||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/verify/<path:subscription_id>",
|
||||
)
|
||||
class TriggerSubscriptionVerifyApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionVerifyRequest.__name__])
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
|
|
@ -686,9 +657,7 @@ class TriggerSubscriptionVerifyApi(Resource):
|
|||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
verify_request: TriggerSubscriptionVerifyRequest = TriggerSubscriptionVerifyRequest.model_validate(
|
||||
console_ns.payload
|
||||
)
|
||||
verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
result = TriggerProviderService.verify_subscription_credentials(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ from controllers.console.auth.error import (
|
|||
InvalidEmailError,
|
||||
)
|
||||
from controllers.console.error import AccountBannedError
|
||||
from controllers.console.wraps import only_edition_enterprise, setup_required
|
||||
from controllers.console.wraps import (
|
||||
decrypt_code_field,
|
||||
decrypt_password_field,
|
||||
only_edition_enterprise,
|
||||
setup_required,
|
||||
)
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.wraps import decode_jwt_token
|
||||
from libs.helper import email
|
||||
|
|
@ -42,6 +47,7 @@ class LoginApi(Resource):
|
|||
404: "Account not found",
|
||||
}
|
||||
)
|
||||
@decrypt_password_field
|
||||
def post(self):
|
||||
"""Authenticate user and login."""
|
||||
parser = (
|
||||
|
|
@ -181,6 +187,7 @@ class EmailCodeLoginApi(Resource):
|
|||
404: "Account not found",
|
||||
}
|
||||
)
|
||||
@decrypt_code_field
|
||||
def post(self):
|
||||
parser = (
|
||||
reqparse.RequestParser()
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ from extensions.ext_database import db
|
|||
from extensions.ext_redis import redis_client
|
||||
from extensions.otel import WorkflowAppRunnerHandler, trace_span
|
||||
from models import Workflow
|
||||
from models.enums import UserFrom
|
||||
from models.model import App, Conversation, Message, MessageAnnotation
|
||||
from models.workflow import ConversationVariable
|
||||
from services.conversation_variable_updater import ConversationVariableUpdater
|
||||
|
|
@ -106,6 +105,11 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
|||
if not app_record:
|
||||
raise ValueError("App not found")
|
||||
|
||||
invoke_from = self.application_generate_entity.invoke_from
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
invoke_from = InvokeFrom.DEBUGGER
|
||||
user_from = self._resolve_user_from(invoke_from)
|
||||
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
# Handle single iteration or single loop run
|
||||
graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution(
|
||||
|
|
@ -158,6 +162,8 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
|||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
|
||||
db.session.close()
|
||||
|
|
@ -175,12 +181,8 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
|||
graph=graph,
|
||||
graph_config=self._workflow.graph_dict,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=(
|
||||
UserFrom.ACCOUNT
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}
|
||||
else UserFrom.END_USER
|
||||
),
|
||||
invoke_from=self.application_generate_entity.invoke_from,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
variable_pool=variable_pool,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
|
|
|
|||
|
|
@ -73,9 +73,15 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
|||
"""
|
||||
app_config = self.application_generate_entity.app_config
|
||||
app_config = cast(PipelineConfig, app_config)
|
||||
invoke_from = self.application_generate_entity.invoke_from
|
||||
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
invoke_from = InvokeFrom.DEBUGGER
|
||||
|
||||
user_from = self._resolve_user_from(invoke_from)
|
||||
|
||||
user_id = None
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}:
|
||||
if invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}:
|
||||
end_user = db.session.query(EndUser).where(EndUser.id == self.application_generate_entity.user_id).first()
|
||||
if end_user:
|
||||
user_id = end_user.session_id
|
||||
|
|
@ -117,7 +123,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
|||
dataset_id=self.application_generate_entity.dataset_id,
|
||||
datasource_type=self.application_generate_entity.datasource_type,
|
||||
datasource_info=self.application_generate_entity.datasource_info,
|
||||
invoke_from=self.application_generate_entity.invoke_from.value,
|
||||
invoke_from=invoke_from.value,
|
||||
)
|
||||
|
||||
rag_pipeline_variables = []
|
||||
|
|
@ -149,6 +155,8 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
|||
graph_runtime_state=graph_runtime_state,
|
||||
start_node_id=self.application_generate_entity.start_node_id,
|
||||
workflow=workflow,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
|
||||
# RUN WORKFLOW
|
||||
|
|
@ -159,12 +167,8 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
|||
graph=graph,
|
||||
graph_config=workflow.graph_dict,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=(
|
||||
UserFrom.ACCOUNT
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}
|
||||
else UserFrom.END_USER
|
||||
),
|
||||
invoke_from=self.application_generate_entity.invoke_from,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
variable_pool=variable_pool,
|
||||
|
|
@ -210,7 +214,12 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
|||
return workflow
|
||||
|
||||
def _init_rag_pipeline_graph(
|
||||
self, workflow: Workflow, graph_runtime_state: GraphRuntimeState, start_node_id: str | None = None
|
||||
self,
|
||||
workflow: Workflow,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
start_node_id: str | None = None,
|
||||
user_from: UserFrom = UserFrom.ACCOUNT,
|
||||
invoke_from: InvokeFrom = InvokeFrom.SERVICE_API,
|
||||
) -> Graph:
|
||||
"""
|
||||
Init pipeline graph
|
||||
|
|
@ -253,8 +262,8 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
|||
workflow_id=workflow.id,
|
||||
graph_config=graph_config,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.SERVICE_API,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
call_depth=0,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ from core.workflow.workflow_entry import WorkflowEntry
|
|||
from extensions.ext_redis import redis_client
|
||||
from extensions.otel import WorkflowAppRunnerHandler, trace_span
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.enums import UserFrom
|
||||
from models.workflow import Workflow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -74,7 +73,12 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
|||
workflow_execution_id=self.application_generate_entity.workflow_execution_id,
|
||||
)
|
||||
|
||||
invoke_from = self.application_generate_entity.invoke_from
|
||||
# if only single iteration or single loop run is requested
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
invoke_from = InvokeFrom.DEBUGGER
|
||||
user_from = self._resolve_user_from(invoke_from)
|
||||
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution(
|
||||
workflow=self._workflow,
|
||||
|
|
@ -102,6 +106,8 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
|||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
root_node_id=self._root_node_id,
|
||||
)
|
||||
|
||||
|
|
@ -120,12 +126,8 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
|||
graph=graph,
|
||||
graph_config=self._workflow.graph_dict,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=(
|
||||
UserFrom.ACCOUNT
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}
|
||||
else UserFrom.END_USER
|
||||
),
|
||||
invoke_from=self.application_generate_entity.invoke_from,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
variable_pool=variable_pool,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
|
|
|
|||
|
|
@ -77,10 +77,18 @@ class WorkflowBasedAppRunner:
|
|||
self._app_id = app_id
|
||||
self._graph_engine_layers = graph_engine_layers
|
||||
|
||||
@staticmethod
|
||||
def _resolve_user_from(invoke_from: InvokeFrom) -> UserFrom:
|
||||
if invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}:
|
||||
return UserFrom.ACCOUNT
|
||||
return UserFrom.END_USER
|
||||
|
||||
def _init_graph(
|
||||
self,
|
||||
graph_config: Mapping[str, Any],
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
user_from: UserFrom,
|
||||
invoke_from: InvokeFrom,
|
||||
workflow_id: str = "",
|
||||
tenant_id: str = "",
|
||||
user_id: str = "",
|
||||
|
|
@ -105,8 +113,8 @@ class WorkflowBasedAppRunner:
|
|||
workflow_id=workflow_id,
|
||||
graph_config=graph_config,
|
||||
user_id=user_id,
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.SERVICE_API,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
call_depth=0,
|
||||
)
|
||||
|
||||
|
|
@ -250,7 +258,7 @@ class WorkflowBasedAppRunner:
|
|||
graph_config=graph_config,
|
||||
user_id="",
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.SERVICE_API,
|
||||
invoke_from=InvokeFrom.DEBUGGER,
|
||||
call_depth=0,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
|
||||
from sqlalchemy import update
|
||||
from sqlalchemy import CursorResult, update
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from configs import dify_config
|
||||
|
|
@ -61,7 +61,12 @@ class CreditPoolService:
|
|||
TenantCreditPool.quota_used + credits_required <= TenantCreditPool.quota_limit,
|
||||
]
|
||||
stmt = update(TenantCreditPool).where(*where_conditions).values(**update_values)
|
||||
session.execute(stmt)
|
||||
result: CursorResult = session.execute(stmt)
|
||||
session.commit()
|
||||
if result.rowcount == 0:
|
||||
raise QuotaExceededError(
|
||||
f"Insufficient credits. Required: {credits_required}, Available: {pool.remaining_credits}"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to deduct credits for tenant %s", tenant_id)
|
||||
raise QuotaExceededError("Failed to deduct credits")
|
||||
|
|
|
|||
|
|
@ -799,7 +799,7 @@ class TriggerProviderService:
|
|||
user_id: str,
|
||||
provider_id: TriggerProviderID,
|
||||
subscription_id: str,
|
||||
credentials: Mapping[str, Any],
|
||||
credentials: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Verify credentials for an existing subscription without updating it.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'
|
||||
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
|
|
@ -20,8 +19,6 @@ import { useAppVoices } from '@/service/use-apps'
|
|||
import { TtsAutoPlay } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type VoiceLanguageKey = I18nKeysWithPrefix<'common', 'voice.language.'>
|
||||
|
||||
type VoiceParamConfigProps = {
|
||||
onClose: () => void
|
||||
onChange?: OnFeaturesChange
|
||||
|
|
|
|||
|
|
@ -59,12 +59,12 @@ const InputItem: FC<Props> = ({
|
|||
}, [onRemove])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'hover:cursor-text hover:bg-state-base-hover', 'relative flex h-full')}>
|
||||
<div className={cn(className, 'hover:cursor-text hover:bg-state-base-hover', 'relative flex !h-[30px] items-center')}>
|
||||
{(!readOnly)
|
||||
? (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'bg-components-input-bg-active' : 'bg-width', 'w-0 grow px-3 py-1')}
|
||||
className={cn(isFocus ? 'bg-components-input-bg-active' : 'bg-width', 'h-full w-0 grow px-3 py-1')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
|
|
@ -79,13 +79,13 @@ const InputItem: FC<Props> = ({
|
|||
)
|
||||
: (
|
||||
<div
|
||||
className="h-[18px] w-full pl-0.5 leading-[18px]"
|
||||
className="h-full w-full pl-0.5 leading-[18px]"
|
||||
>
|
||||
{!hasValue && <div className="text-xs font-normal text-text-quaternary">{placeholder}</div>}
|
||||
{hasValue && (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'w-0 grow rounded-lg border px-3 py-[6px]')}
|
||||
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'h-full w-0 grow rounded-lg border px-3 py-[6px]')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
|
|
|
|||
|
|
@ -158,10 +158,32 @@ We have a list of languages that we support in the `languages.ts` file. But some
|
|||
|
||||
## Utility scripts
|
||||
|
||||
- Auto-fill translations: `pnpm run i18n:gen --file app common --lang zh-Hans ja-JP [--dry-run]`
|
||||
- Use space-separated values; repeat `--file` / `--lang` as needed. Defaults to all en-US files and all supported locales except en-US.
|
||||
- Protects placeholders (`{{var}}`, `${var}`, `<tag>`) before translation and restores them after.
|
||||
- Check missing/extra keys: `pnpm run i18n:check --file app billing --lang zh-Hans [--auto-remove]`
|
||||
- Use space-separated values; repeat `--file` / `--lang` as needed. Returns non-zero on missing/extra keys; `--auto-remove` deletes extra keys automatically.
|
||||
|
||||
Workflows: `.github/workflows/translate-i18n-base-on-english.yml` auto-runs the translation generator on `web/i18n/en-US/*.json` changes to main. `i18n:check` is a manual script (not run in CI).
|
||||
## Automatic Translation
|
||||
|
||||
Translation is handled automatically by Claude Code GitHub Actions. When changes are pushed to `web/i18n/en-US/*.json` on the main branch:
|
||||
|
||||
1. Claude Code analyzes the git diff to detect changes
|
||||
1. Identifies three types of changes:
|
||||
- **ADD**: New keys that need translation
|
||||
- **UPDATE**: Modified keys that need re-translation (even if target language has existing translation)
|
||||
- **DELETE**: Removed keys that need to be deleted from other languages
|
||||
1. Runs `i18n:check` to verify the initial sync status.
|
||||
1. Translates missing/updated keys while preserving placeholders (`{{var}}`, `${var}`, `<tag>`) and removes deleted keys.
|
||||
1. Runs `lint:fix` to sort JSON keys and `i18n:check` again to ensure everything is synchronized.
|
||||
1. Creates a PR with the translations.
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
To manually trigger translation:
|
||||
|
||||
1. Go to Actions > "Translate i18n Files with Claude Code"
|
||||
1. Click "Run workflow"
|
||||
1. Optionally configure:
|
||||
- **files**: Specific files to translate (space-separated, e.g., "app common")
|
||||
- **languages**: Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP")
|
||||
- **mode**: `incremental` (default, only changes) or `full` (check all keys)
|
||||
|
||||
Workflow: `.github/workflows/translate-i18n-claude.yml`
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@
|
|||
"gen-icons": "node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/",
|
||||
"uglify-embed": "node ./bin/uglify-embed",
|
||||
"i18n:check": "tsx ./scripts/check-i18n.js",
|
||||
"i18n:gen": "tsx ./scripts/auto-gen-i18n.js",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:watch": "vitest --watch",
|
||||
|
|
@ -196,7 +195,6 @@
|
|||
"@vitest/coverage-v8": "4.0.16",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.0",
|
||||
"bing-translate-api": "^4.1.0",
|
||||
"code-inspector-plugin": "1.2.9",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
|
|
|
|||
|
|
@ -481,9 +481,6 @@ importers:
|
|||
babel-loader:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||
bing-translate-api:
|
||||
specifier: ^4.1.0
|
||||
version: 4.2.0
|
||||
code-inspector-plugin:
|
||||
specifier: 1.2.9
|
||||
version: 1.2.9
|
||||
|
|
@ -3160,10 +3157,6 @@ packages:
|
|||
resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sindresorhus/is@4.6.0':
|
||||
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@solid-primitives/event-listener@2.4.3':
|
||||
resolution: {integrity: sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==}
|
||||
peerDependencies:
|
||||
|
|
@ -3324,10 +3317,6 @@ packages:
|
|||
'@swc/helpers@0.5.17':
|
||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||
|
||||
'@szmarczak/http-timer@4.0.6':
|
||||
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@tailwindcss/typography@0.5.19':
|
||||
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
|
||||
peerDependencies:
|
||||
|
|
@ -3511,9 +3500,6 @@ packages:
|
|||
'@types/babel__traverse@7.28.0':
|
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||
|
||||
'@types/cacheable-request@6.0.3':
|
||||
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
|
|
@ -3652,9 +3638,6 @@ packages:
|
|||
'@types/html-minifier-terser@6.1.0':
|
||||
resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
|
||||
|
||||
'@types/http-cache-semantics@4.0.4':
|
||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||
|
||||
'@types/js-cookie@3.0.6':
|
||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||
|
||||
|
|
@ -3667,9 +3650,6 @@ packages:
|
|||
'@types/katex@0.16.7':
|
||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||
|
||||
'@types/keyv@3.1.4':
|
||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||
|
||||
|
|
@ -3732,9 +3712,6 @@ packages:
|
|||
'@types/resolve@1.20.6':
|
||||
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
|
||||
|
||||
'@types/responselike@1.0.3':
|
||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
||||
|
||||
'@types/semver@7.7.1':
|
||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||
|
||||
|
|
@ -4338,9 +4315,6 @@ packages:
|
|||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
bing-translate-api@4.2.0:
|
||||
resolution: {integrity: sha512-7a9yo1NbGcHPS8zXTdz8tCOymHZp2pvCuYOChCaXKjOX8EIwdV3SLd4D7RGIqZt1UhffypYBUcAV2gDcTgK0rA==}
|
||||
|
||||
bippy@0.3.34:
|
||||
resolution: {integrity: sha512-vmptmU/20UdIWHHhq7qCSHhHzK7Ro3YJ1utU0fBG7ujUc58LEfTtilKxcF0IOgSjT5XLcm7CBzDjbv4lcKApGQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -4427,14 +4401,6 @@ packages:
|
|||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
cacheable-lookup@5.0.4:
|
||||
resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==}
|
||||
engines: {node: '>=10.6.0'}
|
||||
|
||||
cacheable-request@7.0.4:
|
||||
resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
callsites@3.1.0:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -4588,9 +4554,6 @@ packages:
|
|||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
clone-response@1.0.3:
|
||||
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
|
||||
|
||||
clsx@1.2.1:
|
||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -4996,10 +4959,6 @@ packages:
|
|||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
defer-to-connect@2.0.1:
|
||||
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
define-lazy-prop@2.0.0:
|
||||
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -5712,10 +5671,6 @@ packages:
|
|||
get-own-enumerable-property-symbols@3.0.2:
|
||||
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
|
||||
|
||||
get-stream@5.2.0:
|
||||
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
get-stream@8.0.1:
|
||||
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||
engines: {node: '>=16'}
|
||||
|
|
@ -5772,10 +5727,6 @@ packages:
|
|||
peerDependencies:
|
||||
csstype: ^3.0.10
|
||||
|
||||
got@11.8.6:
|
||||
resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
|
||||
engines: {node: '>=10.19.0'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
|
|
@ -5920,17 +5871,10 @@ packages:
|
|||
htmlparser2@6.1.0:
|
||||
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
|
||||
|
||||
http-cache-semantics@4.2.0:
|
||||
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
http2-wrapper@1.0.3:
|
||||
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
|
||||
engines: {node: '>=10.19.0'}
|
||||
|
||||
https-browserify@1.0.0:
|
||||
resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==}
|
||||
|
||||
|
|
@ -6453,10 +6397,6 @@ packages:
|
|||
lower-case@2.0.2:
|
||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||
|
||||
lowercase-keys@2.0.0:
|
||||
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
lowlight@1.20.0:
|
||||
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
|
||||
|
||||
|
|
@ -6729,10 +6669,6 @@ packages:
|
|||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mimic-response@1.0.1:
|
||||
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -6880,10 +6816,6 @@ packages:
|
|||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
normalize-url@6.1.0:
|
||||
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
normalize-wheel@1.0.1:
|
||||
resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==}
|
||||
|
||||
|
|
@ -6964,10 +6896,6 @@ packages:
|
|||
oxc-resolver@11.15.0:
|
||||
resolution: {integrity: sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==}
|
||||
|
||||
p-cancelable@2.1.1:
|
||||
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -7359,10 +7287,6 @@ packages:
|
|||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
quick-lru@5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
|
|
@ -7705,9 +7629,6 @@ packages:
|
|||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve-alpn@1.2.1:
|
||||
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -7724,9 +7645,6 @@ packages:
|
|||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
responselike@2.0.1:
|
||||
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -11717,8 +11635,6 @@ snapshots:
|
|||
|
||||
'@sindresorhus/base62@1.0.0': {}
|
||||
|
||||
'@sindresorhus/is@4.6.0': {}
|
||||
|
||||
'@solid-primitives/event-listener@2.4.3(solid-js@1.9.10)':
|
||||
dependencies:
|
||||
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
|
||||
|
|
@ -11971,10 +11887,6 @@ snapshots:
|
|||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@szmarczak/http-timer@4.0.6':
|
||||
dependencies:
|
||||
defer-to-connect: 2.0.1
|
||||
|
||||
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
postcss-selector-parser: 6.0.10
|
||||
|
|
@ -12199,13 +12111,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@types/cacheable-request@6.0.3':
|
||||
dependencies:
|
||||
'@types/http-cache-semantics': 4.0.4
|
||||
'@types/keyv': 3.1.4
|
||||
'@types/node': 18.15.0
|
||||
'@types/responselike': 1.0.3
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
|
|
@ -12373,8 +12278,6 @@ snapshots:
|
|||
|
||||
'@types/html-minifier-terser@6.1.0': {}
|
||||
|
||||
'@types/http-cache-semantics@4.0.4': {}
|
||||
|
||||
'@types/js-cookie@3.0.6': {}
|
||||
|
||||
'@types/js-yaml@4.0.9': {}
|
||||
|
|
@ -12383,10 +12286,6 @@ snapshots:
|
|||
|
||||
'@types/katex@0.16.7': {}
|
||||
|
||||
'@types/keyv@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 18.15.0
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
|
@ -12450,10 +12349,6 @@ snapshots:
|
|||
|
||||
'@types/resolve@1.20.6': {}
|
||||
|
||||
'@types/responselike@1.0.3':
|
||||
dependencies:
|
||||
'@types/node': 18.15.0
|
||||
|
||||
'@types/semver@7.7.1': {}
|
||||
|
||||
'@types/sortablejs@1.15.9': {}
|
||||
|
|
@ -13160,10 +13055,6 @@ snapshots:
|
|||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bing-translate-api@4.2.0:
|
||||
dependencies:
|
||||
got: 11.8.6
|
||||
|
||||
bippy@0.3.34(@types/react@19.2.7)(react@19.2.3):
|
||||
dependencies:
|
||||
'@types/react-reconciler': 0.28.9(@types/react@19.2.7)
|
||||
|
|
@ -13273,18 +13164,6 @@ snapshots:
|
|||
|
||||
cac@6.7.14: {}
|
||||
|
||||
cacheable-lookup@5.0.4: {}
|
||||
|
||||
cacheable-request@7.0.4:
|
||||
dependencies:
|
||||
clone-response: 1.0.3
|
||||
get-stream: 5.2.0
|
||||
http-cache-semantics: 4.2.0
|
||||
keyv: 4.5.4
|
||||
lowercase-keys: 2.0.0
|
||||
normalize-url: 6.1.0
|
||||
responselike: 2.0.1
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
camel-case@4.1.2:
|
||||
|
|
@ -13425,10 +13304,6 @@ snapshots:
|
|||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
clone-response@1.0.3:
|
||||
dependencies:
|
||||
mimic-response: 1.0.1
|
||||
|
||||
clsx@1.2.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
|
@ -13859,6 +13734,7 @@ snapshots:
|
|||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
optional: true
|
||||
|
||||
dedent@0.7.0: {}
|
||||
|
||||
|
|
@ -13871,8 +13747,6 @@ snapshots:
|
|||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
defer-to-connect@2.0.1: {}
|
||||
|
||||
define-lazy-prop@2.0.0: {}
|
||||
|
||||
del@4.1.1:
|
||||
|
|
@ -14013,6 +13887,7 @@ snapshots:
|
|||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
optional: true
|
||||
|
||||
endent@2.1.0:
|
||||
dependencies:
|
||||
|
|
@ -14799,10 +14674,6 @@ snapshots:
|
|||
|
||||
get-own-enumerable-property-symbols@3.0.2: {}
|
||||
|
||||
get-stream@5.2.0:
|
||||
dependencies:
|
||||
pump: 3.0.3
|
||||
|
||||
get-stream@8.0.1: {}
|
||||
|
||||
get-tsconfig@4.13.0:
|
||||
|
|
@ -14862,20 +14733,6 @@ snapshots:
|
|||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
got@11.8.6:
|
||||
dependencies:
|
||||
'@sindresorhus/is': 4.6.0
|
||||
'@szmarczak/http-timer': 4.0.6
|
||||
'@types/cacheable-request': 6.0.3
|
||||
'@types/responselike': 1.0.3
|
||||
cacheable-lookup: 5.0.4
|
||||
cacheable-request: 7.0.4
|
||||
decompress-response: 6.0.0
|
||||
http2-wrapper: 1.0.3
|
||||
lowercase-keys: 2.0.0
|
||||
p-cancelable: 2.1.1
|
||||
responselike: 2.0.1
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
graphemer@1.4.0: {}
|
||||
|
|
@ -15120,8 +14977,6 @@ snapshots:
|
|||
domutils: 2.8.0
|
||||
entities: 2.2.0
|
||||
|
||||
http-cache-semantics@4.2.0: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
|
|
@ -15129,11 +14984,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http2-wrapper@1.0.3:
|
||||
dependencies:
|
||||
quick-lru: 5.1.1
|
||||
resolve-alpn: 1.2.1
|
||||
|
||||
https-browserify@1.0.0: {}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
|
|
@ -15604,8 +15454,6 @@ snapshots:
|
|||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
lowercase-keys@2.0.0: {}
|
||||
|
||||
lowlight@1.20.0:
|
||||
dependencies:
|
||||
fault: 1.0.4
|
||||
|
|
@ -16186,9 +16034,8 @@ snapshots:
|
|||
|
||||
mimic-function@5.0.1: {}
|
||||
|
||||
mimic-response@1.0.1: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
mimic-response@3.1.0:
|
||||
optional: true
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
|
|
@ -16360,8 +16207,6 @@ snapshots:
|
|||
|
||||
normalize-range@0.1.2: {}
|
||||
|
||||
normalize-url@6.1.0: {}
|
||||
|
||||
normalize-wheel@1.0.1: {}
|
||||
|
||||
npm-run-path@5.3.0:
|
||||
|
|
@ -16445,8 +16290,6 @@ snapshots:
|
|||
'@oxc-resolver/binding-win32-ia32-msvc': 11.15.0
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.15.0
|
||||
|
||||
p-cancelable@2.1.1: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
|
@ -16809,6 +16652,7 @@ snapshots:
|
|||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
optional: true
|
||||
|
||||
punycode@1.4.1: {}
|
||||
|
||||
|
|
@ -16828,8 +16672,6 @@ snapshots:
|
|||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quick-lru@5.1.1: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
|
@ -17296,8 +17138,6 @@ snapshots:
|
|||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-alpn@1.2.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
|
@ -17316,10 +17156,6 @@ snapshots:
|
|||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
responselike@2.0.1:
|
||||
dependencies:
|
||||
lowercase-keys: 2.0.0
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
dependencies:
|
||||
onetime: 7.0.0
|
||||
|
|
|
|||
|
|
@ -1,336 +0,0 @@
|
|||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { translate } from 'bing-translate-api'
|
||||
import data from '../i18n-config/languages'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const targetLanguage = 'en-US'
|
||||
const i18nFolder = '../i18n' // Path to i18n folder relative to this script
|
||||
// https://github.com/plainheart/bing-translate-api/blob/master/src/met/lang.json
|
||||
const languageKeyMap = data.languages.reduce((map, language) => {
|
||||
if (language.supported) {
|
||||
if (language.value === 'zh-Hans' || language.value === 'zh-Hant')
|
||||
map[language.value] = language.value
|
||||
else
|
||||
map[language.value] = language.value.split('-')[0]
|
||||
}
|
||||
|
||||
return map
|
||||
}, {})
|
||||
|
||||
const supportedLanguages = Object.keys(languageKeyMap)
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
files: [],
|
||||
languages: [],
|
||||
isDryRun: false,
|
||||
help: false,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
const collectValues = (startIndex) => {
|
||||
const values = []
|
||||
let cursor = startIndex + 1
|
||||
while (cursor < argv.length && !argv[cursor].startsWith('--')) {
|
||||
const value = argv[cursor].trim()
|
||||
if (value)
|
||||
values.push(value)
|
||||
cursor++
|
||||
}
|
||||
return { values, nextIndex: cursor - 1 }
|
||||
}
|
||||
|
||||
const validateList = (values, flag) => {
|
||||
if (!values.length) {
|
||||
args.errors.push(`${flag} requires at least one value. Example: ${flag} app billing`)
|
||||
return false
|
||||
}
|
||||
|
||||
const invalid = values.find(value => value.includes(','))
|
||||
if (invalid) {
|
||||
args.errors.push(`${flag} expects space-separated values. Example: ${flag} app billing`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
for (let index = 2; index < argv.length; index++) {
|
||||
const arg = argv[index]
|
||||
|
||||
if (arg === '--dry-run') {
|
||||
args.isDryRun = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
args.help = true
|
||||
break
|
||||
}
|
||||
|
||||
if (arg.startsWith('--file=')) {
|
||||
args.errors.push('--file expects space-separated values. Example: --file app billing')
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === '--file') {
|
||||
const { values, nextIndex } = collectValues(index)
|
||||
if (validateList(values, '--file'))
|
||||
args.files.push(...values)
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg.startsWith('--lang=')) {
|
||||
args.errors.push('--lang expects space-separated values. Example: --lang zh-Hans ja-JP')
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === '--lang') {
|
||||
const { values, nextIndex } = collectValues(index)
|
||||
if (validateList(values, '--lang'))
|
||||
args.languages.push(...values)
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: pnpm run i18n:gen [options]
|
||||
|
||||
Options:
|
||||
--file <name...> Process only specific files; provide space-separated names and repeat --file if needed
|
||||
--lang <locale> Process only specific locales; provide space-separated locales and repeat --lang if needed (default: all supported except en-US)
|
||||
--dry-run Preview changes without writing files
|
||||
-h, --help Show help
|
||||
|
||||
Examples:
|
||||
pnpm run i18n:gen --file app common --lang zh-Hans ja-JP
|
||||
pnpm run i18n:gen --dry-run
|
||||
`)
|
||||
}
|
||||
|
||||
function protectPlaceholders(text) {
|
||||
const placeholders = []
|
||||
let safeText = text
|
||||
const patterns = [
|
||||
/\{\{[^{}]+\}\}/g, // mustache
|
||||
/\$\{[^{}]+\}/g, // template expressions
|
||||
/<[^>]+>/g, // html-like tags
|
||||
]
|
||||
|
||||
patterns.forEach((pattern) => {
|
||||
safeText = safeText.replace(pattern, (match) => {
|
||||
const token = `__PH_${placeholders.length}__`
|
||||
placeholders.push({ token, value: match })
|
||||
return token
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
safeText,
|
||||
restore(translated) {
|
||||
return placeholders.reduce((result, { token, value }) => result.replace(new RegExp(token, 'g'), value), translated)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function translateText(source, toLanguage) {
|
||||
if (typeof source !== 'string')
|
||||
return { value: source, skipped: false }
|
||||
|
||||
const trimmed = source.trim()
|
||||
if (!trimmed)
|
||||
return { value: source, skipped: false }
|
||||
|
||||
const { safeText, restore } = protectPlaceholders(source)
|
||||
|
||||
try {
|
||||
const { translation } = await translate(safeText, null, languageKeyMap[toLanguage])
|
||||
return { value: restore(translation), skipped: false }
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`❌ Error translating to ${toLanguage}:`, error.message)
|
||||
return { value: source, skipped: true, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
async function translateMissingKeys(sourceObj, targetObject, toLanguage) {
|
||||
const skippedKeys = []
|
||||
const translatedKeys = []
|
||||
|
||||
for (const key of Object.keys(sourceObj)) {
|
||||
const sourceValue = sourceObj[key]
|
||||
const targetValue = targetObject[key]
|
||||
|
||||
// Skip if target already has this key
|
||||
if (targetValue !== undefined)
|
||||
continue
|
||||
|
||||
const translationResult = await translateText(sourceValue, toLanguage)
|
||||
targetObject[key] = translationResult.value ?? ''
|
||||
if (translationResult.skipped)
|
||||
skippedKeys.push(`${key}: ${sourceValue}`)
|
||||
else
|
||||
translatedKeys.push(key)
|
||||
}
|
||||
|
||||
return { skipped: skippedKeys, translated: translatedKeys }
|
||||
}
|
||||
async function autoGenTrans(fileName, toGenLanguage, isDryRun = false) {
|
||||
const fullKeyFilePath = path.resolve(__dirname, i18nFolder, targetLanguage, `${fileName}.json`)
|
||||
const toGenLanguageFilePath = path.resolve(__dirname, i18nFolder, toGenLanguage, `${fileName}.json`)
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(fullKeyFilePath, 'utf8')
|
||||
const fullKeyContent = JSON.parse(content)
|
||||
|
||||
if (!fullKeyContent || typeof fullKeyContent !== 'object')
|
||||
throw new Error(`Failed to extract translation object from ${fullKeyFilePath}`)
|
||||
|
||||
// if toGenLanguageFilePath does not exist, create it with empty object
|
||||
let toGenOutPut = {}
|
||||
if (fs.existsSync(toGenLanguageFilePath)) {
|
||||
const existingContent = fs.readFileSync(toGenLanguageFilePath, 'utf8')
|
||||
toGenOutPut = JSON.parse(existingContent)
|
||||
}
|
||||
|
||||
console.log(`\n🌍 Processing ${fileName} for ${toGenLanguage}...`)
|
||||
const result = await translateMissingKeys(fullKeyContent, toGenOutPut, toGenLanguage)
|
||||
|
||||
// Generate summary report
|
||||
console.log(`\n📊 Translation Summary for ${fileName} -> ${toGenLanguage}:`)
|
||||
console.log(` ✅ Translated: ${result.translated.length} keys`)
|
||||
console.log(` ⏭️ Skipped: ${result.skipped.length} keys`)
|
||||
|
||||
if (result.skipped.length > 0) {
|
||||
console.log(`\n⚠️ Skipped keys in ${fileName} (${toGenLanguage}):`)
|
||||
result.skipped.slice(0, 5).forEach(item => console.log(` - ${item}`))
|
||||
if (result.skipped.length > 5)
|
||||
console.log(` ... and ${result.skipped.length - 5} more`)
|
||||
}
|
||||
|
||||
const res = `${JSON.stringify(toGenOutPut, null, 2)}\n`
|
||||
|
||||
if (!isDryRun) {
|
||||
fs.writeFileSync(toGenLanguageFilePath, res)
|
||||
console.log(`💾 Saved translations to ${toGenLanguageFilePath}`)
|
||||
}
|
||||
else {
|
||||
console.log(`🔍 [DRY RUN] Would save translations to ${toGenLanguageFilePath}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error processing file ${fullKeyFilePath}:`, error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Add command line argument support
|
||||
const args = parseArgs(process.argv)
|
||||
const isDryRun = args.isDryRun
|
||||
const targetFiles = args.files
|
||||
const targetLangs = args.languages
|
||||
|
||||
// Rate limiting helper
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.help) {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if (args.errors.length) {
|
||||
args.errors.forEach(message => console.error(`❌ ${message}`))
|
||||
printHelp()
|
||||
process.exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🚀 Starting i18n:gen script...')
|
||||
console.log(`📋 Mode: ${isDryRun ? 'DRY RUN (no files will be modified)' : 'LIVE MODE'}`)
|
||||
|
||||
const filesInEn = fs
|
||||
.readdirSync(path.resolve(__dirname, i18nFolder, targetLanguage))
|
||||
.filter(file => /\.json$/.test(file)) // Only process .json files
|
||||
.map(file => file.replace(/\.json$/, ''))
|
||||
|
||||
// Filter by target files if specified
|
||||
const filesToProcess = targetFiles.length > 0 ? filesInEn.filter(f => targetFiles.includes(f)) : filesInEn
|
||||
const languagesToProcess = Array.from(new Set((targetLangs.length > 0 ? targetLangs : supportedLanguages)
|
||||
.filter(lang => lang !== targetLanguage)))
|
||||
|
||||
const unknownLangs = languagesToProcess.filter(lang => !languageKeyMap[lang])
|
||||
if (unknownLangs.length) {
|
||||
console.error(`❌ Unsupported languages: ${unknownLangs.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!filesToProcess.length) {
|
||||
console.log('ℹ️ No files to process based on provided arguments')
|
||||
return
|
||||
}
|
||||
|
||||
if (!languagesToProcess.length) {
|
||||
console.log('ℹ️ No languages to process (did you only specify en-US?)')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`📁 Files to process: ${filesToProcess.join(', ')}`)
|
||||
console.log(`🌍 Languages to process: ${languagesToProcess.join(', ')}`)
|
||||
|
||||
let totalTranslated = 0
|
||||
let totalSkipped = 0
|
||||
let totalErrors = 0
|
||||
|
||||
// Process files sequentially to avoid API rate limits
|
||||
for (const file of filesToProcess) {
|
||||
console.log(`\n📄 Processing file: ${file}`)
|
||||
|
||||
// Process languages with rate limiting
|
||||
for (const language of languagesToProcess) {
|
||||
try {
|
||||
const result = await autoGenTrans(file, language, isDryRun)
|
||||
totalTranslated += result.translated.length
|
||||
totalSkipped += result.skipped.length
|
||||
|
||||
// Rate limiting: wait 500ms between language processing
|
||||
await delay(500)
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`❌ Error translating ${file} to ${language}:`, e.message)
|
||||
totalErrors++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final summary
|
||||
console.log('\n🎉 Auto-translation completed!')
|
||||
console.log('📊 Final Summary:')
|
||||
console.log(` ✅ Total keys translated: ${totalTranslated}`)
|
||||
console.log(` ⏭️ Total keys skipped: ${totalSkipped}`)
|
||||
console.log(` ❌ Total errors: ${totalErrors}`)
|
||||
|
||||
if (isDryRun)
|
||||
console.log('\n💡 This was a dry run. To actually translate, run without --dry-run flag.')
|
||||
|
||||
if (totalErrors > 0)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Unexpected error:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
Loading…
Reference in New Issue