Merge branch 'main' of https://github.com/Donetick/frontend
This commit is contained in:
12
crowdin.yml
12
crowdin.yml
@@ -1,3 +1,11 @@
|
|||||||
|
# Crowdin configuration for Donetick
|
||||||
|
# See: https://support.crowdin.com/configuration-file/
|
||||||
|
|
||||||
|
project_id: "donetick"
|
||||||
|
api_token_env: CROWDIN_API_TOKEN
|
||||||
|
preserve_hierarchy: true
|
||||||
|
|
||||||
files:
|
files:
|
||||||
- source: /public/local/en/main.json
|
- source: /public/locales/en/**/*.json
|
||||||
translation: /public/local/%two_letters_code%/%original_file_name%
|
translation: /public/locales/%two_letters_code%/**/%original_file_name%
|
||||||
|
update_option: update_as_unapproved
|
||||||
|
|||||||
123
package-lock.json
generated
123
package-lock.json
generated
@@ -45,6 +45,9 @@
|
|||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"event-source-polyfill": "^1.0.31",
|
"event-source-polyfill": "^1.0.31",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"i18next": "^25.8.14",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"murmurhash": "^2.0.1",
|
"murmurhash": "^2.0.1",
|
||||||
@@ -56,6 +59,7 @@
|
|||||||
"react-calendar": "^5.1.0",
|
"react-calendar": "^5.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-easy-crop": "^5.4.2",
|
"react-easy-crop": "^5.4.2",
|
||||||
|
"react-i18next": "^16.5.6",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"reactjs-social-login": "^2.6.3",
|
"reactjs-social-login": "^2.6.3",
|
||||||
@@ -1407,9 +1411,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.28.3",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -5548,6 +5552,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-fetch": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.6.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8070,6 +8083,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-parse-stringify": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"void-elements": "3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/husky": {
|
"node_modules/husky": {
|
||||||
"version": "8.0.3",
|
"version": "8.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8084,6 +8106,55 @@
|
|||||||
"url": "https://github.com/sponsors/typicode"
|
"url": "https://github.com/sponsors/typicode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "25.8.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.14.tgz",
|
||||||
|
"integrity": "sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com/i18next.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/i18next-browser-languagedetector": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.23.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/i18next-http-backend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-fetch": "4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ico-endec": {
|
"node_modules/ico-endec": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.6",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
@@ -9530,7 +9601,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^5.0.0"
|
"whatwg-url": "^5.0.0"
|
||||||
@@ -10736,6 +10806,33 @@
|
|||||||
"react-dom": ">=16.4.0"
|
"react-dom": ">=16.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-i18next": {
|
||||||
|
"version": "16.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.6.tgz",
|
||||||
|
"integrity": "sha512-Ua7V2/efA88ido7KyK51fb8Ki8M/sRfW8LR/rZ/9ZKr2luhuTI7kwYZN5agT1rWG7aYm5G0RYE/6JR8KJoCMDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"html-parse-stringify": "^3.0.1",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"i18next": ">= 25.6.2",
|
||||||
|
"react": ">= 16.8.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.1.1",
|
"version": "19.1.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -13277,7 +13374,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/tr46": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tree-kill": {
|
"node_modules/tree-kill": {
|
||||||
@@ -13482,7 +13578,7 @@
|
|||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -13631,7 +13727,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@@ -13795,6 +13893,15 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/void-elements": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/warning": {
|
"node_modules/warning": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -13804,12 +13911,10 @@
|
|||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tr46": "~0.0.3",
|
"tr46": "~0.0.3",
|
||||||
|
|||||||
@@ -72,6 +72,9 @@
|
|||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"event-source-polyfill": "^1.0.31",
|
"event-source-polyfill": "^1.0.31",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"i18next": "^25.8.14",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"murmurhash": "^2.0.1",
|
"murmurhash": "^2.0.1",
|
||||||
@@ -83,6 +86,7 @@
|
|||||||
"react-calendar": "^5.1.0",
|
"react-calendar": "^5.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-easy-crop": "^5.4.2",
|
"react-easy-crop": "^5.4.2",
|
||||||
|
"react-i18next": "^16.5.6",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"reactjs-social-login": "^2.6.3",
|
"reactjs-social-login": "^2.6.3",
|
||||||
|
|||||||
75
public/locales/ar/chores.json
Normal file
75
public/locales/ar/chores.json
Normal file
@@ -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": "حذف الجلسة"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/locales/ar/common.json
Normal file
33
public/locales/ar/common.json
Normal file
@@ -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": "الإعدادات"
|
||||||
|
}
|
||||||
|
}
|
||||||
174
public/locales/ar/settings.json
Normal file
174
public/locales/ar/settings.json
Normal file
@@ -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",
|
||||||
|
"webhookDescription": "أدخل رابط webhook لتلقي إشعارات أحداث الدائرة",
|
||||||
|
"webhookPlaceholder": "https://your-webhook-url.com"
|
||||||
|
},
|
||||||
|
"accountSettings": {
|
||||||
|
"title": "إعدادات الحساب",
|
||||||
|
"subscription": "الاشتراك",
|
||||||
|
"subscriptionStatus": "الخطة الحالية",
|
||||||
|
"free": "مجاني",
|
||||||
|
"plus": "بلس",
|
||||||
|
"upgrade": "ترقية",
|
||||||
|
"cancel": "إلغاء",
|
||||||
|
"changePassword": "تغيير كلمة المرور",
|
||||||
|
"password": "كلمة المرور",
|
||||||
|
"dangerZone": "منطقة الخطر",
|
||||||
|
"dangerZoneDescription": "بمجرد حذف حسابك، لا يمكن التراجع. يرجى التأكد.",
|
||||||
|
"deleteAccount": "حذف الحساب"
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "التوطين",
|
||||||
|
"description": "تخصيص اللغة وتنسيق التاريخ والتفضيلات الإقليمية لحسابك.",
|
||||||
|
"language": "اللغة",
|
||||||
|
"languageDescription": "اختر لغتك المفضلة",
|
||||||
|
"dateFormat": "تنسيق التاريخ",
|
||||||
|
"dateFormatDescription": "اختر كيفية عرض التواريخ في التطبيق",
|
||||||
|
"timeFormat": "تنسيق الوقت",
|
||||||
|
"timeFormatDescription": "اختر تنسيق 12 أو 24 ساعة",
|
||||||
|
"12hour": "12 ساعة (ص/م)",
|
||||||
|
"24hour": "24 ساعة",
|
||||||
|
"firstDayOfWeek": "أول يوم في الأسبوع",
|
||||||
|
"firstDayOfWeekDescription": "اختر اليوم الذي يبدأ به أسبوعك",
|
||||||
|
"sunday": "الأحد",
|
||||||
|
"monday": "الاثنين",
|
||||||
|
"saturday": "السبت",
|
||||||
|
"formats": {
|
||||||
|
"mdy": "MM/DD/YYYY (الولايات المتحدة)",
|
||||||
|
"dmy": "DD/MM/YYYY (أوروبا)",
|
||||||
|
"ymd": "YYYY-MM-DD (ISO)",
|
||||||
|
"long": "تنسيق طويل (مثل 1 يناير 2024)",
|
||||||
|
"short": "تنسيق قصير (مثل 1 يناير 2024)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "تخصيص اللوحة الجانبية",
|
||||||
|
"description": "قم بتخصيص تخطيط ورؤية البطاقات في اللوحة الجانبية. هذا القسم متاح فقط على أجهزة الشاشة الكبيرة مثل الأجهزة اللوحية وأجهزة سطح المكتب."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "تفضيلات المظهر",
|
||||||
|
"description": "اختر كيف يبدو الموقع لك. حدد مظهرًا واحدًا أو قم بالمزامنة مع نظامك والتبديل تلقائيًا بين مظاهر النهار والليل.",
|
||||||
|
"themeMode": "وضع المظهر",
|
||||||
|
"light": "فاتح",
|
||||||
|
"dark": "داكن",
|
||||||
|
"system": "النظام"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"settingsSaved": "تم حفظ الإعدادات بنجاح",
|
||||||
|
"settingsSaveFailed": "فشل حفظ الإعدادات",
|
||||||
|
"invalidWebhook": "رابط webhook غير صالح"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "إعدادات الملف الشخصي",
|
||||||
|
"description": "تحديث اسم العرض وصورة الملف الشخصي.",
|
||||||
|
"photoUpdated": "تم تحديث الصورة",
|
||||||
|
"photoUpdatedMessage": "تم تحديث صورة ملفك الشخصي بنجاح!",
|
||||||
|
"uploadFailed": "فشل التحميل",
|
||||||
|
"uploadFailedMessage": "فشل تحميل صورتك. يرجى المحاولة مرة أخرى.",
|
||||||
|
"profileUpdated": "تم تحديث الملف الشخصي",
|
||||||
|
"profileUpdatedMessage": "تم حفظ معلومات ملفك الشخصي بنجاح!",
|
||||||
|
"updateFailed": "فشل التحديث",
|
||||||
|
"updateFailedMessage": "تعذر تحديث ملفك الشخصي. يرجى التحقق من اتصالك والمحاولة مرة أخرى.",
|
||||||
|
"changePhoto": "تغيير الصورة",
|
||||||
|
"displayName": "اسم العرض",
|
||||||
|
"displayNamePlaceholder": "أدخل اسم العرض الخاص بك",
|
||||||
|
"timezone": "المنطقة الزمنية",
|
||||||
|
"timezonePlaceholder": "اختر منطقتك الزمنية",
|
||||||
|
"save": "حفظ",
|
||||||
|
"cancel": "إلغاء"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "الإعدادات",
|
||||||
|
"subtitle": "قم بتخصيص تجربتك وإدارة تفضيلات حسابك",
|
||||||
|
"upgrade": {
|
||||||
|
"title": "الترقية إلى بلس",
|
||||||
|
"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": "تكوين webhooks والتحديثات في الوقت الفعلي وميزات متقدمة أخرى لتعزيز الإنتاجية."
|
||||||
|
},
|
||||||
|
"developer": {
|
||||||
|
"title": "إعدادات المطور",
|
||||||
|
"description": "عرض المعلومات الفنية حول رموز المصادقة واتصالات SSE وبيانات التصحيح."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
public/locales/en/chores.json
Normal file
75
public/locales/en/chores.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"title": "Chores",
|
||||||
|
"myChores": "My Chores",
|
||||||
|
"allChores": "All Chores",
|
||||||
|
"addChore": "Add Chore",
|
||||||
|
"editChore": "Edit Chore",
|
||||||
|
"deleteChore": "Delete Chore",
|
||||||
|
"completeChore": "Complete Task",
|
||||||
|
"dueDate": "Due Date",
|
||||||
|
"assignedTo": "Assigned To",
|
||||||
|
"priority": "Priority",
|
||||||
|
"status": "Status",
|
||||||
|
"description": "Description",
|
||||||
|
"choreView": {
|
||||||
|
"assignment": "Assignment",
|
||||||
|
"assigned": "Assigned",
|
||||||
|
"last": "Last",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"due": "Due",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"completed": "Completed",
|
||||||
|
"times": "times",
|
||||||
|
"details": "Details",
|
||||||
|
"createdBy": "Created By",
|
||||||
|
"na": "N/A",
|
||||||
|
"taskCompleted": "Task Completed",
|
||||||
|
"taskCompletedMessage": "Your task has been marked as complete",
|
||||||
|
"taskCompletionUndone": "Task completion has been undone.",
|
||||||
|
"taskSkipUndone": "Task skip has been undone.",
|
||||||
|
"undoSuccessful": "Undo Successful",
|
||||||
|
"undoFailed": "Undo Failed",
|
||||||
|
"undoFailedMessage": "Unable to undo the action. Please try again.",
|
||||||
|
"resetTimer": "Reset Timer",
|
||||||
|
"resetTimerConfirmation": "Are you sure you want to reset the timer? This will clear all time records since you started the task.",
|
||||||
|
"clearAllTimeRecords": "Clear All Time Records",
|
||||||
|
"clearAllTimeConfirmation": "This will permanently delete all timers for this task and set it back to \"not started\".",
|
||||||
|
"descriptionTitle": "Description",
|
||||||
|
"description": "Description :",
|
||||||
|
"previousNote": "Previous Note",
|
||||||
|
"previousNoteLabel": "Previous note:",
|
||||||
|
"subtasksLabel": "Subtasks :",
|
||||||
|
"taskActions": "Task Actions",
|
||||||
|
"addNote": "Add a note",
|
||||||
|
"additionalNotes": "Additional Notes:",
|
||||||
|
"notePlaceholder": "Add a note about the completion...",
|
||||||
|
"setCustomCompletionTime": "Set custom completion time",
|
||||||
|
"skipTask": "Skip Task",
|
||||||
|
"skipTaskConfirmation": "Are you sure you want to skip this task?",
|
||||||
|
"markComplete": "Mark Complete",
|
||||||
|
"markAsDone": "Mark as done",
|
||||||
|
"edit": "Edit",
|
||||||
|
"archive": "Archive",
|
||||||
|
"unarchive": "Unarchive",
|
||||||
|
"viewHistory": "View History",
|
||||||
|
"history": "History",
|
||||||
|
"startTimer": "Start Timer",
|
||||||
|
"start": "Start",
|
||||||
|
"pauseTimer": "Pause Timer",
|
||||||
|
"approve": "Approve",
|
||||||
|
"reject": "Reject",
|
||||||
|
"pendingApproval": "Pending Approval",
|
||||||
|
"undo": "Undo",
|
||||||
|
"skip": "Skip",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"noPriority": "No Priority",
|
||||||
|
"subtasks": "Subtasks",
|
||||||
|
"noDescription": "No description available",
|
||||||
|
"timer": {
|
||||||
|
"active": "Timer Active",
|
||||||
|
"paused": "Timer Paused",
|
||||||
|
"reset": "Reset Timer",
|
||||||
|
"delete": "Delete Session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/locales/en/common.json
Normal file
33
public/locales/en/common.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"close": "Close",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "Error",
|
||||||
|
"success": "Success",
|
||||||
|
"warning": "Warning",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied!",
|
||||||
|
"settings": "Settings",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"back": "Back",
|
||||||
|
"backToCalendar": "Back to Calendar",
|
||||||
|
"logout": "Logout",
|
||||||
|
"version": "Version",
|
||||||
|
"navigation": {
|
||||||
|
"allTasks": "All Tasks",
|
||||||
|
"archived": "Archived",
|
||||||
|
"things": "Things",
|
||||||
|
"labels": "Labels",
|
||||||
|
"projects": "Projects",
|
||||||
|
"filters": "Filters",
|
||||||
|
"activities": "Activities",
|
||||||
|
"points": "Points",
|
||||||
|
"settings": "Settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
174
public/locales/en/settings.json
Normal file
174
public/locales/en/settings.json
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"title": "Settings",
|
||||||
|
"circleSettings": {
|
||||||
|
"title": "Circle settings",
|
||||||
|
"description": "Your account is automatically connected to a Circle when you create or join one. Easily invite friends by sharing the unique Circle code or link below. You'll receive a notification below when someone requests to join your Circle. If you'd like to leave, simply hit the 'Leave Circle' button.",
|
||||||
|
"circleCode": "Circle Code",
|
||||||
|
"copyCode": "Copy Code",
|
||||||
|
"copyLink": "Copy Link",
|
||||||
|
"codeCopied": "Circle code copied!",
|
||||||
|
"linkCopied": "Circle link copied!",
|
||||||
|
"joinCircle": "Join a Circle",
|
||||||
|
"joinCirclePlaceholder": "Enter Circle Code",
|
||||||
|
"join": "Join",
|
||||||
|
"leave": "Leave Circle",
|
||||||
|
"leaveConfirmTitle": "Leave Circle",
|
||||||
|
"leaveConfirmMessage": "Are you sure you want to leave this circle?",
|
||||||
|
"circleMembers": "Circle Members",
|
||||||
|
"circleMemberRequests": "Circle Member Requests",
|
||||||
|
"admin": "Admin",
|
||||||
|
"member": "Member",
|
||||||
|
"pending": "Pending",
|
||||||
|
"accept": "Accept",
|
||||||
|
"reject": "Reject",
|
||||||
|
"makeAdmin": "Make Admin",
|
||||||
|
"makeMember": "Make Member",
|
||||||
|
"remove": "Remove",
|
||||||
|
"webhookURL": "Webhook URL",
|
||||||
|
"webhookDescription": "Enter a webhook URL to receive notifications for circle events",
|
||||||
|
"webhookPlaceholder": "https://your-webhook-url.com"
|
||||||
|
},
|
||||||
|
"accountSettings": {
|
||||||
|
"title": "Account Settings",
|
||||||
|
"subscription": "Subscription",
|
||||||
|
"subscriptionStatus": "Current Plan",
|
||||||
|
"free": "Free",
|
||||||
|
"plus": "Plus",
|
||||||
|
"upgrade": "Upgrade",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"changePassword": "Change Password",
|
||||||
|
"password": "Password",
|
||||||
|
"dangerZone": "Danger Zone",
|
||||||
|
"dangerZoneDescription": "Once you delete your account, there is no going back. Please be certain.",
|
||||||
|
"deleteAccount": "Delete Account"
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Localization",
|
||||||
|
"description": "Customize language, date format, and regional preferences for your account.",
|
||||||
|
"language": "Language",
|
||||||
|
"languageDescription": "Select your preferred language",
|
||||||
|
"dateFormat": "Date Format",
|
||||||
|
"dateFormatDescription": "Choose how dates should be displayed throughout the application",
|
||||||
|
"timeFormat": "Time Format",
|
||||||
|
"timeFormatDescription": "Select 12-hour or 24-hour time format",
|
||||||
|
"12hour": "12-hour (AM/PM)",
|
||||||
|
"24hour": "24-hour",
|
||||||
|
"firstDayOfWeek": "First Day of Week",
|
||||||
|
"firstDayOfWeekDescription": "Select which day starts your week",
|
||||||
|
"sunday": "Sunday",
|
||||||
|
"monday": "Monday",
|
||||||
|
"saturday": "Saturday",
|
||||||
|
"formats": {
|
||||||
|
"mdy": "MM/DD/YYYY (US)",
|
||||||
|
"dmy": "DD/MM/YYYY (Europe)",
|
||||||
|
"ymd": "YYYY-MM-DD (ISO)",
|
||||||
|
"long": "Long format (e.g., January 1, 2024)",
|
||||||
|
"short": "Short format (e.g., Jan 1, 2024)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Sidepanel Customization",
|
||||||
|
"description": "Customize the layout and visibility of cards in the sidepanel. This section is only available on large screen devices such as tablets and desktops."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Theme preferences",
|
||||||
|
"description": "Choose how the site looks to you. Select a single theme, or sync with your system and automatically switch between day and night themes.",
|
||||||
|
"themeMode": "Theme mode",
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"settingsSaved": "Settings saved successfully",
|
||||||
|
"settingsSaveFailed": "Failed to save settings",
|
||||||
|
"invalidWebhook": "Invalid webhook URL"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Profile Settings",
|
||||||
|
"description": "Update your display name and profile photo.",
|
||||||
|
"photoUpdated": "Photo Updated",
|
||||||
|
"photoUpdatedMessage": "Your profile photo has been updated successfully!",
|
||||||
|
"uploadFailed": "Upload Failed",
|
||||||
|
"uploadFailedMessage": "Failed to upload your photo. Please try again.",
|
||||||
|
"profileUpdated": "Profile Updated",
|
||||||
|
"profileUpdatedMessage": "Your profile information has been saved successfully!",
|
||||||
|
"updateFailed": "Update Failed",
|
||||||
|
"updateFailedMessage": "Unable to update your profile. Please check your connection and try again.",
|
||||||
|
"changePhoto": "Change Photo",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"displayNamePlaceholder": "Enter your display name",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"timezonePlaceholder": "Select your timezone",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "Settings",
|
||||||
|
"subtitle": "Customize your experience and manage your account preferences",
|
||||||
|
"upgrade": {
|
||||||
|
"title": "Upgrade to Plus",
|
||||||
|
"description": "Unlock powerful features to enhance your productivity",
|
||||||
|
"button": "Upgrade Now",
|
||||||
|
"features": {
|
||||||
|
"richText": "Rich text descriptions",
|
||||||
|
"notifications": "Task notifications",
|
||||||
|
"apiIntegrations": "API integrations",
|
||||||
|
"advancedAutomation": "Advanced automation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"profile": {
|
||||||
|
"title": "Profile Settings",
|
||||||
|
"description": "Update your profile information, photo, display name, and timezone preferences."
|
||||||
|
},
|
||||||
|
"circle": {
|
||||||
|
"title": "Circle Settings",
|
||||||
|
"description": "Manage your circle, invite members, and handle join requests."
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"title": "Account Settings",
|
||||||
|
"description": "Manage your subscription, change password, and account deletion options."
|
||||||
|
},
|
||||||
|
"subaccounts": {
|
||||||
|
"title": "Managed Accounts",
|
||||||
|
"description": "Create and manage sub accounts to log in and complete assigned tasks."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"title": "Notifications",
|
||||||
|
"description": "Configure push notifications, email alerts, and notification targets for tasks."
|
||||||
|
},
|
||||||
|
"mfa": {
|
||||||
|
"title": "Multi-Factor Authentication",
|
||||||
|
"description": "Add an extra layer of security with MFA using authenticator apps."
|
||||||
|
},
|
||||||
|
"apitokens": {
|
||||||
|
"title": "API Tokens",
|
||||||
|
"description": "Generate and manage access tokens for third-party integrations and API access."
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Storage Settings",
|
||||||
|
"description": "Backup and restore your data, manage local storage and sync preferences."
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Sidepanel Customization",
|
||||||
|
"description": "Customize the layout and visibility of cards in the sidepanel interface."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Theme Preferences",
|
||||||
|
"description": "Choose your preferred theme and configure dark/light mode settings."
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Localization",
|
||||||
|
"description": "Customize language, date format, time format, and regional preferences."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Advanced Settings",
|
||||||
|
"description": "Configure webhooks, real-time updates, and other advanced features for enhanced productivity."
|
||||||
|
},
|
||||||
|
"developer": {
|
||||||
|
"title": "Developer Settings",
|
||||||
|
"description": "View technical information about authentication tokens, SSE connections, and debug data."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
public/locales/es/chores.json
Normal file
75
public/locales/es/chores.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"title": "Tareas",
|
||||||
|
"myChores": "Mis Tareas",
|
||||||
|
"allChores": "Todas las Tareas",
|
||||||
|
"addChore": "Agregar Tarea",
|
||||||
|
"editChore": "Editar Tarea",
|
||||||
|
"deleteChore": "Eliminar Tarea",
|
||||||
|
"completeChore": "Completar Tarea",
|
||||||
|
"dueDate": "Fecha de Vencimiento",
|
||||||
|
"assignedTo": "Asignado a",
|
||||||
|
"priority": "Prioridad",
|
||||||
|
"status": "Estado",
|
||||||
|
"description": "Descripción",
|
||||||
|
"choreView": {
|
||||||
|
"assignment": "Asignación",
|
||||||
|
"assigned": "Asignado",
|
||||||
|
"last": "Último",
|
||||||
|
"schedule": "Horario",
|
||||||
|
"due": "Vencimiento",
|
||||||
|
"statistics": "Estadísticas",
|
||||||
|
"completed": "Completado",
|
||||||
|
"times": "veces",
|
||||||
|
"details": "Detalles",
|
||||||
|
"createdBy": "Creado por",
|
||||||
|
"na": "N/D",
|
||||||
|
"taskCompleted": "Tarea Completada",
|
||||||
|
"taskCompletedMessage": "Tu tarea ha sido marcada como completada",
|
||||||
|
"taskCompletionUndone": "La finalización de la tarea ha sido deshecha.",
|
||||||
|
"taskSkipUndone": "El salto de tarea ha sido deshecho.",
|
||||||
|
"undoSuccessful": "Deshacer Exitoso",
|
||||||
|
"undoFailed": "Deshacer Fallido",
|
||||||
|
"undoFailedMessage": "No se pudo deshacer la acción. Por favor, inténtalo de nuevo.",
|
||||||
|
"resetTimer": "Reiniciar Temporizador",
|
||||||
|
"resetTimerConfirmation": "¿Estás seguro de que quieres reiniciar el temporizador? Esto borrará todos los registros de tiempo desde que iniciaste la tarea.",
|
||||||
|
"clearAllTimeRecords": "Borrar Todos los Registros de Tiempo",
|
||||||
|
"clearAllTimeConfirmation": "Esto eliminará permanentemente todos los temporizadores de esta tarea y la volverá al estado \"no iniciada\".",
|
||||||
|
"descriptionTitle": "Descripción",
|
||||||
|
"description": "Descripción :",
|
||||||
|
"previousNote": "Nota Anterior",
|
||||||
|
"previousNoteLabel": "Nota anterior:",
|
||||||
|
"subtasksLabel": "Subtareas :",
|
||||||
|
"taskActions": "Acciones de Tarea",
|
||||||
|
"addNote": "Agregar una nota",
|
||||||
|
"additionalNotes": "Notas Adicionales:",
|
||||||
|
"notePlaceholder": "Agregar una nota sobre la finalización...",
|
||||||
|
"setCustomCompletionTime": "Establecer hora de finalización personalizada",
|
||||||
|
"skipTask": "Saltar Tarea",
|
||||||
|
"skipTaskConfirmation": "¿Estás seguro de que quieres saltar esta tarea?",
|
||||||
|
"markComplete": "Marcar como Completada",
|
||||||
|
"markAsDone": "Marcar como hecha",
|
||||||
|
"edit": "Editar",
|
||||||
|
"archive": "Archivar",
|
||||||
|
"unarchive": "Desarchivar",
|
||||||
|
"viewHistory": "Ver Historial",
|
||||||
|
"history": "Historial",
|
||||||
|
"startTimer": "Iniciar Temporizador",
|
||||||
|
"start": "Iniciar",
|
||||||
|
"pauseTimer": "Pausar Temporizador",
|
||||||
|
"approve": "Aprobar",
|
||||||
|
"reject": "Rechazar",
|
||||||
|
"pendingApproval": "Pendiente de Aprobación",
|
||||||
|
"undo": "Deshacer",
|
||||||
|
"skip": "Saltar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"noPriority": "Sin Prioridad",
|
||||||
|
"subtasks": "Subtareas",
|
||||||
|
"noDescription": "No hay descripción disponible",
|
||||||
|
"timer": {
|
||||||
|
"active": "Temporizador Activo",
|
||||||
|
"paused": "Temporizador Pausado",
|
||||||
|
"reset": "Reiniciar Temporizador",
|
||||||
|
"delete": "Eliminar Sesión"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/locales/es/common.json
Normal file
33
public/locales/es/common.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"save": "Guardar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"error": "Error",
|
||||||
|
"success": "Éxito",
|
||||||
|
"warning": "Advertencia",
|
||||||
|
"refresh": "Actualizar",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copied": "¡Copiado!",
|
||||||
|
"settings": "Configuración",
|
||||||
|
"yes": "Sí",
|
||||||
|
"no": "No",
|
||||||
|
"back": "Atrás",
|
||||||
|
"backToCalendar": "Volver al Calendario",
|
||||||
|
"logout": "Cerrar Sesión",
|
||||||
|
"version": "Versión",
|
||||||
|
"navigation": {
|
||||||
|
"allTasks": "Todas las Tareas",
|
||||||
|
"archived": "Archivadas",
|
||||||
|
"things": "Cosas",
|
||||||
|
"labels": "Etiquetas",
|
||||||
|
"projects": "Proyectos",
|
||||||
|
"filters": "Filtros",
|
||||||
|
"activities": "Actividades",
|
||||||
|
"points": "Puntos",
|
||||||
|
"settings": "Configuración"
|
||||||
|
}
|
||||||
|
}
|
||||||
174
public/locales/es/settings.json
Normal file
174
public/locales/es/settings.json
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"title": "Configuración",
|
||||||
|
"circleSettings": {
|
||||||
|
"title": "Configuración del círculo",
|
||||||
|
"description": "Tu cuenta se conecta automáticamente a un Círculo cuando creas o te unes a uno. Invita fácilmente a amigos compartiendo el código único del Círculo o el enlace a continuación.",
|
||||||
|
"circleCode": "Código del Círculo",
|
||||||
|
"copyCode": "Copiar Código",
|
||||||
|
"copyLink": "Copiar Enlace",
|
||||||
|
"codeCopied": "¡Código del círculo copiado!",
|
||||||
|
"linkCopied": "¡Enlace copiado!",
|
||||||
|
"joinCircle": "Unirse a un Círculo",
|
||||||
|
"joinCirclePlaceholder": "Ingresa el Código del Círculo",
|
||||||
|
"join": "Unirse",
|
||||||
|
"leave": "Salir del Círculo",
|
||||||
|
"leaveConfirmTitle": "Salir del Círculo",
|
||||||
|
"leaveConfirmMessage": "¿Estás seguro de que quieres salir de este círculo?",
|
||||||
|
"circleMembers": "Miembros del Círculo",
|
||||||
|
"circleMemberRequests": "Solicitudes de Miembros del Círculo",
|
||||||
|
"admin": "Administrador",
|
||||||
|
"member": "Miembro",
|
||||||
|
"pending": "Pendiente",
|
||||||
|
"accept": "Aceptar",
|
||||||
|
"reject": "Rechazar",
|
||||||
|
"makeAdmin": "Hacer Administrador",
|
||||||
|
"makeMember": "Hacer Miembro",
|
||||||
|
"remove": "Eliminar",
|
||||||
|
"webhookURL": "URL del Webhook",
|
||||||
|
"webhookDescription": "Ingresa una URL de webhook para recibir notificaciones de eventos del círculo",
|
||||||
|
"webhookPlaceholder": "https://tu-url-webhook.com"
|
||||||
|
},
|
||||||
|
"accountSettings": {
|
||||||
|
"title": "Configuración de la Cuenta",
|
||||||
|
"subscription": "Suscripción",
|
||||||
|
"subscriptionStatus": "Plan Actual",
|
||||||
|
"free": "Gratis",
|
||||||
|
"plus": "Plus",
|
||||||
|
"upgrade": "Actualizar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"changePassword": "Cambiar Contraseña",
|
||||||
|
"password": "Contraseña",
|
||||||
|
"dangerZone": "Zona de Peligro",
|
||||||
|
"dangerZoneDescription": "Una vez que elimines tu cuenta, no hay vuelta atrás. Por favor, está seguro.",
|
||||||
|
"deleteAccount": "Eliminar Cuenta"
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Localización",
|
||||||
|
"description": "Personaliza el idioma, formato de fecha y preferencias regionales para tu cuenta.",
|
||||||
|
"language": "Idioma",
|
||||||
|
"languageDescription": "Selecciona tu idioma preferido",
|
||||||
|
"dateFormat": "Formato de Fecha",
|
||||||
|
"dateFormatDescription": "Elige cómo se deben mostrar las fechas en toda la aplicación",
|
||||||
|
"timeFormat": "Formato de Hora",
|
||||||
|
"timeFormatDescription": "Selecciona formato de 12 o 24 horas",
|
||||||
|
"12hour": "12 horas (AM/PM)",
|
||||||
|
"24hour": "24 horas",
|
||||||
|
"firstDayOfWeek": "Primer Día de la Semana",
|
||||||
|
"firstDayOfWeekDescription": "Selecciona qué día comienza tu semana",
|
||||||
|
"sunday": "Domingo",
|
||||||
|
"monday": "Lunes",
|
||||||
|
"saturday": "Sábado",
|
||||||
|
"formats": {
|
||||||
|
"mdy": "MM/DD/AAAA (EE.UU.)",
|
||||||
|
"dmy": "DD/MM/AAAA (Europa)",
|
||||||
|
"ymd": "AAAA-MM-DD (ISO)",
|
||||||
|
"long": "Formato largo (ej., 1 de enero de 2024)",
|
||||||
|
"short": "Formato corto (ej., 1 ene 2024)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Personalización del Panel Lateral",
|
||||||
|
"description": "Personaliza el diseño y la visibilidad de las tarjetas en el panel lateral. Esta sección solo está disponible en dispositivos de pantalla grande como tabletas y computadoras de escritorio."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Preferencias de tema",
|
||||||
|
"description": "Elige cómo se ve el sitio para ti. Selecciona un solo tema o sincronízalo con tu sistema y cambia automáticamente entre temas de día y noche.",
|
||||||
|
"themeMode": "Modo de tema",
|
||||||
|
"light": "Claro",
|
||||||
|
"dark": "Oscuro",
|
||||||
|
"system": "Sistema"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"settingsSaved": "Configuración guardada con éxito",
|
||||||
|
"settingsSaveFailed": "Error al guardar la configuración",
|
||||||
|
"invalidWebhook": "URL de webhook no válida"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Configuración del Perfil",
|
||||||
|
"description": "Actualiza tu nombre para mostrar y foto de perfil.",
|
||||||
|
"photoUpdated": "Foto Actualizada",
|
||||||
|
"photoUpdatedMessage": "¡Tu foto de perfil ha sido actualizada con éxito!",
|
||||||
|
"uploadFailed": "Error al Subir",
|
||||||
|
"uploadFailedMessage": "Error al subir tu foto. Por favor, inténtalo de nuevo.",
|
||||||
|
"profileUpdated": "Perfil Actualizado",
|
||||||
|
"profileUpdatedMessage": "¡Tu información de perfil ha sido guardada con éxito!",
|
||||||
|
"updateFailed": "Error al Actualizar",
|
||||||
|
"updateFailedMessage": "No se pudo actualizar tu perfil. Por favor, verifica tu conexión e inténtalo de nuevo.",
|
||||||
|
"changePhoto": "Cambiar Foto",
|
||||||
|
"displayName": "Nombre para Mostrar",
|
||||||
|
"displayNamePlaceholder": "Ingresa tu nombre para mostrar",
|
||||||
|
"timezone": "Zona Horaria",
|
||||||
|
"timezonePlaceholder": "Selecciona tu zona horaria",
|
||||||
|
"save": "Guardar",
|
||||||
|
"cancel": "Cancelar"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "Configuración",
|
||||||
|
"subtitle": "Personaliza tu experiencia y administra las preferencias de tu cuenta",
|
||||||
|
"upgrade": {
|
||||||
|
"title": "Actualizar a Plus",
|
||||||
|
"description": "Desbloquea funciones potentes para mejorar tu productividad",
|
||||||
|
"button": "Actualizar Ahora",
|
||||||
|
"features": {
|
||||||
|
"richText": "Descripciones en texto enriquecido",
|
||||||
|
"notifications": "Notificaciones de tareas",
|
||||||
|
"apiIntegrations": "Integraciones API",
|
||||||
|
"advancedAutomation": "Automatización avanzada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"profile": {
|
||||||
|
"title": "Configuración del Perfil",
|
||||||
|
"description": "Actualiza tu información de perfil, foto, nombre para mostrar y preferencias de zona horaria."
|
||||||
|
},
|
||||||
|
"circle": {
|
||||||
|
"title": "Configuración del Círculo",
|
||||||
|
"description": "Administra tu círculo, invita miembros y gestiona solicitudes de unión."
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"title": "Configuración de la Cuenta",
|
||||||
|
"description": "Administra tu suscripción, cambia la contraseña y opciones de eliminación de cuenta."
|
||||||
|
},
|
||||||
|
"subaccounts": {
|
||||||
|
"title": "Cuentas Administradas",
|
||||||
|
"description": "Crea y administra subcuentas para iniciar sesión y completar tareas asignadas."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"title": "Notificaciones",
|
||||||
|
"description": "Configura notificaciones push, alertas por correo electrónico y destinos de notificación para tareas."
|
||||||
|
},
|
||||||
|
"mfa": {
|
||||||
|
"title": "Autenticación Multifactor",
|
||||||
|
"description": "Agrega una capa adicional de seguridad con MFA usando aplicaciones de autenticación."
|
||||||
|
},
|
||||||
|
"apitokens": {
|
||||||
|
"title": "Tokens API",
|
||||||
|
"description": "Genera y administra tokens de acceso para integraciones de terceros y acceso a la API."
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Configuración de Almacenamiento",
|
||||||
|
"description": "Respalda y restaura tus datos, administra el almacenamiento local y las preferencias de sincronización."
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Personalización del Panel Lateral",
|
||||||
|
"description": "Personaliza el diseño y la visibilidad de las tarjetas en la interfaz del panel lateral."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Preferencias de Tema",
|
||||||
|
"description": "Elige tu tema preferido y configura los ajustes de modo oscuro/claro."
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Localización",
|
||||||
|
"description": "Personaliza el idioma, formato de fecha, formato de hora y preferencias regionales."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Configuración Avanzada",
|
||||||
|
"description": "Configura webhooks, actualizaciones en tiempo real y otras funciones avanzadas para mejorar la productividad."
|
||||||
|
},
|
||||||
|
"developer": {
|
||||||
|
"title": "Configuración de Desarrollador",
|
||||||
|
"description": "Ver información técnica sobre tokens de autenticación, conexiones SSE y datos de depuración."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
public/locales/fr/chores.json
Normal file
75
public/locales/fr/chores.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"title": "Tâches",
|
||||||
|
"myChores": "Mes Tâches",
|
||||||
|
"allChores": "Toutes les Tâches",
|
||||||
|
"addChore": "Ajouter une Tâche",
|
||||||
|
"editChore": "Modifier la Tâche",
|
||||||
|
"deleteChore": "Supprimer la Tâche",
|
||||||
|
"completeChore": "Terminer la Tâche",
|
||||||
|
"dueDate": "Date d'Échéance",
|
||||||
|
"assignedTo": "Assigné à",
|
||||||
|
"priority": "Priorité",
|
||||||
|
"status": "Statut",
|
||||||
|
"description": "Description",
|
||||||
|
"choreView": {
|
||||||
|
"assignment": "Attribution",
|
||||||
|
"assigned": "Assigné",
|
||||||
|
"last": "Dernier",
|
||||||
|
"schedule": "Calendrier",
|
||||||
|
"due": "Échéance",
|
||||||
|
"statistics": "Statistiques",
|
||||||
|
"completed": "Terminé",
|
||||||
|
"times": "fois",
|
||||||
|
"details": "Détails",
|
||||||
|
"createdBy": "Créé par",
|
||||||
|
"na": "N/A",
|
||||||
|
"taskCompleted": "Tâche Terminée",
|
||||||
|
"taskCompletedMessage": "Votre tâche a été marquée comme terminée",
|
||||||
|
"taskCompletionUndone": "La complétion de la tâche a été annulée.",
|
||||||
|
"taskSkipUndone": "Le saut de tâche a été annulé.",
|
||||||
|
"undoSuccessful": "Annulation Réussie",
|
||||||
|
"undoFailed": "Annulation Échouée",
|
||||||
|
"undoFailedMessage": "Impossible d'annuler l'action. Veuillez réessayer.",
|
||||||
|
"resetTimer": "Réinitialiser le Minuteur",
|
||||||
|
"resetTimerConfirmation": "Êtes-vous sûr de vouloir réinitialiser le minuteur ? Cela effacera tous les enregistrements de temps depuis le début de la tâche.",
|
||||||
|
"clearAllTimeRecords": "Effacer Tous les Enregistrements de Temps",
|
||||||
|
"clearAllTimeConfirmation": "Cela supprimera définitivement tous les minuteurs de cette tâche et la remettra à l'état \"non démarrée\".",
|
||||||
|
"descriptionTitle": "Description",
|
||||||
|
"description": "Description :",
|
||||||
|
"previousNote": "Note Précédente",
|
||||||
|
"previousNoteLabel": "Note précédente :",
|
||||||
|
"subtasksLabel": "Sous-tâches :",
|
||||||
|
"taskActions": "Actions de Tâche",
|
||||||
|
"addNote": "Ajouter une note",
|
||||||
|
"additionalNotes": "Notes Supplémentaires :",
|
||||||
|
"notePlaceholder": "Ajouter une note sur la complétion...",
|
||||||
|
"setCustomCompletionTime": "Définir une heure de complétion personnalisée",
|
||||||
|
"skipTask": "Passer la Tâche",
|
||||||
|
"skipTaskConfirmation": "Êtes-vous sûr de vouloir passer cette tâche ?",
|
||||||
|
"markComplete": "Marquer comme Terminée",
|
||||||
|
"markAsDone": "Marquer comme fait",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"archive": "Archiver",
|
||||||
|
"unarchive": "Désarchiver",
|
||||||
|
"viewHistory": "Voir l'Historique",
|
||||||
|
"history": "Historique",
|
||||||
|
"startTimer": "Démarrer le Minuteur",
|
||||||
|
"start": "Démarrer",
|
||||||
|
"pauseTimer": "Mettre en Pause le Minuteur",
|
||||||
|
"approve": "Approuver",
|
||||||
|
"reject": "Rejeter",
|
||||||
|
"pendingApproval": "En Attente d'Approbation",
|
||||||
|
"undo": "Annuler",
|
||||||
|
"skip": "Passer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"noPriority": "Aucune Priorité",
|
||||||
|
"subtasks": "Sous-tâches",
|
||||||
|
"noDescription": "Aucune description disponible",
|
||||||
|
"timer": {
|
||||||
|
"active": "Minuteur Actif",
|
||||||
|
"paused": "Minuteur en Pause",
|
||||||
|
"reset": "Réinitialiser le Minuteur",
|
||||||
|
"delete": "Supprimer la Session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/locales/fr/common.json
Normal file
33
public/locales/fr/common.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"close": "Fermer",
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"error": "Erreur",
|
||||||
|
"success": "Succès",
|
||||||
|
"warning": "Avertissement",
|
||||||
|
"refresh": "Actualiser",
|
||||||
|
"copy": "Copier",
|
||||||
|
"copied": "Copié !",
|
||||||
|
"settings": "Paramètres",
|
||||||
|
"yes": "Oui",
|
||||||
|
"no": "Non",
|
||||||
|
"back": "Retour",
|
||||||
|
"backToCalendar": "Retour au Calendrier",
|
||||||
|
"logout": "Déconnexion",
|
||||||
|
"version": "Version",
|
||||||
|
"navigation": {
|
||||||
|
"allTasks": "Toutes les Tâches",
|
||||||
|
"archived": "Archivées",
|
||||||
|
"things": "Objets",
|
||||||
|
"labels": "Étiquettes",
|
||||||
|
"projects": "Projets",
|
||||||
|
"filters": "Filtres",
|
||||||
|
"activities": "Activités",
|
||||||
|
"points": "Points",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
}
|
||||||
|
}
|
||||||
174
public/locales/fr/settings.json
Normal file
174
public/locales/fr/settings.json
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"title": "Paramètres",
|
||||||
|
"circleSettings": {
|
||||||
|
"title": "Paramètres du cercle",
|
||||||
|
"description": "Votre compte est automatiquement connecté à un Cercle lorsque vous en créez un ou en rejoignez un. Invitez facilement des amis en partageant le code unique du Cercle ou le lien ci-dessous.",
|
||||||
|
"circleCode": "Code du Cercle",
|
||||||
|
"copyCode": "Copier le Code",
|
||||||
|
"copyLink": "Copier le Lien",
|
||||||
|
"codeCopied": "Code du cercle copié !",
|
||||||
|
"linkCopied": "Lien copié !",
|
||||||
|
"joinCircle": "Rejoindre un Cercle",
|
||||||
|
"joinCirclePlaceholder": "Entrer le Code du Cercle",
|
||||||
|
"join": "Rejoindre",
|
||||||
|
"leave": "Quitter le Cercle",
|
||||||
|
"leaveConfirmTitle": "Quitter le Cercle",
|
||||||
|
"leaveConfirmMessage": "Êtes-vous sûr de vouloir quitter ce cercle ?",
|
||||||
|
"circleMembers": "Membres du Cercle",
|
||||||
|
"circleMemberRequests": "Demandes de Membres du Cercle",
|
||||||
|
"admin": "Administrateur",
|
||||||
|
"member": "Membre",
|
||||||
|
"pending": "En attente",
|
||||||
|
"accept": "Accepter",
|
||||||
|
"reject": "Rejeter",
|
||||||
|
"makeAdmin": "Nommer Administrateur",
|
||||||
|
"makeMember": "Nommer Membre",
|
||||||
|
"remove": "Supprimer",
|
||||||
|
"webhookURL": "URL du Webhook",
|
||||||
|
"webhookDescription": "Entrez une URL de webhook pour recevoir des notifications pour les événements du cercle",
|
||||||
|
"webhookPlaceholder": "https://votre-url-webhook.com"
|
||||||
|
},
|
||||||
|
"accountSettings": {
|
||||||
|
"title": "Paramètres du Compte",
|
||||||
|
"subscription": "Abonnement",
|
||||||
|
"subscriptionStatus": "Plan Actuel",
|
||||||
|
"free": "Gratuit",
|
||||||
|
"plus": "Plus",
|
||||||
|
"upgrade": "Mettre à Niveau",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"changePassword": "Changer le Mot de Passe",
|
||||||
|
"password": "Mot de passe",
|
||||||
|
"dangerZone": "Zone Dangereuse",
|
||||||
|
"dangerZoneDescription": "Une fois votre compte supprimé, il n'y a pas de retour en arrière. Veuillez être certain.",
|
||||||
|
"deleteAccount": "Supprimer le Compte"
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Localisation",
|
||||||
|
"description": "Personnalisez la langue, le format de date et les préférences régionales pour votre compte.",
|
||||||
|
"language": "Langue",
|
||||||
|
"languageDescription": "Sélectionnez votre langue préférée",
|
||||||
|
"dateFormat": "Format de Date",
|
||||||
|
"dateFormatDescription": "Choisissez comment les dates doivent être affichées dans l'application",
|
||||||
|
"timeFormat": "Format de l'Heure",
|
||||||
|
"timeFormatDescription": "Sélectionnez le format 12 heures ou 24 heures",
|
||||||
|
"12hour": "12 heures (AM/PM)",
|
||||||
|
"24hour": "24 heures",
|
||||||
|
"firstDayOfWeek": "Premier Jour de la Semaine",
|
||||||
|
"firstDayOfWeekDescription": "Sélectionnez quel jour commence votre semaine",
|
||||||
|
"sunday": "Dimanche",
|
||||||
|
"monday": "Lundi",
|
||||||
|
"saturday": "Samedi",
|
||||||
|
"formats": {
|
||||||
|
"mdy": "MM/JJ/AAAA (États-Unis)",
|
||||||
|
"dmy": "JJ/MM/AAAA (Europe)",
|
||||||
|
"ymd": "AAAA-MM-JJ (ISO)",
|
||||||
|
"long": "Format long (ex., 1 janvier 2024)",
|
||||||
|
"short": "Format court (ex., 1 janv. 2024)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Personnalisation du Panneau Latéral",
|
||||||
|
"description": "Personnalisez la disposition et la visibilité des cartes dans le panneau latéral. Cette section n'est disponible que sur les grands écrans comme les tablettes et les ordinateurs de bureau."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Préférences de thème",
|
||||||
|
"description": "Choisissez comment le site vous apparaît. Sélectionnez un thème unique ou synchronisez avec votre système pour basculer automatiquement entre les thèmes jour et nuit.",
|
||||||
|
"themeMode": "Mode de thème",
|
||||||
|
"light": "Clair",
|
||||||
|
"dark": "Sombre",
|
||||||
|
"system": "Système"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"settingsSaved": "Paramètres enregistrés avec succès",
|
||||||
|
"settingsSaveFailed": "Échec de l'enregistrement des paramètres",
|
||||||
|
"invalidWebhook": "URL de webhook invalide"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Paramètres du Profil",
|
||||||
|
"description": "Mettez à jour votre nom d'affichage et votre photo de profil.",
|
||||||
|
"photoUpdated": "Photo Mise à Jour",
|
||||||
|
"photoUpdatedMessage": "Votre photo de profil a été mise à jour avec succès !",
|
||||||
|
"uploadFailed": "Échec du Téléchargement",
|
||||||
|
"uploadFailedMessage": "Échec du téléchargement de votre photo. Veuillez réessayer.",
|
||||||
|
"profileUpdated": "Profil Mis à Jour",
|
||||||
|
"profileUpdatedMessage": "Vos informations de profil ont été enregistrées avec succès !",
|
||||||
|
"updateFailed": "Échec de la Mise à Jour",
|
||||||
|
"updateFailedMessage": "Impossible de mettre à jour votre profil. Veuillez vérifier votre connexion et réessayer.",
|
||||||
|
"changePhoto": "Changer la Photo",
|
||||||
|
"displayName": "Nom d'Affichage",
|
||||||
|
"displayNamePlaceholder": "Entrez votre nom d'affichage",
|
||||||
|
"timezone": "Fuseau Horaire",
|
||||||
|
"timezonePlaceholder": "Sélectionnez votre fuseau horaire",
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"cancel": "Annuler"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "Paramètres",
|
||||||
|
"subtitle": "Personnalisez votre expérience et gérez les préférences de votre compte",
|
||||||
|
"upgrade": {
|
||||||
|
"title": "Passer à Plus",
|
||||||
|
"description": "Débloquez des fonctionnalités puissantes pour améliorer votre productivité",
|
||||||
|
"button": "Mettre à Niveau Maintenant",
|
||||||
|
"features": {
|
||||||
|
"richText": "Descriptions en texte enrichi",
|
||||||
|
"notifications": "Notifications de tâches",
|
||||||
|
"apiIntegrations": "Intégrations API",
|
||||||
|
"advancedAutomation": "Automatisation avancée"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"profile": {
|
||||||
|
"title": "Paramètres du Profil",
|
||||||
|
"description": "Mettez à jour vos informations de profil, photo, nom d'affichage et préférences de fuseau horaire."
|
||||||
|
},
|
||||||
|
"circle": {
|
||||||
|
"title": "Paramètres du Cercle",
|
||||||
|
"description": "Gérez votre cercle, invitez des membres et gérez les demandes d'adhésion."
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"title": "Paramètres du Compte",
|
||||||
|
"description": "Gérez votre abonnement, changez le mot de passe et les options de suppression de compte."
|
||||||
|
},
|
||||||
|
"subaccounts": {
|
||||||
|
"title": "Comptes Gérés",
|
||||||
|
"description": "Créez et gérez des sous-comptes pour vous connecter et accomplir les tâches assignées."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"title": "Notifications",
|
||||||
|
"description": "Configurez les notifications push, les alertes par e-mail et les cibles de notification pour les tâches."
|
||||||
|
},
|
||||||
|
"mfa": {
|
||||||
|
"title": "Authentification Multi-Facteurs",
|
||||||
|
"description": "Ajoutez une couche de sécurité supplémentaire avec MFA en utilisant des applications d'authentification."
|
||||||
|
},
|
||||||
|
"apitokens": {
|
||||||
|
"title": "Jetons API",
|
||||||
|
"description": "Générez et gérez des jetons d'accès pour les intégrations tierces et l'accès à l'API."
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Paramètres de Stockage",
|
||||||
|
"description": "Sauvegardez et restaurez vos données, gérez le stockage local et les préférences de synchronisation."
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Personnalisation du Panneau Latéral",
|
||||||
|
"description": "Personnalisez la disposition et la visibilité des cartes dans l'interface du panneau latéral."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Préférences de Thème",
|
||||||
|
"description": "Choisissez votre thème préféré et configurez les paramètres de mode sombre/clair."
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Localisation",
|
||||||
|
"description": "Personnalisez la langue, le format de date, le format de l'heure et les préférences régionales."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Paramètres Avancés",
|
||||||
|
"description": "Configurez les webhooks, les mises à jour en temps réel et d'autres fonctionnalités avancées pour améliorer la productivité."
|
||||||
|
},
|
||||||
|
"developer": {
|
||||||
|
"title": "Paramètres Développeur",
|
||||||
|
"description": "Consultez les informations techniques sur les jetons d'authentification, les connexions SSE et les données de débogage."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
public/locales/nl/chores.json
Normal file
75
public/locales/nl/chores.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"title": "Taken",
|
||||||
|
"myChores": "Mijn Taken",
|
||||||
|
"allChores": "Alle Taken",
|
||||||
|
"addChore": "Taak Toevoegen",
|
||||||
|
"editChore": "Taak Bewerken",
|
||||||
|
"deleteChore": "Taak Verwijderen",
|
||||||
|
"completeChore": "Taak Voltooien",
|
||||||
|
"dueDate": "Vervaldatum",
|
||||||
|
"assignedTo": "Toegewezen aan",
|
||||||
|
"priority": "Prioriteit",
|
||||||
|
"status": "Status",
|
||||||
|
"description": "Beschrijving",
|
||||||
|
"choreView": {
|
||||||
|
"assignment": "Toewijzing",
|
||||||
|
"assigned": "Toegewezen",
|
||||||
|
"last": "Laatste",
|
||||||
|
"schedule": "Schema",
|
||||||
|
"due": "Vervaldatum",
|
||||||
|
"statistics": "Statistieken",
|
||||||
|
"completed": "Voltooid",
|
||||||
|
"times": "keer",
|
||||||
|
"details": "Details",
|
||||||
|
"createdBy": "Aangemaakt door",
|
||||||
|
"na": "N/B",
|
||||||
|
"taskCompleted": "Taak Voltooid",
|
||||||
|
"taskCompletedMessage": "Uw taak is gemarkeerd als voltooid",
|
||||||
|
"taskCompletionUndone": "De voltooiing van de taak is ongedaan gemaakt.",
|
||||||
|
"taskSkipUndone": "Het overslaan van de taak is ongedaan gemaakt.",
|
||||||
|
"undoSuccessful": "Ongedaan Maken Geslaagd",
|
||||||
|
"undoFailed": "Ongedaan Maken Mislukt",
|
||||||
|
"undoFailedMessage": "Kan de actie niet ongedaan maken. Probeer het opnieuw.",
|
||||||
|
"resetTimer": "Timer Resetten",
|
||||||
|
"resetTimerConfirmation": "Weet u zeker dat u de timer wilt resetten? Dit wist alle tijdregistraties sinds u de taak bent begonnen.",
|
||||||
|
"clearAllTimeRecords": "Alle Tijdregistraties Wissen",
|
||||||
|
"clearAllTimeConfirmation": "Dit verwijdert permanent alle timers voor deze taak en zet hem terug naar \"niet gestart\".",
|
||||||
|
"descriptionTitle": "Beschrijving",
|
||||||
|
"description": "Beschrijving :",
|
||||||
|
"previousNote": "Vorige Notitie",
|
||||||
|
"previousNoteLabel": "Vorige notitie:",
|
||||||
|
"subtasksLabel": "Subtaken :",
|
||||||
|
"taskActions": "Taakacties",
|
||||||
|
"addNote": "Een notitie toevoegen",
|
||||||
|
"additionalNotes": "Aanvullende Notities:",
|
||||||
|
"notePlaceholder": "Voeg een notitie toe over de voltooiing...",
|
||||||
|
"setCustomCompletionTime": "Aangepaste voltooitijd instellen",
|
||||||
|
"skipTask": "Taak Overslaan",
|
||||||
|
"skipTaskConfirmation": "Weet u zeker dat u deze taak wilt overslaan?",
|
||||||
|
"markComplete": "Markeren als Voltooid",
|
||||||
|
"markAsDone": "Markeren als gedaan",
|
||||||
|
"edit": "Bewerken",
|
||||||
|
"archive": "Archiveren",
|
||||||
|
"unarchive": "Dearchiveren",
|
||||||
|
"viewHistory": "Geschiedenis Bekijken",
|
||||||
|
"history": "Geschiedenis",
|
||||||
|
"startTimer": "Timer Starten",
|
||||||
|
"start": "Starten",
|
||||||
|
"pauseTimer": "Timer Pauzeren",
|
||||||
|
"approve": "Goedkeuren",
|
||||||
|
"reject": "Afwijzen",
|
||||||
|
"pendingApproval": "In Afwachting van Goedkeuring",
|
||||||
|
"undo": "Ongedaan Maken",
|
||||||
|
"skip": "Overslaan",
|
||||||
|
"cancel": "Annuleren",
|
||||||
|
"noPriority": "Geen Prioriteit",
|
||||||
|
"subtasks": "Subtaken",
|
||||||
|
"noDescription": "Geen beschrijving beschikbaar",
|
||||||
|
"timer": {
|
||||||
|
"active": "Timer Actief",
|
||||||
|
"paused": "Timer Gepauzeerd",
|
||||||
|
"reset": "Timer Resetten",
|
||||||
|
"delete": "Sessie Verwijderen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
public/locales/nl/common.json
Normal file
33
public/locales/nl/common.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"save": "Opslaan",
|
||||||
|
"cancel": "Annuleren",
|
||||||
|
"delete": "Verwijderen",
|
||||||
|
"edit": "Bewerken",
|
||||||
|
"close": "Sluiten",
|
||||||
|
"confirm": "Bevestigen",
|
||||||
|
"loading": "Laden...",
|
||||||
|
"error": "Fout",
|
||||||
|
"success": "Succes",
|
||||||
|
"warning": "Waarschuwing",
|
||||||
|
"refresh": "Vernieuwen",
|
||||||
|
"copy": "Kopiëren",
|
||||||
|
"copied": "Gekopieerd!",
|
||||||
|
"settings": "Instellingen",
|
||||||
|
"yes": "Ja",
|
||||||
|
"no": "Nee",
|
||||||
|
"back": "Terug",
|
||||||
|
"backToCalendar": "Terug naar Kalender",
|
||||||
|
"logout": "Uitloggen",
|
||||||
|
"version": "Versie",
|
||||||
|
"navigation": {
|
||||||
|
"allTasks": "Alle Taken",
|
||||||
|
"archived": "Gearchiveerd",
|
||||||
|
"things": "Dingen",
|
||||||
|
"labels": "Labels",
|
||||||
|
"projects": "Projecten",
|
||||||
|
"filters": "Filters",
|
||||||
|
"activities": "Activiteiten",
|
||||||
|
"points": "Punten",
|
||||||
|
"settings": "Instellingen"
|
||||||
|
}
|
||||||
|
}
|
||||||
174
public/locales/nl/settings.json
Normal file
174
public/locales/nl/settings.json
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"title": "Instellingen",
|
||||||
|
"circleSettings": {
|
||||||
|
"title": "Cirkelinstellingen",
|
||||||
|
"description": "Uw account wordt automatisch verbonden met een Cirkel wanneer u er een aanmaakt of lid wordt. Nodig eenvoudig vrienden uit door de unieke Cirkelcode of onderstaande link te delen.",
|
||||||
|
"circleCode": "Cirkelcode",
|
||||||
|
"copyCode": "Code Kopiëren",
|
||||||
|
"copyLink": "Link Kopiëren",
|
||||||
|
"codeCopied": "Cirkelcode gekopieerd!",
|
||||||
|
"linkCopied": "Link gekopieerd!",
|
||||||
|
"joinCircle": "Lid Worden van een Cirkel",
|
||||||
|
"joinCirclePlaceholder": "Voer Cirkelcode in",
|
||||||
|
"join": "Lid Worden",
|
||||||
|
"leave": "Cirkel Verlaten",
|
||||||
|
"leaveConfirmTitle": "Cirkel Verlaten",
|
||||||
|
"leaveConfirmMessage": "Weet u zeker dat u deze cirkel wilt verlaten?",
|
||||||
|
"circleMembers": "Cirkelleden",
|
||||||
|
"circleMemberRequests": "Aanvragen voor Cirkellidmaatschap",
|
||||||
|
"admin": "Beheerder",
|
||||||
|
"member": "Lid",
|
||||||
|
"pending": "In behandeling",
|
||||||
|
"accept": "Accepteren",
|
||||||
|
"reject": "Afwijzen",
|
||||||
|
"makeAdmin": "Beheerder Maken",
|
||||||
|
"makeMember": "Lid Maken",
|
||||||
|
"remove": "Verwijderen",
|
||||||
|
"webhookURL": "Webhook URL",
|
||||||
|
"webhookDescription": "Voer een webhook URL in om meldingen te ontvangen voor cirkelgebeurtenissen",
|
||||||
|
"webhookPlaceholder": "https://uw-webhook-url.com"
|
||||||
|
},
|
||||||
|
"accountSettings": {
|
||||||
|
"title": "Accountinstellingen",
|
||||||
|
"subscription": "Abonnement",
|
||||||
|
"subscriptionStatus": "Huidig Plan",
|
||||||
|
"free": "Gratis",
|
||||||
|
"plus": "Plus",
|
||||||
|
"upgrade": "Upgraden",
|
||||||
|
"cancel": "Annuleren",
|
||||||
|
"changePassword": "Wachtwoord Wijzigen",
|
||||||
|
"password": "Wachtwoord",
|
||||||
|
"dangerZone": "Gevarenzone",
|
||||||
|
"dangerZoneDescription": "Zodra u uw account verwijdert, is er geen weg terug. Wees zeker van uw beslissing.",
|
||||||
|
"deleteAccount": "Account Verwijderen"
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Lokalisatie",
|
||||||
|
"description": "Pas de taal, datumnotatie en regionale voorkeuren voor uw account aan.",
|
||||||
|
"language": "Taal",
|
||||||
|
"languageDescription": "Selecteer uw voorkeurstaal",
|
||||||
|
"dateFormat": "Datumnotatie",
|
||||||
|
"dateFormatDescription": "Kies hoe datums in de applicatie worden weergegeven",
|
||||||
|
"timeFormat": "Tijdnotatie",
|
||||||
|
"timeFormatDescription": "Selecteer 12-uurs of 24-uurs tijdnotatie",
|
||||||
|
"12hour": "12-uurs (AM/PM)",
|
||||||
|
"24hour": "24-uurs",
|
||||||
|
"firstDayOfWeek": "Eerste Dag van de Week",
|
||||||
|
"firstDayOfWeekDescription": "Selecteer welke dag uw week begint",
|
||||||
|
"sunday": "Zondag",
|
||||||
|
"monday": "Maandag",
|
||||||
|
"saturday": "Zaterdag",
|
||||||
|
"formats": {
|
||||||
|
"mdy": "MM/DD/JJJJ (VS)",
|
||||||
|
"dmy": "DD/MM/JJJJ (Europa)",
|
||||||
|
"ymd": "JJJJ-MM-DD (ISO)",
|
||||||
|
"long": "Lang formaat (bijv. 1 januari 2024)",
|
||||||
|
"short": "Kort formaat (bijv. 1 jan 2024)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Zijpaneel Aanpassen",
|
||||||
|
"description": "Pas de indeling en zichtbaarheid van kaarten in het zijpaneel aan. Deze sectie is alleen beschikbaar op grote schermapparaten zoals tablets en desktops."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Themavoorkeuren",
|
||||||
|
"description": "Kies hoe de site er voor u uitziet. Selecteer een enkel thema of synchroniseer met uw systeem en schakel automatisch tussen dag- en nachtthema's.",
|
||||||
|
"themeMode": "Thamamodus",
|
||||||
|
"light": "Licht",
|
||||||
|
"dark": "Donker",
|
||||||
|
"system": "Systeem"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"settingsSaved": "Instellingen succesvol opgeslagen",
|
||||||
|
"settingsSaveFailed": "Opslaan van instellingen mislukt",
|
||||||
|
"invalidWebhook": "Ongeldige webhook URL"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Profielinstellingen",
|
||||||
|
"description": "Werk uw weergavenaam en profielfoto bij.",
|
||||||
|
"photoUpdated": "Foto Bijgewerkt",
|
||||||
|
"photoUpdatedMessage": "Uw profielfoto is succesvol bijgewerkt!",
|
||||||
|
"uploadFailed": "Uploaden Mislukt",
|
||||||
|
"uploadFailedMessage": "Het uploaden van uw foto is mislukt. Probeer het opnieuw.",
|
||||||
|
"profileUpdated": "Profiel Bijgewerkt",
|
||||||
|
"profileUpdatedMessage": "Uw profielgegevens zijn succesvol opgeslagen!",
|
||||||
|
"updateFailed": "Bijwerken Mislukt",
|
||||||
|
"updateFailedMessage": "Kan uw profiel niet bijwerken. Controleer uw verbinding en probeer het opnieuw.",
|
||||||
|
"changePhoto": "Foto Wijzigen",
|
||||||
|
"displayName": "Weergavenaam",
|
||||||
|
"displayNamePlaceholder": "Voer uw weergavenaam in",
|
||||||
|
"timezone": "Tijdzone",
|
||||||
|
"timezonePlaceholder": "Selecteer uw tijdzone",
|
||||||
|
"save": "Opslaan",
|
||||||
|
"cancel": "Annuleren"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "Instellingen",
|
||||||
|
"subtitle": "Pas uw ervaring aan en beheer uw accountvoorkeuren",
|
||||||
|
"upgrade": {
|
||||||
|
"title": "Upgraden naar Plus",
|
||||||
|
"description": "Ontgrendel krachtige functies om uw productiviteit te verbeteren",
|
||||||
|
"button": "Nu Upgraden",
|
||||||
|
"features": {
|
||||||
|
"richText": "Rijke tekstbeschrijvingen",
|
||||||
|
"notifications": "Taakmeldingen",
|
||||||
|
"apiIntegrations": "API-integraties",
|
||||||
|
"advancedAutomation": "Geavanceerde automatisering"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"profile": {
|
||||||
|
"title": "Profielinstellingen",
|
||||||
|
"description": "Werk uw profielgegevens, foto, weergavenaam en tijdzonevoorkeuren bij."
|
||||||
|
},
|
||||||
|
"circle": {
|
||||||
|
"title": "Cirkelinstellingen",
|
||||||
|
"description": "Beheer uw cirkel, nodig leden uit en behandel lidmaatschapsverzoeken."
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"title": "Accountinstellingen",
|
||||||
|
"description": "Beheer uw abonnement, wijzig het wachtwoord en opties voor accountverwijdering."
|
||||||
|
},
|
||||||
|
"subaccounts": {
|
||||||
|
"title": "Beheerde Accounts",
|
||||||
|
"description": "Maak subaccounts aan en beheer ze om in te loggen en toegewezen taken te voltooien."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"title": "Meldingen",
|
||||||
|
"description": "Configureer pushmeldingen, e-mailwaarschuwingen en meldingsdoelen voor taken."
|
||||||
|
},
|
||||||
|
"mfa": {
|
||||||
|
"title": "Multi-Factor Authenticatie",
|
||||||
|
"description": "Voeg een extra beveiligingslaag toe met MFA via authenticatie-apps."
|
||||||
|
},
|
||||||
|
"apitokens": {
|
||||||
|
"title": "API-tokens",
|
||||||
|
"description": "Genereer en beheer toegangstokens voor integraties van derden en API-toegang."
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Opslaginstellingen",
|
||||||
|
"description": "Maak een back-up van uw gegevens en herstel ze, beheer lokale opslag en synchronisatievoorkeuren."
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"title": "Zijpaneel Aanpassen",
|
||||||
|
"description": "Pas de indeling en zichtbaarheid van kaarten in de zijpaneelinterface aan."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Themavoorkeuren",
|
||||||
|
"description": "Kies uw voorkeursthema en configureer de instellingen voor donker/licht modus."
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"title": "Lokalisatie",
|
||||||
|
"description": "Pas de taal, datumnotatie, tijdnotatie en regionale voorkeuren aan."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Geavanceerde Instellingen",
|
||||||
|
"description": "Configureer webhooks, real-time updates en andere geavanceerde functies voor verbeterde productiviteit."
|
||||||
|
},
|
||||||
|
"developer": {
|
||||||
|
"title": "Ontwikkelaarsinstellingen",
|
||||||
|
"description": "Bekijk technische informatie over authenticatietokens, SSE-verbindingen en foutopsporingsgegevens."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AlertsProvider } from '../service/AlertsProvider'
|
import { AlertsProvider } from '../service/AlertsProvider'
|
||||||
import { NotificationProvider } from '../service/NotificationProvider'
|
import { NotificationProvider } from '../service/NotificationProvider'
|
||||||
|
import { LocalizationProvider } from './LocalizationContext'
|
||||||
import QueryContext from './QueryContext'
|
import QueryContext from './QueryContext'
|
||||||
import RouterContext from './RouterContext'
|
import RouterContext from './RouterContext'
|
||||||
import ThemeContext from './ThemeContext'
|
import ThemeContext from './ThemeContext'
|
||||||
@@ -8,6 +9,7 @@ const Contexts = ({ children }) => {
|
|||||||
const contexts = [
|
const contexts = [
|
||||||
AlertsProvider,
|
AlertsProvider,
|
||||||
ThemeContext,
|
ThemeContext,
|
||||||
|
LocalizationProvider,
|
||||||
QueryContext,
|
QueryContext,
|
||||||
NotificationProvider,
|
NotificationProvider,
|
||||||
RouterContext,
|
RouterContext,
|
||||||
|
|||||||
127
src/contexts/LocalizationContext.jsx
Normal file
127
src/contexts/LocalizationContext.jsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import useStickyState from '@/hooks/useStickyState'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { createContext, useContext, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const LocalizationContext = createContext()
|
||||||
|
|
||||||
|
export const DATE_FORMATS = {
|
||||||
|
MDY: 'MM/DD/YYYY',
|
||||||
|
DMY: 'DD/MM/YYYY',
|
||||||
|
YMD: 'YYYY-MM-DD',
|
||||||
|
LONG: 'MMMM D, YYYY',
|
||||||
|
SHORT: 'MMM D, YYYY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TIME_FORMATS = {
|
||||||
|
HOUR_12: 'h:mm A',
|
||||||
|
HOUR_24: 'HH:mm',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']
|
||||||
|
|
||||||
|
export const AVAILABLE_LANGUAGES = [
|
||||||
|
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||||
|
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
||||||
|
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
||||||
|
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const LocalizationProvider = ({ children }) => {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const [dateFormat, setDateFormat] = useStickyState(
|
||||||
|
DATE_FORMATS.MDY,
|
||||||
|
'dateFormat',
|
||||||
|
)
|
||||||
|
const [timeFormat, setTimeFormat] = useStickyState(
|
||||||
|
TIME_FORMATS.HOUR_12,
|
||||||
|
'timeFormat',
|
||||||
|
)
|
||||||
|
const [firstDayOfWeek, setFirstDayOfWeek] = useStickyState(
|
||||||
|
0,
|
||||||
|
'firstDayOfWeek',
|
||||||
|
) // 0 = Sunday, 1 = Monday
|
||||||
|
const [language, setLanguage] = useStickyState('en', 'language')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
i18n.changeLanguage(language)
|
||||||
|
moment.locale(language)
|
||||||
|
}, [language, i18n])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRTL = RTL_LANGUAGES.includes(language)
|
||||||
|
document.documentElement.dir = isRTL ? 'rtl' : 'ltr'
|
||||||
|
document.documentElement.lang = language
|
||||||
|
}, [language])
|
||||||
|
|
||||||
|
const formatDate = (date, format = dateFormat) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateTime = (date, format) => {
|
||||||
|
if (!date) return ''
|
||||||
|
const dateTimeFormat = format || `${dateFormat} ${timeFormat}`
|
||||||
|
return moment(date).format(dateTimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (date, format = timeFormat) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatRelative = date => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).fromNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatCalendar = date => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).calendar(null, {
|
||||||
|
sameDay: `[Today] ${timeFormat}`,
|
||||||
|
nextDay: `[Tomorrow] ${timeFormat}`,
|
||||||
|
nextWeek: `dddd ${timeFormat}`,
|
||||||
|
lastDay: `[Yesterday] ${timeFormat}`,
|
||||||
|
lastWeek: `[Last] dddd ${timeFormat}`,
|
||||||
|
sameElse: `${dateFormat} ${timeFormat}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRTL = RTL_LANGUAGES.includes(language)
|
||||||
|
|
||||||
|
const fmt = {
|
||||||
|
date: formatDate,
|
||||||
|
dateTime: formatDateTime,
|
||||||
|
time: formatTime,
|
||||||
|
relative: formatRelative,
|
||||||
|
calendar: formatCalendar,
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
dateFormat,
|
||||||
|
setDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
setTimeFormat,
|
||||||
|
firstDayOfWeek,
|
||||||
|
setFirstDayOfWeek,
|
||||||
|
language,
|
||||||
|
setLanguage,
|
||||||
|
isRTL,
|
||||||
|
fmt,
|
||||||
|
availableLanguages: AVAILABLE_LANGUAGES,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LocalizationContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</LocalizationContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLocalization = () => {
|
||||||
|
const context = useContext(LocalizationContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useLocalization must be used within LocalizationProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import PaymentSuccessView from '../views/Payments/PaymentSuccessView'
|
|||||||
import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView'
|
import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView'
|
||||||
import ProjectView from '../views/Projects/ProjectView'
|
import ProjectView from '../views/Projects/ProjectView'
|
||||||
import APITokenSettings from '../views/Settings/APITokenSettings'
|
import APITokenSettings from '../views/Settings/APITokenSettings'
|
||||||
|
import LocalizationSettings from '../views/Settings/LocalizationSettings'
|
||||||
import MFASettings from '../views/Settings/MFASettings'
|
import MFASettings from '../views/Settings/MFASettings'
|
||||||
import NotificationSetting from '../views/Settings/NotificationSetting'
|
import NotificationSetting from '../views/Settings/NotificationSetting'
|
||||||
import ProfileSettings from '../views/Settings/ProfileSettings'
|
import ProfileSettings from '../views/Settings/ProfileSettings'
|
||||||
@@ -115,6 +116,10 @@ const Router = createBrowserRouter([
|
|||||||
path: 'theme',
|
path: 'theme',
|
||||||
element: <ThemeSettings />,
|
element: <ThemeSettings />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'localization',
|
||||||
|
element: <LocalizationSettings />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'advanced',
|
path: 'advanced',
|
||||||
element: <AdvancedSettings />,
|
element: <AdvancedSettings />,
|
||||||
|
|||||||
117
src/i18n/README.md
Normal file
117
src/i18n/README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Internationalization (i18n) Setup
|
||||||
|
|
||||||
|
This directory contains the internationalization configuration for Donetick.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/i18n/
|
||||||
|
├── config.js # i18next configuration
|
||||||
|
└── README.md # This file
|
||||||
|
|
||||||
|
public/locales/
|
||||||
|
├── en/ # English (default)
|
||||||
|
│ ├── common.json
|
||||||
|
│ ├── settings.json
|
||||||
|
│ └── chores.json
|
||||||
|
├── es/ # Spanish
|
||||||
|
├── ar/ # Arabic (RTL)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Components
|
||||||
|
|
||||||
|
### Using translations
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { t } = useTranslation('settings') // or 'common', 'chores'
|
||||||
|
|
||||||
|
return <h1>{t('title')}</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using date formatting
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useLocalization } from '@/contexts/LocalizationContext'
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { formatDate, formatDateTime, formatRelative } = useLocalization()
|
||||||
|
|
||||||
|
const date = new Date()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Date: {formatDate(date)}</p>
|
||||||
|
<p>DateTime: {formatDateTime(date)}</p>
|
||||||
|
<p>Relative: {formatRelative(date)}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using language/format settings
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useLocalization } from '@/contexts/LocalizationContext'
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const {
|
||||||
|
language,
|
||||||
|
setLanguage,
|
||||||
|
dateFormat,
|
||||||
|
setDateFormat,
|
||||||
|
isRTL
|
||||||
|
} = useLocalization()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir={isRTL ? 'rtl' : 'ltr'}>
|
||||||
|
Current language: {language}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Namespaces
|
||||||
|
|
||||||
|
- **common**: General UI elements (buttons, messages, etc.)
|
||||||
|
- **settings**: Settings page translations
|
||||||
|
- **chores**: Chores-related translations
|
||||||
|
|
||||||
|
## Adding New Translations
|
||||||
|
|
||||||
|
1. Add the text to the appropriate JSON file in `public/locales/en/`
|
||||||
|
2. Use the translation in your component with `t('key')`
|
||||||
|
3. Upload to translation platform for community translation
|
||||||
|
|
||||||
|
## RTL Support
|
||||||
|
|
||||||
|
Languages in the `RTL_LANGUAGES` array automatically get:
|
||||||
|
- `dir="rtl"` on the document
|
||||||
|
- RTL-specific CSS styles
|
||||||
|
- Proper text alignment
|
||||||
|
|
||||||
|
Currently supported RTL languages: Arabic (ar), Hebrew (he), Persian (fa), Urdu (ur)
|
||||||
|
|
||||||
|
## Date Format Preferences
|
||||||
|
|
||||||
|
Users can choose from:
|
||||||
|
- MM/DD/YYYY (US)
|
||||||
|
- DD/MM/YYYY (Europe)
|
||||||
|
- YYYY-MM-DD (ISO)
|
||||||
|
- Long format (January 1, 2024)
|
||||||
|
- Short format (Jan 1, 2024)
|
||||||
|
|
||||||
|
## Time Format Preferences
|
||||||
|
|
||||||
|
- 12-hour (with AM/PM)
|
||||||
|
- 24-hour
|
||||||
|
|
||||||
|
## First Day of Week
|
||||||
|
|
||||||
|
Users can choose:
|
||||||
|
- Sunday
|
||||||
|
- Monday
|
||||||
36
src/i18n/config.js
Normal file
36
src/i18n/config.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import i18n from 'i18next'
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
|
import HttpBackend from 'i18next-http-backend'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(HttpBackend)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: 'en',
|
||||||
|
debug: import.meta.env.DEV,
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
backend: {
|
||||||
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
|
},
|
||||||
|
|
||||||
|
ns: ['common', 'settings', 'chores'],
|
||||||
|
defaultNS: 'common',
|
||||||
|
|
||||||
|
detection: {
|
||||||
|
order: ['localStorage', 'navigator'],
|
||||||
|
caches: ['localStorage'],
|
||||||
|
lookupLocalStorage: 'i18nextLng',
|
||||||
|
},
|
||||||
|
|
||||||
|
react: {
|
||||||
|
useSuspense: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
@@ -54,3 +54,70 @@ html {
|
|||||||
.animate-optimized.animation-complete {
|
.animate-optimized.animation-complete {
|
||||||
will-change: auto;
|
will-change: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RTL Support */
|
||||||
|
[dir='rtl'] {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] .rtl-mirror {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle margins and paddings for RTL */
|
||||||
|
[dir='rtl'] .ml-auto {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] .mr-auto {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flip icons and arrows in RTL */
|
||||||
|
[dir='rtl'] .rtl-flip {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper text alignment in RTL */
|
||||||
|
[dir='rtl'] input,
|
||||||
|
[dir='rtl'] textarea {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle border radius for RTL */
|
||||||
|
[dir='rtl'] .rounded-l-none {
|
||||||
|
border-radius: 0 0.375rem 0.375rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] .rounded-r-none {
|
||||||
|
border-radius: 0.375rem 0 0 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix flex alignment for RTL */
|
||||||
|
[dir='rtl'] .flex {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure cards and containers align properly in RTL */
|
||||||
|
[dir='rtl'] .MuiCard-root,
|
||||||
|
[dir='rtl'] .MuiBox-root,
|
||||||
|
[dir='rtl'] .MuiStack-root {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix list item alignment in RTL */
|
||||||
|
[dir='rtl'] .MuiListItem-root,
|
||||||
|
[dir='rtl'] .MuiListItemButton-root {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix gap alignment in RTL */
|
||||||
|
[dir='rtl'] .gap-1,
|
||||||
|
[dir='rtl'] .gap-2,
|
||||||
|
[dir='rtl'] .gap-3,
|
||||||
|
[dir='rtl'] .gap-4 {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import Contexts from './contexts/Contexts.jsx'
|
import Contexts from './contexts/Contexts.jsx'
|
||||||
|
import './i18n/config'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
|||||||
@@ -19,12 +19,22 @@ const allMonths = [
|
|||||||
* @param {Object} chore - The chore object (needed for nextDueDate null check)
|
* @param {Object} chore - The chore object (needed for nextDueDate null check)
|
||||||
* @returns {string} The formatted due date text
|
* @returns {string} The formatted due date text
|
||||||
*/
|
*/
|
||||||
export const getDueDateChipText = (nextDueDate, chore) => {
|
export const getDueDateChipText = (nextDueDate, chore, timeFormat = 'h:mm A') => {
|
||||||
if (chore?.nextDueDate === null || nextDueDate === null) return 'No Due Date'
|
if (chore?.nextDueDate === null || nextDueDate === null) return 'No Due Date'
|
||||||
|
|
||||||
const dueDate = moment(nextDueDate)
|
const dueDate = moment(nextDueDate)
|
||||||
const diff = moment(nextDueDate).diff(moment(), 'hours')
|
const diff = moment(nextDueDate).diff(moment(), 'hours')
|
||||||
|
|
||||||
|
const calendarFormat = {
|
||||||
|
sameDay: `[Today] ${timeFormat}`,
|
||||||
|
nextDay: `[Tomorrow] ${timeFormat}`,
|
||||||
|
nextWeek: `dddd ${timeFormat}`,
|
||||||
|
lastDay: `[Yesterday] ${timeFormat}`,
|
||||||
|
lastWeek: `[Last] dddd ${timeFormat}`,
|
||||||
|
sameElse: `MMM D ${timeFormat}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// if time is 23:59:59, treat as end-of-day (date only, no specific time)
|
// if time is 23:59:59, treat as end-of-day (date only, no specific time)
|
||||||
if (dueDate.hours() === 23 && dueDate.minutes() === 59 && dueDate.seconds() === 59) {
|
if (dueDate.hours() === 23 && dueDate.minutes() === 59 && dueDate.seconds() === 59) {
|
||||||
if (diff < 0) {
|
if (diff < 0) {
|
||||||
@@ -33,22 +43,22 @@ export const getDueDateChipText = (nextDueDate, chore) => {
|
|||||||
if (absDiff <= 48) {
|
if (absDiff <= 48) {
|
||||||
return (
|
return (
|
||||||
'Overdue ' +
|
'Overdue ' +
|
||||||
moment(nextDueDate).calendar().split(' at ')[0].toLowerCase()
|
moment(nextDueDate).calendar(null, calendarFormat).split(' ')[0].toLowerCase()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return 'Overdue ' + dueDate.fromNow()
|
return 'Overdue ' + dueDate.fromNow()
|
||||||
}
|
}
|
||||||
// if due in next 48 hours, show calendar format without time (e.g., "Tomorrow")
|
// if due in next 48 hours, show calendar format without time (e.g., "Tomorrow")
|
||||||
if (diff < 48 && diff > 0) {
|
if (diff < 48 && diff > 0) {
|
||||||
return moment(nextDueDate).calendar().split(' at ')[0]
|
return moment(nextDueDate).calendar(null, calendarFormat).split(' ')[0]
|
||||||
}
|
}
|
||||||
// if due date is after 48 hours, show it in format: Due in 3 days
|
// if due date is after 48 hours, show it in format: Due in 3 days
|
||||||
return 'Due ' + dueDate.fromNow()
|
return 'Due ' + dueDate.fromNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// if due in next 48 hours, we should show it in this format: Tomorrow 11:00 AM
|
// if due in next 48 hours, we should show it in this format: Tomorrow 11:00
|
||||||
if (diff < 48 && diff > 0) {
|
if (diff < 48 && diff > 0) {
|
||||||
return moment(nextDueDate).calendar().replace(' at', '')
|
return moment(nextDueDate).calendar(null, calendarFormat)
|
||||||
}
|
}
|
||||||
return 'Due ' + moment(nextDueDate).fromNow()
|
return 'Due ' + moment(nextDueDate).fromNow()
|
||||||
}
|
}
|
||||||
|
|||||||
83
src/utils/DateFormatter.js
Normal file
83
src/utils/DateFormatter.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export const createDateFormatter = (
|
||||||
|
dateFormat,
|
||||||
|
timeFormat,
|
||||||
|
firstDayOfWeek,
|
||||||
|
) => {
|
||||||
|
moment.updateLocale('en', {
|
||||||
|
week: {
|
||||||
|
dow: firstDayOfWeek,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatDate: (date, customFormat) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).format(customFormat || dateFormat)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDateTime: (date, customFormat) => {
|
||||||
|
if (!date) return ''
|
||||||
|
const format = customFormat || `${dateFormat} ${timeFormat}`
|
||||||
|
return moment(date).format(format)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime: (date, customFormat) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).format(customFormat || timeFormat)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatRelative: date => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).fromNow()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatCalendar: (date, opts) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).calendar(null, opts)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatShortDate: date => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).format('MMM D, YYYY')
|
||||||
|
},
|
||||||
|
|
||||||
|
formatLongDate: date => {
|
||||||
|
if (!date) return ''
|
||||||
|
return moment(date).format('MMMM D, YYYY')
|
||||||
|
},
|
||||||
|
|
||||||
|
isBefore: (date, compareDate) => {
|
||||||
|
return moment(date).isBefore(compareDate)
|
||||||
|
},
|
||||||
|
|
||||||
|
isAfter: (date, compareDate) => {
|
||||||
|
return moment(date).isAfter(compareDate)
|
||||||
|
},
|
||||||
|
|
||||||
|
diff: (date1, date2, unit) => {
|
||||||
|
return moment(date1).diff(moment(date2), unit)
|
||||||
|
},
|
||||||
|
|
||||||
|
add: (date, amount, unit) => {
|
||||||
|
return moment(date).add(amount, unit).toDate()
|
||||||
|
},
|
||||||
|
|
||||||
|
subtract: (date, amount, unit) => {
|
||||||
|
return moment(date).subtract(amount, unit).toDate()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDateFormatter = () => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return createDateFormatter('MM/DD/YYYY', 'h:mm A', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateFormat = localStorage.getItem('dateFormat') || 'MM/DD/YYYY'
|
||||||
|
const timeFormat = localStorage.getItem('timeFormat') || 'h:mm A'
|
||||||
|
const firstDayOfWeek = parseInt(localStorage.getItem('firstDayOfWeek') || '0')
|
||||||
|
|
||||||
|
return createDateFormatter(dateFormat, timeFormat, firstDayOfWeek)
|
||||||
|
}
|
||||||
@@ -243,7 +243,7 @@ const SignupView = () => {
|
|||||||
name='displayName'
|
name='displayName'
|
||||||
label='Display Name'
|
label='Display Name'
|
||||||
id='displayName'
|
id='displayName'
|
||||||
placeholder='Confirm password'
|
placeholder='How others see your name'
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setDisplayNameError(null)
|
setDisplayNameError(null)
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ import { Divider } from '@mui/material'
|
|||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
|
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
|
||||||
import {
|
import {
|
||||||
useChoreTimer,
|
useChoreTimer,
|
||||||
@@ -75,6 +77,8 @@ import TimePassedCard from './TimePassedCard.jsx'
|
|||||||
import TimerSplitButton from './TimerSplitButton.jsx'
|
import TimerSplitButton from './TimerSplitButton.jsx'
|
||||||
|
|
||||||
const ChoreView = () => {
|
const ChoreView = () => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const [chore, setChore] = useState({})
|
const [chore, setChore] = useState({})
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [performers, setPerformers] = useState([])
|
const [performers, setPerformers] = useState([])
|
||||||
@@ -143,13 +147,13 @@ const ChoreView = () => {
|
|||||||
{
|
{
|
||||||
size: 6,
|
size: 6,
|
||||||
icon: <PeopleAlt />,
|
icon: <PeopleAlt />,
|
||||||
title: 'Assignment',
|
title: t('choreView.assignment'),
|
||||||
text: `Assigned: ${
|
text: `${t('choreView.assigned')}: ${
|
||||||
performers.find(p => p.userId === chore.assignedTo)?.displayName ||
|
performers.find(p => p.userId === chore.assignedTo)?.displayName ||
|
||||||
'N/A'
|
t('choreView.na')
|
||||||
}`,
|
}`,
|
||||||
subtext: ` Last: ${
|
subtext: ` ${t('choreView.last')}: ${
|
||||||
chore.lastCompletedDate && chore.lastCompletedBy
|
chore.lastCompletedDate
|
||||||
? performers.find(p => p.userId === chore.lastCompletedBy)
|
? performers.find(p => p.userId === chore.lastCompletedBy)
|
||||||
?.displayName
|
?.displayName
|
||||||
: 'N/A'
|
: 'N/A'
|
||||||
@@ -158,14 +162,14 @@ const ChoreView = () => {
|
|||||||
{
|
{
|
||||||
size: 6,
|
size: 6,
|
||||||
icon: <CalendarMonth />,
|
icon: <CalendarMonth />,
|
||||||
title: 'Schedule',
|
title: t('choreView.schedule'),
|
||||||
text: `Due: ${
|
text: `${t('choreView.due')}: ${
|
||||||
chore.nextDueDate ? moment(chore.nextDueDate).fromNow() : 'N/A'
|
chore.nextDueDate ? moment(chore.nextDueDate).fromNow() : t('choreView.na')
|
||||||
}`,
|
}`,
|
||||||
subtext: `Last: ${
|
subtext: `${t('choreView.last')}: ${
|
||||||
chore.lastCompletedDate
|
chore.lastCompletedDate
|
||||||
? moment(chore.lastCompletedDate).fromNow()
|
? moment(chore.lastCompletedDate).fromNow()
|
||||||
: 'N/A'
|
: t('choreView.na')
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
subtext2:
|
subtext2:
|
||||||
@@ -176,16 +180,16 @@ const ChoreView = () => {
|
|||||||
{
|
{
|
||||||
size: 6,
|
size: 6,
|
||||||
icon: <Checklist />,
|
icon: <Checklist />,
|
||||||
title: 'Statistics',
|
title: t('choreView.statistics'),
|
||||||
text: `Completed: ${chore.totalCompletedCount || 0} times`,
|
text: `${t('choreView.completed')}: ${chore.totalCompletedCount || 0} ${t('choreView.times')}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
size: 6,
|
size: 6,
|
||||||
icon: <Person />,
|
icon: <Person />,
|
||||||
title: 'Details',
|
title: t('choreView.details'),
|
||||||
subtext: `Created By: ${
|
subtext: `${t('choreView.createdBy')}: ${
|
||||||
performers.find(p => p.userId === chore.createdBy)?.displayName ||
|
performers.find(p => p.userId === chore.createdBy)?.displayName ||
|
||||||
'N/A'
|
t('choreView.na')
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -225,8 +229,8 @@ const ChoreView = () => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
// Show undo notification
|
// Show undo notification
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Task Completed',
|
title: t('choreView.taskCompleted'),
|
||||||
message: 'Your task has been marked as complete',
|
message: t('choreView.taskCompletedMessage'),
|
||||||
undoAction: async () => {
|
undoAction: async () => {
|
||||||
try {
|
try {
|
||||||
const undoResponse = await UndoChoreAction(choreId)
|
const undoResponse = await UndoChoreAction(choreId)
|
||||||
@@ -239,16 +243,16 @@ const ChoreView = () => {
|
|||||||
queryClient.invalidateQueries(['chores'])
|
queryClient.invalidateQueries(['chores'])
|
||||||
}
|
}
|
||||||
showUndo({
|
showUndo({
|
||||||
title: 'Undo Successful',
|
title: t('choreView.undoSuccessful'),
|
||||||
message: 'Task completion has been undone.',
|
message: t('choreView.taskCompletionUndone'),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to undo')
|
throw new Error('Failed to undo')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Undo Failed',
|
title: t('choreView.undoFailed'),
|
||||||
message: 'Unable to undo the action. Please try again.',
|
message: t('choreView.undoFailedMessage'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -266,7 +270,7 @@ const ChoreView = () => {
|
|||||||
|
|
||||||
// Show undo notification
|
// Show undo notification
|
||||||
showSuccess({
|
showSuccess({
|
||||||
message: 'Task skipped',
|
message: t('choreView.skipTask'),
|
||||||
undoAction: async () => {
|
undoAction: async () => {
|
||||||
try {
|
try {
|
||||||
const undoResponse = await UndoChoreAction(choreId)
|
const undoResponse = await UndoChoreAction(choreId)
|
||||||
@@ -279,16 +283,16 @@ const ChoreView = () => {
|
|||||||
queryClient.invalidateQueries(['chores'])
|
queryClient.invalidateQueries(['chores'])
|
||||||
}
|
}
|
||||||
showUndo({
|
showUndo({
|
||||||
title: 'Undo Successful',
|
title: t('choreView.undoSuccessful'),
|
||||||
message: 'Task skip has been undone.',
|
message: t('choreView.taskSkipUndone'),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to undo')
|
throw new Error('Failed to undo')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Undo Failed',
|
title: t('choreView.undoFailed'),
|
||||||
message: 'Unable to undo the action. Please try again.',
|
message: t('choreView.undoFailedMessage'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -324,11 +328,10 @@ const ChoreView = () => {
|
|||||||
const handleResetTimer = () => {
|
const handleResetTimer = () => {
|
||||||
setTimerActionConfig({
|
setTimerActionConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: 'Reset Timer',
|
title: t('choreView.resetTimer'),
|
||||||
message:
|
message: t('choreView.resetTimerConfirmation'),
|
||||||
'Are you sure you want to reset the timer? This will clear all time records since you started the task.',
|
confirmText: t('choreView.resetTimer'),
|
||||||
confirmText: 'Reset Timer',
|
cancelText: t('common:cancel'),
|
||||||
cancelText: 'Cancel',
|
|
||||||
onClose: confirmed => {
|
onClose: confirmed => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
resetChoreTimer.mutate(choreId, {
|
resetChoreTimer.mutate(choreId, {
|
||||||
@@ -349,11 +352,10 @@ const ChoreView = () => {
|
|||||||
const handleClearAllTime = () => {
|
const handleClearAllTime = () => {
|
||||||
setTimerActionConfig({
|
setTimerActionConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: 'Clear All Time Records',
|
title: t('choreView.clearAllTimeRecords'),
|
||||||
message:
|
message: t('choreView.clearAllTimeConfirmation'),
|
||||||
'This will permanently delete all timers for this task and set it back to "not started".',
|
confirmText: t('choreView.clearAllTimeRecords'),
|
||||||
confirmText: 'Clear All Time',
|
cancelText: t('common:cancel'),
|
||||||
cancelText: 'Cancel',
|
|
||||||
onClose: async confirmed => {
|
onClose: async confirmed => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
if (choreTimer?.res?.id) {
|
if (choreTimer?.res?.id) {
|
||||||
@@ -473,13 +475,13 @@ const ChoreView = () => {
|
|||||||
color='warning'
|
color='warning'
|
||||||
sx={{ mb: 1 }}
|
sx={{ mb: 1 }}
|
||||||
>
|
>
|
||||||
Archived
|
{t('choreView.archive')}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
<Chip startDecorator={<CalendarMonth />} size='md' sx={{ mb: 1 }}>
|
<Chip startDecorator={<CalendarMonth />} size='md' sx={{ mb: 1 }}>
|
||||||
{chore.nextDueDate
|
{chore.nextDueDate
|
||||||
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
|
? `${t('choreView.due')} ${fmt.dateTime(chore.nextDueDate)}`
|
||||||
: 'N/A'}
|
: t('choreView.na')}
|
||||||
</Chip>
|
</Chip>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -628,7 +630,7 @@ const ChoreView = () => {
|
|||||||
variant='plain'
|
variant='plain'
|
||||||
>
|
>
|
||||||
{chorePriority ? chorePriority.icon : <LowPriority />}
|
{chorePriority ? chorePriority.icon : <LowPriority />}
|
||||||
{chorePriority ? chorePriority.name : 'No Priority'}
|
{chorePriority ? chorePriority.name : t('choreView.noPriority')}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<Menu>
|
<Menu>
|
||||||
{Priorities.map((priority, index) => (
|
{Priorities.map((priority, index) => (
|
||||||
@@ -655,13 +657,13 @@ const ChoreView = () => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleUpdatePriority({
|
handleUpdatePriority({
|
||||||
name: 'No Priority',
|
name: t('choreView.noPriority'),
|
||||||
value: 0,
|
value: 0,
|
||||||
})
|
})
|
||||||
setChorePriority(null)
|
setChorePriority(null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
No Priority
|
{t('choreView.noPriority')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -682,7 +684,7 @@ const ChoreView = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<History />
|
<History />
|
||||||
History
|
{t('choreView.history')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -708,7 +710,7 @@ const ChoreView = () => {
|
|||||||
{chore.description && (
|
{chore.description && (
|
||||||
<>
|
<>
|
||||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||||
Description :
|
{t('choreView.description')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
@@ -722,7 +724,7 @@ const ChoreView = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNoteViewerConfig({
|
setNoteViewerConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: 'Description',
|
title: t('choreView.descriptionTitle'),
|
||||||
content: chore.description,
|
content: chore.description,
|
||||||
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||||
})
|
})
|
||||||
@@ -754,7 +756,7 @@ const ChoreView = () => {
|
|||||||
{chore.notes && (
|
{chore.notes && (
|
||||||
<>
|
<>
|
||||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||||
Previous note:
|
{t('choreView.previousNoteLabel')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Sheet
|
<Sheet
|
||||||
variant='plain'
|
variant='plain'
|
||||||
@@ -767,7 +769,7 @@ const ChoreView = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNoteViewerConfig({
|
setNoteViewerConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: 'Previous Note',
|
title: t('choreView.previousNote'),
|
||||||
content: chore.notes,
|
content: chore.notes,
|
||||||
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||||
})
|
})
|
||||||
@@ -798,7 +800,7 @@ const ChoreView = () => {
|
|||||||
{chore.subTasks && chore.subTasks.length > 0 && (
|
{chore.subTasks && chore.subTasks.length > 0 && (
|
||||||
<Box sx={{ p: 0, m: 0, mb: 2 }}>
|
<Box sx={{ p: 0, m: 0, mb: 2 }}>
|
||||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||||
Subtasks :
|
{t('choreView.subtasksLabel')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Sheet
|
<Sheet
|
||||||
variant='plain'
|
variant='plain'
|
||||||
@@ -836,7 +838,7 @@ const ChoreView = () => {
|
|||||||
variant='soft'
|
variant='soft'
|
||||||
>
|
>
|
||||||
<Typography level='body-md' sx={{ mb: 1 }}>
|
<Typography level='body-md' sx={{ mb: 1 }}>
|
||||||
Task Actions
|
{t('choreView.taskActions')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<FormControl size='sm'>
|
<FormControl size='sm'>
|
||||||
@@ -860,7 +862,7 @@ const ChoreView = () => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add a note
|
{t('choreView.addNote')}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -868,13 +870,13 @@ const ChoreView = () => {
|
|||||||
{note !== null && (
|
{note !== null && (
|
||||||
<Box sx={{ mb: 1 }}>
|
<Box sx={{ mb: 1 }}>
|
||||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||||
Additional Notes:
|
{t('choreView.additionalNotes')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
value={note || ''}
|
value={note || ''}
|
||||||
onChange={setNote}
|
onChange={setNote}
|
||||||
entityType={'chore_completion_note'}
|
entityType={'chore_completion_note'}
|
||||||
placeholder='Add a note about the completion...'
|
placeholder={t('choreView.notePlaceholder')}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -908,7 +910,7 @@ const ChoreView = () => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Set custom completion time
|
{t('choreView.setCustomCompletionTime')}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -942,7 +944,7 @@ const ChoreView = () => {
|
|||||||
color='primary'
|
color='primary'
|
||||||
startDecorator={<Unarchive />}
|
startDecorator={<Unarchive />}
|
||||||
>
|
>
|
||||||
Unarchive
|
{t('choreView.unarchive')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
@@ -980,7 +982,7 @@ const ChoreView = () => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Approve
|
{t('choreView.approve')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -992,7 +994,7 @@ const ChoreView = () => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>Reject</Box>
|
<Box>{t('choreView.reject')}</Box>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -1003,7 +1005,7 @@ const ChoreView = () => {
|
|||||||
color='neutral'
|
color='neutral'
|
||||||
startDecorator={<HourglassEmpty />}
|
startDecorator={<HourglassEmpty />}
|
||||||
>
|
>
|
||||||
<Box>Pending Approval</Box>
|
<Box>{t('choreView.pendingApproval')}</Box>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -1022,7 +1024,7 @@ const ChoreView = () => {
|
|||||||
flex: 4,
|
flex: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>Mark as done</Box>
|
<Box>{t('choreView.markAsDone')}</Box>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -1031,12 +1033,10 @@ const ChoreView = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setConfirmModelConfig({
|
setConfirmModelConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: 'Skip Task',
|
title: t('choreView.skipTask'),
|
||||||
|
message: t('choreView.skipTaskConfirmation'),
|
||||||
message: 'Are you sure you want to skip this task?',
|
confirmText: t('choreView.skip'),
|
||||||
|
cancelText: t('choreView.cancel'),
|
||||||
confirmText: 'Skip',
|
|
||||||
cancelText: 'Cancel',
|
|
||||||
onClose: confirmed => {
|
onClose: confirmed => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
handleSkippingTask()
|
handleSkippingTask()
|
||||||
@@ -1053,7 +1053,7 @@ const ChoreView = () => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>Skip</Box>
|
<Box>{t('choreView.skip')}</Box>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1108,7 +1108,7 @@ const ChoreView = () => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Start
|
{t('choreView.start')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
import ThingTriggerSection from './ThingTriggerSection'
|
import ThingTriggerSection from './ThingTriggerSection'
|
||||||
@@ -76,7 +77,7 @@ const DAY_OCCURRENCE_OPTIONS = [
|
|||||||
{ value: -1, label: 'Last occurrence' },
|
{ value: -1, label: 'Last occurrence' },
|
||||||
]
|
]
|
||||||
// Helper function to generate schedule preview text
|
// Helper function to generate schedule preview text
|
||||||
const generateSchedulePreview = metadata => {
|
const generateSchedulePreview = (metadata, formatTimeFn) => {
|
||||||
if (!metadata?.days?.length) return ''
|
if (!metadata?.days?.length) return ''
|
||||||
|
|
||||||
const dayNames = metadata.days
|
const dayNames = metadata.days
|
||||||
@@ -84,7 +85,7 @@ const generateSchedulePreview = metadata => {
|
|||||||
.join(', ')
|
.join(', ')
|
||||||
|
|
||||||
const timeStr = metadata.time
|
const timeStr = metadata.time
|
||||||
? moment(metadata.time).format('h:mm A')
|
? formatTimeFn(metadata.time)
|
||||||
: '6:00 PM'
|
: '6:00 PM'
|
||||||
|
|
||||||
if (metadata.weekPattern === 'every_week' || !metadata.weekPattern) {
|
if (metadata.weekPattern === 'every_week' || !metadata.weekPattern) {
|
||||||
@@ -114,6 +115,7 @@ const RepeatOnSections = ({
|
|||||||
frequencyMetadata,
|
frequencyMetadata,
|
||||||
onFrequencyMetadataUpdate,
|
onFrequencyMetadataUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { fmt } = useLocalization()
|
||||||
// if time on frequencyMetadata is not set, try to set it to the nextDueDate if available,
|
// if time on frequencyMetadata is not set, try to set it to the nextDueDate if available,
|
||||||
// otherwise set it to 18:00 of the current day
|
// otherwise set it to 18:00 of the current day
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -400,7 +402,7 @@ const RepeatOnSections = ({
|
|||||||
{frequencyMetadata?.days?.length > 0 && (
|
{frequencyMetadata?.days?.length > 0 && (
|
||||||
<Card mt={2} p={2}>
|
<Card mt={2} p={2}>
|
||||||
<Typography level='body-sm' color='primary'>
|
<Typography level='body-sm' color='primary'>
|
||||||
{generateSchedulePreview(frequencyMetadata)}
|
{generateSchedulePreview(frequencyMetadata, fmt.time)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import {
|
|||||||
import { Box, Card, Chip, Typography } from '@mui/joy'
|
import { Box, Card, Chip, Typography } from '@mui/joy'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
|
|
||||||
const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const [time, setTime] = useState(0)
|
const [time, setTime] = useState(0)
|
||||||
const [shouldAnimate, setShouldAnimate] = useState(false)
|
const [shouldAnimate, setShouldAnimate] = useState(false)
|
||||||
const [prevStatus, setPrevStatus] = useState(null) // Initialize as null
|
const [prevStatus, setPrevStatus] = useState(null) // Initialize as null
|
||||||
@@ -187,10 +189,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
|||||||
size='md'
|
size='md'
|
||||||
startDecorator={<Flag sx={{ fontSize: 14 }} />}
|
startDecorator={<Flag sx={{ fontSize: 14 }} />}
|
||||||
>
|
>
|
||||||
{new Date(chore.startTime).toLocaleTimeString([], {
|
{fmt.time(chore.startTime)}
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -202,10 +201,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
|||||||
size='md'
|
size='md'
|
||||||
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
|
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
|
||||||
>
|
>
|
||||||
{new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
|
{fmt.time(chore.timerUpdatedAt)}
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -219,10 +215,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
|||||||
size='md'
|
size='md'
|
||||||
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
|
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
|
||||||
>
|
>
|
||||||
{new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
|
{fmt.time(chore.timerUpdatedAt)}
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||||
import {
|
import {
|
||||||
getDueDateChipColor,
|
getDueDateChipColor,
|
||||||
@@ -45,6 +46,7 @@ const ChoreCard = ({
|
|||||||
onSelectionToggle,
|
onSelectionToggle,
|
||||||
}) => {
|
}) => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const { timeFormat } = useLocalization()
|
||||||
|
|
||||||
const { impersonatedUser } = useImpersonateUser()
|
const { impersonatedUser } = useImpersonateUser()
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ const ChoreCard = ({
|
|||||||
}}
|
}}
|
||||||
color={getDueDateChipColor(chore.nextDueDate, chore)}
|
color={getDueDateChipColor(chore.nextDueDate, chore)}
|
||||||
>
|
>
|
||||||
{getDueDateChipText(chore.nextDueDate, chore)}
|
{getDueDateChipText(chore.nextDueDate, chore, timeFormat)}
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|
||||||
<Chip
|
<Chip
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
|
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||||
import {
|
import {
|
||||||
getDueDateChipColor,
|
getDueDateChipColor,
|
||||||
@@ -41,6 +42,7 @@ const CompactChoreCard = ({
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const { timeFormat } = useLocalization()
|
||||||
const { data: circleMembersData } = useCircleMembers()
|
const { data: circleMembersData } = useCircleMembers()
|
||||||
|
|
||||||
const { impersonatedUser } = useImpersonateUser()
|
const { impersonatedUser } = useImpersonateUser()
|
||||||
@@ -395,7 +397,7 @@ const CompactChoreCard = ({
|
|||||||
ml: 1,
|
ml: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getDueDateChipText(chore.nextDueDate, chore)}
|
{getDueDateChipText(chore.nextDueDate, chore, timeFormat)}
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||||
import {
|
import {
|
||||||
useChoreHistory,
|
useChoreHistory,
|
||||||
@@ -44,6 +45,7 @@ const ChoreHistory = () => {
|
|||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||||
const [editHistory, setEditHistory] = useState(null)
|
const [editHistory, setEditHistory] = useState(null)
|
||||||
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
|
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
|
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
|
||||||
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
||||||
const { showSuccess, showError } = useNotification()
|
const { showSuccess, showError } = useNotification()
|
||||||
@@ -367,7 +369,7 @@ const ChoreHistory = () => {
|
|||||||
onViewNote={notes => {
|
onViewNote={notes => {
|
||||||
setNoteViewerConfig({
|
setNoteViewerConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: `Updated at ${moment(historyEntry.updatedAt).format('LLLL')}`,
|
title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`,
|
||||||
content: notes,
|
content: notes,
|
||||||
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Avatar, Box, Chip, Grid, IconButton, Typography } from '@mui/joy'
|
import { Avatar, Box, Chip, Grid, IconButton, Typography } from '@mui/joy'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||||
|
|
||||||
const getCompletedChip = historyEntry => {
|
const getCompletedChip = historyEntry => {
|
||||||
@@ -96,6 +97,7 @@ const HistoryCard = ({
|
|||||||
onToggleActions,
|
onToggleActions,
|
||||||
onViewNote,
|
onViewNote,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const performer = performers.find(p => p.userId === historyEntry.completedBy)
|
const performer = performers.find(p => p.userId === historyEntry.completedBy)
|
||||||
const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo)
|
const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo)
|
||||||
|
|
||||||
@@ -198,13 +200,12 @@ const HistoryCard = ({
|
|||||||
? 'Rescheduled'
|
? 'Rescheduled'
|
||||||
: 'Completed'}
|
: 'Completed'}
|
||||||
</Typography>
|
</Typography>
|
||||||
{historyEntry.performedAt && (
|
|
||||||
<Chip size='sm' startDecorator={<EventNote />}>
|
<Chip size='sm' startDecorator={<EventNote />}>
|
||||||
{moment(
|
{fmt.dateTime(
|
||||||
historyEntry.performedAt || historyEntry.updatedAt,
|
historyEntry.performedAt || historyEntry.updatedAt,
|
||||||
).format('MMM DD, h:mm A')}
|
)}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
{getCompletedChip(historyEntry)}
|
{getCompletedChip(historyEntry)}
|
||||||
@@ -224,7 +225,7 @@ const HistoryCard = ({
|
|||||||
>
|
>
|
||||||
{historyEntry.dueDate && (
|
{historyEntry.dueDate && (
|
||||||
<Chip size='sm' startDecorator={<CalendarMonth />}>
|
<Chip size='sm' startDecorator={<CalendarMonth />}>
|
||||||
{moment(historyEntry.dueDate).format('MMM DD h:mm A')}
|
{fmt.dateTime(historyEntry.dueDate)}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useLocalization } from '../../../contexts/LocalizationContext'
|
||||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||||
import { useNotification } from '../../../service/NotificationProvider'
|
import { useNotification } from '../../../service/NotificationProvider'
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +24,7 @@ import ConfirmationModal from './ConfirmationModal'
|
|||||||
|
|
||||||
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||||
const { ResponsiveModal } = useResponsiveModal()
|
const { ResponsiveModal } = useResponsiveModal()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
|
|
||||||
const [timerData, setTimerData] = useState(null)
|
const [timerData, setTimerData] = useState(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -632,11 +634,9 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
|||||||
const sessionDate = moment(pause.start).format(
|
const sessionDate = moment(pause.start).format(
|
||||||
'MMM DD',
|
'MMM DD',
|
||||||
)
|
)
|
||||||
const startTime = moment(pause.start).format(
|
const startTime = fmt.time(pause.start)
|
||||||
'HH:mm',
|
|
||||||
)
|
|
||||||
const endTime = pause.end
|
const endTime = pause.end
|
||||||
? moment(pause.end).format('HH:mm')
|
? fmt.time(pause.end)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const realTimeDuration = isOngoing
|
const realTimeDuration = isOngoing
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +28,7 @@ import SettingsLayout from './SettingsLayout'
|
|||||||
const APITokenSettings = () => {
|
const APITokenSettings = () => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
const { showNotification } = useNotification()
|
const { showNotification } = useNotification()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const [tokens, setTokens] = useState([])
|
const [tokens, setTokens] = useState([])
|
||||||
const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
|
const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
|
||||||
const [showTokenId, setShowTokenId] = useState(null)
|
const [showTokenId, setShowTokenId] = useState(null)
|
||||||
@@ -106,7 +108,7 @@ const APITokenSettings = () => {
|
|||||||
<Typography level='body-md'>{token.name}</Typography>
|
<Typography level='body-md'>{token.name}</Typography>
|
||||||
<Typography level='body-xs'>
|
<Typography level='body-xs'>
|
||||||
{moment(token.createdAt).fromNow()}(
|
{moment(token.createdAt).fromNow()}(
|
||||||
{moment(token.createdAt).format('lll')})
|
{fmt.dateTime(token.createdAt)})
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import SubscriptionModal from '../../components/SubscriptionModal'
|
import SubscriptionModal from '../../components/SubscriptionModal'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import { CancelSubscription, UpdatePassword } from '../../utils/Fetcher'
|
import { CancelSubscription, UpdatePassword } from '../../utils/Fetcher'
|
||||||
@@ -17,6 +18,7 @@ const AccountSettings = () => {
|
|||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showNotification } = useNotification()
|
const { showNotification } = useNotification()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
|
|
||||||
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
||||||
const [subscriptionModal, setSubscriptionModal] = useState(false)
|
const [subscriptionModal, setSubscriptionModal] = useState(false)
|
||||||
@@ -37,13 +39,9 @@ const AccountSettings = () => {
|
|||||||
|
|
||||||
const getSubscriptionDetails = () => {
|
const getSubscriptionDetails = () => {
|
||||||
if (userProfile?.subscription === 'active') {
|
if (userProfile?.subscription === 'active') {
|
||||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
|
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(userProfile?.expiration)}.`
|
||||||
userProfile?.expiration,
|
|
||||||
).format('MMM DD, YYYY')}.`
|
|
||||||
} else if (userProfile?.subscription === 'cancelled') {
|
} else if (userProfile?.subscription === 'cancelled') {
|
||||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
|
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date(userProfile?.expiration)}.`
|
||||||
userProfile?.expiration,
|
|
||||||
).format('MMM DD, YYYY')}.`
|
|
||||||
} else {
|
} else {
|
||||||
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
||||||
}
|
}
|
||||||
@@ -54,9 +52,7 @@ const AccountSettings = () => {
|
|||||||
return `Plus`
|
return `Plus`
|
||||||
} else if (userProfile?.subscription === 'cancelled') {
|
} else if (userProfile?.subscription === 'cancelled') {
|
||||||
if (moment().isBefore(userProfile?.expiration)) {
|
if (moment().isBefore(userProfile?.expiration)) {
|
||||||
return `Plus(until ${moment(userProfile?.expiration).format(
|
return `Plus(until ${fmt.date(userProfile?.expiration)})`
|
||||||
'MMM DD, YYYY',
|
|
||||||
)})`
|
|
||||||
}
|
}
|
||||||
return `Free`
|
return `Free`
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +37,7 @@ const CircleSettings = () => {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showNotification } = useNotification()
|
const { showNotification } = useNotification()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
|
|
||||||
const [userCircles, setUserCircles] = useState([])
|
const [userCircles, setUserCircles] = useState([])
|
||||||
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
||||||
@@ -221,12 +223,12 @@ const CircleSettings = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
{member.isActive ? (
|
{member.isActive ? (
|
||||||
<Typography level='body-sm'>
|
<Typography level='body-sm'>
|
||||||
Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
|
Joined on {fmt.date(member.createdAt)}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography level='body-sm' color='danger'>
|
<Typography level='body-sm' color='danger'>
|
||||||
Request to join{' '}
|
Request to join{' '}
|
||||||
{moment(member.updatedAt).format('MMM DD, YYYY')}
|
{fmt.date(member.updatedAt)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -365,7 +367,7 @@ const CircleSettings = () => {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
{lastRefresh && (
|
{lastRefresh && (
|
||||||
<Typography level='body-sm' color='neutral'>
|
<Typography level='body-sm' color='neutral'>
|
||||||
Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
|
Last updated: {fmt.dateTime(lastRefresh)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
196
src/views/Settings/LocalizationSettings.jsx
Normal file
196
src/views/Settings/LocalizationSettings.jsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import {
|
||||||
|
DATE_FORMATS,
|
||||||
|
TIME_FORMATS,
|
||||||
|
useLocalization,
|
||||||
|
} from '@/contexts/LocalizationContext'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
|
const LocalizationSettings = () => {
|
||||||
|
const { t } = useTranslation('settings')
|
||||||
|
const {
|
||||||
|
language,
|
||||||
|
setLanguage,
|
||||||
|
dateFormat,
|
||||||
|
setDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
setTimeFormat,
|
||||||
|
firstDayOfWeek,
|
||||||
|
setFirstDayOfWeek,
|
||||||
|
availableLanguages,
|
||||||
|
isRTL,
|
||||||
|
} = useLocalization()
|
||||||
|
|
||||||
|
const sampleDate = moment('2024-01-15 14:30:00')
|
||||||
|
|
||||||
|
const dateFormatOptions = [
|
||||||
|
{ value: DATE_FORMATS.MDY, label: t('localization.formats.mdy') },
|
||||||
|
{ value: DATE_FORMATS.DMY, label: t('localization.formats.dmy') },
|
||||||
|
{ value: DATE_FORMATS.YMD, label: t('localization.formats.ymd') },
|
||||||
|
{ value: DATE_FORMATS.LONG, label: t('localization.formats.long') },
|
||||||
|
{ value: DATE_FORMATS.SHORT, label: t('localization.formats.short') },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsLayout title='Localization'>
|
||||||
|
<div className='grid gap-4 py-4'>
|
||||||
|
<Typography level='body-md'>{t('localization.description')}</Typography>
|
||||||
|
|
||||||
|
<Typography level='h3'>{t('localization.language')}</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography level='body-md'>
|
||||||
|
{t('localization.languageDescription')}
|
||||||
|
</Typography>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
value={language}
|
||||||
|
onChange={(_, value) => setLanguage(value)}
|
||||||
|
sx={{ maxWidth: '300px' }}
|
||||||
|
>
|
||||||
|
{availableLanguages.map(lang => (
|
||||||
|
<Option key={lang.code} value={lang.code}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
|
<Typography>{lang.nativeName}</Typography>
|
||||||
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
|
({lang.name})
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{isRTL && (
|
||||||
|
<FormHelperText>
|
||||||
|
{t(
|
||||||
|
'localization.rtlNotice',
|
||||||
|
'This language uses right-to-left (RTL) text direction',
|
||||||
|
)}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Typography level='h3'>{t('localization.dateFormat')}</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography level='body-md'>
|
||||||
|
{t('localization.dateFormatDescription')}
|
||||||
|
</Typography>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
value={dateFormat}
|
||||||
|
onChange={(_, value) => setDateFormat(value)}
|
||||||
|
sx={{ maxWidth: '300px' }}
|
||||||
|
>
|
||||||
|
{dateFormatOptions.map(option => (
|
||||||
|
<Option key={option.value} value={option.value}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>{option.label}</Typography>
|
||||||
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
|
{sampleDate.format(option.value)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<FormHelperText>
|
||||||
|
Preview: {sampleDate.format(dateFormat)}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Typography level='h3'>{t('localization.timeFormat')}</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography level='body-md'>
|
||||||
|
{t('localization.timeFormatDescription')}
|
||||||
|
</Typography>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
value={timeFormat}
|
||||||
|
onChange={(_, value) => setTimeFormat(value)}
|
||||||
|
sx={{ maxWidth: '300px' }}
|
||||||
|
>
|
||||||
|
<Option value={TIME_FORMATS.HOUR_12}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>{t('localization.12hour')}</Typography>
|
||||||
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
|
{sampleDate.format(TIME_FORMATS.HOUR_12)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Option>
|
||||||
|
<Option value={TIME_FORMATS.HOUR_24}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>{t('localization.24hour')}</Typography>
|
||||||
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
|
{sampleDate.format(TIME_FORMATS.HOUR_24)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
<FormHelperText>
|
||||||
|
Preview: {sampleDate.format(timeFormat)}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Typography level='h3'>{t('localization.firstDayOfWeek')}</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography level='body-md'>
|
||||||
|
{t('localization.firstDayOfWeekDescription')}
|
||||||
|
</Typography>
|
||||||
|
<FormControl>
|
||||||
|
<ButtonGroup variant='outlined'>
|
||||||
|
<Button
|
||||||
|
variant={firstDayOfWeek === 0 ? 'solid' : 'outlined'}
|
||||||
|
onClick={() => setFirstDayOfWeek(0)}
|
||||||
|
>
|
||||||
|
{t('localization.sunday')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={firstDayOfWeek === 1 ? 'solid' : 'outlined'}
|
||||||
|
onClick={() => setFirstDayOfWeek(1)}
|
||||||
|
>
|
||||||
|
{t('localization.monday')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={firstDayOfWeek === 6 ? 'solid' : 'outlined'}
|
||||||
|
onClick={() => setFirstDayOfWeek(6)}
|
||||||
|
>
|
||||||
|
{t('localization.saturday')}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LocalizationSettings
|
||||||
@@ -13,6 +13,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
|||||||
import imageCompression from 'browser-image-compression'
|
import imageCompression from 'browser-image-compression'
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import Cropper from 'react-easy-crop'
|
import Cropper from 'react-easy-crop'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import { apiClient } from '../../utils/ApiClient'
|
import { apiClient } from '../../utils/ApiClient'
|
||||||
@@ -22,6 +23,7 @@ import { getCroppedImg } from '../../utils/imageCropUtils'
|
|||||||
import SettingsLayout from './SettingsLayout'
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
const ProfileSettings = () => {
|
const ProfileSettings = () => {
|
||||||
|
const { t } = useTranslation('settings')
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
const { showSuccess, showError } = useNotification()
|
const { showSuccess, showError } = useNotification()
|
||||||
@@ -92,13 +94,13 @@ const ProfileSettings = () => {
|
|||||||
|
|
||||||
setPhotoURL(url)
|
setPhotoURL(url)
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Photo Updated',
|
title: t('profile.photoUpdated'),
|
||||||
message: 'Your profile photo has been updated successfully!',
|
message: t('profile.photoUpdatedMessage'),
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Upload Failed',
|
title: t('profile.uploadFailed'),
|
||||||
message: 'Failed to upload your photo. Please try again.',
|
message: t('profile.uploadFailedMessage'),
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
@@ -118,8 +120,8 @@ const ProfileSettings = () => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Profile Updated',
|
title: t('profile.profileUpdated'),
|
||||||
message: 'Your profile information has been saved successfully!',
|
message: t('profile.profileUpdatedMessage'),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to update profile')
|
throw new Error('Failed to update profile')
|
||||||
@@ -128,9 +130,8 @@ const ProfileSettings = () => {
|
|||||||
console.log(err)
|
console.log(err)
|
||||||
|
|
||||||
showError({
|
showError({
|
||||||
title: 'Update Failed',
|
title: t('profile.updateFailed'),
|
||||||
message:
|
message: t('profile.updateFailedMessage'),
|
||||||
'Unable to update your profile. Please check your connection and try again.',
|
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
@@ -140,10 +141,10 @@ const ProfileSettings = () => {
|
|||||||
// Helper to resolve photoURL with baseURL if needed
|
// Helper to resolve photoURL with baseURL if needed
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout title='Profile Settings'>
|
<SettingsLayout title={t('profile.title')}>
|
||||||
<div className='grid gap-4 py-4' id='profile'>
|
<div className='grid gap-4 py-4' id='profile'>
|
||||||
<Typography level='body-md'>
|
<Typography level='body-md'>
|
||||||
Update your display name and profile photo.
|
{t('profile.description')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
@@ -163,7 +164,7 @@ const ProfileSettings = () => {
|
|||||||
loading={isUploading}
|
loading={isUploading}
|
||||||
sx={{ mb: 1 }}
|
sx={{ mb: 1 }}
|
||||||
>
|
>
|
||||||
Change Photo
|
{t('profile.changePhoto')}
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
@@ -227,7 +228,7 @@ const ProfileSettings = () => {
|
|||||||
size='md'
|
size='md'
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
>
|
>
|
||||||
Save
|
{t('profile.save')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -237,24 +238,24 @@ const ProfileSettings = () => {
|
|||||||
variant='soft'
|
variant='soft'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
>
|
>
|
||||||
Cancel
|
{t('profile.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Box sx={{ maxWidth: 400, mt: 3 }}>
|
<Box sx={{ maxWidth: 400, mt: 3 }}>
|
||||||
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
||||||
Display Name
|
{t('profile.displayName')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={e => setDisplayName(e.target.value)}
|
onChange={e => setDisplayName(e.target.value)}
|
||||||
placeholder='Enter your display name'
|
placeholder={t('profile.displayNamePlaceholder')}
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
||||||
Timezone
|
{t('profile.timezone')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
value={timezone}
|
value={timezone}
|
||||||
@@ -283,7 +284,7 @@ const ProfileSettings = () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
placeholder='Select your timezone'
|
placeholder={t('profile.timezonePlaceholder')}
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -294,7 +295,7 @@ const ProfileSettings = () => {
|
|||||||
loading={isSaving}
|
loading={isSaving}
|
||||||
sx={{ width: 120 }}
|
sx={{ width: 120 }}
|
||||||
>
|
>
|
||||||
Save
|
{t('profile.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import RealTimeSettings from '../../components/RealTimeSettings'
|
import RealTimeSettings from '../../components/RealTimeSettings'
|
||||||
import SubscriptionModal from '../../components/SubscriptionModal'
|
import SubscriptionModal from '../../components/SubscriptionModal'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import Logo from '../../Logo'
|
import Logo from '../../Logo'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
@@ -46,6 +47,7 @@ import NativeCancelSubscriptionModal from '../Modals/Inputs/NativeCancelSubscrip
|
|||||||
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
|
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
|
||||||
import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
|
import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
|
||||||
import APITokenSettings from './APITokenSettings'
|
import APITokenSettings from './APITokenSettings'
|
||||||
|
import LocalizationSettings from './LocalizationSettings'
|
||||||
import MFASettings from './MFASettings'
|
import MFASettings from './MFASettings'
|
||||||
import NotificationSetting from './NotificationSetting'
|
import NotificationSetting from './NotificationSetting'
|
||||||
import ProfileSettings from './ProfileSettings'
|
import ProfileSettings from './ProfileSettings'
|
||||||
@@ -58,6 +60,7 @@ const Settings = () => {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { showNotification } = useNotification()
|
const { showNotification } = useNotification()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
|
|
||||||
const [userCircles, setUserCircles] = useState([])
|
const [userCircles, setUserCircles] = useState([])
|
||||||
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
||||||
@@ -188,13 +191,13 @@ const Settings = () => {
|
|||||||
|
|
||||||
const getSubscriptionDetails = () => {
|
const getSubscriptionDetails = () => {
|
||||||
if (userProfile?.subscription === 'active') {
|
if (userProfile?.subscription === 'active') {
|
||||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
|
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(
|
||||||
userProfile?.expiration,
|
userProfile?.expiration,
|
||||||
).format('MMM DD, YYYY')}.`
|
)}.`
|
||||||
} else if (userProfile?.subscription === 'cancelled') {
|
} else if (userProfile?.subscription === 'cancelled') {
|
||||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
|
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date(
|
||||||
userProfile?.expiration,
|
userProfile?.expiration,
|
||||||
).format('MMM DD, YYYY')}.`
|
)}.`
|
||||||
} else {
|
} else {
|
||||||
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
||||||
}
|
}
|
||||||
@@ -204,9 +207,7 @@ const Settings = () => {
|
|||||||
return `Plus`
|
return `Plus`
|
||||||
} else if (userProfile?.subscription === 'cancelled') {
|
} else if (userProfile?.subscription === 'cancelled') {
|
||||||
if (moment().isBefore(userProfile?.expiration)) {
|
if (moment().isBefore(userProfile?.expiration)) {
|
||||||
return `Plus(until ${moment(userProfile?.expiration).format(
|
return `Plus(until ${fmt.date(userProfile?.expiration)})`
|
||||||
'MMM DD, YYYY',
|
|
||||||
)})`
|
|
||||||
}
|
}
|
||||||
return `Free`
|
return `Free`
|
||||||
} else {
|
} else {
|
||||||
@@ -339,12 +340,12 @@ const Settings = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
{member.isActive ? (
|
{member.isActive ? (
|
||||||
<Typography level='body-sm'>
|
<Typography level='body-sm'>
|
||||||
Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
|
Joined on {fmt.date(member.createdAt)}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography level='body-sm' color='danger'>
|
<Typography level='body-sm' color='danger'>
|
||||||
Request to join{' '}
|
Request to join{' '}
|
||||||
{moment(member.updatedAt).format('MMM DD, YYYY')}
|
{fmt.date(member.updatedAt)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -485,7 +486,7 @@ const Settings = () => {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
{lastRefresh && (
|
{lastRefresh && (
|
||||||
<Typography level='body-sm' color='neutral'>
|
<Typography level='body-sm' color='neutral'>
|
||||||
Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
|
Last updated: {fmt.dateTime(lastRefresh)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -916,6 +917,16 @@ const Settings = () => {
|
|||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-4 py-4' id='localization'>
|
||||||
|
<Typography level='h3'>Localization</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography level='body-md'>
|
||||||
|
Customize language, date format, and regional preferences for your
|
||||||
|
account. These settings will apply throughout the application.
|
||||||
|
</Typography>
|
||||||
|
<LocalizationSettings />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
{confirmModalConfig?.isOpen && (
|
{confirmModalConfig?.isOpen && (
|
||||||
<ConfirmationModal config={confirmModalConfig} />
|
<ConfirmationModal config={confirmModalConfig} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Circle,
|
Circle,
|
||||||
Code,
|
Code,
|
||||||
FamilyRestroom,
|
FamilyRestroom,
|
||||||
|
Language,
|
||||||
Notifications,
|
Notifications,
|
||||||
Palette,
|
Palette,
|
||||||
Person,
|
Person,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
@@ -29,98 +31,95 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
import { isParentUser } from '../../utils/UserHelpers'
|
import { isParentUser } from '../../utils/UserHelpers'
|
||||||
|
|
||||||
const SettingsOverview = () => {
|
const SettingsOverview = () => {
|
||||||
|
const { t } = useTranslation('settings')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
|
||||||
const settingsCards = [
|
const settingsCards = [
|
||||||
{
|
{
|
||||||
id: 'profile',
|
id: 'profile',
|
||||||
title: 'Profile Settings',
|
title: t('overview.sections.profile.title'),
|
||||||
description:
|
description: t('overview.sections.profile.description'),
|
||||||
'Update your profile information, photo, display name, and timezone preferences.',
|
|
||||||
icon: <Person />,
|
icon: <Person />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'circle',
|
id: 'circle',
|
||||||
title: 'Circle Settings',
|
title: t('overview.sections.circle.title'),
|
||||||
description:
|
description: t('overview.sections.circle.description'),
|
||||||
'Manage your circle, invite members, and handle join requests.',
|
|
||||||
icon: <Circle />,
|
icon: <Circle />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'account',
|
id: 'account',
|
||||||
title: 'Account Settings',
|
title: t('overview.sections.account.title'),
|
||||||
description:
|
description: t('overview.sections.account.description'),
|
||||||
'Manage your subscription, change password, and account deletion options.',
|
|
||||||
icon: <AccountCircle />,
|
icon: <AccountCircle />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'subaccounts',
|
id: 'subaccounts',
|
||||||
title: 'Managed Accounts',
|
title: t('overview.sections.subaccounts.title'),
|
||||||
description:
|
description: t('overview.sections.subaccounts.description'),
|
||||||
'Create and manage sub accounts to log in and complete assigned tasks.',
|
|
||||||
icon: <FamilyRestroom />,
|
icon: <FamilyRestroom />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notifications',
|
id: 'notifications',
|
||||||
title: 'Notifications',
|
title: t('overview.sections.notifications.title'),
|
||||||
description:
|
description: t('overview.sections.notifications.description'),
|
||||||
'Configure push notifications, email alerts, and notification targets for tasks.',
|
|
||||||
icon: <Notifications />,
|
icon: <Notifications />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mfa',
|
id: 'mfa',
|
||||||
title: 'Multi-Factor Authentication',
|
title: t('overview.sections.mfa.title'),
|
||||||
description:
|
description: t('overview.sections.mfa.description'),
|
||||||
'Add an extra layer of security with MFA using authenticator apps.',
|
|
||||||
icon: <Security />,
|
icon: <Security />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'apitokens',
|
id: 'apitokens',
|
||||||
title: 'API Tokens',
|
title: t('overview.sections.apitokens.title'),
|
||||||
description:
|
description: t('overview.sections.apitokens.description'),
|
||||||
'Generate and manage access tokens for third-party integrations and API access.',
|
|
||||||
icon: <Api />,
|
icon: <Api />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'storage',
|
id: 'storage',
|
||||||
title: 'Storage Settings',
|
title: t('overview.sections.storage.title'),
|
||||||
description:
|
description: t('overview.sections.storage.description'),
|
||||||
'Backup and restore your data, manage local storage and sync preferences.',
|
|
||||||
icon: <Storage />,
|
icon: <Storage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sidepanel',
|
id: 'sidepanel',
|
||||||
title: 'Sidepanel Customization',
|
title: t('overview.sections.sidepanel.title'),
|
||||||
description:
|
description: t('overview.sections.sidepanel.description'),
|
||||||
'Customize the layout and visibility of cards in the sidepanel interface.',
|
|
||||||
icon: <ViewSidebar />,
|
icon: <ViewSidebar />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'theme',
|
id: 'theme',
|
||||||
title: 'Theme Preferences',
|
title: t('overview.sections.theme.title'),
|
||||||
description:
|
description: t('overview.sections.theme.description'),
|
||||||
'Choose your preferred theme and configure dark/light mode settings.',
|
|
||||||
icon: <Palette />,
|
icon: <Palette />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'localization',
|
||||||
|
title: t('overview.sections.localization.title'),
|
||||||
|
description: t('overview.sections.localization.description'),
|
||||||
|
icon: <Language />,
|
||||||
|
isBeta: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'advanced',
|
id: 'advanced',
|
||||||
title: 'Advanced Settings',
|
title: t('overview.sections.advanced.title'),
|
||||||
description:
|
description: t('overview.sections.advanced.description'),
|
||||||
'Configure webhooks, real-time updates, and other advanced features for enhanced productivity.',
|
|
||||||
icon: <Settings />,
|
icon: <Settings />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'developer',
|
id: 'developer',
|
||||||
title: 'Developer Settings',
|
title: t('overview.sections.developer.title'),
|
||||||
description:
|
description: t('overview.sections.developer.description'),
|
||||||
'View technical information about authentication tokens, SSE connections, and debug data.',
|
|
||||||
icon: <Code />,
|
icon: <Code />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -159,10 +158,10 @@ const SettingsOverview = () => {
|
|||||||
level='h3'
|
level='h3'
|
||||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||||
>
|
>
|
||||||
Settings
|
{t('overview.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
Customize your experience and manage your account preferences
|
{t('overview.subtitle')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -226,7 +225,7 @@ const SettingsOverview = () => {
|
|||||||
fontSize: { xs: '0.95rem', md: '1.25rem' },
|
fontSize: { xs: '0.95rem', md: '1.25rem' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Upgrade to Plus
|
{t('overview.upgrade.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
level='body-md'
|
level='body-md'
|
||||||
@@ -237,7 +236,7 @@ const SettingsOverview = () => {
|
|||||||
lineHeight: { xs: 1.3, md: 1.5 },
|
lineHeight: { xs: 1.3, md: 1.5 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Unlock powerful features to enhance your productivity
|
{t('overview.upgrade.description')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -248,10 +247,16 @@ const SettingsOverview = () => {
|
|||||||
color: 'rgba(255, 255, 255, 0.8)',
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>• Rich text descriptions</span>
|
<span>• {t('overview.upgrade.features.richText')}</span>
|
||||||
<span>• Task notifications</span>
|
<span>
|
||||||
<span>• API integrations</span>
|
• {t('overview.upgrade.features.notifications')}
|
||||||
<span>• Advanced automation</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
• {t('overview.upgrade.features.apiIntegrations')}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
• {t('overview.upgrade.features.advancedAutomation')}
|
||||||
|
</span>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -279,7 +284,7 @@ const SettingsOverview = () => {
|
|||||||
navigate('/settings/account')
|
navigate('/settings/account')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Upgrade Now
|
{t('overview.upgrade.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -319,12 +324,33 @@ const SettingsOverview = () => {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemDecorator>
|
</ListItemDecorator>
|
||||||
<ListItemContent sx={{ ml: 2 }}>
|
<ListItemContent sx={{ ml: 2 }}>
|
||||||
<Typography
|
<Box
|
||||||
level='title-md'
|
sx={{
|
||||||
sx={{ mb: 0.5, fontWeight: 'lg' }}
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
mb: 0.5,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{setting.title}
|
<Typography level='title-md' sx={{ fontWeight: 'lg' }}>
|
||||||
</Typography>
|
{setting.title}
|
||||||
|
</Typography>
|
||||||
|
{setting.isBeta && (
|
||||||
|
<Chip
|
||||||
|
variant='outlined'
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
height: '20px',
|
||||||
|
fontSize: '0.65rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'warning.main',
|
||||||
|
borderColor: 'warning.main',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Early Access
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
<Typography
|
<Typography
|
||||||
level='body-sm'
|
level='body-sm'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import {
|
|||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const ELEMENTID = 'select-theme-mode'
|
const ELEMENTID = 'select-theme-mode'
|
||||||
|
|
||||||
const ThemeToggle = () => {
|
const ThemeToggle = () => {
|
||||||
|
const { t } = useTranslation('settings')
|
||||||
const { mode, setMode } = useColorScheme()
|
const { mode, setMode } = useColorScheme()
|
||||||
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ const ThemeToggle = () => {
|
|||||||
id={`${ELEMENTID}-label`}
|
id={`${ELEMENTID}-label`}
|
||||||
htmlFor='select-theme-mode'
|
htmlFor='select-theme-mode'
|
||||||
>
|
>
|
||||||
Theme mode
|
{t('theme.themeMode')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,13 +47,13 @@ const ThemeToggle = () => {
|
|||||||
onChange={handleThemeModeChange}
|
onChange={handleThemeModeChange}
|
||||||
>
|
>
|
||||||
<Button startDecorator={<LightModeOutlined />} value='light'>
|
<Button startDecorator={<LightModeOutlined />} value='light'>
|
||||||
Light
|
{t('theme.light')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startDecorator={<DarkModeOutlined />} value='dark'>
|
<Button startDecorator={<DarkModeOutlined />} value='dark'>
|
||||||
Dark
|
{t('theme.dark')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startDecorator={<LaptopOutlined />} value='system'>
|
<Button startDecorator={<LaptopOutlined />} value='system'>
|
||||||
System
|
{t('theme.system')}
|
||||||
</Button>
|
</Button>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { useTheme } from '@mui/joy/styles'
|
import { useTheme } from '@mui/joy/styles'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import {
|
import {
|
||||||
Line,
|
Line,
|
||||||
LineChart,
|
LineChart,
|
||||||
@@ -40,6 +41,7 @@ import LoadingComponent from '../components/Loading'
|
|||||||
const ThingsHistory = () => {
|
const ThingsHistory = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
@@ -294,9 +296,7 @@ const ThingsHistory = () => {
|
|||||||
tick='false'
|
tick='false'
|
||||||
tickLine='false'
|
tickLine='false'
|
||||||
axisLine='false'
|
axisLine='false'
|
||||||
tickFormatter={tick =>
|
tickFormatter={tick => fmt.dateTime(tick)}
|
||||||
moment(tick).format('ddd MM/DD/yyyy HH:mm:ss')
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
hide='true'
|
hide='true'
|
||||||
@@ -306,9 +306,7 @@ const ThingsHistory = () => {
|
|||||||
axisLine='false'
|
axisLine='false'
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
labelFormatter={label =>
|
labelFormatter={label => fmt.dateTime(label)}
|
||||||
moment(label).format('ddd MM/DD/yyyy HH:mm:ss')
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Line
|
<Line
|
||||||
@@ -399,7 +397,7 @@ const ThingsHistory = () => {
|
|||||||
color='primary'
|
color='primary'
|
||||||
startDecorator={<Schedule />}
|
startDecorator={<Schedule />}
|
||||||
>
|
>
|
||||||
{moment(history.updatedAt).format('MMM DD, h:mm A')}
|
{fmt.dateTime(history.updatedAt)}
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import {
|
import {
|
||||||
useChoreTimer,
|
useChoreTimer,
|
||||||
usePauseChore,
|
usePauseChore,
|
||||||
@@ -52,6 +53,7 @@ import LoadingComponent from '../components/Loading'
|
|||||||
|
|
||||||
const TimerDetails = () => {
|
const TimerDetails = () => {
|
||||||
const { choreId } = useParams()
|
const { choreId } = useParams()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const [timerData, setTimerData] = useState(null)
|
const [timerData, setTimerData] = useState(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [editingSessions, setEditingSessions] = useState({})
|
const [editingSessions, setEditingSessions] = useState({})
|
||||||
@@ -819,14 +821,14 @@ const TimerDetails = () => {
|
|||||||
level='body-xs'
|
level='body-xs'
|
||||||
sx={{ color: 'text.tertiary' }}
|
sx={{ color: 'text.tertiary' }}
|
||||||
>
|
>
|
||||||
Started: {moment(timerData.startTime).format('HH:mm')}
|
Started: {fmt.time(timerData.startTime)}
|
||||||
</Typography>
|
</Typography>
|
||||||
{timerData.endTime && (
|
{timerData.endTime && (
|
||||||
<Typography
|
<Typography
|
||||||
level='body-xs'
|
level='body-xs'
|
||||||
sx={{ color: 'text.tertiary' }}
|
sx={{ color: 'text.tertiary' }}
|
||||||
>
|
>
|
||||||
Ended: {moment(timerData.endTime).format('HH:mm')}
|
Ended: {fmt.time(timerData.endTime)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{!timerData.endTime && (
|
{!timerData.endTime && (
|
||||||
@@ -834,7 +836,7 @@ const TimerDetails = () => {
|
|||||||
level='body-xs'
|
level='body-xs'
|
||||||
sx={{ color: 'success.500' }}
|
sx={{ color: 'success.500' }}
|
||||||
>
|
>
|
||||||
Now: {moment(currentTime).format('HH:mm')}
|
Now: {fmt.time(currentTime)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
@@ -925,9 +927,9 @@ const TimerDetails = () => {
|
|||||||
const sessionDate = moment(pause.start).format(
|
const sessionDate = moment(pause.start).format(
|
||||||
'MMM DD',
|
'MMM DD',
|
||||||
)
|
)
|
||||||
const startTime = moment(pause.start).format('HH:mm')
|
const startTime = fmt.time(pause.start)
|
||||||
const endTime = pause.end
|
const endTime = pause.end
|
||||||
? moment(pause.end).format('HH:mm')
|
? fmt.time(pause.end)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const realTimeDuration = isOngoing
|
const realTimeDuration = isOngoing
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
||||||
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
|
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||||
@@ -142,7 +143,10 @@ const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onView
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
||||||
|
const { fmt } = useLocalization()
|
||||||
|
|
||||||
const groupedHistory = groupByDate(history)
|
const groupedHistory = groupByDate(history)
|
||||||
|
|
||||||
const sortedEntries = Object.entries(groupedHistory).sort(
|
const sortedEntries = Object.entries(groupedHistory).sort(
|
||||||
@@ -161,12 +165,7 @@ const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
|||||||
{Object.entries(groupedHistory).map(([date, items]) => (
|
{Object.entries(groupedHistory).map(([date, items]) => (
|
||||||
<Box key={date} sx={{ mb: 4 }}>
|
<Box key={date} sx={{ mb: 4 }}>
|
||||||
<Typography level='title-sm' sx={{ mb: 0.5 }}>
|
<Typography level='title-sm' sx={{ mb: 0.5 }}>
|
||||||
{new Date(date).toLocaleDateString([], {
|
{fmt.date(date)}
|
||||||
weekday: 'long',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
@@ -174,12 +173,10 @@ const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
|||||||
<>
|
<>
|
||||||
<ChoreHistoryItem
|
<ChoreHistoryItem
|
||||||
key={record.id}
|
key={record.id}
|
||||||
time={new Date(
|
|
||||||
|
time={fmt.time(
|
||||||
record.performedAt || record.updatedAt,
|
record.performedAt || record.updatedAt,
|
||||||
).toLocaleTimeString([], {
|
)}
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
name={record.choreName}
|
name={record.choreName}
|
||||||
points={record.points}
|
points={record.points}
|
||||||
status={record.status}
|
status={record.status}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Avatar, Box, Chip, Grid, Typography } from '@mui/joy'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||||
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ const getAssigneeColor = (assignee, userProfile) => {
|
|||||||
}
|
}
|
||||||
const CalendarCard = ({ chores }) => {
|
const CalendarCard = ({ chores }) => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const { fmt } = useLocalization()
|
||||||
|
|
||||||
const [selectedDate, setSeletedDate] = useState(null)
|
const [selectedDate, setSeletedDate] = useState(null)
|
||||||
const Navigate = useNavigate()
|
const Navigate = useNavigate()
|
||||||
@@ -197,7 +199,7 @@ const CalendarCard = ({ chores }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography level='title-md'>
|
<Typography level='title-md'>
|
||||||
{moment(selectedDate).format('MMMM D, YYYY')}
|
{fmt.date(selectedDate)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip variant='soft' color='primary' size='md'>
|
<Chip variant='soft' color='primary' size='md'>
|
||||||
{(() => {
|
{(() => {
|
||||||
@@ -291,7 +293,7 @@ const CalendarCard = ({ chores }) => {
|
|||||||
color: 'neutral.500',
|
color: 'neutral.500',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{moment(chore.nextDueDate).format('h:mm A')}
|
{fmt.time(chore.nextDueDate)}
|
||||||
</Typography>
|
</Typography>
|
||||||
{/* <Typography
|
{/* <Typography
|
||||||
level='body-xs'
|
level='body-xs'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Calendar from 'react-calendar'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||||
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import styles from './CalendarDual.module.css'
|
import styles from './CalendarDual.module.css'
|
||||||
|
|
||||||
const getAssigneeColor = (assignee, userProfile) => {
|
const getAssigneeColor = (assignee, userProfile) => {
|
||||||
@@ -13,6 +14,9 @@ const getAssigneeColor = (assignee, userProfile) => {
|
|||||||
}
|
}
|
||||||
const CalendarDual = ({ chores, onDateChange }) => {
|
const CalendarDual = ({ chores, onDateChange }) => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const { firstDayOfWeek, fmt } = useLocalization()
|
||||||
|
const calendarType =
|
||||||
|
firstDayOfWeek === 1 ? 'iso8601' : firstDayOfWeek === 6 ? 'islamic' : 'gregory'
|
||||||
|
|
||||||
const [selectedDate, setSeletedDate] = useState(null)
|
const [selectedDate, setSeletedDate] = useState(null)
|
||||||
const [currentDate, setCurrentDate] = useState(new Date())
|
const [currentDate, setCurrentDate] = useState(new Date())
|
||||||
@@ -96,16 +100,14 @@ const CalendarDual = ({ chores, onDateChange }) => {
|
|||||||
{isSecondary && (
|
{isSecondary && (
|
||||||
<div className={styles.secondaryCalendarHeader}>
|
<div className={styles.secondaryCalendarHeader}>
|
||||||
<Typography level='title-md' sx={{ textAlign: 'center', mb: 1 }}>
|
<Typography level='title-md' sx={{ textAlign: 'center', mb: 1 }}>
|
||||||
{date.toLocaleDateString('en-US', {
|
{fmt.date(date, 'MMMM YYYY')}
|
||||||
month: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Calendar
|
<Calendar
|
||||||
className={styles.reactCalendar}
|
className={styles.reactCalendar}
|
||||||
tileContent={tileContent}
|
tileContent={tileContent}
|
||||||
|
calendarType={calendarType}
|
||||||
onChange={d => {
|
onChange={d => {
|
||||||
let date = new Date(d)
|
let date = new Date(d)
|
||||||
setSeletedDate(date)
|
setSeletedDate(date)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import Calendar from 'react-calendar'
|
import Calendar from 'react-calendar'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { getPriorityColor } from '../../utils/Colors'
|
import { getPriorityColor } from '../../utils/Colors'
|
||||||
import styles from './Calendar.module.css'
|
import styles from './Calendar.module.css'
|
||||||
const CalendarMonthly = ({ chores, onDateChange }) => {
|
const CalendarMonthly = ({ chores, onDateChange }) => {
|
||||||
|
const { firstDayOfWeek } = useLocalization()
|
||||||
|
const calendarType =
|
||||||
|
firstDayOfWeek === 1 ? 'iso8601' : firstDayOfWeek === 6 ? 'islamic' : 'gregory'
|
||||||
const tileContent = ({ date, view }) => {
|
const tileContent = ({ date, view }) => {
|
||||||
if (view === 'month') {
|
if (view === 'month') {
|
||||||
const dayChores = chores.filter(chore => {
|
const dayChores = chores.filter(chore => {
|
||||||
@@ -51,6 +55,7 @@ const CalendarMonthly = ({ chores, onDateChange }) => {
|
|||||||
<div className={styles.reactCalendar}>
|
<div className={styles.reactCalendar}>
|
||||||
<Calendar
|
<Calendar
|
||||||
tileContent={tileContent}
|
tileContent={tileContent}
|
||||||
|
calendarType={calendarType}
|
||||||
onChange={d => {
|
onChange={d => {
|
||||||
onDateChange(new Date(d))
|
onDateChange(new Date(d))
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -25,78 +25,12 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { version } from '../../../package.json'
|
import { version } from '../../../package.json'
|
||||||
import UserProfileAvatar from '../../components/UserProfileAvatar'
|
import UserProfileAvatar from '../../components/UserProfileAvatar'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import NavBarLink from './NavBarLink'
|
import NavBarLink from './NavBarLink'
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
to: '/chores',
|
|
||||||
label: 'All Tasks',
|
|
||||||
icon: <Inbox />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: '/archived',
|
|
||||||
label: 'Archived',
|
|
||||||
icon: <Archive />,
|
|
||||||
},
|
|
||||||
|
|
||||||
// {
|
|
||||||
// to: '/chores',
|
|
||||||
// label: 'Desktop View',
|
|
||||||
// icon: <ListAltRounded />,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
to: '/things',
|
|
||||||
label: 'Things',
|
|
||||||
icon: <Widgets />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: 'labels',
|
|
||||||
label: 'Labels',
|
|
||||||
icon: <ListAlt />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: 'projects',
|
|
||||||
label: 'Projects',
|
|
||||||
icon: <FolderOpen />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: 'filters',
|
|
||||||
label: 'Filters',
|
|
||||||
icon: <FilterAlt />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: 'activities',
|
|
||||||
label: 'Activities',
|
|
||||||
icon: <History />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: 'points',
|
|
||||||
label: 'Points',
|
|
||||||
icon: <Toll />,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// to: '/settings#sharing',
|
|
||||||
// label: 'Sharing',
|
|
||||||
// icon: <ShareOutlined />,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// to: '/settings#notifications',
|
|
||||||
// label: 'Notifications',
|
|
||||||
// icon: <Message />,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// to: '/settings#account',
|
|
||||||
// label: 'Account',
|
|
||||||
// icon: <AccountBox />,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
to: '/settings',
|
|
||||||
label: 'Settings',
|
|
||||||
icon: <SettingsOutlined />,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
import { SafeArea } from 'capacitor-plugin-safe-area'
|
import { SafeArea } from 'capacitor-plugin-safe-area'
|
||||||
import Z_INDEX from '../../constants/zIndex'
|
import Z_INDEX from '../../constants/zIndex'
|
||||||
@@ -105,10 +39,60 @@ import { apiClient } from '../../utils/ApiClient'
|
|||||||
|
|
||||||
const publicPages = ['/landing', '/privacy', '/terms']
|
const publicPages = ['/landing', '/privacy', '/terms']
|
||||||
const NavBar = () => {
|
const NavBar = () => {
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
const { isRTL } = useLocalization()
|
||||||
const { data: resource } = useResource()
|
const { data: resource } = useResource()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
to: '/chores',
|
||||||
|
label: t('navigation.allTasks'),
|
||||||
|
icon: <Inbox />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/archived',
|
||||||
|
label: t('navigation.archived'),
|
||||||
|
icon: <Archive />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/things',
|
||||||
|
label: t('navigation.things'),
|
||||||
|
icon: <Widgets />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: 'labels',
|
||||||
|
label: t('navigation.labels'),
|
||||||
|
icon: <ListAlt />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: 'projects',
|
||||||
|
label: t('navigation.projects'),
|
||||||
|
icon: <FolderOpen />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: 'filters',
|
||||||
|
label: t('navigation.filters'),
|
||||||
|
icon: <FilterAlt />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: 'activities',
|
||||||
|
label: t('navigation.activities'),
|
||||||
|
icon: <History />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: 'points',
|
||||||
|
label: t('navigation.points'),
|
||||||
|
icon: <Toll />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/settings',
|
||||||
|
label: t('navigation.settings'),
|
||||||
|
icon: <SettingsOutlined />,
|
||||||
|
},
|
||||||
|
]
|
||||||
const [openDrawer, closeDrawer] = [
|
const [openDrawer, closeDrawer] = [
|
||||||
() => setDrawerOpen(true),
|
() => setDrawerOpen(true),
|
||||||
() => setDrawerOpen(false),
|
() => setDrawerOpen(false),
|
||||||
@@ -157,7 +141,7 @@ const NavBar = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
searchParams.get('from') === 'calendar' ? 'Back to Calendar' : 'Back'
|
searchParams.get('from') === 'calendar' ? t('backToCalendar') : t('back')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
@@ -221,13 +205,14 @@ const NavBar = () => {
|
|||||||
<Drawer
|
<Drawer
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
onClose={closeDrawer}
|
onClose={closeDrawer}
|
||||||
|
anchor={isRTL ? 'right' : 'left'}
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={closeDrawer}
|
onClick={closeDrawer}
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiDrawer-content': {
|
'& .MuiDrawer-content': {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
// pt: 'calc(var(--safe-area-inset-top, 0px))',
|
// pt: 'calc(var(--safe-area-inset-top, 0px))',
|
||||||
left: 0,
|
...(isRTL ? { right: 0 } : { left: 0 }),
|
||||||
// pb: 'calc(var(--safe-area-inset-bottom, 0px))',
|
// pb: 'calc(var(--safe-area-inset-bottom, 0px))',
|
||||||
// height:
|
// height:
|
||||||
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',
|
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',
|
||||||
@@ -297,7 +282,7 @@ const NavBar = () => {
|
|||||||
<ListItemDecorator>
|
<ListItemDecorator>
|
||||||
<Logout />
|
<Logout />
|
||||||
</ListItemDecorator>
|
</ListItemDecorator>
|
||||||
<ListItemContent>Logout</ListItemContent>
|
<ListItemContent>{t('logout')}</ListItemContent>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
<Typography
|
<Typography
|
||||||
onClick={
|
onClick={
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { CompleteSubTask } from '../../utils/Fetcher'
|
import { CompleteSubTask } from '../../utils/Fetcher'
|
||||||
@@ -48,6 +49,7 @@ function SortableItem({
|
|||||||
editMode,
|
editMode,
|
||||||
performers = [],
|
performers = [],
|
||||||
}) {
|
}) {
|
||||||
|
const { fmt } = useLocalization()
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
useSortable({
|
useSortable({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
@@ -210,7 +212,7 @@ function SortableItem({
|
|||||||
fontSize: 'sm',
|
fontSize: 'sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{new Date(task.completedAt).toLocaleString()}
|
{fmt.dateTime(task.completedAt)}
|
||||||
{performers.find(p => p.userId === task.completedBy) ? (
|
{performers.find(p => p.userId === task.completedBy) ? (
|
||||||
<Chip>
|
<Chip>
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user