On charts/tables/visualization we support UTF-8 character, that means whichever language you have the data in, it will appear exactly in the same format in the visualizations. Even the text components or headers or column headers alias etc which you are adding at report/dashboard it can be in anylanguage.
However, by default Open Source BI Helical Insight entire text comes in English language across all the pages, but you may want to change the text to your respective language based on your customers preference. In a similar way if you would like to change the date timeone into some other timezones, number formatting, currency formatting that also is explained. So in this blog we would explaining localization or internationalization in Open Source BI product Helical Insight. This blog is relevant to version 5.0 build 1083 onwards.
Here we are passing the things like localize and timezone in the URL and accordingly we are changing the entire text and date date based on that.
Changing Language
- The first step is to download the patch (LocalizationPatch.zip ) and unzip it. Then put the unzipped folder (when we unzip we will get folder by this name :LocalizationPatch) at the location (path : …\hi\apache-tomcat-9\webapps\hi-ee\js) .
- After this step we must open the file loginBody.jsp in edit mode and add script tag (path of this file is : path : …\hi\apache-tomcat-9\webapps\hi-ee\WEB-INF\jsp\login )as shown below. In the script tag we are pointing to the files i.e. home.js and dataModule.js which are part of the LocalizationPatch from the previous step.
Tags :
<scriptsrc="${baseURL}/js/LocalizationPatch/home.js"></script> <scriptsrc="${baseURL}/js/LocalizationPatch/dataModule.js"></script>
- The LocalizationPatch folder contains below files :
Langs(folder) –– english.json
– french.json
– spanish.json
home.js
dataModule.js
- Langs folder contains all the languages json file, for example in spanish.json there will be key value pairs of the words in English and their Spanish translations, same goes for other two languages as well. In a similar way if you would like to create a new language pack, then that respective language.json (like tamil.json, Arabic.jsonetc) can be created in which you can in a similar way save the key value pair from English to respective language.
- In home.js file we have written the logic for switching text based on the locale that is passed in the URL.
- The dataModule.js file contains all the logic related to conversion of date, time based on locale and timezone (that is passed in URL)
Note : As In this method we are taking the locale and timeZone, we need to make sure to pass these timeone, locale etc in URL for every request. If not passed it will show in the default.
- Now coming to actual working logic, the language of the text and conversion of date and time is done based on the locale and timezone that is passed in th URL
Eg : ‘http://216.48.177.235:8085/bi-ee/#/admin/overview?locale=fr&timezone=EST‘;
Here in the above url we are passing the locale as fr(french) and timezoneas EST, hence the language of the text will be converted to french and the time and data in the reports are adjusted to the format of fr or EST.
It is mandatory to use both locale and timezone in the URL and pass some values even if you want to use only one, otherwise it might give error message. You can even pass even blank values also in the URL.
- NOTE: In this step 5, we have elaborated of the working code which is developed and you can completely skip this part also. However you can go through the code for understanding of how it works.
In home.js, Total functionality is divided into small functions to reduce the repetition of the code.
- Function -elementChecker() :
This function is responsible for checking whether the required element in present in the dom or not. In this function the code will try to get the required element every half second for 10 seconds once it gets the element at any moment of time this function will be stopped, and the element is returned. If not it will give an error after 10 seconds. This can also be changed as well.
Code:
const elementChecker = async (elementIdentifier) => {
const checkInterval = 500; // Check every 500 milliseconds
const maxAttempts = 10; // Try for a maximum of 10 seconds (20 * 500ms)
let attempts = 0;
while (attempts <maxAttempts) {
const el = document.querySelector(elementIdentifier);
if (el !== null) {
return el;
}
await new Promise((resolve) =>setTimeout(resolve, checkInterval));
attempts++;
}
return null;
};
- Function – getParams() :
This Function is responsible to get the locale and timezone from the Url and provide to all other function that call this function. It will return an array which contains two values i.e. locale and timezone.
Code :
const getParams = () => { const hash = window.location.hash; const parts = hash.split("?"); if (parts.length> 1) { const query = parts[1]; const params = new URLSearchParams(query); const localeValue = params.get("locale"); const timezoneValue = params.get("timezone"); return [localeValue, timezoneValue]; } else { console.log("No query parameters in the fragment identifier."); } };
- Function – pathChecker() :
This Function will check the path and trigger only the functions that have access to that particular path. For example, if we are in the home page then it will call only the functions that are responsible for the text conversion of home page. Here in this function we are just writing few if statements like if this is the path trigger this function.
const pathChecker = async () => { let currentURL = window.location.href; let dataSourcePaths = [ `${baseURL}/#/datasource/all`, `${baseURL}/#/datasource/supported`, `${baseURL}/#/datasource/bigdata`, `${baseURL}/#/datasource/flatfiles`, `${baseURL}/#/datasource/rdbms`, `${baseURL}/#/datasource/nosql`, `${baseURL}/#/datasource/nosql`, ]; if (currentURL.startsWith(`${baseURL}/#/admin/overview`)) { homeMainFunction(); } else if (dataSourcePaths.includes(currentURL)) { dataSourceMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/metadata`)) { metadataMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/helical-report`)) { reportMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/dashboard-designer`)) { dashboardMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/admin/recycle-bin`)) { homeRecycleBinMainFunction(); } else if ( currentURL.startsWith(`${baseURL}/#/admin/usermanagement/organizations`) ) { homeUserManagementOrgMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/admin/usermanagement/roles`)) { homeUserManagementRoleMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/admin/usermanagement/users`)) { homeUserManagementUserMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/admin/management`)) { homeManagementMainFunction(); } else if (currentURL.startsWith(`${baseURL}/#/admin/Schedule`)) { homeExtraPathsFunction(); } else if (currentURL.startsWith(`${baseURL}/#/admin/plugins`)) { homeExtraPathsFunction(); } else if (currentURL.startsWith(`${baseURL}/#/report-viewer`)) { // setTimeout(addingDropDownInNewPage, 5000); } };
- Function – onStart() :
This is a very small function that will trigger when we refresh the page(it makes all the localization happen for the first time after refresh)
Code :
const onStart = async () => { pathChecker(); };
- There is aneventListenter which is also a very small function that will trigger the pathChecker function whenever there is change in the hash(URL) like going from one page to another page.
Code :
window.addEventListener("hashchange", async function () { pathChecker(); });
- Function – loadJSONFile() :
This will take two parameters i.e. one is file path and second is callback function.This function is responsible for mapping the page to correct language json file based on the locale in the URL.When we are triggering this function we will give the path of the json file that needs to be loaded and returned. This will be done dynamically in another function after successfully loading the json file. We can access the json file using call back function.
Code :
const loadJSONFile = (filePath, callback) => { const xhr = new XMLHttpRequest(); xhr.overrideMimeType("application/json"); xhr.open("GET", filePath, true); xhr.onreadystatechange = function () { if (xhr.readyState === 4 &&xhr.status === 200) { try { const data = JSON.parse(xhr.responseText); callback(data); } catch (error) { console.error("Error parsing JSON:", error); } } }; xhr.send(null); };
- Now there are some key functions (homeMainFunction() , dataSourcMainFunction() , metadataMainFunction() etc ). These functions will be triggered when there is a change in hash. This functions responsibility is to check whether the dom is completely loaded and if yes it will trigger again a subfunctions(eg : changingHomeUIetc). This function will know whether the dom is fully loaded or not by triggering the elementChecker function (with the id/class of the element) if this return the element then the dom is loaded .
One example from above Function :
const metadataMainFunction = async () => { let el = await elementChecker( "section.edit-section .connections-container-1 span.ant-divider-inner-text" ); let [prefLang, prefTimeZone] = getParams(); if (prefLang.startsWith("fr")) { changingMetadataUI("french"); } else if (prefLang.startsWith("es")) { changingMetadataUI("spanish"); } else { changingMetadataUI("english"); } };
- Whenever we trigger the pathChecker function it will trigger the above mentioned function , and that function will trigger subFunction( changingHomeUI ,changingMetadataUI ,changingReportUI ,changingDataSourceUI ,changingDashboardUI ). This functions responsibility is to trigger the loadJSONfile() Function by passing the path by injecting the locale from the URL into the path as parameter , this will return a call back function which will contain the json file of the required language. From this function we will pass the translation to all other sub functions.
One example from above Function :
const changingMetadataUI = (locale) => { loadJSONFile( `${baseURL}/js/jspatches/langs/${locale}.json`, function (translation) { changingMetadataSideBar(translation); changingMetadataEditBar(translation); } ); };
- Remaining all other functions will do actual work of changing the text to other language.They do that by getting the element using class/Id of a element and change the textContent of that element.This way the language will be changed based on the locale that is passed.
One example For this kind of Function :
const changingMetadataEditBar = (t) => { let firstTitle = document.querySelector( "section.edit-section .connections-container-1 span.ant-divider-inner-text" ); firstTitle.textContent = t["Current DataSource(s)"]; let sideEditBarUl = document.querySelectorAll( "aside.metadata-editor-sider ul.ant-menu li" ); let index = 0; let keys = ["Info", "Joins", "", "Views", "Security"]; sideEditBarUl.forEach((li) => { let reqText = li.querySelector("div.hi-metadata-sider-menu-title"); if (index !== 2) { reqText.textContent = t[keys[index]]; } index++; }); };
- Function -elementChecker() :
- Now coming to dataModule.js file this file contains all the logic that is required to change the report data into preferred format like date, date time, numbers etc.
- Change the Numeric format: For example when we are in USA, the cost will be shown in US Dollars, when it is in Europe it will be shows in Euros etc.
const changePropsForNumeric = (properties, fields) => { let [prefLang, prefTimeZone] = getParams(); fields.forEach((item) => { if (item.type.dataType === "numeric") { if (prefLang === "fr") { frenchFormat(item, properties); } else if (prefLang === "es") { spanishFormat(item, properties); } else { englishFormat(item, properties); } } }); };
- There is a function called changePropsNumeric(). This function takes two parameters that are properties and fields. We will call this function from the postfetch in the report or can be automated. The properties and fields are inbuilt apis, properties will contain all the properties of the columns and in the fields api there will be all the columns that are used in making the report.
- Now coming to the function, it will take the locale from the getParams() function and after that we will loop through the fields and if the datatype of the field is numeric then it will add to the properties which is an object which will contain all the properties that is applied to the report.
- Here in the function we are calling 3 different functions based on the locale. If the locale isen-US then we are calling englishFormat function, if the locale is es then we are calling spanishFormat function and if the locale is fr then we are calling the frenchFormat function.
- These three functions follow same format i.e. take the item(which is field) and properties.Now we are going to set the properties.format.activeFieldId = item.id and after that we are going to push an object to the properties.format.formatFields
Code :
const englishFormat = (item, properties) => { properties.format.activeFieldId = item.id; properties.format.formatDatatype = "numeric"; properties.format.formatFields.push({ id: item.id, values: { apply: ["pane", "axis", "tooltip", "label", "actions"], decimalPlace: 2, displayUnits: "None", field: item.id, formatDatatype: "numeric", isApplyClicked: true, numberCustom: "", percentage: "", prefix: "$", suffix: "", showAllFormatFields: false, thousandSperator: true, }, }); };
Here we can define all the properties we want in that field. All the keys in the values object is same as we see in the frontEnd in the report properties. For ‘apply’ it contains the array that will store all the places that we need to show the updated format.
- Apply : contains the array that will store all the places that we need to show the updated format.
- DecimalPlace : In how many decimal places we need to show the number
- DisplayUnits : whether we need to show in thousands , millions etc.
- Prefix : what you need to add in the front of the number(value)
- Suffix : what you need to add at the end of the number(value)
- ThousandSperator: whether you want apply thousandSeperator or not . It takes true or false.
We will add this line in the postfetch in the operations in the report where we want to make the changes
Code: changePropsForNumeric(properties , fields)
- To change the time format we have other function called changePropsForDateTimeZone() this function will take formData as a parameter which we will pass while calling this function from the operations post fetch. Here we are taking the timeZone from the getParams(),then we are writing a simple if statement . Like if timeZone is ‘EST’ we are setting the parameters.to to “’EST’” ;
CODE :const changePropsForDateTimeZone = (formData) => { let [prefLang, prefTimeZone] = getParams(); if (prefTimeZone === "EST") { formData.columns[0].databaseFunction.parameters.to = "'EST'"; } else if (prefTimeZone === "NZ") { formData.columns[0].databaseFunction.parameters.to = "'NZ'"; } else if (prefTimeZone === "MET") { formData.columns[0].databaseFunction.parameters.to = "'MET'"; } };
- To change the Date Format we have a function called changePropsForDate() this will take formdata as a parameter. That we will pass as argument while calling this function from the postfetch in the properties of a report. This function is also same as above function we will write a if function like if the locale is ‘en-US’ set the parameters.format to ‘%m/%d/%Y’
Code :const changePropsForDate = (formData) => { let [prefLang, prefTimeZone] = getParams(); if (prefLang === "en-US") { formData.columns[0].databaseFunction.parameters.format = '"%m/%d/%Y"'; } else if (prefLang === "fr") { formData.columns[0].databaseFunction.parameters.format = '"%d/%m/%Y"'; } else if (prefLang === "es") { formData.columns[0].databaseFunction.parameters.format = '"%Y/%m/%d"'; } };
- Change the Numeric format: For example when we are in USA, the cost will be shown in US Dollars, when it is in Europe it will be shows in Euros etc.