feat: Internationalize AuthCallback, Chores, ErrorNotFound, GroupDetail pages
This commit introduces internationalization for several pages: - AuthCallbackPage.vue - ChoresPage.vue (a comprehensive page with many elements) - ErrorNotFound.vue - GroupDetailPage.vue (including sub-sections for members, invites, chores summary, and expenses summary) Key changes: - Integrated `useI18n` in each listed page to handle translatable strings. - Replaced hardcoded text in templates and relevant script sections (notifications, dynamic messages, fallbacks, etc.) with `t('key')` calls. - Added new translation keys, organized under page-specific namespaces (e.g., `authCallbackPage`, `choresPage`, `errorNotFoundPage`, `groupDetailPage`), to `fe/src/i18n/en.json`. - Added corresponding keys with placeholder translations (prefixed with DE:, FR:, ES:) to `fe/src/i18n/de.json`, `fe/src/i18n/fr.json`, and `fe/src/i18n/es.json`. - Reused existing translation keys (e.g., for chore frequency options) where applicable.
This commit is contained in:
parent
e6c15210c1
commit
554814ad63
189
fe/package-lock.json
generated
189
fe/package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"workbox-background-sync": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@storybook/addon-docs": "^9.0.2",
|
||||
"@storybook/addon-onboarding": "^9.0.2",
|
||||
@ -2558,14 +2558,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/bundle-utils": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-10.0.1.tgz",
|
||||
"integrity": "sha512-WkaXfSevtpgtUR4t8K2M6lbR7g03mtOxFeh+vXp5KExvPqS12ppaRj1QxzwRuRI5VUto54A22BjKoBMLyHILWQ==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-8.0.0.tgz",
|
||||
"integrity": "sha512-1B++zykRnMwQ+20SpsZI1JCnV/YJt9Oq7AGlEurzkWJOFtFAVqaGc/oV36PBRYeiKnTbY9VYfjBimr2Vt42wLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "^11.1.2",
|
||||
"@intlify/shared": "^11.1.2",
|
||||
"@intlify/message-compiler": "^9.4.0",
|
||||
"@intlify/shared": "^9.4.0",
|
||||
"acorn": "^8.8.2",
|
||||
"escodegen": "^2.1.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
@ -2575,7 +2574,7 @@
|
||||
"yaml-eslint-parser": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 14.16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"petite-vue-i18n": {
|
||||
@ -2587,13 +2586,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/bundle-utils/node_modules/@intlify/message-compiler": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.3.tgz",
|
||||
"integrity": "sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==",
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
|
||||
"integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.1.3",
|
||||
"@intlify/shared": "9.14.4",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -2604,11 +2602,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/bundle-utils/node_modules/@intlify/shared": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.3.tgz",
|
||||
"integrity": "sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==",
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.4.tgz",
|
||||
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
@ -2661,19 +2658,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/unplugin-vue-i18n": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-6.0.8.tgz",
|
||||
"integrity": "sha512-Vvm3KhjE6TIBVUQAk37rBiaYy2M5OcWH0ZcI1XKEsOTeN1o0bErk+zeuXmcrcMc/73YggfI8RoxOUz9EB/69JQ==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-4.0.0.tgz",
|
||||
"integrity": "sha512-q2Mhqa/mLi0tulfLFO4fMXXvEbkSZpI5yGhNNsLTNJJ41icEGUuyDe+j5zRZIKSkOJRgX6YbCyibTDJdRsukmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@intlify/bundle-utils": "^10.0.1",
|
||||
"@intlify/shared": "^11.1.2",
|
||||
"@intlify/vue-i18n-extensions": "^8.0.0",
|
||||
"@intlify/bundle-utils": "^8.0.0",
|
||||
"@intlify/shared": "^9.4.0",
|
||||
"@rollup/pluginutils": "^5.1.0",
|
||||
"@typescript-eslint/scope-manager": "^8.13.0",
|
||||
"@typescript-eslint/typescript-estree": "^8.13.0",
|
||||
"@vue/compiler-sfc": "^3.2.47",
|
||||
"debug": "^4.3.3",
|
||||
"fast-glob": "^3.2.12",
|
||||
"js-yaml": "^4.1.0",
|
||||
@ -2681,16 +2674,15 @@
|
||||
"pathe": "^1.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2",
|
||||
"unplugin": "^1.1.0",
|
||||
"vue": "^3.4"
|
||||
"unplugin": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 14.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"petite-vue-i18n": "*",
|
||||
"vue": "^3.2.25",
|
||||
"vue-i18n": "*"
|
||||
"vue-i18n": "*",
|
||||
"vue-i18n-bridge": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"petite-vue-i18n": {
|
||||
@ -2698,15 +2690,17 @@
|
||||
},
|
||||
"vue-i18n": {
|
||||
"optional": true
|
||||
},
|
||||
"vue-i18n-bridge": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/unplugin-vue-i18n/node_modules/@intlify/shared": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.3.tgz",
|
||||
"integrity": "sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==",
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.4.tgz",
|
||||
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
@ -2744,117 +2738,6 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-extensions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/vue-i18n-extensions/-/vue-i18n-extensions-8.0.0.tgz",
|
||||
"integrity": "sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.6",
|
||||
"@intlify/shared": "^10.0.0",
|
||||
"@vue/compiler-dom": "^3.2.45",
|
||||
"vue-i18n": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@intlify/shared": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"@vue/compiler-dom": "^3.0.0",
|
||||
"vue": "^3.0.0",
|
||||
"vue-i18n": "^9.0.0 || ^10.0.0 || ^11.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@intlify/shared": {
|
||||
"optional": true
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
},
|
||||
"vue-i18n": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-extensions/node_modules/@intlify/core-base": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.7.tgz",
|
||||
"integrity": "sha512-mE71aUH5baH0me8duB4FY5qevUJizypHsYw3eCvmOx07QvmKppgOONx3dYINxuA89Z2qkAGb/K6Nrpi7aAMwew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "10.0.7",
|
||||
"@intlify/shared": "10.0.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-extensions/node_modules/@intlify/message-compiler": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.7.tgz",
|
||||
"integrity": "sha512-nrC4cDL/UHZSUqd8sRbVz+DPukzZ8NnG5OK+EB/nlxsH35deyzyVkXP/QuR8mFZrISJ+4hCd6VtCQCcT+RO+5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "10.0.7",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-extensions/node_modules/@intlify/shared": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.7.tgz",
|
||||
"integrity": "sha512-oeoq0L5+5P4ShXa6jBQcx+BT+USe3MjX0xJexZO1y7rfDJdwZ9+QP3jO4tcS1nxhBYYdjvFTqe4bmnLijV0GxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-extensions/node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@intlify/vue-i18n-extensions/node_modules/vue-i18n": {
|
||||
"version": "10.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.7.tgz",
|
||||
"integrity": "sha512-bKsk0PYwP9gdYF4nqSAT0kDpnLu1gZzlxFl885VH4mHVhEnqP16+/mAU05r1U6NIrc0fGDWP89tZ8GzeJZpe+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "10.0.7",
|
||||
"@intlify/shared": "10.0.7",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@ -6040,8 +5923,7 @@
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
@ -6848,7 +6730,6 @@
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
|
||||
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esprima": "^4.0.1",
|
||||
"estraverse": "^5.2.0",
|
||||
@ -9092,7 +8973,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz",
|
||||
"integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.5.0",
|
||||
"eslint-visitor-keys": "^3.0.0",
|
||||
@ -9111,7 +8991,6 @@
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.9.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
@ -9444,7 +9323,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
||||
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"pathe": "^2.0.1",
|
||||
@ -10118,7 +9996,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.4",
|
||||
@ -12273,8 +12150,7 @@
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
|
||||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.1.0",
|
||||
@ -13868,7 +13744,6 @@
|
||||
"resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^3.0.0",
|
||||
"yaml": "^2.0.0"
|
||||
|
@ -34,7 +34,7 @@
|
||||
"workbox-background-sync": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@storybook/addon-docs": "^9.0.2",
|
||||
"@storybook/addon-onboarding": "^9.0.2",
|
||||
|
274
fe/src/i18n/de.json
Normal file
274
fe/src/i18n/de.json
Normal file
@ -0,0 +1,274 @@
|
||||
{
|
||||
"message": {
|
||||
"hello": "Hallo"
|
||||
},
|
||||
"loginPage": {
|
||||
"emailLabel": "DE: Email",
|
||||
"passwordLabel": "DE: Password",
|
||||
"togglePasswordVisibilityLabel": "DE: Toggle password visibility",
|
||||
"loginButton": "DE: Login",
|
||||
"signupLink": "DE: Don't have an account? Sign up",
|
||||
"errors": {
|
||||
"emailRequired": "DE: Email is required",
|
||||
"emailInvalid": "DE: Invalid email format",
|
||||
"passwordRequired": "DE: Password is required",
|
||||
"loginFailed": "DE: Login failed. Please check your credentials."
|
||||
},
|
||||
"notifications": {
|
||||
"loginSuccess": "DE: Login successful"
|
||||
}
|
||||
},
|
||||
"listsPage": {
|
||||
"retryButton": "DE: Retry",
|
||||
"emptyState": {
|
||||
"noListsForGroup": "DE: No lists found for this group.",
|
||||
"noListsYet": "DE: You have no lists yet.",
|
||||
"personalGlobalInfo": "DE: Create a personal list or join a group to see shared lists.",
|
||||
"groupSpecificInfo": "DE: This group doesn't have any lists yet."
|
||||
},
|
||||
"createNewListButton": "DE: Create New List",
|
||||
"loadingLists": "DE: Loading lists...",
|
||||
"noDescription": "DE: No description",
|
||||
"addItemPlaceholder": "DE: Add new item...",
|
||||
"createCard": {
|
||||
"title": "DE: + Create a new list"
|
||||
},
|
||||
"pageTitle": {
|
||||
"forGroup": "DE: Lists for {groupName}",
|
||||
"forGroupId": "DE: Lists for Group {groupId}",
|
||||
"myLists": "DE: My Lists"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "DE: Failed to fetch lists."
|
||||
}
|
||||
},
|
||||
"groupsPage": {
|
||||
"retryButton": "DE: Retry",
|
||||
"emptyState": {
|
||||
"title": "DE: No Groups Yet!",
|
||||
"description": "DE: You are not a member of any groups yet. Create one or join using an invite code.",
|
||||
"createButton": "DE: Create New Group"
|
||||
},
|
||||
"groupCard": {
|
||||
"newListButton": "DE: List"
|
||||
},
|
||||
"createCard": {
|
||||
"title": "DE: + Group"
|
||||
},
|
||||
"joinGroup": {
|
||||
"title": "DE: Join a Group with Invite Code",
|
||||
"inputLabel": "DE: Enter Invite Code",
|
||||
"inputPlaceholder": "DE: Enter Invite Code",
|
||||
"joinButton": "DE: Join"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "DE: Create New Group",
|
||||
"closeButtonLabel": "DE: Close",
|
||||
"groupNameLabel": "DE: Group Name",
|
||||
"cancelButton": "DE: Cancel",
|
||||
"createButton": "DE: Create"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "DE: Failed to load groups",
|
||||
"groupNameRequired": "DE: Group name is required",
|
||||
"createFailed": "DE: Failed to create group. Please try again.",
|
||||
"inviteCodeRequired": "DE: Invite code is required",
|
||||
"joinFailed": "DE: Failed to join group. Please check the invite code and try again."
|
||||
},
|
||||
"notifications": {
|
||||
"groupCreatedSuccess": "DE: Group '{groupName}' created successfully.",
|
||||
"joinSuccessNamed": "DE: Successfully joined group '{groupName}'.",
|
||||
"joinSuccessGeneric": "DE: Successfully joined group.",
|
||||
"listCreatedSuccess": "DE: List '{listName}' created successfully."
|
||||
}
|
||||
},
|
||||
"authCallbackPage": {
|
||||
"redirecting": "DE: Redirecting...",
|
||||
"errors": {
|
||||
"authenticationFailed": "DE: Authentication failed"
|
||||
}
|
||||
},
|
||||
"choresPage": {
|
||||
"title": "DE: Chores",
|
||||
"tabs": {
|
||||
"overdue": "DE: Overdue",
|
||||
"today": "DE: Today",
|
||||
"upcoming": "DE: Upcoming",
|
||||
"allPending": "DE: All Pending",
|
||||
"completed": "DE: Completed"
|
||||
},
|
||||
"viewToggle": {
|
||||
"calendarLabel": "DE: Calendar View",
|
||||
"calendarText": "DE: Calendar",
|
||||
"listLabel": "DE: List View",
|
||||
"listText": "DE: List"
|
||||
},
|
||||
"newChoreButtonLabel": "DE: New Chore",
|
||||
"newChoreButtonText": "DE: New Chore",
|
||||
"loadingState": {
|
||||
"loadingChores": "DE: Loading chores..."
|
||||
},
|
||||
"calendar": {
|
||||
"prevMonthLabel": "DE: Previous month",
|
||||
"nextMonthLabel": "DE: Next month",
|
||||
"weekdays": {
|
||||
"sun": "DE: Sun",
|
||||
"mon": "DE: Mon",
|
||||
"tue": "DE: Tue",
|
||||
"wed": "DE: Wed",
|
||||
"thu": "DE: Thu",
|
||||
"fri": "DE: Fri",
|
||||
"sat": "DE: Sat"
|
||||
},
|
||||
"addChoreToDayLabel": "DE: Add chore to this day",
|
||||
"emptyState": "DE: No chores to display for this period."
|
||||
},
|
||||
"listView": {
|
||||
"choreTypePersonal": "DE: Personal",
|
||||
"choreTypeGroupFallback": "DE: Group",
|
||||
"completedDatePrefix": "DE: Completed:",
|
||||
"actions": {
|
||||
"doneTitle": "DE: Mark as Done",
|
||||
"doneText": "DE: Done",
|
||||
"undoTitle": "DE: Mark as Not Done",
|
||||
"undoText": "DE: Undo",
|
||||
"editTitle": "DE: Edit",
|
||||
"editLabel": "DE: Edit chore",
|
||||
"editText": "DE: Edit",
|
||||
"deleteTitle": "DE: Delete",
|
||||
"deleteLabel": "DE: Delete chore",
|
||||
"deleteText": "DE: Delete"
|
||||
},
|
||||
"emptyState": {
|
||||
"message": "DE: No chores in this view. Well done!",
|
||||
"viewAllButton": "DE: View All Pending"
|
||||
}
|
||||
},
|
||||
"choreModal": {
|
||||
"editTitle": "DE: Edit Chore",
|
||||
"newTitle": "DE: New Chore",
|
||||
"closeButtonLabel": "DE: Close modal",
|
||||
"nameLabel": "DE: Name",
|
||||
"namePlaceholder": "DE: Enter chore name",
|
||||
"typeLabel": "DE: Type",
|
||||
"typePersonal": "DE: Personal",
|
||||
"typeGroup": "DE: Group",
|
||||
"groupLabel": "DE: Group",
|
||||
"groupSelectDefault": "DE: Select a group",
|
||||
"descriptionLabel": "DE: Description",
|
||||
"descriptionPlaceholder": "DE: Add a description (optional)",
|
||||
"frequencyLabel": "DE: Frequency",
|
||||
"intervalLabel": "DE: Interval (days)",
|
||||
"intervalPlaceholder": "DE: e.g. 3",
|
||||
"dueDateLabel": "DE: Due Date",
|
||||
"quickDueDateToday": "DE: Today",
|
||||
"quickDueDateTomorrow": "DE: Tomorrow",
|
||||
"quickDueDateNextWeek": "DE: Next Week",
|
||||
"cancelButton": "DE: Cancel",
|
||||
"saveButton": "DE: Save"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "DE: Delete Chore",
|
||||
"confirmationText": "DE: Are you sure you want to delete this chore? This action cannot be undone.",
|
||||
"deleteButton": "DE: Delete"
|
||||
},
|
||||
"shortcutsModal": {
|
||||
"title": "DE: Keyboard Shortcuts",
|
||||
"descNewChore": "DE: New Chore",
|
||||
"descToggleView": "DE: Toggle View (List/Calendar)",
|
||||
"descToggleShortcuts": "DE: Show/Hide Shortcuts",
|
||||
"descCloseModal": "DE: Close any open Modal/Dialog"
|
||||
},
|
||||
"frequencyOptions": {
|
||||
"oneTime": "DE: One Time",
|
||||
"daily": "DE: Daily",
|
||||
"weekly": "DE: Weekly",
|
||||
"monthly": "DE: Monthly",
|
||||
"custom": "DE: Custom"
|
||||
},
|
||||
"formatters": {
|
||||
"noDueDate": "DE: No due date",
|
||||
"dueToday": "DE: Due Today",
|
||||
"dueTomorrow": "DE: Due Tomorrow",
|
||||
"overdueFull": "DE: Overdue: {date}",
|
||||
"dueFull": "DE: Due {date}",
|
||||
"invalidDate": "DE: Invalid Date"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "DE: Failed to load chores",
|
||||
"updateSuccess": "DE: Chore '{name}' updated successfully",
|
||||
"createSuccess": "DE: Chore '{name}' created successfully",
|
||||
"updateFailed": "DE: Failed to update chore",
|
||||
"createFailed": "DE: Failed to create chore",
|
||||
"deleteSuccess": "DE: Chore '{name}' deleted successfully",
|
||||
"deleteFailed": "DE: Failed to delete chore",
|
||||
"markedDone": "DE: {name} marked as done.",
|
||||
"markedNotDone": "DE: {name} marked as not done.",
|
||||
"statusUpdateFailed": "DE: Failed to update chore status."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "DE: Chore name is required.",
|
||||
"groupRequired": "DE: Please select a group for group chores.",
|
||||
"intervalRequired": "DE: Custom interval must be at least 1 day.",
|
||||
"dueDateRequired": "DE: Due date is required.",
|
||||
"invalidDueDate": "DE: Invalid due date format."
|
||||
},
|
||||
"unsavedChangesConfirmation": "DE: You have unsaved changes in the chore form. Are you sure you want to leave?"
|
||||
},
|
||||
"errorNotFoundPage": {
|
||||
"errorCode": "DE: 404",
|
||||
"errorMessage": "DE: Oops. Nothing here...",
|
||||
"goHomeButton": "DE: Go Home"
|
||||
},
|
||||
"groupDetailPage": {
|
||||
"loadingLabel": "DE: Loading group details...",
|
||||
"retryButton": "DE: Retry",
|
||||
"groupNotFound": "DE: Group not found or an error occurred.",
|
||||
"members": {
|
||||
"title": "DE: Group Members",
|
||||
"defaultRole": "DE: Member",
|
||||
"removeButton": "DE: Remove",
|
||||
"emptyState": "DE: No members found."
|
||||
},
|
||||
"invites": {
|
||||
"title": "DE: Invite Members",
|
||||
"regenerateButton": "DE: Regenerate Invite Code",
|
||||
"generateButton": "DE: Generate Invite Code",
|
||||
"activeCodeLabel": "DE: Current Active Invite Code:",
|
||||
"copyButtonLabel": "DE: Copy invite code",
|
||||
"copySuccess": "DE: Invite code copied to clipboard!",
|
||||
"emptyState": "DE: No active invite code. Click the button above to generate one.",
|
||||
"errors": {
|
||||
"newDataInvalid": "DE: New invite code data is invalid."
|
||||
}
|
||||
},
|
||||
"chores": {
|
||||
"title": "DE: Group Chores",
|
||||
"manageButton": "DE: Manage Chores",
|
||||
"duePrefix": "DE: Due:",
|
||||
"emptyState": "DE: No chores scheduled. Click \"Manage Chores\" to create some!"
|
||||
},
|
||||
"expenses": {
|
||||
"title": "DE: Group Expenses",
|
||||
"manageButton": "DE: Manage Expenses",
|
||||
"emptyState": "DE: No expenses recorded. Click \"Manage Expenses\" to add some!",
|
||||
"splitTypes": {
|
||||
"equal": "DE: Equal",
|
||||
"exactAmounts": "DE: Exact Amounts",
|
||||
"percentage": "DE: Percentage",
|
||||
"shares": "DE: Shares",
|
||||
"itemBased": "DE: Item Based"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"fetchDetailsFailed": "DE: Failed to fetch group details.",
|
||||
"fetchInviteFailed": "DE: Failed to fetch active invite code.",
|
||||
"generateInviteSuccess": "DE: New invite code generated successfully!",
|
||||
"generateInviteError": "DE: Failed to generate invite code.",
|
||||
"clipboardNotSupported": "DE: Clipboard not supported or no code to copy.",
|
||||
"copyInviteFailed": "DE: Failed to copy invite code.",
|
||||
"removeMemberSuccess": "DE: Member removed successfully",
|
||||
"removeMemberFailed": "DE: Failed to remove member"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
// This is just an example,
|
||||
// so you can safely delete all default props below
|
||||
|
||||
export default {
|
||||
failed: 'Action failed',
|
||||
success: 'Action was successful'
|
||||
};
|
274
fe/src/i18n/en.json
Normal file
274
fe/src/i18n/en.json
Normal file
@ -0,0 +1,274 @@
|
||||
{
|
||||
"message": {
|
||||
"hello": "Hello"
|
||||
},
|
||||
"loginPage": {
|
||||
"emailLabel": "Email",
|
||||
"passwordLabel": "Password",
|
||||
"togglePasswordVisibilityLabel": "Toggle password visibility",
|
||||
"loginButton": "Login",
|
||||
"signupLink": "Don't have an account? Sign up",
|
||||
"errors": {
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Invalid email format",
|
||||
"passwordRequired": "Password is required",
|
||||
"loginFailed": "Login failed. Please check your credentials."
|
||||
},
|
||||
"notifications": {
|
||||
"loginSuccess": "Login successful"
|
||||
}
|
||||
},
|
||||
"listsPage": {
|
||||
"retryButton": "Retry",
|
||||
"emptyState": {
|
||||
"noListsForGroup": "No lists found for this group.",
|
||||
"noListsYet": "You have no lists yet.",
|
||||
"personalGlobalInfo": "Create a personal list or join a group to see shared lists.",
|
||||
"groupSpecificInfo": "This group doesn't have any lists yet."
|
||||
},
|
||||
"createNewListButton": "Create New List",
|
||||
"loadingLists": "Loading lists...",
|
||||
"noDescription": "No description",
|
||||
"addItemPlaceholder": "Add new item...",
|
||||
"createCard": {
|
||||
"title": "+ Create a new list"
|
||||
},
|
||||
"pageTitle": {
|
||||
"forGroup": "Lists for {groupName}",
|
||||
"forGroupId": "Lists for Group {groupId}",
|
||||
"myLists": "My Lists"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Failed to fetch lists."
|
||||
}
|
||||
},
|
||||
"groupsPage": {
|
||||
"retryButton": "Retry",
|
||||
"emptyState": {
|
||||
"title": "No Groups Yet!",
|
||||
"description": "You are not a member of any groups yet. Create one or join using an invite code.",
|
||||
"createButton": "Create New Group"
|
||||
},
|
||||
"groupCard": {
|
||||
"newListButton": "List"
|
||||
},
|
||||
"createCard": {
|
||||
"title": "+ Group"
|
||||
},
|
||||
"joinGroup": {
|
||||
"title": "Join a Group with Invite Code",
|
||||
"inputLabel": "Enter Invite Code",
|
||||
"inputPlaceholder": "Enter Invite Code",
|
||||
"joinButton": "Join"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "Create New Group",
|
||||
"closeButtonLabel": "Close",
|
||||
"groupNameLabel": "Group Name",
|
||||
"cancelButton": "Cancel",
|
||||
"createButton": "Create"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Failed to load groups",
|
||||
"groupNameRequired": "Group name is required",
|
||||
"createFailed": "Failed to create group. Please try again.",
|
||||
"inviteCodeRequired": "Invite code is required",
|
||||
"joinFailed": "Failed to join group. Please check the invite code and try again."
|
||||
},
|
||||
"notifications": {
|
||||
"groupCreatedSuccess": "Group '{groupName}' created successfully.",
|
||||
"joinSuccessNamed": "Successfully joined group '{groupName}'.",
|
||||
"joinSuccessGeneric": "Successfully joined group.",
|
||||
"listCreatedSuccess": "List '{listName}' created successfully."
|
||||
}
|
||||
},
|
||||
"authCallbackPage": {
|
||||
"redirecting": "Redirecting...",
|
||||
"errors": {
|
||||
"authenticationFailed": "Authentication failed"
|
||||
}
|
||||
},
|
||||
"choresPage": {
|
||||
"title": "Chores",
|
||||
"tabs": {
|
||||
"overdue": "Overdue",
|
||||
"today": "Today",
|
||||
"upcoming": "Upcoming",
|
||||
"allPending": "All Pending",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"viewToggle": {
|
||||
"calendarLabel": "Calendar View",
|
||||
"calendarText": "Calendar",
|
||||
"listLabel": "List View",
|
||||
"listText": "List"
|
||||
},
|
||||
"newChoreButtonLabel": "New Chore",
|
||||
"newChoreButtonText": "New Chore",
|
||||
"loadingState": {
|
||||
"loadingChores": "Loading chores..."
|
||||
},
|
||||
"calendar": {
|
||||
"prevMonthLabel": "Previous month",
|
||||
"nextMonthLabel": "Next month",
|
||||
"weekdays": {
|
||||
"sun": "Sun",
|
||||
"mon": "Mon",
|
||||
"tue": "Tue",
|
||||
"wed": "Wed",
|
||||
"thu": "Thu",
|
||||
"fri": "Fri",
|
||||
"sat": "Sat"
|
||||
},
|
||||
"addChoreToDayLabel": "Add chore to this day",
|
||||
"emptyState": "No chores to display for this period."
|
||||
},
|
||||
"listView": {
|
||||
"choreTypePersonal": "Personal",
|
||||
"choreTypeGroupFallback": "Group",
|
||||
"completedDatePrefix": "Completed:",
|
||||
"actions": {
|
||||
"doneTitle": "Mark as Done",
|
||||
"doneText": "Done",
|
||||
"undoTitle": "Mark as Not Done",
|
||||
"undoText": "Undo",
|
||||
"editTitle": "Edit",
|
||||
"editLabel": "Edit chore",
|
||||
"editText": "Edit",
|
||||
"deleteTitle": "Delete",
|
||||
"deleteLabel": "Delete chore",
|
||||
"deleteText": "Delete"
|
||||
},
|
||||
"emptyState": {
|
||||
"message": "No chores in this view. Well done!",
|
||||
"viewAllButton": "View All Pending"
|
||||
}
|
||||
},
|
||||
"choreModal": {
|
||||
"editTitle": "Edit Chore",
|
||||
"newTitle": "New Chore",
|
||||
"closeButtonLabel": "Close modal",
|
||||
"nameLabel": "Name",
|
||||
"namePlaceholder": "Enter chore name",
|
||||
"typeLabel": "Type",
|
||||
"typePersonal": "Personal",
|
||||
"typeGroup": "Group",
|
||||
"groupLabel": "Group",
|
||||
"groupSelectDefault": "Select a group",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Add a description (optional)",
|
||||
"frequencyLabel": "Frequency",
|
||||
"intervalLabel": "Interval (days)",
|
||||
"intervalPlaceholder": "e.g. 3",
|
||||
"dueDateLabel": "Due Date",
|
||||
"quickDueDateToday": "Today",
|
||||
"quickDueDateTomorrow": "Tomorrow",
|
||||
"quickDueDateNextWeek": "Next Week",
|
||||
"cancelButton": "Cancel",
|
||||
"saveButton": "Save"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Delete Chore",
|
||||
"confirmationText": "Are you sure you want to delete this chore? This action cannot be undone.",
|
||||
"deleteButton": "Delete"
|
||||
},
|
||||
"shortcutsModal": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"descNewChore": "New Chore",
|
||||
"descToggleView": "Toggle View (List/Calendar)",
|
||||
"descToggleShortcuts": "Show/Hide Shortcuts",
|
||||
"descCloseModal": "Close any open Modal/Dialog"
|
||||
},
|
||||
"frequencyOptions": {
|
||||
"oneTime": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"formatters": {
|
||||
"noDueDate": "No due date",
|
||||
"dueToday": "Due Today",
|
||||
"dueTomorrow": "Due Tomorrow",
|
||||
"overdueFull": "Overdue: {date}",
|
||||
"dueFull": "Due {date}",
|
||||
"invalidDate": "Invalid Date"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load chores",
|
||||
"updateSuccess": "Chore '{name}' updated successfully",
|
||||
"createSuccess": "Chore '{name}' created successfully",
|
||||
"updateFailed": "Failed to update chore",
|
||||
"createFailed": "Failed to create chore",
|
||||
"deleteSuccess": "Chore '{name}' deleted successfully",
|
||||
"deleteFailed": "Failed to delete chore",
|
||||
"markedDone": "{name} marked as done.",
|
||||
"markedNotDone": "{name} marked as not done.",
|
||||
"statusUpdateFailed": "Failed to update chore status."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Chore name is required.",
|
||||
"groupRequired": "Please select a group for group chores.",
|
||||
"intervalRequired": "Custom interval must be at least 1 day.",
|
||||
"dueDateRequired": "Due date is required.",
|
||||
"invalidDueDate": "Invalid due date format."
|
||||
},
|
||||
"unsavedChangesConfirmation": "You have unsaved changes in the chore form. Are you sure you want to leave?"
|
||||
},
|
||||
"errorNotFoundPage": {
|
||||
"errorCode": "404",
|
||||
"errorMessage": "Oops. Nothing here...",
|
||||
"goHomeButton": "Go Home"
|
||||
},
|
||||
"groupDetailPage": {
|
||||
"loadingLabel": "Loading group details...",
|
||||
"retryButton": "Retry",
|
||||
"groupNotFound": "Group not found or an error occurred.",
|
||||
"members": {
|
||||
"title": "Group Members",
|
||||
"defaultRole": "Member",
|
||||
"removeButton": "Remove",
|
||||
"emptyState": "No members found."
|
||||
},
|
||||
"invites": {
|
||||
"title": "Invite Members",
|
||||
"regenerateButton": "Regenerate Invite Code",
|
||||
"generateButton": "Generate Invite Code",
|
||||
"activeCodeLabel": "Current Active Invite Code:",
|
||||
"copyButtonLabel": "Copy invite code",
|
||||
"copySuccess": "Invite code copied to clipboard!",
|
||||
"emptyState": "No active invite code. Click the button above to generate one.",
|
||||
"errors": {
|
||||
"newDataInvalid": "New invite code data is invalid."
|
||||
}
|
||||
},
|
||||
"chores": {
|
||||
"title": "Group Chores",
|
||||
"manageButton": "Manage Chores",
|
||||
"duePrefix": "Due:",
|
||||
"emptyState": "No chores scheduled. Click \"Manage Chores\" to create some!"
|
||||
},
|
||||
"expenses": {
|
||||
"title": "Group Expenses",
|
||||
"manageButton": "Manage Expenses",
|
||||
"emptyState": "No expenses recorded. Click \"Manage Expenses\" to add some!",
|
||||
"splitTypes": {
|
||||
"equal": "Equal",
|
||||
"exactAmounts": "Exact Amounts",
|
||||
"percentage": "Percentage",
|
||||
"shares": "Shares",
|
||||
"itemBased": "Item Based"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"fetchDetailsFailed": "Failed to fetch group details.",
|
||||
"fetchInviteFailed": "Failed to fetch active invite code.",
|
||||
"generateInviteSuccess": "New invite code generated successfully!",
|
||||
"generateInviteError": "Failed to generate invite code.",
|
||||
"clipboardNotSupported": "Clipboard not supported or no code to copy.",
|
||||
"copyInviteFailed": "Failed to copy invite code.",
|
||||
"removeMemberSuccess": "Member removed successfully",
|
||||
"removeMemberFailed": "Failed to remove member"
|
||||
}
|
||||
}
|
||||
}
|
274
fe/src/i18n/es.json
Normal file
274
fe/src/i18n/es.json
Normal file
@ -0,0 +1,274 @@
|
||||
{
|
||||
"message": {
|
||||
"hello": "Hola"
|
||||
},
|
||||
"loginPage": {
|
||||
"emailLabel": "ES: Email",
|
||||
"passwordLabel": "ES: Password",
|
||||
"togglePasswordVisibilityLabel": "ES: Toggle password visibility",
|
||||
"loginButton": "ES: Login",
|
||||
"signupLink": "ES: Don't have an account? Sign up",
|
||||
"errors": {
|
||||
"emailRequired": "ES: Email is required",
|
||||
"emailInvalid": "ES: Invalid email format",
|
||||
"passwordRequired": "ES: Password is required",
|
||||
"loginFailed": "ES: Login failed. Please check your credentials."
|
||||
},
|
||||
"notifications": {
|
||||
"loginSuccess": "ES: Login successful"
|
||||
}
|
||||
},
|
||||
"listsPage": {
|
||||
"retryButton": "ES: Retry",
|
||||
"emptyState": {
|
||||
"noListsForGroup": "ES: No lists found for this group.",
|
||||
"noListsYet": "ES: You have no lists yet.",
|
||||
"personalGlobalInfo": "ES: Create a personal list or join a group to see shared lists.",
|
||||
"groupSpecificInfo": "ES: This group doesn't have any lists yet."
|
||||
},
|
||||
"createNewListButton": "ES: Create New List",
|
||||
"loadingLists": "ES: Loading lists...",
|
||||
"noDescription": "ES: No description",
|
||||
"addItemPlaceholder": "ES: Add new item...",
|
||||
"createCard": {
|
||||
"title": "ES: + Create a new list"
|
||||
},
|
||||
"pageTitle": {
|
||||
"forGroup": "ES: Lists for {groupName}",
|
||||
"forGroupId": "ES: Lists for Group {groupId}",
|
||||
"myLists": "ES: My Lists"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "ES: Failed to fetch lists."
|
||||
}
|
||||
},
|
||||
"groupsPage": {
|
||||
"retryButton": "ES: Retry",
|
||||
"emptyState": {
|
||||
"title": "ES: No Groups Yet!",
|
||||
"description": "ES: You are not a member of any groups yet. Create one or join using an invite code.",
|
||||
"createButton": "ES: Create New Group"
|
||||
},
|
||||
"groupCard": {
|
||||
"newListButton": "ES: List"
|
||||
},
|
||||
"createCard": {
|
||||
"title": "ES: + Group"
|
||||
},
|
||||
"joinGroup": {
|
||||
"title": "ES: Join a Group with Invite Code",
|
||||
"inputLabel": "ES: Enter Invite Code",
|
||||
"inputPlaceholder": "ES: Enter Invite Code",
|
||||
"joinButton": "ES: Join"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "ES: Create New Group",
|
||||
"closeButtonLabel": "ES: Close",
|
||||
"groupNameLabel": "ES: Group Name",
|
||||
"cancelButton": "ES: Cancel",
|
||||
"createButton": "ES: Create"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "ES: Failed to load groups",
|
||||
"groupNameRequired": "ES: Group name is required",
|
||||
"createFailed": "ES: Failed to create group. Please try again.",
|
||||
"inviteCodeRequired": "ES: Invite code is required",
|
||||
"joinFailed": "ES: Failed to join group. Please check the invite code and try again."
|
||||
},
|
||||
"notifications": {
|
||||
"groupCreatedSuccess": "ES: Group '{groupName}' created successfully.",
|
||||
"joinSuccessNamed": "ES: Successfully joined group '{groupName}'.",
|
||||
"joinSuccessGeneric": "ES: Successfully joined group.",
|
||||
"listCreatedSuccess": "ES: List '{listName}' created successfully."
|
||||
}
|
||||
},
|
||||
"authCallbackPage": {
|
||||
"redirecting": "ES: Redirecting...",
|
||||
"errors": {
|
||||
"authenticationFailed": "ES: Authentication failed"
|
||||
}
|
||||
},
|
||||
"choresPage": {
|
||||
"title": "ES: Chores",
|
||||
"tabs": {
|
||||
"overdue": "ES: Overdue",
|
||||
"today": "ES: Today",
|
||||
"upcoming": "ES: Upcoming",
|
||||
"allPending": "ES: All Pending",
|
||||
"completed": "ES: Completed"
|
||||
},
|
||||
"viewToggle": {
|
||||
"calendarLabel": "ES: Calendar View",
|
||||
"calendarText": "ES: Calendar",
|
||||
"listLabel": "ES: List View",
|
||||
"listText": "ES: List"
|
||||
},
|
||||
"newChoreButtonLabel": "ES: New Chore",
|
||||
"newChoreButtonText": "ES: New Chore",
|
||||
"loadingState": {
|
||||
"loadingChores": "ES: Loading chores..."
|
||||
},
|
||||
"calendar": {
|
||||
"prevMonthLabel": "ES: Previous month",
|
||||
"nextMonthLabel": "ES: Next month",
|
||||
"weekdays": {
|
||||
"sun": "ES: Sun",
|
||||
"mon": "ES: Mon",
|
||||
"tue": "ES: Tue",
|
||||
"wed": "ES: Wed",
|
||||
"thu": "ES: Thu",
|
||||
"fri": "ES: Fri",
|
||||
"sat": "ES: Sat"
|
||||
},
|
||||
"addChoreToDayLabel": "ES: Add chore to this day",
|
||||
"emptyState": "ES: No chores to display for this period."
|
||||
},
|
||||
"listView": {
|
||||
"choreTypePersonal": "ES: Personal",
|
||||
"choreTypeGroupFallback": "ES: Group",
|
||||
"completedDatePrefix": "ES: Completed:",
|
||||
"actions": {
|
||||
"doneTitle": "ES: Mark as Done",
|
||||
"doneText": "ES: Done",
|
||||
"undoTitle": "ES: Mark as Not Done",
|
||||
"undoText": "ES: Undo",
|
||||
"editTitle": "ES: Edit",
|
||||
"editLabel": "ES: Edit chore",
|
||||
"editText": "ES: Edit",
|
||||
"deleteTitle": "ES: Delete",
|
||||
"deleteLabel": "ES: Delete chore",
|
||||
"deleteText": "ES: Delete"
|
||||
},
|
||||
"emptyState": {
|
||||
"message": "ES: No chores in this view. Well done!",
|
||||
"viewAllButton": "ES: View All Pending"
|
||||
}
|
||||
},
|
||||
"choreModal": {
|
||||
"editTitle": "ES: Edit Chore",
|
||||
"newTitle": "ES: New Chore",
|
||||
"closeButtonLabel": "ES: Close modal",
|
||||
"nameLabel": "ES: Name",
|
||||
"namePlaceholder": "ES: Enter chore name",
|
||||
"typeLabel": "ES: Type",
|
||||
"typePersonal": "ES: Personal",
|
||||
"typeGroup": "ES: Group",
|
||||
"groupLabel": "ES: Group",
|
||||
"groupSelectDefault": "ES: Select a group",
|
||||
"descriptionLabel": "ES: Description",
|
||||
"descriptionPlaceholder": "ES: Add a description (optional)",
|
||||
"frequencyLabel": "ES: Frequency",
|
||||
"intervalLabel": "ES: Interval (days)",
|
||||
"intervalPlaceholder": "ES: e.g. 3",
|
||||
"dueDateLabel": "ES: Due Date",
|
||||
"quickDueDateToday": "ES: Today",
|
||||
"quickDueDateTomorrow": "ES: Tomorrow",
|
||||
"quickDueDateNextWeek": "ES: Next Week",
|
||||
"cancelButton": "ES: Cancel",
|
||||
"saveButton": "ES: Save"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "ES: Delete Chore",
|
||||
"confirmationText": "ES: Are you sure you want to delete this chore? This action cannot be undone.",
|
||||
"deleteButton": "ES: Delete"
|
||||
},
|
||||
"shortcutsModal": {
|
||||
"title": "ES: Keyboard Shortcuts",
|
||||
"descNewChore": "ES: New Chore",
|
||||
"descToggleView": "ES: Toggle View (List/Calendar)",
|
||||
"descToggleShortcuts": "ES: Show/Hide Shortcuts",
|
||||
"descCloseModal": "ES: Close any open Modal/Dialog"
|
||||
},
|
||||
"frequencyOptions": {
|
||||
"oneTime": "ES: One Time",
|
||||
"daily": "ES: Daily",
|
||||
"weekly": "ES: Weekly",
|
||||
"monthly": "ES: Monthly",
|
||||
"custom": "ES: Custom"
|
||||
},
|
||||
"formatters": {
|
||||
"noDueDate": "ES: No due date",
|
||||
"dueToday": "ES: Due Today",
|
||||
"dueTomorrow": "ES: Due Tomorrow",
|
||||
"overdueFull": "ES: Overdue: {date}",
|
||||
"dueFull": "ES: Due {date}",
|
||||
"invalidDate": "ES: Invalid Date"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "ES: Failed to load chores",
|
||||
"updateSuccess": "ES: Chore '{name}' updated successfully",
|
||||
"createSuccess": "ES: Chore '{name}' created successfully",
|
||||
"updateFailed": "ES: Failed to update chore",
|
||||
"createFailed": "ES: Failed to create chore",
|
||||
"deleteSuccess": "ES: Chore '{name}' deleted successfully",
|
||||
"deleteFailed": "ES: Failed to delete chore",
|
||||
"markedDone": "ES: {name} marked as done.",
|
||||
"markedNotDone": "ES: {name} marked as not done.",
|
||||
"statusUpdateFailed": "ES: Failed to update chore status."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "ES: Chore name is required.",
|
||||
"groupRequired": "ES: Please select a group for group chores.",
|
||||
"intervalRequired": "ES: Custom interval must be at least 1 day.",
|
||||
"dueDateRequired": "ES: Due date is required.",
|
||||
"invalidDueDate": "ES: Invalid due date format."
|
||||
},
|
||||
"unsavedChangesConfirmation": "ES: You have unsaved changes in the chore form. Are you sure you want to leave?"
|
||||
},
|
||||
"errorNotFoundPage": {
|
||||
"errorCode": "ES: 404",
|
||||
"errorMessage": "ES: Oops. Nothing here...",
|
||||
"goHomeButton": "ES: Go Home"
|
||||
},
|
||||
"groupDetailPage": {
|
||||
"loadingLabel": "ES: Loading group details...",
|
||||
"retryButton": "ES: Retry",
|
||||
"groupNotFound": "ES: Group not found or an error occurred.",
|
||||
"members": {
|
||||
"title": "ES: Group Members",
|
||||
"defaultRole": "ES: Member",
|
||||
"removeButton": "ES: Remove",
|
||||
"emptyState": "ES: No members found."
|
||||
},
|
||||
"invites": {
|
||||
"title": "ES: Invite Members",
|
||||
"regenerateButton": "ES: Regenerate Invite Code",
|
||||
"generateButton": "ES: Generate Invite Code",
|
||||
"activeCodeLabel": "ES: Current Active Invite Code:",
|
||||
"copyButtonLabel": "ES: Copy invite code",
|
||||
"copySuccess": "ES: Invite code copied to clipboard!",
|
||||
"emptyState": "ES: No active invite code. Click the button above to generate one.",
|
||||
"errors": {
|
||||
"newDataInvalid": "ES: New invite code data is invalid."
|
||||
}
|
||||
},
|
||||
"chores": {
|
||||
"title": "ES: Group Chores",
|
||||
"manageButton": "ES: Manage Chores",
|
||||
"duePrefix": "ES: Due:",
|
||||
"emptyState": "ES: No chores scheduled. Click \"Manage Chores\" to create some!"
|
||||
},
|
||||
"expenses": {
|
||||
"title": "ES: Group Expenses",
|
||||
"manageButton": "ES: Manage Expenses",
|
||||
"emptyState": "ES: No expenses recorded. Click \"Manage Expenses\" to add some!",
|
||||
"splitTypes": {
|
||||
"equal": "ES: Equal",
|
||||
"exactAmounts": "ES: Exact Amounts",
|
||||
"percentage": "ES: Percentage",
|
||||
"shares": "ES: Shares",
|
||||
"itemBased": "ES: Item Based"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"fetchDetailsFailed": "ES: Failed to fetch group details.",
|
||||
"fetchInviteFailed": "ES: Failed to fetch active invite code.",
|
||||
"generateInviteSuccess": "ES: New invite code generated successfully!",
|
||||
"generateInviteError": "ES: Failed to generate invite code.",
|
||||
"clipboardNotSupported": "ES: Clipboard not supported or no code to copy.",
|
||||
"copyInviteFailed": "ES: Failed to copy invite code.",
|
||||
"removeMemberSuccess": "ES: Member removed successfully",
|
||||
"removeMemberFailed": "ES: Failed to remove member"
|
||||
}
|
||||
}
|
||||
}
|
274
fe/src/i18n/fr.json
Normal file
274
fe/src/i18n/fr.json
Normal file
@ -0,0 +1,274 @@
|
||||
{
|
||||
"message": {
|
||||
"hello": "Bonjour"
|
||||
},
|
||||
"loginPage": {
|
||||
"emailLabel": "FR: Email",
|
||||
"passwordLabel": "FR: Password",
|
||||
"togglePasswordVisibilityLabel": "FR: Toggle password visibility",
|
||||
"loginButton": "FR: Login",
|
||||
"signupLink": "FR: Don't have an account? Sign up",
|
||||
"errors": {
|
||||
"emailRequired": "FR: Email is required",
|
||||
"emailInvalid": "FR: Invalid email format",
|
||||
"passwordRequired": "FR: Password is required",
|
||||
"loginFailed": "FR: Login failed. Please check your credentials."
|
||||
},
|
||||
"notifications": {
|
||||
"loginSuccess": "FR: Login successful"
|
||||
}
|
||||
},
|
||||
"listsPage": {
|
||||
"retryButton": "FR: Retry",
|
||||
"emptyState": {
|
||||
"noListsForGroup": "FR: No lists found for this group.",
|
||||
"noListsYet": "FR: You have no lists yet.",
|
||||
"personalGlobalInfo": "FR: Create a personal list or join a group to see shared lists.",
|
||||
"groupSpecificInfo": "FR: This group doesn't have any lists yet."
|
||||
},
|
||||
"createNewListButton": "FR: Create New List",
|
||||
"loadingLists": "FR: Loading lists...",
|
||||
"noDescription": "FR: No description",
|
||||
"addItemPlaceholder": "FR: Add new item...",
|
||||
"createCard": {
|
||||
"title": "FR: + Create a new list"
|
||||
},
|
||||
"pageTitle": {
|
||||
"forGroup": "FR: Lists for {groupName}",
|
||||
"forGroupId": "FR: Lists for Group {groupId}",
|
||||
"myLists": "FR: My Lists"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "FR: Failed to fetch lists."
|
||||
}
|
||||
},
|
||||
"groupsPage": {
|
||||
"retryButton": "FR: Retry",
|
||||
"emptyState": {
|
||||
"title": "FR: No Groups Yet!",
|
||||
"description": "FR: You are not a member of any groups yet. Create one or join using an invite code.",
|
||||
"createButton": "FR: Create New Group"
|
||||
},
|
||||
"groupCard": {
|
||||
"newListButton": "FR: List"
|
||||
},
|
||||
"createCard": {
|
||||
"title": "FR: + Group"
|
||||
},
|
||||
"joinGroup": {
|
||||
"title": "FR: Join a Group with Invite Code",
|
||||
"inputLabel": "FR: Enter Invite Code",
|
||||
"inputPlaceholder": "FR: Enter Invite Code",
|
||||
"joinButton": "FR: Join"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "FR: Create New Group",
|
||||
"closeButtonLabel": "FR: Close",
|
||||
"groupNameLabel": "FR: Group Name",
|
||||
"cancelButton": "FR: Cancel",
|
||||
"createButton": "FR: Create"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "FR: Failed to load groups",
|
||||
"groupNameRequired": "FR: Group name is required",
|
||||
"createFailed": "FR: Failed to create group. Please try again.",
|
||||
"inviteCodeRequired": "FR: Invite code is required",
|
||||
"joinFailed": "FR: Failed to join group. Please check the invite code and try again."
|
||||
},
|
||||
"notifications": {
|
||||
"groupCreatedSuccess": "FR: Group '{groupName}' created successfully.",
|
||||
"joinSuccessNamed": "FR: Successfully joined group '{groupName}'.",
|
||||
"joinSuccessGeneric": "FR: Successfully joined group.",
|
||||
"listCreatedSuccess": "FR: List '{listName}' created successfully."
|
||||
}
|
||||
},
|
||||
"authCallbackPage": {
|
||||
"redirecting": "FR: Redirecting...",
|
||||
"errors": {
|
||||
"authenticationFailed": "FR: Authentication failed"
|
||||
}
|
||||
},
|
||||
"choresPage": {
|
||||
"title": "FR: Chores",
|
||||
"tabs": {
|
||||
"overdue": "FR: Overdue",
|
||||
"today": "FR: Today",
|
||||
"upcoming": "FR: Upcoming",
|
||||
"allPending": "FR: All Pending",
|
||||
"completed": "FR: Completed"
|
||||
},
|
||||
"viewToggle": {
|
||||
"calendarLabel": "FR: Calendar View",
|
||||
"calendarText": "FR: Calendar",
|
||||
"listLabel": "FR: List View",
|
||||
"listText": "FR: List"
|
||||
},
|
||||
"newChoreButtonLabel": "FR: New Chore",
|
||||
"newChoreButtonText": "FR: New Chore",
|
||||
"loadingState": {
|
||||
"loadingChores": "FR: Loading chores..."
|
||||
},
|
||||
"calendar": {
|
||||
"prevMonthLabel": "FR: Previous month",
|
||||
"nextMonthLabel": "FR: Next month",
|
||||
"weekdays": {
|
||||
"sun": "FR: Sun",
|
||||
"mon": "FR: Mon",
|
||||
"tue": "FR: Tue",
|
||||
"wed": "FR: Wed",
|
||||
"thu": "FR: Thu",
|
||||
"fri": "FR: Fri",
|
||||
"sat": "FR: Sat"
|
||||
},
|
||||
"addChoreToDayLabel": "FR: Add chore to this day",
|
||||
"emptyState": "FR: No chores to display for this period."
|
||||
},
|
||||
"listView": {
|
||||
"choreTypePersonal": "FR: Personal",
|
||||
"choreTypeGroupFallback": "FR: Group",
|
||||
"completedDatePrefix": "FR: Completed:",
|
||||
"actions": {
|
||||
"doneTitle": "FR: Mark as Done",
|
||||
"doneText": "FR: Done",
|
||||
"undoTitle": "FR: Mark as Not Done",
|
||||
"undoText": "FR: Undo",
|
||||
"editTitle": "FR: Edit",
|
||||
"editLabel": "FR: Edit chore",
|
||||
"editText": "FR: Edit",
|
||||
"deleteTitle": "FR: Delete",
|
||||
"deleteLabel": "FR: Delete chore",
|
||||
"deleteText": "FR: Delete"
|
||||
},
|
||||
"emptyState": {
|
||||
"message": "FR: No chores in this view. Well done!",
|
||||
"viewAllButton": "FR: View All Pending"
|
||||
}
|
||||
},
|
||||
"choreModal": {
|
||||
"editTitle": "FR: Edit Chore",
|
||||
"newTitle": "FR: New Chore",
|
||||
"closeButtonLabel": "FR: Close modal",
|
||||
"nameLabel": "FR: Name",
|
||||
"namePlaceholder": "FR: Enter chore name",
|
||||
"typeLabel": "FR: Type",
|
||||
"typePersonal": "FR: Personal",
|
||||
"typeGroup": "FR: Group",
|
||||
"groupLabel": "FR: Group",
|
||||
"groupSelectDefault": "FR: Select a group",
|
||||
"descriptionLabel": "FR: Description",
|
||||
"descriptionPlaceholder": "FR: Add a description (optional)",
|
||||
"frequencyLabel": "FR: Frequency",
|
||||
"intervalLabel": "FR: Interval (days)",
|
||||
"intervalPlaceholder": "FR: e.g. 3",
|
||||
"dueDateLabel": "FR: Due Date",
|
||||
"quickDueDateToday": "FR: Today",
|
||||
"quickDueDateTomorrow": "FR: Tomorrow",
|
||||
"quickDueDateNextWeek": "FR: Next Week",
|
||||
"cancelButton": "FR: Cancel",
|
||||
"saveButton": "FR: Save"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "FR: Delete Chore",
|
||||
"confirmationText": "FR: Are you sure you want to delete this chore? This action cannot be undone.",
|
||||
"deleteButton": "FR: Delete"
|
||||
},
|
||||
"shortcutsModal": {
|
||||
"title": "FR: Keyboard Shortcuts",
|
||||
"descNewChore": "FR: New Chore",
|
||||
"descToggleView": "FR: Toggle View (List/Calendar)",
|
||||
"descToggleShortcuts": "FR: Show/Hide Shortcuts",
|
||||
"descCloseModal": "FR: Close any open Modal/Dialog"
|
||||
},
|
||||
"frequencyOptions": {
|
||||
"oneTime": "FR: One Time",
|
||||
"daily": "FR: Daily",
|
||||
"weekly": "FR: Weekly",
|
||||
"monthly": "FR: Monthly",
|
||||
"custom": "FR: Custom"
|
||||
},
|
||||
"formatters": {
|
||||
"noDueDate": "FR: No due date",
|
||||
"dueToday": "FR: Due Today",
|
||||
"dueTomorrow": "FR: Due Tomorrow",
|
||||
"overdueFull": "FR: Overdue: {date}",
|
||||
"dueFull": "FR: Due {date}",
|
||||
"invalidDate": "FR: Invalid Date"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "FR: Failed to load chores",
|
||||
"updateSuccess": "FR: Chore '{name}' updated successfully",
|
||||
"createSuccess": "FR: Chore '{name}' created successfully",
|
||||
"updateFailed": "FR: Failed to update chore",
|
||||
"createFailed": "FR: Failed to create chore",
|
||||
"deleteSuccess": "FR: Chore '{name}' deleted successfully",
|
||||
"deleteFailed": "FR: Failed to delete chore",
|
||||
"markedDone": "FR: {name} marked as done.",
|
||||
"markedNotDone": "FR: {name} marked as not done.",
|
||||
"statusUpdateFailed": "FR: Failed to update chore status."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "FR: Chore name is required.",
|
||||
"groupRequired": "FR: Please select a group for group chores.",
|
||||
"intervalRequired": "FR: Custom interval must be at least 1 day.",
|
||||
"dueDateRequired": "FR: Due date is required.",
|
||||
"invalidDueDate": "FR: Invalid due date format."
|
||||
},
|
||||
"unsavedChangesConfirmation": "FR: You have unsaved changes in the chore form. Are you sure you want to leave?"
|
||||
},
|
||||
"errorNotFoundPage": {
|
||||
"errorCode": "FR: 404",
|
||||
"errorMessage": "FR: Oops. Nothing here...",
|
||||
"goHomeButton": "FR: Go Home"
|
||||
},
|
||||
"groupDetailPage": {
|
||||
"loadingLabel": "FR: Loading group details...",
|
||||
"retryButton": "FR: Retry",
|
||||
"groupNotFound": "FR: Group not found or an error occurred.",
|
||||
"members": {
|
||||
"title": "FR: Group Members",
|
||||
"defaultRole": "FR: Member",
|
||||
"removeButton": "FR: Remove",
|
||||
"emptyState": "FR: No members found."
|
||||
},
|
||||
"invites": {
|
||||
"title": "FR: Invite Members",
|
||||
"regenerateButton": "FR: Regenerate Invite Code",
|
||||
"generateButton": "FR: Generate Invite Code",
|
||||
"activeCodeLabel": "FR: Current Active Invite Code:",
|
||||
"copyButtonLabel": "FR: Copy invite code",
|
||||
"copySuccess": "FR: Invite code copied to clipboard!",
|
||||
"emptyState": "FR: No active invite code. Click the button above to generate one.",
|
||||
"errors": {
|
||||
"newDataInvalid": "FR: New invite code data is invalid."
|
||||
}
|
||||
},
|
||||
"chores": {
|
||||
"title": "FR: Group Chores",
|
||||
"manageButton": "FR: Manage Chores",
|
||||
"duePrefix": "FR: Due:",
|
||||
"emptyState": "FR: No chores scheduled. Click \"Manage Chores\" to create some!"
|
||||
},
|
||||
"expenses": {
|
||||
"title": "FR: Group Expenses",
|
||||
"manageButton": "FR: Manage Expenses",
|
||||
"emptyState": "FR: No expenses recorded. Click \"Manage Expenses\" to add some!",
|
||||
"splitTypes": {
|
||||
"equal": "FR: Equal",
|
||||
"exactAmounts": "FR: Exact Amounts",
|
||||
"percentage": "FR: Percentage",
|
||||
"shares": "FR: Shares",
|
||||
"itemBased": "FR: Item Based"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"fetchDetailsFailed": "FR: Failed to fetch group details.",
|
||||
"fetchInviteFailed": "FR: Failed to fetch active invite code.",
|
||||
"generateInviteSuccess": "FR: New invite code generated successfully!",
|
||||
"generateInviteError": "FR: Failed to generate invite code.",
|
||||
"clipboardNotSupported": "FR: Clipboard not supported or no code to copy.",
|
||||
"copyInviteFailed": "FR: Failed to copy invite code.",
|
||||
"removeMemberSuccess": "FR: Member removed successfully",
|
||||
"removeMemberFailed": "FR: Failed to remove member"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
import enUS from './en-US';
|
||||
import en from './en.json'; // Changed from enUS and path
|
||||
import de from './de.json';
|
||||
import fr from './fr.json';
|
||||
import es from './es.json';
|
||||
|
||||
export default {
|
||||
'en-US': enUS
|
||||
'en': en, // Changed from 'en-US': enUS
|
||||
'de': de,
|
||||
'fr': fr,
|
||||
'es': es
|
||||
};
|
||||
|
@ -4,8 +4,8 @@ import * as Sentry from '@sentry/vue';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
// import { createI18n } from 'vue-i18n';
|
||||
// import messages from '@/i18n'; // Import from absolute path
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import messages from '@/i18n';
|
||||
|
||||
// Global styles
|
||||
import './assets/main.scss';
|
||||
@ -15,21 +15,22 @@ import { api, globalAxios } from '@/services/api'; // Renamed from boot/axios to
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
// Vue I18n setup (from your i18n boot file)
|
||||
// export type MessageLanguages = keyof typeof messages;
|
||||
// export type MessageSchema = (typeof messages)['en-US'];
|
||||
// // export type MessageLanguages = keyof typeof messages;
|
||||
// // export type MessageSchema = (typeof messages)['en-US'];
|
||||
|
||||
// declare module 'vue-i18n' {
|
||||
// export interface DefineLocaleMessage extends MessageSchema {}
|
||||
// // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
// export interface DefineDateTimeFormat {}
|
||||
// // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
// export interface DefineNumberFormat {}
|
||||
// }
|
||||
// const i18n = createI18n<{ message: MessageSchema }>({
|
||||
// locale: 'en-US',
|
||||
// fallbackLocale: 'en-US',
|
||||
// messages,
|
||||
// });
|
||||
// // declare module 'vue-i18n' {
|
||||
// // export interface DefineLocaleMessage extends MessageSchema {}
|
||||
// // // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
// // export interface DefineDateTimeFormat {}
|
||||
// // // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
// // export interface DefineNumberFormat {}
|
||||
// // }
|
||||
const i18n = createI18n({
|
||||
legacy: false, // Recommended for Vue 3
|
||||
locale: 'en', // Default locale
|
||||
fallbackLocale: 'en', // Fallback locale
|
||||
messages,
|
||||
});
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
@ -62,7 +63,7 @@ if (authStore.accessToken) {
|
||||
}
|
||||
|
||||
app.use(router);
|
||||
// app.use(i18n);
|
||||
app.use(i18n);
|
||||
|
||||
// Make API instance globally available (optional, prefer provide/inject or store)
|
||||
app.config.globalProperties.$api = api;
|
||||
|
@ -6,7 +6,7 @@
|
||||
<span /><span /><span />
|
||||
</div>
|
||||
<p v-else-if="error" class="text-error">{{ error }}</p>
|
||||
<p v-else>Redirecting...</p>
|
||||
<p v-else>{{ t('authCallbackPage.redirecting') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ -14,6 +14,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useNotificationStore } from '@/stores/notifications';
|
||||
@ -23,6 +24,8 @@ const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
@ -39,10 +42,10 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
await authStore.setTokens({ access_token: tokenToUse, refresh_token: refreshToken });
|
||||
notificationStore.addNotification({ message: 'Login successful', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('loginPage.notifications.loginSuccess'), type: 'success' });
|
||||
router.push('/');
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Authentication failed';
|
||||
error.value = err instanceof Error ? err.message : t('authCallbackPage.errors.authenticationFailed');
|
||||
notificationStore.addNotification({ message: error.value, type: 'error' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
@ -2,54 +2,54 @@
|
||||
<main class="neo-container page-padding">
|
||||
<div class="neo-list-header">
|
||||
<div class="header-left">
|
||||
<h1 class="neo-title">Chores</h1>
|
||||
<h1 class="neo-title">{{ t('choresPage.title') }}</h1>
|
||||
<div class="view-tabs" role="tablist">
|
||||
<button class="neo-tab-btn" :class="{ active: activeView === 'overdue' }" @click="activeView = 'overdue'"
|
||||
:disabled="isLoading" role="tab" :aria-selected="activeView === 'overdue'">
|
||||
<span class="material-icons">warning</span>
|
||||
Overdue
|
||||
{{ t('choresPage.tabs.overdue') }}
|
||||
<span v-if="counts.overdue > 0" class="neo-tab-count">{{ counts.overdue }}</span>
|
||||
</button>
|
||||
<button class="neo-tab-btn" :class="{ active: activeView === 'today' }" @click="activeView = 'today'"
|
||||
:disabled="isLoading" role="tab" :aria-selected="activeView === 'today'">
|
||||
<span class="material-icons">today</span>
|
||||
Today
|
||||
{{ t('choresPage.tabs.today') }}
|
||||
<span v-if="counts.today > 0" class="neo-tab-count">{{ counts.today }}</span>
|
||||
</button>
|
||||
<button class="neo-tab-btn" :class="{ active: activeView === 'upcoming' }" @click="activeView = 'upcoming'"
|
||||
:disabled="isLoading" role="tab" :aria-selected="activeView === 'upcoming'">
|
||||
<span class="material-icons">upcoming</span>
|
||||
Upcoming
|
||||
{{ t('choresPage.tabs.upcoming') }}
|
||||
</button>
|
||||
<button class="neo-tab-btn" :class="{ active: activeView === 'all' }" @click="activeView = 'all'"
|
||||
:disabled="isLoading" role="tab" :aria-selected="activeView === 'all'">
|
||||
<span class="material-icons">list</span>
|
||||
All Pending
|
||||
{{ t('choresPage.tabs.allPending') }}
|
||||
</button>
|
||||
<button class="neo-tab-btn" :class="{ active: activeView === 'completed' }" @click="activeView = 'completed'"
|
||||
:disabled="isLoading" role="tab" :aria-selected="activeView === 'completed'">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Completed
|
||||
{{ t('choresPage.tabs.completed') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="neo-view-toggle">
|
||||
<button class="neo-toggle-btn" :class="{ active: viewMode === 'calendar' }" @click="viewMode = 'calendar'"
|
||||
:disabled="isLoading" :aria-pressed="viewMode === 'calendar'" aria-label="Calendar View">
|
||||
:disabled="isLoading" :aria-pressed="viewMode === 'calendar'" :aria-label="t('choresPage.viewToggle.calendarLabel')">
|
||||
<span class="material-icons">calendar_month</span>
|
||||
<span class="btn-text hide-text-on-mobile">Calendar</span>
|
||||
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.viewToggle.calendarText') }}</span>
|
||||
</button>
|
||||
<button class="neo-toggle-btn" :class="{ active: viewMode === 'list' }" @click="viewMode = 'list'"
|
||||
:disabled="isLoading" :aria-pressed="viewMode === 'list'" aria-label="List View">
|
||||
:disabled="isLoading" :aria-pressed="viewMode === 'list'" :aria-label="t('choresPage.viewToggle.listLabel')">
|
||||
<span class="material-icons">view_list</span>
|
||||
<span class="btn-text hide-text-on-mobile">List</span>
|
||||
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.viewToggle.listText') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="neo-action-button" @click="openCreateChoreModal(null)" :disabled="isLoading"
|
||||
aria-label="New Chore">
|
||||
:aria-label="t('choresPage.newChoreButtonLabel')">
|
||||
<span class="material-icons">add</span>
|
||||
<span class="btn-text hide-text-on-mobile-sm">New Chore</span>
|
||||
<span class="btn-text hide-text-on-mobile-sm">{{ t('choresPage.newChoreButtonText') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -57,24 +57,24 @@
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="neo-loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading chores...</p>
|
||||
<p>{{ t('choresPage.loadingState.loadingChores') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Calendar View -->
|
||||
<div v-else-if="viewMode === 'calendar'" class="calendar-view">
|
||||
<div class="calendar-header">
|
||||
<button class="btn btn-icon" @click="previousMonth" aria-label="Previous month">
|
||||
<button class="btn btn-icon" @click="previousMonth" :aria-label="t('choresPage.calendar.prevMonthLabel')">
|
||||
<span class="material-icons">chevron_left</span>
|
||||
</button>
|
||||
<h2>{{ currentMonthYear }}</h2>
|
||||
<button class="btn btn-icon" @click="nextMonth" aria-label="Next month">
|
||||
<button class="btn btn-icon" @click="nextMonth" :aria-label="t('choresPage.calendar.nextMonthLabel')">
|
||||
<span class="material-icons">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="calendarDays.length > 0">
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-weekdays">
|
||||
<div v-for="day in weekDays" :key="day" class="weekday">{{ day }}</div>
|
||||
<div v-for="dayNameKey in weekDayKeys" :key="dayNameKey" class="weekday">{{ t(dayNameKey) }}</div>
|
||||
</div>
|
||||
<div class="calendar-days">
|
||||
<div v-for="(day, index) in calendarDays" :key="index" class="calendar-day" :class="{
|
||||
@ -86,7 +86,7 @@
|
||||
<div class="day-header">
|
||||
<span class="day-number">{{ day.date.getDate() }}</span>
|
||||
<button v-if="!day.isOtherMonth" class="add-chore-indicator"
|
||||
@click.stop="openCreateChoreModal(null, day.date)" aria-label="Add chore to this day">
|
||||
@click.stop="openCreateChoreModal(null, day.date)" :aria-label="t('choresPage.calendar.addChoreToDayLabel')">
|
||||
<span class="material-icons">add_circle_outline</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -107,7 +107,7 @@
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<span class="material-icons empty-icon">calendar_today</span>
|
||||
<p>No chores to display for this period.</p>
|
||||
<p>{{ t('choresPage.calendar.emptyState') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -123,7 +123,7 @@
|
||||
<h3>{{ chore.name }}</h3>
|
||||
<div class="chore-tags">
|
||||
<span class="chore-type-tag" :class="chore.type">
|
||||
{{ chore.type === 'personal' ? 'Personal' : getGroupName(chore.group_id) || 'Group' }}
|
||||
{{ chore.type === 'personal' ? t('choresPage.listView.choreTypePersonal') : getGroupName(chore.group_id) || t('choresPage.listView.choreTypeGroupFallback') }}
|
||||
</span>
|
||||
<span v-if="!chore.is_completed" class="chore-frequency-tag" :class="chore.frequency">
|
||||
{{ formatFrequency(chore.frequency) }}
|
||||
@ -137,7 +137,7 @@
|
||||
</div>
|
||||
<div v-else class="chore-completed-date">
|
||||
<span class="material-icons">check_circle_outline</span>
|
||||
Completed: {{ formatDate(chore.completed_at || chore.next_due_date) }}
|
||||
{{ t('choresPage.listView.completedDatePrefix') }} {{ formatDate(chore.completed_at || chore.next_due_date) }}
|
||||
</div>
|
||||
<div v-if="chore.description" class="chore-description">
|
||||
{{ chore.description }}
|
||||
@ -146,21 +146,21 @@
|
||||
</div>
|
||||
<div class="chore-card-actions">
|
||||
<button v-if="!chore.is_completed" class="btn btn-success btn-sm btn-complete"
|
||||
@click="toggleChoreCompletion(chore)" title="Mark as Done">
|
||||
<span class="material-icons">check_circle</span> Done
|
||||
@click="toggleChoreCompletion(chore)" :title="t('choresPage.listView.actions.doneTitle')">
|
||||
<span class="material-icons">check_circle</span> {{ t('choresPage.listView.actions.doneText') }}
|
||||
</button>
|
||||
<button v-else class="btn btn-warning btn-sm btn-undo" @click="toggleChoreCompletion(chore)"
|
||||
title="Mark as Not Done">
|
||||
<span class="material-icons">undo</span> Undo
|
||||
:title="t('choresPage.listView.actions.undoTitle')">
|
||||
<span class="material-icons">undo</span> {{ t('choresPage.listView.actions.undoText') }}
|
||||
</button>
|
||||
<button class="btn btn-icon" @click="openEditChoreModal(chore)" title="Edit" aria-label="Edit chore">
|
||||
<button class="btn btn-icon" @click="openEditChoreModal(chore)" :title="t('choresPage.listView.actions.editTitle')" :aria-label="t('choresPage.listView.actions.editLabel')">
|
||||
<span class="material-icons">edit</span>
|
||||
<span class="btn-text hide-text-on-mobile">Edit</span>
|
||||
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.listView.actions.editText') }}</span>
|
||||
</button>
|
||||
<button class="btn btn-icon btn-danger-icon" @click="confirmDeleteChore(chore)" title="Delete"
|
||||
aria-label="Delete chore">
|
||||
<button class="btn btn-icon btn-danger-icon" @click="confirmDeleteChore(chore)" :title="t('choresPage.listView.actions.deleteTitle')"
|
||||
:aria-label="t('choresPage.listView.actions.deleteLabel')">
|
||||
<span class="material-icons">delete</span>
|
||||
<span class="btn-text hide-text-on-mobile">Delete</span>
|
||||
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.listView.actions.deleteText') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -168,9 +168,8 @@
|
||||
</transition-group>
|
||||
<div v-if="!isLoading && filteredChores.length === 0" class="empty-state">
|
||||
<span class="material-icons empty-icon"> Rtask_alt</span>
|
||||
<p>No chores in this view. Well done!</p>
|
||||
<button v-if="activeView !== 'all'" class="btn btn-sm btn-outline" @click="activeView = 'all'">View All
|
||||
Pending</button>
|
||||
<p>{{ t('choresPage.listView.emptyState.message') }}</p>
|
||||
<button v-if="activeView !== 'all'" class="btn btn-sm btn-outline" @click="activeView = 'all'">{{ t('choresPage.listView.emptyState.viewAllButton') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -179,40 +178,40 @@
|
||||
aria-modal="true" :aria-labelledby="isEditing ? 'editChoreModalTitle' : 'newChoreModalTitle'">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h3 :id="isEditing ? 'editChoreModalTitle' : 'newChoreModalTitle'">{{ isEditing ? 'Edit Chore' : 'New Chore'
|
||||
<h3 :id="isEditing ? 'editChoreModalTitle' : 'newChoreModalTitle'">{{ isEditing ? t('choresPage.choreModal.editTitle') : t('choresPage.choreModal.newTitle')
|
||||
}}</h3>
|
||||
<button class="btn btn-icon" @click="showChoreModal = false" aria-label="Close modal">
|
||||
<button class="btn btn-icon" @click="showChoreModal = false" :aria-label="t('choresPage.choreModal.closeButtonLabel')">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form @submit.prevent="onSubmit" class="modal-form">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" v-model="choreForm.name" type="text" class="form-input" placeholder="Enter chore name"
|
||||
<label for="name">{{ t('choresPage.choreModal.nameLabel') }}</label>
|
||||
<input id="name" v-model="choreForm.name" type="text" class="form-input" :placeholder="t('choresPage.choreModal.namePlaceholder')"
|
||||
required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Type</label>
|
||||
<label>{{ t('choresPage.choreModal.typeLabel') }}</label>
|
||||
<div class="type-selector">
|
||||
<button type="button" class="type-btn" :class="{ active: choreForm.type === 'personal' }"
|
||||
@click="choreForm.type = 'personal'; choreForm.group_id = undefined"
|
||||
:aria-pressed="choreForm.type === 'personal' ? 'true' : 'false'">
|
||||
<span class="material-icons">person</span>
|
||||
Personal
|
||||
{{ t('choresPage.choreModal.typePersonal') }}
|
||||
</button>
|
||||
<button type="button" class="type-btn" :class="{ active: choreForm.type === 'group' }"
|
||||
@click="choreForm.type = 'group'" :aria-pressed="choreForm.type === 'group' ? 'true' : 'false'">
|
||||
<span class="material-icons">group</span>
|
||||
Group
|
||||
{{ t('choresPage.choreModal.typeGroup') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="choreForm.type === 'group'" class="form-group">
|
||||
<label for="group">Group</label>
|
||||
<label for="group">{{ t('choresPage.choreModal.groupLabel') }}</label>
|
||||
<select id="group" v-model="choreForm.group_id" class="form-input" required>
|
||||
<option :value="undefined" disabled>Select a group</option>
|
||||
<option :value="undefined" disabled>{{ t('choresPage.choreModal.groupSelectDefault') }}</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
@ -220,14 +219,14 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<label for="description">{{ t('choresPage.choreModal.descriptionLabel') }}</label>
|
||||
<textarea id="description" v-model="choreForm.description" class="form-input" rows="3"
|
||||
placeholder="Add a description (optional)"></textarea>
|
||||
:placeholder="t('choresPage.choreModal.descriptionPlaceholder')"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="frequency">Frequency</label>
|
||||
<label for="frequency">{{ t('choresPage.choreModal.frequencyLabel') }}</label>
|
||||
<select id="frequency" v-model="choreForm.frequency" class="form-input" required>
|
||||
<option v-for="option in frequencyOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
@ -236,27 +235,26 @@
|
||||
</div>
|
||||
|
||||
<div v-if="choreForm.frequency === 'custom'" class="form-group">
|
||||
<label for="interval">Interval (days)</label>
|
||||
<label for="interval">{{ t('choresPage.choreModal.intervalLabel') }}</label>
|
||||
<input id="interval" v-model.number="choreForm.custom_interval_days" type="number" class="form-input"
|
||||
min="1" placeholder="e.g. 3" required />
|
||||
min="1" :placeholder="t('choresPage.choreModal.intervalPlaceholder')" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dueDate">Due Date</label>
|
||||
<label for="dueDate">{{ t('choresPage.choreModal.dueDateLabel') }}</label>
|
||||
<div class="quick-due-dates">
|
||||
<button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('today')">Today</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('today')">{{ t('choresPage.choreModal.quickDueDateToday') }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline"
|
||||
@click="setQuickDueDate('tomorrow')">Tomorrow</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('next_week')">Next
|
||||
Week</button>
|
||||
@click="setQuickDueDate('tomorrow')">{{ t('choresPage.choreModal.quickDueDateTomorrow') }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('next_week')">{{ t('choresPage.choreModal.quickDueDateNextWeek') }}</button>
|
||||
</div>
|
||||
<input id="dueDate" v-model="choreForm.next_due_date" type="date" class="form-input" required />
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-neutral" @click="showChoreModal = false">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-neutral" @click="showChoreModal = false">{{ t('choresPage.choreModal.cancelButton') }}</button>
|
||||
<button type="submit" class="btn btn-primary">{{ t('choresPage.choreModal.saveButton') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -267,17 +265,17 @@
|
||||
aria-modal="true" aria-labelledby="deleteDialogTitle">
|
||||
<div class="modal-container delete-confirm">
|
||||
<div class="modal-header">
|
||||
<h3 id="deleteDialogTitle">Delete Chore</h3>
|
||||
<button class="btn btn-icon" @click="showDeleteDialog = false" aria-label="Close modal">
|
||||
<h3 id="deleteDialogTitle">{{ t('choresPage.deleteDialog.title') }}</h3>
|
||||
<button class="btn btn-icon" @click="showDeleteDialog = false" :aria-label="t('choresPage.choreModal.closeButtonLabel')">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this chore? This action cannot be undone.</p>
|
||||
<p>{{ t('choresPage.deleteDialog.confirmationText') }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-neutral" @click="showDeleteDialog = false">Cancel</button>
|
||||
<button class="btn btn-danger" @click="deleteChore">Delete</button>
|
||||
<button class="btn btn-neutral" @click="showDeleteDialog = false">{{ t('choresPage.choreModal.cancelButton') }}</button>
|
||||
<button class="btn btn-danger" @click="deleteChore">{{ t('choresPage.deleteDialog.deleteButton') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -287,8 +285,8 @@
|
||||
aria-modal="true" aria-labelledby="shortcutsModalTitle">
|
||||
<div class="modal-container shortcuts-modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="shortcutsModalTitle">Keyboard Shortcuts</h3>
|
||||
<button class="btn btn-icon" @click="showShortcutsModal = false" aria-label="Close modal">
|
||||
<h3 id="shortcutsModalTitle">{{ t('choresPage.shortcutsModal.title') }}</h3>
|
||||
<button class="btn btn-icon" @click="showShortcutsModal = false" :aria-label="t('choresPage.choreModal.closeButtonLabel')">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -298,25 +296,25 @@
|
||||
<div class="shortcut-keys">
|
||||
<kbd>Ctrl/Cmd</kbd> + <kbd>N</kbd>
|
||||
</div>
|
||||
<div class="shortcut-description">New Chore</div>
|
||||
<div class="shortcut-description">{{ t('choresPage.shortcutsModal.descNewChore') }}</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<div class="shortcut-keys">
|
||||
<kbd>Ctrl/Cmd</kbd> + <kbd>/</kbd>
|
||||
</div>
|
||||
<div class="shortcut-description">Toggle View</div>
|
||||
<div class="shortcut-description">{{ t('choresPage.shortcutsModal.descToggleView') }}</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<div class="shortcut-keys">
|
||||
<kbd>Ctrl/Cmd</kbd> + <kbd>?</kbd>
|
||||
</div>
|
||||
<div class="shortcut-description">Show/Hide Shortcuts</div>
|
||||
<div class="shortcut-description">{{ t('choresPage.shortcutsModal.descToggleShortcuts') }}</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<div class="shortcut-keys">
|
||||
<kbd>Esc</kbd>
|
||||
</div>
|
||||
<div class="shortcut-description">Close Modal</div>
|
||||
<div class="shortcut-description">{{ t('choresPage.shortcutsModal.descCloseModal') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -327,6 +325,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, onUnmounted, watch, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { format, startOfDay, addDays, addWeeks, isBefore, isEqual, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday as isTodayDate } from 'date-fns'
|
||||
import { choreService } from '../services/choreService'
|
||||
import { useNotificationStore } from '../stores/notifications'
|
||||
@ -335,6 +334,8 @@ import { useRoute } from 'vue-router'
|
||||
import { groupService } from '../services/groupService'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Types
|
||||
interface ChoreWithCompletion extends Chore {
|
||||
is_completed: boolean;
|
||||
@ -386,13 +387,13 @@ const getGroupName = (groupId?: number | null): string | undefined => {
|
||||
return groups.value.find(g => g.id === groupId)?.name;
|
||||
};
|
||||
|
||||
const frequencyOptions: { label: string; value: ChoreFrequency }[] = [
|
||||
{ label: 'One Time', value: 'one_time' },
|
||||
{ label: 'Daily', value: 'daily' },
|
||||
{ label: 'Weekly', value: 'weekly' },
|
||||
{ label: 'Monthly', value: 'monthly' },
|
||||
{ label: 'Custom', value: 'custom' }
|
||||
];
|
||||
const frequencyOptions = computed(() => [
|
||||
{ label: t('choresPage.frequencyOptions.oneTime'), value: 'one_time' as ChoreFrequency },
|
||||
{ label: t('choresPage.frequencyOptions.daily'), value: 'daily' as ChoreFrequency },
|
||||
{ label: t('choresPage.frequencyOptions.weekly'), value: 'weekly' as ChoreFrequency },
|
||||
{ label: t('choresPage.frequencyOptions.monthly'), value: 'monthly' as ChoreFrequency },
|
||||
{ label: t('choresPage.frequencyOptions.custom'), value: 'custom' as ChoreFrequency }
|
||||
]);
|
||||
|
||||
const isLoading = ref(true)
|
||||
|
||||
@ -409,7 +410,7 @@ const loadChores = async () => {
|
||||
cachedTimestamp.value = Date.now()
|
||||
} catch (error) {
|
||||
console.error('Failed to load all chores:', error)
|
||||
notificationStore.addNotification({ message: 'Failed to load chores', type: 'error' })
|
||||
notificationStore.addNotification({ message: t('choresPage.notifications.loadFailed'), type: 'error' })
|
||||
if (!cachedChores.value || cachedChores.value.length === 0) chores.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@ -419,8 +420,8 @@ const loadChores = async () => {
|
||||
const viewMode = ref<'calendar' | 'list'>('calendar')
|
||||
const currentDate = ref(new Date())
|
||||
|
||||
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
// For smaller screens, you might use: const weekDays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
const weekDayKeys = ['choresPage.calendar.weekdays.sun', 'choresPage.calendar.weekdays.mon', 'choresPage.calendar.weekdays.tue', 'choresPage.calendar.weekdays.wed', 'choresPage.calendar.weekdays.thu', 'choresPage.calendar.weekdays.fri', 'choresPage.calendar.weekdays.sat']
|
||||
// For smaller screens, you might use: const weekDayKeys = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
|
||||
|
||||
const currentMonthYear = computed(() => {
|
||||
@ -523,10 +524,10 @@ const onSubmit = async () => {
|
||||
|
||||
if (isEditing.value && selectedChore.value) {
|
||||
await choreService.updateChore(selectedChore.value.id, choreDataSubmit as ChoreUpdate)
|
||||
notificationMessage = `Chore '${choreForm.value.name}' updated successfully`
|
||||
notificationMessage = t('choresPage.notifications.updateSuccess', { name: choreForm.value.name })
|
||||
} else {
|
||||
await choreService.createChore(choreDataSubmit as ChoreCreate)
|
||||
notificationMessage = `Chore '${choreForm.value.name}' created successfully`
|
||||
notificationMessage = t('choresPage.notifications.createSuccess', { name: choreForm.value.name })
|
||||
}
|
||||
|
||||
notificationStore.addNotification({ message: notificationMessage, type: 'success' })
|
||||
@ -535,7 +536,7 @@ const onSubmit = async () => {
|
||||
await loadChores()
|
||||
} catch (error) {
|
||||
console.error('Failed to save chore:', error)
|
||||
notificationStore.addNotification({ message: `Failed to ${isEditing.value ? 'update' : 'create'} chore`, type: 'error' })
|
||||
notificationStore.addNotification({ message: t(isEditing.value ? 'choresPage.notifications.updateFailed' : 'choresPage.notifications.createFailed'), type: 'error' })
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@ -556,11 +557,11 @@ const deleteChore = async () => {
|
||||
selectedChore.value.group_id ?? undefined
|
||||
);
|
||||
showDeleteDialog.value = false;
|
||||
notificationStore.addNotification({ message: `Chore '${selectedChore.value.name}' deleted successfully`, type: 'success' })
|
||||
notificationStore.addNotification({ message: t('choresPage.notifications.deleteSuccess', { name: selectedChore.value.name }), type: 'success' })
|
||||
await loadChores()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete chore:', error);
|
||||
notificationStore.addNotification({ message: 'Failed to delete chore', type: 'error' })
|
||||
notificationStore.addNotification({ message: t('choresPage.notifications.deleteFailed'), type: 'error' })
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
selectedChore.value = null;
|
||||
@ -686,19 +687,19 @@ const getDueDateClass = (chore: ChoreWithCompletion) => {
|
||||
}
|
||||
|
||||
const formatDueDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'No due date';
|
||||
if (!dateString) return t('choresPage.formatters.noDueDate');
|
||||
try {
|
||||
const dueDate = startOfDay(new Date(dateString.replace(/-/g, '/')));
|
||||
if (isEqual(dueDate, today.value)) return 'Due Today';
|
||||
if (isEqual(dueDate, today.value)) return t('choresPage.formatters.dueToday');
|
||||
|
||||
const tomorrow = addDays(today.value, 1);
|
||||
if (isEqual(dueDate, tomorrow)) return 'Due Tomorrow';
|
||||
if (isEqual(dueDate, tomorrow)) return t('choresPage.formatters.dueTomorrow');
|
||||
|
||||
if (isBefore(dueDate, today.value)) return `Overdue: ${formatDate(dateString)}`;
|
||||
if (isBefore(dueDate, today.value)) return t('choresPage.formatters.overdueFull', { date: formatDate(dateString) });
|
||||
|
||||
return `Due ${formatDate(dateString)}`;
|
||||
return t('choresPage.formatters.dueFull', { date: formatDate(dateString) });
|
||||
} catch {
|
||||
return 'Invalid date'
|
||||
return t('choresPage.formatters.invalidDate')
|
||||
}
|
||||
}
|
||||
|
||||
@ -723,7 +724,7 @@ const toggleChoreCompletion = async (choreToToggle: ChoreWithCompletion) => {
|
||||
} as ChoreUpdate);
|
||||
|
||||
notificationStore.addNotification({
|
||||
message: `${choreToToggle.name} marked as ${choreToToggle.is_completed ? 'done' : 'not done'}.`,
|
||||
message: t(choreToToggle.is_completed ? 'choresPage.notifications.markedDone' : 'choresPage.notifications.markedNotDone', { name: choreToToggle.name }),
|
||||
type: choreToToggle.is_completed ? 'success' : 'info'
|
||||
});
|
||||
} catch (error) {
|
||||
@ -734,7 +735,7 @@ const toggleChoreCompletion = async (choreToToggle: ChoreWithCompletion) => {
|
||||
if (index !== -1) chores.value.splice(index, 1, { ...choreToToggle });
|
||||
cachedChores.value = [...chores.value];
|
||||
|
||||
notificationStore.addNotification({ message: 'Failed to update chore status.', type: 'error' });
|
||||
notificationStore.addNotification({ message: t('choresPage.notifications.statusUpdateFailed'), type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -783,21 +784,21 @@ onUnmounted(() => {
|
||||
|
||||
const validateForm = () => {
|
||||
if (!choreForm.value.name.trim()) {
|
||||
notificationStore.addNotification({ message: 'Chore name is required.', type: 'error' }); return false;
|
||||
notificationStore.addNotification({ message: t('choresPage.validation.nameRequired'), type: 'error' }); return false;
|
||||
}
|
||||
if (choreForm.value.type === 'group' && !choreForm.value.group_id) {
|
||||
notificationStore.addNotification({ message: 'Please select a group for group chores.', type: 'error' }); return false;
|
||||
notificationStore.addNotification({ message: t('choresPage.validation.groupRequired'), type: 'error' }); return false;
|
||||
}
|
||||
if (choreForm.value.frequency === 'custom' && (!choreForm.value.custom_interval_days || choreForm.value.custom_interval_days < 1)) {
|
||||
notificationStore.addNotification({ message: 'Custom interval must be at least 1 day.', type: 'error' }); return false;
|
||||
notificationStore.addNotification({ message: t('choresPage.validation.intervalRequired'), type: 'error' }); return false;
|
||||
}
|
||||
if (!choreForm.value.next_due_date) {
|
||||
notificationStore.addNotification({ message: 'Due date is required.', type: 'error' }); return false;
|
||||
notificationStore.addNotification({ message: t('choresPage.validation.dueDateRequired'), type: 'error' }); return false;
|
||||
}
|
||||
try {
|
||||
new Date(choreForm.value.next_due_date.replace(/-/g, '/')); // check if valid date
|
||||
} catch {
|
||||
notificationStore.addNotification({ message: 'Invalid due date format.', type: 'error' }); return false;
|
||||
notificationStore.addNotification({ message: t('choresPage.validation.invalidDueDate'), type: 'error' }); return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -851,7 +852,7 @@ watch(showChoreModal, (isOpen) => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (hasUnsavedChanges.value && showChoreModal.value) { // Only prompt if modal is open with changes
|
||||
const confirmLeave = window.confirm('You have unsaved changes in the chore form. Are you sure you want to leave?')
|
||||
const confirmLeave = window.confirm(t('choresPage.unsavedChangesConfirmation'))
|
||||
if (!confirmLeave) {
|
||||
return false // This typically doesn't prevent component unmount in Vue 3 composition API directly
|
||||
// but is a common pattern. For SPA, router guards are better.
|
||||
|
@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<div class="fullscreen-error text-center">
|
||||
<div>
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-message">Oops. Nothing here...</div>
|
||||
<router-link to="/" class="btn btn-primary mt-3">Go Home</router-link>
|
||||
<div class="error-code">{{ t('errorNotFoundPage.errorCode') }}</div>
|
||||
<div class="error-message">{{ t('errorNotFoundPage.errorMessage') }}</div>
|
||||
<router-link to="/" class="btn btn-primary mt-3">{{ t('errorNotFoundPage.goHomeButton') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
// No script logic needed for this simple page
|
||||
</script>
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<main class="container page-padding">
|
||||
<div v-if="loading" class="text-center">
|
||||
<VSpinner label="Loading group details..." />
|
||||
<VSpinner :label="t('groupDetailPage.loadingLabel')" />
|
||||
</div>
|
||||
<VAlert v-else-if="error" type="error" :message="error" class="mb-3">
|
||||
<template #actions>
|
||||
<VButton variant="danger" size="sm" @click="fetchGroupDetails">Retry</VButton>
|
||||
<VButton variant="danger" size="sm" @click="fetchGroupDetails">{{ t('groupDetailPage.retryButton') }}</VButton>
|
||||
</template>
|
||||
</VAlert>
|
||||
<div v-else-if="group">
|
||||
@ -14,42 +14,42 @@
|
||||
<div class="neo-grid">
|
||||
<!-- Group Members Section -->
|
||||
<VCard>
|
||||
<template #header><VHeading level="3">Group Members</VHeading></template>
|
||||
<template #header><VHeading level="3">{{ t('groupDetailPage.members.title') }}</VHeading></template>
|
||||
<VList v-if="group.members && group.members.length > 0">
|
||||
<VListItem v-for="member in group.members" :key="member.id" class="flex justify-between items-center">
|
||||
<div class="neo-member-info">
|
||||
<span class="neo-member-name">{{ member.email }}</span>
|
||||
<VBadge :text="member.role || 'Member'" :variant="member.role?.toLowerCase() === 'owner' ? 'primary' : 'secondary'" />
|
||||
<VBadge :text="member.role || t('groupDetailPage.members.defaultRole')" :variant="member.role?.toLowerCase() === 'owner' ? 'primary' : 'secondary'" />
|
||||
</div>
|
||||
<VButton v-if="canRemoveMember(member)" variant="danger" size="sm" @click="removeMember(member.id)" :disabled="removingMember === member.id">
|
||||
<VSpinner v-if="removingMember === member.id" size="sm"/> Remove
|
||||
<VSpinner v-if="removingMember === member.id" size="sm"/> {{ t('groupDetailPage.members.removeButton') }}
|
||||
</VButton>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<div v-else class="text-center py-4">
|
||||
<VIcon name="users" size="lg" class="opacity-50 mb-2" />
|
||||
<p>No members found.</p>
|
||||
<p>{{ t('groupDetailPage.members.emptyState') }}</p>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<!-- Invite Members Section -->
|
||||
<VCard>
|
||||
<template #header><VHeading level="3">Invite Members</VHeading></template>
|
||||
<template #header><VHeading level="3">{{ t('groupDetailPage.invites.title') }}</VHeading></template>
|
||||
<VButton variant="primary" class="w-full" @click="generateInviteCode" :disabled="generatingInvite">
|
||||
<VSpinner v-if="generatingInvite" size="sm"/> {{ inviteCode ? 'Regenerate Invite Code' : 'Generate Invite Code' }}
|
||||
<VSpinner v-if="generatingInvite" size="sm"/> {{ inviteCode ? t('groupDetailPage.invites.regenerateButton') : t('groupDetailPage.invites.generateButton') }}
|
||||
</VButton>
|
||||
<div v-if="inviteCode" class="neo-invite-code mt-3">
|
||||
<VFormField label="Current Active Invite Code:" :label-sr-only="false">
|
||||
<VFormField :label="t('groupDetailPage.invites.activeCodeLabel')" :label-sr-only="false">
|
||||
<div class="flex items-center gap-2">
|
||||
<VInput id="inviteCodeInput" :model-value="inviteCode" readonly class="flex-grow" />
|
||||
<VButton variant="neutral" :icon-only="true" iconLeft="clipboard" @click="copyInviteCodeHandler" aria-label="Copy invite code" />
|
||||
<VButton variant="neutral" :icon-only="true" iconLeft="clipboard" @click="copyInviteCodeHandler" :aria-label="t('groupDetailPage.invites.copyButtonLabel')" />
|
||||
</div>
|
||||
</VFormField>
|
||||
<p v-if="copySuccess" class="text-sm text-green-600 mt-1">Invite code copied to clipboard!</p>
|
||||
<p v-if="copySuccess" class="text-sm text-green-600 mt-1">{{ t('groupDetailPage.invites.copySuccess') }}</p>
|
||||
</div>
|
||||
<div v-else class="text-center py-4 mt-3">
|
||||
<VIcon name="link" size="lg" class="opacity-50 mb-2" />
|
||||
<p>No active invite code. Click the button above to generate one.</p>
|
||||
<p>{{ t('groupDetailPage.invites.emptyState') }}</p>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
@ -63,9 +63,9 @@
|
||||
<VCard class="mt-4">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<VHeading level="3">Group Chores</VHeading>
|
||||
<VHeading level="3">{{ t('groupDetailPage.chores.title') }}</VHeading>
|
||||
<VButton :to="`/groups/${groupId}/chores`" variant="primary">
|
||||
<span class="material-icons" style="margin-right: 0.25em;">cleaning_services</span> Manage Chores
|
||||
<span class="material-icons" style="margin-right: 0.25em;">cleaning_services</span> {{ t('groupDetailPage.chores.manageButton') }}
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -73,14 +73,14 @@
|
||||
<VListItem v-for="chore in upcomingChores" :key="chore.id" class="flex justify-between items-center">
|
||||
<div class="neo-chore-info">
|
||||
<span class="neo-chore-name">{{ chore.name }}</span>
|
||||
<span class="neo-chore-due">Due: {{ formatDate(chore.next_due_date) }}</span>
|
||||
<span class="neo-chore-due">{{ t('groupDetailPage.chores.duePrefix') }} {{ formatDate(chore.next_due_date) }}</span>
|
||||
</div>
|
||||
<VBadge :text="formatFrequency(chore.frequency)" :variant="getFrequencyBadgeVariant(chore.frequency)" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
<div v-else class="text-center py-4">
|
||||
<VIcon name="cleaning_services" size="lg" class="opacity-50 mb-2" /> {/* Assuming cleaning_services is a valid VIcon name or will be added */}
|
||||
<p>No chores scheduled. Click "Manage Chores" to create some!</p>
|
||||
<p>{{ t('groupDetailPage.chores.emptyState') }}</p>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
@ -88,9 +88,9 @@
|
||||
<VCard class="mt-4">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<VHeading level="3">Group Expenses</VHeading>
|
||||
<VHeading level="3">{{ t('groupDetailPage.expenses.title') }}</VHeading>
|
||||
<VButton :to="`/groups/${groupId}/expenses`" variant="primary">
|
||||
<span class="material-icons" style="margin-right: 0.25em;">payments</span> Manage Expenses
|
||||
<span class="material-icons" style="margin-right: 0.25em;">payments</span> {{ t('groupDetailPage.expenses.manageButton') }}
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -108,18 +108,19 @@
|
||||
</VList>
|
||||
<div v-else class="text-center py-4">
|
||||
<VIcon name="payments" size="lg" class="opacity-50 mb-2" /> {/* Assuming payments is a valid VIcon name or will be added */}
|
||||
<p>No expenses recorded. Click "Manage Expenses" to add some!</p>
|
||||
<p>{{ t('groupDetailPage.expenses.emptyState') }}</p>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
</div>
|
||||
|
||||
<VAlert v-else type="info" message="Group not found or an error occurred." />
|
||||
<VAlert v-else type="info" :message="t('groupDetailPage.groupNotFound')" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
// import { useRoute } from 'vue-router';
|
||||
import { apiClient, API_ENDPOINTS } from '@/config/api'; // Assuming path
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
@ -141,6 +142,8 @@ import VInput from '@/components/valerie/VInput.vue';
|
||||
import VFormField from '@/components/valerie/VFormField.vue';
|
||||
import VIcon from '@/components/valerie/VIcon.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Group {
|
||||
id: string | number;
|
||||
name: string;
|
||||
@ -238,13 +241,13 @@ const generateInviteCode = async () => {
|
||||
if (response.data && response.data.code) {
|
||||
inviteCode.value = response.data.code;
|
||||
inviteExpiresAt.value = response.data.expires_at; // Update with new expiry
|
||||
notificationStore.addNotification({ message: 'New invite code generated successfully!', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('groupDetailPage.notifications.generateInviteSuccess'), type: 'success' });
|
||||
} else {
|
||||
// Should not happen if POST is successful and returns the code
|
||||
throw new Error('New invite code data is invalid.');
|
||||
throw new Error(t('groupDetailPage.invites.errors.newDataInvalid'));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to generate invite code.';
|
||||
const message = err instanceof Error ? err.message : t('groupDetailPage.notifications.generateInviteError');
|
||||
console.error('Error generating invite code:', err);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
} finally {
|
||||
@ -254,7 +257,7 @@ const generateInviteCode = async () => {
|
||||
|
||||
const copyInviteCodeHandler = async () => {
|
||||
if (!clipboardIsSupported.value || !inviteCode.value) {
|
||||
notificationStore.addNotification({ message: 'Clipboard not supported or no code to copy.', type: 'warning' });
|
||||
notificationStore.addNotification({ message: t('groupDetailPage.notifications.clipboardNotSupported'), type: 'warning' });
|
||||
return;
|
||||
}
|
||||
await copy(inviteCode.value);
|
||||
@ -264,7 +267,7 @@ const copyInviteCodeHandler = async () => {
|
||||
// Optionally, notify success via store if preferred over inline message
|
||||
// notificationStore.addNotification({ message: 'Invite code copied!', type: 'info' });
|
||||
} else {
|
||||
notificationStore.addNotification({ message: 'Failed to copy invite code.', type: 'error' });
|
||||
notificationStore.addNotification({ message: t('groupDetailPage.notifications.copyInviteFailed'), type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -283,11 +286,11 @@ const removeMember = async (memberId: number) => {
|
||||
// Refresh group details to update the members list
|
||||
await fetchGroupDetails();
|
||||
notificationStore.addNotification({
|
||||
message: 'Member removed successfully',
|
||||
message: t('groupDetailPage.notifications.removeMemberSuccess'),
|
||||
type: 'success'
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to remove member';
|
||||
const message = err instanceof Error ? err.message : t('groupDetailPage.notifications.removeMemberFailed');
|
||||
console.error('Error removing member:', err);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
} finally {
|
||||
@ -314,15 +317,15 @@ const formatDate = (date: string) => {
|
||||
}
|
||||
|
||||
const formatFrequency = (frequency: ChoreFrequency) => {
|
||||
const options = {
|
||||
one_time: 'One Time',
|
||||
daily: 'Daily',
|
||||
weekly: 'Weekly',
|
||||
monthly: 'Monthly',
|
||||
custom: 'Custom'
|
||||
}
|
||||
return options[frequency] || frequency
|
||||
}
|
||||
const options: Record<ChoreFrequency, string> = {
|
||||
one_time: t('choresPage.frequencyOptions.oneTime'), // Reusing existing keys
|
||||
daily: t('choresPage.frequencyOptions.daily'),
|
||||
weekly: t('choresPage.frequencyOptions.weekly'),
|
||||
monthly: t('choresPage.frequencyOptions.monthly'),
|
||||
custom: t('choresPage.frequencyOptions.custom')
|
||||
};
|
||||
return options[frequency] || frequency;
|
||||
};
|
||||
|
||||
const getFrequencyBadgeVariant = (frequency: ChoreFrequency): string => {
|
||||
const colorMap: Record<ChoreFrequency, string> = {
|
||||
@ -351,10 +354,14 @@ const formatAmount = (amount: string) => {
|
||||
}
|
||||
|
||||
const formatSplitType = (type: string) => {
|
||||
return type.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||
).join(' ')
|
||||
}
|
||||
// Assuming 'type' is like 'exact_amounts' or 'item_based'
|
||||
const key = `groupDetailPage.expenses.splitTypes.${type.toLowerCase().replace(/_([a-z])/g, g => g[1].toUpperCase())}`;
|
||||
// This creates keys like 'groupDetailPage.expenses.splitTypes.exactAmounts'
|
||||
// Check if translation exists, otherwise fallback to a simple formatted string
|
||||
// For simplicity in this subtask, we'll assume keys will be added.
|
||||
// A more robust solution would check i18n.global.te(key) or have a fallback.
|
||||
return t(key);
|
||||
};
|
||||
|
||||
const getSplitTypeBadgeVariant = (type: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
|
@ -9,20 +9,20 @@
|
||||
</svg>
|
||||
{{ fetchError }}
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-danger" @click="fetchGroups">Retry</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" @click="fetchGroups">{{ t('groupsPage.retryButton') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="groups.length === 0" class="card empty-state-card">
|
||||
<svg class="icon icon-lg" aria-hidden="true">
|
||||
<use xlink:href="#icon-clipboard" />
|
||||
</svg>
|
||||
<h3>No Groups Yet!</h3>
|
||||
<p>You are not a member of any groups yet. Create one or join using an invite code.</p>
|
||||
<h3>{{ t('groupsPage.emptyState.title') }}</h3>
|
||||
<p>{{ t('groupsPage.emptyState.description') }}</p>
|
||||
<button class="btn btn-primary mt-2" @click="openCreateGroupDialog">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-plus" />
|
||||
</svg>
|
||||
Create New Group
|
||||
{{ t('groupsPage.emptyState.createButton') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -35,12 +35,12 @@
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-plus" />
|
||||
</svg>
|
||||
List
|
||||
{{ t('groupsPage.groupCard.newListButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="neo-create-group-card" @click="openCreateGroupDialog">
|
||||
+ Group
|
||||
{{ t('groupsPage.createCard.title') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -50,20 +50,20 @@
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-user" />
|
||||
</svg>
|
||||
Join a Group with Invite Code
|
||||
{{ t('groupsPage.joinGroup.title') }}
|
||||
</h3>
|
||||
<span class="expand-icon" aria-hidden="true">▼</span>
|
||||
</summary>
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="handleJoinGroup" class="flex items-center" style="gap: 0.5rem;">
|
||||
<div class="form-group flex-grow" style="margin-bottom: 0;">
|
||||
<label for="joinInviteCodeInput" class="sr-only">Enter Invite Code</label>
|
||||
<label for="joinInviteCodeInput" class="sr-only">{{ t('groupsPage.joinGroup.inputLabel') }}</label>
|
||||
<input type="text" id="joinInviteCodeInput" v-model="inviteCodeToJoin" class="form-input"
|
||||
placeholder="Enter Invite Code" required ref="joinInviteCodeInputRef" />
|
||||
:placeholder="t('groupsPage.joinGroup.inputPlaceholder')" required ref="joinInviteCodeInputRef" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary" :disabled="joiningGroup">
|
||||
<span v-if="joiningGroup" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
|
||||
Join
|
||||
{{ t('groupsPage.joinGroup.joinButton') }}
|
||||
</button>
|
||||
</form>
|
||||
<p v-if="joinGroupFormError" class="form-error-text mt-1">{{ joinGroupFormError }}</p>
|
||||
@ -76,8 +76,8 @@
|
||||
<div class="modal-container" ref="createGroupModalRef" role="dialog" aria-modal="true"
|
||||
aria-labelledby="createGroupTitle">
|
||||
<div class="modal-header">
|
||||
<h3 id="createGroupTitle">Create New Group</h3>
|
||||
<button class="close-button" @click="closeCreateGroupDialog" aria-label="Close">
|
||||
<h3 id="createGroupTitle">{{ t('groupsPage.createDialog.title') }}</h3>
|
||||
<button class="close-button" @click="closeCreateGroupDialog" :aria-label="t('groupsPage.createDialog.closeButtonLabel')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-close" />
|
||||
</svg>
|
||||
@ -86,17 +86,17 @@
|
||||
<form @submit.prevent="handleCreateGroup">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="newGroupNameInput" class="form-label">Group Name</label>
|
||||
<label for="newGroupNameInput" class="form-label">{{ t('groupsPage.createDialog.groupNameLabel') }}</label>
|
||||
<input type="text" id="newGroupNameInput" v-model="newGroupName" class="form-input" required
|
||||
ref="newGroupNameInputRef" />
|
||||
<p v-if="createGroupFormError" class="form-error-text">{{ createGroupFormError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-neutral" @click="closeCreateGroupDialog">Cancel</button>
|
||||
<button type="button" class="btn btn-neutral" @click="closeCreateGroupDialog">{{ t('groupsPage.createDialog.cancelButton') }}</button>
|
||||
<button type="submit" class="btn btn-primary ml-2" :disabled="creatingGroup">
|
||||
<span v-if="creatingGroup" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
|
||||
Create
|
||||
{{ t('groupsPage.createDialog.createButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -110,6 +110,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { apiClient, API_ENDPOINTS } from '@/config/api';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
@ -119,6 +120,8 @@ import CreateListModal from '@/components/CreateListModal.vue';
|
||||
import VButton from '@/components/valerie/VButton.vue';
|
||||
import VIcon from '@/components/valerie/VIcon.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
@ -172,7 +175,7 @@ const fetchGroups = async () => {
|
||||
cachedGroups.value = response.data;
|
||||
cachedTimestamp.value = Date.now();
|
||||
} catch (err) {
|
||||
fetchError.value = err instanceof Error ? err.message : 'Failed to load groups';
|
||||
fetchError.value = err instanceof Error ? err.message : t('groupsPage.errors.fetchFailed');
|
||||
// If we have cached data, keep showing it even if refresh failed
|
||||
if (cachedGroups.value.length === 0) {
|
||||
groups.value = [];
|
||||
@ -197,7 +200,7 @@ onClickOutside(createGroupModalRef, closeCreateGroupDialog);
|
||||
|
||||
const handleCreateGroup = async () => {
|
||||
if (!newGroupName.value.trim()) {
|
||||
createGroupFormError.value = 'Group name is required';
|
||||
createGroupFormError.value = t('groupsPage.errors.groupNameRequired');
|
||||
newGroupNameInputRef.value?.focus();
|
||||
return;
|
||||
}
|
||||
@ -211,7 +214,7 @@ const handleCreateGroup = async () => {
|
||||
if (newGroup && newGroup.id && newGroup.name) {
|
||||
groups.value.push(newGroup);
|
||||
closeCreateGroupDialog();
|
||||
notificationStore.addNotification({ message: `Group '${newGroup.name}' created successfully.`, type: 'success' });
|
||||
notificationStore.addNotification({ message: t('groupsPage.notifications.groupCreatedSuccess', { groupName: newGroup.name }), type: 'success' });
|
||||
// Update cache
|
||||
cachedGroups.value = groups.value;
|
||||
cachedTimestamp.value = Date.now();
|
||||
@ -219,7 +222,7 @@ const handleCreateGroup = async () => {
|
||||
throw new Error('Invalid data received from server.');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create group. Please try again.';
|
||||
const message = error instanceof Error ? error.message : t('groupsPage.errors.createFailed');
|
||||
createGroupFormError.value = message;
|
||||
console.error('Error creating group:', error);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
@ -230,7 +233,7 @@ const handleCreateGroup = async () => {
|
||||
|
||||
const handleJoinGroup = async () => {
|
||||
if (!inviteCodeToJoin.value.trim()) {
|
||||
joinGroupFormError.value = 'Invite code is required';
|
||||
joinGroupFormError.value = t('groupsPage.errors.inviteCodeRequired');
|
||||
joinInviteCodeInputRef.value?.focus();
|
||||
return;
|
||||
}
|
||||
@ -245,7 +248,7 @@ const handleJoinGroup = async () => {
|
||||
groups.value.push(joinedGroup);
|
||||
}
|
||||
inviteCodeToJoin.value = '';
|
||||
notificationStore.addNotification({ message: `Successfully joined group '${joinedGroup.name}'.`, type: 'success' });
|
||||
notificationStore.addNotification({ message: t('groupsPage.notifications.joinSuccessNamed', { groupName: joinedGroup.name }), type: 'success' });
|
||||
// Update cache
|
||||
cachedGroups.value = groups.value;
|
||||
cachedTimestamp.value = Date.now();
|
||||
@ -253,10 +256,10 @@ const handleJoinGroup = async () => {
|
||||
// If API returns only success message, re-fetch groups
|
||||
await fetchGroups(); // Refresh the list of groups
|
||||
inviteCodeToJoin.value = '';
|
||||
notificationStore.addNotification({ message: `Successfully joined group.`, type: 'success' });
|
||||
notificationStore.addNotification({ message: t('groupsPage.notifications.joinSuccessGeneric'), type: 'success' });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to join group. Please check the invite code and try again.';
|
||||
const message = error instanceof Error ? error.message : t('groupsPage.errors.joinFailed');
|
||||
joinGroupFormError.value = message;
|
||||
console.error('Error joining group:', error);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
@ -285,7 +288,7 @@ const openCreateListDialog = (group: Group) => {
|
||||
|
||||
const onListCreated = (newList: any) => {
|
||||
notificationStore.addNotification({
|
||||
message: `List '${newList.name}' created successfully.`,
|
||||
message: t('groupsPage.notifications.listCreatedSuccess', { listName: newList.name }),
|
||||
type: 'success'
|
||||
});
|
||||
// Optionally refresh the groups list to show the new list
|
||||
|
@ -4,25 +4,25 @@
|
||||
|
||||
<VAlert v-if="error" type="error" :message="error" class="mb-3" :closable="false">
|
||||
<template #actions>
|
||||
<VButton variant="danger" size="sm" @click="fetchListsAndGroups">Retry</VButton>
|
||||
<VButton variant="danger" size="sm" @click="fetchListsAndGroups">{{ t('listsPage.retryButton') }}</VButton>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<VCard v-else-if="lists.length === 0 && !loading" variant="empty-state" empty-icon="clipboard"
|
||||
:empty-title="noListsMessage">
|
||||
:empty-title="t(noListsMessageKey.value)">
|
||||
<template #default>
|
||||
<p v-if="!currentGroupId">Create a personal list or join a group to see shared lists.</p>
|
||||
<p v-else>This group doesn't have any lists yet.</p>
|
||||
<p v-if="!currentGroupId">{{ t('listsPage.emptyState.personalGlobalInfo') }}</p>
|
||||
<p v-else>{{ t('listsPage.emptyState.groupSpecificInfo') }}</p>
|
||||
</template>
|
||||
<template #empty-actions>
|
||||
<VButton variant="primary" class="mt-2" @click="showCreateModal = true" icon-left="plus">
|
||||
Create New List
|
||||
{{ t('listsPage.createNewListButton') }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VCard>
|
||||
|
||||
<div v-else-if="loading && lists.length === 0" class="loading-placeholder">
|
||||
Loading lists...
|
||||
{{ t('listsPage.loadingLists') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
@ -32,7 +32,7 @@
|
||||
@touchstart="handleTouchStart(list.id)" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd"
|
||||
:data-list-id="list.id">
|
||||
<div class="neo-list-header">{{ list.name }}</div>
|
||||
<div class="neo-list-desc">{{ list.description || 'No description' }}</div>
|
||||
<div class="neo-list-desc">{{ list.description || t('listsPage.noDescription') }}</div>
|
||||
<ul class="neo-item-list">
|
||||
<li v-for="item in list.items" :key="item.id || item.tempId" class="neo-list-item" :data-item-id="item.id"
|
||||
:data-item-temp-id="item.tempId" :class="{ 'is-updating': item.updating }">
|
||||
@ -47,7 +47,7 @@
|
||||
<li class="neo-list-item new-item-input-container">
|
||||
<label class="neo-checkbox-label">
|
||||
<input type="checkbox" disabled />
|
||||
<input type="text" class="neo-new-item-input" placeholder="Add new item..." ref="newItemInputRefs"
|
||||
<input type="text" class="neo-new-item-input" :placeholder="t('listsPage.addItemPlaceholder')" ref="newItemInputRefs"
|
||||
:data-list-id="list.id" @keyup.enter="addNewItem(list, $event)"
|
||||
@blur="handleNewItemBlur(list, $event)" @click.stop />
|
||||
</label>
|
||||
@ -55,7 +55,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="neo-create-list-card" @click="showCreateModal = true" ref="createListCardRef">
|
||||
+ Create a new list
|
||||
{{ t('listsPage.createCard.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,15 +66,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch, onUnmounted, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { apiClient, API_ENDPOINTS } from '@/config/api'; // Adjust path as needed
|
||||
import CreateListModal from '@/components/CreateListModal.vue'; // Adjust path as needed
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import VAlert from '@/components/valerie/VAlert.vue'; // Adjust path as needed
|
||||
import VCard from '@/components/valerie/VCard.vue'; // Adjust path as needed
|
||||
import VButton from '@/components/valerie/VButton.vue'; // Adjust path as needed
|
||||
import VAlert from '@/components/valerie/VAlert.vue'; // Adjust path as needed
|
||||
import VCard from '@/components/valerie/VCard.vue'; // Adjust path as needed
|
||||
import VButton from '@/components/valerie/VButton.vue'; // Adjust path as needed
|
||||
import { animate } from 'motion';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface List {
|
||||
id: number;
|
||||
name: string;
|
||||
@ -155,17 +158,17 @@ const fetchCurrentViewGroupName = async () => {
|
||||
const pageTitle = computed(() => {
|
||||
if (currentGroupId.value) {
|
||||
return currentViewedGroup.value
|
||||
? `Lists for ${currentViewedGroup.value.name}`
|
||||
: `Lists for Group ${currentGroupId.value}`;
|
||||
? t('listsPage.pageTitle.forGroup', { groupName: currentViewedGroup.value.name })
|
||||
: t('listsPage.pageTitle.forGroupId', { groupId: currentGroupId.value });
|
||||
}
|
||||
return 'My Lists';
|
||||
return t('listsPage.pageTitle.myLists');
|
||||
});
|
||||
|
||||
const noListsMessage = computed(() => {
|
||||
const noListsMessageKey = computed(() => {
|
||||
if (currentGroupId.value) {
|
||||
return 'No lists found for this group.';
|
||||
return 'listsPage.emptyState.noListsForGroup';
|
||||
}
|
||||
return 'You have no lists yet.';
|
||||
return 'listsPage.emptyState.noListsYet';
|
||||
});
|
||||
|
||||
const fetchAllAccessibleGroups = async () => {
|
||||
@ -202,7 +205,7 @@ const fetchLists = async () => {
|
||||
cachedLists.value = JSON.parse(JSON.stringify(response.data));
|
||||
cachedTimestamp.value = Date.now();
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch lists.';
|
||||
error.value = err instanceof Error ? err.message : t('listsPage.errors.fetchFailed');
|
||||
console.error(error.value, err);
|
||||
if (cachedLists.value.length === 0) lists.value = [];
|
||||
} finally {
|
||||
|
@ -7,18 +7,18 @@
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="onSubmit" class="form-layout">
|
||||
<div class="form-group mb-2">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<label for="email" class="form-label">{{ t('loginPage.emailLabel') }}</label>
|
||||
<input type="email" id="email" v-model="email" class="form-input" required autocomplete="email" />
|
||||
<p v-if="formErrors.email" class="form-error-text">{{ formErrors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<label for="password" class="form-label">{{ t('loginPage.passwordLabel') }}</label>
|
||||
<div class="input-with-icon-append">
|
||||
<input :type="isPwdVisible ? 'text' : 'password'" id="password" v-model="password" class="form-input"
|
||||
required autocomplete="current-password" />
|
||||
<button type="button" @click="isPwdVisible = !isPwdVisible" class="icon-append-btn"
|
||||
aria-label="Toggle password visibility">
|
||||
:aria-label="t('loginPage.togglePasswordVisibilityLabel')">
|
||||
<svg class="icon icon-sm">
|
||||
<use :xlink:href="isPwdVisible ? '#icon-settings' : '#icon-settings'"></use>
|
||||
</svg> <!-- Placeholder for visibility icons -->
|
||||
@ -31,11 +31,11 @@
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full mt-2" :disabled="loading">
|
||||
<span v-if="loading" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
|
||||
Login
|
||||
{{ t('loginPage.loginButton') }}
|
||||
</button>
|
||||
|
||||
<div class="text-center mt-2">
|
||||
<router-link to="/auth/signup" class="link-styled">Don't have an account? Sign up</router-link>
|
||||
<router-link to="/auth/signup" class="link-styled">{{ t('loginPage.signupLink') }}</router-link>
|
||||
</div>
|
||||
|
||||
<SocialLoginButtons />
|
||||
@ -47,6 +47,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth'; // Assuming path
|
||||
import { useNotificationStore } from '@/stores/notifications';
|
||||
@ -57,6 +58,8 @@ const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
const isPwdVisible = ref(false);
|
||||
@ -71,12 +74,12 @@ const isValidEmail = (val: string): boolean => {
|
||||
const validateForm = (): boolean => {
|
||||
formErrors.value = {};
|
||||
if (!email.value.trim()) {
|
||||
formErrors.value.email = 'Email is required';
|
||||
formErrors.value.email = t('loginPage.errors.emailRequired');
|
||||
} else if (!isValidEmail(email.value)) {
|
||||
formErrors.value.email = 'Invalid email format';
|
||||
formErrors.value.email = t('loginPage.errors.emailInvalid');
|
||||
}
|
||||
if (!password.value) {
|
||||
formErrors.value.password = 'Password is required';
|
||||
formErrors.value.password = t('loginPage.errors.passwordRequired');
|
||||
}
|
||||
return Object.keys(formErrors.value).length === 0;
|
||||
};
|
||||
@ -89,11 +92,11 @@ const onSubmit = async () => {
|
||||
formErrors.value.general = undefined; // Clear previous general errors
|
||||
try {
|
||||
await authStore.login(email.value, password.value);
|
||||
notificationStore.addNotification({ message: 'Login successful', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('loginPage.notifications.loginSuccess'), type: 'success' });
|
||||
const redirectPath = (route.query.redirect as string) || '/';
|
||||
router.push(redirectPath);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Login failed. Please check your credentials.';
|
||||
const message = error instanceof Error ? error.message : t('loginPage.errors.loginFailed');
|
||||
formErrors.value.general = message;
|
||||
console.error(message, error);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
|
Loading…
Reference in New Issue
Block a user