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

View File

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

View File

@ -1,47 +1,56 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useAuthStore } from 'src/stores/auth-store';
import { useUiStore } from 'src/stores/ui-store';
import { ref } from 'vue';
import { RouteNames } from 'src/router/router-constants';
import { useRouter } from 'vue-router';
import { useAuthStore } from 'src/stores/auth-store';
import { useUiStore } from 'src/stores/ui-store';
import { ref } from 'vue';
import { RouteNames } from 'src/router/router-constants';
const authStore = useAuthStore();
const uiStore = useUiStore();
const router = useRouter();
const miniState = ref(true);
const authStore = useAuthStore();
const uiStore = useUiStore();
const router = useRouter();
const miniState = ref(true);
const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
});
};
const handleLogout = () => {
authStore.logout();
const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
});
};
router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err);
})
}
const handleLogout = () => {
authStore.logout();
router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err);
})
}
</script>
<template>
<q-drawer
v-model="uiStore.isRightDrawerOpen"
overlay
elevated
side="left"
:mini="miniState"
@mouseenter="miniState = false"
@mouseleave="miniState = true"
class="bg-dark"
<q-drawer
v-model="uiStore.isRightDrawerOpen"
overlay
elevated
side="left"
:mini="miniState"
@mouseenter="miniState = false"
@mouseleave="miniState = true"
class="bg-dark"
>
<q-scroll-area class="fit">
<q-list>
<!-- 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-icon name="home" color="primary" />
<q-icon
name="home"
color="primary"
/>
</q-item-section>
<q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
@ -49,42 +58,77 @@
</q-item>
<!-- Timesheet Validation -- Supervisor and Accounting only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
v-if="['supervisor', 'accounting'].includes(authStore.user.role)">
<q-item
v-ripple
clickable
side
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
v-if="['supervisor', 'accounting'].includes(authStore.user.role)"
>
<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-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>
<!-- Employee List -- Supervisor, Accounting and HR only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
v-if="['supervisor', 'accounting', 'human_resources'].includes(authStore.user.role)">
<q-item
v-ripple
clickable
side
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
v-if="['supervisor', 'accounting', 'human_resources'].includes(authStore.user.role)"
>
<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-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>
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_TEMP)"
v-if="['supervisor', 'accounting', 'employee'].includes(authStore.user.role)">
<q-item
v-ripple
clickable
side
@click="goToPageName(RouteNames.TIMESHEET_TEMP)"
v-if="['supervisor', 'accounting', 'employee'].includes(authStore.user.role)"
>
<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-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>
<!-- 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-icon name="account_box" color="primary" />
<q-icon
name="account_box"
color="primary"
/>
</q-item-section>
<q-item-section>
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
@ -92,9 +136,16 @@
</q-item>
<!-- Help -->
<q-item v-ripple clickable @click="goToPageName('help')">
<q-item
v-ripple
clickable
@click="goToPageName('help')"
>
<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-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
@ -103,9 +154,17 @@
</q-list>
<!-- 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-icon name="exit_to_app" color="primary" />
<q-icon
name="exit_to_app"
color="primary"
/>
</q-item-section>
<q-item-section>
<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 = () => {
dragging = false
}
window.addEventListener('mousemove', onDrag)
window.addEventListener('mouseup', stopDrag)
@ -40,7 +39,6 @@ onMounted(() => {
chatStore.showInstructionsOnce();
})
const handleSend = async () => {
const userMessage = text.value.trim();
if (!userMessage) return
@ -83,7 +81,7 @@ const handleSend = async () => {
throw error;
}
};
//---------------------------------------------
// Block to handle sending the url to n8n as context
const route = useRoute()
const sendUlr = chatbotService.sendUlrContext;
@ -110,7 +108,7 @@ watch(
<div
class="chat-header"
style="text-align:start; padding: 10px 10px 0px;"
>AI Assistant</div>
>{{ $t('chatbot.chat_header') }}</div>
<div class="line-separator"></div>
<div class="chat-body">
@ -125,7 +123,7 @@ watch(
<q-input
v-model="text"
label="Enter a Message"
:label="$t('chatbot.chat_placeholder')"
autogrow
class="col"
@keydown.enter="handleSend"

View File

@ -29,7 +29,8 @@ watch(props.messages, async () => {
v-if="!msg.isThinking"
:text="msg.text"
: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']"
/>
<!-- 💭 Thinking bubble -->

View File

@ -2,10 +2,12 @@ import type { Message } from "src/modules/chatbot/types/dialogue-message";
import { defineStore } from "pinia";
import { chatbotService } from "src/modules/chatbot/services/messageService";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
export const useChatStore = defineStore("chat", () => {
const messages = ref<Message[]>([]);
const hasShownInstructions = ref(false);
const { t } = useI18n();
const sendMessage = async (userInput: string) => {
const reply = await chatbotService.getChatMessage(userInput);
@ -15,7 +17,7 @@ export const useChatStore = defineStore("chat", () => {
const showInstructionsOnce = () => {
if (!hasShownInstructions.value) {
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,
isThinking: false,
});