Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/core/src/PageAgentCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,13 @@ export class PageAgentCore extends EventTarget {
return this.config.customSystemPrompt
}

const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
const targetLanguage =
this.config.language === 'zh-CN'
? '中文'
: this.config.language?.startsWith('ar')
? 'العربية'
: 'English'

const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { PageAgentCore } from './PageAgentCore'
import type { PageAgentTool } from './tools'

/** Supported UI languages */
export type SupportedLanguage = 'en-US' | 'zh-CN'
export type SupportedLanguage = 'en-US' | 'zh-CN' | 'ar-EG' | 'ar'

export interface AgentConfig extends LLMConfig {
language?: SupportedLanguage
Expand Down
11 changes: 7 additions & 4 deletions packages/extension/src/agent/MultiPageAgent.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { type AgentConfig, PageAgentCore } from '@page-agent/core'
import { type AgentConfig, PageAgentCore, type SupportedLanguage } from '@page-agent/core'

import { RemotePageController } from './RemotePageController'
import { TabsController } from './TabsController'
import SYSTEM_PROMPT from './system_prompt.md?raw'
import { createTabTools } from './tabTools'

/** Detect user language from browser settings */
function detectLanguage(): 'en-US' | 'zh-CN' {
function detectLanguage(): SupportedLanguage {
const lang = navigator.language || navigator.languages?.[0] || 'en-US'
return lang.startsWith('zh') ? 'zh-CN' : 'en-US'
if (lang.startsWith('zh')) return 'zh-CN'
if (lang.startsWith('ar')) return 'ar-EG'
return 'en-US'
}

interface MultiPageAgentConfig extends AgentConfig {
Expand All @@ -30,7 +32,8 @@ export class MultiPageAgent extends PageAgentCore {

// system prompt - auto-detect language if not specified
const language = config.language ?? detectLanguage()
const targetLanguage = language === 'zh-CN' ? '中文' : 'English'
const targetLanguage =
language === 'zh-CN' ? '中文' : language.startsWith('ar') ? 'العربية' : 'English'
const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
Expand Down
1 change: 1 addition & 0 deletions packages/extension/src/components/ConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<option value="">System</option>
<option value="en-US">English</option>
<option value="zh-CN">中文</option>
<option value="ar-EG">العربية</option>
</select>
</div>

Expand Down
4 changes: 2 additions & 2 deletions packages/page-agent/src/demo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* IIFE demo entry - auto-initializes with built-in demo API for testing
*/
import { PageAgent, type PageAgentConfig } from './PageAgent'
import { PageAgent, type PageAgentConfig, type SupportedLanguage } from './PageAgent'

const currentScript = document.currentScript as HTMLScriptElement | null
const currentScriptURL = currentScript?.src ? new URL(currentScript.src) : null
Expand Down Expand Up @@ -32,7 +32,7 @@ if (autoInit) {
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
const language = (url.searchParams.get('lang') as SupportedLanguage) || 'zh-CN'
showPanel = ((url.searchParams.get('showPanel') as 'true' | 'false') || 'true') === 'true'
config = { model, baseURL, apiKey, language }
} else {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"test": "vitest run",
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
}
}

20 changes: 20 additions & 0 deletions packages/ui/src/i18n/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'

import { I18n } from './index'

describe('I18n Arabic Support', () => {
it('should translate correctly for ar-EG', () => {
const i18n = new I18n('ar-EG')
expect(i18n.getLanguage()).toBe('ar-EG')
expect(i18n.t('ui.panel.ready')).toBe('جاهز')
expect(i18n.t('ui.panel.thinking')).toBe('جاري التفكير...')
expect(i18n.t('ui.panel.step', { number: 5 })).toBe('الخطوة 5')
})

it('should translate correctly for ar alias', () => {
const i18n = new I18n('ar')
expect(i18n.getLanguage()).toBe('ar')
expect(i18n.t('ui.panel.stop')).toBe('إيقاف')
expect(i18n.t('ui.tools.done')).toBe('انتهت المهمة')
})
})
50 changes: 50 additions & 0 deletions packages/ui/src/i18n/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,54 @@ const zhCN = {
},
} as const

// Arabic translations (must match the structure of enUS)
const arEG = {
ui: {
panel: {
ready: 'جاهز',
thinking: 'جاري التفكير...',
taskInput: 'أدخل مهمة جديدة، واشرح الخطوات بالتفصيل، واضغط Enter للإرسال',
userAnswerPrompt: 'يرجى الإجابة على السؤال أعلاه، واضغط Enter للإرسال',
taskTerminated: 'تم إنهاء المهمة',
taskCompleted: 'تمت المهمة',
userAnswer: 'إجابة المستخدم: {{input}}',
question: 'سؤال: {{question}}',
waitingPlaceholder: 'في انتظار بدء المهمة...',
stop: 'إيقاف',
close: 'إغلاق',
expand: 'توسيع السجل',
collapse: 'طي السجل',
step: 'الخطوة {{number}}',
},
tools: {
clicking: 'جاري النقر على العنصر [{{index}}]...',
inputting: 'جاري إدخال النص في العنصر [{{index}}]...',
selecting: 'جاري تحديد الخيار "{{text}}"...',
scrolling: 'جاري التمرير في الصفحة...',
waiting: 'جاري الانتظار {{seconds}} ثوانٍ...',
askingUser: 'جاري سؤال المستخدم...',
done: 'انتهت المهمة',
clicked: '🖱️ تم النقر على العنصر [{{index}}]',
inputted: '⌨️ تم إدخال النص "{{text}}"',
selected: '☑️ تم تحديد الخيار "{{text}}"',
scrolled: '🛞 تم تمرير الصفحة',
waited: '⌛️ اكتمل الانتظار',
executing: 'جاري تنفيذ {{toolName}}...',
resultSuccess: 'نجاح',
resultFailure: 'فشل',
resultError: 'خطأ',
},
errors: {
elementNotFound: 'لم يتم العثور على عنصر تفاعلي عند الفهرس {{index}}',
taskRequired: 'وصف المهمة مطلوب',
executionFailed: 'فشل تنفيذ المهمة',
notInputElement: 'العنصر ليس حقل إدخال أو منطقة نصية',
notSelectElement: 'العنصر ليس قائمة اختيار',
optionNotFound: 'لم يتم العثور على الخيار "{{text}}"',
},
},
} as const

// Type definitions generated from English base structure (but with string values)
type DeepStringify<T> = {
[K in keyof T]: T[K] extends string ? string : T[K] extends object ? DeepStringify<T[K]> : T[K]
Expand All @@ -117,6 +165,8 @@ export type TranslationParams = Record<string, string | number>
export const locales = {
'en-US': enUS,
'zh-CN': zhCN,
'ar-EG': arEG,
ar: arEG,
} as const

export type SupportedLanguage = keyof typeof locales
3 changes: 3 additions & 0 deletions packages/ui/src/panel/Panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ export class Panel {
wrapper.className = styles.wrapper
wrapper.setAttribute('data-browser-use-ignore', 'true')
wrapper.setAttribute('data-page-agent-ignore', 'true')
if (this.#i18n.getLanguage().startsWith('ar')) {
wrapper.setAttribute('dir', 'rtl')
}

wrapper.innerHTML = `
<div class="${styles.background}"></div>
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/vitest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { configDefaults, defineConfig } from 'vitest/config'

export default defineConfig({
test: {
environment: 'jsdom',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the missing jsdom dependency for UI tests

In a clean checkout, the new @page-agent/ui test script is picked up by root npm test/CI, and this config asks Vitest to start the jsdom environment; Vitest documents that this environment uses the jsdom package (docs), but the repo only declares happy-dom and vitest, not jsdom, so the UI test workspace will fail before running any tests. Add jsdom to the repo devDependencies/lockfile or use the already-installed happy-dom/default node environment instead.

Useful? React with 👍 / 👎.

include: ['src/**/*.test.ts'],
exclude: [...configDefaults.exclude],
},
})