From 08f3928ebc317794f11a3a3a81e878f24cbde1e6 Mon Sep 17 00:00:00 2001 From: tanomitsu <64388900+tanomitsu@users.noreply.github.com> Date: Tue, 26 May 2026 15:28:29 +0900 Subject: [PATCH 1/5] feat: add Japanese (ja) localization - Add ja translations for common, settings, and chores namespaces - Register ja in AVAILABLE_LANGUAGES so it appears in the language picker - Import moment/locale/ja so date and relative-time formatting uses Japanese --- public/locales/ja/chores.json | 75 ++++++++++++ public/locales/ja/common.json | 33 +++++ public/locales/ja/settings.json | 174 +++++++++++++++++++++++++++ src/contexts/LocalizationContext.jsx | 1 + src/i18n/config.js | 1 + 5 files changed, 284 insertions(+) create mode 100644 public/locales/ja/chores.json create mode 100644 public/locales/ja/common.json create mode 100644 public/locales/ja/settings.json diff --git a/public/locales/ja/chores.json b/public/locales/ja/chores.json new file mode 100644 index 0000000..cb1dac3 --- /dev/null +++ b/public/locales/ja/chores.json @@ -0,0 +1,75 @@ +{ + "title": "タスク", + "myChores": "自分のタスク", + "allChores": "すべてのタスク", + "addChore": "タスクを追加", + "editChore": "タスクを編集", + "deleteChore": "タスクを削除", + "completeChore": "タスクを完了", + "dueDate": "期限", + "assignedTo": "担当者", + "priority": "優先度", + "status": "ステータス", + "description": "説明", + "choreView": { + "assignment": "担当", + "assigned": "担当者", + "last": "前回", + "schedule": "スケジュール", + "due": "期限", + "statistics": "統計", + "completed": "完了", + "times": "回", + "details": "詳細", + "createdBy": "作成者", + "na": "該当なし", + "taskCompleted": "タスク完了", + "taskCompletedMessage": "タスクを完了としてマークしました", + "taskCompletionUndone": "タスクの完了を取り消しました。", + "taskSkipUndone": "タスクのスキップを取り消しました。", + "undoSuccessful": "取り消し成功", + "undoFailed": "取り消し失敗", + "undoFailedMessage": "操作を取り消せませんでした。もう一度お試しください。", + "resetTimer": "タイマーをリセット", + "resetTimerConfirmation": "本当にタイマーをリセットしますか?このタスクを開始してからのすべての時間記録が削除されます。", + "clearAllTimeRecords": "すべての時間記録を削除", + "clearAllTimeConfirmation": "このタスクのすべてのタイマーを完全に削除し、「未開始」の状態に戻します。", + "descriptionTitle": "説明", + "description": "説明 :", + "previousNote": "前回のメモ", + "previousNoteLabel": "前回のメモ:", + "subtasksLabel": "サブタスク :", + "taskActions": "タスク操作", + "addNote": "メモを追加", + "additionalNotes": "追加メモ:", + "notePlaceholder": "完了についてのメモを追加...", + "setCustomCompletionTime": "完了時刻をカスタム指定", + "skipTask": "タスクをスキップ", + "skipTaskConfirmation": "本当にこのタスクをスキップしますか?", + "markComplete": "完了としてマーク", + "markAsDone": "完了にする", + "edit": "編集", + "archive": "アーカイブ", + "unarchive": "アーカイブ解除", + "viewHistory": "履歴を表示", + "history": "履歴", + "startTimer": "タイマー開始", + "start": "開始", + "pauseTimer": "タイマー一時停止", + "approve": "承認", + "reject": "却下", + "pendingApproval": "承認待ち", + "undo": "取り消し", + "skip": "スキップ", + "cancel": "キャンセル", + "noPriority": "優先度なし", + "subtasks": "サブタスク", + "noDescription": "説明はありません", + "timer": { + "active": "タイマー動作中", + "paused": "タイマー一時停止中", + "reset": "タイマーをリセット", + "delete": "セッションを削除" + } + } +} diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json new file mode 100644 index 0000000..9b96c62 --- /dev/null +++ b/public/locales/ja/common.json @@ -0,0 +1,33 @@ +{ + "save": "保存", + "cancel": "キャンセル", + "delete": "削除", + "edit": "編集", + "close": "閉じる", + "confirm": "確認", + "loading": "読み込み中...", + "error": "エラー", + "success": "成功", + "warning": "警告", + "refresh": "更新", + "copy": "コピー", + "copied": "コピーしました!", + "settings": "設定", + "yes": "はい", + "no": "いいえ", + "back": "戻る", + "backToCalendar": "カレンダーに戻る", + "logout": "ログアウト", + "version": "バージョン", + "navigation": { + "allTasks": "すべてのタスク", + "archived": "アーカイブ済み", + "things": "Things", + "labels": "ラベル", + "projects": "プロジェクト", + "filters": "フィルター", + "activities": "アクティビティ", + "points": "ポイント", + "settings": "設定" + } +} diff --git a/public/locales/ja/settings.json b/public/locales/ja/settings.json new file mode 100644 index 0000000..75581d5 --- /dev/null +++ b/public/locales/ja/settings.json @@ -0,0 +1,174 @@ +{ + "title": "設定", + "circleSettings": { + "title": "サークル設定", + "description": "アカウントは、サークルを作成または参加すると自動的にそのサークルに接続されます。下記のユニークなサークルコードまたはリンクを共有することで、友達を簡単に招待できます。誰かがサークルへの参加をリクエストした際は、下に通知が表示されます。サークルから脱退したい場合は、「サークルを脱退」ボタンを押してください。", + "circleCode": "サークルコード", + "copyCode": "コードをコピー", + "copyLink": "リンクをコピー", + "codeCopied": "サークルコードをコピーしました!", + "linkCopied": "サークルリンクをコピーしました!", + "joinCircle": "サークルに参加", + "joinCirclePlaceholder": "サークルコードを入力", + "join": "参加", + "leave": "サークルを脱退", + "leaveConfirmTitle": "サークルを脱退", + "leaveConfirmMessage": "本当にこのサークルから脱退しますか?", + "circleMembers": "サークルメンバー", + "circleMemberRequests": "サークル参加リクエスト", + "admin": "管理者", + "member": "メンバー", + "pending": "保留中", + "accept": "承認", + "reject": "拒否", + "makeAdmin": "管理者にする", + "makeMember": "メンバーにする", + "remove": "削除", + "webhookURL": "Webhook URL", + "webhookDescription": "サークルイベントの通知を受け取るための Webhook URL を入力してください", + "webhookPlaceholder": "https://your-webhook-url.com" + }, + "accountSettings": { + "title": "アカウント設定", + "subscription": "サブスクリプション", + "subscriptionStatus": "現在のプラン", + "free": "無料", + "plus": "Plus", + "upgrade": "アップグレード", + "cancel": "キャンセル", + "changePassword": "パスワードを変更", + "password": "パスワード", + "dangerZone": "危険な操作", + "dangerZoneDescription": "アカウントを削除すると元に戻すことはできません。十分にご確認ください。", + "deleteAccount": "アカウントを削除" + }, + "localization": { + "title": "地域設定", + "description": "言語、日付形式、地域の設定をカスタマイズします。", + "language": "言語", + "languageDescription": "使用する言語を選択してください", + "dateFormat": "日付形式", + "dateFormatDescription": "アプリケーション全体で日付をどのように表示するかを選択します", + "timeFormat": "時刻形式", + "timeFormatDescription": "12時間表記または24時間表記を選択します", + "12hour": "12時間 (AM/PM)", + "24hour": "24時間", + "firstDayOfWeek": "週の開始曜日", + "firstDayOfWeekDescription": "週の最初の曜日を選択します", + "sunday": "日曜日", + "monday": "月曜日", + "saturday": "土曜日", + "formats": { + "mdy": "MM/DD/YYYY (米国)", + "dmy": "DD/MM/YYYY (欧州)", + "ymd": "YYYY-MM-DD (ISO)", + "long": "長い形式 (例: January 1, 2024)", + "short": "短い形式 (例: Jan 1, 2024)" + } + }, + "sidepanel": { + "title": "サイドパネルのカスタマイズ", + "description": "サイドパネル内のカードのレイアウトと表示をカスタマイズします。この機能はタブレットやデスクトップなど大画面のデバイスでのみ利用できます。" + }, + "theme": { + "title": "テーマ設定", + "description": "サイトの見た目を選択します。単一のテーマを選ぶか、システム設定に同期して昼夜のテーマを自動で切り替えることができます。", + "themeMode": "テーマモード", + "light": "ライト", + "dark": "ダーク", + "system": "システム" + }, + "notifications": { + "settingsSaved": "設定を保存しました", + "settingsSaveFailed": "設定の保存に失敗しました", + "invalidWebhook": "Webhook URL が無効です" + }, + "profile": { + "title": "プロフィール設定", + "description": "表示名とプロフィール写真を更新します。", + "photoUpdated": "写真を更新しました", + "photoUpdatedMessage": "プロフィール写真が正常に更新されました!", + "uploadFailed": "アップロード失敗", + "uploadFailedMessage": "写真のアップロードに失敗しました。もう一度お試しください。", + "profileUpdated": "プロフィールを更新しました", + "profileUpdatedMessage": "プロフィール情報が正常に保存されました!", + "updateFailed": "更新失敗", + "updateFailedMessage": "プロフィールを更新できませんでした。接続を確認してから再度お試しください。", + "changePhoto": "写真を変更", + "displayName": "表示名", + "displayNamePlaceholder": "表示名を入力してください", + "timezone": "タイムゾーン", + "timezonePlaceholder": "タイムゾーンを選択してください", + "save": "保存", + "cancel": "キャンセル" + }, + "overview": { + "title": "設定", + "subtitle": "体験をカスタマイズし、アカウント設定を管理します", + "upgrade": { + "title": "Plus へアップグレード", + "description": "強力な機能を解放して、生産性を向上させましょう", + "button": "今すぐアップグレード", + "features": { + "richText": "リッチテキストの説明", + "notifications": "タスク通知", + "apiIntegrations": "API 連携", + "advancedAutomation": "高度な自動化" + } + }, + "sections": { + "profile": { + "title": "プロフィール設定", + "description": "プロフィール情報、写真、表示名、タイムゾーンを更新します。" + }, + "circle": { + "title": "サークル設定", + "description": "サークルの管理、メンバーの招待、参加リクエストへの対応を行います。" + }, + "account": { + "title": "アカウント設定", + "description": "サブスクリプションの管理、パスワード変更、アカウント削除を行います。" + }, + "subaccounts": { + "title": "管理アカウント", + "description": "サブアカウントを作成・管理し、ログインや割り当てられたタスクの完了を行えるようにします。" + }, + "notifications": { + "title": "通知", + "description": "プッシュ通知、メール通知、タスクの通知先を設定します。" + }, + "mfa": { + "title": "多要素認証", + "description": "認証アプリを使った MFA でセキュリティをさらに強化します。" + }, + "apitokens": { + "title": "API トークン", + "description": "サードパーティ連携や API アクセス用のトークンを生成・管理します。" + }, + "storage": { + "title": "ストレージ設定", + "description": "データのバックアップと復元、ローカルストレージと同期の設定を管理します。" + }, + "sidepanel": { + "title": "サイドパネルのカスタマイズ", + "description": "サイドパネルのカードのレイアウトと表示をカスタマイズします。" + }, + "theme": { + "title": "テーマ設定", + "description": "好みのテーマを選択し、ダーク/ライトモードの設定を行います。" + }, + "localization": { + "title": "地域設定", + "description": "言語、日付形式、時刻形式、地域の設定をカスタマイズします。" + }, + "advanced": { + "title": "詳細設定", + "description": "Webhook、リアルタイム更新、その他の高度な機能を設定して生産性を高めます。" + }, + "developer": { + "title": "開発者設定", + "description": "認証トークン、SSE 接続、デバッグデータなどの技術情報を確認できます。" + } + } + } +} diff --git a/src/contexts/LocalizationContext.jsx b/src/contexts/LocalizationContext.jsx index bfd5016..3172400 100644 --- a/src/contexts/LocalizationContext.jsx +++ b/src/contexts/LocalizationContext.jsx @@ -25,6 +25,7 @@ export const AVAILABLE_LANGUAGES = [ { code: 'es', name: 'Spanish', nativeName: 'Español' }, { code: 'fr', name: 'French', nativeName: 'Français' }, { code: 'nl', name: 'Dutch', nativeName: 'Nederlands' }, + { code: 'ja', name: 'Japanese', nativeName: '日本語' }, ] export const LocalizationProvider = ({ children }) => { diff --git a/src/i18n/config.js b/src/i18n/config.js index 1b3589f..437d6d8 100644 --- a/src/i18n/config.js +++ b/src/i18n/config.js @@ -1,6 +1,7 @@ import i18n from 'i18next' import LanguageDetector from 'i18next-browser-languagedetector' import HttpBackend from 'i18next-http-backend' +import 'moment/locale/ja' import { initReactI18next } from 'react-i18next' i18n From aeed19532ff467bc1ae3702f0250e7ab0f6641ed Mon Sep 17 00:00:00 2001 From: cm-moment <50913840+cm-moment@users.noreply.github.com> Date: Thu, 9 Jul 2026 20:17:36 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=AE=80?= =?UTF-8?q?=E4=BD=93=E4=B8=AD=E6=96=87(zh-CN)=E8=AF=AD=E8=A8=80=E6=B1=89?= =?UTF-8?q?=E5=8C=96=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 zh-CN 简体中文国际化目录 2. 完成 chores、common、settings 页面全部文本汉化 3. 语言编码遵循 BCP47 标准,与现有多语言目录结构统一 --- public/locales/zh-CN/chores.json | 75 +++++++++++++ public/locales/zh-CN/common.json | 33 ++++++ public/locales/zh-CN/settings.json | 174 +++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 public/locales/zh-CN/chores.json create mode 100644 public/locales/zh-CN/common.json create mode 100644 public/locales/zh-CN/settings.json diff --git a/public/locales/zh-CN/chores.json b/public/locales/zh-CN/chores.json new file mode 100644 index 0000000..3f966f1 --- /dev/null +++ b/public/locales/zh-CN/chores.json @@ -0,0 +1,75 @@ +{ + "title": "家务", + "myChores": "我的家务", + "allChores": "所有家务", + "addChore": "添加家务", + "editChore": "编辑家务", + "deleteChore": "删除家务", + "completeChore": "完成任务", + "dueDate": "截止日期", + "assignedTo": "分配给", + "priority": "优先级", + "status": "状态", + "description": "描述", + "choreView": { + "assignment": "分配", + "assigned": "已分配", + "last": "上次", + "schedule": "日程", + "due": "截止", + "statistics": "统计", + "completed": "已完成", + "times": "次", + "details": "详情", + "createdBy": "创建者", + "na": "无", + "taskCompleted": "任务已完成", + "taskCompletedMessage": "您的任务已标记为完成", + "taskCompletionUndone": "任务完成已撤销。", + "taskSkipUndone": "任务跳过已撤销。", + "undoSuccessful": "撤销成功", + "undoFailed": "撤销失败", + "undoFailedMessage": "无法撤销该操作。请重试。", + "resetTimer": "重置计时器", + "resetTimerConfirmation": "您确定要重置计时器吗?这将清除自您开始任务以来的所有时间记录。", + "clearAllTimeRecords": "清除所有时间记录", + "clearAllTimeConfirmation": "这将永久删除此任务的所有计时器,并将其重置为“未开始”。", + "descriptionTitle": "描述", + "description": "描述:", + "previousNote": "上一条备注", + "previousNoteLabel": "上一条备注:", + "subtasksLabel": "子任务:", + "taskActions": "任务操作", + "addNote": "添加备注", + "additionalNotes": "附加备注:", + "notePlaceholder": "添加关于完成的备注...", + "setCustomCompletionTime": "设置自定义完成时间", + "skipTask": "跳过任务", + "skipTaskConfirmation": "您确定要跳过此任务吗?", + "markComplete": "标记完成", + "markAsDone": "标记为已完成", + "edit": "编辑", + "archive": "归档", + "unarchive": "取消归档", + "viewHistory": "查看历史", + "history": "历史", + "startTimer": "开始计时", + "start": "开始", + "pauseTimer": "暂停计时", + "approve": "批准", + "reject": "拒绝", + "pendingApproval": "待批准", + "undo": "撤销", + "skip": "跳过", + "cancel": "取消", + "noPriority": "无优先级", + "subtasks": "子任务", + "noDescription": "暂无描述", + "timer": { + "active": "计时中", + "paused": "已暂停", + "reset": "重置计时器", + "delete": "删除会话" + } + } +} \ No newline at end of file diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json new file mode 100644 index 0000000..47bf53b --- /dev/null +++ b/public/locales/zh-CN/common.json @@ -0,0 +1,33 @@ +{ + "save": "保存", + "cancel": "取消", + "delete": "删除", + "edit": "编辑", + "close": "关闭", + "confirm": "确认", + "loading": "加载中...", + "error": "错误", + "success": "成功", + "warning": "警告", + "refresh": "刷新", + "copy": "复制", + "copied": "已复制!", + "settings": "设置", + "yes": "是", + "no": "否", + "back": "返回", + "backToCalendar": "返回日历", + "logout": "退出登录", + "version": "版本", + "navigation": { + "allTasks": "所有任务", + "archived": "已归档", + "things": "事项", + "labels": "标签", + "projects": "项目", + "filters": "筛选", + "activities": "活动", + "points": "积分", + "settings": "设置" + } +} \ No newline at end of file diff --git a/public/locales/zh-CN/settings.json b/public/locales/zh-CN/settings.json new file mode 100644 index 0000000..9294aca --- /dev/null +++ b/public/locales/zh-CN/settings.json @@ -0,0 +1,174 @@ +{ + "title": "设置", + "circleSettings": { + "title": "圈子设置", + "description": "当您创建或加入圈子时,您的账户会自动连接到该圈子。通过分享下方的唯一圈子代码或链接,即可轻松邀请好友。当有人请求加入您的圈子时,您会在下方收到通知。如果您想离开,只需点击“离开圈子”按钮。", + "circleCode": "圈子代码", + "copyCode": "复制代码", + "copyLink": "复制链接", + "codeCopied": "圈子代码已复制!", + "linkCopied": "圈子链接已复制!", + "joinCircle": "加入圈子", + "joinCirclePlaceholder": "输入圈子代码", + "join": "加入", + "leave": "离开圈子", + "leaveConfirmTitle": "离开圈子", + "leaveConfirmMessage": "您确定要离开此圈子吗?", + "circleMembers": "圈子成员", + "circleMemberRequests": "圈子成员申请", + "admin": "管理员", + "member": "成员", + "pending": "待处理", + "accept": "接受", + "reject": "拒绝", + "makeAdmin": "设为管理员", + "makeMember": "设为成员", + "remove": "移除", + "webhookURL": "Webhook URL", + "webhookDescription": "输入 Webhook URL 以接收圈子事件通知", + "webhookPlaceholder": "https://your-webhook-url.com" + }, + "accountSettings": { + "title": "账户设置", + "subscription": "订阅", + "subscriptionStatus": "当前方案", + "free": "免费版", + "plus": "Plus 版", + "upgrade": "升级", + "cancel": "取消", + "changePassword": "修改密码", + "password": "密码", + "dangerZone": "危险区域", + "dangerZoneDescription": "一旦删除账户,将无法恢复。请谨慎操作。", + "deleteAccount": "删除账户" + }, + "localization": { + "title": "本地化", + "description": "自定义您账户的语言、日期格式和区域偏好。", + "language": "语言", + "languageDescription": "选择您的首选语言", + "dateFormat": "日期格式", + "dateFormatDescription": "选择整个应用中日期的显示方式", + "timeFormat": "时间格式", + "timeFormatDescription": "选择 12 小时制或 24 小时制", + "12hour": "12 小时制 (AM/PM)", + "24hour": "24 小时制", + "firstDayOfWeek": "每周起始日", + "firstDayOfWeekDescription": "选择每周的第一天", + "sunday": "星期日", + "monday": "星期一", + "saturday": "星期六", + "formats": { + "mdy": "MM/DD/YYYY (美国)", + "dmy": "DD/MM/YYYY (欧洲)", + "ymd": "YYYY-MM-DD (ISO)", + "long": "长格式 (例如:2024年1月1日)", + "short": "短格式 (例如:2024年1月1日)" + } + }, + "sidepanel": { + "title": "侧边栏自定义", + "description": "自定义侧边栏中卡片的布局和可见性。此部分仅适用于平板电脑和台式机等大屏设备。" + }, + "theme": { + "title": "主题偏好", + "description": "选择网站的显示外观。您可以选择单一主题,或与系统同步,自动切换日间和夜间主题。", + "themeMode": "主题模式", + "light": "浅色", + "dark": "深色", + "system": "跟随系统" + }, + "notifications": { + "settingsSaved": "设置保存成功", + "settingsSaveFailed": "设置保存失败", + "invalidWebhook": "无效的 Webhook URL" + }, + "profile": { + "title": "个人资料设置", + "description": "更新您的显示名称和个人头像。", + "photoUpdated": "头像已更新", + "photoUpdatedMessage": "您的个人头像已成功更新!", + "uploadFailed": "上传失败", + "uploadFailedMessage": "头像上传失败。请重试。", + "profileUpdated": "资料已更新", + "profileUpdatedMessage": "您的个人资料已成功保存!", + "updateFailed": "更新失败", + "updateFailedMessage": "无法更新您的个人资料。请检查网络连接后重试。", + "changePhoto": "更换头像", + "displayName": "显示名称", + "displayNamePlaceholder": "输入您的显示名称", + "timezone": "时区", + "timezonePlaceholder": "选择您的时区", + "save": "保存", + "cancel": "取消" + }, + "overview": { + "title": "设置", + "subtitle": "自定义您的体验并管理账户偏好", + "upgrade": { + "title": "升级到 Plus", + "description": "解锁强大功能,提升您的效率", + "button": "立即升级", + "features": { + "richText": "富文本描述", + "notifications": "任务通知", + "apiIntegrations": "API 集成", + "advancedAutomation": "高级自动化" + } + }, + "sections": { + "profile": { + "title": "个人资料设置", + "description": "更新您的个人资料信息、头像、显示名称和时区偏好。" + }, + "circle": { + "title": "圈子设置", + "description": "管理您的圈子、邀请成员并处理加入申请。" + }, + "account": { + "title": "账户设置", + "description": "管理订阅、修改密码及账户删除选项。" + }, + "subaccounts": { + "title": "托管账户", + "description": "创建和管理子账户,以便登录并完成分配的任务。" + }, + "notifications": { + "title": "通知", + "description": "配置推送通知、邮件提醒及任务通知目标。" + }, + "mfa": { + "title": "多因素认证", + "description": "使用身份验证器应用启用 MFA,增加一层安全保障。" + }, + "apitokens": { + "title": "API 令牌", + "description": "生成和管理用于第三方集成及 API 访问的访问令牌。" + }, + "storage": { + "title": "存储设置", + "description": "备份和恢复数据,管理本地存储及同步偏好。" + }, + "sidepanel": { + "title": "侧边栏自定义", + "description": "自定义侧边栏界面中卡片的布局和可见性。" + }, + "theme": { + "title": "主题偏好", + "description": "选择您喜欢的主题并配置深色/浅色模式设置。" + }, + "localization": { + "title": "本地化", + "description": "自定义语言、日期格式、时间格式及区域偏好。" + }, + "advanced": { + "title": "高级设置", + "description": "配置 Webhook、实时更新及其他高级功能以提升效率。" + }, + "developer": { + "title": "开发者设置", + "description": "查看有关身份验证令牌、SSE 连接及调试数据的技术信息。" + } + } + } +} \ No newline at end of file From d207f7e2b20ae28d9dca1ad43f25c8c442cd5189 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Thu, 9 Jul 2026 14:44:05 -0400 Subject: [PATCH 3/5] fix: implement UUID generation utility and replace crypto.randomUUID usage (to support genertaion when we are in http )in task and chore components --- src/utils/UUID.js | 10 + src/views/Authorization/LoginSettings.jsx | 66 ++++- src/views/ChoreEdit/ChoreEdit.jsx | 3 +- src/views/History/ChoreHistory.jsx | 301 ++++++++++++---------- src/views/components/AddTaskModal.jsx | 6 +- 5 files changed, 251 insertions(+), 135 deletions(-) create mode 100644 src/utils/UUID.js diff --git a/src/utils/UUID.js b/src/utils/UUID.js new file mode 100644 index 0000000..49b1d2c --- /dev/null +++ b/src/utils/UUID.js @@ -0,0 +1,10 @@ +export function generateUUID() { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID() + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/src/views/Authorization/LoginSettings.jsx b/src/views/Authorization/LoginSettings.jsx index e8fe85e..914e0e6 100644 --- a/src/views/Authorization/LoginSettings.jsx +++ b/src/views/Authorization/LoginSettings.jsx @@ -39,13 +39,14 @@ const LoginSettings = () => { } const testConnection = async url => { + const testURL = url.replace(/\/+$/, '') + '/api/v1/resource' const controller = new AbortController() const timeoutId = setTimeout( () => controller.abort(), CONNECTION_TIMEOUT_MS, ) + try { - const testURL = url.replace(/\/+$/, '') + '/api/v1/resource' const response = await fetch(testURL, { method: 'GET', signal: controller.signal, @@ -55,18 +56,79 @@ const LoginSettings = () => { if (response.status < 500) { return { ok: true } } + if (response.status === 503) { + return { + ok: false, + message: + 'Server is starting up or temporarily unavailable (503). Try again in a moment.', + } + } return { ok: false, message: `Server responded with error ${response.status}. Please check your Donetick server.`, } } catch (err) { clearTimeout(timeoutId) + if (err.name === 'AbortError') { return { ok: false, - message: `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000}s. Check the URL and ensure the server is running.`, + message: `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000}s. The host may be unreachable or behind a firewall — check the IP/hostname and network.`, } } + + // Try no-cors to distinguish CORS misconfiguration from server being down + const noCorsController = new AbortController() + const noCorsTimeout = setTimeout(() => noCorsController.abort(), 3000) + try { + const probeStart = Date.now() + const probe = await fetch(testURL, { + method: 'GET', + mode: 'no-cors', + signal: noCorsController.signal, + }) + clearTimeout(noCorsTimeout) + if (probe.type === 'opaque') { + // Server responded but CORS headers blocked the real request + return { + ok: false, + message: + 'Server is reachable but blocked the request (CORS). Ensure your Donetick server allows requests from this origin, or check the server CORS config.', + } + } + // Opaque is the only expected type for no-cors success; anything else is odd + void probeStart + } catch (probeErr) { + clearTimeout(noCorsTimeout) + if (probeErr.name !== 'AbortError') { + // Both normal and no-cors fetch threw immediately → port refused + const msg = probeErr.message?.toLowerCase() ?? '' + if ( + msg.includes('getaddrinfo') || + msg.includes('name not resolved') || + msg.includes('err_name') + ) { + return { + ok: false, + message: + 'Hostname could not be resolved. Check the URL for typos or verify DNS.', + } + } + return { + ok: false, + message: + 'Connection refused. The port may be wrong or nothing is listening — verify the URL and port (default Donetick port is 2021).', + } + } + // no-cors also timed out → server/host truly unreachable + return { + ok: false, + message: + 'Unable to reach the server. Check the URL, port, and network connection.', + } + } + + // Fallback (should rarely hit) return { ok: false, message: diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 7ac6cd4..23f3fee 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -58,6 +58,7 @@ import { UploadChoreAttachment, } from '../../utils/Fetcher' import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers' +import { generateUUID } from '../../utils/UUID' import Priorities from '../../utils/Priorities.jsx' import { getIconComponent } from '../../utils/ProjectIcons' import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js' @@ -131,7 +132,7 @@ const ChoreEdit = () => { const [createdBy, setCreatedBy] = useState(0) const [errors, setErrors] = useState({}) const [attemptToSave, setAttemptToSave] = useState(false) - const [draftId] = useState(() => crypto.randomUUID()) + const [draftId] = useState(() => generateUUID()) const [attachments, setAttachments] = useState([]) const [isUploadingAttachment, setIsUploadingAttachment] = useState(false) const [addLabelModalOpen, setAddLabelModalOpen] = useState(false) diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index 84ac352..3298408 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -99,12 +99,42 @@ const ChoreHistory = () => { type: 'multi-select', icon: , options: [ - { value: ChoreHistoryStatus.COMPLETED, label: 'Completed', color: 'success', icon: }, - { value: ChoreHistoryStatus.SKIPPED, label: 'Skipped', color: 'warning', icon: }, - { value: ChoreHistoryStatus.PENDING_APPROVAL, label: 'Pending', color: 'neutral', icon: }, - { value: ChoreHistoryStatus.REJECTED, label: 'Rejected', color: 'danger', icon: }, - { value: 5, label: 'Missed', color: 'danger', icon: }, - { value: 6, label: 'Rescheduled', color: 'warning', icon: }, + { + value: ChoreHistoryStatus.COMPLETED, + label: 'Completed', + color: 'success', + icon: , + }, + { + value: ChoreHistoryStatus.SKIPPED, + label: 'Skipped', + color: 'warning', + icon: , + }, + { + value: ChoreHistoryStatus.PENDING_APPROVAL, + label: 'Pending', + color: 'neutral', + icon: , + }, + { + value: ChoreHistoryStatus.REJECTED, + label: 'Rejected', + color: 'danger', + icon: , + }, + { + value: 5, + label: 'Missed', + color: 'danger', + icon: , + }, + { + value: 6, + label: 'Rescheduled', + color: 'warning', + icon: , + }, ], filterFn: (item, values) => values.includes(item.status), }, @@ -143,8 +173,23 @@ const ChoreHistory = () => { [performers], ) - const { filteredData: filteredHistory, activeFilters, setFilter, clearAll, activeFilterCount } = - useFilter(choreHistory, filterDefs) + const { + filteredData: filteredHistory, + activeFilters, + setFilter, + clearAll, + activeFilterCount, + } = useFilter(choreHistory, filterDefs) + + const sortedHistory = useMemo( + () => + [...filteredHistory].sort( + (a, b) => + new Date(b.performedAt || b.updatedAt) - + new Date(a.performedAt || a.updatedAt), + ), + [filteredHistory], + ) const handleDelete = historyEntry => { showConfirmation( @@ -296,7 +341,7 @@ const ChoreHistory = () => { {/* Enhanced Header Section */} - {/* */} + {/* */} {/* Statistics Cards Grid - Compact Design */} @@ -374,9 +419,8 @@ const ChoreHistory = () => { {/* History Section Header */} - + - { - - - - {filteredHistory.length === 0 && activeFilterCount > 0 && ( + + + + {sortedHistory.length === 0 && activeFilterCount > 0 && ( { )} - {filteredHistory.length > 0 && ( - - {/* Chore History List (Updated Style) */} + {sortedHistory.length > 0 && ( + + {/* Chore History List (Updated Style) */} - - {filteredHistory.map((historyEntry, index) => ( - - - handleEdit(historyEntry)}> - - - - Edit - - - - handleDelete(historyEntry)}> - - - - Delete - - - - - - } - > - { - setDetailModalConfig({ - isOpen: true, - entry: historyEntry, - performers, - onClose: () => setDetailModalConfig({ isOpen: false }), - onEdit: record => { - setDetailModalConfig({ isOpen: false }) - setEditHistory(record) - setIsEditModalOpen(true) - }, - }) - }} - pendingCommands={pendingByHistoryId[historyEntry.id] || []} - onViewNote={notes => { - setNoteViewerConfig({ - isOpen: true, - title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`, - content: notes, - onClose: () => setNoteViewerConfig({ isOpen: false }), - }) - }} - onToggleActions={() => { - const id = historyEntry.id || index - if (showMoreInfoId === id) { - setShowMoreInfoId(null) - } else { - setShowMoreInfoId(id) - } - }} - /> - - ))} - - + + {sortedHistory.map((historyEntry, index) => ( + + + handleEdit(historyEntry)}> + + + + Edit + + + + handleDelete(historyEntry)}> + + + + Delete + + + + + + } + > + { + setDetailModalConfig({ + isOpen: true, + entry: historyEntry, + performers, + onClose: () => setDetailModalConfig({ isOpen: false }), + onEdit: record => { + setDetailModalConfig({ isOpen: false }) + setEditHistory(record) + setIsEditModalOpen(true) + }, + }) + }} + pendingCommands={pendingByHistoryId[historyEntry.id] || []} + onViewNote={notes => { + setNoteViewerConfig({ + isOpen: true, + title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`, + content: notes, + onClose: () => setNoteViewerConfig({ isOpen: false }), + }) + }} + onToggleActions={() => { + const id = historyEntry.id || index + if (showMoreInfoId === id) { + setShowMoreInfoId(null) + } else { + setShowMoreInfoId(id) + } + }} + /> + + ))} + + )} { - + ) } diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index e73c121..fcf6207 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -8,6 +8,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal' import { useCreateChore } from '../../queries/ChoreQueries' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries' import { isPlusAccount } from '../../utils/Helpers' +import { generateUUID } from '../../utils/UUID' import { useLabels } from '../Labels/LabelQueries' import { useProjects } from '../Projects/ProjectQueries' import { @@ -106,7 +107,8 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false) const [projectId, setProjectId] = useState(getInitialProject()) const [attachments, setAttachments] = useState([]) - const [draftId, setDraftId] = useState(() => crypto.randomUUID()) + + const [draftId, setDraftId] = useState(() => generateUUID()) const [showScan, setShowScan] = useState(false) const [scanAutoCapture, setScanAutoCapture] = useState(false) const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null) @@ -595,7 +597,7 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { setDueTime(null) setUseCustomTime(false) setAttachments([]) - setDraftId(crypto.randomUUID()) + setDraftId(generateUUID()) } const createChore = () => { From 09909514d594cab2f2b171d989972909651615a1 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Fri, 10 Jul 2026 01:07:55 -0400 Subject: [PATCH 4/5] fix: update NFC URL generation to support iOS platform in ChoreModals component --- src/views/Chores/components/ChoreModals.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/views/Chores/components/ChoreModals.jsx b/src/views/Chores/components/ChoreModals.jsx index 9191088..4f31ad0 100644 --- a/src/views/Chores/components/ChoreModals.jsx +++ b/src/views/Chores/components/ChoreModals.jsx @@ -6,7 +6,7 @@ import TextModal from '../../Modals/Inputs/TextModal' import WriteNFCModal from '../../Modals/Inputs/WriteNFCModal' const getNFCUrl = choreId => - Capacitor.getPlatform() === 'android' + Capacitor.getPlatform() === 'android' || Capacitor.getPlatform() === 'ios' ? `donetick://chores/${choreId}` : `${window.location.origin}/chores/${choreId}` @@ -77,7 +77,6 @@ const ChoreModals = ({ /> )} - {activeModal === 'nudge' && modalChore && ( Date: Fri, 10 Jul 2026 01:45:16 -0400 Subject: [PATCH 5/5] fix: correct file patterns in Crowdin configuration for locale files --- crowdin.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crowdin.yml b/crowdin.yml index 677bd2c..eae070f 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -6,6 +6,6 @@ api_token_env: CROWDIN_API_TOKEN preserve_hierarchy: true files: - - source: /public/locales/en/**/*.json - translation: /public/locales/%two_letters_code%/**/%original_file_name% + - source: /public/locales/en/*.json + translation: /public/locales/%two_letters_code%/%original_file_name% update_option: update_as_unapproved