From 68909832b01f7b7c7e812b40cf5cc9855a3286b4 Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 21 Oct 2026 00:02:40 +0200 Subject: [PATCH] Add translator context for ambiguous Analytics strings Signed-off-by: Rello Assisted-by: Codex:GPT-5 --- AGENTS.md | 2 + CHANGELOG.md | 1 + js/app.js | 5 ++ js/dataset.js | 3 + js/filter.js | 73 +++++++++++++++++++++-- js/jsonPicker.js | 3 + js/panorama.js | 3 + js/panoramaFilters.js | 1 + js/report.js | 2 + js/sidebar.js | 2 + js/tableOptions.js | 1 + js/visualization.js | 7 ++- lib/Activity/Provider.php | 18 ++++++ lib/Controller/DatasourceController.php | 1 + lib/Datasource/ExternalJson.php | 1 + lib/Datasource/Github.php | 15 ++++- lib/Datasource/LocalJson.php | 1 + lib/Datasource/LocalSpreadsheet.php | 1 + lib/Service/ThresholdService.php | 2 + templates/part.menu.php | 15 +++-- templates/part.templates.php | 78 +++++++++++++++++-------- templates/wizard.php | 15 +++-- 22 files changed, 208 insertions(+), 42 deletions(-) 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 @@
  • @@ -69,17 +70,20 @@
    @@ -187,6 +191,7 @@ class="radio"> title="t('Report was changed - Press here to save the current state')); ?>">
    + title="t('Filter')); ?>">
    diff --git a/templates/part.templates.php b/templates/part.templates.php index fb4555bb..ec3727e5 100644 --- a/templates/part.templates.php +++ b/templates/part.templates.php @@ -87,7 +87,8 @@ class="sidebarPointer">t('Visualization')); ?>
  • t('Share')); ?>
  • + class="icon-share analytics-navigation-menu-icon">t('Share')); ?>
  • t('Dataset maintenance')); ?>
  • @@ -733,7 +745,8 @@ class="icon-category-customization analytics-navigation-menu-icon">
  • t('Delete')); ?>
  • t('Unshare')); ?>
  • + class="icon-close analytics-navigation-menu-icon">
    t('Unshare')); ?> @@ -770,14 +783,16 @@ class="icon-close analytics-navigation-menu-icon">t('Un - +
  • - t('Unshare')); ?> + t('Unshare')); ?>
  • @@ -821,7 +836,8 @@ class="sidebarInput linkPassText"> - +
  • @@ -853,7 +869,8 @@ class="icon-confirm share-pass-submit"
  • - t('Unshare')); ?> + t('Unshare')); ?>
  • @@ -1177,7 +1194,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose"
    - t('Line')); ?> + t('Line')); ?>
    @@ -1197,7 +1215,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose" - t('Bar')); ?> + t('Bar')); ?>
    @@ -1207,7 +1226,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose" - t('Bar - Stacked')); ?> + t('Bar - Stacked')); ?>
    @@ -1217,7 +1237,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose" - t('Bar - Stacked 100%%')); ?> + t('Bar - Stacked 100%%')); ?>
    @@ -1227,7 +1248,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose" - t('Area')); ?> + t('Area')); ?>
    @@ -1237,7 +1259,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose" - t('Area - 100%%')); ?> + t('Area - 100%%')); ?>
    @@ -1247,7 +1270,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose" - t('Doughnut')); ?> + t('Doughnut')); ?>
    @@ -1257,7 +1281,8 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose" - t('Funnel')); ?> + t('Funnel')); ?>
    @@ -1282,8 +1307,10 @@ class="analyticsDialogClose tableOptionsCalculatedColumnsEditorClose"
    - - + +
    @@ -1399,7 +1426,8 @@ class="analyticsModelCardIcon analyticsModelCardIcon--time" alt="">

    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"
    - +
    @@ -417,7 +418,8 @@ class="userGuidance"
    - +
    @@ -426,7 +428,8 @@ class="userGuidance"
    - +
    @@ -435,7 +438,8 @@ class="userGuidance"
    - +
    @@ -447,7 +451,8 @@ class="userGuidance"
    - +