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: {
@ -64,7 +70,7 @@ export default {
},
errors: {
must_enter_birthdate: "You must enter a valid birthdate",
}
},
},
shared: {
@ -162,26 +168,29 @@ export default {
},
},
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",
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"
attach_file: "Attach File",
},
mileage: "mileage",
open_btn: "list of expenses",

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: {
@ -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",
@ -162,26 +168,29 @@ export default {
},
},
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",
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"
attach_file: "Pièce jointe",
},
mileage: "Kilométrage",
open_btn: "Liste des Dépenses",
@ -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",

View File

@ -10,6 +10,7 @@
const router = useRouter();
const miniState = ref(true);
const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
@ -39,9 +40,17 @@
<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,
});