feat: lang
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import { ref, onMounted, defineAsyncComponent } from "vue";
|
||||
const TaskList = defineAsyncComponent(() => import("@/components/TaskList.vue"));
|
||||
const PageFooter = defineAsyncComponent(() => import("@/components/PageFooter.vue"));
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
@@ -12,8 +13,8 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main :style="{ display: visible ? 'inherit' : 'none' }">
|
||||
<div class="hero bg-base-200 min-h-screen">
|
||||
<main :style="{ display: visible ? 'flex' : 'none' }" class="min-h-screen flex-col">
|
||||
<div class="hero bg-base-200 grow">
|
||||
<div class="hero-content text-center">
|
||||
<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>
|
||||
@@ -21,5 +22,6 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<page-footer />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -63,41 +63,24 @@ watch(cron, () => {
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<select v-model="cronType" class="select" @change="onChangeCronType">
|
||||
<option :value="CronType.ONE_TIME">One time (do not repeat)</option>
|
||||
<option :value="CronType.EVERY_DAY">Every day at</option>
|
||||
<option :value="CronType.EVERY_WEEK">Every week on</option>
|
||||
<option :value="CronType.EVERY_MONTH">Every month on the</option>
|
||||
<option :value="CronType.EVERY_YEAR">Every year on</option>
|
||||
<option :value="CronType.SPECIFIC">Specific</option>
|
||||
<option :value="CronType.ONE_TIME">{{ $t('cron.type.one_time') }}</option>
|
||||
<option :value="CronType.EVERY_DAY">{{ $t('cron.type.every_day') }}</option>
|
||||
<option :value="CronType.EVERY_WEEK">{{ $t('cron.type.every_week') }}</option>
|
||||
<option :value="CronType.EVERY_MONTH">{{ $t('cron.type.every_month') }}</option>
|
||||
<option :value="CronType.EVERY_YEAR">{{ $t('cron.type.every_year') }}</option>
|
||||
<option :value="CronType.SPECIFIC">{{ $t('cron.type.specific') }}</option>
|
||||
</select>
|
||||
<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>
|
||||
</select>
|
||||
<select v-if="cronType === CronType.EVERY_WEEK" v-model="cronValue" class="select" @change="onChangeCronValue">
|
||||
<option :value="1">Monday</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>
|
||||
<option v-for="i in Array.from(Array(7).keys())" :key="`w${i}`" :value="i + 1">{{ $t(`cron.week.${i.toFixed(0)}`) }}</option>
|
||||
</select>
|
||||
<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 v-if="cronType === CronType.EVERY_YEAR" v-model="cronValue" class="select" @change="onChangeCronValue">
|
||||
<option :value="1">January</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>
|
||||
<option v-for="i in Array.from(Array(12).keys())" :key="`m${i}`" :value="i + 1">{{ $t(`cron.year.${i.toFixed(0)}`) }}</option>
|
||||
</select>
|
||||
<input v-if="cronType === CronType.SPECIFIC" v-model="rawCron" type="text" class="input font-mono" :class="valid ? '' : 'input-error'" @input="onInputRawCron" />
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,9 +1,9 @@
|
||||
<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 { Copy, Trash, Save, X } from "@lucide/vue";
|
||||
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 { relativeTime } from "@/lib/dates";
|
||||
|
||||
@@ -85,10 +85,10 @@ watch(task, () => {
|
||||
>
|
||||
<div>{{ task.name }}</div>
|
||||
<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 v-if="showNextReset && checked && nextReset" class="font-light text-xs italic">
|
||||
Resets {{ relativeTime(nextReset) }}
|
||||
{{ $t('resets') }} {{ relativeTime(nextReset, $i18n.locale) }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeMount, computed } from "vue";
|
||||
import { ref, onBeforeMount, computed, defineAsyncComponent } from "vue";
|
||||
import type { Task } from "@/types";
|
||||
import { getTasks, createTask, deleteTask } from "@/api/tasks";
|
||||
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 { SortType } from "@/enums";
|
||||
import { booleanCookieRef, enumCookieRef } from "@/lib/cookies";
|
||||
@@ -93,7 +93,7 @@ onBeforeMount(fetchTasks);
|
||||
<input
|
||||
v-model="taskInput"
|
||||
type="text"
|
||||
placeholder="New Task"
|
||||
:placeholder="$t('new_task_hint')"
|
||||
class="input"
|
||||
@keyup.enter="onNewTask"
|
||||
/>
|
||||
@@ -111,7 +111,7 @@ onBeforeMount(fetchTasks);
|
||||
</ul>
|
||||
<div v-if="separateCompleted && hasCompleted" class="collapse collapse-arrow">
|
||||
<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">
|
||||
<ul class="list">
|
||||
<template v-for="(task, i) in tasks" :key="`${task.id}-completed`">
|
||||
@@ -130,34 +130,34 @@ onBeforeMount(fetchTasks);
|
||||
</div>
|
||||
<div class="collapse collapse-arrow font-light text-sm">
|
||||
<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">
|
||||
<fieldset class="fieldset flex flex-col gap-2">
|
||||
<label class="label">
|
||||
Sort by
|
||||
<select v-model="sortType" class="select" @change="onChangeSortType">
|
||||
<option :value="SortType.CREATED_AT">Creation date</option>
|
||||
<option :value="SortType.UPDATED_AT">Creation or update date</option>
|
||||
<option :value="SortType.CHECK_DATE">Last check date</option>
|
||||
<option :value="SortType.RESET_DATE">Next reset date</option>
|
||||
<option :value="SortType.NAME">Name</option>
|
||||
<option :value="SortType.CREATED_AT">{{ $t('options.sort.created_at') }}</option>
|
||||
<option :value="SortType.UPDATED_AT">{{ $t('options.sort.updated_at') }}</option>
|
||||
<option :value="SortType.CHECK_DATE">{{ $t('options.sort.check_date') }}</option>
|
||||
<option :value="SortType.RESET_DATE">{{ $t('options.sort.reset_date') }}</option>
|
||||
<option :value="SortType.NAME">{{ $t('options.sort.name') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<input v-model="sortReverse" type="checkbox" class="checkbox" @change="onChangeSortReverse" />
|
||||
Reverse sort
|
||||
{{ $t('options.reverse_sort') }}
|
||||
</label>
|
||||
<label class="label">
|
||||
<input v-model="showLastChecked" type="checkbox" class="checkbox" />
|
||||
Show last checked time
|
||||
{{ $t('options.last_checked') }}
|
||||
</label>
|
||||
<label class="label">
|
||||
<input v-model="showNextReset" type="checkbox" class="checkbox" />
|
||||
Show next reset time
|
||||
{{ $t('options.next_reset') }}
|
||||
</label>
|
||||
<label class="label">
|
||||
<input v-model="separateCompleted" type="checkbox" class="checkbox" />
|
||||
Move completed tasks below
|
||||
{{ $t('options.separate_completed') }}
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { enUS, fr } from 'date-fns/locale';
|
||||
|
||||
|
||||
export function relativeTime(date: Date): string {
|
||||
return formatDistanceToNow(date, {addSuffix: true, includeSeconds: false});
|
||||
export function relativeTime(date: Date, locale: string): string {
|
||||
return formatDistanceToNow(date, {addSuffix: true, includeSeconds: false, locale: locale === 'fr' ? fr : enUS});
|
||||
}
|
||||
|
||||
@@ -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
@@ -1,4 +1,17 @@
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user