refactor: support i18n in frontend

This commit is contained in:
2026-07-21 19:34:19 +08:00
parent 1a9eb9299b
commit e16a40148a
24 changed files with 628 additions and 400 deletions
+1
View File
@@ -21,6 +21,7 @@
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.32", "vue": "^3.5.32",
"vue-i18n": "^11.4.7",
"vue-router": "^5.0.4" "vue-router": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
+56 -191
View File
@@ -17,9 +17,6 @@ importers:
'@fortawesome/vue-fontawesome': '@fortawesome/vue-fontawesome':
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3)) version: 3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3))
axios:
specifier: 1.14.0
version: 1.14.0
bulma: bulma:
specifier: 0.9.1 specifier: 0.9.1
version: 0.9.1 version: 0.9.1
@@ -32,6 +29,9 @@ importers:
vue: vue:
specifier: ^3.5.32 specifier: ^3.5.32
version: 3.5.33(typescript@6.0.3) version: 3.5.33(typescript@6.0.3)
vue-i18n:
specifier: ^11.4.7
version: 11.4.7(vue@3.5.33(typescript@6.0.3))
vue-router: vue-router:
specifier: ^5.0.4 specifier: ^5.0.4
version: 5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3)) version: 5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3))
@@ -301,6 +301,22 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'} engines: {node: '>=18.18'}
'@intlify/core-base@11.4.7':
resolution: {integrity: sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==}
engines: {node: '>= 22'}
'@intlify/devtools-types@11.4.7':
resolution: {integrity: sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==}
engines: {node: '>= 22'}
'@intlify/message-compiler@11.4.7':
resolution: {integrity: sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==}
engines: {node: '>= 22'}
'@intlify/shared@11.4.7':
resolution: {integrity: sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==}
engines: {node: '>= 22'}
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -782,6 +798,9 @@ packages:
'@vue/compiler-ssr@3.5.33': '@vue/compiler-ssr@3.5.33':
resolution: {integrity: sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==} resolution: {integrity: sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==}
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/devtools-api@7.7.9': '@vue/devtools-api@7.7.9':
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
@@ -879,12 +898,6 @@ packages:
resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.14.0:
resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==}
balanced-match@4.0.4: balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -920,10 +933,6 @@ packages:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
caniuse-lite@1.0.30001791: caniuse-lite@1.0.30001791:
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
@@ -935,10 +944,6 @@ packages:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
confbox@0.1.8: confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
@@ -991,18 +996,10 @@ packages:
defu@6.1.7: defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.1.2: detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
electron-to-chromium@1.5.344: electron-to-chromium@1.5.344:
resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==}
@@ -1013,22 +1010,6 @@ packages:
error-stack-parser-es@1.0.5: error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
escalade@3.2.0: escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1148,39 +1129,15 @@ packages:
flatted@3.4.2: flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
gensync@1.0.0-beta.2: gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob-parent@5.1.2: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -1189,22 +1146,6 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.3:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -1395,10 +1336,6 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
memorystream@0.3.1: memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
@@ -1411,14 +1348,6 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@10.2.5: minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -1571,10 +1500,6 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1807,6 +1732,12 @@ packages:
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
vue-i18n@11.4.7:
resolution: {integrity: sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==}
engines: {node: '>= 22'}
peerDependencies:
vue: ^3.0.0
vue-router@5.0.6: vue-router@5.0.6:
resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==} resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==}
peerDependencies: peerDependencies:
@@ -2143,6 +2074,24 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {} '@humanwhocodes/retry@0.4.3': {}
'@intlify/core-base@11.4.7':
dependencies:
'@intlify/devtools-types': 11.4.7
'@intlify/message-compiler': 11.4.7
'@intlify/shared': 11.4.7
'@intlify/devtools-types@11.4.7':
dependencies:
'@intlify/core-base': 11.4.7
'@intlify/shared': 11.4.7
'@intlify/message-compiler@11.4.7':
dependencies:
'@intlify/shared': 11.4.7
source-map-js: 1.2.1
'@intlify/shared@11.4.7': {}
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -2551,6 +2500,8 @@ snapshots:
'@vue/compiler-dom': 3.5.33 '@vue/compiler-dom': 3.5.33
'@vue/shared': 3.5.33 '@vue/shared': 3.5.33
'@vue/devtools-api@6.6.4': {}
'@vue/devtools-api@7.7.9': '@vue/devtools-api@7.7.9':
dependencies: dependencies:
'@vue/devtools-kit': 7.7.9 '@vue/devtools-kit': 7.7.9
@@ -2669,16 +2620,6 @@ snapshots:
'@babel/parser': 7.29.2 '@babel/parser': 7.29.2
ast-kit: 2.2.0 ast-kit: 2.2.0
asynckit@0.4.0: {}
axios@1.14.0:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
balanced-match@4.0.4: {} balanced-match@4.0.4: {}
baseline-browser-mapping@2.10.23: {} baseline-browser-mapping@2.10.23: {}
@@ -2709,11 +2650,6 @@ snapshots:
dependencies: dependencies:
run-applescript: 7.1.0 run-applescript: 7.1.0
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
caniuse-lite@1.0.30001791: {} caniuse-lite@1.0.30001791: {}
chokidar@4.0.3: chokidar@4.0.3:
@@ -2725,10 +2661,6 @@ snapshots:
dependencies: dependencies:
readdirp: 5.0.0 readdirp: 5.0.0
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
confbox@0.1.8: {} confbox@0.1.8: {}
confbox@0.2.4: {} confbox@0.2.4: {}
@@ -2766,37 +2698,14 @@ snapshots:
defu@6.1.7: {} defu@6.1.7: {}
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
electron-to-chromium@1.5.344: {} electron-to-chromium@1.5.344: {}
entities@7.0.1: {} entities@7.0.1: {}
error-stack-parser-es@1.0.5: {} error-stack-parser-es@1.0.5: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.3
escalade@3.2.0: {} escalade@3.2.0: {}
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
@@ -2931,41 +2840,11 @@ snapshots:
flatted@3.4.2: {} flatted@3.4.2: {}
follow-redirects@1.16.0: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.3
mime-types: 2.1.35
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
function-bind@1.1.2: {}
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.3
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
glob-parent@5.1.2: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -2974,18 +2853,6 @@ snapshots:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
gopd@1.2.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.3:
dependencies:
function-bind: 1.1.2
hookable@5.5.3: {} hookable@5.5.3: {}
ignore@5.3.2: {} ignore@5.3.2: {}
@@ -3121,8 +2988,6 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
memorystream@0.3.1: {} memorystream@0.3.1: {}
merge2@1.4.1: {} merge2@1.4.1: {}
@@ -3132,12 +2997,6 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.2 picomatch: 2.3.2
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@10.2.5: minimatch@10.2.5:
dependencies: dependencies:
brace-expansion: 5.0.5 brace-expansion: 5.0.5
@@ -3289,8 +3148,6 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
proxy-from-env@2.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
quansync@0.2.11: {} quansync@0.2.11: {}
@@ -3519,6 +3376,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vue-i18n@11.4.7(vue@3.5.33(typescript@6.0.3)):
dependencies:
'@intlify/core-base': 11.4.7
'@intlify/devtools-types': 11.4.7
'@intlify/shared': 11.4.7
'@vue/devtools-api': 6.6.4
vue: 3.5.33(typescript@6.0.3)
vue-router@5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3)): vue-router@5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3)):
dependencies: dependencies:
'@babel/generator': 7.29.1 '@babel/generator': 7.29.1
+17 -11
View File
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, watch } from 'vue';
import { useLanguageStore } from './stores/language'; import { useLanguageStore } from './stores/language';
import { useTokenStore } from './stores/token'; import { useTokenStore } from './stores/token';
import MessageBox from '@/components/MessageBox.vue'; import MessageBox from '@/components/MessageBox.vue';
import { logout as apiCommonLogout } from './api/common'; import { logout as apiCommonLogout } from './api/common';
import { goToHome } from '@/router'; import { goToHome } from '@/router';
import { i18n, Language } from './utils/i18n';
const language = useLanguageStore(); const language = useLanguageStore();
const token = useTokenStore(); const token = useTokenStore();
@@ -13,6 +14,12 @@ const isBurgerActive = ref<boolean>(false);
const messagebox = ref<InstanceType<typeof MessageBox>>(); const messagebox = ref<InstanceType<typeof MessageBox>>();
// Sync the vue-i18n locale with the persisted language store (immediately, so
// the very first render uses the saved language; and on every later change).
watch(() => language.language, (lang) => {
i18n.global.locale.value = lang === Language.English ? 'en-US' : 'zh-CN';
}, { immediate: true });
const logout = async () => { const logout = async () => {
const tokenStore = useTokenStore(); const tokenStore = useTokenStore();
const rv = await apiCommonLogout(tokenStore.currentToken); const rv = await apiCommonLogout(tokenStore.currentToken);
@@ -24,7 +31,7 @@ const logout = async () => {
goToHome(); goToHome();
} else { } else {
// Show logout error. // Show logout error.
messagebox.value?.show("Fail to logout due to unknow reason. Consider refreshing page to solve problem."); messagebox.value?.show(i18n.global.t('js-fail-logout'));
} }
} }
@@ -55,24 +62,23 @@ const toggleBurger = () => {
<div id="coleaf-navbar" class="navbar-menu" :class="{ 'is-active': isBurgerActive }"> <div id="coleaf-navbar" class="navbar-menu" :class="{ 'is-active': isBurgerActive }">
<div class="navbar-start"> <div class="navbar-start">
<router-link class="navbar-item" to="/home">Home</router-link> <router-link class="navbar-item" to="/home">{{ $t('header-nav-home') }}</router-link>
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/collection">Collection</router-link> <router-link v-if="token.isLoggedIn" class="navbar-item" to="/collection">{{ $t('header-nav-collection') }}</router-link>
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/calendar">Calendar</router-link> <router-link v-if="token.isLoggedIn" class="navbar-item" to="/calendar">{{ $t('header-nav-calendar') }}</router-link>
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/todo">Todo</router-link> <router-link v-if="token.isLoggedIn" class="navbar-item" to="/todo">{{ $t('header-nav-todo') }}</router-link>
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/admin">Admin</router-link> <router-link v-if="token.isLoggedIn" class="navbar-item" to="/admin">{{ $t('header-nav-admin') }}</router-link>
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<p class="navbar-item"> <p class="navbar-item">
<router-link v-if="!token.isLoggedIn" class="button is-primary" to="/login">Login</router-link> <router-link v-if="!token.isLoggedIn" class="button is-primary" to="/login">{{ $t('header-user-login') }}</router-link>
</p> </p>
<p class="navbar-item"> <p class="navbar-item">
<a v-if="token.isLoggedIn" class="button is-primary" @click="logout">Logout</a> <a v-if="token.isLoggedIn" class="button is-primary" @click="logout">{{ $t('header-user-logout') }}</a>
</p> </p>
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a v-if="language.isEnglish" class="navbar-link">English</a> <a class="navbar-link">{{ $t('header-language') }}</a>
<a v-else-if="language.isSimplifiedChinese" class="navbar-link">简体中文</a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a @click="language.changeToEnglish()" class="navbar-item">English</a> <a @click="language.changeToEnglish()" class="navbar-item">English</a>
+11 -11
View File
@@ -19,7 +19,7 @@ import {
dayOfWeek, dayOfWeek,
isLeapYear, isLeapYear,
} from '@/utils/datetime'; } from '@/utils/datetime';
import { MONTH_NAMES, WEEK_NAMES, universalGetMonth } from '@/utils/i18n'; import { i18n, universalGetMonth } from '@/utils/i18n';
// region: Constants // region: Constants
@@ -121,11 +121,11 @@ const isHourTab = computed<boolean>(() => currentTab.value === TabType.Hour);
const isMinuteTab = computed<boolean>(() => currentTab.value === TabType.Minute); const isMinuteTab = computed<boolean>(() => currentTab.value === TabType.Minute);
const tabLabel = (t: TabType): string => { const tabLabel = (t: TabType): string => {
if (t === TabType.Year) return 'Year'; if (t === TabType.Year) return i18n.global.t('universal-text-year');
if (t === TabType.Month) return 'Month'; if (t === TabType.Month) return i18n.global.t('universal-text-month');
if (t === TabType.Day) return 'Day'; if (t === TabType.Day) return i18n.global.t('universal-text-day');
if (t === TabType.Hour) return 'Hour'; if (t === TabType.Hour) return i18n.global.t('universal-text-hour');
return 'Minute'; return i18n.global.t('universal-text-minute');
} }
const headerValue = (t: TabType): number => { const headerValue = (t: TabType): number => {
@@ -200,9 +200,9 @@ interface MonthCell {
const monthCells = computed<MonthCell[]>(() => { const monthCells = computed<MonthCell[]>(() => {
const sameYear = displayCacheDateTime.value.getFullYear() === internalDateTime.value.getFullYear(); const sameYear = displayCacheDateTime.value.getFullYear() === internalDateTime.value.getFullYear();
const pickedMonth = internalDateTime.value.getMonth(); const pickedMonth = internalDateTime.value.getMonth();
return MONTH_NAMES.map((name, idx) => ({ return Array.from({ length: 12 }, (_, idx) => ({
idx, idx,
name, name: universalGetMonth(idx),
picked: sameYear && idx === pickedMonth, picked: sameYear && idx === pickedMonth,
})); }));
}) })
@@ -551,7 +551,7 @@ watch(currentTab, (t) => {
</nav> </nav>
<div class="perfectTable"> <div class="perfectTable">
<div> <div>
<div v-for="(w, i) in WEEK_NAMES" :key="i">{{ w }}</div> <div v-for="i in 7" :key="i">{{ $t('universal-week-' + i) }}</div>
</div> </div>
<div v-for="(row, ri) in dayRows" :key="ri"> <div v-for="(row, ri) in dayRows" :key="ri">
<div v-for="(cell, ci) in row" :key="ci" :class="{ picked: cell.picked }" @click="clickDay(cell)"> <div v-for="(cell, ci) in row" :key="ci" :class="{ picked: cell.picked }" @click="clickDay(cell)">
@@ -587,8 +587,8 @@ watch(currentTab, (t) => {
</div> </div>
<footer class="modal-card-foot"> <footer class="modal-card-foot">
<a class="button is-success" @click="confirm"><span>OK</span></a> <a class="button is-success" @click="confirm"><span>{{ $t('datetimepicker-confirm') }}</span></a>
<a class="button" @click="cancel"><span>Cancel</span></a> <a class="button" @click="cancel"><span>{{ $t('datetimepicker-cancel') }}</span></a>
</footer> </footer>
</div> </div>
</div> </div>
+3 -2
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { i18n } from '@/utils/i18n';
const isVisible = ref(false); const isVisible = ref(false);
const title = ref<string>(""); const title = ref<string>("");
@@ -10,7 +11,7 @@ const emit = defineEmits<{
}>() }>()
const show = (_content: string, _title?: string) => { const show = (_content: string, _title?: string) => {
title.value = _title ?? "Notification"; title.value = _title ?? i18n.global.t('messagebox-title');
content.value = _content; content.value = _content;
isVisible.value = true; isVisible.value = true;
} }
@@ -42,7 +43,7 @@ defineExpose({
<p>{{ content }}</p> <p>{{ content }}</p>
</div> </div>
<footer class="modal-card-foot"> <footer class="modal-card-foot">
<button class="button is-success" @click="ok">OK</button> <button class="button is-success" @click="ok">{{ $t('messagebox-confirm') }}</button>
</footer> </footer>
</div> </div>
</div> </div>
+4 -4
View File
@@ -36,21 +36,21 @@ const deleteItem = () => {
<div class="paperbox-item-multiline-words"> <div class="paperbox-item-multiline-words">
<b>{{ uuid }}</b> <b>{{ uuid }}</b>
<p> <p>
<span>User Agent: </span> <span>{{ $t('tokenItem-ua') }}</span>
<span>{{ ua }}</span> <span>{{ ua }}</span>
</p> </p>
<p> <p>
<span>IP: </span> <span>{{ $t('tokenItem-ip') }}</span>
<span>{{ ip }}</span> <span>{{ ip }}</span>
</p> </p>
<p> <p>
<span>Expire On: </span> <span>{{ $t('tokenItem-expireOn') }}</span>
<span>{{ expireOn }}</span> <span>{{ expireOn }}</span>
</p> </p>
<p v-if="isMe"> <p v-if="isMe">
<span class="icon is-small"><font-awesome-icon <span class="icon is-small"><font-awesome-icon
icon="fas fa-exclamation-triangle"></font-awesome-icon></span> icon="fas fa-exclamation-triangle"></font-awesome-icon></span>
<span>This is the login credentials you are currently using.</span> <span>{{ $t('tokenItem-isMe') }}</span>
</p> </p>
</div> </div>
+2 -2
View File
@@ -61,14 +61,14 @@ const cancelUpdateItem = () => {
<div>{{ username }}</div> <div>{{ username }}</div>
</div> </div>
<div v-show="isEditing" class="field"> <div v-show="isEditing" class="field">
<label class="label">New Password</label> <label class="label">{{ $t('userItem-newPassword') }}</label>
<div class="control"> <div class="control">
<input v-model="editingPassword" class="input" type="password"> <input v-model="editingPassword" class="input" type="password">
</div> </div>
</div> </div>
<div v-show="isEditing" class="field"> <div v-show="isEditing" class="field">
<label class="checkbox"> <label class="checkbox">
<input v-model="editingIsAdmin" type="checkbox"><span>Is Admin</span> <input v-model="editingIsAdmin" type="checkbox"><span>{{ $t('userItem-isAdmin') }}</span>
</label> </label>
</div> </div>
</div> </div>
@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { WEEK_NAMES } from '@/utils/i18n';
import type { CalendarCell } from './types'; import type { CalendarCell } from './types';
import CalendarItem from './CalendarItem.vue'; import CalendarItem from './CalendarItem.vue';
@@ -22,8 +21,8 @@ const rows = computed<CalendarCell[][]>(() => {
<template> <template>
<div class="calendarGrid card" style="padding: 1.25rem; display: flex; flex-flow: column;"> <div class="calendarGrid card" style="padding: 1.25rem; display: flex; flex-flow: column;">
<div style="margin: 0 0 0.75em 0;"> <div style="margin: 0 0 0.75em 0;">
<div v-for="(w, i) in WEEK_NAMES" :key="i"> <div v-for="i in 7" :key="i">
<b :style="i >= 5 ? { color: 'red' } : null">{{ w }}</b> <b :style="i >= 6 ? { color: 'red' } : null">{{ $t('universal-week-' + i) }}</b>
</div> </div>
</div> </div>
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import type { CalendarCell } from './types'; import type { CalendarCell } from './types';
import { i18n } from '@/utils/i18n';
const props = defineProps({ const props = defineProps({
cell: { cell: {
@@ -31,7 +32,7 @@ const slots = computed<EventBoxSlot[]>(() => {
const moreLabel = computed<string | null>(() => { const moreLabel = computed<string | null>(() => {
const len = props.cell.events.length const len = props.cell.events.length
return len > 4 ? `${len} items` : null return len > 4 ? i18n.global.t('calendar-calendar-stripedEvents', [len]) : null
}) })
</script> </script>
@@ -28,7 +28,7 @@ const toggle = () => {
<div class="paperbox-item-words"> <div class="paperbox-item-words">
<b>{{ name }}</b> <b>{{ name }}</b>
<p> <p>
<span>Shared by: </span> <span>{{ $t('sharedItem-sharedBy') }}</span>
<span>{{ username }}</span> <span>{{ username }}</span>
</p> </p>
</div> </div>
+181
View File
@@ -0,0 +1,181 @@
export const messages: Record<string, string> = {
// page names (reserved for future document.title use)
'pageName-home': 'coconut-leaf - A light, self-host calendar system.',
'pageName-collection': 'coconut-leaf - Collection',
'pageName-calendar': 'coconut-leaf - Calendar',
'pageName-event': 'coconut-leaf - Event',
'pageName-todo': 'coconut-leaf - Todo',
'pageName-admin': 'coconut-leaf - Admin',
'pageName-login': 'coconut-leaf - Login',
// header navbar
'header-nav-home': 'Home',
'header-nav-collection': 'Collection',
'header-nav-calendar': 'Calendar',
'header-nav-todo': 'Todo',
'header-nav-admin': 'Admin',
'header-user-login': 'Login',
'header-user-logout': 'Logout',
'header-language': 'Language',
// universal datetime labels
'universal-text-year': 'Year',
'universal-text-month': 'Month',
'universal-text-day': 'Day',
'universal-text-hour': 'Hour',
'universal-text-minute': 'Minute',
'universal-week-1': 'Monday',
'universal-week-2': 'Tuesday',
'universal-week-3': 'Wednesday',
'universal-week-4': 'Thursday',
'universal-week-5': 'Friday',
'universal-week-6': 'Saturday',
'universal-week-7': 'Sunday',
'universal-month-1': 'January',
'universal-month-2': 'February',
'universal-month-3': 'March',
'universal-month-4': 'April',
'universal-month-5': 'May',
'universal-month-6': 'June',
'universal-month-7': 'July',
'universal-month-8': 'August',
'universal-month-9': 'September',
'universal-month-10': 'October',
'universal-month-11': 'November',
'universal-month-12': 'December',
// messagebox
'messagebox-confirm': 'OK',
'messagebox-title': 'Notification',
// datetimepicker
'datetimepicker-confirm': 'OK',
'datetimepicker-cancel': 'Cancel',
// js operation messages
'js-fail-login': 'Fail to login. Please check your username or password.',
'js-fail-logout': 'Fail to logout due to unknow reason. Consider refreshing page to solve problem.',
'js-fail-get': 'A get operation failed. It may caused by server internal error or your limited permission. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
'js-fail-add': 'An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
'js-fail-update': 'An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
'js-fail-delete': 'A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
'js-success': 'Operation OK.',
'js-fail-form': 'Your filled event form is not fufilled or have error. Please check it and try again.',
// home page
'home-tagline': 'A light, self-host and multi-account calendar system.',
'home-intent': 'The original intention of this system is served for yyc12345 personal use.',
'home-source': 'See our {link} for the source code in detail.',
'home-source-link': 'GitHub project',
'home-license': 'The source code of this project is licensed under {link}.',
'home-license-link': 'AGPL v3',
// login
'login-form-username': 'Username',
'login-form-password': 'Password',
'login-form-login': 'Login',
// todo
'todo-todoList': 'Todo list',
// calendar page
'calendar-calendar-today': 'Today',
'calendar-calendar-add': 'Add...',
'calendar-calendar-stripedEvents': '{0} items',
'calendar-calendar-scheduleList': 'Schedule',
'calendar-tabcontrol-tabCalendar': 'Calendar',
'calendar-tabcontrol-tabCollection': 'Collection',
'calendar-tabcontrol-tabDisplay': 'Display',
'calendar-owned-list': 'My collections',
'calendar-shared-list': 'Shared collections',
'calendar-display-firstDayOfWeek': 'The first day of week',
'calendar-display-subcalendar': 'Sub-Calendar',
'calendar-display-subcalendar-chineseLunisolarCalendar': 'Chinese Lunisolar Calendar',
'calendar-display-subcalendar-none': 'None',
// collection page
'collection-owned-list': 'Owned',
'collection-sharing-list': 'Sharing target',
'collection-sharing-editing': 'Editing: ',
// event page
'event-header': 'Edit Event',
'event-title': 'Title',
'event-description': 'Description',
'event-color': 'Color',
'event-collection': 'Collection',
'event-startDateTime': 'Start Date Time',
'event-endDateTime': 'Stop Date Time',
'event-btnSpot': 'Spot',
'event-btnFullDay': 'Full day',
'event-loop': 'Event Loop',
'event-loop-never': 'Never',
'event-loop-day': 'Day',
'event-loop-week': 'Week',
'event-loop-month': 'Month',
'event-loop-year': 'Year',
'event-loopDay-span': 'Day span',
'event-loopWeek-span': 'Week span',
'event-loopWeek-option': 'Week options',
'event-loopMonth-span': 'Month span',
'event-loopMonth-option': 'Month mode',
'event-loopWeek-optionA': 'Day {0} in month',
'event-loopWeek-optionB': 'Day {0} from the end of the month',
'event-loopWeek-optionC': 'Day {1} in week {0}',
'event-loopWeek-optionD': 'Day {1} in week {0} from the end of the month',
'event-loopYear-span': 'Year span',
'event-loopStop': 'Event Loop Stop',
'event-loopStop-forever': 'Forever',
'event-loopStop-datetime': 'Date Time',
'event-loopStop-times': 'Times',
'event-timezone-title': 'Timezone',
'event-timezone-warning': 'The timezone of this event is not corresponding with your current timezone. All of date and time in this page are shown as the original timezone of this event. You can choose a timezone option in follwing content. If you are not familar with this, please pick keep timezone.',
'event-timezone-keep': 'Keep timezone',
'event-timezone-replace': 'Use my timezone',
'event-strictMode-title': 'Strict Mode in Event Loop',
'event-strictMode-warning': 'You can choose strict mode or rough mode in following content. This is only effect on looped event.',
'event-strictMode-strict': 'Strict Mode. If ordered day is not existing, skip it.',
'event-strictMode-rough': 'Rough mode. If ordered day is not existing, choose the day closing with original day to arrange event.',
'event-btnSubmit': 'Submit',
'event-btnCancel': 'Cancel',
// shared item
'sharedItem-sharedBy': 'Shared by: ',
// admin page
'admin-tabcontrol-tabProfile': 'My Profile',
'admin-tabcontrol-tabToken': 'Manage Multi-login',
'admin-tabcontrol-tabUserList': 'Manager User',
'admin-changePassword': 'Change Password',
'admin-manageToken': 'Manage multi-login',
'admin-manageToken-desc': 'Manage the multi-login of the current account. You can forced logout some login in there.',
'admin-userList': 'User List',
// admin user/token items
'userItem-newPassword': 'New Password',
'userItem-isAdmin': 'Is Admin',
'tokenItem-ua': 'User Agent: ',
'tokenItem-ip': 'IP: ',
'tokenItem-expireOn': 'Expire On: ',
'tokenItem-isMe': 'This is the login credentials you are currently using.',
// datetime loop rule human-readable text (English translations authored for
// this migration — the legacy en-US properties omitted these keys.)
'datetime-loopStopRuleText-infinity': 'Loop forever.',
'datetime-loopStopRuleText-datetime': 'Stop looping at {0}.',
'datetime-loopStopRuleText-times': 'Loop {0} time(s).',
'datetime-loopRuleText-modeStrict': 'Strict mode.',
'datetime-loopRuleText-modeRough': 'Rough mode.',
'datetime-loopRuleText-year': 'Loop once every {0} year(s) on {1}.',
'datetime-loopRuleText-monthA': 'Loop once on day {1} of every {0} month(s).',
'datetime-loopRuleText-monthB': 'Loop once on the {1}th day from the end of every {0} month(s).',
'datetime-loopRuleText-monthC': 'Loop once every {0} month(s) on the {2} of week {1}.',
'datetime-loopRuleText-monthD': 'Loop once every {0} month(s) on the {2} of week {1} from the end.',
'datetime-loopRuleText-week': 'Loop once every {0} week(s) on {1}.',
'datetime-loopRuleText-day': 'Loop once every {0} day(s).',
// not found page
'notfound-title': 'Oops!',
'notfound-desc1': 'You are wandering in the desert of coconut-leaf.',
'notfound-desc2': 'Please back to previous page and try again.',
}
+180
View File
@@ -0,0 +1,180 @@
export const messages: Record<string, string> = {
// 页面名(预留给日后设置 document.title
'pageName-home': 'coconut-leaf - 一个轻量的自建日历系统',
'pageName-collection': 'coconut-leaf - 集合',
'pageName-calendar': 'coconut-leaf - 日历',
'pageName-event': 'coconut-leaf - 事件',
'pageName-todo': 'coconut-leaf - 待办',
'pageName-admin': 'coconut-leaf - 管理',
'pageName-login': 'coconut-leaf - 登录',
// 顶部导航栏
'header-nav-home': '主页',
'header-nav-collection': '集合',
'header-nav-calendar': '日历',
'header-nav-todo': '待办',
'header-nav-admin': '管理',
'header-user-login': '登录',
'header-user-logout': '登出',
'header-language': '语言',
// 通用日期时间标签
'universal-text-year': '年',
'universal-text-month': '月',
'universal-text-day': '日',
'universal-text-hour': '时',
'universal-text-minute': '分',
'universal-week-1': '星期一',
'universal-week-2': '星期二',
'universal-week-3': '星期三',
'universal-week-4': '星期四',
'universal-week-5': '星期五',
'universal-week-6': '星期六',
'universal-week-7': '星期日',
'universal-month-1': '1月',
'universal-month-2': '2月',
'universal-month-3': '3月',
'universal-month-4': '4月',
'universal-month-5': '5月',
'universal-month-6': '6月',
'universal-month-7': '7月',
'universal-month-8': '8月',
'universal-month-9': '9月',
'universal-month-10': '10月',
'universal-month-11': '11月',
'universal-month-12': '12月',
// 消息框
'messagebox-confirm': '确认',
'messagebox-title': '通知',
// 日期时间选择器
'datetimepicker-confirm': '确认',
'datetimepicker-cancel': '取消',
// 操作提示信息
'js-fail-login': '登陆失败,请检查您的用户名和密码。',
'js-fail-logout': '由于未知原因,登出失败,请考虑刷新页面解决问题。',
'js-fail-get': '一个获取操作失败了,可能是系统错误或者您的权限不足。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。',
'js-fail-add': '一个添加操作失败了,可能是您输入的参数有误。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。',
'js-fail-update': '一个更新操作失败了,可能是没有找到匹配的条目或者您的参数输入错误。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。',
'js-fail-delete': '一个删除操作失败了,可能是没有找到对应条目。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。',
'js-success': '操作成功',
'js-fail-form': '您所填写的事件内容存在缺漏或有错误字段,请检查后再提交。',
// 主页
'home-tagline': '一个轻量的、自托管的多账号日历系统。',
'home-intent': '本系统最初是为 yyc12345 个人使用而打造的。',
'home-source': '请访问我们的 {link} 查看源代码详情。',
'home-source-link': 'GitHub 项目',
'home-license': '本项目源代码采用 {link} 授权。',
'home-license-link': 'AGPL v3',
// 登录
'login-form-username': '用户名',
'login-form-password': '密码',
'login-form-login': '登录',
// 待办
'todo-todoList': '待办列表',
// 日历页
'calendar-calendar-today': '今天',
'calendar-calendar-add': '添加...',
'calendar-calendar-stripedEvents': '共{0}项',
'calendar-calendar-scheduleList': '日程安排',
'calendar-tabcontrol-tabCalendar': '日历',
'calendar-tabcontrol-tabCollection': '集合',
'calendar-tabcontrol-tabDisplay': '显示设置',
'calendar-owned-list': '我的集合',
'calendar-shared-list': '被共享的集合',
'calendar-display-firstDayOfWeek': '每周开始星期',
'calendar-display-subcalendar': '副日历',
'calendar-display-subcalendar-chineseLunisolarCalendar': '中国农历',
'calendar-display-subcalendar-none': '无',
// 集合页
'collection-owned-list': '我的集合',
'collection-sharing-list': '分享目标',
'collection-sharing-editing': '正在编辑集合:',
// 事件页
'event-header': '编辑事件',
'event-title': '标题',
'event-description': '描述',
'event-color': '颜色',
'event-collection': '集合',
'event-startDateTime': '开始时间',
'event-endDateTime': '结束时间',
'event-btnSpot': '时间点',
'event-btnFullDay': '全天',
'event-loop': '事件循环',
'event-loop-never': '从不',
'event-loop-day': '按天',
'event-loop-week': '按周',
'event-loop-month': '按月',
'event-loop-year': '按年',
'event-loopDay-span': '间隔天数',
'event-loopWeek-span': '间隔周数',
'event-loopWeek-option': '在下列这些星期上循环',
'event-loopMonth-span': '间隔月数',
'event-loopMonth-option': '月份模式',
'event-loopWeek-optionA': '第{0}天',
'event-loopWeek-optionB': '倒数第{0}天',
'event-loopWeek-optionC': '第{0}个星期{1}',
'event-loopWeek-optionD': '倒数第{0}个星期{1}',
'event-loopYear-span': '间隔年数',
'event-loopStop': '事件循环停止方式',
'event-loopStop-forever': '永不停止',
'event-loopStop-datetime': '指定时间',
'event-loopStop-times': '指定次数',
'event-timezone-title': '时区设定',
'event-timezone-warning': '您当前设置的事件的时区与您的时区不匹配,本页面以事件的原时区进行时间显示。您可以在下面修改您对于此事件的时区选择,如果您不熟悉时区,请选择保持原有时区。',
'event-timezone-keep': '保持原有时区',
'event-timezone-replace': '使用我现在的时区',
'event-strictMode-title': '循环的严格与宽松',
'event-strictMode-warning': '允许您在严格模式与宽松模式种进行选择,此选择只对循环事件有效。',
'event-strictMode-strict': '严格模式,严格遵守日期要求,如果日期不存在,就不安排。',
'event-strictMode-rough': '宽松模式,不在意日期要求的精确性,如果日期不存在,则找到最近的符合条件的日子安排。',
'event-btnSubmit': '提交',
'event-btnCancel': '取消',
// 共享项
'sharedItem-sharedBy': '共享人:',
// 管理页
'admin-tabcontrol-tabProfile': '我的资料',
'admin-tabcontrol-tabToken': '管理多端登录',
'admin-tabcontrol-tabUserList': '管理用户',
'admin-changePassword': '更改密码',
'admin-manageToken': '管理多端登录',
'admin-manageToken-desc': '管理当前帐号的多端登录。您可以在这里强制下线某些地方的帐号。',
'admin-userList': '用户列表',
// 管理用户/令牌项
'userItem-newPassword': '新密码',
'userItem-isAdmin': '是管理员',
'tokenItem-ua': 'UA',
'tokenItem-ip': 'IP',
'tokenItem-expireOn': '过期时间: ',
'tokenItem-isMe': '这是你当前使用的登录凭据',
// 循环规则可读文本
'datetime-loopStopRuleText-infinity': '永远循环。',
'datetime-loopStopRuleText-datetime': '到{0}停止循环。',
'datetime-loopStopRuleText-times': '循环{0}次。',
'datetime-loopRuleText-modeStrict': '严格模式。',
'datetime-loopRuleText-modeRough': '宽松模式。',
'datetime-loopRuleText-year': '每{0}年于{1}循环一次。',
'datetime-loopRuleText-monthA': '每{0}月的第{1}日循环一次。',
'datetime-loopRuleText-monthB': '每{0}月的倒数第{1}日循环一次。',
'datetime-loopRuleText-monthC': '每{0}月的第{1}个星期{2}循环一次。',
'datetime-loopRuleText-monthD': '每{0}月的倒数第{1}个星期{2}循环一次。',
'datetime-loopRuleText-week': '每{0}周的{1}循环一次。',
'datetime-loopRuleText-day': '每{0}天循环一次。',
// 404 页
'notfound-title': '糟糕!',
'notfound-desc1': '你在 coconut-leaf 的荒漠中徘徊。',
'notfound-desc2': '请返回上一页重试。',
}
+3
View File
@@ -7,6 +7,8 @@ import { faUser, faLock, faPen, faShare, faTrash, faCheck, faTimes, faPlus, faSy
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { i18n } from './utils/i18n'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
@@ -18,6 +20,7 @@ pinia.use(piniaPluginPersistedstate);
const app = createApp(App); const app = createApp(App);
app.use(pinia); app.use(pinia);
app.use(router); app.use(router);
app.use(i18n);
library.add(faUser, faLock, faPen, faShare, faTrash, faCheck, faTimes, faPlus, faSync, faChevronCircleLeft, faChevronCircleRight, faEye, faEyeSlash, faRetweet, faGlobe, faKey, faSignOutAlt, faExclamationTriangle, faWrench); library.add(faUser, faLock, faPen, faShare, faTrash, faCheck, faTimes, faPlus, faSync, faChevronCircleLeft, faChevronCircleRight, faEye, faEyeSlash, faRetweet, faGlobe, faKey, faSignOutAlt, faExclamationTriangle, faWrench);
app.component('font-awesome-icon', FontAwesomeIcon); app.component('font-awesome-icon', FontAwesomeIcon);
+28 -34
View File
@@ -1,5 +1,5 @@
import { lcm, format } from "./utils"; import { lcm } from "./utils";
import { universalGetDayOfWeek } from "./i18n"; import { i18n, universalGetDayOfWeek } from "./i18n";
// YYC MARK: // YYC MARK:
// This file is synchronized with "dt.py". // This file is synchronized with "dt.py".
@@ -327,18 +327,19 @@ export function resolveLoopRules4Text(strl: string, startDateTime: number, timez
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
if ((match = PRECOMPILED_LOOP_RULES.year.exec(sp[0]!)) !== null) { if ((match = PRECOMPILED_LOOP_RULES.year.exec(sp[0]!)) !== null) {
if (match[1]! == 'S') loopRules = match[1]! == 'S'
loopRules = "严格模式。"; ? i18n.global.t('datetime-loopRuleText-modeStrict')
else : i18n.global.t('datetime-loopRuleText-modeRough');
loopRules = "宽松模式。"; loopRules += i18n.global.t('datetime-loopRuleText-year', [
loopRules += format("每{0}年于{1}循环一次。", parseInt(match[2]!),
parseInt(match[2]!), datetimeInstance.toLocaleDateString(undefined, { timeZone: "UTC" }) datetimeInstance.toLocaleDateString(undefined, { timeZone: "UTC" }),
); ]);
} else if ((match = PRECOMPILED_LOOP_RULES.month.exec(sp[0]!)) !== null) { } else if ((match = PRECOMPILED_LOOP_RULES.month.exec(sp[0]!)) !== null) {
if (match[1]! == 'S') // The mode prefix assigned here is faithfully overwritten by the switch
loopRules = "严格模式。"; // below (matching legacy behaviour); it also keeps definite-assignment happy.
else loopRules = match[1]! == 'S'
loopRules = "宽松模式。"; ? i18n.global.t('datetime-loopRuleText-modeStrict')
: i18n.global.t('datetime-loopRuleText-modeRough');
const dayInMonth = getDayInMonth( const dayInMonth = getDayInMonth(
datetimeInstance.getUTCFullYear(), datetimeInstance.getUTCFullYear(),
@@ -346,24 +347,16 @@ export function resolveLoopRules4Text(strl: string, startDateTime: number, timez
datetimeInstance.getUTCDate()); datetimeInstance.getUTCDate());
switch (match[2]!) { switch (match[2]!) {
case 'A': case 'A':
loopRules = format("每{0}月的第{1}日循环一次。", loopRules = i18n.global.t('datetime-loopRuleText-monthA', [parseInt(match[3]!), dayInMonth[0]]);
parseInt(match[3]!), dayInMonth[0]
);
break; break;
case 'B': case 'B':
loopRules = format("每{0}月的倒数第{1}日循环一次。", loopRules = i18n.global.t('datetime-loopRuleText-monthB', [parseInt(match[3]!), dayInMonth[1]]);
parseInt(match[3]!), dayInMonth[1]
);
break; break;
case 'C': case 'C':
loopRules = format("每{0}月的第{1}个星期{2}循环一次。", loopRules = i18n.global.t('datetime-loopRuleText-monthC', [parseInt(match[3]!), dayInMonth[2], dayInMonth[3]]);
parseInt(match[3]!), dayInMonth[2], dayInMonth[3]
);
break; break;
case 'D': case 'D':
loopRules = format("每{0}月的倒数第{1}个星期{2}循环一次。", loopRules = i18n.global.t('datetime-loopRuleText-monthD', [parseInt(match[3]!), dayInMonth[4], dayInMonth[5]]);
parseInt(match[3]!), dayInMonth[4], dayInMonth[5]
);
break; break;
} }
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(sp[0]!)) !== null) { } else if ((match = PRECOMPILED_LOOP_RULES.week.exec(sp[0]!)) !== null) {
@@ -373,22 +366,23 @@ export function resolveLoopRules4Text(strl: string, startDateTime: number, timez
weekOfDayCache.push(universalGetDayOfWeek(i)); weekOfDayCache.push(universalGetDayOfWeek(i));
} }
loopRules = format("每{0}周的{1}循环一次。", loopRules = i18n.global.t('datetime-loopRuleText-week', [
parseInt(match[2]!), weekOfDayCache.join(', ') parseInt(match[2]!),
); weekOfDayCache.join(', '),
]);
} else if ((match = PRECOMPILED_LOOP_RULES.day.exec(sp[0]!)) !== null) { } else if ((match = PRECOMPILED_LOOP_RULES.day.exec(sp[0]!)) !== null) {
loopRules = format("每{0}天循环一次。", parseInt(match[1]!)); loopRules = i18n.global.t('datetime-loopRuleText-day', [parseInt(match[1]!)]);
} else return ""; } else return "";
if ((match = PRECOMPILED_LOOP_STOP_RULES.infinity.exec(sp[1]!)) !== null) { if ((match = PRECOMPILED_LOOP_STOP_RULES.infinity.exec(sp[1]!)) !== null) {
loopStopRules = "永远循环。"; loopStopRules = i18n.global.t('datetime-loopStopRuleText-infinity');
} else if ((match = PRECOMPILED_LOOP_STOP_RULES.datetime.exec(sp[1]!)) !== null) { } else if ((match = PRECOMPILED_LOOP_STOP_RULES.datetime.exec(sp[1]!)) !== null) {
loopStopRules = format("到{0}停止循环。", loopStopRules = i18n.global.t('datetime-loopStopRuleText-datetime', [
new Date(parseInt(match[1]!)).toLocaleDateString() new Date(parseInt(match[1]!)).toLocaleDateString(),
); ]);
} else if ((match = PRECOMPILED_LOOP_STOP_RULES.times.exec(sp[1]!)) !== null) { } else if ((match = PRECOMPILED_LOOP_STOP_RULES.times.exec(sp[1]!)) !== null) {
loopStopRules = format("循环{0}次。", parseInt(match[1]!)); loopStopRules = i18n.global.t('datetime-loopStopRuleText-times', [parseInt(match[1]!)]);
} else return ""; } else return "";
return (loopRules + loopStopRules); return (loopRules + loopStopRules);
+23 -16
View File
@@ -1,31 +1,38 @@
import { createI18n } from 'vue-i18n'
import { messages as enUSMessages } from '@/locales/enUS'
import { messages as zhCNMessages } from '@/locales/zhCN'
export enum Language { export enum Language {
English, English,
SimplifiedChinese, SimplifiedChinese,
} }
export const MONTH_NAMES: string[] = [ export const i18n = createI18n({
"January", "February", "March", "April", "May", "June", legacy: false,
"July", "August", "September", "October", "November", "December" globalInjection: true,
]; locale: 'en-US',
fallbackLocale: 'en-US',
export const WEEK_NAMES: string[] = [ messages: {
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" 'en-US': enUSMessages,
]; 'zh-CN': zhCNMessages,
},
})
/** /**
* * Localised month name.
* @param month Zero-based month. * @param month Zero-based month (0 = January).
* @returns * @returns The translated month name.
*/ */
export function universalGetMonth(month: number): string { export function universalGetMonth(month: number): string {
return MONTH_NAMES[month] ?? ""; return i18n.global.t('universal-month-' + (month + 1))
} }
/** /**
* * Localised day-of-week name (Monday-first).
* @param dayOfWeek Zero-based day of week * @param dayOfWeek Zero-based day of week (0 = Monday ... 6 = Sunday).
* @returns * @returns The translated weekday name.
*/ */
export function universalGetDayOfWeek(dayOfWeek: number): string { export function universalGetDayOfWeek(dayOfWeek: number): string {
return WEEK_NAMES[dayOfWeek] ?? ""; return i18n.global.t('universal-week-' + (dayOfWeek + 1))
} }
-18
View File
@@ -10,24 +10,6 @@ export function lcm(a: number, b: number): number {
return a / gcd(a, b) * b; return a / gcd(a, b) * b;
} }
/**
* `{n}`
* @param str `null`/`undefined`/
* @param args
* @returns
*/
export function format(str: string, ...args: any[]): string {
// 若 str 为假值(null/undefined/''),直接返回空字符串
if (!str) return '';
return str.replace(/\{(\d+)\}/g, (match, index) => {
const idx = parseInt(index, 10);
const value = args[idx];
// 若参数存在且不为 null/undefined,则转为字符串,否则保留占位符
return value != null ? String(value) : match;
});
}
/** /**
* Get Monday-first the day of week from given date instead of Sunday-first. * Get Monday-first the day of week from given date instead of Sunday-first.
* @param date The date for getting weekday. * @param date The date for getting weekday.
+14 -14
View File
@@ -18,6 +18,7 @@ import {
} from '@/api/admin'; } from '@/api/admin';
import TokenItem from '@/components/admin/TokenItem.vue'; import TokenItem from '@/components/admin/TokenItem.vue';
import UserItem from '@/components/admin/UserItem.vue'; import UserItem from '@/components/admin/UserItem.vue';
import { i18n } from '@/utils/i18n';
const token = useTokenStore(); const token = useTokenStore();
const messagebox = ref<InstanceType<typeof MessageBox>>(); const messagebox = ref<InstanceType<typeof MessageBox>>();
@@ -54,10 +55,10 @@ const changePassword = async () => {
const rv = await apiProfileChangePassword(token.currentToken, newpassword); const rv = await apiProfileChangePassword(token.currentToken, newpassword);
if (rv) { if (rv) {
messagebox.value?.show("Operation OK."); messagebox.value?.show(i18n.global.t('js-success'));
profilePendingPassword.value = ""; profilePendingPassword.value = "";
} else { } else {
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-update'));
} }
} }
@@ -85,7 +86,7 @@ const refreshTokenList = async () => {
const deleteTokenItem = async (uuid: string) => { const deleteTokenItem = async (uuid: string) => {
const rv = await apiProfileDeleteToken(token.currentToken, uuid); const rv = await apiProfileDeleteToken(token.currentToken, uuid);
if (!rv) { if (!rv) {
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-delete'));
return; return;
} }
@@ -123,7 +124,7 @@ const addUser = async () => {
const rv = await apiAdminAdd(token.currentToken, username); const rv = await apiAdminAdd(token.currentToken, username);
if (typeof rv === 'undefined') { if (typeof rv === 'undefined') {
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-add'));
return; return;
} }
@@ -133,7 +134,7 @@ const addUser = async () => {
const deleteUser = async (username: string) => { const deleteUser = async (username: string) => {
const rv = await apiAdminDel(token.currentToken, username); const rv = await apiAdminDel(token.currentToken, username);
if (!rv) { if (!rv) {
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-delete'));
return; return;
} }
@@ -147,7 +148,7 @@ const updateUser = async (username: string, newPassword: string, newIsAdmin: boo
const rv = await apiAdminUpdate(token.currentToken, username, passwordArg, isAdminArg); const rv = await apiAdminUpdate(token.currentToken, username, passwordArg, isAdminArg);
if (!rv) { if (!rv) {
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-update'));
return; return;
} }
@@ -163,15 +164,15 @@ const updateUser = async (username: string, newPassword: string, newIsAdmin: boo
<div class="container" style="margin-top: 20px;"> <div class="container" style="margin-top: 20px;">
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li :class="{ 'is-active': activeTab === 1 }" @click="activeTab = 1"><a>My Profile</a></li> <li :class="{ 'is-active': activeTab === 1 }" @click="activeTab = 1"><a>{{ $t('admin-tabcontrol-tabProfile') }}</a></li>
<li :class="{ 'is-active': activeTab === 2 }" @click="activeTab = 2"><a>Manage Multi-login</a></li> <li :class="{ 'is-active': activeTab === 2 }" @click="activeTab = 2"><a>{{ $t('admin-tabcontrol-tabToken') }}</a></li>
<li v-if="showUserTab" :class="{ 'is-active': activeTab === 3 }" @click="activeTab = 3"><a>Manager User</a></li> <li v-if="showUserTab" :class="{ 'is-active': activeTab === 3 }" @click="activeTab = 3"><a>{{ $t('admin-tabcontrol-tabUserList') }}</a></li>
</ul> </ul>
</div> </div>
</div> </div>
<div v-show="activeTab === 1" class="container" style="margin-top: 20px;"> <div v-show="activeTab === 1" class="container" style="margin-top: 20px;">
<h1 class="title">Change Password</h1> <h1 class="title">{{ $t('admin-changePassword') }}</h1>
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
<input v-model="profilePendingPassword" class="input" type="password"> <input v-model="profilePendingPassword" class="input" type="password">
@@ -185,9 +186,8 @@ const updateUser = async (username: string, newPassword: string, newIsAdmin: boo
</div> </div>
<div v-show="activeTab === 2" class="container" style="margin-top: 20px;"> <div v-show="activeTab === 2" class="container" style="margin-top: 20px;">
<h1 class="title">Manage multi-login</h1> <h1 class="title">{{ $t('admin-manageToken') }}</h1>
<h2 class="subtitle">Manage the multi-login of the current account. You can forced logout some login in <h2 class="subtitle">{{ $t('admin-manageToken-desc') }}</h2>
there.</h2>
<div class="control" @click="refreshTokenList"> <div class="control" @click="refreshTokenList">
<a class="button is-primary"> <a class="button is-primary">
<span class="icon is-small"><font-awesome-icon icon="fas fa-sync"></font-awesome-icon></span> <span class="icon is-small"><font-awesome-icon icon="fas fa-sync"></font-awesome-icon></span>
@@ -205,7 +205,7 @@ const updateUser = async (username: string, newPassword: string, newIsAdmin: boo
</div> </div>
<div v-show="activeTab === 3" class="container" style="margin-top: 20px;"> <div v-show="activeTab === 3" class="container" style="margin-top: 20px;">
<h1 class="title">User List</h1> <h1 class="title">{{ $t('admin-userList') }}</h1>
<div class="button-list"> <div class="button-list">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
+14 -14
View File
@@ -20,7 +20,7 @@ import {
type SharedCollectionRow, type SharedCollectionRow,
} from '@/api/collection'; } from '@/api/collection';
import { DAY1_SPAN, dayOfWeek, resolveLoopRules2Event, resolveLoopRules4Text } from '@/utils/datetime'; import { DAY1_SPAN, dayOfWeek, resolveLoopRules2Event, resolveLoopRules4Text } from '@/utils/datetime';
import { universalGetMonth, WEEK_NAMES } from '@/utils/i18n'; import { universalGetMonth } from '@/utils/i18n';
import { getWeekday } from '@/utils/utils'; import { getWeekday } from '@/utils/utils';
enum CalendarTab { enum CalendarTab {
@@ -248,11 +248,11 @@ onMounted(async () => {
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li :class="{ 'is-active': activeTab === CalendarTab.Calendar }" @click="activeTab = CalendarTab.Calendar"> <li :class="{ 'is-active': activeTab === CalendarTab.Calendar }" @click="activeTab = CalendarTab.Calendar">
<a>Calendar</a> <a>{{ $t('calendar-tabcontrol-tabCalendar') }}</a>
</li> </li>
<li :class="{ 'is-active': activeTab === CalendarTab.Collection }" <li :class="{ 'is-active': activeTab === CalendarTab.Collection }"
@click="activeTab = CalendarTab.Collection"><a>Collection</a></li> @click="activeTab = CalendarTab.Collection"><a>{{ $t('calendar-tabcontrol-tabCollection') }}</a></li>
<li :class="{ 'is-active': activeTab === CalendarTab.Display }" @click="activeTab = CalendarTab.Display"><a>Display</a></li> <li :class="{ 'is-active': activeTab === CalendarTab.Display }" @click="activeTab = CalendarTab.Display"><a>{{ $t('calendar-tabcontrol-tabDisplay') }}</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -284,17 +284,17 @@ onMounted(async () => {
<nav class="level is-mobile"> <nav class="level is-mobile">
<div class="level-item control"> <div class="level-item control">
<a class="button is-info" @click="goToday">Today</a> <a class="button is-info" @click="goToday">{{ $t('calendar-calendar-today') }}</a>
</div> </div>
<div class="level-item control"> <div class="level-item control">
<a class="button is-primary" @click="goAddEvent">Add...</a> <a class="button is-primary" @click="goAddEvent">{{ $t('calendar-calendar-add') }}</a>
</div> </div>
</nav> </nav>
<CalendarGrid :cells="displayCache" /> <CalendarGrid :cells="displayCache" />
<div class="container" style="padding: 1.25rem; display: flex; flex-flow: column; margin-top: 1.25rem;"> <div class="container" style="padding: 1.25rem; display: flex; flex-flow: column; margin-top: 1.25rem;">
<h1 class="title">Schedule</h1> <h1 class="title">{{ $t('calendar-calendar-scheduleList') }}</h1>
<ScheduleList :cells="displayCache" @edit="onEventEdit" /> <ScheduleList :cells="displayCache" @edit="onEventEdit" />
</div> </div>
</div> </div>
@@ -305,14 +305,14 @@ onMounted(async () => {
<span class="icon is-small"><font-awesome-icon icon="fas fa-sync"></font-awesome-icon></span> <span class="icon is-small"><font-awesome-icon icon="fas fa-sync"></font-awesome-icon></span>
</a> </a>
</div> </div>
<h1 class="title">My collections</h1> <h1 class="title">{{ $t('calendar-owned-list') }}</h1>
<div style="display: flex; flex-flow: column; margin-top: 1.25rem; margin-bottom: 1.25rem;"> <div style="display: flex; flex-flow: column; margin-top: 1.25rem; margin-bottom: 1.25rem;">
<div v-for="c in ownedCollections" :key="c[0]" <div v-for="c in ownedCollections" :key="c[0]"
style="display: flex; flex-flow: column; margin-top: 1.25rem;"> style="display: flex; flex-flow: column; margin-top: 1.25rem;">
<OwnedItem :name="c[1]" :is-visible="ownedVisible.get(c[0]) ?? false" @toggle="toggleOwned(c[0])" /> <OwnedItem :name="c[1]" :is-visible="ownedVisible.get(c[0]) ?? false" @toggle="toggleOwned(c[0])" />
</div> </div>
</div> </div>
<h1 class="title">Shared collections</h1> <h1 class="title">{{ $t('calendar-shared-list') }}</h1>
<div style="display: flex; flex-flow: column; margin-top: 1.25rem; margin-bottom: 1.25rem;"> <div style="display: flex; flex-flow: column; margin-top: 1.25rem; margin-bottom: 1.25rem;">
<div v-for="c in sharedCollections" :key="c[0]" <div v-for="c in sharedCollections" :key="c[0]"
style="display: flex; flex-flow: column; margin-top: 1.25rem;"> style="display: flex; flex-flow: column; margin-top: 1.25rem;">
@@ -324,22 +324,22 @@ onMounted(async () => {
<div v-show="activeTab === CalendarTab.Display" class="container" style="margin-top: 20px;"> <div v-show="activeTab === CalendarTab.Display" class="container" style="margin-top: 20px;">
<div class="field"> <div class="field">
<label class="label">The first day of week</label> <label class="label">{{ $t('calendar-display-firstDayOfWeek') }}</label>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select> <select>
<option v-for="(w, i) in WEEK_NAMES" :key="i">{{ w }}</option> <option v-for="i in 7" :key="i">{{ $t('universal-week-' + i) }}</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Sub-Calendar</label> <label class="label">{{ $t('calendar-display-subcalendar') }}</label>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select> <select>
<option>None</option> <option>{{ $t('calendar-display-subcalendar-none') }}</option>
<option>Chinese Lunisolar Calendar</option> <option>{{ $t('calendar-display-subcalendar-chineseLunisolarCalendar') }}</option>
</select> </select>
</div> </div>
</div> </div>
+51 -54
View File
@@ -15,8 +15,8 @@ import {
} from '@/api/calendar'; } from '@/api/calendar';
import { getFullOwn as apiCollectionGetFullOwn, type CollectionRow } from '@/api/collection'; import { getFullOwn as apiCollectionGetFullOwn, type CollectionRow } from '@/api/collection';
import { resolveLoopRules4UI, getDayInMonth } from '@/utils/datetime'; import { resolveLoopRules4UI, getDayInMonth } from '@/utils/datetime';
import { WEEK_NAMES } from '@/utils/i18n'; import { i18n } from '@/utils/i18n';
import { format, getWeekday, DEFAULT_COLOR } from '@/utils/utils'; import { getWeekday, DEFAULT_COLOR } from '@/utils/utils';
const token = useTokenStore(); const token = useTokenStore();
const route = useRoute(); const route = useRoute();
@@ -75,10 +75,10 @@ const showBoxLoopStopTimes = computed<boolean>(() => loopStop.value === 'times')
const monthOptionLabels = computed(() => { const monthOptionLabels = computed(() => {
const d = getDayInMonth(startDt.value.getFullYear(), startDt.value.getMonth() + 1, startDt.value.getDate()); const d = getDayInMonth(startDt.value.getFullYear(), startDt.value.getMonth() + 1, startDt.value.getDate());
return { return {
a: format('Day {0} in month', d[0]), a: i18n.global.t('event-loopWeek-optionA', [d[0]]),
b: format('Day {0} from the end of the month', d[1]), b: i18n.global.t('event-loopWeek-optionB', [d[1]]),
c: format('Day {1} in week {0}', d[2], d[3] + 1), c: i18n.global.t('event-loopWeek-optionC', [d[2], d[3] + 1]),
d: format('Day {1} in week {0} from the end of the month', d[4], d[5] + 1), d: i18n.global.t('event-loopWeek-optionD', [d[4], d[5] + 1]),
}; };
}) })
@@ -369,7 +369,7 @@ const getForm = (): [string, string, string, number, number, number, string] | u
const submit = async () => { const submit = async () => {
const submitData = getForm(); const submitData = getForm();
if (typeof submitData === 'undefined') { if (typeof submitData === 'undefined') {
messagebox.value?.show("Your filled event form is not fufilled or have error. Please check it and try again."); messagebox.value?.show(i18n.global.t('js-fail-form'));
return; return;
} }
@@ -386,7 +386,7 @@ const submit = async () => {
submitData[5], submitData[5],
); );
if (typeof result === 'undefined') { if (typeof result === 'undefined') {
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-add'));
} else { } else {
goToCalendar(); goToCalendar();
} }
@@ -405,7 +405,7 @@ const submit = async () => {
ev[7] === submitData[5] ? undefined : submitData[5], ev[7] === submitData[5] ? undefined : submitData[5],
); );
if (typeof result === 'undefined') { if (typeof result === 'undefined') {
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-update'));
} else { } else {
goToCalendar(); goToCalendar();
} }
@@ -425,29 +425,29 @@ onMounted(async () => {
<template> <template>
<div class="eventFormBody container" style="margin-top: 20px;"> <div class="eventFormBody container" style="margin-top: 20px;">
<h1 class="title">Edit Event</h1> <h1 class="title">{{ $t('event-header') }}</h1>
<section class="section"> <section class="section">
<div class="field"> <div class="field">
<label class="label">Title</label> <label class="label">{{ $t('event-title') }}</label>
<div class="control"> <div class="control">
<input v-model="title" class="input" type="text"> <input v-model="title" class="input" type="text">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Description</label> <label class="label">{{ $t('event-description') }}</label>
<div class="control"> <div class="control">
<textarea v-model="description" class="textarea"></textarea> <textarea v-model="description" class="textarea"></textarea>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Color</label> <label class="label">{{ $t('event-color') }}</label>
<div class="control"> <div class="control">
<input v-model="color" class="input" type="color"> <input v-model="color" class="input" type="color">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Collection</label> <label class="label">{{ $t('event-collection') }}</label>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select v-model="belongTo"> <select v-model="belongTo">
@@ -459,18 +459,18 @@ onMounted(async () => {
</section> </section>
<section class="section"> <section class="section">
<h2 class="subtitle">Start Date Time</h2> <h2 class="subtitle">{{ $t('event-startDateTime') }}</h2>
<a class="button" @click="openStartPicker"> <a class="button" @click="openStartPicker">
<span>{{ startDt.toLocaleString() }}</span> <span>{{ startDt.toLocaleString() }}</span>
</a> </a>
<h2 class="subtitle">Stop Date Time</h2> <h2 class="subtitle">{{ $t('event-endDateTime') }}</h2>
<div class="button-list"> <div class="button-list">
<div class="control"> <div class="control">
<a class="button is-link" @click="spot">Spot</a> <a class="button is-link" @click="spot">{{ $t('event-btnSpot') }}</a>
</div> </div>
<div class="control"> <div class="control">
<a class="button is-link" @click="fullDay">Full day</a> <a class="button is-link" @click="fullDay">{{ $t('event-btnFullDay') }}</a>
</div> </div>
</div> </div>
<a class="button" @click="openEndPicker"> <a class="button" @click="openEndPicker">
@@ -479,33 +479,33 @@ onMounted(async () => {
</section> </section>
<section class="section"> <section class="section">
<h2 class="subtitle">Event Loop</h2> <h2 class="subtitle">{{ $t('event-loop') }}</h2>
<div class="button-list"> <div class="button-list">
<label class="radio"> <label class="radio">
<input type="radio" value="never" v-model="loopMethod"> <input type="radio" value="never" v-model="loopMethod">
<span>Never</span> <span>{{ $t('event-loop-never') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="day" v-model="loopMethod"> <input type="radio" value="day" v-model="loopMethod">
<span>Day</span> <span>{{ $t('event-loop-day') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="week" v-model="loopMethod"> <input type="radio" value="week" v-model="loopMethod">
<span>Week</span> <span>{{ $t('event-loop-week') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="month" v-model="loopMethod"> <input type="radio" value="month" v-model="loopMethod">
<span>Month</span> <span>{{ $t('event-loop-month') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="year" v-model="loopMethod"> <input type="radio" value="year" v-model="loopMethod">
<span>Year</span> <span>{{ $t('event-loop-year') }}</span>
</label> </label>
</div> </div>
<div v-show="showBoxLoopDay"> <div v-show="showBoxLoopDay">
<div class="field"> <div class="field">
<label class="label">Day span</label> <label class="label">{{ $t('event-loopDay-span') }}</label>
<div class="control"> <div class="control">
<input v-model="loopDaySpan" class="input spanpicker" type="number" min="1" max="100" step="1"> <input v-model="loopDaySpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div> </div>
@@ -513,54 +513,54 @@ onMounted(async () => {
</div> </div>
<div v-show="showBoxLoopWeek"> <div v-show="showBoxLoopWeek">
<div class="field"> <div class="field">
<label class="label">Week span</label> <label class="label">{{ $t('event-loopWeek-span') }}</label>
<div class="control"> <div class="control">
<input v-model="loopWeekSpan" class="input spanpicker" type="number" min="1" max="100" step="1"> <input v-model="loopWeekSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Week options</label> <label class="label">{{ $t('event-loopWeek-option') }}</label>
<div class="button-list"> <div class="button-list">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="loopWeekChecks[0]"> <input type="checkbox" v-model="loopWeekChecks[0]">
<span>{{ WEEK_NAMES[0] }}</span> <span>{{ $t('universal-week-1') }}</span>
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="loopWeekChecks[1]"> <input type="checkbox" v-model="loopWeekChecks[1]">
<span>{{ WEEK_NAMES[1] }}</span> <span>{{ $t('universal-week-2') }}</span>
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="loopWeekChecks[2]"> <input type="checkbox" v-model="loopWeekChecks[2]">
<span>{{ WEEK_NAMES[2] }}</span> <span>{{ $t('universal-week-3') }}</span>
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="loopWeekChecks[3]"> <input type="checkbox" v-model="loopWeekChecks[3]">
<span>{{ WEEK_NAMES[3] }}</span> <span>{{ $t('universal-week-4') }}</span>
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="loopWeekChecks[4]"> <input type="checkbox" v-model="loopWeekChecks[4]">
<span>{{ WEEK_NAMES[4] }}</span> <span>{{ $t('universal-week-5') }}</span>
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="loopWeekChecks[5]"> <input type="checkbox" v-model="loopWeekChecks[5]">
<span>{{ WEEK_NAMES[5] }}</span> <span>{{ $t('universal-week-6') }}</span>
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="loopWeekChecks[6]"> <input type="checkbox" v-model="loopWeekChecks[6]">
<span>{{ WEEK_NAMES[6] }}</span> <span>{{ $t('universal-week-7') }}</span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div v-show="showBoxLoopMonth"> <div v-show="showBoxLoopMonth">
<div class="field"> <div class="field">
<label class="label">Month span</label> <label class="label">{{ $t('event-loopMonth-span') }}</label>
<div class="control"> <div class="control">
<input v-model="loopMonthSpan" class="input spanpicker" type="number" min="1" max="100" step="1"> <input v-model="loopMonthSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Month mode</label> <label class="label">{{ $t('event-loopMonth-option') }}</label>
<div class="button-list"> <div class="button-list">
<label class="radio"> <label class="radio">
<input type="radio" value="A" v-model="loopMonthMode"> <input type="radio" value="A" v-model="loopMonthMode">
@@ -583,7 +583,7 @@ onMounted(async () => {
</div> </div>
<div v-show="showBoxLoopYear"> <div v-show="showBoxLoopYear">
<div class="field"> <div class="field">
<label class="label">Year span</label> <label class="label">{{ $t('event-loopYear-span') }}</label>
<div class="control"> <div class="control">
<input v-model="loopYearSpan" class="input spanpicker" type="number" min="1" max="100" step="1"> <input v-model="loopYearSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div> </div>
@@ -592,19 +592,19 @@ onMounted(async () => {
</section> </section>
<section v-show="showBoxLoopStop" class="section"> <section v-show="showBoxLoopStop" class="section">
<h2 class="subtitle">Event Loop Stop</h2> <h2 class="subtitle">{{ $t('event-loopStop') }}</h2>
<div class="button-list"> <div class="button-list">
<label class="radio"> <label class="radio">
<input type="radio" value="forever" v-model="loopStop"> <input type="radio" value="forever" v-model="loopStop">
<span>Forever</span> <span>{{ $t('event-loopStop-forever') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="datetime" v-model="loopStop"> <input type="radio" value="datetime" v-model="loopStop">
<span>Date Time</span> <span>{{ $t('event-loopStop-datetime') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="times" v-model="loopStop"> <input type="radio" value="times" v-model="loopStop">
<span>Times</span> <span>{{ $t('event-loopStop-times') }}</span>
</label> </label>
</div> </div>
<div v-show="showBoxLoopStopDateTime"> <div v-show="showBoxLoopStopDateTime">
@@ -622,42 +622,39 @@ onMounted(async () => {
</section> </section>
<section v-show="showBoxStrictMode" class="section"> <section v-show="showBoxStrictMode" class="section">
<h2 class="subtitle">Strict Mode in Event Loop</h2> <h2 class="subtitle">{{ $t('event-strictMode-title') }}</h2>
<p>You can choose strict mode or rough mode in following content. This is only effect on looped event.</p> <p>{{ $t('event-strictMode-warning') }}</p>
<div class="button-list"> <div class="button-list">
<label class="radio"> <label class="radio">
<input type="radio" value="strict" v-model="strictMode"> <input type="radio" value="strict" v-model="strictMode">
<span>Strict Mode. If ordered day is not existing, skip it.</span> <span>{{ $t('event-strictMode-strict') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="rough" v-model="strictMode"> <input type="radio" value="rough" v-model="strictMode">
<span>Rough mode. If ordered day is not existing, choose the day closing with original day to arrange <span>{{ $t('event-strictMode-rough') }}</span>
event.</span>
</label> </label>
</div> </div>
</section> </section>
<section v-show="showTimezoneBox" class="section"> <section v-show="showTimezoneBox" class="section">
<h2 class="subtitle">Timezone</h2> <h2 class="subtitle">{{ $t('event-timezone-title') }}</h2>
<p>The timezone of this event is not corresponding with your current timezone. All of date and time in this page <p>{{ $t('event-timezone-warning') }}</p>
are shown as the original timezone of this event. You can choose a timezone option in follwing content. If you
are not familar with this, please pick keep timezone.</p>
<div class="button-list"> <div class="button-list">
<label class="radio"> <label class="radio">
<input type="radio" value="keep" v-model="timezoneOption"> <input type="radio" value="keep" v-model="timezoneOption">
<span>Keep timezone</span> <span>{{ $t('event-timezone-keep') }}</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="replace" v-model="timezoneOption"> <input type="radio" value="replace" v-model="timezoneOption">
<span>Use my timezone</span> <span>{{ $t('event-timezone-replace') }}</span>
</label> </label>
</div> </div>
</section> </section>
<section class="section"> <section class="section">
<div class="button-list"> <div class="button-list">
<a class="button is-success" @click="submit">Submit</a> <a class="button is-success" @click="submit">{{ $t('event-btnSubmit') }}</a>
<a class="button" @click="cancel">Cancel</a> <a class="button" @click="cancel">{{ $t('event-btnCancel') }}</a>
</div> </div>
</section> </section>
+10 -9
View File
@@ -15,6 +15,7 @@ import {
} from '@/api/collection'; } from '@/api/collection';
import OwnedItem from '@/components/collection/OwnedItem.vue'; import OwnedItem from '@/components/collection/OwnedItem.vue';
import SharingItem from '@/components/collection/SharingItem.vue'; import SharingItem from '@/components/collection/SharingItem.vue';
import { i18n } from '@/utils/i18n';
const ownedItems = ref<Map<string, CollectionRow>>(new Map()); const ownedItems = ref<Map<string, CollectionRow>>(new Map());
const sharingItems = ref<string[]>([]); const sharingItems = ref<string[]>([]);
@@ -61,14 +62,14 @@ const addOwnedItem = async () => {
// first add it // first add it
const rv = await apiCollectionAddOwn(token.currentToken, new_name); const rv = await apiCollectionAddOwn(token.currentToken, new_name);
if (typeof rv === 'undefined') { if (typeof rv === 'undefined') {
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-add'));
return; return;
} }
// second get its detail // second get its detail
const rv2 = await apiCollectionGetDetailOwn(token.currentToken, rv); const rv2 = await apiCollectionGetDetailOwn(token.currentToken, rv);
if (typeof rv2 === 'undefined') { if (typeof rv2 === 'undefined') {
messagebox.value?.show("A get operation failed. It may caused by server internal error or your limited permission. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-get'));
return; return;
} }
@@ -79,7 +80,7 @@ const addOwnedItem = async () => {
const deleteOwnedItem = async (uuid: string) => { const deleteOwnedItem = async (uuid: string) => {
const rv = await apiCollectionDeleteOwn(token.currentToken, uuid, ownedItems.value.get(uuid)![2]); const rv = await apiCollectionDeleteOwn(token.currentToken, uuid, ownedItems.value.get(uuid)![2]);
if (!rv) { if (!rv) {
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-delete'));
return; return;
} }
@@ -115,7 +116,7 @@ const updateOwnedItem = async (uuid: string, name: string) => {
const lastChange = ownedItems.value.get(uuid)![2]; const lastChange = ownedItems.value.get(uuid)![2];
const rv = await apiCollectionUpdateOwn(token.currentToken, uuid, name, lastChange); const rv = await apiCollectionUpdateOwn(token.currentToken, uuid, name, lastChange);
if (typeof rv === 'undefined') { if (typeof rv === 'undefined') {
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-update'));
return; return;
} }
@@ -162,7 +163,7 @@ const addSharingItem = async () => {
// order adding // order adding
const rv = await apiCollectionAddSharing(token.currentToken, uuid, new_username, lastChange); const rv = await apiCollectionAddSharing(token.currentToken, uuid, new_username, lastChange);
if (typeof rv === 'undefined') { if (typeof rv === 'undefined') {
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-add'));
return; return;
} }
@@ -179,7 +180,7 @@ const deleteSharingItem = async (username: string) => {
// order deleting // order deleting
const rv = await apiCollectionDeleteSharing(token.currentToken, uuid, username, lastChange); const rv = await apiCollectionDeleteSharing(token.currentToken, uuid, username, lastChange);
if (typeof rv === 'undefined') { if (typeof rv === 'undefined') {
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-delete'));
return; return;
} }
@@ -193,7 +194,7 @@ const deleteSharingItem = async (username: string) => {
<template> <template>
<div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;"> <div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
<h1 class="title">Owned</h1> <h1 class="title">{{ $t('collection-owned-list') }}</h1>
<div class="button-list"> <div class="button-list">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
@@ -219,9 +220,9 @@ const deleteSharingItem = async (username: string) => {
</div> </div>
<div v-show="isSharingItem" class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;"> <div v-show="isSharingItem" class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
<h1 class="title">Sharing target</h1> <h1 class="title">{{ $t('collection-sharing-list') }}</h1>
<label class="label"> <label class="label">
<span>Editing: </span> <span>{{ $t('collection-sharing-editing') }}</span>
<span>{{ isSharingItem ? ownedItems.get(sharingItemUuid!)![1] : "" }}</span> <span>{{ isSharingItem ? ownedItems.get(sharingItemUuid!)![1] : "" }}</span>
</label> </label>
+12 -4
View File
@@ -4,11 +4,19 @@
<div class="container" style="margin-top: 1.25rem;"> <div class="container" style="margin-top: 1.25rem;">
<article> <article>
<h1 class="title">coconut-leaf</h1> <h1 class="title">coconut-leaf</h1>
<p>A light, self-host and multi-account calendar system.</p> <p>{{ $t('home-tagline') }}</p>
<p>The original intention of this system is served for yyc12345 personal use.</p> <p>{{ $t('home-intent') }}</p>
<br /> <br />
<p>See our <a href="https://github.com/yyc12345/coconut-leaf">GitHub project</a> for the source code in detail.</p> <i18n-t keypath="home-source" tag="p">
<p>The source code of this project is licensed under <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL v3</a>.</p> <template #link>
<a href="https://github.com/yyc12345/coconut-leaf">{{ $t('home-source-link') }}</a>
</template>
</i18n-t>
<i18n-t keypath="home-license" tag="p">
<template #link>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">{{ $t('home-license-link') }}</a>
</template>
</i18n-t>
</article> </article>
</div> </div>
</template> </template>
+5 -4
View File
@@ -4,6 +4,7 @@ import { useTokenStore } from '@/stores/token';
import MessageBox from '@/components/MessageBox.vue'; import MessageBox from '@/components/MessageBox.vue';
import { webLogin as apiCommonWebLogin } from '@/api/common'; import { webLogin as apiCommonWebLogin } from '@/api/common';
import { goToHome } from '@/router'; import { goToHome } from '@/router';
import { i18n } from '@/utils/i18n';
const isLoggingIn = ref<boolean>(false); const isLoggingIn = ref<boolean>(false);
const username = ref<string>(""); const username = ref<string>("");
@@ -35,7 +36,7 @@ const login = async () => {
goToHome(); goToHome();
} else { } else {
// Show login error. // Show login error.
messagebox.value?.show("Fail to login. Please check your username or password."); messagebox.value?.show(i18n.global.t('js-fail-login'));
} }
// Enable all UI // Enable all UI
@@ -48,7 +49,7 @@ const login = async () => {
<div style="margin-top: 1.25rem; width: 100%; display: flex; justify-content: center; align-items: center;"> <div style="margin-top: 1.25rem; width: 100%; display: flex; justify-content: center; align-items: center;">
<div class="card" style="padding: 1.25rem;"> <div class="card" style="padding: 1.25rem;">
<div class="field"> <div class="field">
<label class="label">User Name</label> <label class="label">{{ $t('login-form-username') }}</label>
<div class="control has-icons-left has-icons-right"> <div class="control has-icons-left has-icons-right">
<input v-model="username" :disabled="isLoggingIn" class="input" type="text"> <input v-model="username" :disabled="isLoggingIn" class="input" type="text">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
@@ -57,7 +58,7 @@ const login = async () => {
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Password</label> <label class="label">{{ $t('login-form-password') }}</label>
<p class="control has-icons-left"> <p class="control has-icons-left">
<input v-model="password" :disabled="isLoggingIn" class="input" type="password"> <input v-model="password" :disabled="isLoggingIn" class="input" type="password">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
@@ -67,7 +68,7 @@ const login = async () => {
</div> </div>
<div class="control"> <div class="control">
<button class="button is-primary" :disabled="isLoggingIn" @click="login">Login</button> <button class="button is-primary" :disabled="isLoggingIn" @click="login">{{ $t('login-form-login') }}</button>
</div> </div>
</div> </div>
+3 -3
View File
@@ -3,9 +3,9 @@
<template> <template>
<div class="container"> <div class="container">
<article> <article>
<h1 class="title">Oops!</h1> <h1 class="title">{{ $t('notfound-title') }}</h1>
<p>You are wandering in the desert of coconut-leaf.</p> <p>{{ $t('notfound-desc1') }}</p>
<p>Please back to previous page and try again.</p> <p>{{ $t('notfound-desc2') }}</p>
</article> </article>
</div> </div>
</template> </template>
+5 -4
View File
@@ -10,6 +10,7 @@ import {
type TodoRow, type TodoRow,
} from '@/api/todo'; } from '@/api/todo';
import TodoItem from '@/components/todo/TodoItem.vue'; import TodoItem from '@/components/todo/TodoItem.vue';
import { i18n } from '@/utils/i18n';
const todoItems = ref<Map<string, TodoRow>>(new Map()); const todoItems = ref<Map<string, TodoRow>>(new Map());
@@ -40,7 +41,7 @@ const refreshTodoItem = async () => {
const addTodoItem = async () => { const addTodoItem = async () => {
const rv = await apiTodoAdd(token.currentToken); const rv = await apiTodoAdd(token.currentToken);
if (typeof rv === 'undefined') { if (typeof rv === 'undefined') {
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-add'));
return; return;
} }
@@ -50,7 +51,7 @@ const addTodoItem = async () => {
const deleteTodoItem = async (uuid: string) => { const deleteTodoItem = async (uuid: string) => {
const rv = await apiTodoDel(token.currentToken, uuid, todoItems.value.get(uuid)![3]); const rv = await apiTodoDel(token.currentToken, uuid, todoItems.value.get(uuid)![3]);
if (!rv) { if (!rv) {
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-delete'));
return; return;
} }
@@ -62,7 +63,7 @@ const updateTodoItem = async (uuid: string, data: string) => {
const lastChange = todoItems.value.get(uuid)![3]; const lastChange = todoItems.value.get(uuid)![3];
const rv = await apiTodoUpdate(token.currentToken, uuid, data, lastChange); const rv = await apiTodoUpdate(token.currentToken, uuid, data, lastChange);
if (typeof rv === 'undefined') { if (typeof rv === 'undefined') {
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data."); messagebox.value?.show(i18n.global.t('js-fail-update'));
return; return;
} }
@@ -76,7 +77,7 @@ const updateTodoItem = async (uuid: string, data: string) => {
<template> <template>
<div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;"> <div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
<h1 class="title">Todo list</h1> <h1 class="title">{{ $t('todo-todoList') }}</h1>
<div class="button-list"> <div class="button-list">
<div class="control" @click="addTodoItem"> <div class="control" @click="addTodoItem">
<a class="button is-primary"> <a class="button is-primary">