Skip to content

Commit ad2f572

Browse files
committed
Add i18n support
1 parent 108eeb5 commit ad2f572

File tree

12 files changed

+458
-12
lines changed

12 files changed

+458
-12
lines changed

README.md

+44
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,50 @@ Frappe Gantt exposes a few helpful methods for you to interact with the chart:
136136
| `.scroll_current` | Scrolls to the current date | No parameters. |
137137
| `.update_task` | Re-renders a specific task bar alone | `task_id` - id of task and `new_details` - object containing the task properties to be updated. |
138138

139+
## Internationalization (i18n)
140+
141+
The Gantt chart supports multiple languages. By default, it's in English, but you can easily switch to other languages:
142+
143+
### Basic usage
144+
145+
```javascript
146+
import Gantt from 'frappe-gantt';
147+
148+
// Create Gantt chart with English (default)
149+
const gantt = new Gantt('#gantt', tasks);
150+
```
151+
152+
### Using a specific language
153+
154+
```javascript
155+
import Gantt from 'frappe-gantt';
156+
157+
const gantt = new Gantt('#gantt', tasks, {
158+
language: 'it', // or 'it-IT'
159+
});
160+
```
161+
162+
### Adding custom translations
163+
164+
You can also create your own translations:
165+
166+
```javascript
167+
import Gantt from 'frappe-gantt';
168+
169+
const myEoLocale = {
170+
Mode: 'Modo',
171+
Today: 'Hodiaŭ',
172+
Year: 'Jaro',
173+
// ...
174+
};
175+
176+
const gantt = new Gantt('#gantt', tasks, {
177+
language: 'eo',
178+
locales: {
179+
'eo': myEoLocale,
180+
},
181+
});
182+
139183
## Development Setup
140184
If you want to contribute enhancements or fixes:
141185

src/defaults.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import date_utils from './date_utils';
2+
import { gettext } from './i18n';
23

34
function getDecade(d) {
45
const year = d.getFullYear();
@@ -130,19 +131,24 @@ const DEFAULT_OPTIONS = {
130131
if (ctx.task.description) ctx.set_subtitle(ctx.task.description);
131132
else ctx.set_subtitle('');
132133

134+
const lang = ctx.chart.options.language;
133135
const start_date = date_utils.format(
134136
ctx.task._start,
135137
'MMM D',
136-
ctx.chart.options.language,
138+
lang
137139
);
138140
const end_date = date_utils.format(
139141
date_utils.add(ctx.task._end, -1, 'second'),
140142
'MMM D',
141-
ctx.chart.options.language,
143+
lang
142144
);
143145

146+
const excluded_text = ctx.task.ignored_duration
147+
? ` + ${ctx.task.ignored_duration} ${gettext('excluded', lang)}`
148+
: '';
149+
144150
ctx.set_details(
145-
`${start_date} - ${end_date} (${ctx.task.actual_duration} days${ctx.task.ignored_duration ? ' + ' + ctx.task.ignored_duration + ' excluded' : ''})<br/>Progress: ${Math.floor(ctx.task.progress * 100) / 100}%`,
151+
`${gettext('Dates', lang)}: ${start_date} - ${end_date} (${ctx.task.actual_duration} ${gettext(ctx.task.actual_duration === 1 ? 'day' : 'days', lang)}${excluded_text})<br/>${gettext('Progress', lang)}: ${Math.floor(ctx.task.progress * 100) / 100}%`
146152
);
147153
},
148154
popup_on: 'click',

src/i18n.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Import all locale files directly
2+
import en_US from './locales/en-US.json';
3+
import it_IT from './locales/it-IT.json';
4+
import fr_FR from './locales/fr-FR.json';
5+
import es_ES from './locales/es-ES.json';
6+
import de_DE from './locales/de-DE.json';
7+
8+
// Static map of all available locales
9+
const locales = {
10+
'en': en_US,
11+
'en-US': en_US,
12+
'it': it_IT,
13+
'it-IT': it_IT,
14+
'fr': fr_FR,
15+
'fr-FR': fr_FR,
16+
'es': es_ES,
17+
'es-ES': es_ES,
18+
'de': de_DE,
19+
'de-DE': de_DE
20+
};
21+
22+
/**
23+
* Add custom locales to the available locales
24+
* @param {Object} customLocales - Object containing custom locale translations
25+
*/
26+
export function addLocales(customLocales) {
27+
if (!customLocales) return;
28+
29+
// Merge custom locales with existing ones
30+
Object.keys(customLocales).forEach(langCode => {
31+
locales[langCode] = customLocales[langCode];
32+
});
33+
}
34+
35+
/**
36+
* Get a translation for a key in the specified language
37+
* @param {string} key - The translation key
38+
* @param {string} lang - The language code (defaults to 'en')
39+
* @param {Object} params - Parameters to replace in the translation
40+
* @returns {string} The translated text or the key if not found
41+
*/
42+
export function translate(key, lang = 'en', params = {}) {
43+
// Get the appropriate locale or fall back to English
44+
const langCode = normalizeLangCode(lang);
45+
const locale = locales[langCode] || locales['en'];
46+
47+
if (!locale) {
48+
return key;
49+
}
50+
51+
let text = locale[key] || key;
52+
53+
// Replace any parameters in the text
54+
if (params && Object.keys(params).length > 0) {
55+
Object.keys(params).forEach(param => {
56+
text = text.replace(new RegExp(`{${param}}`, 'g'), params[param]);
57+
});
58+
}
59+
60+
return text;
61+
}
62+
63+
/**
64+
* Alias for translate function for backward compatibility
65+
*/
66+
export function gettext(key, lang, params) {
67+
return translate(key, lang, params);
68+
}
69+
70+
/**
71+
* Normalize language code to find the most appropriate match
72+
* @param {string} langCode - The language code to normalize
73+
* @returns {string} The normalized language code
74+
*/
75+
function normalizeLangCode(langCode) {
76+
if (!langCode) return 'en';
77+
78+
// First check exact match
79+
if (locales[langCode]) return langCode;
80+
81+
// Check language part only (e.g., 'en' from 'en-US')
82+
const mainLang = langCode.split('-')[0];
83+
if (locales[mainLang]) return mainLang;
84+
85+
// Check for any variant of the language
86+
const allKeys = Object.keys(locales);
87+
for (let i = 0; i < allKeys.length; i++) {
88+
if (allKeys[i].startsWith(mainLang + '-')) {
89+
return allKeys[i];
90+
}
91+
}
92+
93+
return 'en';
94+
}
95+
96+
/**
97+
* Get all available languages
98+
* @returns {Object} An object with language codes as keys
99+
*/
100+
export function getAvailableLanguages() {
101+
return Object.keys(locales);
102+
}
103+
104+
export default {
105+
translate,
106+
gettext,
107+
getAvailableLanguages,
108+
addLocales
109+
};

src/index.js

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import date_utils from './date_utils';
2+
import { gettext, addLocales } from './i18n';
23
import { $, createSVG } from './svg_utils';
34

45
import Arrow from './arrow';
@@ -13,6 +14,9 @@ export default class Gantt {
1314
constructor(wrapper, tasks, options) {
1415
this.setup_wrapper(wrapper);
1516
this.setup_options(options);
17+
if (options.locales) {
18+
addLocales(options.locales);
19+
}
1620
this.setup_tasks(tasks);
1721
this.change_view_mode();
1822
this.bind_events();
@@ -26,7 +30,7 @@ export default class Gantt {
2630
let el = document.querySelector(element);
2731
if (!el) {
2832
throw new ReferenceError(
29-
`CSS selector "${element}" could not be found in DOM`,
33+
`CSS selector "${element}" could not be found in DOM`
3034
);
3135
}
3236
element = el;
@@ -41,7 +45,7 @@ export default class Gantt {
4145
} else {
4246
throw new TypeError(
4347
'Frappe Gantt only supports usage of a string CSS selector,' +
44-
" HTML DOM element or SVG DOM element for the 'element' parameter",
48+
" HTML DOM element or SVG DOM element for the 'element' parameter"
4549
);
4650
}
4751

@@ -124,7 +128,7 @@ export default class Gantt {
124128
.map((task, i) => {
125129
if (!task.start) {
126130
console.error(
127-
`task "${task.id}" doesn't have a start date`,
131+
gettext('task_no_start_date', this.options.language, { id: task.id || '' })
128132
);
129133
return false;
130134
}
@@ -141,23 +145,25 @@ export default class Gantt {
141145
});
142146
}
143147
if (!task.end) {
144-
console.error(`task "${task.id}" doesn't have an end date`);
148+
console.error(
149+
gettext('task_no_end_date', this.options.language, { id: task.id || '' })
150+
);
145151
return false;
146152
}
147153
task._end = date_utils.parse(task.end);
148154

149155
let diff = date_utils.diff(task._end, task._start, 'year');
150156
if (diff < 0) {
151157
console.error(
152-
`start of task can't be after end of task: in task "${task.id}"`,
158+
gettext('task_start_after_end', this.options.language, { id: task.id || '' })
153159
);
154160
return false;
155161
}
156162

157163
// make task invalid if duration too large
158164
if (date_utils.diff(task._end, task._start, 'year') > 10) {
159165
console.error(
160-
`the duration of task "${task.id}" is too long (above ten years)`,
166+
gettext('task_duration_too_long', this.options.language, { id: task.id || '' })
161167
);
162168
return false;
163169
}
@@ -476,13 +482,13 @@ export default class Gantt {
476482
const $el = document.createElement('option');
477483
$el.selected = true;
478484
$el.disabled = true;
479-
$el.textContent = 'Mode';
485+
$el.textContent = gettext('Mode', this.options.language);
480486
$select.appendChild($el);
481487

482488
for (const mode of this.options.view_modes) {
483489
const $option = document.createElement('option');
484490
$option.value = mode.name;
485-
$option.textContent = mode.name;
491+
$option.textContent = gettext(mode.name, this.options.language);
486492
if (mode.name === this.config.view_mode.name)
487493
$option.selected = true;
488494
$select.appendChild($option);
@@ -501,7 +507,7 @@ export default class Gantt {
501507
if (this.options.today_button) {
502508
let $today_button = document.createElement('button');
503509
$today_button.classList.add('today-button');
504-
$today_button.textContent = 'Today';
510+
$today_button.textContent = gettext('Today', this.options.language);
505511
$today_button.onclick = this.scroll_current.bind(this);
506512
this.$side_header.prepend($today_button);
507513
this.$today_button = $today_button;

src/locales/de-DE.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"Mode": "Modus",
3+
"Today": "Heute",
4+
"Year": "Jahr",
5+
"Month": "Monat",
6+
"Week": "Woche",
7+
"Day": "Tag",
8+
"Hour": "Stunde",
9+
"Quarter Day": "Viertel Tag",
10+
"Half Day": "Halber Tag",
11+
"year": "Jahr",
12+
"years": "Jahre",
13+
"month": "Monat",
14+
"months": "Monate",
15+
"day": "Tag",
16+
"days": "Tage",
17+
"hour": "Stunde",
18+
"hours": "Stunden",
19+
"minute": "Minute",
20+
"minutes": "Minuten",
21+
"second": "Sekunde",
22+
"seconds": "Sekunden",
23+
"Progress": "Fortschritt",
24+
"Duration": "Dauer",
25+
"Dates": "Termine",
26+
"excluded": "ausgeschlossen",
27+
"task_no_start_date": "Aufgabe \"{id}\" hat kein Startdatum",
28+
"task_no_end_date": "Aufgabe \"{id}\" hat kein Enddatum",
29+
"task_start_after_end": "Der Start der Aufgabe kann nicht nach dem Ende der Aufgabe liegen: in Aufgabe \"{id}\"",
30+
"task_duration_too_long": "Die Dauer der Aufgabe \"{id}\" ist zu lang (über zehn Jahre)"
31+
}

src/locales/en-UK.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"Mode": "Mode",
3+
"Today": "Today",
4+
"Year": "Year",
5+
"Month": "Month",
6+
"Week": "Week",
7+
"Day": "Day",
8+
"Hour": "Hour",
9+
"Quarter Day": "Quarter Day",
10+
"Half Day": "Half Day",
11+
"year": "year",
12+
"years": "years",
13+
"month": "month",
14+
"months": "months",
15+
"day": "day",
16+
"days": "days",
17+
"hour": "hour",
18+
"hours": "hours",
19+
"minute": "minute",
20+
"minutes": "minutes",
21+
"second": "second",
22+
"seconds": "seconds",
23+
"Progress": "Progress",
24+
"Duration": "Duration",
25+
"Dates": "Dates",
26+
"excluded": "excluded",
27+
"task_no_start_date": "task \"{id}\" doesn't have a start date",
28+
"task_no_end_date": "task \"{id}\" doesn't have an end date",
29+
"task_start_after_end": "start of task can't be after end of task: in task \"{id}\"",
30+
"task_duration_too_long": "the duration of task \"{id}\" is too long (above ten years)"
31+
}

src/locales/en-US.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"Mode": "Mode",
3+
"Today": "Today",
4+
"Year": "Year",
5+
"Month": "Month",
6+
"Week": "Week",
7+
"Day": "Day",
8+
"Hour": "Hour",
9+
"Quarter Day": "Quarter Day",
10+
"Half Day": "Half Day",
11+
"year": "year",
12+
"years": "years",
13+
"month": "month",
14+
"months": "months",
15+
"day": "day",
16+
"days": "days",
17+
"hour": "hour",
18+
"hours": "hours",
19+
"minute": "minute",
20+
"minutes": "minutes",
21+
"second": "second",
22+
"seconds": "seconds",
23+
"Progress": "Progress",
24+
"Duration": "Duration",
25+
"Dates": "Dates",
26+
"excluded": "excluded",
27+
"task_no_start_date": "task \"{id}\" doesn't have a start date",
28+
"task_no_end_date": "task \"{id}\" doesn't have an end date",
29+
"task_start_after_end": "start of task can't be after end of task: in task \"{id}\"",
30+
"task_duration_too_long": "the duration of task \"{id}\" is too long (above ten years)"
31+
}

0 commit comments

Comments
 (0)