From 5a5beee50e7f6ade3bf142392c847e66d524d472 Mon Sep 17 00:00:00 2001 From: whtis Date: Tue, 19 May 2026 01:08:20 +0800 Subject: [PATCH 1/4] feat(dashboard): add Simplified Chinese (zh) translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Simplified Chinese locale to the dashboard, following the i18n pattern introduced in #64. Chinese is now available alongside English and Hebrew via the existing sidebar language switcher. What's included: - New `src/i18n/locales/zh.json` mirroring the structure of `en.json`, covering all pages, components, and toast messages (404 keys + a new `common.chinese` label, identical hierarchy to `en.json`) - `zh` registered in `src/i18n/index.ts` (`supportedLanguages`, `resources`) so the language detector picks up `zh-*` browsers automatically and the cycle-language button rotates through it - Language label in `Layout.tsx` switched from a binary `he`/EN ternary to a `SupportedLanguage` lookup, so adding more languages later only requires extending the map; `zh` shows as `中文` - `common.chinese` key added to `en.json` for parity with the existing `common.english` / `common.hebrew` entries No code, API, schema, or styling changes outside i18n. RTL list is unchanged (Chinese is LTR). Existing users see no behavior change unless their browser is set to `zh-*` or they pick 中文 from the sidebar. Verified: - `npm run build` passes (tsc -b clean, vite build succeeds) - `npm run lint` reports no new errors (3 pre-existing warnings) - Key parity check: `en.json` and `zh.json` have identical key sets apart from the intentional new `common.chinese` - Chinese strings present in the production bundle (verified via grep on `dist/assets/index-*.js`) --- dashboard/src/components/Layout.tsx | 7 +- dashboard/src/i18n/index.ts | 4 +- dashboard/src/i18n/locales/en.json | 1 + dashboard/src/i18n/locales/zh.json | 513 ++++++++++++++++++++++++++++ 4 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 dashboard/src/i18n/locales/zh.json diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index f2815bb..c101f78 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -85,7 +85,12 @@ export function Layout({ onLogout, userRole }: LayoutProps) { const next = supportedLanguages[(idx + 1) % supportedLanguages.length]; void i18n.changeLanguage(next); }; - const languageLabel = currentLang === 'he' ? 'עברית' : 'EN'; + const languageLabels: Record = { + en: 'EN', + he: 'עברית', + zh: '中文', + }; + const languageLabel = languageLabels[currentLang] ?? 'EN'; const isRtl = currentLang === 'he'; return ( diff --git a/dashboard/src/i18n/index.ts b/dashboard/src/i18n/index.ts index cfa5b8c..51dd062 100644 --- a/dashboard/src/i18n/index.ts +++ b/dashboard/src/i18n/index.ts @@ -3,8 +3,9 @@ import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import en from './locales/en.json'; import he from './locales/he.json'; +import zh from './locales/zh.json'; -export const supportedLanguages = ['en', 'he'] as const; +export const supportedLanguages = ['en', 'he', 'zh'] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; export const rtlLanguages: SupportedLanguage[] = ['he']; @@ -16,6 +17,7 @@ void i18n resources: { en: { translation: en }, he: { translation: he }, + zh: { translation: zh }, }, fallbackLng: 'en', supportedLngs: supportedLanguages as unknown as string[], diff --git a/dashboard/src/i18n/locales/en.json b/dashboard/src/i18n/locales/en.json index 5304924..11b87b9 100644 --- a/dashboard/src/i18n/locales/en.json +++ b/dashboard/src/i18n/locales/en.json @@ -37,6 +37,7 @@ "language": "Language", "english": "English", "hebrew": "עברית", + "chinese": "中文", "back": "Back", "next": "Next", "previous": "Previous", diff --git a/dashboard/src/i18n/locales/zh.json b/dashboard/src/i18n/locales/zh.json new file mode 100644 index 0000000..37f81eb --- /dev/null +++ b/dashboard/src/i18n/locales/zh.json @@ -0,0 +1,513 @@ +{ + "common": { + "appName": "OpenWA", + "appSubtitle": "WhatsApp API", + "loading": "加载中...", + "save": "保存", + "cancel": "取消", + "delete": "删除", + "edit": "编辑", + "create": "新建", + "close": "关闭", + "submit": "提交", + "confirm": "确认", + "search": "搜索", + "filter": "筛选", + "actions": "操作", + "status": "状态", + "name": "名称", + "role": "角色", + "type": "类型", + "url": "URL", + "port": "端口", + "host": "主机", + "username": "用户名", + "password": "密码", + "active": "已启用", + "inactive": "未启用", + "enabled": "已开启", + "disabled": "已关闭", + "connected": "已连接", + "disconnected": "已断开", + "never": "从未", + "justNow": "刚刚", + "minAgo": "{{count}} 分钟前", + "hoursAgo": "{{count}} 小时前", + "optional": "可选", + "language": "语言", + "english": "English", + "hebrew": "עברית", + "chinese": "中文", + "back": "返回", + "next": "下一步", + "previous": "上一步", + "expand": "展开", + "collapse": "收起", + "logout": "退出登录", + "refresh": "刷新", + "errorGeneric": "发生错误", + "unknownError": "未知错误" + }, + "theme": { + "light": "浅色", + "dark": "深色", + "system": "跟随系统", + "label": "主题:{{value}}" + }, + "nav": { + "dashboard": "仪表盘", + "sessions": "会话", + "webhooks": "Webhook", + "apiKeys": "API 密钥", + "messageTester": "消息测试", + "infrastructure": "基础设施", + "plugins": "插件", + "logs": "日志" + }, + "errorBoundary": { + "title": "出了点问题", + "description": "仪表盘遇到意外错误。", + "reload": "重新加载仪表盘" + }, + "login": { + "apiKey": "API 密钥", + "apiKeyPlaceholder": "请输入你的 API 密钥", + "apiKeyRequired": "API 密钥不能为空", + "connect": "连接", + "connecting": "正在连接...", + "invalidKey": "API 密钥无效", + "connectionError": "无法连接到服务器,请稍后重试。", + "help": "需要帮助?", + "viewDocs": "查看文档", + "footer": "由 Yudhi Armyndharis 和 OpenWA 社区用心打造 ❤️", + "version": "v{{version}} · {{date}}" + }, + "dashboard": { + "title": "仪表盘", + "subtitle": "查看 WhatsApp 会话与活动概览", + "stats": { + "activeSessions": "活跃会话", + "messagesToday": "今日消息", + "webhooksConfigured": "已配置 Webhook", + "apiCalls": "API 调用(24 小时)" + }, + "sessionsOverview": "会话概览", + "showingSessions": "正在显示 {{shown}} / {{total}} 个会话", + "columns": { + "sessionId": "会话 ID", + "phone": "手机号", + "status": "状态", + "lastActive": "最近活跃", + "actions": "操作" + }, + "noSessions": "暂无会话,先新建一个吧。", + "view": "查看", + "disconnect": "断开连接", + "errorPrefix": "错误:{{message}}", + "loadError": "数据加载失败" + }, + "sessionStatus": { + "created": "新建", + "idle": "空闲", + "initializing": "正在启动...", + "connecting": "连接中...", + "qr_ready": "等待扫码", + "ready": "已连接", + "disconnected": "已断开" + }, + "sessions": { + "title": "会话", + "subtitle": "管理 WhatsApp 会话和二维码连接", + "newSession": "新建会话", + "searchPlaceholder": "搜索会话...", + "filter": { + "all": "全部状态", + "active": "活跃(已连接)", + "inactive": "未活跃", + "connecting": "连接中" + }, + "empty": { + "title": "暂无会话", + "description": "新建一个会话开始使用" + }, + "create": { + "title": "新建会话", + "label": "会话名称", + "placeholder": "例如 marketing-bot", + "hint": "只能使用小写字母、数字和连字符。示例:customer-supportbot-1", + "invalidChars": "包含非法字符,只能使用小写字母、数字和连字符。", + "tooLong": "名称长度不能超过 50 个字符(当前 {{length}}/50)。", + "duplicate": "已存在同名会话。", + "successTitle": "会话已创建", + "successDesc": "会话 “{{name}}” 创建成功", + "errorTitle": "创建失败", + "errorDefault": "会话创建失败" + }, + "delete": { + "title": "确认删除", + "message": "确定要删除会话 “{{name}}” 吗?", + "warning": "此操作无法撤销。", + "successTitle": "会话已删除", + "successDescNamed": "会话 “{{name}}” 已删除", + "successDescGeneric": "会话已删除", + "errorTitle": "删除失败", + "errorDefault": "会话删除失败" + }, + "qr": { + "title": "扫描二维码", + "step1": "1. 在手机上打开 WhatsApp", + "step2": "2. 依次点击 菜单已连接的设备", + "step3": "3. 点击 连接设备 并扫描此二维码", + "autoRefresh": "二维码每 5 秒自动刷新一次", + "generating": "正在生成二维码...", + "unavailable": "二维码尚未就绪,请稍后再试。", + "preparing": "准备二维码...", + "showQr": "显示二维码", + "loading": "加载中...", + "scanToConnect": "扫描二维码以连接" + }, + "details": { + "title": "会话详情", + "name": "名称", + "status": "状态", + "sessionId": "会话 ID", + "phone": "手机号", + "phoneNone": "未连接", + "created": "创建时间", + "lastActive": "最近活跃" + }, + "actions": { + "view": "查看", + "start": "启动", + "stop": "停止", + "reconnect": "重新连接", + "delete": "删除" + }, + "toasts": { + "readyTitle": "会话已就绪", + "readyDesc": "WhatsApp 会话已成功连接", + "disconnectedTitle": "会话已断开", + "disconnectedDesc": "WhatsApp 会话已断开连接" + }, + "card": { + "phone": "手机号", + "sessionId": "会话 ID", + "lastActive": "最近活跃" + } + }, + "webhooks": { + "title": "Webhook", + "subtitle": "配置 HTTP 回调,实时接收事件通知", + "addWebhook": "添加 Webhook", + "createTitle": "新建 Webhook", + "editTitle": "编辑 Webhook", + "deleteTitle": "删除 Webhook", + "deleteConfirm": "确定要删除这个 Webhook 吗?", + "selectSession": "选择会话...", + "session": "会话", + "events": "事件", + "available": "可订阅事件", + "saveChanges": "保存修改", + "columns": { + "url": "URL", + "events": "事件", + "session": "会话", + "status": "状态", + "actions": "操作" + }, + "empty": { + "title": "尚未配置 Webhook", + "description": "添加 Webhook 以实时接收事件通知" + }, + "toasts": { + "created": "Webhook 创建成功", + "createFailed": "Webhook 创建失败:{{message}}", + "deleted": "Webhook 已删除", + "deleteFailed": "Webhook 删除失败:{{message}}", + "updated": "Webhook 更新成功", + "updateFailed": "Webhook 更新失败:{{message}}", + "testOk": "Webhook 测试成功!状态码:{{status}}", + "testFailed": "Webhook 测试失败:{{message}}", + "testError": "Webhook 测试请求失败:{{message}}" + }, + "actions": { + "test": "测试", + "edit": "编辑", + "delete": "删除" + }, + "eventDescriptions": { + "message.received": "收到消息时触发", + "message.sent": "发送消息时触发", + "session.connected": "会话连接成功时触发", + "session.disconnected": "会话断开时触发", + "session.qr": "生成二维码时触发", + "all": "所有事件" + } + }, + "apiKeys": { + "title": "API 密钥", + "subtitle": "管理用于鉴权与访问控制的 API 密钥", + "createBtn": "新建 API 密钥", + "modalTitle": "新建 API 密钥", + "createdTitle": "API 密钥已创建", + "createdHint": "请立即复制此密钥,关闭后将无法再次查看。", + "namePlaceholder": "例如:生产环境密钥", + "rolesTitle": "角色说明", + "roles": { + "admin": "管理员", + "operator": "操作员", + "viewer": "只读" + }, + "roleDescriptions": { + "admin": "对所有资源拥有完整权限", + "operator": "可管理会话和消息", + "viewer": "仅可读,不可修改" + }, + "columns": { + "name": "名称", + "key": "密钥", + "role": "角色", + "status": "状态", + "lastUsed": "最近使用", + "actions": "操作" + }, + "statuses": { + "active": "有效", + "revoked": "已撤销" + }, + "empty": { + "title": "暂无 API 密钥", + "description": "新建 API 密钥以对请求进行鉴权" + }, + "confirm": { + "deleteTitle": "删除 API 密钥", + "revokeTitle": "撤销 API 密钥", + "deleteMessage": "确定要永久删除 {{name}} 吗?此操作无法撤销。", + "revokeMessage": "确定要撤销 {{name}} 吗?撤销后该密钥将立即失效。", + "delete": "删除", + "revoke": "撤销" + }, + "actions": { + "copy": "复制", + "revoke": "撤销", + "delete": "删除" + } + }, + "logs": { + "title": "审计日志", + "subtitle": "追踪和查看所有 API 操作与系统事件", + "exportCsv": "导出 CSV", + "searchPlaceholder": "搜索日志...", + "severity": { + "all": "全部级别", + "info": "信息", + "warn": "警告", + "error": "错误" + }, + "columns": { + "timestamp": "时间", + "action": "操作", + "session": "会话", + "apiKey": "API 密钥", + "ip": "IP 地址", + "severity": "级别" + }, + "empty": { + "title": "暂无日志", + "description": "执行操作后,审计日志会显示在这里" + } + }, + "messageTester": { + "title": "消息测试", + "subtitle": "通过 API 发送测试消息", + "compose": "发送测试消息", + "responseTitle": "API 响应", + "session": "会话", + "noReadySessions": "暂无可用会话", + "sessionOptionPhoneNone": "未绑定手机号", + "recipientType": "接收方类型", + "personal": "个人", + "group": "群组", + "selectGroup": "选择群组", + "recipientPhone": "接收方手机号", + "loadingGroups": "正在加载群组...", + "noGroupsFound": "未找到群组", + "selectGroupHint": "从你的 WhatsApp 中选择一个群组", + "phoneHint": "请使用国际格式,不带空格", + "messageType": "消息类型", + "types": { + "text": "文本", + "image": "图片", + "video": "视频", + "audio": "音频", + "document": "文档" + }, + "messageContent": "消息内容", + "messagePlaceholder": "在这里输入消息...", + "mediaUrl": "媒体 URL", + "caption": "说明文字", + "filename": "文件名", + "captionPlaceholder": "输入说明文字...", + "filenamePlaceholder": "document.pdf", + "send": "发送消息", + "sending": "发送中...", + "viewOnly": "只读", + "responseEmpty": "发送消息后,这里会显示 API 响应", + "successLabel": "200 OK - 成功", + "failedLabel": "400 - 失败", + "response": { + "timestamp": "时间", + "messageId": "消息 ID", + "error": "错误" + }, + "sendFailed": "消息发送失败" + }, + "infrastructure": { + "title": "基础设施", + "subtitle": "配置服务器、数据库、缓存、存储和引擎", + "saveConfig": "保存配置", + "saving": "保存中...", + "server": { + "title": "服务器配置", + "production": "生产环境", + "development": "开发环境", + "environment": "运行环境", + "domain": "域名", + "apiPort": "API 端口", + "dashboardPort": "仪表盘端口", + "corsOrigins": "CORS 来源", + "corsPlaceholder": "填写 * 或以逗号分隔的来源列表", + "publicApiUrl": "公网 API 地址(可选)", + "publicDashboardUrl": "公网仪表盘地址(可选)" + }, + "webhook": { + "title": "Webhook 与限流", + "settings": "Webhook 配置", + "timeout": "超时时间(毫秒)", + "maxRetries": "最大重试次数", + "retryDelay": "重试间隔(毫秒)", + "rateLimit": "请求限流", + "window": "时间窗口(秒)", + "maxReq": "窗口内最大请求数" + }, + "database": { + "title": "数据库配置", + "sqlite": "SQLite", + "sqliteDesc": "基于本地文件的数据库", + "postgres": "PostgreSQL", + "postgresDesc": "适合生产环境的数据库", + "useBuiltIn": "使用内置 PostgreSQL 容器", + "builtInDesc": "OpenWA 会为你自动管理一个 PostgreSQL 容器", + "dbName": "数据库名", + "poolSize": "连接池大小", + "ssl": "SSL 连接", + "sslDesc": "启用 TLS/SSL 以加密数据库连接", + "migrationsTitle": "数据库迁移", + "migrationsStatus": "Schema 由 TypeORM 自动同步", + "migrationsHint": "迁移会自动执行,如需手动迁移请使用 CLI。" + }, + "redis": { + "title": "Redis", + "enable": "启用 Redis", + "enableDesc": "缓存、会话存储和 BullMQ 队列需要 Redis", + "useBuiltIn": "使用内置 Redis 容器", + "builtInDesc": "OpenWA 会为你自动管理一个 Redis 容器", + "queueTitle": "启用 BullMQ 队列", + "queueDesc": "通过消息队列实现可靠的消息和 Webhook 投递", + "statsTitle": "队列统计", + "messageQueue": "消息队列", + "webhookQueue": "Webhook 队列", + "pending": "等待中", + "completed": "已完成", + "failed": "失败", + "clearFailed": "清空失败任务", + "viewBullMq": "查看 BullMQ 面板", + "disabledTitle": "Redis 已关闭", + "disabledDesc": "开启 Redis 以使用缓存、会话存储和 BullMQ 消息队列。", + "passwordOptional": "(可选)" + }, + "storage": { + "title": "存储配置", + "local": "本地文件系统", + "localDesc": "将媒体文件存放在本地", + "s3": "Amazon S3", + "s3Desc": "云存储(兼容 S3)", + "useBuiltIn": "使用内置 MinIO 容器", + "builtInDesc": "OpenWA 会为你自动管理一个 MinIO(S3 兼容)容器", + "storagePath": "存储路径", + "bucket": "Bucket 名称", + "region": "区域", + "accessKey": "Access Key", + "secretKey": "Secret Key", + "endpoint": "自定义 Endpoint(可选)", + "endpointHint": "适用于 MinIO 或其他 S3 兼容服务" + }, + "statusLabels": { + "connected": "已连接", + "disconnected": "已断开", + "disabled": "已关闭" + }, + "restart": { + "idleTitle": "⚙️ 配置已保存", + "restartingTitle": "🔄 正在重启服务...", + "waitingTitle": "⏳ 请稍候...", + "successTitle": "✅ 服务已就绪", + "errorTitle": "❌ 重启失败", + "idleDesc": "配置已写入 .env.generated
需要重启服务才能让改动生效。", + "later": "稍后重启", + "now": "立即重启", + "restartingMsg": "服务重启中... 已用时 {{count}} 秒", + "checking": "正在检查服务状态...", + "dontClose": "请不要关闭此窗口", + "successMsg": "服务已恢复,页面即将自动刷新。", + "errorMsg": "30 秒内未收到服务响应,请检查 Docker 日志并手动重启。", + "reload": "刷新页面" + }, + "toasts": { + "saveFailed": "保存失败" + } + }, + "plugins": { + "title": "插件", + "subtitle": "管理引擎、存储后端和各类扩展", + "refresh": "刷新", + "engineCard": "当前 WhatsApp 引擎", + "running": "运行中", + "supportedFeatures": "支持的功能:", + "more": "另有 {{count}} 项", + "builtIn": "内置", + "noDescription": "暂无描述", + "required": "必需", + "active": "已启用", + "activate": "启用", + "enable": "开启", + "disable": "关闭", + "healthCheck": "健康检查", + "configure": "配置", + "empty": { + "title": "未找到插件", + "description": "安装插件即可扩展 OpenWA 的能力" + }, + "config": { + "title": "配置 {{name}}", + "restartNotice": "需要重启服务才能让改动生效", + "engineType": "引擎类型", + "headless": "无界面模式", + "headlessDesc": "运行浏览器但不显示窗口(推荐用于生产环境)", + "sessionDataPath": "会话数据路径", + "browserArgs": "浏览器启动参数", + "noOptions": "此插件没有可配置项", + "save": "保存配置" + }, + "toasts": { + "errorTitle": "插件错误", + "errorDefault": "插件切换失败", + "healthOk": "健康检查通过", + "healthFail": "健康检查失败", + "healthError": "健康检查出错", + "savedTitle": "配置已保存", + "savedDesc": "需要重启服务才能让改动生效。", + "saveFailed": "保存失败" + } + } +} From 03e8aba0c1136f4ddba530202e68c4386a281f4a Mon Sep 17 00:00:00 2001 From: MastersCon Date: Wed, 20 May 2026 12:36:12 -0400 Subject: [PATCH 2/4] fix: resolve windows postinstall crash and postgresql entity validation --- package-lock.json | 51 ++++++++----------------------------- package.json | 2 +- src/database/data-source.ts | 13 ++++++++-- 3 files changed, 23 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3f6b17..41d3f01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -745,7 +745,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1251,7 +1250,6 @@ "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.1.5.tgz", "integrity": "sha512-EW0sbTtGIysu9vipdVpPQeToPqOpPgVZTt+pn1Ut3gbSS/GLWbEgIfFtMmSQDUoSL9WH00RzjgUY5K+43nWh0A==", "license": "MIT", - "peer": true, "dependencies": { "redis-info": "^3.1.0" }, @@ -1290,7 +1288,6 @@ "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.1.5.tgz", "integrity": "sha512-2IkatKwNRx/1M9/lAZIptcxS1FPNq6icpp2M46Upwd4olVxs/ujF9Kvs+Ff9ExtIO/OgYfwx7mG2IprGZ+nQCg==", "license": "MIT", - "peer": true, "dependencies": { "@bull-board/api": "7.1.5" } @@ -1580,7 +1577,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -1612,7 +1608,6 @@ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -2880,7 +2875,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "2.8.1" }, @@ -2955,7 +2949,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3126,7 +3119,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.21.tgz", "integrity": "sha512-YV1HYDGsm2rnR0vrLKidtrG6jYX5yqiIjeur1j8++dKGqhhsJ6cjMs0RfQRSTUH7IjgDemA59/znQ8nRrE0D9g==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -3174,7 +3166,6 @@ "integrity": "sha512-fqo0BHgny3MOuAL8GSfG3ZUKFVVBaBQD/0iyibnwTONT5vPexjQxJzu+945iloVvBDmrnAaRWxC1gqCDEs/AXQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3235,7 +3226,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.21.tgz", "integrity": "sha512-lA3ViycOnz4Df3EstIKpuAVFhqxQixTnjAVk0M+LRyNBlGM6VSCaNJaAIrb9Pcry39T4hTHpNVbRqGLSvhL8gA==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3257,7 +3247,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.21.tgz", "integrity": "sha512-Tq5JgaVS+auD3DXuRBy8UMU3mf69HJO8Ep+BuRS9GYMXGd/5sdMHqIQvXlXkGih9tQXdeeG9WoqURe/+IjPKng==", "license": "MIT", - "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -3443,7 +3432,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -4040,7 +4028,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4160,7 +4147,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -4382,7 +4368,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -5096,7 +5081,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5186,7 +5170,6 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5734,7 +5717,6 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6037,7 +6019,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6128,7 +6109,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.10.tgz", "integrity": "sha512-LWve7SpQjYSpCP2GEsWmoyzTz2H37L8HRmSTu3YihYsTOr5kJxrfEX6aEV7m6eskEMWXSHZYTMZepX6qNaH6CQ==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", @@ -6455,15 +6435,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -7183,8 +7161,7 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7683,7 +7660,6 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -7740,7 +7716,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9202,7 +9177,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", @@ -9487,7 +9461,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -11413,6 +11386,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 6" } @@ -11792,7 +11766,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", @@ -12085,7 +12058,6 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12774,8 +12746,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/require-directory": { "version": "2.1.1", @@ -12925,7 +12896,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13452,7 +13422,6 @@ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", @@ -13975,7 +13944,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14329,7 +14297,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14514,7 +14481,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.29.tgz", "integrity": "sha512-wwPEX/df4l72gCmOsrs0otJZYLGA9lLQkUZCkukbsymEycV4zXv2KM7wU7v2r8L01TaCgY9ApSSqHQWBOUhEoQ==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -14734,7 +14700,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15153,6 +15118,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -15171,6 +15137,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -15184,6 +15151,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -15198,6 +15166,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -15207,7 +15176,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -15215,6 +15185,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index ec1de08..2ee4cb4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dashboard:install": "cd dashboard && npm install", "dashboard:dev": "cd dashboard && npm run dev", "dashboard:build": "cd dashboard && npm run build", - "postinstall": "[ -d dashboard ] && npm run dashboard:install || true", + "postinstall": "node -e \"if (require('fs').existsSync('dashboard')) { require('child_process').execSync('npm run dashboard:install', { stdio: 'inherit' }); }\"", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", diff --git a/src/database/data-source.ts b/src/database/data-source.ts index b1715a5..9cca160 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -10,7 +10,11 @@ const dbType = process.env.DATABASE_TYPE || 'sqlite'; const sqliteDataSource = new DataSource({ type: 'sqlite', database: process.env.DATABASE_NAME || './data/openwa.sqlite', - entities: [__dirname + '/../**/*.entity{.ts,.js}'], + entities: [ + __dirname + '/../modules/session/**/*.entity{.ts,.js}', + __dirname + '/../modules/webhook/**/*.entity{.ts,.js}', + __dirname + '/../modules/message/**/*.entity{.ts,.js}', + ], migrations: [__dirname + '/migrations/*{.ts,.js}'], synchronize: false, logging: process.env.DATABASE_LOGGING === 'true', @@ -24,7 +28,11 @@ const postgresDataSource = new DataSource({ username: process.env.DATABASE_USERNAME, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME || 'openwa', - entities: [__dirname + '/../**/*.entity{.ts,.js}'], + entities: [ + __dirname + '/../modules/session/**/*.entity{.ts,.js}', + __dirname + '/../modules/webhook/**/*.entity{.ts,.js}', + __dirname + '/../modules/message/**/*.entity{.ts,.js}', + ], migrations: [__dirname + '/migrations/*{.ts,.js}'], synchronize: false, // Never auto-sync in production logging: process.env.DATABASE_LOGGING === 'true', @@ -41,3 +49,4 @@ const postgresDataSource = new DataSource({ // Export the appropriate data source based on DATABASE_TYPE export default dbType === 'postgres' ? postgresDataSource : sqliteDataSource; + From ea4858e2c8ccf8130e2aeec4ff5a21f02b91748d Mon Sep 17 00:00:00 2001 From: MastersCon Date: Wed, 20 May 2026 12:36:17 -0400 Subject: [PATCH 3/4] feat: add full spanish translation and custom dropdown language switcher --- dashboard/src/components/Layout.css | 94 +++++ dashboard/src/components/Layout.tsx | 32 +- dashboard/src/i18n/index.ts | 4 +- dashboard/src/i18n/locales/es.json | 513 ++++++++++++++++++++++++++++ 4 files changed, 627 insertions(+), 16 deletions(-) create mode 100644 dashboard/src/i18n/locales/es.json diff --git a/dashboard/src/components/Layout.css b/dashboard/src/components/Layout.css index 0db1288..c5f6863 100644 --- a/dashboard/src/components/Layout.css +++ b/dashboard/src/components/Layout.css @@ -195,6 +195,100 @@ color: var(--text-primary); } +.language-select-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 0.9rem; + color: var(--text-secondary); + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + overflow: hidden; +} + +.language-select-wrapper:hover { + background: var(--bg-light); + color: var(--text-primary); +} + +.language-icon { + flex-shrink: 0; + pointer-events: none; + z-index: 1; +} + +.language-select { + width: 100%; + border: none; + background: transparent; + color: inherit; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + cursor: pointer; + outline: none; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + padding-right: 1.2rem; + padding-left: 0; + z-index: 2; +} + +.language-select option { + background: var(--bg-white); + color: var(--text-primary); +} + +.language-arrow { + position: absolute; + right: 0.9rem; + font-size: 0.6rem; + color: var(--text-muted); + pointer-events: none; + transition: transform 0.2s; + z-index: 1; +} + +/* RTL styles */ +[dir="rtl"] .language-select { + padding-right: 0; + padding-left: 1.2rem; +} + +[dir="rtl"] .language-arrow { + right: auto; + left: 0.9rem; +} + +/* Collapsed sidebar styles */ +.sidebar.collapsed .language-select-wrapper { + justify-content: center; + padding: 0.6rem; + width: auto; + overflow: visible; +} + +.sidebar.collapsed .language-select { + position: absolute; + inset: 0; + opacity: 0; + width: 100%; + height: 100%; + padding: 0; +} + +.sidebar.collapsed .language-arrow { + display: none; +} + .logout-btn { display: flex; align-items: center; diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index f2815bb..3c34a68 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -22,7 +22,7 @@ import { } from 'lucide-react'; import { useTheme } from '../hooks/useTheme'; import { type UserRole } from '../hooks/useRole'; -import { supportedLanguages, type SupportedLanguage } from '../i18n'; +import { type SupportedLanguage } from '../i18n'; import './Layout.css'; interface LayoutProps { @@ -80,12 +80,9 @@ export function Layout({ onLogout, userRole }: LayoutProps) { const toggleMobile = () => setIsMobileOpen(!isMobileOpen); const currentLang = (i18n.resolvedLanguage || i18n.language || 'en').split('-')[0] as SupportedLanguage; - const cycleLanguage = () => { - const idx = supportedLanguages.indexOf(currentLang); - const next = supportedLanguages[(idx + 1) % supportedLanguages.length]; - void i18n.changeLanguage(next); + const handleLanguageChange = (lang: string) => { + void i18n.changeLanguage(lang); }; - const languageLabel = currentLang === 'he' ? 'עברית' : 'EN'; const isRtl = currentLang === 'he'; return ( @@ -151,15 +148,20 @@ export function Layout({ onLogout, userRole }: LayoutProps) {
- +
+ + + {!isCollapsed && } +
+ {showLangDropdown && !isCollapsed && ( +
+ {(['en', 'ar', 'es', 'zh', 'he'] as SupportedLanguage[]).map(lang => ( +
{ + handleLanguageChange(lang); + setShowLangDropdown(false); + }} + > + {languageLabels[lang]} +
+ ))} +
+ )}