feat: lang
TS Lint / Oxlint (push) Successful in 43s
TS Lint / TypeScript (push) Successful in 49s
TS Lint / ESLint (push) Successful in 1m10s

This commit is contained in:
2026-07-14 10:45:35 +02:00
parent d969b9e21b
commit c7b1f3ed49
16 changed files with 237 additions and 55 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
- [x] list option: hide completed - [x] list option: hide completed
- [x] options in cookies - [x] options in cookies
- [ ] Empty list message - [ ] Empty list message
- [ ] change lang - [x] change lang
- [ ] toasts - [ ] toasts
- [ ] handle errors - [ ] handle errors
- [ ] list per URL - [ ] list per URL
+13
View File
@@ -9,6 +9,7 @@
"cron-parser": "^5.6.1", "cron-parser": "^5.6.1",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"vue": "^3.5.38", "vue": "^3.5.38",
"vue-i18n": "^11.4.6",
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
@@ -134,6 +135,14 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@intlify/core-base": ["@intlify/core-base@11.4.6", "", { "dependencies": { "@intlify/devtools-types": "11.4.6", "@intlify/message-compiler": "11.4.6", "@intlify/shared": "11.4.6" } }, "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw=="],
"@intlify/devtools-types": ["@intlify/devtools-types@11.4.6", "", { "dependencies": { "@intlify/core-base": "11.4.6", "@intlify/shared": "11.4.6" } }, "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA=="],
"@intlify/message-compiler": ["@intlify/message-compiler@11.4.6", "", { "dependencies": { "@intlify/shared": "11.4.6", "source-map-js": "^1.0.2" } }, "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q=="],
"@intlify/shared": ["@intlify/shared@11.4.6", "", {}, "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -340,6 +349,8 @@
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.39", "", { "dependencies": { "@vue/compiler-dom": "3.5.39", "@vue/shared": "3.5.39" } }, "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw=="], "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.39", "", { "dependencies": { "@vue/compiler-dom": "3.5.39", "@vue/shared": "3.5.39" } }, "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw=="],
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@vue/devtools-core": ["@vue/devtools-core@8.1.5", "", { "dependencies": { "@vue/devtools-kit": "^8.1.5", "@vue/devtools-shared": "^8.1.5" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-5e5jQOEssCdZA1wlFEUkIDtb+cAOWuLNWJ52fm4PBWbF7e3oTnM2fneaL42E5lJoolAaUQ678tv/XEb3h4e86Q=="], "@vue/devtools-core": ["@vue/devtools-core@8.1.5", "", { "dependencies": { "@vue/devtools-kit": "^8.1.5", "@vue/devtools-shared": "^8.1.5" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-5e5jQOEssCdZA1wlFEUkIDtb+cAOWuLNWJ52fm4PBWbF7e3oTnM2fneaL42E5lJoolAaUQ678tv/XEb3h4e86Q=="],
"@vue/devtools-kit": ["@vue/devtools-kit@8.1.5", "", { "dependencies": { "@vue/devtools-shared": "^8.1.5", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA=="], "@vue/devtools-kit": ["@vue/devtools-kit@8.1.5", "", { "dependencies": { "@vue/devtools-shared": "^8.1.5", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA=="],
@@ -800,6 +811,8 @@
"vue-eslint-parser": ["vue-eslint-parser@10.4.1", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA=="], "vue-eslint-parser": ["vue-eslint-parser@10.4.1", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA=="],
"vue-i18n": ["vue-i18n@11.4.6", "", { "dependencies": { "@intlify/core-base": "11.4.6", "@intlify/devtools-types": "11.4.6", "@intlify/shared": "11.4.6", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg=="],
"vue-tsc": ["vue-tsc@3.3.6", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.3.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-ERXGgbKSBGFUkavrJ1Iwj0ZVxKqB/5UOx65IXy7fPf2UsoI21n3ssQLwdvx8xyUGgJe9PvSZTM/FYzIdwUDPFA=="], "vue-tsc": ["vue-tsc@3.3.6", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.3.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-ERXGgbKSBGFUkavrJ1Iwj0ZVxKqB/5UOx65IXy7fPf2UsoI21n3ssQLwdvx8xyUGgJe9PvSZTM/FYzIdwUDPFA=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
+2 -1
View File
@@ -23,7 +23,8 @@
"@lucide/vue": "^1.23.0", "@lucide/vue": "^1.23.0",
"cron-parser": "^5.6.1", "cron-parser": "^5.6.1",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"vue": "^3.5.38" "vue": "^3.5.38",
"vue-i18n": "^11.4.6"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
+60
View File
@@ -0,0 +1,60 @@
{
"cron": {
"type": {
"one_time": "One time",
"every_day": "Every day",
"every_week": "Every week",
"every_month": "Every month",
"every_year": "Every year",
"specific": "Specific"
},
"week": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
],
"month": {
"st": "st",
"nd": "nd",
"rd": "rd",
"th": "th"
},
"year": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
]
},
"checked": "Checked",
"resets": "Resets",
"completed": "Completed",
"new_task_hint": "New task",
"options": {
"title": "Options",
"sort_by": "Sort by",
"sort": {
"created_at": "Creation date",
"updated_at": "Creation or update date",
"check_date": "Last check date",
"reset_date": "Next reset date",
"name": "Name"
},
"reverse_sort": "Reverse sort",
"last_checked": "Show last checked time",
"next_reset": "Show next reset time",
"separate_completed": "Separate completed tasks below"
}
}
+60
View File
@@ -0,0 +1,60 @@
{
"cron": {
"type": {
"one_time": "Une seule fois",
"every_day": "Tous les jours",
"every_week": "Toutes les semaines",
"every_month": "Tous les mois",
"every_year": "Tous les ans",
"specific": "Spécifique"
},
"week": [
"Lundi",
"Mardi",
"Mercredi",
"Jeudi",
"Vendredi",
"Samedi",
"Dimanche"
],
"month": {
"st": "",
"nd": "",
"rd": "",
"th": ""
},
"year": [
"Janvier",
"Février",
"Mars",
"Avril",
"Mai",
"Juin",
"Juillet",
"Août",
"Septembre",
"Octobre",
"Novembre",
"Décembre"
]
},
"checked": "Complété",
"resets": "Reviens",
"completed": "Complété",
"new_task_hint": "Nouvelle tâche",
"options": {
"title": "Options",
"sort_by": "Trier par",
"sort": {
"created_at": "Date de création",
"updated_at": "Date de modification",
"check_date": "Dernière completion",
"reset_date": "Prochaine réinitialisation",
"name": "Nom"
},
"reverse_sort": "Inverser le tri",
"last_checked": "Afficher la dernière completion",
"next_reset": "Afficher la prochaine réinitialisation",
"separate_completed": "Séparer les tâches complétées plus bas"
}
}
+6 -4
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted, defineAsyncComponent } from "vue";
import TaskList from "@/components/TaskList.vue"; const TaskList = defineAsyncComponent(() => import("@/components/TaskList.vue"));
const PageFooter = defineAsyncComponent(() => import("@/components/PageFooter.vue"));
const visible = ref<boolean>(false); const visible = ref<boolean>(false);
@@ -12,8 +13,8 @@ onMounted(() => {
</script> </script>
<template> <template>
<main :style="{ display: visible ? 'inherit' : 'none' }"> <main :style="{ display: visible ? 'flex' : 'none' }" class="min-h-screen flex-col">
<div class="hero bg-base-200 min-h-screen"> <div class="hero bg-base-200 grow">
<div class="hero-content text-center"> <div class="hero-content text-center">
<div class="max-w-md min-w-96 bg-base-100 rounded-box shadow-md"> <div class="max-w-md min-w-96 bg-base-100 rounded-box shadow-md">
<h1 class="p-4 pb-2 text-4xl font-bold opacity-60 tracking-wide select-none">Tout Doux</h1> <h1 class="p-4 pb-2 text-4xl font-bold opacity-60 tracking-wide select-none">Tout Doux</h1>
@@ -21,5 +22,6 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
<page-footer />
</main> </main>
</template> </template>
+9 -26
View File
@@ -63,41 +63,24 @@ watch(cron, () => {
<template> <template>
<div class="flex gap-2"> <div class="flex gap-2">
<select v-model="cronType" class="select" @change="onChangeCronType"> <select v-model="cronType" class="select" @change="onChangeCronType">
<option :value="CronType.ONE_TIME">One time (do not repeat)</option> <option :value="CronType.ONE_TIME">{{ $t('cron.type.one_time') }}</option>
<option :value="CronType.EVERY_DAY">Every day at</option> <option :value="CronType.EVERY_DAY">{{ $t('cron.type.every_day') }}</option>
<option :value="CronType.EVERY_WEEK">Every week on</option> <option :value="CronType.EVERY_WEEK">{{ $t('cron.type.every_week') }}</option>
<option :value="CronType.EVERY_MONTH">Every month on the</option> <option :value="CronType.EVERY_MONTH">{{ $t('cron.type.every_month') }}</option>
<option :value="CronType.EVERY_YEAR">Every year on</option> <option :value="CronType.EVERY_YEAR">{{ $t('cron.type.every_year') }}</option>
<option :value="CronType.SPECIFIC">Specific</option> <option :value="CronType.SPECIFIC">{{ $t('cron.type.specific') }}</option>
</select> </select>
<select v-if="cronType === CronType.EVERY_DAY" v-model="cronValue" class="select" @change="onChangeCronValue"> <select v-if="cronType === CronType.EVERY_DAY" v-model="cronValue" class="select" @change="onChangeCronValue">
<option v-for="i in Array.from(Array(24).keys())" :key="`h${i}`" :value="i">{{ i.toFixed(0).padStart(2, '0') }}:00</option> <option v-for="i in Array.from(Array(24).keys())" :key="`h${i}`" :value="i">{{ i.toFixed(0).padStart(2, '0') }}:00</option>
</select> </select>
<select v-if="cronType === CronType.EVERY_WEEK" v-model="cronValue" class="select" @change="onChangeCronValue"> <select v-if="cronType === CronType.EVERY_WEEK" v-model="cronValue" class="select" @change="onChangeCronValue">
<option :value="1">Monday</option> <option v-for="i in Array.from(Array(7).keys())" :key="`w${i}`" :value="i + 1">{{ $t(`cron.week.${i.toFixed(0)}`) }}</option>
<option :value="2">Tuesday</option>
<option :value="3">Wednesday</option>
<option :value="4">Thursday</option>
<option :value="5">Friday</option>
<option :value="6">Saturday</option>
<option :value="7">Sunday</option>
</select> </select>
<select v-if="cronType === CronType.EVERY_MONTH" v-model="cronValue" class="select" @change="onChangeCronValue"> <select v-if="cronType === CronType.EVERY_MONTH" v-model="cronValue" class="select" @change="onChangeCronValue">
<option v-for="i in Array.from(Array(32).keys())" :key="`d${i}`" :value="i">{{i}}{{ i % 10 == 1 ? 'st' : (i % 10 === 2 ? 'nd': (i % 10 === 3 ? 'rd' : 'th')) }}</option> <option v-for="i in Array.from(Array(31).keys())" :key="`d${i}`" :value="i + 1">{{i + 1}}{{ i % 10 == 0 ? $t('cron.month.st') : (i % 10 === 1 ? $t('cron.month.nd'): (i % 10 === 2 ? $t('cron.month.rd') : $t('cron.month.th'))) }}</option>
</select> </select>
<select v-if="cronType === CronType.EVERY_YEAR" v-model="cronValue" class="select" @change="onChangeCronValue"> <select v-if="cronType === CronType.EVERY_YEAR" v-model="cronValue" class="select" @change="onChangeCronValue">
<option :value="1">January</option> <option v-for="i in Array.from(Array(12).keys())" :key="`m${i}`" :value="i + 1">{{ $t(`cron.year.${i.toFixed(0)}`) }}</option>
<option :value="2">February</option>
<option :value="3">March</option>
<option :value="4">April</option>
<option :value="5">May</option>
<option :value="6">June</option>
<option :value="7">July</option>
<option :value="8">August</option>
<option :value="9">September</option>
<option :value="10">October</option>
<option :value="11">November</option>
<option :value="12">December</option>
</select> </select>
<input v-if="cronType === CronType.SPECIFIC" v-model="rawCron" type="text" class="input font-mono" :class="valid ? '' : 'input-error'" @input="onInputRawCron" /> <input v-if="cronType === CronType.SPECIFIC" v-model="rawCron" type="text" class="input font-mono" :class="valid ? '' : 'input-error'" @input="onInputRawCron" />
</div> </div>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { setCookie } from '@/lib/cookies';
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
function onSetLang(lang: string) {
i18n.locale.value = lang;
setCookie('lang', lang);
}
</script>
<template>
<a href="#" @click.prevent="onSetLang('fr')">🇫🇷</a> / <a href="#" @click.prevent="onSetLang('en')">🇺🇸</a>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { defineAsyncComponent } from "vue";
const LangInput = defineAsyncComponent(() => import("@/components/LangInput.vue"));
</script>
<template>
<footer class="footer sm:footer-horizontal footer-center bg-base-300 text-base-content p-2">
<aside>
<p>
<lang-input /> - 2026 -
<a
href="https://git.klemek.fr/klemek/tout-doux"
class="underline">
klemek
</a>
</p>
</aside>
</footer>
</template>
+4 -4
View File
@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount, ref, watch, computed } from "vue"; import { onBeforeMount, ref, watch, computed, defineAsyncComponent } from "vue";
import type { Task } from "@/types"; import type { Task } from "@/types";
import { Copy, Trash, Save, X } from "@lucide/vue"; import { Copy, Trash, Save, X } from "@lucide/vue";
import { updateTask } from "@/api/tasks"; import { updateTask } from "@/api/tasks";
import CronInput from "@/components/CronInput.vue"; const CronInput = defineAsyncComponent(() => import("@/components/CronInput.vue"));
import { taskCompleted, taskNextReset } from "@/lib/tasks"; import { taskCompleted, taskNextReset } from "@/lib/tasks";
import { relativeTime } from "@/lib/dates"; import { relativeTime } from "@/lib/dates";
@@ -85,10 +85,10 @@ watch(task, () => {
> >
<div>{{ task.name }}</div> <div>{{ task.name }}</div>
<div v-if="showLastChecked && !checked && task.check_date" class="font-light text-xs italic"> <div v-if="showLastChecked && !checked && task.check_date" class="font-light text-xs italic">
Checked {{ relativeTime(task.check_date) }} {{ $t('checked') }} {{ relativeTime(task.check_date, $i18n.locale) }}
</div> </div>
<div v-if="showNextReset && checked && nextReset" class="font-light text-xs italic"> <div v-if="showNextReset && checked && nextReset" class="font-light text-xs italic">
Resets {{ relativeTime(nextReset) }} {{ $t('resets') }} {{ relativeTime(nextReset, $i18n.locale) }}
</div> </div>
</div> </div>
<template v-else> <template v-else>
+14 -14
View File
@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onBeforeMount, computed } from "vue"; import { ref, onBeforeMount, computed, defineAsyncComponent } from "vue";
import type { Task } from "@/types"; import type { Task } from "@/types";
import { getTasks, createTask, deleteTask } from "@/api/tasks"; import { getTasks, createTask, deleteTask } from "@/api/tasks";
import { CirclePlus } from "@lucide/vue"; import { CirclePlus } from "@lucide/vue";
import TaskItem from "@/components/TaskItem.vue"; const TaskItem = defineAsyncComponent(() => import("@/components/TaskItem.vue"));
import { taskCompleted, sortTasks } from "@/lib/tasks"; import { taskCompleted, sortTasks } from "@/lib/tasks";
import { SortType } from "@/enums"; import { SortType } from "@/enums";
import { booleanCookieRef, enumCookieRef } from "@/lib/cookies"; import { booleanCookieRef, enumCookieRef } from "@/lib/cookies";
@@ -93,7 +93,7 @@ onBeforeMount(fetchTasks);
<input <input
v-model="taskInput" v-model="taskInput"
type="text" type="text"
placeholder="New Task" :placeholder="$t('new_task_hint')"
class="input" class="input"
@keyup.enter="onNewTask" @keyup.enter="onNewTask"
/> />
@@ -111,7 +111,7 @@ onBeforeMount(fetchTasks);
</ul> </ul>
<div v-if="separateCompleted && hasCompleted" class="collapse collapse-arrow"> <div v-if="separateCompleted && hasCompleted" class="collapse collapse-arrow">
<input id="completed-dropdown" type="checkbox" /> <input id="completed-dropdown" type="checkbox" />
<div class="collapse-title mb-0">Completed</div> <div class="collapse-title mb-0">{{ $t('completed') }}</div>
<div class="collapse-content p-0"> <div class="collapse-content p-0">
<ul class="list"> <ul class="list">
<template v-for="(task, i) in tasks" :key="`${task.id}-completed`"> <template v-for="(task, i) in tasks" :key="`${task.id}-completed`">
@@ -130,34 +130,34 @@ onBeforeMount(fetchTasks);
</div> </div>
<div class="collapse collapse-arrow font-light text-sm"> <div class="collapse collapse-arrow font-light text-sm">
<input id="options-dropdown" type="checkbox"/> <input id="options-dropdown" type="checkbox"/>
<div class="collapse-title">Options</div> <div class="collapse-title">{{ $t('options.title') }}</div>
<div class="collapse-content text-sm"> <div class="collapse-content text-sm">
<fieldset class="fieldset flex flex-col gap-2"> <fieldset class="fieldset flex flex-col gap-2">
<label class="label"> <label class="label">
Sort by Sort by
<select v-model="sortType" class="select" @change="onChangeSortType"> <select v-model="sortType" class="select" @change="onChangeSortType">
<option :value="SortType.CREATED_AT">Creation date</option> <option :value="SortType.CREATED_AT">{{ $t('options.sort.created_at') }}</option>
<option :value="SortType.UPDATED_AT">Creation or update date</option> <option :value="SortType.UPDATED_AT">{{ $t('options.sort.updated_at') }}</option>
<option :value="SortType.CHECK_DATE">Last check date</option> <option :value="SortType.CHECK_DATE">{{ $t('options.sort.check_date') }}</option>
<option :value="SortType.RESET_DATE">Next reset date</option> <option :value="SortType.RESET_DATE">{{ $t('options.sort.reset_date') }}</option>
<option :value="SortType.NAME">Name</option> <option :value="SortType.NAME">{{ $t('options.sort.name') }}</option>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<input v-model="sortReverse" type="checkbox" class="checkbox" @change="onChangeSortReverse" /> <input v-model="sortReverse" type="checkbox" class="checkbox" @change="onChangeSortReverse" />
Reverse sort {{ $t('options.reverse_sort') }}
</label> </label>
<label class="label"> <label class="label">
<input v-model="showLastChecked" type="checkbox" class="checkbox" /> <input v-model="showLastChecked" type="checkbox" class="checkbox" />
Show last checked time {{ $t('options.last_checked') }}
</label> </label>
<label class="label"> <label class="label">
<input v-model="showNextReset" type="checkbox" class="checkbox" /> <input v-model="showNextReset" type="checkbox" class="checkbox" />
Show next reset time {{ $t('options.next_reset') }}
</label> </label>
<label class="label"> <label class="label">
<input v-model="separateCompleted" type="checkbox" class="checkbox" /> <input v-model="separateCompleted" type="checkbox" class="checkbox" />
Move completed tasks below {{ $t('options.separate_completed') }}
</label> </label>
</fieldset> </fieldset>
</div> </div>
+3 -3
View File
@@ -1,6 +1,6 @@
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { enUS, fr } from 'date-fns/locale';
export function relativeTime(date: Date, locale: string): string {
export function relativeTime(date: Date): string { return formatDistanceToNow(date, {addSuffix: true, includeSeconds: false, locale: locale === 'fr' ? fr : enUS});
return formatDistanceToNow(date, {addSuffix: true, includeSeconds: false});
} }
+11
View File
@@ -0,0 +1,11 @@
import { getCookie, setCookie } from "./cookies";
export function getLang() {
const params = new URLSearchParams(document.location.search);
const paramsLang = params.get("lang");
if (paramsLang !== null) {
setCookie("lang", paramsLang);
return paramsLang;
}
return getCookie("lang", "en");
}
+15 -2
View File
@@ -1,4 +1,17 @@
import { createApp } from "vue"; import { createApp } from "vue";
import App from "./App.vue"; import { createI18n } from "vue-i18n";
import App from "@/App.vue";
import { getLang } from "@/lib/lang";
const en = await import("@lang/en.json");
const fr = await import("@lang/fr.json");
createApp(App).mount("#app"); const i18n = createI18n({
locale: getLang(),
fallbackLocale: "en",
messages: {
en: en,
fr: fr,
},
});
createApp(App).use(i18n).mount("#app");
+2
View File
@@ -5,6 +5,7 @@
"vue.d.ts", "vue.d.ts",
"resources/ts/**/*", "resources/ts/**/*",
"resources/ts/**/*.vue", "resources/ts/**/*.vue",
"resources/lang/*.json",
], ],
"exclude": ["resources/**/__tests__/*"], "exclude": ["resources/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
@@ -12,6 +13,7 @@
"paths": { "paths": {
"@/*": ["./resources/ts/*"], "@/*": ["./resources/ts/*"],
"@lang/*": ["./resources/lang/*"],
}, },
}, },
} }
+1
View File
@@ -22,6 +22,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
"@": fileURLToPath(new URL("./resources/ts", import.meta.url)), "@": fileURLToPath(new URL("./resources/ts", import.meta.url)),
"@lang": fileURLToPath(new URL("./resources/lang", import.meta.url)),
}, },
}, },
}); });