Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,51 @@ Frappe Gantt exposes a few helpful methods for you to interact with the chart:
| `.scroll_current` | Scrolls to the current date | No parameters. |
| `.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. |

## Internationalization (i18n)

The Gantt chart supports multiple languages. By default, it's in English, but you can easily switch to other languages:

### Basic usage

```javascript
import Gantt from 'frappe-gantt';

// Create Gantt chart with English (default)
const gantt = new Gantt('#gantt', tasks);
```

### Using a specific language

```javascript
import Gantt from 'frappe-gantt';

const gantt = new Gantt('#gantt', tasks, {
language: 'it', // or 'it-IT'
});
```

### Adding custom translations

You can also create your own translations:

```javascript
import Gantt from 'frappe-gantt';

const myEoLocale = {
Mode: 'Modo',
Today: 'Hodiaŭ',
Year: 'Jaro',
// ...
};

const gantt = new Gantt('#gantt', tasks, {
language: 'eo',
locales: {
'eo': myEoLocale,
},
});
```

## Development Setup
If you want to contribute enhancements or fixes:

Expand Down
12 changes: 9 additions & 3 deletions src/defaults.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import date_utils from './date_utils';
import { gettext } from './i18n';

function getDecade(d) {
const year = d.getFullYear();
Expand Down Expand Up @@ -130,19 +131,24 @@ const DEFAULT_OPTIONS = {
if (ctx.task.description) ctx.set_subtitle(ctx.task.description);
else ctx.set_subtitle('');

const lang = ctx.chart.options.language;
const start_date = date_utils.format(
ctx.task._start,
'MMM D',
ctx.chart.options.language,
lang,
);
const end_date = date_utils.format(
date_utils.add(ctx.task._end, -1, 'second'),
'MMM D',
ctx.chart.options.language,
lang,
);

const excluded_text = ctx.task.ignored_duration
? ` + ${ctx.task.ignored_duration} ${gettext('excluded', lang)}`
: '';

ctx.set_details(
`${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}%`,
`${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}%`,
);
},
popup_on: 'click',
Expand Down
109 changes: 109 additions & 0 deletions src/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Import all locale files directly
import en_US from './locales/en-US.json';
import it_IT from './locales/it-IT.json';
import fr_FR from './locales/fr-FR.json';
import es_ES from './locales/es-ES.json';
import de_DE from './locales/de-DE.json';

// Static map of all available locales
const locales = {
'en': en_US,
'en-US': en_US,
'it': it_IT,
'it-IT': it_IT,
'fr': fr_FR,
'fr-FR': fr_FR,
'es': es_ES,
'es-ES': es_ES,
'de': de_DE,
'de-DE': de_DE
};

/**
* Add custom locales to the available locales
* @param {Object} customLocales - Object containing custom locale translations
*/
export function addLocales(customLocales) {
if (!customLocales) return;

// Merge custom locales with existing ones
Object.keys(customLocales).forEach(langCode => {
locales[langCode] = customLocales[langCode];
});
}

/**
* Get a translation for a key in the specified language
* @param {string} key - The translation key
* @param {string} lang - The language code (defaults to 'en')
* @param {Object} params - Parameters to replace in the translation
* @returns {string} The translated text or the key if not found
*/
export function translate(key, lang = 'en', params = {}) {
// Get the appropriate locale or fall back to English
const langCode = normalizeLangCode(lang);
const locale = locales[langCode] || locales['en'];

if (!locale) {
return key;
}

let text = locale[key] || key;

// Replace any parameters in the text
if (params && Object.keys(params).length > 0) {
Object.keys(params).forEach(param => {
text = text.replace(new RegExp(`{${param}}`, 'g'), params[param]);
});
}

return text;
}

/**
* Alias for translate function for backward compatibility
*/
export function gettext(key, lang, params) {
return translate(key, lang, params);
}

/**
* Normalize language code to find the most appropriate match
* @param {string} langCode - The language code to normalize
* @returns {string} The normalized language code
*/
function normalizeLangCode(langCode) {
if (!langCode) return 'en';

// First check exact match
if (locales[langCode]) return langCode;

// Check language part only (e.g., 'en' from 'en-US')
const mainLang = langCode.split('-')[0];
if (locales[mainLang]) return mainLang;

// Check for any variant of the language
const allKeys = Object.keys(locales);
for (let i = 0; i < allKeys.length; i++) {
if (allKeys[i].startsWith(mainLang + '-')) {
return allKeys[i];
}
}

return 'en';
}

/**
* Get all available languages
* @returns {Object} An object with language codes as keys
*/
export function getAvailableLanguages() {
return Object.keys(locales);
}

export default {
translate,
gettext,
getAvailableLanguages,
addLocales
};
20 changes: 13 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import date_utils from './date_utils';
import { gettext, addLocales } from './i18n';
import { $, createSVG } from './svg_utils';

import Arrow from './arrow';
Expand All @@ -13,6 +14,9 @@ export default class Gantt {
constructor(wrapper, tasks, options) {
this.setup_wrapper(wrapper);
this.setup_options(options);
if (options.locales) {
addLocales(options.locales);
}
this.setup_tasks(tasks);
this.change_view_mode();
this.bind_events();
Expand Down Expand Up @@ -124,7 +128,7 @@ export default class Gantt {
.map((task, i) => {
if (!task.start) {
console.error(
`task "${task.id}" doesn't have a start date`,
gettext('task_no_start_date', this.options.language, { id: task.id || '' }),
);
return false;
}
Expand All @@ -141,23 +145,25 @@ export default class Gantt {
});
}
if (!task.end) {
console.error(`task "${task.id}" doesn't have an end date`);
console.error(
gettext('task_no_end_date', this.options.language, { id: task.id || '' }),
);
return false;
}
task._end = date_utils.parse(task.end);

let diff = date_utils.diff(task._end, task._start, 'year');
if (diff < 0) {
console.error(
`start of task can't be after end of task: in task "${task.id}"`,
gettext('task_start_after_end', this.options.language, { id: task.id || '' }),
);
return false;
}

// make task invalid if duration too large
if (date_utils.diff(task._end, task._start, 'year') > 10) {
console.error(
`the duration of task "${task.id}" is too long (above ten years)`,
gettext('task_duration_too_long', this.options.language, { id: task.id || '' }),
);
return false;
}
Expand Down Expand Up @@ -476,13 +482,13 @@ export default class Gantt {
const $el = document.createElement('option');
$el.selected = true;
$el.disabled = true;
$el.textContent = 'Mode';
$el.textContent = gettext('Mode', this.options.language);
$select.appendChild($el);

for (const mode of this.options.view_modes) {
const $option = document.createElement('option');
$option.value = mode.name;
$option.textContent = mode.name;
$option.textContent = gettext(mode.name, this.options.language);
if (mode.name === this.config.view_mode.name)
$option.selected = true;
$select.appendChild($option);
Expand All @@ -501,7 +507,7 @@ export default class Gantt {
if (this.options.today_button) {
let $today_button = document.createElement('button');
$today_button.classList.add('today-button');
$today_button.textContent = 'Today';
$today_button.textContent = gettext('Today', this.options.language);
$today_button.onclick = this.scroll_current.bind(this);
this.$side_header.prepend($today_button);
this.$today_button = $today_button;
Expand Down
31 changes: 31 additions & 0 deletions src/locales/de-DE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"Mode": "Modus",
"Today": "Heute",
"Year": "Jahr",
"Month": "Monat",
"Week": "Woche",
"Day": "Tag",
"Hour": "Stunde",
"Quarter Day": "Viertel Tag",
"Half Day": "Halber Tag",
"year": "Jahr",
"years": "Jahre",
"month": "Monat",
"months": "Monate",
"day": "Tag",
"days": "Tage",
"hour": "Stunde",
"hours": "Stunden",
"minute": "Minute",
"minutes": "Minuten",
"second": "Sekunde",
"seconds": "Sekunden",
"Progress": "Fortschritt",
"Duration": "Dauer",
"Dates": "Termine",
"excluded": "ausgeschlossen",
"task_no_start_date": "Aufgabe \"{id}\" hat kein Startdatum",
"task_no_end_date": "Aufgabe \"{id}\" hat kein Enddatum",
"task_start_after_end": "Der Start der Aufgabe kann nicht nach dem Ende der Aufgabe liegen: in Aufgabe \"{id}\"",
"task_duration_too_long": "Die Dauer der Aufgabe \"{id}\" ist zu lang (über zehn Jahre)"
}
31 changes: 31 additions & 0 deletions src/locales/en-UK.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"Mode": "Mode",
"Today": "Today",
"Year": "Year",
"Month": "Month",
"Week": "Week",
"Day": "Day",
"Hour": "Hour",
"Quarter Day": "Quarter Day",
"Half Day": "Half Day",
"year": "year",
"years": "years",
"month": "month",
"months": "months",
"day": "day",
"days": "days",
"hour": "hour",
"hours": "hours",
"minute": "minute",
"minutes": "minutes",
"second": "second",
"seconds": "seconds",
"Progress": "Progress",
"Duration": "Duration",
"Dates": "Dates",
"excluded": "excluded",
"task_no_start_date": "task \"{id}\" doesn't have a start date",
"task_no_end_date": "task \"{id}\" doesn't have an end date",
"task_start_after_end": "start of task can't be after end of task: in task \"{id}\"",
"task_duration_too_long": "the duration of task \"{id}\" is too long (above ten years)"
}
31 changes: 31 additions & 0 deletions src/locales/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"Mode": "Mode",
"Today": "Today",
"Year": "Year",
"Month": "Month",
"Week": "Week",
"Day": "Day",
"Hour": "Hour",
"Quarter Day": "Quarter Day",
"Half Day": "Half Day",
"year": "year",
"years": "years",
"month": "month",
"months": "months",
"day": "day",
"days": "days",
"hour": "hour",
"hours": "hours",
"minute": "minute",
"minutes": "minutes",
"second": "second",
"seconds": "seconds",
"Progress": "Progress",
"Duration": "Duration",
"Dates": "Dates",
"excluded": "excluded",
"task_no_start_date": "task \"{id}\" doesn't have a start date",
"task_no_end_date": "task \"{id}\" doesn't have an end date",
"task_start_after_end": "start of task can't be after end of task: in task \"{id}\"",
"task_duration_too_long": "the duration of task \"{id}\" is too long (above ten years)"
}
Loading