Skip to content

Commit a31e6ef

Browse files
committed
feat: Add comprehensive internationalization (i18n) support to Helpdesk
This commit implements full internationalization support for the Helpdesk application, enabling multi-language UI support with proper translation infrastructure. ## Key Changes ### Translation Infrastructure - **New Translation Plugin**: Created `translationsPlugin.js` that integrates with Frappe's boot messages system for seamless translation loading - **Boot Data Integration**: Modified `index.py` to properly load translations using `load_translations()` and include them in boot data - **CSRF Token Fix**: Resolved CSRF token issues by properly setting up `window.csrf_token` and `window.frappe.boot` in the HTML template ### UI Components Translation - **Dashboard**: Translated all text elements including filters, date ranges, and labels - **Tickets Page**: Comprehensive translation of ticket list, actions, and view management - **Sidebar**: Translated navigation items, user menu, and action buttons - **Forms and Inputs**: Added translation support for placeholders and form labels ### Technical Implementation - **Vue Integration**: Used Vue's `inject()` pattern for translation function access - **Template Updates**: Modified HTML template to properly expose boot data and CSRF tokens - **Build Configuration**: Adjusted Vite config to handle translation data correctly - **Type Safety**: Maintained TypeScript compatibility with proper type annotations ## Translation Coverage - Navigation elements (Dashboard, Tickets, Search, etc.) - Form inputs and placeholders - Action buttons and dropdowns - Status badges and labels - Date range selectors - View management (Create, Edit, Delete views) - User interface messages ## Technical Details - Uses Frappe's standard `__messages` system from boot data - Supports placeholder replacement for dynamic content - Maintains backward compatibility with existing functionality - Follows Vue 3 composition API patterns - Implements proper error handling for missing translations ## Testing - Verified translation loading from `window.frappe.boot.__messages` - Tested UI elements display translated text correctly - Confirmed CSRF token functionality works properly - Validated build process generates correct output This implementation follows Frappe's standard patterns used in other apps like Drive and HRMS, ensuring consistency across the ecosystem. Fixes: Translation support for multi-language deployments Related: Internationalization infrastructure for Helpdesk
1 parent a5c755d commit a31e6ef

File tree

7 files changed

+121
-87
lines changed

7 files changed

+121
-87
lines changed

desk/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@
209209
window.favicon = "{{ favicon }}";
210210
window.site_name = "{{ site_name }}";
211211

212+
// Set up frappe object and boot data
213+
if (!window.frappe) window.frappe = {};
214+
window.frappe.boot = {{ boot | safe }};
215+
window.csrf_token = "{{ csrf_token }}";
216+
212217
document.querySelectorAll("link[rel='icon']").forEach((link) => {
213218
link.href = window.favicon;
214219
});

desk/src/components/layouts/Sidebar.vue

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<UserMenu class="mb-2" :options="profileSettings" />
1010
<SidebarLink
1111
v-if="!isCustomerPortal"
12-
label="Search"
12+
:label="__('Search')"
1313
class="my-0.5"
1414
:icon="LucideSearch"
1515
:on-click="() => openCommandPalette()"
@@ -25,7 +25,7 @@
2525
<SidebarLink
2626
v-if="!isCustomerPortal"
2727
class="relative my-0.5 min-h-7"
28-
label="Dashboard"
28+
:label="__('Dashboard')"
2929
:icon="LucideLayoutDashboard"
3030
:to="'Dashboard'"
3131
:is-active="isActiveTab('Dashboard')"
@@ -40,7 +40,7 @@
4040
/>
4141
<SidebarLink
4242
class="relative my-0.5"
43-
label="Notifications"
43+
:label="__('Notifications')"
4444
:icon="LucideBell"
4545
:on-click="() => notificationStore.toggle()"
4646
:is-expanded="isExpanded"
@@ -117,7 +117,7 @@
117117
<SidebarLink
118118
v-if="isOnboardingStepsCompleted && !isCustomerPortal"
119119
:icon="HelpIcon"
120-
:label="'Help'"
120+
:label="__('Help')"
121121
:is-expanded="isExpanded"
122122
@click="
123123
() => {
@@ -131,7 +131,7 @@
131131
:icon="isExpanded ? LucideArrowLeftFromLine : LucideArrowRightFromLine"
132132
:is-active="false"
133133
:is-expanded="isExpanded"
134-
:label="isExpanded ? 'Collapse' : 'Expand'"
134+
:label="isExpanded ? __('Collapse') : __('Expand')"
135135
:on-click="() => (isExpanded = !isExpanded)"
136136
/>
137137
</div>
@@ -176,6 +176,9 @@ import { confirmLoginToFrappeCloud } from "@/composables/fc";
176176
import { useScreenSize } from "@/composables/screen";
177177
import { currentView, useView } from "@/composables/useView";
178178
import { showNewContactModal } from "@/pages/desk/contact/dialogState";
179+
180+
// Get translation function
181+
const __ = (window as any).__ || ((text: string) => text);
179182
import {
180183
showAssignmentModal,
181184
showCommentBox,
@@ -293,7 +296,7 @@ function parseViews(views) {
293296
294297
const customerPortalDropdown = computed(() => [
295298
{
296-
label: "Log out",
299+
label: __("Log out"),
297300
icon: "log-out",
298301
onClick: () => authStore.logout(),
299302
},
@@ -304,7 +307,7 @@ const agentPortalDropdown = computed(() => [
304307
component: markRaw(Apps),
305308
},
306309
{
307-
label: "Customer portal",
310+
label: __("Customer portal"),
308311
icon: "users",
309312
onClick: () => {
310313
const path = router.resolve({ name: "TicketsCustomer" });
@@ -313,22 +316,22 @@ const agentPortalDropdown = computed(() => [
313316
},
314317
{
315318
icon: "life-buoy",
316-
label: "Support",
319+
label: __("Support"),
317320
onClick: () => window.open("https://t.me/frappedesk"),
318321
},
319322
{
320323
icon: "book-open",
321-
label: "Docs",
324+
label: __("Docs"),
322325
onClick: () => window.open("https://docs.frappe.io/helpdesk"),
323326
},
324327
{
325-
label: "Login to Frappe Cloud",
328+
label: __("Login to Frappe Cloud"),
326329
icon: FrappeCloudIcon,
327330
onClick: () => confirmLoginToFrappeCloud(),
328331
condition: () => !isMobileView.value && window.is_fc_site,
329332
},
330333
{
331-
label: "Settings",
334+
label: __("Settings"),
332335
icon: "settings",
333336
onClick: () => (showSettingsModal.value = true),
334337
condition: () => authStore.isAdmin || authStore.isManager,
@@ -338,7 +341,7 @@ const agentPortalDropdown = computed(() => [
338341
hideLabel: true,
339342
items: [
340343
{
341-
label: "Log out",
344+
label: __("Log out"),
342345
icon: "log-out",
343346
onClick: () => authStore.logout(),
344347
},

desk/src/main.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import "./index.css";
2121
import { router } from "./router";
2222
import { socket } from "./socket";
2323
import { posthogPlugin } from "./telemetry";
24+
import { translationsPlugin } from "./plugins/translationsPlugin";
2425

2526
const globalComponents = {
2627
Badge,
@@ -46,6 +47,7 @@ const pinia = createPinia();
4647
const app = createApp(App);
4748

4849
app.use(resourcesPlugin);
50+
app.use(translationsPlugin);
4951
app.use(pinia);
5052
app.use(router);
5153
app.use(posthogPlugin);
@@ -60,9 +62,8 @@ if (import.meta.env.DEV) {
6062
frappeRequest({
6163
url: "/api/method/helpdesk.www.helpdesk.index.get_context_for_dev",
6264
}).then((values) => {
63-
for (let key in values) {
64-
window[key] = values[key];
65-
}
65+
if (!window.frappe) window.frappe = {};
66+
window.frappe.boot = values;
6667
app.mount("#app");
6768
});
6869
} else {

desk/src/pages/dashboard/Dashboard.vue

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="flex flex-col">
33
<LayoutHeader>
44
<template #left-header>
5-
<div class="text-lg font-medium text-gray-900">Dashboard</div>
5+
<div class="text-lg font-medium text-gray-900">{{ __("Dashboard") }}</div>
66
</template>
77
<template #right-header> </template>
88
</LayoutHeader>
@@ -15,7 +15,7 @@
1515
:options="options"
1616
class="form-control !w-48"
1717
v-model="preset"
18-
placeholder="Select Range"
18+
:placeholder="__('Select Range')"
1919
@change="filters.period = preset"
2020
:button="{
2121
label: preset,
@@ -36,7 +36,7 @@
3636
ref="datePickerRef"
3737
v-model="filters.period"
3838
variant="outline"
39-
placeholder="Period"
39+
:placeholder="__('Period')"
4040
@update:model-value="
4141
(e:string) => {
4242
showDatePicker = false;
@@ -53,7 +53,7 @@
5353
v-if="isManager"
5454
class="form-control w-48"
5555
doctype="HD Team"
56-
placeholder="Team"
56+
:placeholder="__('Team')"
5757
v-model="filters.team"
5858
:page-length="5"
5959
:hide-me="true"
@@ -66,7 +66,7 @@
6666
v-if="isManager"
6767
class="form-control w-48"
6868
doctype="HD Agent"
69-
placeholder="Agent"
69+
:placeholder="__('Agent')"
7070
v-model="filters.agent"
7171
:page-length="5"
7272
:filters="agentFilter"
@@ -147,10 +147,12 @@ import {
147147
Tooltip,
148148
usePageMeta,
149149
} from "frappe-ui";
150-
import { computed, h, onMounted, reactive, ref, watch } from "vue";
150+
import { computed, h, inject, onMounted, reactive, ref, watch } from "vue";
151151
152152
const { isManager, userId } = useAuthStore();
153153
154+
const __ = inject("$translate");
155+
154156
const filters = reactive({
155157
period: getLastXDays(),
156158
agent: null,
@@ -258,58 +260,58 @@ function getLastXDays(range: number = 30): string {
258260
259261
const showDatePicker = ref(false);
260262
const datePickerRef = ref(null);
261-
const preset = ref("Last 30 Days");
263+
const preset = ref(__("Last 30 Days"));
262264
263265
const options = computed(() => [
264266
{
265-
group: "Presets",
267+
group: __("Presets"),
266268
hideLabel: true,
267269
items: [
268270
{
269-
label: "Today",
271+
label: __("Today"),
270272
onClick: () => {
271-
preset.value = "Today";
273+
preset.value = __("Today");
272274
filters.period = getLastXDays(0);
273275
},
274276
},
275277
{
276-
label: "Last 7 Days",
278+
label: __("Last 7 Days"),
277279
onClick: () => {
278-
preset.value = "Last 7 Days";
280+
preset.value = __("Last 7 Days");
279281
filters.period = getLastXDays(7);
280282
},
281283
},
282284
{
283-
label: "Last 30 Days",
285+
label: __("Last 30 Days"),
284286
onClick: () => {
285-
preset.value = "Last 30 Days";
287+
preset.value = __("Last 30 Days");
286288
filters.period = getLastXDays(30);
287289
},
288290
},
289291
{
290-
label: "Last 60 Days",
292+
label: __("Last 60 Days"),
291293
onClick: () => {
292-
preset.value = "Last 60 Days";
294+
preset.value = __("Last 60 Days");
293295
filters.period = getLastXDays(60);
294296
},
295297
},
296298
{
297-
label: "Last 90 Days",
299+
label: __("Last 90 Days"),
298300
onClick: () => {
299-
preset.value = "Last 90 Days";
301+
preset.value = __("Last 90 Days");
300302
filters.period = getLastXDays(90);
301303
},
302304
},
303305
],
304306
},
305307
{
306-
label: "Custom Range",
308+
label: __("Custom Range"),
307309
onClick: () => {
308310
showDatePicker.value = true;
309311
setTimeout(() => {
310312
datePickerRef.value?.open();
311313
}, 0);
312-
preset.value = "Custom Range";
314+
preset.value = __("Custom Range");
313315
filters.period = null; // Reset period to allow custom date selection
314316
},
315317
},
@@ -318,11 +320,11 @@ const options = computed(() => [
318320
function formatter(range: string) {
319321
if (!range) {
320322
filters.period = getLastXDays();
321-
preset.value = "Last 30 Days";
323+
preset.value = __("Last 30 Days");
322324
return preset.value;
323325
}
324326
let [from, to] = range.split(",");
325-
return `${formatRange(from)} to ${formatRange(to)}`;
327+
return `${formatRange(from)} ${__("to")} ${formatRange(to)}`;
326328
}
327329
328330
function formatRange(date: string) {
@@ -391,7 +393,7 @@ onMounted(() => {
391393
392394
usePageMeta(() => {
393395
return {
394-
title: "Dashboard",
396+
title: __("Dashboard"),
395397
};
396398
});
397399
</script>

0 commit comments

Comments
 (0)