diff --git a/AGENTS.md b/AGENTS.md
index b604154e..f8b3ee5f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -19,6 +19,8 @@ This `AGENTS.md` file provides guidelines for OpenAI Codex and other AI agents i
Agents should focus on the core application logic and ignore files or folders marked as third-party, sample, or media-related. All changes should preserve the integrity of external dependencies and translations.
+For single-word or otherwise ambiguous translatable strings, add a `// TRANSLATORS:` comment immediately before the translation call when context is needed. Explain the intended meaning, whether the word is a noun or a verb, and its UI context where helpful (for example, “Aggregate” is a verb). Keep hints brief and factual, and put each translation call on its own line so its comment applies to the correct string.
+
All new user-interface elements must reuse the app's existing templates and shared CSS classes. Before adding custom markup or styling, find the corresponding control or dialog pattern in `templates/` and apply it consistently; extend the shared pattern when no suitable template exists instead of introducing an unstyled native control or a one-off design.
For every user-facing change, add a meaningful one-liner to the corresponding section (Added, Changed, Fixed) in CHANGELOG.md. Technical fixes that do not change user-facing behavior, such as correcting a migration's Doctrine type argument, do not require a changelog entry. Add only one changelog entry per feature or fix: when work on the same change is iterated within a chat, update the existing entry as needed instead of adding another line. Do not add entries to a closed milestone which has a data. if no open milestone exists, add a new section to the changelog.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 304c2140..9b6f431c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
- OCS endpoints to list accessible reports and owned datasets, retrieve report metadata and filtered data including shared reports, and add or delete dataset rows.
### Changed
+- Clarify ambiguous report actions, chart and data terminology, and placeholders with translator comments.
- Simplify folder navigation and icons, using the URL to select content and keeping Overview groups collapsed.
### Fixed
diff --git a/js/app.js b/js/app.js
index fbb861e1..97b34daa 100644
--- a/js/app.js
+++ b/js/app.js
@@ -758,6 +758,7 @@ Object.assign(OCA.Analytics.Datasource = {
OCA.Analytics.Notification.htmlDialogUpdate(
editor,
+ // TRANSLATORS: {linkstart} and {linkend} wrap the documentation link. Preserve the text-variable syntax and any
tags in this guidance.
t('analytics', 'Keep the selected path and replace parts of the file name with text variables, for example report_%today%(Y-m-d).csv. The variable is resolved when the data load runs. See the {linkstart}Wiki{linkend}.')
.replace('{linkstart}', '')
.replace('{linkend}', '')
@@ -1139,6 +1140,7 @@ Object.assign(OCA.Analytics.Datasource = {
OCA.Analytics.Notification.htmlDialogUpdate(
content,
+ // TRANSLATORS: {linkstart} and {linkend} wrap the documentation link. Preserve the text-variable syntax and any
tags in this guidance.
t('analytics', 'Select the required columns.
Rearrange the sequence with drag & drop.
Remove all selections to reset.
Add custom columns including text variables for dates. See the {linkstart}Wiki{linkend}.')
.replace('{linkstart}', '')
.replace('{linkend}', '')
@@ -1267,6 +1269,7 @@ Object.assign(OCA.Analytics.Share = {
let navigationItem = evt.target.closest('div');
OCA.Analytics.Notification.htmlDialogInitiate(
+ // TRANSLATORS: Verb: give other people access to this report, dataset, or panorama.
t('analytics', 'Share') + ' ' + navigationItem.dataset.name,
OCA.Analytics.Notification.dialogClose
);
@@ -1336,6 +1339,7 @@ Object.assign(OCA.Analytics.Share = {
linkRow.getElementById('row').dataset.id = id;
if (add) linkRow.getElementById('shareLinkTitle').innerText = t('analytics', 'Add share link');
+ // TRANSLATORS: Noun: a public link that gives access to the shared item.
else linkRow.getElementById('shareLinkTitle').innerText = t('analytics', 'Share link');
if (add) {
@@ -1801,6 +1805,7 @@ Object.assign(OCA.Analytics.Threshold = {
if (data.option === 'new') { // adjust the text for the "new records" option
data.value = '';
+ // TRANSLATORS: Noun: a newly added data row; also used as a notification subject.
data.dimension1 = t('analytics', 'new record');
data.option = '';
}
diff --git a/js/dataset.js b/js/dataset.js
index f8e6a196..07fc28e8 100644
--- a/js/dataset.js
+++ b/js/dataset.js
@@ -735,7 +735,9 @@ Object.assign(OCA.Analytics.Dataset.Dataset = {
const role = document.createElement('select');
role.className = 'sidebarInput flexibleColumnRole';
+ // TRANSLATORS: Noun: a data column used to identify or group records, such as region or date.
role.add(new Option(t('analytics', 'Dimension'), 'dimension'));
+ // TRANSLATORS: Noun: a numeric data column that can be aggregated, such as revenue.
role.add(new Option(t('analytics', 'Measure'), 'measure'));
role.value = column.role || 'dimension';
@@ -744,6 +746,7 @@ Object.assign(OCA.Analytics.Dataset.Dataset = {
nullable.type = 'checkbox';
nullable.className = 'flexibleColumnNullable';
nullable.checked = column.nullable === true;
+ // TRANSLATORS: Column setting: allow missing values in this numeric field.
nullableLabel.append(nullable, document.createTextNode(' ' + t('analytics', 'Nullable')));
const aggregation = document.createElement('select');
diff --git a/js/filter.js b/js/filter.js
index aa68a639..cac86721 100644
--- a/js/filter.js
+++ b/js/filter.js
@@ -296,6 +296,7 @@ OCA.Analytics.Filter = {
|| /\b(?:column|col)\d+\b/i.test(expression)) return null;
return {
id: 'calc:legacy_' + index,
+ // TRANSLATORS: Noun: a numeric report field computed from a formula.
name: calc.title || t('analytics', 'Calculated measure'),
expression,
phase: 'after',
@@ -339,6 +340,7 @@ OCA.Analytics.Filter = {
const include = document.createElement('input');
include.type = 'checkbox';
include.className = 'transformInclude';
+ // TRANSLATORS: Checkbox label; {column} is a data column name, e.g. Revenue.
include.setAttribute('aria-label', t('analytics', 'Include {column}', {column: column.name}));
include.checked = column.role === 'dimension'
? !Object.prototype.hasOwnProperty.call(options.drilldown || {}, column.ref)
@@ -348,14 +350,19 @@ OCA.Analytics.Filter = {
if (column.role === 'measure') {
const aggregation = OCA.Analytics.Filter.transformationSelect([
{value: 'none', label: t('analytics', 'None (original values)')},
+ // TRANSLATORS: Noun: aggregation function that adds the values in a group.
{value: 'sum', label: t('analytics', 'Sum')},
+ // TRANSLATORS: Noun: arithmetic mean of the values in a group.
{value: 'avg', label: t('analytics', 'Average')},
{value: 'min', label: t('analytics', 'Minimum')},
{value: 'max', label: t('analytics', 'Maximum')},
+ // TRANSLATORS: Noun: number of values in a group.
{value: 'count', label: t('analytics', 'Count')},
+ // TRANSLATORS: Aggregation function: number of different values in a group.
{value: 'count_distinct', label: t('analytics', 'Count distinct')},
], options.aggregate === false ? 'none'
: (definition.aggregations || {})[column.ref] || column.defaultAggregation || column.aggregation || 'sum',
+ // TRANSLATORS: Verb: choose an aggregation function; {column} is a numeric column name, e.g. Revenue.
t('analytics', 'Aggregate {column}', {column: column.name}));
aggregation.classList.add('transformAggregation');
row.appendChild(aggregation);
@@ -368,6 +375,7 @@ OCA.Analytics.Filter = {
{value: 'ASC', label: t('analytics', 'Ascending')},
{value: 'DESC', label: t('analytics', 'Descending')},
], sortIndex >= 0 ? String(sort[sortIndex].direction).toUpperCase() : '',
+ // TRANSLATORS: Verb: choose a sort direction; {column} is a column name, e.g. Revenue.
t('analytics', 'Sort {column}', {column: column.name}));
direction.classList.add('transformSort');
row.appendChild(direction);
@@ -376,6 +384,7 @@ OCA.Analytics.Filter = {
priority.min = '1';
priority.step = '1';
priority.className = 'optionsInput transformPriority';
+ // TRANSLATORS: {column} is a column name, e.g. Revenue. Lower priority numbers are sorted first.
priority.setAttribute('aria-label', t('analytics', 'Sort priority for {column}', {column: column.name}));
priority.value = sortIndex >= 0 ? String(sortIndex + 1) : '';
priority.disabled = direction.value === '';
@@ -393,7 +402,9 @@ OCA.Analytics.Filter = {
const name = document.createElement('input');
name.className = 'optionsInput transformCalculationName';
name.type = 'text';
+ // TRANSLATORS: Name of a numeric field computed from a formula.
name.placeholder = t('analytics', 'Measure name');
+ // TRANSLATORS: Name of a numeric field computed from a formula.
name.setAttribute('aria-label', t('analytics', 'Measure name'));
name.value = calculation.name || '';
heading.appendChild(name);
@@ -473,18 +484,25 @@ OCA.Analytics.Filter = {
controls.appendChild(phase);
const aggregation = OCA.Analytics.Filter.transformationSelect([
{value: 'none', label: t('analytics', 'None (original values)')},
+ // TRANSLATORS: Noun: aggregation function that adds the values in a group.
{value: 'sum', label: t('analytics', 'Sum')},
+ // TRANSLATORS: Noun: arithmetic mean of the values in a group.
{value: 'avg', label: t('analytics', 'Average')},
{value: 'min', label: t('analytics', 'Minimum')},
{value: 'max', label: t('analytics', 'Maximum')},
+ // TRANSLATORS: Noun: number of values in a group.
{value: 'count', label: t('analytics', 'Count')},
+ // TRANSLATORS: Aggregation function: number of different values in a group.
{value: 'count_distinct', label: t('analytics', 'Count distinct')},
- ], calculation.aggregation || 'sum', t('analytics', 'Aggregate calculated measure'));
+ ], calculation.aggregation || 'sum',
+ // TRANSLATORS: Verb: choose how calculated numeric values are combined within a group.
+ t('analytics', 'Aggregate calculated measure'));
aggregation.classList.add('transformAggregation');
aggregation.hidden = phase.value !== 'before';
phase.onchange = () => { aggregation.hidden = phase.value !== 'before'; };
controls.appendChild(aggregation);
const include = document.createElement('label');
+ // TRANSLATORS: Verb: include this column or calculated measure in the report.
include.textContent = t('analytics', 'Include');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
@@ -506,6 +524,7 @@ OCA.Analytics.Filter = {
priority.min = '1';
priority.className = 'optionsInput transformPriority';
priority.setAttribute('aria-label', t('analytics', 'Sort priority'));
+ // TRANSLATORS: Noun: sorting precedence; lower numbers are applied first.
priority.placeholder = t('analytics', 'Priority');
priority.value = sortIndex >= 0 ? String(sortIndex + 1) : '';
priority.disabled = direction.value === '';
@@ -747,6 +766,7 @@ OCA.Analytics.Filter = {
OCA.Analytics.Report.hideReportMenu();
OCA.Analytics.Notification.htmlDialogInitiate(
+ // TRANSLATORS: Noun: report filter settings.
t('analytics', 'Filter'),
apply,
context?.dialogOptions || {}
@@ -860,6 +880,7 @@ OCA.Analytics.Filter = {
OCA.Analytics.Notification.htmlDialogUpdate(
container,
+ // TRANSLATORS: {linkstart} and {linkend} wrap the documentation link. Preserve the text-variable syntax and any
tags in this guidance.
t('analytics', 'Dynamic text variables can be used to select dates.
The selection is written between two % (e.g. %last2months%).
Use * and ? as wildcards for contains and not contains filters.
Information on available filters and alternative date formats is available in the {linkstart}Wiki{linkend}.')
.replace('{linkstart}', '')
.replace('{linkend}', '')
@@ -900,6 +921,7 @@ OCA.Analytics.Filter = {
OCA.Analytics.Report.hideReportMenu();
OCA.Analytics.Notification.htmlDialogInitiate(
+ // TRANSLATORS: Ranking option: keep the N highest-ranked items; N is the number chosen by the user.
t('analytics', 'Top N'),
OCA.Analytics.Filter.processTopNDialog
);
@@ -931,7 +953,9 @@ OCA.Analytics.Filter = {
typeSelect.innerHTML = '';
const typeOptions = [
{value: 'none', text: t('analytics', 'none')},
+ // TRANSLATORS: Ranking option: keep the N highest-ranked items; N is the number chosen by the user.
{value: 'top', text: t('analytics', 'Top N')},
+ // TRANSLATORS: Ranking option: keep the N lowest-ranked items; N is the number chosen by the user.
{value: 'flop', text: t('analytics', 'Flop N')}
];
typeOptions.forEach(({value, text}) => {
@@ -1047,7 +1071,12 @@ OCA.Analytics.Filter = {
});
const modeSelect = container.getElementById('timeGroupingMode');
- [{value: 'summation', text: t('analytics', 'summation')}, {value: 'average', text: t('analytics', 'average')}].forEach(({value, text}) => {
+ [
+ // TRANSLATORS: Noun: sum of the values within each time period.
+ {value: 'summation', text: t('analytics', 'summation')},
+ // TRANSLATORS: Noun: arithmetic mean of the values within each time period.
+ {value: 'average', text: t('analytics', 'average')}
+ ].forEach(({value, text}) => {
modeSelect.options.add(new Option(text, value));
});
@@ -1573,6 +1602,7 @@ OCA.Analytics.Filter = {
{value: 'multiply', label: t('analytics', 'Multiply')},
{value: 'divide', label: t('analytics', 'Divide')},
{value: 'percentage', label: t('analytics', 'Percentage')},
+ // TRANSLATORS: Calculation method: a custom mathematical formula entered by the user.
{value: 'formula', label: t('analytics', 'Own Formular')},
];
},
@@ -1745,8 +1775,10 @@ OCA.Analytics.Filter = {
const meta = document.createElement('span');
meta.className = 'tableOptionsCalculatedColumnCardMeta';
if (item.operation === 'formula') {
+ // TRANSLATORS: Calculation method: a custom mathematical formula entered by the user.
meta.textContent = t('analytics', 'Own Formular');
} else {
+ // TRANSLATORS: Calculation summary; {operation} is a localized method such as Add, and {count} is the number of input columns, e.g. 2.
meta.textContent = t('analytics', '{operation} using {count} source columns', {
operation: OCA.Analytics.Filter.getCalculatedColumnOperationLabel(item.operation),
count: item.columns.length
@@ -1770,6 +1802,7 @@ OCA.Analytics.Filter = {
const headers = OCA.Analytics.Filter.calculatedColumnsEditorState.sourceHeaders.length > 0
? OCA.Analytics.Filter.calculatedColumnsEditorState.sourceHeaders
: OCA.Analytics.currentReportData.header || [];
+ // TRANSLATORS: Fallback column label; {column} is the source column index, e.g. 2.
return headers[index] || t('analytics', 'Column {column}', {column: index});
},
@@ -2007,21 +2040,35 @@ OCA.Analytics.Filter = {
getCalculatedColumnSourceRoleLabel: function (operation, index) {
if (operation === 'percentage') {
- return index === 0 ? t('analytics', 'Numerator') : t('analytics', 'Denominator');
+ return index === 0
+ // TRANSLATORS: The dividend in a percentage calculation (numerator / denominator * 100).
+ ? t('analytics', 'Numerator')
+ // TRANSLATORS: The divisor in a percentage calculation (numerator / denominator * 100).
+ : t('analytics', 'Denominator');
}
if (operation === 'substract') {
- return index === 0 ? t('analytics', 'Base value') : t('analytics', 'Subtract value');
+ return index === 0
+ // TRANSLATORS: Noun: the starting value from which other selected values are subtracted.
+ ? t('analytics', 'Base value')
+ // TRANSLATORS: Noun: a value to subtract from the starting value.
+ : t('analytics', 'Subtract value');
}
if (operation === 'divide') {
- return index === 0 ? t('analytics', 'Base value') : t('analytics', 'Divide by');
+ return index === 0
+ // TRANSLATORS: Noun: the starting value divided by the other selected values.
+ ? t('analytics', 'Base value')
+ // TRANSLATORS: Label for the divisor in a calculation.
+ : t('analytics', 'Divide by');
}
if (operation === 'multiply') {
+ // TRANSLATORS: Multiplication operand; {position} is its one-based position, e.g. 2.
return t('analytics', 'Factor {position}', {position: index + 1});
}
+ // TRANSLATORS: Calculation input column; {position} is its one-based position, e.g. 2.
return t('analytics', 'Source {position}', {position: index + 1});
},
@@ -2456,6 +2503,7 @@ OCA.Analytics.Filter = {
up.type = 'button';
up.textContent = '↑';
up.disabled = index === 0;
+ // TRANSLATORS: Reorder action; {column} is a chart field name, e.g. Revenue.
up.setAttribute('aria-label', t('analytics', 'Move {column} up', {column: labelFor(id)}));
up.addEventListener('click', () => {
[mapping[role][index - 1], mapping[role][index]] = [id, mapping[role][index - 1]];
@@ -2468,6 +2516,7 @@ OCA.Analytics.Filter = {
remove.type = 'button';
remove.textContent = '×';
remove.className = 'chartColumnRemove';
+ // TRANSLATORS: Remove a field from the chart mapping; {column} is its name, e.g. Revenue.
remove.setAttribute('aria-label', t('analytics', 'Remove {column}', {column: labelFor(id)}));
remove.addEventListener('click', () => {
mapping[role] = mapping[role].filter(column => column !== id);
@@ -2497,11 +2546,15 @@ OCA.Analytics.Filter = {
// Translate the complete sentence; insert field names as text nodes, never HTML.
const markers = {values: '{values}', category: '{category}', series: '{series}'};
const sentence = dataModel === 'accountModel'
+ // TRANSLATORS: Chart summary; {values} lists numeric field names, e.g. Revenue, and {category} is the field naming each row, e.g. Region.
? t('analytics', 'Show {values}, with one series per {category} row.', markers)
: dataModel === 'timeSeriesModel'
+ // TRANSLATORS: Chart summary; {values} lists numeric field names, e.g. Revenue, and {category} is the date field name, e.g. Date.
? t('analytics', 'Show {values} over time using {category}.', markers)
: mapping.series.length
+ // TRANSLATORS: Chart summary; {values} lists numeric fields, {category} is the category field, and {series} lists grouping fields, e.g. Revenue, Date, and Region.
? t('analytics', 'Show {values} by {category}, broken down by {series}.', markers)
+ // TRANSLATORS: Chart summary; {values} lists numeric field names, e.g. Revenue, and {category} is the category field name, e.g. Region.
: t('analytics', 'Show {values} by {category}.', markers);
summary.replaceChildren();
if (!mapping.measures.length) return;
@@ -2540,6 +2593,7 @@ OCA.Analytics.Filter = {
const previewData = {...basePreviewData, options: {...basePreviewData.options,
chart: document.querySelector('input[name="analyticsChartType"]:checked')?.value || basePreviewData.options.chart}};
const result = OCA.Analytics.Visualization.chartMappingPreview(previewData, dataModel, automatic ? null : mapping, seriesOptions);
+ // TRANSLATORS: {shown} is the number of preview rows and {total} is the number of report rows, e.g. 10 and 200.
status.textContent = t('analytics', 'Preview: {shown} of {total} report rows. Current filters apply.', {
shown: result.shownRows.toLocaleString(), total: result.totalRows.toLocaleString(),
});
@@ -2653,6 +2707,7 @@ OCA.Analytics.Filter = {
if (!rows.length) return;
const controls = document.createElement('div');
controls.className = 'chartMeasureStyle';
+ // TRANSLATORS: Chart type selector; {column} is a numeric field name, e.g. Revenue.
controls.append(sharedSelect(rows, 'optionsChartType', t('analytics', 'Show {column} as', {column: labelFor(id)}), [
['bar', t('analytics', 'Bars')], ['line', t('analytics', 'Line')],
['doughnut', t('analytics', 'Doughnut')], ['funnel', t('analytics', 'Funnel')],
@@ -2660,6 +2715,7 @@ OCA.Analytics.Filter = {
const more = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = t('analytics', 'More options');
+ // TRANSLATORS: Chart axis selector; {column} is a numeric field name, e.g. Revenue.
more.append(summary, sharedSelect(rows, 'optionsYAxis', t('analytics', 'Axis for {column}', {column: labelFor(id)}), [
['primary', t('analytics', 'Left axis')], ['secondary', t('analytics', 'Right axis')],
]));
@@ -2690,6 +2746,7 @@ OCA.Analytics.Filter = {
const color = document.createElement('input');
color.type = 'color';
color.className = 'chartOptionsColorInput';
+ // TRANSLATORS: Chart color selector; {group} is a data series group label, e.g. Europe.
color.setAttribute('aria-label', t('analytics', 'Color for {group}', {group: label}));
color.value = [...values][0] || '#000000';
if (values.size > 1) {
@@ -2798,7 +2855,9 @@ OCA.Analytics.Filter = {
yAxisSelect.id = 'optionsYAxis' + i;
yAxisSelect.name = 'optionsYAxis';
yAxisSelect.className = 'optionsInput';
+ // TRANSLATORS: Chart axis: the primary vertical axis.
yAxisSelect.appendChild(new Option(t('analytics', 'Primary'), 'primary'));
+ // TRANSLATORS: Chart axis: the secondary vertical axis.
yAxisSelect.appendChild(new Option(t('analytics', 'Secondary'), 'secondary'));
yAxisSelect.value = OCA.Analytics.Filter.checkOption(dataOptions, i, 'yAxisID', 'primary', 'primary') ? 'primary' :
(OCA.Analytics.Filter.checkOption(dataOptions, i, 'yAxisID', 'secondary', 'primary') ? 'secondary' : 'primary');
@@ -2811,9 +2870,13 @@ OCA.Analytics.Filter = {
typeSelect.id = 'optionsChartType' + i;
typeSelect.name = 'optionsChartType';
typeSelect.className = 'optionsInput';
+ // TRANSLATORS: Chart type: a line chart.
typeSelect.appendChild(new Option(t('analytics', 'Line'), 'line'));
+ // TRANSLATORS: Chart type: a bar chart.
typeSelect.appendChild(new Option(t('analytics', 'Bar'), 'bar'));
+ // TRANSLATORS: Chart type: a ring-shaped pie chart.
typeSelect.appendChild(new Option(t('analytics', 'Doughnut'), 'doughnut'));
+ // TRANSLATORS: Chart type: values shown as progressively sized funnel sections.
typeSelect.appendChild(new Option(t('analytics', 'Funnel'), 'funnel'));
typeSelect.value = OCA.Analytics.Filter.checkOption(dataOptions, i, 'type', 'line', defaultChartType) ? 'line' :
(OCA.Analytics.Filter.checkOption(dataOptions, i, 'type', 'bar', defaultChartType) ? 'bar' :
diff --git a/js/jsonPicker.js b/js/jsonPicker.js
index fab340ce..928814fa 100644
--- a/js/jsonPicker.js
+++ b/js/jsonPicker.js
@@ -77,6 +77,7 @@ OCA.Analytics.JsonPicker = {
});
let result;
try { result = await response.json(); }
+ // TRANSLATORS: {status} is an HTTP response status code, e.g. 404.
catch (error) { throw new Error(t('analytics', 'The datasource request failed (HTTP {status}).', {status: response.status})); }
if (!response.ok || (result.error && String(result.error) !== '0')) {
throw new Error(result.error || t('analytics', 'Could not load JSON data. Check the datasource settings.'));
@@ -103,6 +104,7 @@ OCA.Analytics.JsonPicker = {
for (let i = 0; i < width; i++) {
const cell = document.createElement('th');
cell.scope = 'col';
+ // TRANSLATORS: Fallback column label; {number} is a one-based column number, e.g. 1.
cell.textContent = result.header?.[i] ?? t('analytics', 'Column {number}', {number: i + 1});
heading.appendChild(cell);
}
@@ -184,6 +186,7 @@ OCA.Analytics.JsonPicker = {
if (!details.open || details.dataset.loaded) return;
details.dataset.loaded = 'true';
if (!fields && isArray && value.some(item => picker.isContainer(item))) {
+ // TRANSLATORS: JSON selection action; {name} is an array or group name from the source data, e.g. orders.
details.appendChild(action(t('analytics', 'Use all {name} and choose the relevant fields', {name: key}), () => showFields(value, path), picker.selectable(path)));
} else if (!fields && Object.values(value).every(item => !picker.isContainer(item))) {
details.appendChild(action(t('analytics', 'Use entries as rows'), () => {
diff --git a/js/panorama.js b/js/panorama.js
index e4a5a7e1..6dc3a322 100644
--- a/js/panorama.js
+++ b/js/panorama.js
@@ -1305,10 +1305,12 @@ Object.assign(OCA.Analytics.Panorama = {
for (const [index, page] of pages.entries()) {
//page.style.backgroundColor = backgroundColor;
+ // TRANSLATORS: PDF export progress; {pageCount} is the one-based page number, e.g. 2.
OCA.Analytics.Notification.htmlDialogUpdateAdd(t('analytics', 'capturing page {pageCount}', {pageCount: index}));
const canvas = await html2canvas(page, {scale: 2});
const imgData = canvas.toDataURL('image/png');
+ // TRANSLATORS: PDF export progress; {pageCount} is the one-based page number, e.g. 2.
OCA.Analytics.Notification.htmlDialogUpdateAdd(t('analytics', 'captured page {pageCount}', {pageCount: index}));
if (index > 0) {
pdf.addPage();
@@ -1370,6 +1372,7 @@ Object.assign(OCA.Analytics.Panorama = {
pdf.text('Created with', pdf.internal.pageSize.getWidth() - 65, pdf.internal.pageSize.getHeight() - 22);
pdf.text('Analytics', pdf.internal.pageSize.getWidth() - 65, pdf.internal.pageSize.getHeight() - 12);
+ // TRANSLATORS: PDF export progress; {pageCount} is the one-based page number, e.g. 2.
OCA.Analytics.Notification.htmlDialogUpdateAdd(t('analytics', 'page {pageCount} added to pdf', {pageCount: index}));
}
diff --git a/js/panoramaFilters.js b/js/panoramaFilters.js
index 1d829ace..477eab06 100644
--- a/js/panoramaFilters.js
+++ b/js/panoramaFilters.js
@@ -351,6 +351,7 @@ OCA.Analytics.PanoramaFilters = {
const mappingHeader = document.createElement('div');
mappingHeader.className = 'panoramaFilterMappingHeader';
mappingHeader.append(Object.assign(document.createElement('span'), {textContent: t('analytics', 'Report')}),
+ // TRANSLATORS: Noun: a data column used to identify or group records, such as region or date.
Object.assign(document.createElement('span'), {textContent: t('analytics', 'Dimension')}), document.createElement('span'));
mappingList.append(mappingHeader);
const candidates = new Map();
diff --git a/js/report.js b/js/report.js
index 1b892d8c..d9948fb7 100644
--- a/js/report.js
+++ b/js/report.js
@@ -711,6 +711,7 @@ Object.assign(OCA.Analytics.Report.Functions = {
let datasetType = 'time';
for (let y = 0; y < numberDatasets; y++) {
let dataset = OCA.Analytics.chartObject.data.datasets[y];
+ // TRANSLATORS: Noun: a linear trend line calculated from a chart series.
let newLabel = dataset.label + " " + t('analytics', 'Trend');
// Add the function only for visible source series.
@@ -718,6 +719,7 @@ Object.assign(OCA.Analytics.Report.Functions = {
// dont add trend twice
if (OCA.Analytics.chartObject.data.datasets.find(o => o.label === newLabel) !== undefined) continue;
// dont add trend for a trend
+ // TRANSLATORS: Noun: a linear trend line calculated from a chart series.
if (dataset.label.substr(dataset.label.length - 5) === t('analytics', 'Trend')) continue;
let yValues = [];
diff --git a/js/sidebar.js b/js/sidebar.js
index eb3ec0ac..5138bf99 100644
--- a/js/sidebar.js
+++ b/js/sidebar.js
@@ -269,6 +269,7 @@ OCA.Analytics.Sidebar.Report = {
{method: 'GET', headers: OCA.Analytics.headers()},
t('analytics', 'Failed to load dataset')
);
+ // TRANSLATORS: {count} is the number of dataset columns, e.g. 5.
summaryElement.textContent = t('analytics', 'Flexible dataset · All {count} columns are included in the report.', {
count: OCA.Analytics.Flexible.columns(descriptor).length,
});
@@ -288,6 +289,7 @@ OCA.Analytics.Sidebar.Report = {
if (flexible) {
dimensionHeader.style.display = 'none';
dimensionSection.style.display = 'none';
+ // TRANSLATORS: {count} is the number of dataset columns, e.g. 5.
summary.textContent = t('analytics', 'Flexible dataset · All {count} columns are included in the report.', {
count: OCA.Analytics.Flexible.columns(data).length,
});
diff --git a/js/tableOptions.js b/js/tableOptions.js
index 69478a8b..ff7bae7c 100644
--- a/js/tableOptions.js
+++ b/js/tableOptions.js
@@ -229,6 +229,7 @@ OCA.Analytics.TableOptions = {
});
this.updatePreviewSelection();
const info = state.preview.page.info();
+ // TRANSLATORS: {shown} is the number of preview rows and {total} is the number of table rows, e.g. 10 and 200.
field('tableOptionsPreviewStatus').textContent = t('analytics', 'Preview: {shown} of {total} table rows. Current report filters apply.', {
shown: info.end.toLocaleString(), total: info.recordsTotal.toLocaleString(),
});
diff --git a/js/visualization.js b/js/visualization.js
index 4fcea5e8..58514713 100644
--- a/js/visualization.js
+++ b/js/visualization.js
@@ -862,9 +862,11 @@ OCA.Analytics.Visualization = {
let defaultColReorder = true;
let data, columns;
let language = {
- // TRANSLATORS Noun
+ // TRANSLATORS: Noun: label for the table search field.
search: t('analytics', 'Search'),
+ // TRANSLATORS: _MENU_ is replaced with a page-size selector, e.g. 10. Preserve this token.
lengthMenu: t('analytics', 'Show _MENU_ entries'),
+ // TRANSLATORS: _START_, _END_, and _TOTAL_ are row numbers, e.g. 1, 10, and 200. Preserve these tokens.
info: t('analytics', 'Showing _START_ to _END_ of _TOTAL_ entries'),
infoEmpty: t('analytics', 'Showing 0 to 0 of 0 entries'),
paginate: {
@@ -1999,7 +2001,9 @@ OCA.Analytics.Visualization = {
return {
label: mode === 'aggregate'
+ // TRANSLATORS: Noun in a chart legend: running total series; {label} is the original series name, e.g. Revenue.
? t('analytics', '{label} Aggregation', {label: dataset.label})
+ // TRANSLATORS: Noun in a chart legend: differences between consecutive values; {label} is the original series name, e.g. Revenue.
: t('analytics', '{label} Disaggregation', {label: dataset.label}),
backgroundColor: dataset.backgroundColor,
borderColor: dataset.borderColor,
@@ -2644,6 +2648,7 @@ OCA.Analytics.Visualization = {
entries.sort((a, b) => isTop ? b[1] - a[1] : a[1] - b[1]);
const keepKeysSet = new Set(entries.slice(0, n).map(e => e[0]));
+ // TRANSLATORS: Label for the combined group of items outside the chosen ranking.
const othersLabel = t('analytics', 'others');
const aggregated = {};
diff --git a/lib/Activity/Provider.php b/lib/Activity/Provider.php
index 472c5488..9a320723 100644
--- a/lib/Activity/Provider.php
+++ b/lib/Activity/Provider.php
@@ -59,65 +59,83 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
switch ($event->getSubject()) {
case ActivityManager::SUBJECT_REPORT_ADD:
+ // TRANSLATORS: {report} is the report name, e.g. Sales.
$parsedSubject = $this->l10n->t('You created a new report: {report}');
break;
case ActivityManager::SUBJECT_REPORT_DELETE:
+ // TRANSLATORS: {report} is the report name, e.g. Sales.
$parsedSubject = $this->l10n->t('You deleted the report {report}');
break;
case ActivityManager::SUBJECT_REPORT_SHARE:
if ($ownActivity) {
+ // TRANSLATORS: {report} is the report name, e.g. Sales.
$parsedSubject = $this->l10n->t('You shared the report {report}');
} else {
+ // TRANSLATORS: {report} is the report name, e.g. Sales. The acting user name is prepended to this sentence fragment.
$parsedSubject = $event->getSubjectParameters()['author'] . ' ' . $this->l10n->t('shared the report {report} with you');
}
break;
case ActivityManager::SUBJECT_DATASET_ADD:
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales.
$parsedSubject = $this->l10n->t('You created a new dataset: {report}');
break;
case ActivityManager::SUBJECT_DATASET_DELETE:
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales.
$parsedSubject = $this->l10n->t('You deleted the dataset {report}');
break;
case ActivityManager::SUBJECT_DATASET_SHARE:
if ($ownActivity) {
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales.
$parsedSubject = $this->l10n->t('You shared the dataset {report}');
} else {
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales. The acting user name is prepended to this sentence fragment.
$parsedSubject = $event->getSubjectParameters()['author'] . ' ' . $this->l10n->t('shared the dataset {report} with you');
}
break;
case ActivityManager::SUBJECT_PANORAMA_ADD:
+ // TRANSLATORS: {report} is the panorama name, e.g. Sales.
$parsedSubject = $this->l10n->t('You created a new panorama: {report}');
break;
case ActivityManager::SUBJECT_PANORAMA_DELETE:
+ // TRANSLATORS: {report} is the panorama name, e.g. Sales.
$parsedSubject = $this->l10n->t('You deleted the panorama {report}');
break;
case ActivityManager::SUBJECT_PANORAMA_SHARE:
if ($ownActivity) {
+ // TRANSLATORS: {report} is the panorama name, e.g. Sales.
$parsedSubject = $this->l10n->t('You shared the panorama {report}');
} else {
+ // TRANSLATORS: {report} is the panorama name, e.g. Sales. The acting user name is prepended to this sentence fragment.
$parsedSubject = $event->getSubjectParameters()['author'] . ' ' . $this->l10n->t('shared the panorama {report} with you');
}
break;
case ActivityManager::SUBJECT_DATA_ADD:
if ($ownActivity) {
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales.
$parsedSubject = $this->l10n->t('You have added new data to dataset {report}');
} else {
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales. The acting user name is prepended to this sentence fragment.
$parsedSubject = $event->getSubjectParameters()['author'] . ' ' . $this->l10n->t('has added new data to dataset {report}');
}
break;
case ActivityManager::SUBJECT_DATA_ADD_IMPORT:
if ($ownActivity) {
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales.
$parsedSubject = $this->l10n->t('You have imported data in dataset {report}');
} else {
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales. The acting user name is prepended to this sentence fragment.
$parsedSubject = $event->getSubjectParameters()['author'] . ' ' . $this->l10n->t('has imported data in dataset {report}');
}
break;
case ActivityManager::SUBJECT_DATA_ADD_API:
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales.
$parsedSubject = $this->l10n->t('New data has been added to dataset {report} via API');
break;
case ActivityManager::SUBJECT_DATA_ADD_DATALOAD:
+ // TRANSLATORS: {report} is the dataset name, e.g. Sales.
$parsedSubject = $this->l10n->t('New data has been added to dataset {report} via data load');
break;
default:
diff --git a/lib/Controller/DatasourceController.php b/lib/Controller/DatasourceController.php
index c65bdac9..cd29d7cc 100644
--- a/lib/Controller/DatasourceController.php
+++ b/lib/Controller/DatasourceController.php
@@ -298,6 +298,7 @@ public function read(int $datasourceId, $datasetMetadata, bool $allowCacheValida
} catch (NotFoundException $e) {
$result = [
+ // TRANSLATORS: %s is the missing file path, e.g. reports/sales.csv.
'error' => $this->l10n->t('File not found: %s', [$option['link'] ?? '']),
'data' => [],
'status' => 'nodata',
diff --git a/lib/Datasource/ExternalJson.php b/lib/Datasource/ExternalJson.php
index 119658a1..4f4c8b6c 100644
--- a/lib/Datasource/ExternalJson.php
+++ b/lib/Datasource/ExternalJson.php
@@ -55,6 +55,7 @@ public function getTemplate(): array
$template = array();
$template[] = ['id' => 'url', 'name' => 'URL', 'placeholder' => 'url'];
$template[] = ['id' => 'method', 'name' => $this->l10n->t('HTTP method'), 'placeholder' => 'GET/POST', 'type' => 'tf'];
+ // TRANSLATORS: JSON datasource setting: path to the nested object or array from which data is extracted.
$template[] = ['id' => 'path', 'name' => $this->l10n->t('Object path'), 'placeholder' => 'x/y/z'];
$template[] = ['id' => 'section', 'name' => $this->l10n->t('More options'), 'type' => 'section'];
$template[] = ['id' => 'content-type', 'name' => 'Header Content-Type', 'placeholder' => 'application/json'];
diff --git a/lib/Datasource/Github.php b/lib/Datasource/Github.php
index 89d8893c..ed10b7ae 100644
--- a/lib/Datasource/Github.php
+++ b/lib/Datasource/Github.php
@@ -53,11 +53,19 @@ public function getTemplate(): array {
$template[] = [
'id' => 'data',
'name' => 'Releases, Packages, Issues or PRs',
- 'placeholder' => 'release-' . $this->l10n->t('Releases') . '/package-' . $this->l10n->t('Packages') . '/issues-' . $this->l10n->t('Issues') . '/pulls-' . $this->l10n->t('Pull Requests'),
+ // TRANSLATORS: Noun: published versions of a GitHub project.
+ 'placeholder' => 'release-' . $this->l10n->t('Releases')
+ // TRANSLATORS: Noun: software packages hosted by GitHub.
+ . '/package-' . $this->l10n->t('Packages')
+ // TRANSLATORS: Noun: GitHub issue tickets.
+ . '/issues-' . $this->l10n->t('Issues')
+ // TRANSLATORS: Noun: proposed code changes on GitHub.
+ . '/pulls-' . $this->l10n->t('Pull Requests'),
'type' => 'tf'
];
$template[] = [
'id' => 'limit',
+ // TRANSLATORS: Noun: maximum number of datasource rows to retrieve.
'name' => $this->l10n->t('Limit'),
'placeholder' => $this->l10n->t('Number of rows'),
'type' => 'number'
@@ -174,6 +182,7 @@ public function readData($option): array {
$header[] = $this->l10n->t('Version');
if (isset($option['showAssets']) and $option['showAssets'] === 'true') {
+ // TRANSLATORS: Noun: a downloadable file attached to a GitHub release.
$header[] = $this->l10n->t('Asset');
}
$header[] = $this->l10n->t('Download count');
@@ -194,6 +203,7 @@ public function readData($option): array {
];
}
+ // TRANSLATORS: Noun: a Git tag identifying a release or package version.
$header[] = $this->l10n->t('Tag');
$header[] = $this->l10n->t('Download count');
} else if ($option['data'] === 'issues') {
@@ -219,8 +229,10 @@ public function readData($option): array {
$issuesCleaned = $issuesTotal - $pulls;
+ // TRANSLATORS: Noun: GitHub issue tickets.
$data[] = [$this->l10n->t('Issues'), $issuesCleaned];
$header[] = $this->l10n->t('Type');
+ // TRANSLATORS: Noun: the number of GitHub issues or pull requests.
$header[] = $this->l10n->t('Count');
} else if ($option['data'] === 'pulls') {
$url = 'https://api.github.com/repos/' . $option['user'] . '/' . $option['repository'] . '/pulls?per_page=100';
@@ -232,6 +244,7 @@ public function readData($option): array {
}
$data[] = [$this->l10n->t('Pull Requests'), count($curlResult['data'])];
$header[] = $this->l10n->t('Type');
+ // TRANSLATORS: Noun: the number of GitHub issues or pull requests.
$header[] = $this->l10n->t('Count');
}
diff --git a/lib/Datasource/LocalJson.php b/lib/Datasource/LocalJson.php
index 9af694b7..6e30ecd8 100644
--- a/lib/Datasource/LocalJson.php
+++ b/lib/Datasource/LocalJson.php
@@ -53,6 +53,7 @@ public function getTemplate(): array {
'type' => 'filePicker',
'allowTextVariables' => true
];
+ // TRANSLATORS: JSON datasource setting: path to the nested object or array from which data is extracted.
$template[] = ['id' => 'path', 'name' => $this->l10n->t('Object path'), 'placeholder' => 'x/y/z'];
$template[] = [
'id' => 'timestamp',
diff --git a/lib/Datasource/LocalSpreadsheet.php b/lib/Datasource/LocalSpreadsheet.php
index 9ae38245..e88d1f9c 100644
--- a/lib/Datasource/LocalSpreadsheet.php
+++ b/lib/Datasource/LocalSpreadsheet.php
@@ -63,6 +63,7 @@ public function getTemplate(): array {
];
$template[] = [
'id' => 'sheet',
+ // TRANSLATORS: Noun: a worksheet within a spreadsheet file.
'name' => $this->l10n->t('Sheet'),
'placeholder' => $this->l10n->t('sheet name'),
'type' => 'sheetPicker'
diff --git a/lib/Service/ThresholdService.php b/lib/Service/ThresholdService.php
index 651a105f..1868998c 100644
--- a/lib/Service/ThresholdService.php
+++ b/lib/Service/ThresholdService.php
@@ -261,6 +261,7 @@ public function validate(int $reportId, $dimension1, $dimension2, $value, int $i
$this->NotificationManager->triggerNotification(NotificationManager::SUBJECT_THRESHOLD, $reportId, $threshold['id'], [
'report' => $datasetMetadata['name'],
'subject' => $subject,
+ // TRANSLATORS: Noun: a newly added data row; also used as a notification subject.
'rule' => $this->l10n->t('new record'),
'value' => ''
], $threshold['user_id']);
@@ -363,6 +364,7 @@ public function validateFlexibleRecords(int $datasetId, array $records): void {
$this->NotificationManager->triggerNotification(NotificationManager::SUBJECT_THRESHOLD, $reportId, $threshold['id'], [
'report' => $report['name'],
'subject' => $columns[$reference]['name'],
+ // TRANSLATORS: Noun: a newly added data row; also used as a notification subject.
'rule' => $threshold['option'] === 'new' ? $this->l10n->t('new record') : $threshold['option'],
'value' => $threshold['option'] === 'new' ? '' : $threshold['target'],
], $threshold['user_id']);
diff --git a/templates/part.menu.php b/templates/part.menu.php
index 6db45b3a..dde9538b 100644
--- a/templates/part.menu.php
+++ b/templates/part.menu.php
@@ -29,7 +29,8 @@
t('Expand a group to select values or entries. For lists of records, choose the fields to extract.')); ?>
-t('Object path')); ?>:
+t('Object path')); ?>:
diff --git a/templates/wizard.php b/templates/wizard.php index e9b99a0f..6e5a32e8 100644 --- a/templates/wizard.php +++ b/templates/wizard.php @@ -398,7 +398,8 @@ class="userGuidance"