RE-POST: Metadata Plugin - Grouping Provider Example (Multiple Translations for Different User Groups)
Summary: Micael Santana, Micael Santana, introduces an improved method for implementing a grouping provider in the Sisense Metadata Plugin, focusing on user groups using different system languages. The article describes a step-by-step process to set up translations in Portuguese and Spanish, explaining the creation of user groups, retrieving group IDs, configuring the plugin with global and datasource-specific translations, and managing group-language routing. Key changes from previous versions include enhanced code readability, improved error handling, and added debug logs. The post emphasizes the solution as a custom workaround and advises thorough testing due to potential compatibility limits with different Sisense versions.
This article shows a good way to implement a grouping provider in the Metadata Plugin. If you have already seen the previous article, this is a newer version of it.
This solution covers the following use case: when multiple user groups use different system languages, the relevant metadata translation is applied to the user based on their assigned group.
In the example below, we will use two different translations: Portuguese and Spanish, to demonstrate the plugin in action.
Step 1: Create the Groups
This plugin uses groups as its foundation, so you should begin by navigating to:
Admin > User Management > Groups > + Add Group
Create the groups required for the plugin setup. For example: Portuguese and Spanish.

Step 2: Retrieve the Group IDs
Go to the REST API section under the Admin tab. Execute the GET /groups API request to obtain the IDs for each group you created.
Step 3: Configure the Plugin
Now that you have the group IDs, you can begin configuring the plugin. There are a few concepts inside the plugin that you must understand.
The configuration is split into two sections:
Configurations that apply translations globally using
globalDatasourceAliasingConfigurations that apply translations at the datasource level using
datasourceAliasing
For each language you support, you should configure both sections inside run.js.
Global Translation:
// Global metadata aliases (not tied to one datasource).
// Where this appears:
// - dashboards: Dashboard names in Sisense UI (home/list/search/header titles).
// - folders: Folder names in Sisense UI (left navigation and browse pages).
const globalDatasourceAliasing_PT = {
// Maps: <original dashboard name> -> <translated dashboard name>
dashboards: {
"General Usage": "Usos Gerais",
},
// Maps: <original folder name> -> <translated folder name>
folders: {
"General": "Geral",
},
};This configuration defines translations that apply globally across Sisense, including dashboard names, folder names, and other global metadata.
Datasource Translation:
// Datasource-level metadata aliases.
// Where this appears:
// - tables: Table names in Data panel/model metadata.
// - formulas: Custom formula names used in fields/widgets.
// - hierarchies: Hierarchy names in filter panel and field browser.
// - titles: Generic metadata titles resolved by Sisense alias engine.
// - columns: Column/field names used in Data panel and widget builders.
// - widgets: Widget titles shown on dashboards.
const datasourceAliasing_PT = {
// Maps: <table name> -> <translated table name>
tables: {
total: "Total",
},
// Maps: <formula name> -> <translated formula name>
formulas: {
Revenue: "Receita",
},
// Maps: <hierarchy name> -> <translated hierarchy name>
hierarchies: {
"Category by Brand and Age Range": "Categoria por marca e faixa etária",
},
// Maps: <title key/name> -> <translated title>
titles: {
"General Usage": "Usos Gerais",
},
// Column alias mappings.
// Preferred structure per table:
// columns: { <tableName>: { <columnName>: <translated column> } }
columns: {
purchases: {
buyer_name: "Nome do Comprador",
},
},
// Maps: <widget title> -> <translated widget title>
widgets: {
"Open Purchases": "Compras em Aberto",
},
};This section controls translations related to tables, formulas, widget titles, hierarchies, columns, and other datasource-specific metadata. The keys must exactly match the names used in your tables, widgets, formulas, and other metadata objects.
Group Config:
You can define multiple globalDatasourceAliasing and datasourceAliasing configurations inside the config variable. This array contains the mapping between Sisense groups and translation configurations.
Here, you must use the group IDs retrieved in Step 2 and assign them to the appropriate translation configuration. Multiple groups can also share the same configuration.
// Group-to-language routing configuration.
// Where this is used:
// - During provider execution, the user's group IDs are matched against `groupIds`.
// - If matched, Sisense receives `globalAlias` + `aliasName` and applies translation.
const config = [
{
// Human-readable label (for maintainers/logging).
language: "Portuguese Translation",
// Sisense group IDs that should receive this translation set.
groupIds: ["69c5488fd868b533ad8b9a43"],
globalAlias: globalDatasourceAliasing,
aliasName: datasourceAliasing_PT,
},
{
// Human-readable label (for maintainers/logging).
language: "Spanish Translation",
// Sisense group IDs that should receive this translation set.
groupIds: ["29c5488fd868b533ad239a43"],
globalAlias: globalDatasourceAliasing,
aliasName: datasourceAliasing_ES,
},
];Note: If a user belongs to more than one group with different translations configured, the default system language will be used. A notification similar to the example below will also be displayed.
Step 4: Install the Plugin
Import the Metadata Plugin with your custom configuration into the plugins directory using the File Manager. Once uploaded, enable the plugin and verify that the translations are being applied correctly.
Changes Compared to Previous Version
Minor improvements in code readability to make the plugin easier to understand.
Added fallback error handling. In the previous version, any invalid configuration could stop all translations from working. Now, only the invalid configuration is ignored.
Added debug logs to simplify troubleshooting and debugging.
The plugin files are attached below for reference:
plugin.json:
{
"name": "metadata",
"source": [
"run.6.js"
],
"style": [],
"folderName": "metadata",
"lastUpdate": "2026-05-05T19:49:50.832Z",
"isEnabled": true,
"version": "2.0.0"
}run.6.js:
/**
* Metadata Translation Plugin (Group-Based)
*
* Translates Sisense metadata (field titles, formula names) based on the
* Sisense group the logged-in user belongs to.
*
* How it works:
* - On every datasource load, Sisense fires 'beforealiascontextinit'.
* - This plugin registers two Provider functions (datasource + global) that
* inspect the current user's groups and return the correct alias mapping.
* - Sisense then replaces all matching metadata labels in widgets and the Data
* panel with the aliased values before rendering.
*
* Grouping Provider (per Sisense docs):
* - Each entry in `config` maps one or more Sisense group IDs to an alias object.
* - Retrieve group IDs via the Sisense REST API: GET /api/v1/groups
* - A user may belong to multiple groups; if more than one group has a distinct
* translation configured, the plugin falls back to default language and shows a
* dismissable notification informing the user of the conflict.
*
* Configuration:
* - Update the datasource aliasing objects with the field
* names exactly as they appear in the ElastiCube (case-insensitive).
* - Update `groupIds` in `config` to match the actual group IDs in your instance.
* - Add additional language blocks following the same pattern.
* - `globalDatasourceAliasing` handles folder/dashboard name aliasing; populate
* it if you also need to translate dashboard or folder names.
*
* References:
* - Sisense Docs: https://docs.sisense.com/main/SisenseLinux/translating-sisense-metadata-on-linux.htm
* - Community example: https://community.sisense.com/kb/add-ons_and_plug-ins/metadata-plugin---grouping-provider-example-multiple-translations-for-different-/17114
*
* Before Implementation:
* - Confirm group IDs via GET /api/v1/groups in the Sisense REST API explorer.
* - Verify field names match exactly what is stored in the ElastiCube.
* - Deploy this file alongside plugin.json into the Sisense plugins directory:
* /opt/sisense/storage/plugins/<plugin-folder>/
* or upload via the Sisense File Manager.
*/
prism.run([
"$q",
"$http",
($q, $http) => {
const DEBUG_LOGS = true;
const log = (step, payload = null, level = "info") => {
if (!DEBUG_LOGS && level !== "error") return;
const method = level === "error" ? console.error : console.log;
if (payload === null || payload === undefined) {
method("[Metadata Translation]", `[${level.toUpperCase()}]`, step);
return;
}
method("[Metadata Translation]", `[${level.toUpperCase()}]`, step, payload);
};
// Global metadata aliases (not tied to one datasource).
// Where this appears:
// - dashboards: Dashboard names in Sisense UI (home/list/search/header titles).
// - folders: Folder names in Sisense UI (left navigation and browse pages).
const globalDatasourceAliasing_PT = {
// Maps: <original dashboard name> -> <translated dashboard name>
dashboards: {
"General Usage": "Usos Gerais",
},
// Maps: <original folder name> -> <translated folder name>
folders: {
"General": "Geral",
},
};
const globalDatasourceAliasing_ES = {
dashboards: {
"General Usage": "Uso General",
},
folders: {
"General": "Geral",
},
};
// Datasource-level metadata aliases.
// Where this appears:
// - tables: Table names in Data panel/model metadata.
// - formulas: Custom formula names used in fields/widgets.
// - hierarchies: Hierarchy names in filter panel and field browser.
// - titles: Generic metadata titles resolved by Sisense alias engine.
// - columns: Column/field names used in Data panel and widget builders.
// - widgets: Widget titles shown on dashboards.
const datasourceAliasing_PT = {
// Maps: <table name> -> <translated table name>
tables: {
total: "Total",
},
// Maps: <formula name> -> <translated formula name>
formulas: {
Revenue: "Receita",
},
// Maps: <hierarchy name> -> <translated hierarchy name>
hierarchies: {
"Category by Brand and Age Range": "Categoria por marca e faixa etária",
},
// Maps: <title key/name> -> <translated title>
titles: {
"General Usage": "Usos Gerais",
},
// Column alias mappings.
// Preferred structure per table:
// columns: { <tableName>: { <columnName>: <translated column> } }
columns: {
purchases: {
buyer_name: "Nome do Comprador",
},
},
// Maps: <widget title> -> <translated widget title>
widgets: {
"Open Purchases": "Compras em Aberto",
},
};
const datasourceAliasing_ES = {
// Maps: <table name> -> <translated table name>
tables: {
total: "Total",
},
// Maps: <formula name> -> <translated formula name>
formulas: {
Revenue: "Ganancia",
},
// Maps: <hierarchy name> -> <translated hierarchy name>
hierarchies: {
"Category by Brand and Age Range": "Categoría por marca y rango de edad",
},
// Maps: <title key/name> -> <translated title>
titles: {
"General Usage": "Uso General",
},
// Column alias mappings.
// Preferred structure per table:
// columns: { <tableName>: { <columnName>: <translated column> } }
columns: {
purchases: {
buyer_name: "Nombre del Comprador",
},
},
// Maps: <widget title> -> <translated widget title>
widgets: {
"Open Purchases": "Compras Abiertas",
},
};
// Group-to-language routing configuration.
// Where this is used:
// - During provider execution, the user's group IDs are matched against `groupIds`.
// - If matched, Sisense receives `globalAlias` + `aliasName` and applies translation.
const config = [
{
// Human-readable label (for maintainers/logging).
language: "Portuguese Translation",
// Sisense group IDs that should receive this translation set.
groupIds: ["69c5488fd868b533ad8b9a43"],
globalAlias: globalDatasourceAliasing_PT,
aliasName: datasourceAliasing_PT,
},
{
// Human-readable label (for maintainers/logging).
language: "Spanish Translation",
// Sisense group IDs that should receive this translation set.
groupIds: ["29c6488fd868b533ad8b9a43"],
globalAlias: globalDatasourceAliasing_ES,
aliasName: datasourceAliasing_ES,
},
];
const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
const cleanMap = (obj) =>
Object.fromEntries(
Object.entries(obj || {}).filter(
([k, v]) => typeof k === "string" && typeof v === "string",
),
);
// Normalizes alias payloads before resolving provider callbacks.
// - Retains only supported Sisense sections.
// - Drops empty sections and invalid key/value pairs.
// - Validates nested column mappings per table.
const sanitizeAliasing = (input = {}) => {
const result = {};
const sections = [
"tables",
"formulas",
"hierarchies",
"titles",
"widgets",
"dashboards",
"folders",
];
for (const key of sections) {
if (isObj(input[key])) {
const cleaned = cleanMap(input[key]);
if (Object.keys(cleaned).length) result[key] = cleaned;
}
}
if (isObj(input.columns)) {
const cols = Object.fromEntries(
Object.entries(input.columns)
.map(([table, colsObj]) => [
table,
isObj(colsObj) ? cleanMap(colsObj) : null,
])
.filter(([, v]) => v && Object.keys(v).length),
);
if (Object.keys(cols).length) result.columns = cols;
}
return result;
}
// Returns the config block that contains the given group ID.
// If no group is configured, returns null (plugin falls back to default labels).
const getConfigByID = (groupId) => {
const matchedConfig = config.find((cfg) =>
cfg.groupIds.includes(groupId),
);
return matchedConfig || null;
};
// Attempts to read current user groups from the in-memory prism session.
// This is the fastest path and avoids an API call when session data is present.
const getGroups = async () => {
await $q.resolve();
if (prism && prism.user && prism.user.groupsName) {
log("Resolved user groups from prism session", prism.user.groupsName);
return prism.user.groupsName;
}
log("User groups unavailable in prism session; API fallback may be required");
return null;
};
// Shows a dismissable warning when a user belongs to multiple translated groups.
// In that conflict case the plugin intentionally resolves null (default language).
const dispatchNotification = () => {
log("Multiple translated groups detected; showing conflict notification");
const notificationDiv = document.createElement("div");
notificationDiv.classList.add("notification_metadata");
notificationDiv.style = `
background: #444f67;
padding: 10px;
position: absolute;
z-index: 999;
border-radius: 10px;
margin: 3% auto;
color: #fafafa;
width: 30%;
left: 50%;
transform: translateX(-50%);
`;
const notificationText = document.createElement("span");
notificationText.textContent =
"You belong to multiple groups. To ensure a consistent user experience, the platform language has been set to the default English version.";
notificationDiv.appendChild(notificationText);
const dismissButton = document.createElement("button");
dismissButton.textContent = "Dismiss";
dismissButton.style = `
background: none;
color: #39a3fa;
border: none;
cursor: pointer;
`;
dismissButton.addEventListener("click", (e) => {
e.preventDefault();
notificationDiv.style.display = "none";
log("Conflict notification dismissed by user");
});
notificationDiv.appendChild(dismissButton);
document.body.appendChild(notificationDiv);
};
// Global provider: resolves dashboard/folder aliases.
// Steps:
// 1) Get user groups (session first, API fallback).
// 2) Collect distinct global alias maps configured for those groups.
// 3) If more than one map exists, notify and resolve null to avoid ambiguity.
// 4) Otherwise resolve the sanitized alias map.
const globalDatasourceProvider = async (resolve, reject) => {
try {
log("globalDatasourceProvider started");
let groups = await getGroups();
// Sometimes the groups are not available in the session, so we need to fetch them from the API.
if (!groups) {
log("Fetching user groups from /api/users/loggedin");
const response = await $http.get("/api/users/loggedin");
groups = response.data.groupsName;
log("Resolved user groups from API", groups);
}
const globalAlias = groups
.map((g) => getConfigByID(g.id))
.filter((cfg) => cfg !== null)
.map((cfg) => cfg.globalAlias);
const alias = Array.from(new Set(globalAlias));
log("Computed unique global aliases", alias.length);
if (alias.length > 1) {
dispatchNotification();
log(
"Conflicting global aliases found; resolving null to use default language",
);
return resolve(null);
}
log("Resolving sanitized global alias map", alias[0]);
return resolve(sanitizeAliasing(alias[0]));
} catch (error) {
log("globalDatasourceProvider failed", error, "error");
reject(error);
}
};
// Datasource provider: resolves metadata aliases (tables, columns, formulas, etc.).
// Mirrors the global provider conflict behavior: multiple distinct alias maps
// trigger a warning and fallback to default labels.
const datasourceProvider = async (_, resolve, reject) => {
try {
log("datasourceProvider started");
const groups = await getGroups();
log("Processing datasource aliases for groups", groups);
const aliasNames = groups
.map((g) => getConfigByID(g.id))
.filter((cfg) => cfg !== null)
.map((cfg) => cfg.aliasName);
const alias = Array.from(new Set(aliasNames));
log("Computed unique datasource aliases", alias.length);
if (alias.length > 1) {
dispatchNotification();
log(
"Conflicting datasource aliases found; resolving null to use default language",
);
return resolve(null);
}
log("Resolving sanitized datasource alias map", alias[0]);
return resolve(sanitizeAliasing(alias[0]));
} catch (error) {
log("datasourceProvider failed", error, "error");
reject(error);
}
};
// Registers providers before Sisense initializes alias context.
// `2000` is the provider timeout in milliseconds.
prism.on("beforealiascontextinit", function (ev, args) {
log("beforealiascontextinit fired; registering alias providers");
args.register(datasourceProvider, globalDatasourceProvider, 2000);
});
},
]);
References/Related Content
Disclaimer: Please note that this blog post contains one possible custom workaround solution for users with similar use cases. We cannot guarantee that the custom code solution described in this post will work in every scenario or with every Sisense software version. As such, we strongly advise users to test solutions in their environment prior to deploying them to ensure that the solutions proffered function as desired in their environment. For the avoidance of doubt, the content of this blog post is provided to you “as-is” and without warranty of any kind, express, implied, or otherwise, including without limitation any warranty of security and or fitness for a particular purpose. The workaround solution described in this post incorporates custom coding, which is outside the Sisense product development environment and is, therefore, not covered by Sisense warranty and support services.