Merge remote-tracking branch 'origin/main' into feat/migrate-es-toolkit-compat-simple

This commit is contained in:
yyh 2025-12-29 15:36:00 +08:00
commit 91bae1f727
No known key found for this signature in database
2624 changed files with 115253 additions and 145391 deletions

8
.claude/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"enabledPlugins": {
"feature-dev@claude-plugins-official": true,
"context7@claude-plugins-official": true,
"typescript-lsp@claude-plugins-official": true,
"pyright-lsp@claude-plugins-official": true
}
}

View File

@ -1,19 +0,0 @@
{
"permissions": {
"allow": [],
"deny": []
},
"env": {
"__comment": "Environment variables for MCP servers. Override in .claude/settings.local.json with actual values.",
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"enabledMcpjsonServers": [
"context7",
"sequential-thinking",
"github",
"fetch",
"playwright",
"ide"
],
"enableAllProjectMcpServers": true
}

View File

@ -187,7 +187,7 @@ const Template = useMemo(() => {
**When**: Component directly handles API calls, data transformation, or complex async operations.
**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks. Project is migrating from SWR to React Query.
**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks.
```typescript
// ❌ Before: API logic in component

View File

@ -22,12 +22,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
@ -57,7 +57,7 @@ jobs:
run: sh .github/workflows/expose_service_ports.sh
- name: Set up Sandbox
uses: hoverkraft-tech/compose-action@v2.0.2
uses: hoverkraft-tech/compose-action@v2
with:
compose-file: |
docker/docker-compose.middleware.yaml

View File

@ -12,7 +12,7 @@ jobs:
if: github.repository == 'langgenius/dify'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Check Docker Compose inputs
id: docker-compose-changes
@ -27,7 +27,7 @@ jobs:
with:
python-version: "3.11"
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@v7
- name: Generate Docker Compose
if: steps.docker-compose-changes.outputs.any_changed == 'true'

View File

@ -90,7 +90,7 @@ jobs:
touch "/tmp/digests/${sanitized_digest}"
- name: Upload digest
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: digests-${{ matrix.context }}-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*

View File

@ -13,13 +13,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: "3.12"
@ -63,13 +63,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: "3.12"

View File

@ -27,7 +27,7 @@ jobs:
vdb-changed: ${{ steps.changes.outputs.vdb }}
migration-changed: ${{ steps.changes.outputs.migration }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: changes
with:
@ -38,6 +38,7 @@ jobs:
- '.github/workflows/api-tests.yml'
web:
- 'web/**'
- '.github/workflows/web-tests.yml'
vdb:
- 'api/core/rag/datasource/**'
- 'docker/**'

View File

@ -19,13 +19,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@v47
with:
files: |
api/**
@ -33,7 +33,7 @@ jobs:
- name: Setup UV and Python
if: steps.changed-files.outputs.any_changed == 'true'
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
enable-cache: false
python-version: "3.12"
@ -68,15 +68,17 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@v47
with:
files: web/**
files: |
web/**
.github/workflows/style.yml
- name: Install pnpm
uses: pnpm/action-setup@v4
@ -85,7 +87,7 @@ jobs:
run_install: false
- name: Setup NodeJS
uses: actions/setup-node@v4
uses: actions/setup-node@v6
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 22
@ -114,14 +116,14 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@v47
with:
files: |
**.sh

View File

@ -25,12 +25,12 @@ jobs:
working-directory: sdks/nodejs-client
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: ''

View File

@ -4,7 +4,7 @@ on:
push:
branches: [main]
paths:
- 'web/i18n/en-US/*.ts'
- 'web/i18n/en-US/*.json'
permissions:
contents: write
@ -18,7 +18,7 @@ jobs:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@ -28,13 +28,13 @@ jobs:
run: |
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/*.ts')
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" .ts)
filename=$(basename "$file" .json)
file_args="$file_args --file $filename"
done
echo "FILE_ARGS=$file_args" >> $GITHUB_ENV
@ -51,7 +51,7 @@ jobs:
- name: Set up Node.js
if: env.FILES_CHANGED == 'true'
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: pnpm

View File

@ -19,19 +19,19 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Free Disk Space
uses: endersonmenezes/free-disk-space@v2
uses: endersonmenezes/free-disk-space@v3
with:
remove_dotnet: true
remove_haskell: true
remove_tool_cache: true
- name: Setup UV and Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: ${{ matrix.python-version }}

View File

@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
@ -29,7 +29,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
@ -360,7 +360,7 @@ jobs:
- name: Upload Coverage Artifact
if: steps.coverage-summary.outputs.has_coverage == 'true'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: web-coverage-report
path: web/coverage

View File

@ -1,34 +0,0 @@
{
"mcpServers": {
"context7": {
"type": "http",
"url": "https://mcp.context7.com/mcp"
},
"sequential-thinking": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
"env": {}
},
"github": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
},
"fetch": {
"type": "stdio",
"command": "uvx",
"args": ["mcp-server-fetch"],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"],
"env": {}
}
}
}

View File

@ -90,6 +90,7 @@ class AppQueueManager:
"""
self._clear_task_belong_cache()
self._q.put(None)
self._graph_runtime_state = None # Release reference to allow GC to reclaim memory
def _clear_task_belong_cache(self) -> None:
"""

View File

@ -1,9 +1,14 @@
from collections.abc import Mapping
from textwrap import dedent
from typing import Any
from core.helper.code_executor.template_transformer import TemplateTransformer
class Jinja2TemplateTransformer(TemplateTransformer):
# Use separate placeholder for base64-encoded template to avoid confusion
_template_b64_placeholder: str = "{{template_b64}}"
@classmethod
def transform_response(cls, response: str):
"""
@ -13,18 +18,35 @@ class Jinja2TemplateTransformer(TemplateTransformer):
"""
return {"result": cls.extract_result_str_from_response(response)}
@classmethod
def assemble_runner_script(cls, code: str, inputs: Mapping[str, Any]) -> str:
"""
Override base class to use base64 encoding for template code.
This prevents issues with special characters (quotes, newlines) in templates
breaking the generated Python script. Fixes #26818.
"""
script = cls.get_runner_script()
# Encode template as base64 to safely embed any content including quotes
code_b64 = cls.serialize_code(code)
script = script.replace(cls._template_b64_placeholder, code_b64)
inputs_str = cls.serialize_inputs(inputs)
script = script.replace(cls._inputs_placeholder, inputs_str)
return script
@classmethod
def get_runner_script(cls) -> str:
runner_script = dedent(f"""
# declare main function
def main(**inputs):
import jinja2
template = jinja2.Template('''{cls._code_placeholder}''')
return template.render(**inputs)
import jinja2
import json
from base64 import b64decode
# declare main function
def main(**inputs):
# Decode base64-encoded template to handle special characters safely
template_code = b64decode('{cls._template_b64_placeholder}').decode('utf-8')
template = jinja2.Template(template_code)
return template.render(**inputs)
# decode and prepare input dict
inputs_obj = json.loads(b64decode('{cls._inputs_placeholder}').decode('utf-8'))

View File

@ -13,6 +13,15 @@ class TemplateTransformer(ABC):
_inputs_placeholder: str = "{{inputs}}"
_result_tag: str = "<<RESULT>>"
@classmethod
def serialize_code(cls, code: str) -> str:
"""
Serialize template code to base64 to safely embed in generated script.
This prevents issues with special characters like quotes breaking the script.
"""
code_bytes = code.encode("utf-8")
return b64encode(code_bytes).decode("utf-8")
@classmethod
def transform_caller(cls, code: str, inputs: Mapping[str, Any]) -> tuple[str, str]:
"""

View File

@ -313,17 +313,20 @@ class StreamableHTTPTransport:
if is_initialization:
self._maybe_extract_session_id_from_response(response)
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
# Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:
# The server MUST NOT send a response to notifications.
if isinstance(message.root, JSONRPCRequest):
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
if content_type.startswith(JSON):
self._handle_json_response(response, ctx.server_to_client_queue)
elif content_type.startswith(SSE):
self._handle_sse_response(response, ctx)
else:
self._handle_unexpected_content_type(
content_type,
ctx.server_to_client_queue,
)
if content_type.startswith(JSON):
self._handle_json_response(response, ctx.server_to_client_queue)
elif content_type.startswith(SSE):
self._handle_sse_response(response, ctx)
else:
self._handle_unexpected_content_type(
content_type,
ctx.server_to_client_queue,
)
def _handle_json_response(
self,

View File

@ -6,7 +6,15 @@ from typing import Any
from core.mcp.auth_client import MCPClientWithAuthRetry
from core.mcp.error import MCPConnectionError
from core.mcp.types import AudioContent, CallToolResult, ImageContent, TextContent
from core.mcp.types import (
AudioContent,
BlobResourceContents,
CallToolResult,
EmbeddedResource,
ImageContent,
TextContent,
TextResourceContents,
)
from core.tools.__base.tool import Tool
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType
@ -53,10 +61,19 @@ class MCPTool(Tool):
for content in result.content:
if isinstance(content, TextContent):
yield from self._process_text_content(content)
elif isinstance(content, ImageContent):
yield self._process_image_content(content)
elif isinstance(content, AudioContent):
yield self._process_audio_content(content)
elif isinstance(content, ImageContent | AudioContent):
yield self.create_blob_message(
blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType}
)
elif isinstance(content, EmbeddedResource):
resource = content.resource
if isinstance(resource, TextResourceContents):
yield self.create_text_message(resource.text)
elif isinstance(resource, BlobResourceContents):
mime_type = resource.mimeType or "application/octet-stream"
yield self.create_blob_message(blob=base64.b64decode(resource.blob), meta={"mime_type": mime_type})
else:
raise ToolInvokeError(f"Unsupported embedded resource type: {type(resource)}")
else:
logger.warning("Unsupported content type=%s", type(content))
@ -101,14 +118,6 @@ class MCPTool(Tool):
for item in json_list:
yield self.create_json_message(item)
def _process_image_content(self, content: ImageContent) -> ToolInvokeMessage:
"""Process image content and return a blob message."""
return self.create_blob_message(blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType})
def _process_audio_content(self, content: AudioContent) -> ToolInvokeMessage:
"""Process audio content and return a blob message."""
return self.create_blob_message(blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType})
def fork_tool_runtime(self, runtime: ToolRuntime) -> "MCPTool":
return MCPTool(
entity=self.entity,

View File

@ -14,7 +14,8 @@ from enums.quota_type import QuotaType, unlimited
from extensions.otel import AppGenerateHandler, trace_span
from models.model import Account, App, AppMode, EndUser
from models.workflow import Workflow
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.app import QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
from services.workflow_service import WorkflowService

View File

@ -21,7 +21,7 @@ from models.model import App, EndUser
from models.trigger import WorkflowTriggerLog
from models.workflow import Workflow
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowNotFoundError
from services.errors.app import QuotaExceededError, WorkflowNotFoundError, WorkflowQuotaLimitError
from services.workflow.entities import AsyncTriggerResponse, TriggerData, WorkflowTaskData
from services.workflow.queue_dispatcher import QueueDispatcherManager, QueuePriority
from services.workflow_service import WorkflowService
@ -141,7 +141,7 @@ class AsyncWorkflowService:
trigger_log_repo.update(trigger_log)
session.commit()
raise InvokeRateLimitError(
raise WorkflowQuotaLimitError(
f"Workflow execution quota limit reached for tenant {trigger_data.tenant_id}"
) from e

View File

@ -18,8 +18,8 @@ class WorkflowIdFormatError(Exception):
pass
class InvokeRateLimitError(Exception):
"""Raised when rate limit is exceeded for workflow invocations."""
class WorkflowQuotaLimitError(Exception):
"""Raised when workflow execution quota is exceeded (for async/background workflows)."""
pass

View File

@ -146,7 +146,7 @@ class PluginParameterService:
provider,
action,
resolved_credentials,
CredentialType.API_KEY.value,
original_subscription.credential_type or CredentialType.UNAUTHORIZED.value,
parameter,
)
.options

View File

@ -868,48 +868,111 @@ class TriggerProviderService:
if not provider_controller:
raise ValueError(f"Provider {provider_id} not found")
subscription = TriggerProviderService.get_subscription_by_id(
tenant_id=tenant_id,
subscription_id=subscription_id,
)
if not subscription:
raise ValueError(f"Subscription {subscription_id} not found")
# Use distributed lock to prevent race conditions on the same subscription
lock_key = f"trigger_subscription_rebuild_lock:{tenant_id}_{subscription_id}"
with redis_client.lock(lock_key, timeout=20):
with Session(db.engine, expire_on_commit=False) as session:
try:
# Get subscription within the transaction
subscription: TriggerSubscription | None = (
session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first()
)
if not subscription:
raise ValueError(f"Subscription {subscription_id} not found")
credential_type = CredentialType.of(subscription.credential_type)
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
raise ValueError("Credential type not supported for rebuild")
credential_type = CredentialType.of(subscription.credential_type)
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
raise ValueError("Credential type not supported for rebuild")
# TODO: Trying to invoke update api of the plugin trigger provider
# Decrypt existing credentials for merging
credential_encrypter, _ = create_trigger_provider_encrypter_for_subscription(
tenant_id=tenant_id,
controller=provider_controller,
subscription=subscription,
)
decrypted_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
# FALLBACK: If the update api is not implemented, delete the previous subscription and create a new one
# Merge credentials: if caller passed HIDDEN_VALUE, retain existing decrypted value
merged_credentials: dict[str, Any] = {
key: value if value != HIDDEN_VALUE else decrypted_credentials.get(key, UNKNOWN_VALUE)
for key, value in credentials.items()
}
# Delete the previous subscription
user_id = subscription.user_id
TriggerManager.unsubscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
subscription=subscription.to_entity(),
credentials=subscription.credentials,
credential_type=credential_type,
)
user_id = subscription.user_id
# Create a new subscription with the same subscription_id and endpoint_id
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
parameters=parameters,
credentials=credentials,
credential_type=credential_type,
)
TriggerProviderService.update_trigger_subscription(
tenant_id=tenant_id,
subscription_id=subscription.id,
name=name,
parameters=parameters,
credentials=credentials,
properties=new_subscription.properties,
expires_at=new_subscription.expires_at,
)
# TODO: Trying to invoke update api of the plugin trigger provider
# FALLBACK: If the update api is not implemented,
# delete the previous subscription and create a new one
# Unsubscribe the previous subscription (external call, but we'll handle errors)
try:
TriggerManager.unsubscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
subscription=subscription.to_entity(),
credentials=decrypted_credentials,
credential_type=credential_type,
)
except Exception as e:
logger.exception("Error unsubscribing trigger during rebuild", exc_info=e)
# Continue anyway - the subscription might already be deleted externally
# Create a new subscription with the same subscription_id and endpoint_id (external call)
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
parameters=parameters,
credentials=merged_credentials,
credential_type=credential_type,
)
# Update the subscription in the same transaction
# Inline update logic to reuse the same session
if name is not None and name != subscription.name:
existing = (
session.query(TriggerSubscription)
.filter_by(tenant_id=tenant_id, provider_id=str(provider_id), name=name)
.first()
)
if existing and existing.id != subscription.id:
raise ValueError(f"Subscription name '{name}' already exists for this provider")
subscription.name = name
# Update parameters
subscription.parameters = dict(parameters)
# Update credentials with merged (and encrypted) values
subscription.credentials = dict(credential_encrypter.encrypt(merged_credentials))
# Update properties
if new_subscription.properties:
properties_encrypter, _ = create_provider_encrypter(
tenant_id=tenant_id,
config=provider_controller.get_properties_schema(),
cache=NoOpProviderCredentialCache(),
)
subscription.properties = dict(properties_encrypter.encrypt(dict(new_subscription.properties)))
# Update expiration timestamp
if new_subscription.expires_at is not None:
subscription.expires_at = new_subscription.expires_at
# Commit the transaction
session.commit()
# Clear subscription cache
delete_cache_for_subscription(
tenant_id=tenant_id,
provider_id=subscription.provider_id,
subscription_id=subscription.id,
)
except Exception as e:
# Rollback on any error
session.rollback()
logger.exception("Failed to rebuild trigger subscription", exc_info=e)
raise

View File

@ -863,10 +863,18 @@ class WebhookService:
not_found_in_cache.append(node_id)
continue
with Session(db.engine) as session:
try:
# lock the concurrent webhook trigger creation
redis_client.lock(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock", timeout=10)
lock_key = f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock"
lock = redis_client.lock(lock_key, timeout=10)
lock_acquired = False
try:
# acquire the lock with blocking and timeout
lock_acquired = lock.acquire(blocking=True, blocking_timeout=10)
if not lock_acquired:
logger.warning("Failed to acquire lock for webhook sync, app %s", app.id)
raise RuntimeError("Failed to acquire lock for webhook trigger synchronization")
with Session(db.engine) as session:
# fetch the non-cached nodes from DB
all_records = session.scalars(
select(WorkflowWebhookTrigger).where(
@ -903,11 +911,16 @@ class WebhookService:
session.delete(nodes_id_in_db[node_id])
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:{app.id}:{node_id}")
session.commit()
except Exception:
logger.exception("Failed to sync webhook relationships for app %s", app.id)
raise
finally:
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock")
except Exception:
logger.exception("Failed to sync webhook relationships for app %s", app.id)
raise
finally:
# release the lock only if it was acquired
if lock_acquired:
try:
lock.release()
except Exception:
logger.exception("Failed to release lock for webhook sync, app %s", app.id)
@classmethod
def generate_webhook_id(cls) -> str:

View File

@ -7,11 +7,14 @@ CODE_LANGUAGE = CodeLanguage.JINJA2
def test_jinja2():
"""Test basic Jinja2 template rendering."""
template = "Hello {{template}}"
# Template must be base64 encoded to match the new safe embedding approach
template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8")
inputs = base64.b64encode(b'{"template": "World"}').decode("utf-8")
code = (
Jinja2TemplateTransformer.get_runner_script()
.replace(Jinja2TemplateTransformer._code_placeholder, template)
.replace(Jinja2TemplateTransformer._template_b64_placeholder, template_b64)
.replace(Jinja2TemplateTransformer._inputs_placeholder, inputs)
)
result = CodeExecutor.execute_code(
@ -21,6 +24,7 @@ def test_jinja2():
def test_jinja2_with_code_template():
"""Test template rendering via the high-level workflow API."""
result = CodeExecutor.execute_workflow_code_template(
language=CODE_LANGUAGE, code="Hello {{template}}", inputs={"template": "World"}
)
@ -28,7 +32,64 @@ def test_jinja2_with_code_template():
def test_jinja2_get_runner_script():
"""Test that runner script contains required placeholders."""
runner_script = Jinja2TemplateTransformer.get_runner_script()
assert runner_script.count(Jinja2TemplateTransformer._code_placeholder) == 1
assert runner_script.count(Jinja2TemplateTransformer._template_b64_placeholder) == 1
assert runner_script.count(Jinja2TemplateTransformer._inputs_placeholder) == 1
assert runner_script.count(Jinja2TemplateTransformer._result_tag) == 2
def test_jinja2_template_with_special_characters():
"""
Test that templates with special characters (quotes, newlines) render correctly.
This is a regression test for issue #26818 where textarea pre-fill values
containing special characters would break template rendering.
"""
# Template with triple quotes, single quotes, double quotes, and newlines
template = """<html>
<body>
<input value="{{ task.get('Task ID', '') }}"/>
<textarea>{{ task.get('Issues', 'No issues reported') }}</textarea>
<p>Status: "{{ status }}"</p>
<pre>'''code block'''</pre>
</body>
</html>"""
inputs = {"task": {"Task ID": "TASK-123", "Issues": "Line 1\nLine 2\nLine 3"}, "status": "completed"}
result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs)
# Verify the template rendered correctly with all special characters
output = result["result"]
assert 'value="TASK-123"' in output
assert "<textarea>Line 1\nLine 2\nLine 3</textarea>" in output
assert 'Status: "completed"' in output
assert "'''code block'''" in output
def test_jinja2_template_with_html_textarea_prefill():
"""
Specific test for HTML textarea with Jinja2 variable pre-fill.
Verifies fix for issue #26818.
"""
template = "<textarea name='notes'>{{ notes }}</textarea>"
notes_content = "This is a multi-line note.\nWith special chars: 'single' and \"double\" quotes."
inputs = {"notes": notes_content}
result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs)
expected_output = f"<textarea name='notes'>{notes_content}</textarea>"
assert result["result"] == expected_output
def test_jinja2_assemble_runner_script_encodes_template():
"""Test that assemble_runner_script properly base64 encodes the template."""
template = "Hello {{ name }}!"
inputs = {"name": "World"}
script = Jinja2TemplateTransformer.assemble_runner_script(template, inputs)
# The template should be base64 encoded in the script
template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8")
assert template_b64 in script
# The raw template should NOT appear in the script (it's encoded)
assert "Hello {{ name }}!" not in script

View File

@ -0,0 +1,682 @@
from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from constants import HIDDEN_VALUE, UNKNOWN_VALUE
from core.plugin.entities.plugin_daemon import CredentialType
from core.trigger.entities.entities import Subscription as TriggerSubscriptionEntity
from extensions.ext_database import db
from models.provider_ids import TriggerProviderID
from models.trigger import TriggerSubscription
from services.trigger.trigger_provider_service import TriggerProviderService
class TestTriggerProviderService:
"""Integration tests for TriggerProviderService using testcontainers."""
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.trigger.trigger_provider_service.TriggerManager") as mock_trigger_manager,
patch("services.trigger.trigger_provider_service.redis_client") as mock_redis_client,
patch("services.trigger.trigger_provider_service.delete_cache_for_subscription") as mock_delete_cache,
patch("services.account_service.FeatureService") as mock_account_feature_service,
):
# Setup default mock returns
mock_provider_controller = MagicMock()
mock_provider_controller.get_credential_schema_config.return_value = MagicMock()
mock_provider_controller.get_properties_schema.return_value = MagicMock()
mock_trigger_manager.get_trigger_provider.return_value = mock_provider_controller
# Mock redis lock
mock_lock = MagicMock()
mock_lock.__enter__ = MagicMock(return_value=None)
mock_lock.__exit__ = MagicMock(return_value=None)
mock_redis_client.lock.return_value = mock_lock
# Setup account feature service mock
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
yield {
"trigger_manager": mock_trigger_manager,
"redis_client": mock_redis_client,
"delete_cache": mock_delete_cache,
"provider_controller": mock_provider_controller,
"account_feature_service": mock_account_feature_service,
}
def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies):
"""
Helper method to create a test account and tenant for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
mock_external_service_dependencies: Mock dependencies
Returns:
tuple: (account, tenant) - Created account and tenant instances
"""
fake = Faker()
from services.account_service import AccountService, TenantService
# Setup mocks for account creation
mock_external_service_dependencies[
"account_feature_service"
].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies[
"trigger_manager"
].get_trigger_provider.return_value = mock_external_service_dependencies["provider_controller"]
# Create account and tenant
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=fake.password(length=12),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
return account, tenant
def _create_test_subscription(
self,
db_session_with_containers,
tenant_id,
user_id,
provider_id,
credential_type,
credentials,
mock_external_service_dependencies,
):
"""
Helper method to create a test trigger subscription.
Args:
db_session_with_containers: Database session
tenant_id: Tenant ID
user_id: User ID
provider_id: Provider ID
credential_type: Credential type
credentials: Credentials dict
mock_external_service_dependencies: Mock dependencies
Returns:
TriggerSubscription: Created subscription instance
"""
fake = Faker()
from core.helper.provider_cache import NoOpProviderCredentialCache
from core.helper.provider_encryption import create_provider_encrypter
# Use mock provider controller to encrypt credentials
provider_controller = mock_external_service_dependencies["provider_controller"]
# Create encrypter for credentials
credential_encrypter, _ = create_provider_encrypter(
tenant_id=tenant_id,
config=provider_controller.get_credential_schema_config(credential_type),
cache=NoOpProviderCredentialCache(),
)
subscription = TriggerSubscription(
name=fake.word(),
tenant_id=tenant_id,
user_id=user_id,
provider_id=str(provider_id),
endpoint_id=fake.uuid4(),
parameters={"param1": "value1"},
properties={"prop1": "value1"},
credentials=dict(credential_encrypter.encrypt(credentials)),
credential_type=credential_type.value,
credential_expires_at=-1,
expires_at=-1,
)
db.session.add(subscription)
db.session.commit()
db.session.refresh(subscription)
return subscription
def test_rebuild_trigger_subscription_success_with_merged_credentials(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful rebuild with credential merging (HIDDEN_VALUE handling).
This test verifies:
- Credentials are properly merged (HIDDEN_VALUE replaced with existing values)
- Single transaction wraps all operations
- Merged credentials are used for subscribe and update
- Database state is correctly updated
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Create initial subscription with credentials
original_credentials = {"api_key": "original-secret-key", "api_secret": "original-secret"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# Prepare new credentials with HIDDEN_VALUE for api_key (should keep original)
# and new value for api_secret (should update)
new_credentials = {
"api_key": HIDDEN_VALUE, # Should be replaced with original
"api_secret": "new-secret-value", # Should be updated
}
# Mock subscribe_trigger to return a new subscription entity
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={"param1": "value1"},
properties={"prop1": "new_prop_value"},
expires_at=1234567890,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
# Mock unsubscribe_trigger
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={"param1": "updated_value"},
name="updated_name",
)
# Verify unsubscribe was called with decrypted original credentials
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.assert_called_once()
unsubscribe_call_args = mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.call_args
assert unsubscribe_call_args.kwargs["tenant_id"] == tenant.id
assert unsubscribe_call_args.kwargs["provider_id"] == provider_id
assert unsubscribe_call_args.kwargs["credential_type"] == credential_type
# Verify subscribe was called with merged credentials (api_key from original, api_secret new)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == original_credentials["api_key"] # Merged from original
assert subscribe_credentials["api_secret"] == "new-secret-value" # New value
# Verify database state was updated
db.session.refresh(subscription)
assert subscription.name == "updated_name"
assert subscription.parameters == {"param1": "updated_value"}
# Verify credentials in DB were updated with merged values (decrypt to check)
from core.helper.provider_cache import NoOpProviderCredentialCache
from core.helper.provider_encryption import create_provider_encrypter
# Use mock provider controller to decrypt credentials
provider_controller = mock_external_service_dependencies["provider_controller"]
credential_encrypter, _ = create_provider_encrypter(
tenant_id=tenant.id,
config=provider_controller.get_credential_schema_config(credential_type),
cache=NoOpProviderCredentialCache(),
)
decrypted_db_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
assert decrypted_db_credentials["api_key"] == original_credentials["api_key"]
assert decrypted_db_credentials["api_secret"] == "new-secret-value"
# Verify cache was cleared
mock_external_service_dependencies["delete_cache"].assert_called_once_with(
tenant_id=tenant.id,
provider_id=subscription.provider_id,
subscription_id=subscription.id,
)
def test_rebuild_trigger_subscription_with_all_new_credentials(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test rebuild when all credentials are new (no HIDDEN_VALUE).
This test verifies:
- All new credentials are used when no HIDDEN_VALUE is present
- Merged credentials contain only new values
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Create initial subscription
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# All new credentials (no HIDDEN_VALUE)
new_credentials = {
"api_key": "completely-new-key",
"api_secret": "completely-new-secret",
}
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={},
)
# Verify subscribe was called with all new credentials
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == "completely-new-key"
assert subscribe_credentials["api_secret"] == "completely-new-secret"
def test_rebuild_trigger_subscription_with_all_hidden_values(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test rebuild when all credentials are HIDDEN_VALUE (preserve all existing).
This test verifies:
- All HIDDEN_VALUE credentials are replaced with existing values
- Original credentials are preserved
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# All HIDDEN_VALUE (should preserve all original)
new_credentials = {
"api_key": HIDDEN_VALUE,
"api_secret": HIDDEN_VALUE,
}
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={},
)
# Verify subscribe was called with all original credentials
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
assert subscribe_credentials["api_secret"] == original_credentials["api_secret"]
def test_rebuild_trigger_subscription_with_missing_key_uses_unknown_value(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test rebuild when HIDDEN_VALUE is used for a key that doesn't exist in original.
This test verifies:
- UNKNOWN_VALUE is used when HIDDEN_VALUE key doesn't exist in original credentials
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Original has only api_key
original_credentials = {"api_key": "original-key"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# HIDDEN_VALUE for non-existent key should use UNKNOWN_VALUE
new_credentials = {
"api_key": HIDDEN_VALUE,
"non_existent_key": HIDDEN_VALUE, # This key doesn't exist in original
}
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={},
)
# Verify subscribe was called with original api_key and UNKNOWN_VALUE for missing key
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
assert subscribe_credentials["non_existent_key"] == UNKNOWN_VALUE
def test_rebuild_trigger_subscription_rollback_on_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that transaction is rolled back on error.
This test verifies:
- Database transaction is rolled back when an error occurs
- Original subscription state is preserved
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
original_credentials = {"api_key": "original-key"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
original_name = subscription.name
original_parameters = subscription.parameters.copy()
# Make subscribe_trigger raise an error
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.side_effect = ValueError(
"Subscribe failed"
)
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild and expect error
with pytest.raises(ValueError, match="Subscribe failed"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials={"api_key": "new-key"},
parameters={},
)
# Verify subscription state was not changed (rolled back)
db.session.refresh(subscription)
assert subscription.name == original_name
assert subscription.parameters == original_parameters
def test_rebuild_trigger_subscription_unsubscribe_error_continues(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that unsubscribe errors are handled gracefully and operation continues.
This test verifies:
- Unsubscribe errors are caught and logged but don't stop the rebuild
- Rebuild continues even if unsubscribe fails
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
original_credentials = {"api_key": "original-key"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# Make unsubscribe_trigger raise an error (should be caught and continue)
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.side_effect = ValueError(
"Unsubscribe failed"
)
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
# Execute rebuild - should succeed despite unsubscribe error
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials={"api_key": "new-key"},
parameters={},
)
# Verify subscribe was still called (operation continued)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
# Verify subscription was updated
db.session.refresh(subscription)
assert subscription.parameters == {}
def test_rebuild_trigger_subscription_subscription_not_found(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test error when subscription is not found.
This test verifies:
- Proper error is raised when subscription doesn't exist
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
fake_subscription_id = fake.uuid4()
with pytest.raises(ValueError, match="not found"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=fake_subscription_id,
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_provider_not_found(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test error when provider is not found.
This test verifies:
- Proper error is raised when provider doesn't exist
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("non_existent_org/non_existent_plugin/non_existent_provider")
# Make get_trigger_provider return None
mock_external_service_dependencies["trigger_manager"].get_trigger_provider.return_value = None
with pytest.raises(ValueError, match="Provider.*not found"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=fake.uuid4(),
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_unsupported_credential_type(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test error when credential type is not supported for rebuild.
This test verifies:
- Proper error is raised for unsupported credential types (not OAUTH2 or API_KEY)
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.UNAUTHORIZED # Not supported
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
{},
mock_external_service_dependencies,
)
with pytest.raises(ValueError, match="Credential type not supported for rebuild"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_name_uniqueness_check(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that name uniqueness is checked when updating name.
This test verifies:
- Error is raised when new name conflicts with existing subscription
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Create first subscription
subscription1 = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
{"api_key": "key1"},
mock_external_service_dependencies,
)
# Create second subscription with different name
subscription2 = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
{"api_key": "key2"},
mock_external_service_dependencies,
)
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription2.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Try to rename subscription2 to subscription1's name (should fail)
with pytest.raises(ValueError, match="already exists"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription2.id,
credentials={"api_key": "new-key"},
parameters={},
name=subscription1.name, # Conflicting name
)

View File

@ -12,10 +12,12 @@ class TestJinja2CodeExecutor(CodeExecutorTestMixin):
_, Jinja2TemplateTransformer = self.jinja2_imports
template = "Hello {{template}}"
# Template must be base64 encoded to match the new safe embedding approach
template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8")
inputs = base64.b64encode(b'{"template": "World"}').decode("utf-8")
code = (
Jinja2TemplateTransformer.get_runner_script()
.replace(Jinja2TemplateTransformer._code_placeholder, template)
.replace(Jinja2TemplateTransformer._template_b64_placeholder, template_b64)
.replace(Jinja2TemplateTransformer._inputs_placeholder, inputs)
)
result = CodeExecutor.execute_code(
@ -37,6 +39,34 @@ class TestJinja2CodeExecutor(CodeExecutorTestMixin):
_, Jinja2TemplateTransformer = self.jinja2_imports
runner_script = Jinja2TemplateTransformer.get_runner_script()
assert runner_script.count(Jinja2TemplateTransformer._code_placeholder) == 1
assert runner_script.count(Jinja2TemplateTransformer._template_b64_placeholder) == 1
assert runner_script.count(Jinja2TemplateTransformer._inputs_placeholder) == 1
assert runner_script.count(Jinja2TemplateTransformer._result_tag) == 2
def test_jinja2_template_with_special_characters(self, flask_app_with_containers):
"""
Test that templates with special characters (quotes, newlines) render correctly.
This is a regression test for issue #26818 where textarea pre-fill values
containing special characters would break template rendering.
"""
CodeExecutor, CodeLanguage = self.code_executor_imports
# Template with triple quotes, single quotes, double quotes, and newlines
template = """<html>
<body>
<input value="{{ task.get('Task ID', '') }}"/>
<textarea>{{ task.get('Issues', 'No issues reported') }}</textarea>
<p>Status: "{{ status }}"</p>
<pre>'''code block'''</pre>
</body>
</html>"""
inputs = {"task": {"Task ID": "TASK-123", "Issues": "Line 1\nLine 2\nLine 3"}, "status": "completed"}
result = CodeExecutor.execute_workflow_code_template(language=CodeLanguage.JINJA2, code=template, inputs=inputs)
# Verify the template rendered correctly with all special characters
output = result["result"]
assert 'value="TASK-123"' in output
assert "<textarea>Line 1\nLine 2\nLine 3</textarea>" in output
assert 'Status: "completed"' in output
assert "'''code block'''" in output

View File

@ -0,0 +1,122 @@
import base64
from unittest.mock import Mock, patch
import pytest
from core.mcp.types import (
AudioContent,
BlobResourceContents,
CallToolResult,
EmbeddedResource,
ImageContent,
TextResourceContents,
)
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage
from core.tools.mcp_tool.tool import MCPTool
def _make_mcp_tool(output_schema: dict | None = None) -> MCPTool:
identity = ToolIdentity(
author="test",
name="test_mcp_tool",
label=I18nObject(en_US="Test MCP Tool", zh_Hans="测试MCP工具"),
provider="test_provider",
)
entity = ToolEntity(identity=identity, output_schema=output_schema or {})
runtime = Mock(spec=ToolRuntime)
runtime.credentials = {}
return MCPTool(
entity=entity,
runtime=runtime,
tenant_id="test_tenant",
icon="",
server_url="https://server.invalid",
provider_id="provider_1",
headers={},
)
class TestMCPToolInvoke:
@pytest.mark.parametrize(
("content_factory", "mime_type"),
[
(
lambda b64, mt: ImageContent(type="image", data=b64, mimeType=mt),
"image/png",
),
(
lambda b64, mt: AudioContent(type="audio", data=b64, mimeType=mt),
"audio/mpeg",
),
],
)
def test_invoke_image_or_audio_yields_blob(self, content_factory, mime_type) -> None:
tool = _make_mcp_tool()
raw = b"\x00\x01test-bytes\x02"
b64 = base64.b64encode(raw).decode()
content = content_factory(b64, mime_type)
result = CallToolResult(content=[content])
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
assert len(messages) == 1
msg = messages[0]
assert msg.type == ToolInvokeMessage.MessageType.BLOB
assert isinstance(msg.message, ToolInvokeMessage.BlobMessage)
assert msg.message.blob == raw
assert msg.meta == {"mime_type": mime_type}
def test_invoke_embedded_text_resource_yields_text(self) -> None:
tool = _make_mcp_tool()
text_resource = TextResourceContents(uri="file://test.txt", mimeType="text/plain", text="hello world")
content = EmbeddedResource(type="resource", resource=text_resource)
result = CallToolResult(content=[content])
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
assert len(messages) == 1
msg = messages[0]
assert msg.type == ToolInvokeMessage.MessageType.TEXT
assert isinstance(msg.message, ToolInvokeMessage.TextMessage)
assert msg.message.text == "hello world"
@pytest.mark.parametrize(
("mime_type", "expected_mime"),
[("application/pdf", "application/pdf"), (None, "application/octet-stream")],
)
def test_invoke_embedded_blob_resource_yields_blob(self, mime_type, expected_mime) -> None:
tool = _make_mcp_tool()
raw = b"binary-data"
b64 = base64.b64encode(raw).decode()
blob_resource = BlobResourceContents(uri="file://doc.bin", mimeType=mime_type, blob=b64)
content = EmbeddedResource(type="resource", resource=blob_resource)
result = CallToolResult(content=[content])
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
assert len(messages) == 1
msg = messages[0]
assert msg.type == ToolInvokeMessage.MessageType.BLOB
assert isinstance(msg.message, ToolInvokeMessage.BlobMessage)
assert msg.message.blob == raw
assert msg.meta == {"mime_type": expected_mime}
def test_invoke_yields_variables_when_structured_content_and_schema(self) -> None:
tool = _make_mcp_tool(output_schema={"type": "object"})
result = CallToolResult(content=[], structuredContent={"a": 1, "b": "x"})
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
# Expect two variable messages corresponding to keys a and b
assert len(messages) == 2
var_msgs = [m for m in messages if isinstance(m.message, ToolInvokeMessage.VariableMessage)]
assert {m.message.variable_name for m in var_msgs} == {"a", "b"}
# Validate values
values = {m.message.variable_name: m.message.variable_value for m in var_msgs}
assert values == {"a": 1, "b": "x"}

View File

@ -3072,11 +3072,11 @@ wheels = [
[[package]]
name = "json-repair"
version = "0.54.1"
version = "0.54.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/46/d3a4d9a3dad39bb4a2ad16b8adb9fe2e8611b20b71197fe33daa6768e85d/json_repair-0.54.1.tar.gz", hash = "sha256:d010bc31f1fc66e7c36dc33bff5f8902674498ae5cb8e801ad455a53b455ad1d", size = 38555, upload-time = "2025-11-19T14:55:24.265Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/86/48b12ac02032f121ac7e5f11a32143edca6c1e3d19ffc54d6fb9ca0aafd0/json_repair-0.54.3.tar.gz", hash = "sha256:e50feec9725e52ac91f12184609754684ac1656119dfbd31de09bdaf9a1d8bf6", size = 38626, upload-time = "2025-12-15T09:41:58.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/96/c9aad7ee949cc1bf15df91f347fbc2d3bd10b30b80c7df689ce6fe9332b5/json_repair-0.54.1-py3-none-any.whl", hash = "sha256:016160c5db5d5fe443164927bb58d2dfbba5f43ad85719fa9bc51c713a443ab1", size = 29311, upload-time = "2025-11-19T14:55:22.886Z" },
{ url = "https://files.pythonhosted.org/packages/e9/08/abe317237add63c3e62f18a981bccf92112b431835b43d844aedaf61f4a0/json_repair-0.54.3-py3-none-any.whl", hash = "sha256:4cdc132ee27d4780576f71bf27a113877046224a808bfc17392e079cb344fb81", size = 29357, upload-time = "2025-12-15T09:41:57.436Z" },
]
[[package]]

View File

@ -54,17 +54,17 @@
"publish:npm": "./scripts/publish.sh"
},
"dependencies": {
"axios": "^1.3.5"
"axios": "^1.13.2"
},
"devDependencies": {
"@eslint/js": "^9.2.0",
"@types/node": "^20.11.30",
"@eslint/js": "^9.39.2",
"@types/node": "^25.0.3",
"@typescript-eslint/eslint-plugin": "^8.50.1",
"@typescript-eslint/parser": "^8.50.1",
"@vitest/coverage-v8": "1.6.1",
"eslint": "^9.2.0",
"@vitest/coverage-v8": "4.0.16",
"eslint": "^9.39.2",
"tsup": "^8.5.1",
"typescript": "^5.4.5",
"vitest": "^1.5.0"
"typescript": "^5.9.3",
"vitest": "^4.0.16"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

View File

@ -16,7 +16,7 @@ const getSupportedLocales = (): string[] => {
// Helper function to load translation file content
const loadTranslationContent = (locale: string): string => {
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')
if (!fs.existsSync(filePath))
throw new Error(`Translation file not found: ${filePath}`)
@ -24,14 +24,14 @@ const loadTranslationContent = (locale: string): string => {
return fs.readFileSync(filePath, 'utf-8')
}
// Helper function to check if upload features exist
// Helper function to check if upload features exist (supports flattened JSON)
const hasUploadFeatures = (content: string): { [key: string]: boolean } => {
return {
fileUpload: /fileUpload\s*:\s*\{/.test(content),
imageUpload: /imageUpload\s*:\s*\{/.test(content),
documentUpload: /documentUpload\s*:\s*\{/.test(content),
audioUpload: /audioUpload\s*:\s*\{/.test(content),
featureBar: /bar\s*:\s*\{/.test(content),
fileUpload: /"feature\.fileUpload\.title"/.test(content),
imageUpload: /"feature\.imageUpload\.title"/.test(content),
documentUpload: /"feature\.documentUpload\.title"/.test(content),
audioUpload: /"feature\.audioUpload\.title"/.test(content),
featureBar: /"feature\.bar\.empty"/.test(content),
}
}
@ -45,7 +45,7 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
it('all locales should have translation files', () => {
supportedLocales.forEach((locale) => {
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')
expect(fs.existsSync(filePath)).toBe(true)
})
})
@ -76,12 +76,9 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
previouslyMissingLocales.forEach((locale) => {
const content = loadTranslationContent(locale)
// Verify audioUpload exists
expect(/audioUpload\s*:\s*\{/.test(content)).toBe(true)
// Verify it has title and description
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
// Verify audioUpload exists with title and description (flattened JSON format)
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)
console.log(`${locale} - Issue #23062 resolved: audioUpload feature present`)
})
@ -91,28 +88,28 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
supportedLocales.forEach((locale) => {
const content = loadTranslationContent(locale)
// Check fileUpload has required properties
if (/fileUpload\s*:\s*\{/.test(content)) {
expect(/fileUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/fileUpload[^}]*description\s*:/.test(content)).toBe(true)
// Check fileUpload has required properties (flattened JSON format)
if (/"feature\.fileUpload\.title"/.test(content)) {
expect(/"feature\.fileUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.fileUpload\.description"/.test(content)).toBe(true)
}
// Check imageUpload has required properties
if (/imageUpload\s*:\s*\{/.test(content)) {
expect(/imageUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/imageUpload[^}]*description\s*:/.test(content)).toBe(true)
if (/"feature\.imageUpload\.title"/.test(content)) {
expect(/"feature\.imageUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.imageUpload\.description"/.test(content)).toBe(true)
}
// Check documentUpload has required properties
if (/documentUpload\s*:\s*\{/.test(content)) {
expect(/documentUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/documentUpload[^}]*description\s*:/.test(content)).toBe(true)
if (/"feature\.documentUpload\.title"/.test(content)) {
expect(/"feature\.documentUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.documentUpload\.description"/.test(content)).toBe(true)
}
// Check audioUpload has required properties
if (/audioUpload\s*:\s*\{/.test(content)) {
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
if (/"feature\.audioUpload\.title"/.test(content)) {
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)
}
})
})

View File

@ -70,7 +70,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const navConfig = [
...(isCurrentWorkspaceEditor
? [{
name: t('common.appMenus.promptEng'),
name: t('appMenus.promptEng', { ns: 'common' }),
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
@ -78,7 +78,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
: []
),
{
name: t('common.appMenus.apiAccess'),
name: t('appMenus.apiAccess', { ns: 'common' }),
href: `/app/${appId}/develop`,
icon: RiTerminalBoxLine,
selectedIcon: RiTerminalBoxFill,
@ -86,8 +86,8 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
...(isCurrentWorkspaceEditor
? [{
name: mode !== AppModeEnum.WORKFLOW
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
? t('appMenus.logAndAnn', { ns: 'common' })
: t('appMenus.logs', { ns: 'common' }),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
@ -95,7 +95,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
: []
),
{
name: t('common.appMenus.overview'),
name: t('appMenus.overview', { ns: 'common' }),
href: `/app/${appId}/overview`,
icon: RiDashboard2Line,
selectedIcon: RiDashboard2Fill,
@ -104,7 +104,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return navConfig
}, [t])
useDocumentTitle(appDetail?.name || t('common.menus.appDetail'))
useDocumentTitle(appDetail?.name || t('menus.appDetail', { ns: 'common' }))
useEffect(() => {
if (appDetail) {

View File

@ -4,6 +4,7 @@ import type { IAppCardProps } from '@/app/components/app/overview/app-card'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import type { App } from '@/types/app'
import type { I18nKeysByPrefix } from '@/types/i18n'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -62,7 +63,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const buildTriggerModeMessage = useCallback((featureName: string) => (
<div className="flex flex-col gap-1">
<div className="text-xs text-text-secondary">
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
</div>
<div
className="cursor-pointer text-xs font-medium text-text-accent hover:underline"
@ -71,19 +72,19 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
window.open(triggerDocUrl, '_blank')
}}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
</div>
</div>
), [t, triggerDocUrl])
const disableWebAppTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
? buildTriggerModeMessage(t('overview.appInfo.title', { ns: 'appOverview' }))
: null
const disableApiTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
? buildTriggerModeMessage(t('overview.apiInfo.title', { ns: 'appOverview' }))
: null
const disableMcpTooltip = disableAppCards
? buildTriggerModeMessage(t('tools.mcp.server.title'))
? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' }))
: null
const updateAppDetail = async () => {
@ -94,7 +95,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
catch (error) { console.error(error) }
}
const handleCallbackResult = (err: Error | null, message?: string) => {
const handleCallbackResult = (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => {
const type = err ? 'error' : 'success'
message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
@ -104,7 +105,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
notify({
type,
message: t(`common.actionMsg.${message}` as any) as string,
message: t(`actionMsg.${message}`, { ns: 'common' }) as string,
})
}

View File

@ -1,5 +1,6 @@
'use client'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import * as React from 'react'
@ -16,7 +17,9 @@ dayjs.extend(quarterOfYear)
const today = dayjs()
const TIME_PERIOD_MAPPING = [
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [
{ value: 0, name: 'today' },
{ value: 7, name: 'last7days' },
{ value: 30, name: 'last30days' },
@ -35,8 +38,8 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
const isWorkflow = appDetail?.mode === 'workflow'
const [period, setPeriod] = useState<PeriodParams>(IS_CLOUD_EDITION
? { name: t('appLog.filter.period.today'), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
: { name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
? { name: t('filter.period.today', { ns: 'appLog' }), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
: { name: t('filter.period.last7days', { ns: 'appLog' }), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
)
if (!appDetail)
@ -45,7 +48,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
return (
<div>
<div className="mb-4">
<div className="system-xl-semibold mb-2 text-text-primary">{t('common.appMenus.overview')}</div>
<div className="system-xl-semibold mb-2 text-text-primary">{t('appMenus.overview', { ns: 'common' })}</div>
<div className="flex items-center justify-between">
{IS_CLOUD_EDITION
? (

View File

@ -2,13 +2,16 @@
import type { FC } from 'react'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { SimpleSelect } from '@/app/components/base/select'
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
type Props = {
periodMapping: { [key: string]: { value: number, name: string } }
periodMapping: { [key: string]: { value: number, name: TimePeriodName } }
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}
@ -25,9 +28,9 @@ const LongTimeRangePicker: FC<Props> = ({
const handleSelect = React.useCallback((item: Item) => {
const id = item.value
const value = periodMapping[id]?.value ?? '-1'
const name = item.name || t('appLog.filter.period.allTime')
const name = item.name || t('filter.period.allTime', { ns: 'appLog' })
if (value === -1) {
onSelect({ name: t('appLog.filter.period.allTime'), query: undefined })
onSelect({ name: t('filter.period.allTime', { ns: 'appLog' }), query: undefined })
}
else if (value === 0) {
const startOfToday = today.startOf('day').format(queryDateFormat)
@ -53,7 +56,7 @@ const LongTimeRangePicker: FC<Props> = ({
return (
<SimpleSelect
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}` as any) as string }))}
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelect}

View File

@ -2,6 +2,7 @@
import type { Dayjs } from 'dayjs'
import type { FC } from 'react'
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import * as React from 'react'
import { useCallback, useState } from 'react'
@ -13,8 +14,10 @@ import RangeSelector from './range-selector'
const today = dayjs()
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
type Props = {
ranges: { value: number, name: string }[]
ranges: { value: number, name: TimePeriodName }[]
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import dayjs from 'dayjs'
import * as React from 'react'
@ -12,9 +13,11 @@ import { cn } from '@/utils/classnames'
const today = dayjs()
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
type Props = {
isCustomRange: boolean
ranges: { value: number, name: string }[]
ranges: { value: number, name: TimePeriodName }[]
onSelect: (payload: PeriodParamsWithTimeRange) => void
}
@ -42,7 +45,7 @@ const RangeSelector: FC<Props> = ({
const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => {
return (
<div className={cn('flex h-8 cursor-pointer items-center space-x-1.5 rounded-lg bg-components-input-bg-normal pl-3 pr-2', isOpen && 'bg-state-base-hover-alt')}>
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('appLog.filter.period.custom') : item?.name}</div>
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('filter.period.custom', { ns: 'appLog' }) : item?.name}</div>
<RiArrowDownSLine className={cn('size-4 text-text-quaternary', isOpen && 'text-text-secondary')} />
</div>
)
@ -66,7 +69,7 @@ const RangeSelector: FC<Props> = ({
}, [])
return (
<SimpleSelect
items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}` as any) as string }))}
items={ranges.map(v => ({ ...v, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelectRange}

View File

@ -15,7 +15,7 @@ import ProviderPanel from './provider-panel'
import TracingIcon from './tracing-icon'
import { TracingProvider } from './type'
const I18N_PREFIX = 'app.tracing'
const I18N_PREFIX = 'tracing'
export type PopupProps = {
appId: string
@ -327,19 +327,19 @@ const ConfigPopup: FC<PopupProps> = ({
<div className="flex items-center justify-between">
<div className="flex items-center">
<TracingIcon size="md" className="mr-2" />
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`)}</div>
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`, { ns: 'app' })}</div>
</div>
<div className="flex items-center">
<Indicator color={enabled ? 'green' : 'gray'} />
<div className={cn('system-xs-semibold-uppercase ml-1 text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
</div>
{!readOnly && (
<>
{providerAllNotConfigured
? (
<Tooltip
popupContent={t(`${I18N_PREFIX}.disabledTip`)}
popupContent={t(`${I18N_PREFIX}.disabledTip`, { ns: 'app' })}
>
{switchContent}
</Tooltip>
@ -351,14 +351,14 @@ const ConfigPopup: FC<PopupProps> = ({
</div>
<div className="system-xs-regular mt-2 text-text-tertiary">
{t(`${I18N_PREFIX}.tracingDescription`)}
{t(`${I18N_PREFIX}.tracingDescription`, { ns: 'app' })}
</div>
<Divider className="my-3" />
<div className="relative">
{(providerAllConfigured || providerAllNotConfigured)
? (
<>
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`, { ns: 'app' })}</div>
<div className="mt-2 max-h-96 space-y-2 overflow-y-auto">
{langfusePanel}
{langSmithPanel}
@ -375,11 +375,11 @@ const ConfigPopup: FC<PopupProps> = ({
)
: (
<>
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.configured`, { ns: 'app' })}</div>
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
{configuredProviderPanel()}
</div>
<div className="system-xs-medium-uppercase mt-3 text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
<div className="system-xs-medium-uppercase mt-3 text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`, { ns: 'app' })}</div>
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
{moreProviderPanel()}
</div>

View File

@ -23,7 +23,7 @@ import ConfigButton from './config-button'
import TracingIcon from './tracing-icon'
import { TracingProvider } from './type'
const I18N_PREFIX = 'app.tracing'
const I18N_PREFIX = 'tracing'
const Panel: FC = () => {
const { t } = useTranslation()
@ -45,7 +45,7 @@ const Panel: FC = () => {
if (!noToast) {
Toast.notify({
type: 'success',
message: t('common.api.success'),
message: t('api.success', { ns: 'common' }),
})
}
}
@ -254,7 +254,7 @@ const Panel: FC = () => {
)}
>
<TracingIcon size="md" />
<div className="system-sm-semibold mx-2 text-text-secondary">{t(`${I18N_PREFIX}.title`)}</div>
<div className="system-sm-semibold mx-2 text-text-secondary">{t(`${I18N_PREFIX}.title`, { ns: 'app' })}</div>
<div className="rounded-md p-1">
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div>
@ -295,7 +295,7 @@ const Panel: FC = () => {
<div className="ml-4 mr-1 flex items-center">
<Indicator color={enabled ? 'green' : 'gray'} />
<div className="system-xs-semibold-uppercase ml-1.5 text-text-tertiary">
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
</div>
</div>
{InUseProviderIcon && <InUseProviderIcon className="ml-1 h-4" />}

View File

@ -30,7 +30,7 @@ type Props = {
onChosen: (provider: TracingProvider) => void
}
const I18N_PREFIX = 'app.tracing.configProvider'
const I18N_PREFIX = 'tracing.configProvider'
const arizeConfigTemplate = {
api_key: '',
@ -157,7 +157,7 @@ const ProviderConfigModal: FC<Props> = ({
})
Toast.notify({
type: 'success',
message: t('common.api.remove'),
message: t('api.remove', { ns: 'common' }),
})
onRemoved()
hideRemoveConfirm()
@ -177,37 +177,37 @@ const ProviderConfigModal: FC<Props> = ({
if (type === TracingProvider.arize) {
const postData = config as ArizeConfig
if (!postData.api_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
if (!postData.space_id)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Space ID' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Space ID' })
if (!errorMessage && !postData.project)
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
}
if (type === TracingProvider.phoenix) {
const postData = config as PhoenixConfig
if (!postData.api_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
if (!errorMessage && !postData.project)
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
}
if (type === TracingProvider.langSmith) {
const postData = config as LangSmithConfig
if (!postData.api_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
if (!errorMessage && !postData.project)
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
}
if (type === TracingProvider.langfuse) {
const postData = config as LangFuseConfig
if (!errorMessage && !postData.secret_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.secretKey`) })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.secretKey`, { ns: 'app' }) })
if (!errorMessage && !postData.public_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.publicKey`) })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.publicKey`, { ns: 'app' }) })
if (!errorMessage && !postData.host)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Host' })
}
if (type === TracingProvider.opik) {
@ -218,43 +218,43 @@ const ProviderConfigModal: FC<Props> = ({
if (type === TracingProvider.weave) {
const postData = config as WeaveConfig
if (!errorMessage && !postData.api_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
if (!errorMessage && !postData.project)
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
}
if (type === TracingProvider.aliyun) {
const postData = config as AliyunConfig
if (!errorMessage && !postData.app_name)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'App Name' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'App Name' })
if (!errorMessage && !postData.license_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'License Key' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'License Key' })
if (!errorMessage && !postData.endpoint)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Endpoint' })
}
if (type === TracingProvider.mlflow) {
const postData = config as MLflowConfig
if (!errorMessage && !postData.tracking_uri)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Tracking URI' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Tracking URI' })
}
if (type === TracingProvider.databricks) {
const postData = config as DatabricksConfig
if (!errorMessage && !postData.experiment_id)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Experiment ID' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Experiment ID' })
if (!errorMessage && !postData.host)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Host' })
}
if (type === TracingProvider.tencent) {
const postData = config as TencentConfig
if (!errorMessage && !postData.token)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Token' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Token' })
if (!errorMessage && !postData.endpoint)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Endpoint' })
if (!errorMessage && !postData.service_name)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Service Name' })
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Service Name' })
}
return errorMessage
@ -281,7 +281,7 @@ const ProviderConfigModal: FC<Props> = ({
})
Toast.notify({
type: 'success',
message: t('common.api.success'),
message: t('api.success', { ns: 'common' }),
})
onSaved(config)
if (isAdd)
@ -303,8 +303,8 @@ const ProviderConfigModal: FC<Props> = ({
<div className="px-8 pt-8">
<div className="mb-4 flex items-center justify-between">
<div className="title-2xl-semi-bold text-text-primary">
{t(`${I18N_PREFIX}.title`)}
{t(`app.tracing.${type}.title`)}
{t(`${I18N_PREFIX}.title`, { ns: 'app' })}
{t(`tracing.${type}.title`, { ns: 'app' })}
</div>
</div>
@ -317,7 +317,7 @@ const ProviderConfigModal: FC<Props> = ({
isRequired
value={(config as ArizeConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
/>
<Field
label="Space ID"
@ -325,15 +325,15 @@ const ProviderConfigModal: FC<Props> = ({
isRequired
value={(config as ArizeConfig).space_id}
onChange={handleConfigChange('space_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Space ID' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Space ID' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
labelClassName="!text-sm"
isRequired
value={(config as ArizeConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
/>
<Field
label="Endpoint"
@ -352,15 +352,15 @@ const ProviderConfigModal: FC<Props> = ({
isRequired
value={(config as PhoenixConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
labelClassName="!text-sm"
isRequired
value={(config as PhoenixConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
/>
<Field
label="Endpoint"
@ -379,7 +379,7 @@ const ProviderConfigModal: FC<Props> = ({
isRequired
value={(config as AliyunConfig).license_key}
onChange={handleConfigChange('license_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'License Key' })!}
/>
<Field
label="Endpoint"
@ -404,7 +404,7 @@ const ProviderConfigModal: FC<Props> = ({
isRequired
value={(config as TencentConfig).token}
onChange={handleConfigChange('token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Token' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Token' })!}
/>
<Field
label="Endpoint"
@ -432,22 +432,22 @@ const ProviderConfigModal: FC<Props> = ({
isRequired
value={(config as WeaveConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
labelClassName="!text-sm"
isRequired
value={(config as WeaveConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
/>
<Field
label="Entity"
labelClassName="!text-sm"
value={(config as WeaveConfig).entity}
onChange={handleConfigChange('entity')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Entity' })!}
/>
<Field
label="Endpoint"
@ -473,15 +473,15 @@ const ProviderConfigModal: FC<Props> = ({
isRequired
value={(config as LangSmithConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
labelClassName="!text-sm"
isRequired
value={(config as LangSmithConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
/>
<Field
label="Endpoint"
@ -495,20 +495,20 @@ const ProviderConfigModal: FC<Props> = ({
{type === TracingProvider.langfuse && (
<>
<Field
label={t(`${I18N_PREFIX}.secretKey`)!}
label={t(`${I18N_PREFIX}.secretKey`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as LangFuseConfig).secret_key}
isRequired
onChange={handleConfigChange('secret_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.secretKey`, { ns: 'app' }) })!}
/>
<Field
label={t(`${I18N_PREFIX}.publicKey`)!}
label={t(`${I18N_PREFIX}.publicKey`, { ns: 'app' })!}
labelClassName="!text-sm"
isRequired
value={(config as LangFuseConfig).public_key}
onChange={handleConfigChange('public_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.publicKey`, { ns: 'app' }) })!}
/>
<Field
label="Host"
@ -527,14 +527,14 @@ const ProviderConfigModal: FC<Props> = ({
labelClassName="!text-sm"
value={(config as OpikConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as OpikConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
/>
<Field
label="Workspace"
@ -555,7 +555,7 @@ const ProviderConfigModal: FC<Props> = ({
{type === TracingProvider.mlflow && (
<>
<Field
label={t(`${I18N_PREFIX}.trackingUri`)!}
label={t(`${I18N_PREFIX}.trackingUri`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as MLflowConfig).tracking_uri}
isRequired
@ -563,67 +563,67 @@ const ProviderConfigModal: FC<Props> = ({
placeholder="http://localhost:5000"
/>
<Field
label={t(`${I18N_PREFIX}.experimentId`)!}
label={t(`${I18N_PREFIX}.experimentId`, { ns: 'app' })!}
labelClassName="!text-sm"
isRequired
value={(config as MLflowConfig).experiment_id}
onChange={handleConfigChange('experiment_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.experimentId`, { ns: 'app' }) })!}
/>
<Field
label={t(`${I18N_PREFIX}.username`)!}
label={t(`${I18N_PREFIX}.username`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as MLflowConfig).username}
onChange={handleConfigChange('username')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.username`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.username`, { ns: 'app' }) })!}
/>
<Field
label={t(`${I18N_PREFIX}.password`)!}
label={t(`${I18N_PREFIX}.password`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as MLflowConfig).password}
onChange={handleConfigChange('password')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.password`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.password`, { ns: 'app' }) })!}
/>
</>
)}
{type === TracingProvider.databricks && (
<>
<Field
label={t(`${I18N_PREFIX}.experimentId`)!}
label={t(`${I18N_PREFIX}.experimentId`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).experiment_id}
onChange={handleConfigChange('experiment_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.experimentId`, { ns: 'app' }) })!}
isRequired
/>
<Field
label={t(`${I18N_PREFIX}.databricksHost`)!}
label={t(`${I18N_PREFIX}.databricksHost`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).host}
onChange={handleConfigChange('host')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.databricksHost`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.databricksHost`, { ns: 'app' }) })!}
isRequired
/>
<Field
label={t(`${I18N_PREFIX}.clientId`)!}
label={t(`${I18N_PREFIX}.clientId`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).client_id}
onChange={handleConfigChange('client_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientId`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.clientId`, { ns: 'app' }) })!}
/>
<Field
label={t(`${I18N_PREFIX}.clientSecret`)!}
label={t(`${I18N_PREFIX}.clientSecret`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).client_secret}
onChange={handleConfigChange('client_secret')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientSecret`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.clientSecret`, { ns: 'app' }) })!}
/>
<Field
label={t(`${I18N_PREFIX}.personalAccessToken`)!}
label={t(`${I18N_PREFIX}.personalAccessToken`, { ns: 'app' })!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).personal_access_token}
onChange={handleConfigChange('personal_access_token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.personalAccessToken`) })!}
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.personalAccessToken`, { ns: 'app' }) })!}
/>
</>
)}
@ -634,7 +634,7 @@ const ProviderConfigModal: FC<Props> = ({
target="_blank"
href={docURL[type]}
>
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { ns: 'app', key: t(`tracing.${type}.title`, { ns: 'app' }) })}</span>
<LinkExternal02 className="h-3 w-3" />
</a>
<div className="flex items-center">
@ -644,7 +644,7 @@ const ProviderConfigModal: FC<Props> = ({
className="h-9 text-sm font-medium text-text-secondary"
onClick={showRemoveConfirm}
>
<span className="text-[#D92D20]">{t('common.operation.remove')}</span>
<span className="text-[#D92D20]">{t('operation.remove', { ns: 'common' })}</span>
</Button>
<Divider type="vertical" className="mx-3 h-[18px]" />
</>
@ -653,7 +653,7 @@ const ProviderConfigModal: FC<Props> = ({
className="mr-2 h-9 text-sm font-medium text-text-secondary"
onClick={onCancel}
>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
className="h-9 text-sm font-medium"
@ -661,7 +661,7 @@ const ProviderConfigModal: FC<Props> = ({
onClick={handleSave}
loading={isSaving}
>
{t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
{t(`operation.${isAdd ? 'saveAndEnable' : 'save'}`, { ns: 'common' })}
</Button>
</div>
@ -670,7 +670,7 @@ const ProviderConfigModal: FC<Props> = ({
<div className="border-t-[0.5px] border-divider-regular">
<div className="flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary">
<Lock01 className="mr-1 h-3 w-3 text-text-tertiary" />
{t('common.modelProvider.encrypted.front')}
{t('modelProvider.encrypted.front', { ns: 'common' })}
<a
className="mx-1 text-primary-600"
target="_blank"
@ -679,7 +679,7 @@ const ProviderConfigModal: FC<Props> = ({
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
{t('modelProvider.encrypted.back', { ns: 'common' })}
</div>
</div>
</div>
@ -691,8 +691,8 @@ const ProviderConfigModal: FC<Props> = ({
<Confirm
isShow
type="warning"
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
content={t(`${I18N_PREFIX}.removeConfirmContent`)}
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { ns: 'app', key: t(`tracing.${type}.title`, { ns: 'app' }) })!}
content={t(`${I18N_PREFIX}.removeConfirmContent`, { ns: 'app' })}
onConfirm={handleRemove}
onCancel={hideRemoveConfirm}
/>

View File

@ -11,7 +11,7 @@ import { Eye as View } from '@/app/components/base/icons/src/vender/solid/genera
import { cn } from '@/utils/classnames'
import { TracingProvider } from './type'
const I18N_PREFIX = 'app.tracing'
const I18N_PREFIX = 'tracing'
type Props = {
type: TracingProvider
@ -82,14 +82,14 @@ const ProviderPanel: FC<Props> = ({
<div className="flex items-center justify-between space-x-1">
<div className="flex items-center">
<Icon className="h-6" />
{isChosen && <div className="system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary">{t(`${I18N_PREFIX}.inUse`)}</div>}
{isChosen && <div className="system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary">{t(`${I18N_PREFIX}.inUse`, { ns: 'app' })}</div>}
</div>
{!readOnly && (
<div className="flex items-center justify-between space-x-1">
{hasConfigured && (
<div className="flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs" onClick={viewBtnClick}>
<View className="h-3 w-3" />
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.view`)}</div>
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.view`, { ns: 'app' })}</div>
</div>
)}
<div
@ -97,13 +97,13 @@ const ProviderPanel: FC<Props> = ({
onClick={handleConfigBtnClick}
>
<RiEqualizer2Line className="h-3 w-3" />
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.config`)}</div>
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.config`, { ns: 'app' })}</div>
</div>
</div>
)}
</div>
<div className="system-xs-regular mt-2 text-text-tertiary">
{t(`${I18N_PREFIX}.${type}.description`)}
{t(`${I18N_PREFIX}.${type}.description`, { ns: 'app' })}
</div>
</div>
)

View File

@ -15,7 +15,7 @@ const AppDetail: FC<IAppDetail> = ({ children }) => {
const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
useDocumentTitle(t('common.menus.appDetail'))
useDocumentTitle(t('menus.appDetail', { ns: 'common' }))
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)

View File

@ -70,14 +70,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const navigation = useMemo(() => {
const baseNavigation = [
{
name: t('common.datasetMenus.hitTesting'),
name: t('datasetMenus.hitTesting', { ns: 'common' }),
href: `/datasets/${datasetId}/hitTesting`,
icon: RiFocus2Line,
selectedIcon: RiFocus2Fill,
disabled: isButtonDisabledWithPipeline,
},
{
name: t('common.datasetMenus.settings'),
name: t('datasetMenus.settings', { ns: 'common' }),
href: `/datasets/${datasetId}/settings`,
icon: RiEqualizer2Line,
selectedIcon: RiEqualizer2Fill,
@ -87,14 +87,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
if (datasetRes?.provider !== 'external') {
baseNavigation.unshift({
name: t('common.datasetMenus.pipeline'),
name: t('datasetMenus.pipeline', { ns: 'common' }),
href: `/datasets/${datasetId}/pipeline`,
icon: PipelineLine as RemixiconComponentType,
selectedIcon: PipelineFill as RemixiconComponentType,
disabled: false,
})
baseNavigation.unshift({
name: t('common.datasetMenus.documents'),
name: t('datasetMenus.documents', { ns: 'common' }),
href: `/datasets/${datasetId}/documents`,
icon: RiFileTextLine,
selectedIcon: RiFileTextFill,
@ -105,7 +105,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return baseNavigation
}, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider])
useDocumentTitle(datasetRes?.name || t('common.menus.datasets'))
useDocumentTitle(datasetRes?.name || t('menus.datasets', { ns: 'common' }))
const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand)

View File

@ -1,3 +1,4 @@
/* eslint-disable dify-i18n/require-ns-option */
import * as React from 'react'
import Form from '@/app/components/datasets/settings/form'
import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
@ -9,7 +10,7 @@ const Settings = async () => {
return (
<div className="h-full overflow-y-auto">
<div className="flex flex-col gap-y-0.5 px-6 pb-2 pt-3">
<div className="system-xl-semibold text-text-primary">{t('title') as any}</div>
<div className="system-xl-semibold text-text-primary">{t('title')}</div>
<div className="system-sm-regular text-text-tertiary">{t('desc')}</div>
</div>
<Form />

View File

@ -7,7 +7,7 @@ import useDocumentTitle from '@/hooks/use-document-title'
const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.explore'))
useDocumentTitle(t('menus.explore', { ns: 'common' }))
return (
<ExploreClient>
{children}

View File

@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import * as React from 'react'
import { AppInitializer } from '@/app/components/app-initializer'
import AmplitudeProvider from '@/app/components/base/amplitude'
import GA, { GaType } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk'
@ -7,7 +8,6 @@ import GotoAnything from '@/app/components/goto-anything'
import Header from '@/app/components/header'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import SwrInitializer from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ModalContextProvider } from '@/context/modal-context'
@ -20,7 +20,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitializer>
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
@ -38,7 +38,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</EventEmitterContextProvider>
</AppContextProvider>
<Zendesk />
</SwrInitializer>
</AppInitializer>
</>
)
}

View File

@ -12,7 +12,7 @@ const ToolsList: FC = () => {
const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
useDocumentTitle(t('common.menus.tools'))
useDocumentTitle(t('menus.tools', { ns: 'common' }))
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)

View File

@ -81,7 +81,7 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="flex h-full flex-col items-center justify-center gap-y-2">
<AppUnavailable className="h-auto w-auto" code={403} unknownReason="no permission." />
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('common.userProfile.logout')}</span>
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('userProfile.logout', { ns: 'common' })}</span>
</div>
)
}

View File

@ -94,8 +94,8 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
if (message) {
return (
<div className="flex h-full flex-col items-center justify-center gap-y-4">
<AppUnavailable className="h-auto w-auto" code={code || t('share.common.appUnavailable')} unknownReason={message} />
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
<AppUnavailable className="h-auto w-auto" code={code || t('common.appUnavailable', { ns: 'share' })} unknownReason={message} />
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{code === '403' ? t('userProfile.logout', { ns: 'common' }) : t('login.backToHome', { ns: 'share' })}</span>
</div>
)
}

View File

@ -26,14 +26,14 @@ export default function CheckCode() {
if (!code.trim()) {
Toast.notify({
type: 'error',
message: t('login.checkCode.emptyCode'),
message: t('checkCode.emptyCode', { ns: 'login' }),
})
return
}
if (!/\d{6}/.test(code)) {
Toast.notify({
type: 'error',
message: t('login.checkCode.invalidCode'),
message: t('checkCode.invalidCode', { ns: 'login' }),
})
return
}
@ -69,22 +69,22 @@ export default function CheckCode() {
<RiMailSendFill className="h-6 w-6 text-2xl" />
</div>
<div className="pb-4 pt-2">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.checkCode.checkYourEmail')}</h2>
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<span>
{t('login.checkCode.tipsPrefix')}
{t('checkCode.tipsPrefix', { ns: 'login' })}
<strong>{email}</strong>
</span>
<br />
{t('login.checkCode.validTime')}
{t('checkCode.validTime', { ns: 'login' })}
</p>
</div>
<form action="">
<input type="text" className="hidden" />
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className="mt-1" placeholder={t('login.checkCode.verificationCodePlaceholder') || ''} />
<Button loading={loading} disabled={loading} className="my-3 w-full" variant="primary" onClick={verify}>{t('login.checkCode.verify')}</Button>
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className="mt-1" placeholder={t('checkCode.verificationCodePlaceholder', { ns: 'login' }) || ''} />
<Button loading={loading} disabled={loading} className="my-3 w-full" variant="primary" onClick={verify}>{t('checkCode.verify', { ns: 'login' })}</Button>
<Countdown onResend={resendCode} />
</form>
<div className="py-2">
@ -94,7 +94,7 @@ export default function CheckCode() {
<div className="bg-background-default-dimm inline-block rounded-full p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('login.back')}</span>
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
</div>
</div>
)

View File

@ -27,14 +27,14 @@ export default function CheckCode() {
const handleGetEMailVerificationCode = async () => {
try {
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
return
}
if (!emailRegex.test(email)) {
Toast.notify({
type: 'error',
message: t('login.error.emailInValid'),
message: t('error.emailInValid', { ns: 'login' }),
})
return
}
@ -50,7 +50,7 @@ export default function CheckCode() {
else if (res.code === 'account_not_found') {
Toast.notify({
type: 'error',
message: t('login.error.registrationNotAllowed'),
message: t('error.registrationNotAllowed', { ns: 'login' }),
})
}
else {
@ -74,21 +74,21 @@ export default function CheckCode() {
<RiLockPasswordLine className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.resetPassword')}</h2>
<h2 className="title-4xl-semi-bold text-text-primary">{t('resetPassword', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
{t('login.resetPasswordDesc')}
{t('resetPasswordDesc', { ns: 'login' })}
</p>
</div>
<form onSubmit={noop}>
<input type="text" className="hidden" />
<div className="mb-2">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('login.email')}</label>
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
<div className="mt-1">
<Input id="email" type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
<Input id="email" type="email" disabled={loading} value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
</div>
<div className="mt-3">
<Button loading={loading} disabled={loading} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button>
<Button loading={loading} disabled={loading} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('sendVerificationCode', { ns: 'login' })}</Button>
</div>
</div>
</form>
@ -99,7 +99,7 @@ export default function CheckCode() {
<div className="inline-block rounded-full bg-background-default-dimmed p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('login.backToLogin')}</span>
<span className="system-xs-regular ml-2">{t('backToLogin', { ns: 'login' })}</span>
</Link>
</div>
)

View File

@ -45,15 +45,15 @@ const ChangePasswordForm = () => {
const valid = useCallback(() => {
if (!password.trim()) {
showErrorMessage(t('login.error.passwordEmpty'))
showErrorMessage(t('error.passwordEmpty', { ns: 'login' }))
return false
}
if (!validPassword.test(password)) {
showErrorMessage(t('login.error.passwordInvalid'))
showErrorMessage(t('error.passwordInvalid', { ns: 'login' }))
return false
}
if (password !== confirmPassword) {
showErrorMessage(t('common.account.notEqual'))
showErrorMessage(t('account.notEqual', { ns: 'common' }))
return false
}
return true
@ -92,10 +92,10 @@ const ChangePasswordForm = () => {
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">
{t('login.changePassword')}
{t('changePassword', { ns: 'login' })}
</h2>
<p className="body-md-regular mt-2 text-text-secondary">
{t('login.changePasswordTip')}
{t('changePasswordTip', { ns: 'login' })}
</p>
</div>
@ -104,7 +104,7 @@ const ChangePasswordForm = () => {
{/* Password */}
<div className="mb-5">
<label htmlFor="password" className="system-md-semibold my-2 text-text-secondary">
{t('common.account.newPassword')}
{t('account.newPassword', { ns: 'common' })}
</label>
<div className="relative mt-1">
<Input
@ -112,7 +112,7 @@ const ChangePasswordForm = () => {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={e => setPassword(e.target.value)}
placeholder={t('login.passwordPlaceholder') || ''}
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
@ -125,12 +125,12 @@ const ChangePasswordForm = () => {
</Button>
</div>
</div>
<div className="body-xs-regular mt-1 text-text-secondary">{t('login.error.passwordInvalid')}</div>
<div className="body-xs-regular mt-1 text-text-secondary">{t('error.passwordInvalid', { ns: 'login' })}</div>
</div>
{/* Confirm Password */}
<div className="mb-5">
<label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary">
{t('common.account.confirmPassword')}
{t('account.confirmPassword', { ns: 'common' })}
</label>
<div className="relative mt-1">
<Input
@ -138,7 +138,7 @@ const ChangePasswordForm = () => {
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
placeholder={t('login.confirmPasswordPlaceholder') || ''}
placeholder={t('confirmPasswordPlaceholder', { ns: 'login' }) || ''}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
@ -157,7 +157,7 @@ const ChangePasswordForm = () => {
className="w-full"
onClick={handleChangePassword}
>
{t('login.changePasswordBtn')}
{t('changePasswordBtn', { ns: 'login' })}
</Button>
</div>
</div>
@ -171,7 +171,7 @@ const ChangePasswordForm = () => {
<RiCheckboxCircleFill className="h-6 w-6 text-text-success" />
</div>
<h2 className="title-4xl-semi-bold text-text-primary">
{t('login.passwordChangedTip')}
{t('passwordChangedTip', { ns: 'login' })}
</h2>
</div>
<div className="mx-auto mt-6 w-full">
@ -183,7 +183,7 @@ const ChangePasswordForm = () => {
router.replace(getSignInUrl())
}}
>
{t('login.passwordChanged')}
{t('passwordChanged', { ns: 'login' })}
{' '}
(
{Math.round(countdown / 1000)}

View File

@ -45,21 +45,21 @@ export default function CheckCode() {
if (!code.trim()) {
Toast.notify({
type: 'error',
message: t('login.checkCode.emptyCode'),
message: t('checkCode.emptyCode', { ns: 'login' }),
})
return
}
if (!/\d{6}/.test(code)) {
Toast.notify({
type: 'error',
message: t('login.checkCode.invalidCode'),
message: t('checkCode.invalidCode', { ns: 'login' }),
})
return
}
if (!redirectUrl || !appCode) {
Toast.notify({
type: 'error',
message: t('login.error.redirectUrlMissing'),
message: t('error.redirectUrlMissing', { ns: 'login' }),
})
return
}
@ -108,19 +108,19 @@ export default function CheckCode() {
<RiMailSendFill className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.checkCode.checkYourEmail')}</h2>
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<span>
{t('login.checkCode.tipsPrefix')}
{t('checkCode.tipsPrefix', { ns: 'login' })}
<strong>{email}</strong>
</span>
<br />
{t('login.checkCode.validTime')}
{t('checkCode.validTime', { ns: 'login' })}
</p>
</div>
<form onSubmit={handleSubmit}>
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('login.checkCode.verificationCode')}</label>
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<Input
ref={codeInputRef}
id="code"
@ -128,9 +128,9 @@ export default function CheckCode() {
onChange={e => setVerifyCode(e.target.value)}
maxLength={6}
className="mt-1"
placeholder={t('login.checkCode.verificationCodePlaceholder') || ''}
placeholder={t('checkCode.verificationCodePlaceholder', { ns: 'login' }) || ''}
/>
<Button type="submit" loading={loading} disabled={loading} className="my-3 w-full" variant="primary">{t('login.checkCode.verify')}</Button>
<Button type="submit" loading={loading} disabled={loading} className="my-3 w-full" variant="primary">{t('checkCode.verify', { ns: 'login' })}</Button>
<Countdown onResend={resendCode} />
</form>
<div className="py-2">
@ -140,7 +140,7 @@ export default function CheckCode() {
<div className="bg-background-default-dimm inline-block rounded-full p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('login.back')}</span>
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
</div>
</div>
)

View File

@ -23,14 +23,14 @@ export default function MailAndCodeAuth() {
const handleGetEMailVerificationCode = async () => {
try {
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
return
}
if (!emailRegex.test(email)) {
Toast.notify({
type: 'error',
message: t('login.error.emailInValid'),
message: t('error.emailInValid', { ns: 'login' }),
})
return
}
@ -56,12 +56,12 @@ export default function MailAndCodeAuth() {
<form onSubmit={noop}>
<input type="text" className="hidden" />
<div className="mb-2">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('login.email')}</label>
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
<div className="mt-1">
<Input id="email" type="email" value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
<Input id="email" type="email" value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
</div>
<div className="mt-3">
<Button loading={loading} disabled={loading || !email} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('login.signup.verifyMail')}</Button>
<Button loading={loading} disabled={loading || !email} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('signup.verifyMail', { ns: 'login' })}</Button>
</div>
</div>
</form>

View File

@ -46,25 +46,25 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
const appCode = getAppCodeFromRedirectUrl()
const handleEmailPasswordLogin = async () => {
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
return
}
if (!emailRegex.test(email)) {
Toast.notify({
type: 'error',
message: t('login.error.emailInValid'),
message: t('error.emailInValid', { ns: 'login' }),
})
return
}
if (!password?.trim()) {
Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
Toast.notify({ type: 'error', message: t('error.passwordEmpty', { ns: 'login' }) })
return
}
if (!redirectUrl || !appCode) {
Toast.notify({
type: 'error',
message: t('login.error.redirectUrlMissing'),
message: t('error.redirectUrlMissing', { ns: 'login' }),
})
return
}
@ -111,7 +111,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
<form onSubmit={noop}>
<div className="mb-3">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
{t('login.email')}
{t('email', { ns: 'login' })}
</label>
<div className="mt-1">
<Input
@ -120,7 +120,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
id="email"
type="email"
autoComplete="email"
placeholder={t('login.emailPlaceholder') || ''}
placeholder={t('emailPlaceholder', { ns: 'login' }) || ''}
tabIndex={1}
/>
</div>
@ -128,14 +128,14 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
<div className="mb-3">
<label htmlFor="password" className="my-2 flex items-center justify-between">
<span className="system-md-semibold text-text-secondary">{t('login.password')}</span>
<span className="system-md-semibold text-text-secondary">{t('password', { ns: 'login' })}</span>
<Link
href={`/webapp-reset-password?${searchParams.toString()}`}
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
tabIndex={isEmailSetup ? 0 : -1}
aria-disabled={!isEmailSetup}
>
{t('login.forget')}
{t('forget', { ns: 'login' })}
</Link>
</label>
<div className="relative mt-1">
@ -149,7 +149,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
}}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder={t('login.passwordPlaceholder') || ''}
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}
tabIndex={2}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
@ -172,7 +172,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
disabled={isLoading || !email || !password}
className="w-full"
>
{t('login.signBtn')}
{t('signBtn', { ns: 'login' })}
</Button>
</div>
</form>

View File

@ -82,7 +82,7 @@ const SSOAuth: FC<SSOAuthProps> = ({
className="w-full"
>
<Lock01 className="mr-2 h-5 w-5 text-text-accent-light-mode-only" />
<span className="truncate">{t('login.withSSO')}</span>
<span className="truncate">{t('withSSO', { ns: 'login' })}</span>
</Button>
)
}

View File

@ -9,7 +9,7 @@ import { cn } from '@/utils/classnames'
export default function SignInLayout({ children }: PropsWithChildren) {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
useDocumentTitle(t('login.webapp.login'))
useDocumentTitle(t('webapp.login', { ns: 'login' }))
return (
<>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>

View File

@ -60,8 +60,8 @@ const NormalForm = () => {
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.licenseLost')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseLostTip')}</p>
<p className="system-sm-medium text-text-primary">{t('licenseLost', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseLostTip', { ns: 'login' })}</p>
</div>
</div>
</div>
@ -76,8 +76,8 @@ const NormalForm = () => {
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.licenseExpired')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseExpiredTip')}</p>
<p className="system-sm-medium text-text-primary">{t('licenseExpired', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseExpiredTip', { ns: 'login' })}</p>
</div>
</div>
</div>
@ -92,8 +92,8 @@ const NormalForm = () => {
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.licenseInactive')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseInactiveTip')}</p>
<p className="system-sm-medium text-text-primary">{t('licenseInactive', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseInactiveTip', { ns: 'login' })}</p>
</div>
</div>
</div>
@ -104,8 +104,8 @@ const NormalForm = () => {
<>
<div className="mx-auto mt-8 w-full">
<div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('login.pageTitleForE') : t('login.pageTitle')}</h2>
<p className="body-md-regular mt-2 text-text-tertiary">{t('login.welcome')}</p>
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-tertiary">{t('welcome', { ns: 'login' })}</p>
</div>
<div className="relative">
<div className="mt-6 flex flex-col gap-3">
@ -122,7 +122,7 @@ const NormalForm = () => {
<div className="h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
</div>
<div className="relative flex justify-center">
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('or', { ns: 'login' })}</span>
</div>
</div>
)}
@ -134,7 +134,7 @@ const NormalForm = () => {
<MailAndCodeAuth />
{systemFeatures.enable_email_password_login && (
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('password') }}>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('login.usePassword')}</span>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('usePassword', { ns: 'login' })}</span>
</div>
)}
</>
@ -144,7 +144,7 @@ const NormalForm = () => {
<MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} />
{systemFeatures.enable_email_code_login && (
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('code') }}>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('login.useVerificationCode')}</span>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('useVerificationCode', { ns: 'login' })}</span>
</div>
)}
</>
@ -158,8 +158,8 @@ const NormalForm = () => {
<div className="shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
<RiDoorLockLine className="h-5 w-5" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.noLoginMethod')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.noLoginMethodTip')}</p>
<p className="system-sm-medium text-text-primary">{t('noLoginMethod', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('noLoginMethodTip', { ns: 'login' })}</p>
</div>
<div className="relative my-2 py-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
@ -171,7 +171,7 @@ const NormalForm = () => {
{!systemFeatures.branding.enabled && (
<>
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('login.tosDesc')}
{t('tosDesc', { ns: 'login' })}
&nbsp;
<Link
className="system-xs-medium text-text-secondary hover:underline"
@ -179,7 +179,7 @@ const NormalForm = () => {
rel="noopener noreferrer"
href="https://dify.ai/terms"
>
{t('login.tos')}
{t('tos', { ns: 'login' })}
</Link>
&nbsp;&&nbsp;
<Link
@ -188,18 +188,18 @@ const NormalForm = () => {
rel="noopener noreferrer"
href="https://dify.ai/privacy"
>
{t('login.pp')}
{t('pp', { ns: 'login' })}
</Link>
</div>
{IS_CE_EDITION && (
<div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('login.goToInit')}
{t('goToInit', { ns: 'login' })}
&nbsp;
<Link
className="system-xs-medium text-text-secondary hover:underline"
href="/install"
>
{t('login.setAdminAccount')}
{t('setAdminAccount', { ns: 'login' })}
</Link>
</div>
)}

View File

@ -37,7 +37,7 @@ const WebSSOForm: FC = () => {
if (!redirectUrl) {
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable code={t('share.common.appUnavailable')} unknownReason="redirect url is invalid." />
<AppUnavailable code={t('common.appUnavailable', { ns: 'share' })} unknownReason="redirect url is invalid." />
</div>
)
}
@ -45,7 +45,7 @@ const WebSSOForm: FC = () => {
if (!systemFeatures.webapp_auth.enabled) {
return (
<div className="flex h-full items-center justify-center">
<p className="system-xs-regular text-text-tertiary">{t('login.webapp.disabled')}</p>
<p className="system-xs-regular text-text-tertiary">{t('webapp.disabled', { ns: 'login' })}</p>
</div>
)
}
@ -63,7 +63,7 @@ const WebSSOForm: FC = () => {
return (
<div className="flex h-full flex-col items-center justify-center gap-y-4">
<AppUnavailable className="h-auto w-auto" isUnknownReason={true} />
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('share.login.backToHome')}</span>
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('login.backToHome', { ns: 'share' })}</span>
</div>
)
}

View File

@ -48,7 +48,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } })
setIsShowAvatarPicker(false)
onSave?.()
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
}
catch (e) {
notify({ type: 'error', message: (e as Error).message })
@ -58,7 +58,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
const handleDeleteAvatar = useCallback(async () => {
try {
await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
setIsShowDeleteConfirm(false)
onSave?.()
}
@ -145,11 +145,11 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
<div className="flex w-full items-center justify-center gap-2 p-3">
<Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
{t('app.iconPicker.cancel')}
{t('iconPicker.cancel', { ns: 'app' })}
</Button>
<Button variant="primary" className="w-full" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
{t('app.iconPicker.ok')}
{t('iconPicker.ok', { ns: 'app' })}
</Button>
</div>
</Modal>
@ -160,16 +160,16 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
isShow={isShowDeleteConfirm}
onClose={() => setIsShowDeleteConfirm(false)}
>
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('common.avatar.deleteTitle')}</div>
<p className="mb-8 text-text-secondary">{t('common.avatar.deleteDescription')}</p>
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('avatar.deleteTitle', { ns: 'common' })}</div>
<p className="mb-8 text-text-secondary">{t('avatar.deleteDescription', { ns: 'common' })}</p>
<div className="flex w-full items-center justify-center gap-2">
<Button className="w-full" onClick={() => setIsShowDeleteConfirm(false)}>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
{t('common.operation.delete')}
{t('operation.delete', { ns: 'common' })}
</Button>
</div>
</Modal>

View File

@ -209,9 +209,9 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
</div>
{step === STEP.start && (
<>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.title')}</div>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.title', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-medium text-text-warning">{t('common.account.changeEmail.authTip')}</div>
<div className="body-md-medium text-text-warning">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
<div className="body-md-regular text-text-secondary">
<Trans
i18nKey="common.account.changeEmail.content1"
@ -227,20 +227,20 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
variant="primary"
onClick={sendCodeToOriginEmail}
>
{t('common.account.changeEmail.sendVerifyCode')}
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
</Button>
<Button
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
</>
)}
{step === STEP.verifyOrigin && (
<>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.verifyEmail')}</div>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-regular text-text-secondary">
<Trans
@ -251,10 +251,10 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
</div>
</div>
<div className="pt-3">
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
<Input
className="!w-full"
placeholder={t('common.account.changeEmail.codePlaceholder')}
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
value={code}
onChange={e => setCode(e.target.value)}
maxLength={6}
@ -267,46 +267,46 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
variant="primary"
onClick={handleVerifyOriginEmail}
>
{t('common.account.changeEmail.continue')}
{t('account.changeEmail.continue', { ns: 'common' })}
</Button>
<Button
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
<span>{t('common.account.changeEmail.resendTip')}</span>
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
{time > 0 && (
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
)}
{!time && (
<span onClick={sendCodeToOriginEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('common.account.changeEmail.resend')}</span>
<span onClick={sendCodeToOriginEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
)}
</div>
</>
)}
{step === STEP.newEmail && (
<>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.newEmail')}</div>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-regular text-text-secondary">{t('common.account.changeEmail.content3')}</div>
<div className="body-md-regular text-text-secondary">{t('account.changeEmail.content3', { ns: 'common' })}</div>
</div>
<div className="pt-3">
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.emailLabel')}</div>
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
<Input
className="!w-full"
placeholder={t('common.account.changeEmail.emailPlaceholder')}
placeholder={t('account.changeEmail.emailPlaceholder', { ns: 'common' })}
value={mail}
onChange={e => handleNewEmailValueChange(e.target.value)}
destructive={newEmailExited || unAvailableEmail}
/>
{newEmailExited && (
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('common.account.changeEmail.existingEmail')}</div>
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
)}
{unAvailableEmail && (
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('common.account.changeEmail.unAvailableEmail')}</div>
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
)}
</div>
<div className="mt-3 space-y-2">
@ -316,20 +316,20 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
variant="primary"
onClick={sendCodeToNewEmail}
>
{t('common.account.changeEmail.sendVerifyCode')}
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
</Button>
<Button
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
</>
)}
{step === STEP.verifyNew && (
<>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.verifyNew')}</div>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-regular text-text-secondary">
<Trans
@ -340,10 +340,10 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
</div>
</div>
<div className="pt-3">
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
<Input
className="!w-full"
placeholder={t('common.account.changeEmail.codePlaceholder')}
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
value={code}
onChange={e => setCode(e.target.value)}
maxLength={6}
@ -356,22 +356,22 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
variant="primary"
onClick={submitNewEmail}
>
{t('common.account.changeEmail.changeTo', { email: mail })}
{t('account.changeEmail.changeTo', { ns: 'common', email: mail })}
</Button>
<Button
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
<span>{t('common.account.changeEmail.resendTip')}</span>
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
{time > 0 && (
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
)}
{!time && (
<span onClick={sendCodeToNewEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('common.account.changeEmail.resend')}</span>
<span onClick={sendCodeToNewEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
)}
</div>
</>

View File

@ -61,7 +61,7 @@ export default function AccountPage() {
try {
setEditing(true)
await updateUserProfile({ url: 'account/name', body: { name: editName } })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
mutateUserProfile()
setEditNameModalVisible(false)
setEditing(false)
@ -80,15 +80,15 @@ export default function AccountPage() {
}
const valid = () => {
if (!password.trim()) {
showErrorMessage(t('login.error.passwordEmpty'))
showErrorMessage(t('error.passwordEmpty', { ns: 'login' }))
return false
}
if (!validPassword.test(password)) {
showErrorMessage(t('login.error.passwordInvalid'))
showErrorMessage(t('error.passwordInvalid', { ns: 'login' }))
return false
}
if (password !== confirmPassword) {
showErrorMessage(t('common.account.notEqual'))
showErrorMessage(t('account.notEqual', { ns: 'common' }))
return false
}
@ -112,7 +112,7 @@ export default function AccountPage() {
repeat_new_password: confirmPassword,
},
})
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
mutateUserProfile()
setEditPasswordModalVisible(false)
resetPasswordForm()
@ -146,7 +146,7 @@ export default function AccountPage() {
return (
<>
<div className="pb-3 pt-2">
<h4 className="title-2xl-semi-bold text-text-primary">{t('common.account.myAccount')}</h4>
<h4 className="title-2xl-semi-bold text-text-primary">{t('account.myAccount', { ns: 'common' })}</h4>
</div>
<div className="mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6">
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
@ -164,25 +164,25 @@ export default function AccountPage() {
</div>
</div>
<div className="mb-8">
<div className={titleClassName}>{t('common.account.name')}</div>
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
<div className="mt-2 flex w-full items-center justify-between gap-2">
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
<span className="pl-1">{userProfile.name}</span>
</div>
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={handleEditName}>
{t('common.operation.edit')}
{t('operation.edit', { ns: 'common' })}
</div>
</div>
</div>
<div className="mb-8">
<div className={titleClassName}>{t('common.account.email')}</div>
<div className={titleClassName}>{t('account.email', { ns: 'common' })}</div>
<div className="mt-2 flex w-full items-center justify-between gap-2">
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
<span className="pl-1">{userProfile.email}</span>
</div>
{systemFeatures.enable_change_email && (
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={() => setShowUpdateEmail(true)}>
{t('common.operation.change')}
{t('operation.change', { ns: 'common' })}
</div>
)}
</div>
@ -191,26 +191,26 @@ export default function AccountPage() {
systemFeatures.enable_email_password_login && (
<div className="mb-8 flex justify-between gap-2">
<div>
<div className="system-sm-semibold mb-1 text-text-secondary">{t('common.account.password')}</div>
<div className="body-xs-regular mb-2 text-text-tertiary">{t('common.account.passwordTip')}</div>
<div className="system-sm-semibold mb-1 text-text-secondary">{t('account.password', { ns: 'common' })}</div>
<div className="body-xs-regular mb-2 text-text-tertiary">{t('account.passwordTip', { ns: 'common' })}</div>
</div>
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</Button>
</div>
)
}
<div className="mb-6 border-[1px] border-divider-subtle" />
<div className="mb-8">
<div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
<div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
<div className={titleClassName}>{t('account.langGeniusAccount', { ns: 'common' })}</div>
<div className={descriptionClassName}>{t('account.langGeniusAccountTip', { ns: 'common' })}</div>
{!!apps.length && (
<Collapse
title={`${t('common.account.showAppLength', { length: apps.length })}`}
title={`${t('account.showAppLength', { ns: 'common', length: apps.length })}`}
items={apps.map((app: App) => ({ ...app, key: app.id, name: app.name }))}
renderItem={renderAppItem}
wrapperClassName="mt-2"
/>
)}
{!IS_CE_EDITION && <Button className="mt-2 text-components-button-destructive-secondary-text" onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
{!IS_CE_EDITION && <Button className="mt-2 text-components-button-destructive-secondary-text" onClick={() => setShowDeleteAccountModal(true)}>{t('account.delete', { ns: 'common' })}</Button>}
</div>
{
editNameModalVisible && (
@ -219,21 +219,21 @@ export default function AccountPage() {
onClose={() => setEditNameModalVisible(false)}
className="!w-[420px] !p-6"
>
<div className="title-2xl-semi-bold mb-6 text-text-primary">{t('common.account.editName')}</div>
<div className={titleClassName}>{t('common.account.name')}</div>
<div className="title-2xl-semi-bold mb-6 text-text-primary">{t('account.editName', { ns: 'common' })}</div>
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
<Input
className="mt-2"
value={editName}
onChange={e => setEditName(e.target.value)}
/>
<div className="mt-10 flex justify-end">
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button
disabled={editing || !editName}
variant="primary"
onClick={handleSaveName}
>
{t('common.operation.save')}
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</Modal>
@ -249,10 +249,10 @@ export default function AccountPage() {
}}
className="!w-[420px] !p-6"
>
<div className="title-2xl-semi-bold mb-6 text-text-primary">{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
<div className="title-2xl-semi-bold mb-6 text-text-primary">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
{userProfile.is_password_set && (
<>
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
<div className="relative mt-2">
<Input
type={showCurrentPassword ? 'text' : 'password'}
@ -273,7 +273,7 @@ export default function AccountPage() {
</>
)}
<div className="system-sm-semibold mt-8 text-text-secondary">
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
</div>
<div className="relative mt-2">
<Input
@ -291,7 +291,7 @@ export default function AccountPage() {
</Button>
</div>
</div>
<div className="system-sm-semibold mt-8 text-text-secondary">{t('common.account.confirmPassword')}</div>
<div className="system-sm-semibold mt-8 text-text-secondary">{t('account.confirmPassword', { ns: 'common' })}</div>
<div className="relative mt-2">
<Input
type={showConfirmPassword ? 'text' : 'password'}
@ -316,14 +316,14 @@ export default function AccountPage() {
resetPasswordForm()
}}
>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
disabled={editing}
variant="primary"
onClick={handleSavePassword}
>
{userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')}
{userProfile.is_password_set ? t('operation.reset', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
</Button>
</div>
</Modal>

View File

@ -94,7 +94,7 @@ export default function AppSelector() {
className="group flex h-9 cursor-pointer items-center justify-start rounded-lg px-3 hover:bg-state-base-hover"
>
<LogOut01 className="mr-1 flex h-4 w-4 text-text-tertiary" />
<div className="text-[14px] font-normal text-text-secondary">{t('common.userProfile.logout')}</div>
<div className="text-[14px] font-normal text-text-secondary">{t('userProfile.logout', { ns: 'common' })}</div>
</div>
</div>
</MenuItem>

View File

@ -31,22 +31,22 @@ export default function CheckEmail(props: DeleteAccountProps) {
return (
<>
<div className="body-md-medium py-1 text-text-destructive">
{t('common.account.deleteTip')}
{t('account.deleteTip', { ns: 'common' })}
</div>
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
{t('common.account.deletePrivacyLinkTip')}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('common.account.deletePrivacyLink')}</Link>
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
</div>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('common.account.deleteLabel')}</label>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.deleteLabel', { ns: 'common' })}</label>
<Input
placeholder={t('common.account.deletePlaceholder') as string}
placeholder={t('account.deletePlaceholder', { ns: 'common' }) as string}
onChange={(e) => {
setUserInputEmail(e.target.value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
<Button className="w-full" disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('account.sendVerificationButton', { ns: 'common' })}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
</div>
</>
)

View File

@ -28,7 +28,7 @@ export default function FeedBack(props: DeleteAccountProps) {
await logout()
// Tokens are now stored in cookies and cleared by backend
router.push('/signin')
Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') })
Toast.notify({ type: 'info', message: t('account.deleteSuccessTip', { ns: 'common' }) })
}
catch (error) { console.error(error) }
}, [router, t])
@ -50,22 +50,22 @@ export default function FeedBack(props: DeleteAccountProps) {
<CustomDialog
show={true}
onClose={props.onCancel}
title={t('common.account.feedbackTitle')}
title={t('account.feedbackTitle', { ns: 'common' })}
className="max-w-[480px]"
footer={false}
>
<label className="system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary">{t('common.account.feedbackLabel')}</label>
<label className="system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
<Textarea
rows={6}
value={userFeedback}
placeholder={t('common.account.feedbackPlaceholder') as string}
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
onChange={(e) => {
setUserFeedback(e.target.value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('common.operation.submit')}</Button>
<Button className="w-full" onClick={handleSkip}>{t('common.operation.skip')}</Button>
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('operation.submit', { ns: 'common' })}</Button>
<Button className="w-full" onClick={handleSkip}>{t('operation.skip', { ns: 'common' })}</Button>
</div>
</CustomDialog>
)

View File

@ -37,24 +37,24 @@ export default function VerifyEmail(props: DeleteAccountProps) {
return (
<>
<div className="body-md-medium pt-1 text-text-destructive">
{t('common.account.deleteTip')}
{t('account.deleteTip', { ns: 'common' })}
</div>
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
{t('common.account.deletePrivacyLinkTip')}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('common.account.deletePrivacyLink')}</Link>
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
</div>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('common.account.verificationLabel')}</label>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.verificationLabel', { ns: 'common' })}</label>
<Input
minLength={6}
maxLength={6}
placeholder={t('common.account.verificationPlaceholder') as string}
placeholder={t('account.verificationPlaceholder', { ns: 'common' }) as string}
onChange={(e) => {
setVerificationCode(e.target.value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="warning" onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="warning" onClick={handleConfirm}>{t('account.permanentlyDeleteButton', { ns: 'common' })}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
<Countdown onResend={sendEmail} />
</div>
</>

View File

@ -33,7 +33,7 @@ export default function DeleteAccount(props: DeleteAccountProps) {
<CustomDialog
show={true}
onClose={props.onCancel}
title={t('common.account.delete')}
title={t('account.delete', { ns: 'common' })}
className="max-w-[480px]"
footer={false}
>

View File

@ -32,12 +32,12 @@ const Header = () => {
: <DifyLogo />}
</div>
<div className="h-4 w-[1px] origin-center rotate-[11.31deg] bg-divider-regular" />
<p className="title-3xl-semi-bold relative mt-[-2px] text-text-primary">{t('common.account.account')}</p>
<p className="title-3xl-semi-bold relative mt-[-2px] text-text-primary">{t('account.account', { ns: 'common' })}</p>
</div>
<div className="flex shrink-0 items-center gap-3">
<Button className="system-sm-medium gap-2 px-3 py-2" onClick={goToStudio}>
<RiRobot2Line className="h-4 w-4" />
<p>{t('common.account.studio')}</p>
<p>{t('account.studio', { ns: 'common' })}</p>
<RiArrowRightUpLine className="h-4 w-4" />
</Button>
<div className="h-4 w-[1px] bg-divider-regular" />

View File

@ -1,9 +1,9 @@
import type { ReactNode } from 'react'
import * as React from 'react'
import { AppInitializer } from '@/app/components/app-initializer'
import AmplitudeProvider from '@/app/components/base/amplitude'
import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import SwrInitor from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ModalContextProvider } from '@/context/modal-context'
@ -15,7 +15,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitor>
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
@ -30,7 +30,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</SwrInitor>
</AppInitializer>
</>
)
}

View File

@ -5,7 +5,7 @@ import AccountPage from './account-page'
export default function Account() {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.account'))
useDocumentTitle(t('menus.account', { ns: 'common' }))
return (
<div className="mx-auto w-full max-w-[640px] px-6 pt-12">
<AccountPage />

View File

@ -50,23 +50,23 @@ export default function OAuthAuthorize() {
const SCOPE_INFO_MAP: Record<string, { icon: React.ComponentType<{ className?: string }>, label: string }> = {
'read:name': {
icon: RiInfoCardLine,
label: t('oauth.scopes.name'),
label: t('scopes.name', { ns: 'oauth' }),
},
'read:email': {
icon: RiMailLine,
label: t('oauth.scopes.email'),
label: t('scopes.email', { ns: 'oauth' }),
},
'read:avatar': {
icon: RiAccountCircleLine,
label: t('oauth.scopes.avatar'),
label: t('scopes.avatar', { ns: 'oauth' }),
},
'read:interface_language': {
icon: RiTranslate2,
label: t('oauth.scopes.languagePreference'),
label: t('scopes.languagePreference', { ns: 'oauth' }),
},
'read:timezone': {
icon: RiGlobalLine,
label: t('oauth.scopes.timezone'),
label: t('scopes.timezone', { ns: 'oauth' }),
},
}
@ -106,7 +106,7 @@ export default function OAuthAuthorize() {
catch (err: any) {
Toast.notify({
type: 'error',
message: `${t('oauth.error.authorizeFailed')}: ${err.message}`,
message: `${t('error.authorizeFailed', { ns: 'oauth' })}: ${err.message}`,
})
}
}
@ -117,7 +117,7 @@ export default function OAuthAuthorize() {
hasNotifiedRef.current = true
Toast.notify({
type: 'error',
message: invalidParams ? t('oauth.error.invalidParams') : t('oauth.error.authAppInfoFetchFailed'),
message: invalidParams ? t('error.invalidParams', { ns: 'oauth' }) : t('error.authAppInfoFetchFailed', { ns: 'oauth' }),
duration: 0,
})
}
@ -141,11 +141,11 @@ export default function OAuthAuthorize() {
<div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
<div className="title-4xl-semi-bold">
{isLoggedIn && <div className="text-text-primary">{t('oauth.connect')}</div>}
<div className="text-[var(--color-saas-dify-blue-inverted)]">{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')}</div>
{!isLoggedIn && <div className="text-text-primary">{t('oauth.tips.notLoggedIn')}</div>}
{isLoggedIn && <div className="text-text-primary">{t('connect', { ns: 'oauth' })}</div>}
<div className="text-[var(--color-saas-dify-blue-inverted)]">{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })}</div>
{!isLoggedIn && <div className="text-text-primary">{t('tips.notLoggedIn', { ns: 'oauth' })}</div>}
</div>
<div className="body-md-regular text-text-secondary">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')} ${t('oauth.tips.loggedIn')}` : t('oauth.tips.needLogin')}</div>
<div className="body-md-regular text-text-secondary">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })} ${t('tips.loggedIn', { ns: 'oauth' })}` : t('tips.needLogin', { ns: 'oauth' })}</div>
</div>
{isLoggedIn && userProfile && (
@ -157,7 +157,7 @@ export default function OAuthAuthorize() {
<div className="system-xs-regular text-text-tertiary">{userProfile.email}</div>
</div>
</div>
<Button variant="tertiary" size="small" onClick={onLoginSwitchClick}>{t('oauth.switchAccount')}</Button>
<Button variant="tertiary" size="small" onClick={onLoginSwitchClick}>{t('switchAccount', { ns: 'oauth' })}</Button>
</div>
)}
@ -178,12 +178,12 @@ export default function OAuthAuthorize() {
<div className="flex flex-col items-center gap-2 pt-4">
{!isLoggedIn
? (
<Button variant="primary" size="large" className="w-full" onClick={onLoginSwitchClick}>{t('oauth.login')}</Button>
<Button variant="primary" size="large" className="w-full" onClick={onLoginSwitchClick}>{t('login', { ns: 'oauth' })}</Button>
)
: (
<>
<Button variant="primary" size="large" className="w-full" onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('oauth.continue')}</Button>
<Button size="large" className="w-full" onClick={() => router.push('/apps')}>{t('common.operation.cancel')}</Button>
<Button variant="primary" size="large" className="w-full" onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('continue', { ns: 'oauth' })}</Button>
<Button size="large" className="w-full" onClick={() => router.push('/apps')}>{t('operation.cancel', { ns: 'common' })}</Button>
</>
)}
</div>
@ -199,7 +199,7 @@ export default function OAuthAuthorize() {
</defs>
</svg>
</div>
<div className="system-xs-regular mt-3 text-text-tertiary">{t('oauth.tips.common')}</div>
<div className="system-xs-regular mt-3 text-text-tertiary">{t('tips.common', { ns: 'oauth' })}</div>
</div>
)
}

View File

@ -56,11 +56,11 @@ const ActivateForm = () => {
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-[20px] border border-divider-regular bg-components-option-card-option-bg p-5 text-[40px] font-bold shadow-lg">🤷</div>
<h2 className="text-[32px] font-bold text-text-primary">{t('login.invalid')}</h2>
<h2 className="text-[32px] font-bold text-text-primary">{t('invalid', { ns: 'login' })}</h2>
</div>
<div className="mx-auto mt-6 w-full">
<Button variant="primary" className="w-full !text-sm">
<a href="https://dify.ai">{t('login.explore')}</a>
<a href="https://dify.ai">{t('explore', { ns: 'login' })}</a>
</Button>
</div>
</div>

View File

@ -3,7 +3,6 @@
import type { ReactNode } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
import { SWRConfig } from 'swr'
import {
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
@ -11,12 +10,13 @@ import {
import { fetchSetupStatus } from '@/service/common'
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
type SwrInitializerProps = {
type AppInitializerProps = {
children: ReactNode
}
const SwrInitializer = ({
export const AppInitializer = ({
children,
}: SwrInitializerProps) => {
}: AppInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
// Tokens are now stored in cookies, no need to check localStorage
@ -69,20 +69,5 @@ const SwrInitializer = ({
})()
}, [isSetupFinished, router, pathname, searchParams])
return init
? (
<SWRConfig value={{
shouldRetryOnError: false,
revalidateOnFocus: false,
dedupingInterval: 60000,
focusThrottleInterval: 5000,
provider: () => new Map(),
}}
>
{children}
</SWRConfig>
)
: null
return init ? children : null
}
export default SwrInitializer

View File

@ -100,12 +100,12 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
setShowEditModal(false)
notify({
type: 'success',
message: t('app.editDone'),
message: t('editDone', { ns: 'app' }),
})
setAppDetail(app)
}
catch {
notify({ type: 'error', message: t('app.editFailed') })
notify({ type: 'error', message: t('editFailed', { ns: 'app' }) })
}
}, [appDetail, notify, setAppDetail, t])
@ -124,14 +124,14 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
setShowDuplicateModal(false)
notify({
type: 'success',
message: t('app.newApp.appCreated'),
message: t('newApp.appCreated', { ns: 'app' }),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
onPlanInfoChanged()
getRedirection(true, newApp, replace)
}
catch {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
}
}
@ -152,7 +152,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
URL.revokeObjectURL(url)
}
catch {
notify({ type: 'error', message: t('app.exportFailed') })
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
}
}
@ -181,7 +181,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
setSecretEnvList(list)
}
catch {
notify({ type: 'error', message: t('app.exportFailed') })
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
}
}
@ -190,7 +190,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
return
try {
await deleteApp(appDetail.id)
notify({ type: 'success', message: t('app.appDeleted') })
notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) })
onPlanInfoChanged()
setAppDetail()
replace('/apps')
@ -198,7 +198,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
catch (e: any) {
notify({
type: 'error',
message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
message: `${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`,
})
}
setShowConfirmDelete(false)
@ -212,7 +212,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const primaryOperations = [
{
id: 'edit',
title: t('app.editApp'),
title: t('editApp', { ns: 'app' }),
icon: <RiEditLine />,
onClick: () => {
setOpen(false)
@ -222,7 +222,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
},
{
id: 'duplicate',
title: t('app.duplicate'),
title: t('duplicate', { ns: 'app' }),
icon: <RiFileCopy2Line />,
onClick: () => {
setOpen(false)
@ -232,7 +232,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
},
{
id: 'export',
title: t('app.export'),
title: t('export', { ns: 'app' }),
icon: <RiFileDownloadLine />,
onClick: exportCheck,
},
@ -243,7 +243,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW)
? [{
id: 'import',
title: t('workflow.common.importDSL'),
title: t('common.importDSL', { ns: 'workflow' }),
icon: <RiFileUploadLine />,
onClick: () => {
setOpen(false)
@ -263,7 +263,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
// Delete operation
{
id: 'delete',
title: t('common.operation.delete'),
title: t('operation.delete', { ns: 'common' }),
icon: <RiDeleteBinLine />,
onClick: () => {
setOpen(false)
@ -277,7 +277,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT)
? {
id: 'switch',
title: t('app.switch'),
title: t('switch', { ns: 'app' }),
icon: <RiExchange2Line />,
onClick: () => {
setOpen(false)
@ -331,14 +331,14 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
</div>
<div className="system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary">
{appDetail.mode === AppModeEnum.ADVANCED_CHAT
? t('app.types.advanced')
? t('types.advanced', { ns: 'app' })
: appDetail.mode === AppModeEnum.AGENT_CHAT
? t('app.types.agent')
? t('types.agent', { ns: 'app' })
: appDetail.mode === AppModeEnum.CHAT
? t('app.types.chatbot')
? t('types.chatbot', { ns: 'app' })
: appDetail.mode === AppModeEnum.COMPLETION
? t('app.types.completion')
: t('app.types.workflow')}
? t('types.completion', { ns: 'app' })
: t('types.workflow', { ns: 'app' })}
</div>
</div>
)}
@ -364,7 +364,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
/>
<div className="flex flex-1 flex-col items-start justify-center overflow-hidden">
<div className="system-md-semibold w-full truncate text-text-secondary">{appDetail.name}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('types.advanced', { ns: 'app' }) : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('types.agent', { ns: 'app' }) : appDetail.mode === AppModeEnum.CHAT ? t('types.chatbot', { ns: 'app' }) : appDetail.mode === AppModeEnum.COMPLETION ? t('types.completion', { ns: 'app' }) : t('types.workflow', { ns: 'app' })}</div>
</div>
</div>
{/* description */}
@ -438,8 +438,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
)}
{showConfirmDelete && (
<Confirm
title={t('app.deleteAppConfirmTitle')}
content={t('app.deleteAppConfirmContent')}
title={t('deleteAppConfirmTitle', { ns: 'app' })}
content={t('deleteAppConfirmContent', { ns: 'app' })}
isShow={showConfirmDelete}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
@ -462,8 +462,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
<Confirm
type="info"
isShow={showExportWarning}
title={t('workflow.sidebar.exportWarning')}
content={t('workflow.sidebar.exportWarningDesc')}
title={t('sidebar.exportWarning', { ns: 'workflow' })}
content={t('sidebar.exportWarningDesc', { ns: 'workflow' })}
onConfirm={handleConfirmExport}
onCancel={() => setShowExportWarning(false)}
/>

View File

@ -148,7 +148,7 @@ const AppOperations = ({
>
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<span className="system-xs-medium text-components-button-secondary-text">
{t('common.operation.more')}
{t('operation.more', { ns: 'common' })}
</span>
</Button>
</div>
@ -183,7 +183,7 @@ const AppOperations = ({
>
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<span className="system-xs-medium text-components-button-secondary-text">
{t('common.operation.more')}
{t('operation.more', { ns: 'common' })}
</span>
</Button>
</PortalToFollowElemTrigger>

View File

@ -99,7 +99,7 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
<div className="flex w-full">
<div className="system-md-semibold truncate text-text-secondary">{appDetail.name}</div>
</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('types.advanced', { ns: 'app' }) : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('types.agent', { ns: 'app' }) : appDetail.mode === AppModeEnum.CHAT ? t('types.chatbot', { ns: 'app' }) : appDetail.mode === AppModeEnum.COMPLETION ? t('types.completion', { ns: 'app' }) : t('types.workflow', { ns: 'app' })}</div>
</div>
</div>
</div>

View File

@ -98,7 +98,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
<div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div>
)}
{!hideType && !isExtraInLine && (
<div className="system-2xs-medium-uppercase text-text-tertiary">{isExternal ? t('dataset.externalTag') : type}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{isExternal ? t('externalTag', { ns: 'dataset' }) : type}</div>
)}
</div>
)}

View File

@ -73,14 +73,14 @@ const DropDown = ({
URL.revokeObjectURL(url)
}
catch {
Toast.notify({ type: 'error', message: t('app.exportFailed') })
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
}
}, [dataset, exportPipelineConfig, handleTrigger, t])
const detectIsUsedByApp = useCallback(async () => {
try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
setConfirmMessage(isUsedByApp ? t('datasetUsedByApp', { ns: 'dataset' })! : t('deleteDatasetConfirmContent', { ns: 'dataset' })!)
setShowConfirmDelete(true)
}
catch (e: any) {
@ -95,7 +95,7 @@ const DropDown = ({
const onConfirmDelete = useCallback(async () => {
try {
await deleteDataset(dataset.id)
Toast.notify({ type: 'success', message: t('dataset.datasetDeleted') })
Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) })
invalidDatasetList()
replace('/datasets')
}
@ -141,7 +141,7 @@ const DropDown = ({
)}
{showConfirmDelete && (
<Confirm
title={t('dataset.deleteDatasetConfirmTitle')}
title={t('deleteDatasetConfirmTitle', { ns: 'dataset' })}
content={confirmMessage}
isShow={showConfirmDelete}
onConfirm={onConfirmDelete}

View File

@ -70,10 +70,10 @@ const DatasetInfo: FC<DatasetInfoProps> = ({
{dataset.name}
</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">
{isExternalProvider && t('dataset.externalTag')}
{isExternalProvider && t('externalTag', { ns: 'dataset' })}
{!isExternalProvider && isPipelinePublished && dataset.doc_form && dataset.indexing_technique && (
<div className="flex items-center gap-x-2">
<span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any) as string}</span>
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div>
)}

View File

@ -26,13 +26,13 @@ const Menu = ({
<div className="flex flex-col p-1">
<MenuItem
Icon={RiEditLine}
name={t('common.operation.edit')}
name={t('operation.edit', { ns: 'common' })}
handleClick={openRenameModal}
/>
{runtimeMode === 'rag_pipeline' && (
<MenuItem
Icon={RiFileDownloadLine}
name={t('datasetPipeline.operations.exportPipeline')}
name={t('operations.exportPipeline', { ns: 'datasetPipeline' })}
handleClick={handleExportPipeline}
/>
)}
@ -43,7 +43,7 @@ const Menu = ({
<div className="flex flex-col p-1">
<MenuItem
Icon={RiDeleteBinLine}
name={t('common.operation.delete')}
name={t('operation.delete', { ns: 'common' })}
handleClick={detectIsUsedByApp}
/>
</div>

View File

@ -113,10 +113,10 @@ const DatasetSidebarDropdown = ({
{dataset.name}
</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">
{isExternalProvider && t('dataset.externalTag')}
{isExternalProvider && t('externalTag', { ns: 'dataset' })}
{!isExternalProvider && dataset.doc_form && dataset.indexing_technique && (
<div className="flex items-center gap-x-2">
<span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any) as string}</span>
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div>
)}

View File

@ -19,7 +19,7 @@ const TooltipContent = ({
return (
<div className="flex items-center gap-x-1">
<span className="system-xs-medium px-0.5 text-text-secondary">{expand ? t('layout.sidebar.collapseSidebar') : t('layout.sidebar.expandSidebar')}</span>
<span className="system-xs-medium px-0.5 text-text-secondary">{expand ? t('sidebar.collapseSidebar', { ns: 'layout' }) : t('sidebar.expandSidebar', { ns: 'layout' })}</span>
<div className="flex items-center gap-x-0.5">
{
TOGGLE_SHORTCUT.map(key => (

View File

@ -22,8 +22,8 @@ const EditItem: FC<Props> = ({
}) => {
const { t } = useTranslation()
const avatar = type === EditItemType.Query ? <User className="h-6 w-6" /> : <Robot className="h-6 w-6" />
const name = type === EditItemType.Query ? t('appAnnotation.addModal.queryName') : t('appAnnotation.addModal.answerName')
const placeholder = type === EditItemType.Query ? t('appAnnotation.addModal.queryPlaceholder') : t('appAnnotation.addModal.answerPlaceholder')
const name = type === EditItemType.Query ? t('addModal.queryName', { ns: 'appAnnotation' }) : t('addModal.answerName', { ns: 'appAnnotation' })
const placeholder = type === EditItemType.Query ? t('addModal.queryPlaceholder', { ns: 'appAnnotation' }) : t('addModal.answerPlaceholder', { ns: 'appAnnotation' })
return (
<div className="flex" onClick={e => e.stopPropagation()}>

View File

@ -33,10 +33,10 @@ const AddAnnotationModal: FC<Props> = ({
const isValid = (payload: AnnotationItemBasic) => {
if (!payload.question)
return t('appAnnotation.errorMessage.queryRequired')
return t('errorMessage.queryRequired', { ns: 'appAnnotation' })
if (!payload.answer)
return t('appAnnotation.errorMessage.answerRequired')
return t('errorMessage.answerRequired', { ns: 'appAnnotation' })
return true
}
@ -76,7 +76,7 @@ const AddAnnotationModal: FC<Props> = ({
isShow={isShow}
onHide={onHide}
maxWidthClassName="!max-w-[480px]"
title={t('appAnnotation.addModal.title') as string}
title={t('addModal.title', { ns: 'appAnnotation' }) as string}
body={(
<div className="space-y-6 p-6 pb-4">
<EditItem
@ -104,11 +104,11 @@ const AddAnnotationModal: FC<Props> = ({
className="flex items-center space-x-2"
>
<Checkbox id="create-next-checkbox" checked={isCreateNext} onCheck={() => setIsCreateNext(!isCreateNext)} />
<div>{t('appAnnotation.addModal.createNext')}</div>
<div>{t('addModal.createNext', { ns: 'appAnnotation' })}</div>
</div>
<div className="mt-2 flex space-x-2">
<Button className="h-7 text-xs" onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button className="h-7 text-xs" variant="primary" onClick={handleSave} loading={isSaving} disabled={isAnnotationFull}>{t('common.operation.add')}</Button>
<Button className="h-7 text-xs" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="h-7 text-xs" variant="primary" onClick={handleSave} loading={isSaving} disabled={isAnnotationFull}>{t('operation.add', { ns: 'common' })}</Button>
</div>
</div>
</div>

View File

@ -7,7 +7,7 @@ import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { cn } from '@/utils/classnames'
const i18nPrefix = 'appAnnotation.batchAction'
const i18nPrefix = 'batchAction'
type IBatchActionProps = {
className?: string
@ -45,27 +45,27 @@ const BatchAction: FC<IBatchActionProps> = ({
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-text-accent px-1 py-0.5 text-xs font-medium text-text-primary-on-surface">
{selectedIds.length}
</span>
<span className="text-[13px] font-semibold leading-[16px] text-text-accent">{t(`${i18nPrefix}.selected`)}</span>
<span className="text-[13px] font-semibold leading-[16px] text-text-accent">{t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}</span>
</div>
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
<div className="flex cursor-pointer items-center gap-x-0.5 px-3 py-2" onClick={showDeleteConfirm}>
<RiDeleteBinLine className="h-4 w-4 text-components-button-destructive-ghost-text" />
<button type="button" className="px-0.5 text-[13px] font-medium leading-[16px] text-components-button-destructive-ghost-text">
{t('common.operation.delete')}
{t('operation.delete', { ns: 'common' })}
</button>
</div>
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
<button type="button" className="px-3.5 py-2 text-[13px] font-medium leading-[16px] text-components-button-ghost-text" onClick={onCancel}>
{t('common.operation.cancel')}
{t('operation.cancel', { ns: 'common' })}
</button>
</div>
{
isShowDeleteConfirm && (
<Confirm
isShow
title={t('appAnnotation.list.delete.title')}
confirmText={t('common.operation.delete')}
title={t('list.delete.title', { ns: 'appAnnotation' })}
confirmText={t('operation.delete', { ns: 'common' })}
onConfirm={handleBatchDelete}
onCancel={hideDeleteConfirm}
isLoading={isDeleting}

View File

@ -33,36 +33,36 @@ const CSVDownload: FC = () => {
return (
<div className="mt-6">
<div className="system-sm-medium text-text-primary">{t('share.generation.csvStructureTitle')}</div>
<div className="system-sm-medium text-text-primary">{t('generation.csvStructureTitle', { ns: 'share' })}</div>
<div className="mt-2 max-h-[500px] overflow-auto">
<table className="w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-regular text-xs">
<thead className="text-text-tertiary">
<tr>
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('appAnnotation.batchModal.question')}</td>
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('appAnnotation.batchModal.answer')}</td>
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('batchModal.question', { ns: 'appAnnotation' })}</td>
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('batchModal.answer', { ns: 'appAnnotation' })}</td>
</tr>
</thead>
<tbody className="text-text-secondary">
<tr>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]">
{t('appAnnotation.batchModal.question')}
{t('batchModal.question', { ns: 'appAnnotation' })}
{' '}
1
</td>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]">
{t('appAnnotation.batchModal.answer')}
{t('batchModal.answer', { ns: 'appAnnotation' })}
{' '}
1
</td>
</tr>
<tr>
<td className="h-9 pl-3 pr-2 text-[13px]">
{t('appAnnotation.batchModal.question')}
{t('batchModal.question', { ns: 'appAnnotation' })}
{' '}
2
</td>
<td className="h-9 pl-3 pr-2 text-[13px]">
{t('appAnnotation.batchModal.answer')}
{t('batchModal.answer', { ns: 'appAnnotation' })}
{' '}
2
</td>
@ -79,7 +79,7 @@ const CSVDownload: FC = () => {
>
<div className="system-xs-medium flex h-[18px] items-center space-x-1 text-text-accent">
<DownloadIcon className="mr-1 h-3 w-3" />
{t('appAnnotation.batchModal.template')}
{t('batchModal.template', { ns: 'appAnnotation' })}
</div>
</CSVDownloader>
</div>

View File

@ -50,7 +50,7 @@ const CSVUploader: FC<Props> = ({
return
const files = [...e.dataTransfer.files]
if (files.length > 1) {
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
return
}
updateFile(files[0])
@ -98,8 +98,8 @@ const CSVUploader: FC<Props> = ({
<div className="flex w-full items-center justify-center space-x-2">
<CSVIcon className="shrink-0" />
<div className="text-text-tertiary">
{t('appAnnotation.batchModal.csvUploadTitle')}
<span className="cursor-pointer text-text-accent" onClick={selectHandle}>{t('appAnnotation.batchModal.browse')}</span>
{t('batchModal.csvUploadTitle', { ns: 'appAnnotation' })}
<span className="cursor-pointer text-text-accent" onClick={selectHandle}>{t('batchModal.browse', { ns: 'appAnnotation' })}</span>
</div>
</div>
{dragging && <div ref={dragRef} className="absolute left-0 top-0 h-full w-full" />}
@ -113,7 +113,7 @@ const CSVUploader: FC<Props> = ({
<span className="shrink-0 text-text-tertiary">.csv</span>
</div>
<div className="hidden items-center group-hover:flex">
<Button variant="secondary" onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
<Button variant="secondary" onClick={selectHandle}>{t('stepOne.uploader.change', { ns: 'datasetCreation' })}</Button>
<div className="mx-2 h-4 w-px bg-divider-regular" />
<div className="cursor-pointer p-2" onClick={removeFile} data-testid="remove-file-button">
<RiDeleteBinLine className="h-4 w-4 text-text-tertiary" />

View File

@ -54,15 +54,15 @@ const BatchModal: FC<IBatchModalProps> = ({
if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
setTimeout(() => checkProcess(res.job_id), 2500)
if (res.job_status === ProcessStatus.ERROR)
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}` })
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}` })
if (res.job_status === ProcessStatus.COMPLETED) {
notify({ type: 'success', message: `${t('appAnnotation.batchModal.completed')}` })
notify({ type: 'success', message: `${t('batchModal.completed', { ns: 'appAnnotation' })}` })
onAdded()
onCancel()
}
}
catch (e: any) {
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` })
}
}
@ -78,7 +78,7 @@ const BatchModal: FC<IBatchModalProps> = ({
checkProcess(res.job_id)
}
catch (e: any) {
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` })
}
}
@ -90,7 +90,7 @@ const BatchModal: FC<IBatchModalProps> = ({
return (
<Modal isShow={isShow} onClose={noop} className="!max-w-[520px] !rounded-xl px-8 py-6">
<div className="system-xl-medium relative pb-1 text-text-primary">{t('appAnnotation.batchModal.title')}</div>
<div className="system-xl-medium relative pb-1 text-text-primary">{t('batchModal.title', { ns: 'appAnnotation' })}</div>
<div className="absolute right-4 top-4 cursor-pointer p-2" onClick={onCancel}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
@ -108,7 +108,7 @@ const BatchModal: FC<IBatchModalProps> = ({
<div className="mt-[28px] flex justify-end pt-6">
<Button className="system-sm-medium mr-2 text-text-tertiary" onClick={onCancel}>
{t('appAnnotation.batchModal.cancel')}
{t('batchModal.cancel', { ns: 'appAnnotation' })}
</Button>
<Button
variant="primary"
@ -116,7 +116,7 @@ const BatchModal: FC<IBatchModalProps> = ({
disabled={isAnnotationFull || !currentCSV}
loading={importStatus === ProcessStatus.PROCESSING || importStatus === ProcessStatus.WAITING}
>
{t('appAnnotation.batchModal.run')}
{t('batchModal.run', { ns: 'appAnnotation' })}
</Button>
</div>
</Modal>

View File

@ -4,13 +4,16 @@ import ClearAllAnnotationsConfirmModal from './index'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
t: (key: string, options?: { ns?: string }) => {
const translations: Record<string, string> = {
'appAnnotation.table.header.clearAllConfirm': 'Clear all annotations?',
'common.operation.confirm': 'Confirm',
'common.operation.cancel': 'Cancel',
'table.header.clearAllConfirm': 'Clear all annotations?',
'operation.confirm': 'Confirm',
'operation.cancel': 'Cancel',
}
return translations[key] || key
if (translations[key])
return translations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))

View File

@ -24,7 +24,7 @@ const ClearAllAnnotationsConfirmModal: FC<Props> = ({
onCancel={onHide}
onConfirm={onConfirm}
type="danger"
title={t('appAnnotation.table.header.clearAllConfirm')}
title={t('table.header.clearAllConfirm', { ns: 'appAnnotation' })}
/>
)
}

View File

@ -43,9 +43,9 @@ const EditItem: FC<Props> = ({
const [newContent, setNewContent] = useState('')
const showNewContent = newContent && newContent !== content
const avatar = type === EditItemType.Query ? <User className="h-6 w-6" /> : <Robot className="h-6 w-6" />
const name = type === EditItemType.Query ? t('appAnnotation.editModal.queryName') : t('appAnnotation.editModal.answerName')
const editTitle = type === EditItemType.Query ? t('appAnnotation.editModal.yourQuery') : t('appAnnotation.editModal.yourAnswer')
const placeholder = type === EditItemType.Query ? t('appAnnotation.editModal.queryPlaceholder') : t('appAnnotation.editModal.answerPlaceholder')
const name = type === EditItemType.Query ? t('editModal.queryName', { ns: 'appAnnotation' }) : t('editModal.answerName', { ns: 'appAnnotation' })
const editTitle = type === EditItemType.Query ? t('editModal.yourQuery', { ns: 'appAnnotation' }) : t('editModal.yourAnswer', { ns: 'appAnnotation' })
const placeholder = type === EditItemType.Query ? t('editModal.queryPlaceholder', { ns: 'appAnnotation' }) : t('editModal.answerPlaceholder', { ns: 'appAnnotation' })
const [isEdit, setIsEdit] = useState(false)
// Reset newContent when content prop changes
@ -95,7 +95,7 @@ const EditItem: FC<Props> = ({
}}
>
<RiEditLine className="mr-1 h-3.5 w-3.5" />
<div>{t('common.operation.edit')}</div>
<div>{t('operation.edit', { ns: 'common' })}</div>
</div>
)}
@ -119,7 +119,7 @@ const EditItem: FC<Props> = ({
<div className="h-3.5 w-3.5">
<RiDeleteBinLine className="h-3.5 w-3.5" />
</div>
<div>{t('common.operation.delete')}</div>
<div>{t('operation.delete', { ns: 'common' })}</div>
</div>
</div>
)}
@ -136,8 +136,8 @@ const EditItem: FC<Props> = ({
autoFocus
/>
<div className="mt-2 flex space-x-2">
<Button size="small" variant="primary" onClick={handleSave}>{t('common.operation.save')}</Button>
<Button size="small" onClick={handleCancel}>{t('common.operation.cancel')}</Button>
<Button size="small" variant="primary" onClick={handleSave}>{t('operation.save', { ns: 'common' })}</Button>
<Button size="small" onClick={handleCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
</div>
</div>
)}

View File

@ -73,12 +73,12 @@ const EditAnnotationModal: FC<Props> = ({
}
Toast.notify({
message: t('common.api.actionSuccess') as string,
message: t('api.actionSuccess', { ns: 'common' }) as string,
type: 'success',
})
}
catch (error) {
const fallbackMessage = t('common.api.actionFailed') as string
const fallbackMessage = t('api.actionFailed', { ns: 'common' }) as string
const message = error instanceof Error && error.message ? error.message : fallbackMessage
Toast.notify({
message,
@ -96,7 +96,7 @@ const EditAnnotationModal: FC<Props> = ({
isShow={isShow}
onHide={onHide}
maxWidthClassName="!max-w-[480px]"
title={t('appAnnotation.editModal.title') as string}
title={t('editModal.title', { ns: 'appAnnotation' }) as string}
body={(
<div>
<div className="space-y-6 p-6 pb-4">
@ -120,7 +120,7 @@ const EditAnnotationModal: FC<Props> = ({
setShowModal(false)
onHide()
}}
title={t('appDebug.feature.annotation.removeConfirm')}
title={t('feature.annotation.removeConfirm', { ns: 'appDebug' })}
/>
</div>
</div>
@ -142,13 +142,13 @@ const EditAnnotationModal: FC<Props> = ({
onClick={() => setShowModal(true)}
>
<MessageCheckRemove />
<div>{t('appAnnotation.editModal.removeThisCache')}</div>
<div>{t('editModal.removeThisCache', { ns: 'appAnnotation' })}</div>
</div>
{createdAt && (
<div>
{t('appAnnotation.editModal.createdAt')}
{t('editModal.createdAt', { ns: 'appAnnotation' })}
&nbsp;
{formatTime(createdAt, t('appLog.dateTimeFormat') as string)}
{formatTime(createdAt, t('dateTimeFormat', { ns: 'appLog' }) as string)}
</div>
)}
</div>

View File

@ -18,11 +18,11 @@ const EmptyElement: FC = () => {
<div className="flex h-full items-center justify-center">
<div className="box-border h-fit w-[560px] rounded-2xl bg-background-section-burn px-5 py-4">
<span className="system-md-semibold text-text-secondary">
{t('appAnnotation.noData.title')}
{t('noData.title', { ns: 'appAnnotation' })}
<ThreeDotsIcon className="relative -left-1.5 -top-3 inline" />
</span>
<div className="system-sm-regular mt-2 text-text-tertiary">
{t('appAnnotation.noData.description')}
{t('noData.description', { ns: 'appAnnotation' })}
</div>
</div>
</div>

View File

@ -33,7 +33,7 @@ const Filter: FC<IFilterProps> = ({
showLeftIcon
showClearIcon
value={queryParams.keyword}
placeholder={t('common.operation.search')!}
placeholder={t('operation.search', { ns: 'common' })!}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}

View File

@ -108,12 +108,12 @@ const HeaderOptions: FC<Props> = ({
}}
>
<FilePlus02 className="h-4 w-4 text-text-tertiary" />
<span className="system-sm-regular grow text-left text-text-secondary">{t('appAnnotation.table.header.bulkImport')}</span>
<span className="system-sm-regular grow text-left text-text-secondary">{t('table.header.bulkImport', { ns: 'appAnnotation' })}</span>
</button>
<Menu as="div" className="relative h-full w-full">
<MenuButton className="mx-1 flex h-9 w-[calc(100%_-_8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50">
<FileDownload02 className="h-4 w-4 text-text-tertiary" />
<span className="system-sm-regular grow text-left text-text-secondary">{t('appAnnotation.table.header.bulkExport')}</span>
<span className="system-sm-regular grow text-left text-text-secondary">{t('table.header.bulkExport', { ns: 'appAnnotation' })}</span>
<ChevronRight className="h-[14px] w-[14px] shrink-0 text-text-tertiary" />
</MenuButton>
<Transition
@ -156,7 +156,7 @@ const HeaderOptions: FC<Props> = ({
>
<RiDeleteBinLine className="h-4 w-4" />
<span className="system-sm-regular grow text-left">
{t('appAnnotation.table.header.clearAll')}
{t('table.header.clearAll', { ns: 'appAnnotation' })}
</span>
</button>
</div>
@ -169,7 +169,7 @@ const HeaderOptions: FC<Props> = ({
<div className="flex space-x-2">
<Button variant="primary" onClick={() => setShowAddModal(true)}>
<RiAddLine className="mr-0.5 h-4 w-4" />
<div>{t('appAnnotation.table.header.addAnnotation')}</div>
<div>{t('table.header.addAnnotation', { ns: 'appAnnotation' })}</div>
</Button>
<CustomPopover
htmlContent={<Operations />}

View File

@ -98,14 +98,14 @@ const Annotation: FC<Props> = (props) => {
const handleAdd = async (payload: AnnotationItemBasic) => {
await addAnnotation(appDetail.id, payload)
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
fetchList()
setControlUpdateList(Date.now())
}
const handleRemove = async (id: string) => {
await delAnnotation(appDetail.id, id)
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
fetchList()
setControlUpdateList(Date.now())
}
@ -113,13 +113,13 @@ const Annotation: FC<Props> = (props) => {
const handleBatchDelete = async () => {
try {
await delAnnotations(appDetail.id, selectedIds)
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
fetchList()
setControlUpdateList(Date.now())
setSelectedIds([])
}
catch (e: any) {
Toast.notify({ type: 'error', message: e.message || t('common.api.actionFailed') })
Toast.notify({ type: 'error', message: e.message || t('api.actionFailed', { ns: 'common' }) })
}
}
@ -132,7 +132,7 @@ const Annotation: FC<Props> = (props) => {
if (!currItem)
return
await editAnnotation(appDetail.id, currItem.id, { question, answer })
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
fetchList()
setControlUpdateList(Date.now())
}
@ -144,7 +144,7 @@ const Annotation: FC<Props> = (props) => {
return (
<div className="flex h-full flex-col">
<p className="system-sm-regular text-text-tertiary">{t('appLog.description')}</p>
<p className="system-sm-regular text-text-tertiary">{t('description', { ns: 'appLog' })}</p>
<div className="relative flex h-full flex-1 flex-col py-4">
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
<div className="flex items-center space-x-2">
@ -152,7 +152,7 @@ const Annotation: FC<Props> = (props) => {
<>
<div className={cn(!annotationConfig?.enabled && 'pr-2', 'flex h-7 items-center space-x-1 rounded-lg border border-components-panel-border bg-components-panel-bg-blur pl-2')}>
<MessageFast className="h-4 w-4 text-util-colors-indigo-indigo-600" />
<div className="system-sm-medium text-text-primary">{t('appAnnotation.name')}</div>
<div className="system-sm-medium text-text-primary">{t('name', { ns: 'appAnnotation' })}</div>
<Switch
key={controlRefreshSwitch}
defaultValue={annotationConfig?.enabled}
@ -171,7 +171,7 @@ const Annotation: FC<Props> = (props) => {
await ensureJobCompleted(jobId, AnnotationEnableStatus.disable)
await fetchAnnotationConfig()
Toast.notify({
message: t('common.api.actionSuccess'),
message: t('api.actionSuccess', { ns: 'common' }),
type: 'success',
})
}
@ -264,7 +264,7 @@ const Annotation: FC<Props> = (props) => {
await fetchAnnotationConfig()
Toast.notify({
message: t('common.api.actionSuccess'),
message: t('api.actionSuccess', { ns: 'common' }),
type: 'success',
})
setIsShowEdit(false)

View File

@ -68,11 +68,11 @@ const List: FC<Props> = ({
onCheck={handleSelectAll}
/>
</td>
<td className="w-5 whitespace-nowrap bg-background-section-burn pl-2 pr-1">{t('appAnnotation.table.header.question')}</td>
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.answer')}</td>
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.createdAt')}</td>
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.hits')}</td>
<td className="w-[96px] whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.actions')}</td>
<td className="w-5 whitespace-nowrap bg-background-section-burn pl-2 pr-1">{t('table.header.question', { ns: 'appAnnotation' })}</td>
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('table.header.answer', { ns: 'appAnnotation' })}</td>
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('table.header.createdAt', { ns: 'appAnnotation' })}</td>
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('table.header.hits', { ns: 'appAnnotation' })}</td>
<td className="w-[96px] whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3">{t('table.header.actions', { ns: 'appAnnotation' })}</td>
</tr>
</thead>
<tbody className="system-sm-regular text-text-secondary">
@ -110,7 +110,7 @@ const List: FC<Props> = ({
>
{item.answer}
</td>
<td className="p-3 pr-2">{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td className="p-3 pr-2">{formatTime(item.created_at, t('dateTimeFormat', { ns: 'appLog' }) as string)}</td>
<td className="p-3 pr-2">{item.hit_count}</td>
<td className="w-[96px] p-3 pr-2" onClick={e => e.stopPropagation()}>
{/* Actions */}

Some files were not shown because too many files have changed in this diff Show More