feat: Added i18n to chatbot titles, name and message to dynamically change depending on the selected preferance.

This commit is contained in:
Lion Arar 2025-10-08 10:48:21 -04:00
parent c1d48edb1e
commit 1ce5c35a31
6 changed files with 235 additions and 157 deletions

View File

@ -1,4 +1,10 @@
export default { export default {
chatbot: {
chat_header: "AI Assistant",
chat_initial_message:
"Welcome to your technical assistant.\nPlease provide the Client ID and \na description of the problem.",
chat_placeholder: "Enter a Message",
},
employee_list: { employee_list: {
page_header: "Employee Directory", page_header: "Employee Directory",
table: { table: {
@ -19,7 +25,7 @@ export default {
button: { button: {
connect: "connect", connect: "connect",
employee: "employee", employee: "employee",
facebook:"Facebook", facebook: "Facebook",
remember_me: "remember me", remember_me: "remember me",
}, },
tooltip: { tooltip: {
@ -64,10 +70,10 @@ export default {
}, },
errors: { errors: {
must_enter_birthdate: "You must enter a valid birthdate", must_enter_birthdate: "You must enter a valid birthdate",
} },
}, },
shared:{ shared: {
error: { error: {
no_data_found: "no data found", no_data_found: "no data found",
no_search_results: "no results matching search", no_search_results: "no results matching search",
@ -117,20 +123,20 @@ export default {
}, },
timesheet: { timesheet: {
page_header:"Timesheet", page_header: "Timesheet",
nav_button: { nav_button: {
calendar_date_picker:"Calendar", calendar_date_picker: "Calendar",
current_week:"This week", current_week: "This week",
next_week:"Next week", next_week: "Next week",
previous_week:"Previous week", previous_week: "Previous week",
}, },
save_button:"Save", save_button: "Save",
cancel_button:"Cancel", cancel_button: "Cancel",
remote_button: "Remote work", remote_button: "Remote work",
delete_button: "Delete", delete_button: "Delete",
shift: { shift: {
actions: { actions: {
add:"Add Shift", add: "Add Shift",
edit: "Edit shift", edit: "Edit shift",
delete: "Delete shift", delete: "Delete shift",
delete_confirmation_msg: "Do you want to delete this shift completly?", delete_confirmation_msg: "Do you want to delete this shift completly?",
@ -147,53 +153,56 @@ export default {
REMOTE: "Remote work", REMOTE: "Remote work",
}, },
errors: { errors: {
not_found:"Shift not found", not_found: "Shift not found",
overlap:"An overlaps occured between 2 or more shifts", overlap: "An overlaps occured between 2 or more shifts",
invalid:"Invalid shift`s entry", invalid: "Invalid shift`s entry",
unknown:"Unknown error", unknown: "Unknown error",
comment_required:"A comment is required", comment_required: "A comment is required",
comment_too_long:"Your comment is too long", comment_too_long: "Your comment is too long",
}, },
fields: { fields: {
start:"Start (HH:mm)", start: "Start (HH:mm)",
end:"End (HH:mm)", end: "End (HH:mm)",
header_comment:"Shift`s comment", header_comment: "Shift`s comment",
textarea_comment: "Leave a comment here", textarea_comment: "Leave a comment here",
}, },
}, },
expense: { expense: {
add_expense:'Add Expense', add_expense: "Add Expense",
amount:'Amount', amount: "Amount",
date:'Date', date: "Date",
empty_list:'No registered expenses', empty_list: "No registered expenses",
employee_comment:'Comment', employee_comment: "Comment",
supervisor_comment:'Supervisor note', supervisor_comment: "Supervisor note",
errors: { errors: {
date_required_or_invalid:"the date is missing or invalid", date_required_or_invalid: "the date is missing or invalid",
comment_required:"A comment required", comment_required: "A comment required",
comment_too_long:"Your comment is too long", comment_too_long: "Your comment is too long",
amount_must_be_positive:"the amount cannot be under 0$", amount_must_be_positive: "the amount cannot be under 0$",
mileave_must_be_positive:"the mileage cannot be under 0", mileave_must_be_positive: "the mileage cannot be under 0",
amount_xor_mileage:"you cannot enter an amount and a mileage for the same expense", amount_xor_mileage:
mileage_required_for_type:"you need to enter a value for mileage when you enter an expense of that type", "you cannot enter an amount and a mileage for the same expense",
amount_required_for_type:"you need to enter a value for amount when you enter an expense of that type", mileage_required_for_type:
"you need to enter a value for mileage when you enter an expense of that type",
amount_required_for_type:
"you need to enter a value for amount when you enter an expense of that type",
}, },
hints: { hints: {
amount_or_mileage:"Either amount or mileage, not both", amount_or_mileage: "Either amount or mileage, not both",
comment_required:"A comment required", comment_required: "A comment required",
attach_file:"Attach File" attach_file: "Attach File",
}, },
mileage:"mileage", mileage: "mileage",
open_btn:"list of expenses", open_btn: "list of expenses",
title:"List of all expenses", title: "List of all expenses",
total_amount:"Total amount", total_amount: "Total amount",
total_mileage:"Total mileage", total_mileage: "Total mileage",
type:"Type", type: "Type",
types: { types: {
PER_DIEM:"Per Diem", PER_DIEM: "Per Diem",
EXPENSES:"expense", EXPENSES: "expense",
MILEAGE:"mileage", MILEAGE: "mileage",
PRIME_GARDE:"on-call allowance", PRIME_GARDE: "on-call allowance",
}, },
}, },
}, },
@ -222,4 +231,4 @@ export default {
button_detailed_view: "detailed view", button_detailed_view: "detailed view",
}, },
}, },
}; };

View File

@ -1,4 +1,10 @@
export default { export default {
chatbot: {
chat_header: "Agent IA",
chat_initial_message:
"Bienvenue à votre assistant technique.\nVeuillez fournir le ID du Client et \nune description du problème.",
chat_placeholder: "Entré un Message",
},
employee_list: { employee_list: {
page_header: "Répertoire du personnel", page_header: "Répertoire du personnel",
table: { table: {
@ -19,7 +25,7 @@ export default {
button: { button: {
connect: "connecter", connect: "connecter",
employee: "employé", employee: "employé",
facebook:"Facebook", facebook: "Facebook",
remember_me: "rester connecté", remember_me: "rester connecté",
}, },
tooltip: { tooltip: {
@ -64,19 +70,19 @@ export default {
}, },
errors: { errors: {
must_enter_birthdate: "Vous devez entrer une date de naissance valide", must_enter_birthdate: "Vous devez entrer une date de naissance valide",
} },
}, },
shared: { shared: {
error: { error: {
no_data_found: 'aucune donnée à afficher', no_data_found: "aucune donnée à afficher",
no_search_results: 'aucun résultat ne correspond à la recherche', no_search_results: "aucun résultat ne correspond à la recherche",
}, },
label: { label: {
search: 'recherche', search: "recherche",
filter: "filtres", filter: "filtres",
loading: 'chargement en cours...', loading: "chargement en cours...",
language: 'langue', language: "langue",
add: "ajouter", add: "ajouter",
save: "sauvegarder", save: "sauvegarder",
remove: "supprimer", remove: "supprimer",
@ -117,20 +123,20 @@ export default {
}, },
timesheet: { timesheet: {
page_header:"Carte de temps", page_header: "Carte de temps",
nav_button: { nav_button: {
calendar_date_picker:"Calendrier", calendar_date_picker: "Calendrier",
current_week:"Semaine actuelle", current_week: "Semaine actuelle",
next_week:"Prochaine semaine", next_week: "Prochaine semaine",
previous_week:"Semaine précédente", previous_week: "Semaine précédente",
}, },
save_button:"Enregistrer", save_button: "Enregistrer",
cancel_button:"Annuler", cancel_button: "Annuler",
remote_button: "Télétravail", remote_button: "Télétravail",
delete_button: "Supprimer", delete_button: "Supprimer",
shift: { shift: {
actions: { actions: {
add:"Ajouter un Quart", add: "Ajouter un Quart",
edit: "Modifier un Quart", edit: "Modifier un Quart",
delete: "Supprimer un Quart", delete: "Supprimer un Quart",
delete_confirmation_msg: "Voulez-vous complètement supprimer ce quart?", delete_confirmation_msg: "Voulez-vous complètement supprimer ce quart?",
@ -147,53 +153,56 @@ export default {
REMOTE: "Télétravail", REMOTE: "Télétravail",
}, },
errors: { errors: {
not_found:"Aucun quart trouvé", not_found: "Aucun quart trouvé",
overlap:"Il y a un chevauchement entre deux ou plusieurs quarts", overlap: "Il y a un chevauchement entre deux ou plusieurs quarts",
invalid:"Entrée du quart invalide", invalid: "Entrée du quart invalide",
unknown:"Erreur inconnue", unknown: "Erreur inconnue",
comment_required:"un commentaire est requis", comment_required: "un commentaire est requis",
comment_too_long:"votre commentaire est trop long", comment_too_long: "votre commentaire est trop long",
}, },
fields: { fields: {
start:"Début (HH:mm)", start: "Début (HH:mm)",
end:"Fin (HH:mm)", end: "Fin (HH:mm)",
header_comment:"Commentaire du Quart", header_comment: "Commentaire du Quart",
textarea_comment: "Laissez votre commentaire ici", textarea_comment: "Laissez votre commentaire ici",
}, },
}, },
expense: { expense: {
add_expense:'Ajouter une dépense', add_expense: "Ajouter une dépense",
amount:'Montant', amount: "Montant",
date:'Date', date: "Date",
empty_list:'Aucun dépense enregistrée', empty_list: "Aucun dépense enregistrée",
employee_comment:'Commentaire', employee_comment: "Commentaire",
supervisor_comment:'Note du Superviseur', supervisor_comment: "Note du Superviseur",
errors: { errors: {
date_required_or_invalid:"La date est manquante ou invalide", date_required_or_invalid: "La date est manquante ou invalide",
comment_required:"un commentaire est requis", comment_required: "un commentaire est requis",
comment_too_long:"votre commentaire est trop long", comment_too_long: "votre commentaire est trop long",
amount_must_be_positive:"le montant doit être suppérieur à 0$", amount_must_be_positive: "le montant doit être suppérieur à 0$",
mileave_must_be_positive:"le kilométrage doit être suppérieur à 0", mileave_must_be_positive: "le kilométrage doit être suppérieur à 0",
amount_xor_mileage:"Vous ne pouvez pas saisir un montant et un kilométrage pour une même dépense", amount_xor_mileage:
mileage_required_for_type:"Vous devez entrer une valeur en kilométrage pour ce type de dépense", "Vous ne pouvez pas saisir un montant et un kilométrage pour une même dépense",
amount_required_for_type:"Vous devez entrer une valeur en montant $ pour ce type de dépense", mileage_required_for_type:
"Vous devez entrer une valeur en kilométrage pour ce type de dépense",
amount_required_for_type:
"Vous devez entrer une valeur en montant $ pour ce type de dépense",
}, },
hints: { hints: {
amount_or_mileage:"Soit dépense ou kilométrage, pas les deux", amount_or_mileage: "Soit dépense ou kilométrage, pas les deux",
comment_required:"un commentaire est requis", comment_required: "un commentaire est requis",
attach_file:"Pièce jointe" attach_file: "Pièce jointe",
}, },
mileage:"Kilométrage", mileage: "Kilométrage",
open_btn:"Liste des Dépenses", open_btn: "Liste des Dépenses",
title:"Liste des dépenses", title: "Liste des dépenses",
total_amount:"Montant total", total_amount: "Montant total",
total_mileage:"Kilométrage total", total_mileage: "Kilométrage total",
type:"Type", type: "Type",
types: { types: {
PER_DIEM:"Per diem", PER_DIEM: "Per diem",
EXPENSES:"dépense", EXPENSES: "dépense",
MILEAGE:"kilométrage", MILEAGE: "kilométrage",
PRIME_GARDE:"Prime de garde", PRIME_GARDE: "Prime de garde",
}, },
}, },
}, },
@ -210,7 +219,7 @@ export default {
}, },
chart: { chart: {
hours_worked_title: "heures travaillées", hours_worked_title: "heures travaillées",
expenses_title: "dépenses encourues" expenses_title: "dépenses encourues",
}, },
print_report: { print_report: {
company: "compagnie", company: "compagnie",
@ -222,4 +231,4 @@ export default {
button_detailed_view: "vue détaillée", button_detailed_view: "vue détaillée",
}, },
}, },
}; };

View File

@ -1,47 +1,56 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { ref } from 'vue'; import { ref } from 'vue';
import { RouteNames } from 'src/router/router-constants'; import { RouteNames } from 'src/router/router-constants';
const authStore = useAuthStore(); const authStore = useAuthStore();
const uiStore = useUiStore(); const uiStore = useUiStore();
const router = useRouter(); const router = useRouter();
const miniState = ref(true); const miniState = ref(true);
const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
});
};
const handleLogout = () => { const goToPageName = (pageName: string) => {
authStore.logout(); router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
});
};
router.push({ name: 'login' }).catch(err => { const handleLogout = () => {
console.log('could not log you out: ', err); authStore.logout();
})
} router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err);
})
}
</script> </script>
<template> <template>
<q-drawer <q-drawer
v-model="uiStore.isRightDrawerOpen" v-model="uiStore.isRightDrawerOpen"
overlay overlay
elevated elevated
side="left" side="left"
:mini="miniState" :mini="miniState"
@mouseenter="miniState = false" @mouseenter="miniState = false"
@mouseleave="miniState = true" @mouseleave="miniState = true"
class="bg-dark" class="bg-dark"
> >
<q-scroll-area class="fit"> <q-scroll-area class="fit">
<q-list> <q-list>
<!-- Home --> <!-- Home -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.DASHBOARD)"> <q-item
v-ripple
clickable
side
@click="goToPageName(RouteNames.DASHBOARD)"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="home" color="primary" /> <q-icon
name="home"
color="primary"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
@ -49,42 +58,77 @@
</q-item> </q-item>
<!-- Timesheet Validation -- Supervisor and Accounting only --> <!-- Timesheet Validation -- Supervisor and Accounting only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)" <q-item
v-if="['supervisor', 'accounting'].includes(authStore.user.role)"> v-ripple
clickable
side
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
v-if="['supervisor', 'accounting'].includes(authStore.user.role)"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="event_available" color="primary" /> <q-icon
name="event_available"
color="primary"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals')
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<!-- Employee List -- Supervisor, Accounting and HR only --> <!-- Employee List -- Supervisor, Accounting and HR only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)" <q-item
v-if="['supervisor', 'accounting', 'human_resources'].includes(authStore.user.role)"> v-ripple
clickable
side
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
v-if="['supervisor', 'accounting', 'human_resources'].includes(authStore.user.role)"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="view_list" color="primary" /> <q-icon
name="view_list"
color="primary"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list')
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only --> <!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_TEMP)" <q-item
v-if="['supervisor', 'accounting', 'employee'].includes(authStore.user.role)"> v-ripple
clickable
side
@click="goToPageName(RouteNames.TIMESHEET_TEMP)"
v-if="['supervisor', 'accounting', 'employee'].includes(authStore.user.role)"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="punch_clock" color="primary" /> <q-icon
name="punch_clock"
color="primary"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet')
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<!-- Profile --> <!-- Profile -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.PROFILE)"> <q-item
v-ripple
clickable
side
@click="goToPageName(RouteNames.PROFILE)"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="account_box" color="primary" /> <q-icon
name="account_box"
color="primary"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
@ -92,9 +136,16 @@
</q-item> </q-item>
<!-- Help --> <!-- Help -->
<q-item v-ripple clickable @click="goToPageName('help')"> <q-item
v-ripple
clickable
@click="goToPageName('help')"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="contact_support" color="primary" /> <q-icon
name="contact_support"
color="primary"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
@ -103,9 +154,17 @@
</q-list> </q-list>
<!-- Logout --> <!-- Logout -->
<q-item v-ripple clickable @click="handleLogout" class="absolute-bottom"> <q-item
v-ripple
clickable
@click="handleLogout"
class="absolute-bottom"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="exit_to_app" color="primary" /> <q-icon
name="exit_to_app"
color="primary"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label>

View File

@ -26,7 +26,6 @@ const onDrag = (e: MouseEvent) => {
const stopDrag = () => { const stopDrag = () => {
dragging = false dragging = false
} }
window.addEventListener('mousemove', onDrag) window.addEventListener('mousemove', onDrag)
window.addEventListener('mouseup', stopDrag) window.addEventListener('mouseup', stopDrag)
@ -40,7 +39,6 @@ onMounted(() => {
chatStore.showInstructionsOnce(); chatStore.showInstructionsOnce();
}) })
const handleSend = async () => { const handleSend = async () => {
const userMessage = text.value.trim(); const userMessage = text.value.trim();
if (!userMessage) return if (!userMessage) return
@ -83,7 +81,7 @@ const handleSend = async () => {
throw error; throw error;
} }
}; };
//---------------------------------------------
// Block to handle sending the url to n8n as context // Block to handle sending the url to n8n as context
const route = useRoute() const route = useRoute()
const sendUlr = chatbotService.sendUlrContext; const sendUlr = chatbotService.sendUlrContext;
@ -110,7 +108,7 @@ watch(
<div <div
class="chat-header" class="chat-header"
style="text-align:start; padding: 10px 10px 0px;" style="text-align:start; padding: 10px 10px 0px;"
>AI Assistant</div> >{{ $t('chatbot.chat_header') }}</div>
<div class="line-separator"></div> <div class="line-separator"></div>
<div class="chat-body"> <div class="chat-body">
@ -125,7 +123,7 @@ watch(
<q-input <q-input
v-model="text" v-model="text"
label="Enter a Message" :label="$t('chatbot.chat_placeholder')"
autogrow autogrow
class="col" class="col"
@keydown.enter="handleSend" @keydown.enter="handleSend"

View File

@ -29,7 +29,8 @@ watch(props.messages, async () => {
v-if="!msg.isThinking" v-if="!msg.isThinking"
:text="msg.text" :text="msg.text"
:sent="msg.sent" :sent="msg.sent"
:name="msg.sent ? currentUser.firstName : 'AI Assistant'" :name="msg.sent ? currentUser.firstName :
$t('chatbot.chat_header')"
:class="['chat-message', msg.sent ? 'user' : 'ai']" :class="['chat-message', msg.sent ? 'user' : 'ai']"
/> />
<!-- 💭 Thinking bubble --> <!-- 💭 Thinking bubble -->

View File

@ -2,10 +2,12 @@ import type { Message } from "src/modules/chatbot/types/dialogue-message";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { chatbotService } from "src/modules/chatbot/services/messageService"; import { chatbotService } from "src/modules/chatbot/services/messageService";
import { ref } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n";
export const useChatStore = defineStore("chat", () => { export const useChatStore = defineStore("chat", () => {
const messages = ref<Message[]>([]); const messages = ref<Message[]>([]);
const hasShownInstructions = ref(false); const hasShownInstructions = ref(false);
const { t } = useI18n();
const sendMessage = async (userInput: string) => { const sendMessage = async (userInput: string) => {
const reply = await chatbotService.getChatMessage(userInput); const reply = await chatbotService.getChatMessage(userInput);
@ -15,7 +17,7 @@ export const useChatStore = defineStore("chat", () => {
const showInstructionsOnce = () => { const showInstructionsOnce = () => {
if (!hasShownInstructions.value) { if (!hasShownInstructions.value) {
messages.value.push({ messages.value.push({
text: "Welcome to your technical assistant.\nPlease provide the Client ID and \na description of the problem.", text: t("chatbot.chat_initial_message"),
sent: false, sent: false,
isThinking: false, isThinking: false,
}); });