From 5a36c119c96dfc0b84ee328e342836e5a780da06 Mon Sep 17 00:00:00 2001 From: Simon Xiao Date: Wed, 15 Apr 2026 18:47:53 -0400 Subject: [PATCH 01/12] added i18n to _files_table.html.erb --- .../app/views/files/_files_table.html.erb | 20 +++++++++---------- apps/dashboard/app/views/files/index.html.erb | 10 +++++----- .../views/layouts/nav/_dropdown_item.html.erb | 7 ++++--- .../app/views/widgets/_classroom.html.erb | 3 +++ apps/dashboard/config/locales/en.yml | 15 ++++++++++++++ apps/dashboard/config/ondemand.d/ondemand.yml | 6 ++++++ 6 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 apps/dashboard/app/views/widgets/_classroom.html.erb create mode 100644 apps/dashboard/config/ondemand.d/ondemand.yml diff --git a/apps/dashboard/app/views/files/_files_table.html.erb b/apps/dashboard/app/views/files/_files_table.html.erb index bccd1fd57a..e3b676d57c 100755 --- a/apps/dashboard/app/views/files/_files_table.html.erb +++ b/apps/dashboard/app/views/files/_files_table.html.erb @@ -4,22 +4,22 @@ <%= render partial: 'breadcrumb', collection: @path.descend, as: :file, locals: { file_count: @path.descend.count, full_path: @path } %> - Select All + t(dashboard.files_table_select_all) - + - - - - - - - + + + + + + + diff --git a/apps/dashboard/app/views/files/index.html.erb b/apps/dashboard/app/views/files/index.html.erb index 371d084001..84ead023fd 100644 --- a/apps/dashboard/app/views/files/index.html.erb +++ b/apps/dashboard/app/views/files/index.html.erb @@ -6,9 +6,9 @@ <%= render partial: 'shell_dropdown' %> <% end %> - - - + + + <%= render(partial: 'upload_button') %> <%= render(partial: 'download_button') %> <% if Configuration.globus_endpoints %> @@ -17,8 +17,8 @@ <% if @user_configuration.files_select_target %> <%= render partial: 'send_to_target_button' %> <% end %> - - + +
diff --git a/apps/dashboard/app/views/layouts/nav/_dropdown_item.html.erb b/apps/dashboard/app/views/layouts/nav/_dropdown_item.html.erb index 2b27f4c6e3..b17bacf26c 100644 --- a/apps/dashboard/app/views/layouts/nav/_dropdown_item.html.erb +++ b/apps/dashboard/app/views/layouts/nav/_dropdown_item.html.erb @@ -1,7 +1,8 @@ +
<%= link.inspect%>
<%= link_to( link.url.to_s, - title: link.title, + title: t(link.title, default: link.title), class: "dropdown-item", target: link.new_tab? ? "_blank" : nil, aria: ({ current: ('page' if (current_page?(link.url))) }), @@ -9,8 +10,8 @@ data: link.data ) do %> - <%= icon_tag(link.icon_uri, classes: ['app-icon', 'me-1']) unless link.icon_uri.to_s.blank? %> <%= link.title %> + <%= icon_tag(link.icon_uri, classes: ['app-icon', 'me-1']) unless link.icon_uri.to_s.blank? %> <%= t(link.title, default: link.title) %> <% if link.subtitle.present? %> - <%= content_tag(:small, link.subtitle, class: 'visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline ms-2') %> + <%= content_tag(:small, t(link.subtitle, default: link.subtitle), class: 'visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline ms-2') %> <% end %> <% end %> diff --git a/apps/dashboard/app/views/widgets/_classroom.html.erb b/apps/dashboard/app/views/widgets/_classroom.html.erb new file mode 100644 index 0000000000..9652b55c74 --- /dev/null +++ b/apps/dashboard/app/views/widgets/_classroom.html.erb @@ -0,0 +1,3 @@ +
+ Classroom widget placeholder +
diff --git a/apps/dashboard/config/locales/en.yml b/apps/dashboard/config/locales/en.yml index fc8d4aff10..bcd54ca9d8 100644 --- a/apps/dashboard/config/locales/en.yml +++ b/apps/dashboard/config/locales/en.yml @@ -150,6 +150,21 @@ en: files_remote_disabled: Remote file support is not enabled files_remote_empty_dir_unsupported: Remote does not support empty directories files_remote_error_listing_remotes: 'Error listing Rclone remotes: %{error}' + files_refresh: Refresh + files_new_file: New File + files_new_directory: New Directory + files_copy_move: Copy/Move + files_delete: Delete + files_table_select_all: Select All + files_table_content_of_directory: Contents of directory %{path} + files_table_select: Select + files_table_type: Type + files_table_name: Name + files_table_actions: Actions + files_table_size: Size + files_table_modified_at: Modified at + files_table_owner: Owner + files_table_mode: Mode files_send_to_target_title_default: Send selected files metadata to the external application files_shell: Open in Terminal files_shell_dropdown: Select Cluster to Open in Terminal diff --git a/apps/dashboard/config/ondemand.d/ondemand.yml b/apps/dashboard/config/ondemand.d/ondemand.yml new file mode 100644 index 0000000000..f0dcd04786 --- /dev/null +++ b/apps/dashboard/config/ondemand.d/ondemand.yml @@ -0,0 +1,6 @@ +pinned_apps: + - sys/files + - sys/jobs + - sys/* +pinned_apps_menu_length: 4 +pinned_apps_group_by: category From a12d9dc1d336223ffc0ad14e3b66b16a97c71d8e Mon Sep 17 00:00:00 2001 From: Simon Xiao Date: Wed, 15 Apr 2026 18:57:19 -0400 Subject: [PATCH 02/12] added i18n to all static files --- .../app/views/files/_breadcrumb.html.erb | 6 +++--- .../app/views/files/_download_button.html.erb | 2 +- .../app/views/files/_files_table.html.erb | 20 +++++++++---------- .../app/views/files/_globus.html.erb | 2 +- .../app/views/files/_upload_button.html.erb | 2 +- apps/dashboard/app/views/files/index.html.erb | 10 +++++----- apps/dashboard/config/locales/en.yml | 6 ++++++ 7 files changed, 27 insertions(+), 21 deletions(-) diff --git a/apps/dashboard/app/views/files/_breadcrumb.html.erb b/apps/dashboard/app/views/files/_breadcrumb.html.erb index 980ab92f98..9924249910 100644 --- a/apps/dashboard/app/views/files/_breadcrumb.html.erb +++ b/apps/dashboard/app/views/files/_breadcrumb.html.erb @@ -1,6 +1,6 @@ <% if file_counter == 0 %> <% end %> @@ -10,10 +10,10 @@ <%= path_segment_with_slash(@filesystem, file.basename.to_s, file_counter, file_count) %> <% else %>
<%= "Contents of directory #{@path}" %> <%= t(dashboard.files_table_content_of_directory, path: @path) %>
- Select + t(dashboard.files_table_select) TypeNameActionsSizeModified atOwnerModet(dashboard.files_table_type)t(dashboard.files_table_name)t(dashboard.files_table_actions)t(dashboard.files_table_size)t(dashboard.files_table_modified_at)t(dashboard.files_table_owner)t(dashboard.files_table_mode)
- + - - - - - - - + + + + + + + diff --git a/apps/dashboard/app/views/files/_globus.html.erb b/apps/dashboard/app/views/files/_globus.html.erb index fe92865c73..a624347bec 100644 --- a/apps/dashboard/app/views/files/_globus.html.erb +++ b/apps/dashboard/app/views/files/_globus.html.erb @@ -1,5 +1,5 @@ - Open in Globus + <%= t('dashboard.files_open_in_globus') %> diff --git a/apps/dashboard/app/views/files/_upload_button.html.erb b/apps/dashboard/app/views/files/_upload_button.html.erb index 5d312a0063..a6f136f202 100644 --- a/apps/dashboard/app/views/files/_upload_button.html.erb +++ b/apps/dashboard/app/views/files/_upload_button.html.erb @@ -1,5 +1,5 @@ <%- if Configuration.upload_enabled? -%> <%- end -%> diff --git a/apps/dashboard/app/views/files/index.html.erb b/apps/dashboard/app/views/files/index.html.erb index 84ead023fd..805893a142 100644 --- a/apps/dashboard/app/views/files/index.html.erb +++ b/apps/dashboard/app/views/files/index.html.erb @@ -6,9 +6,9 @@ <%= render partial: 'shell_dropdown' %> <% end %> - - - + + + <%= render(partial: 'upload_button') %> <%= render(partial: 'download_button') %> <% if Configuration.globus_endpoints %> @@ -17,8 +17,8 @@ <% if @user_configuration.files_select_target %> <%= render partial: 'send_to_target_button' %> <% end %> - - + +
diff --git a/apps/dashboard/config/locales/en.yml b/apps/dashboard/config/locales/en.yml index bcd54ca9d8..86881bfe74 100644 --- a/apps/dashboard/config/locales/en.yml +++ b/apps/dashboard/config/locales/en.yml @@ -155,6 +155,9 @@ en: files_new_directory: New Directory files_copy_move: Copy/Move files_delete: Delete + files_download: Download + files_upload: Upload + files_open_in_globus: Open in Globus files_table_select_all: Select All files_table_content_of_directory: Contents of directory %{path} files_table_select: Select @@ -165,6 +168,9 @@ en: files_table_modified_at: Modified at files_table_owner: Owner files_table_mode: Mode + files_go_up_directory: Go up directory + files_change_directory: Change directory + files_copy_path: Copy path files_send_to_target_title_default: Send selected files metadata to the external application files_shell: Open in Terminal files_shell_dropdown: Select Cluster to Open in Terminal From 4b2685f86b424c9d9a2ddb7df628ddd6a48e814b Mon Sep 17 00:00:00 2001 From: Simon Xiao Date: Fri, 17 Apr 2026 12:01:25 -0400 Subject: [PATCH 03/12] added i18n to all the js files in files directory --- .../app/javascript/files/clip_board.js | 29 +- .../app/javascript/files/data_table.js | 38 ++- .../app/javascript/files/file_ops.js | 295 +++++++++--------- .../views/files/_page_load_config.html.erb | 48 +++ apps/dashboard/config/locales/en.yml | 90 ++++-- apps/dashboard/config/locales/zh-CN.yml | 65 ++++ 6 files changed, 367 insertions(+), 198 deletions(-) diff --git a/apps/dashboard/app/javascript/files/clip_board.js b/apps/dashboard/app/javascript/files/clip_board.js index 40c2846c1d..f443bc0f83 100755 --- a/apps/dashboard/app/javascript/files/clip_board.js +++ b/apps/dashboard/app/javascript/files/clip_board.js @@ -1,11 +1,11 @@ import ClipboardJS from 'clipboard'; -import {CONTENTID} from './data_table.js'; -import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; -import {EVENTNAME as FILEOPS_EVENTNAME} from './file_ops.js'; +import { CONTENTID } from './data_table.js'; +import { EVENTNAME as SWAL_EVENTNAME } from './sweet_alert.js'; +import { EVENTNAME as FILEOPS_EVENTNAME } from './file_ops.js'; import { csrfToken } from '../config.js'; import { OODAlertError } from '../alert.js'; -export {EVENTNAME}; +export { EVENTNAME }; const EVENTNAME = { clearClipboard: 'clearClipboard', @@ -13,7 +13,12 @@ const EVENTNAME = { updateClipboardView: 'updateClipboardView', } +let filesLabels = null; + jQuery(function () { + const configEl = document.getElementById('files_page_load_config'); + if (!configEl) return; + filesLabels = configEl.dataset; var clipBoard = new ClipBoard(); @@ -30,8 +35,8 @@ jQuery(function () { }); - $(CONTENTID).on('success', function (e) { - $(e.trigger).tooltip({ title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom' }).tooltip('show'); + clipBoard.getClipBoard().on('success', function (e) { + $(e.trigger).tooltip({ title: filesLabels.labelCopySuccessful, trigger: 'manual', placement: 'bottom' }).tooltip('show'); setTimeout(() => $(e.trigger).tooltip('hide'), 2000); e.clearSelection(); }); @@ -47,7 +52,7 @@ jQuery(function () { $(CONTENTID).on(EVENTNAME.updateClipboard, function (e, options) { if (options.selection.length == 0) { - OODAlertError('Select a file, files, or directory to copy or move. You have selected none.'); + OODAlertError(filesLabels.labelCopyMoveError); $(CONTENTID).trigger(EVENTNAME.clearClipboard); } else { @@ -114,12 +119,12 @@ class ClipBoard { closeButton.type = 'button'; closeButton.className = 'btn-close'; closeButton.setAttribute('data-bs-dismiss', 'alert'); - closeButton.setAttribute('aria-label', 'Close'); + closeButton.setAttribute('aria-label', filesLabels.labelClipboardClose); cardBody.appendChild(closeButton); const description = document.createElement('p'); description.className = 'mt-4'; - description.innerHTML = `Copy or move the files below from ${clipboard.from} to the current directory:`; + description.innerHTML = filesLabels.labelClipboardDescription.replace('__FROM__', clipboard.from); cardBody.appendChild(description); card.appendChild(cardBody); @@ -133,7 +138,7 @@ class ClipBoard { listItem.className = 'list-group-item'; const icon = document.createElement('span'); - icon.title = file.directory ? 'directory' : 'file'; + icon.title = file.directory ? filesLabels.labelDirectory : filesLabels.labelFile; icon.className = file.directory ? 'fa fa-folder color-gold' : 'fa fa-file color-lightgrey'; @@ -154,13 +159,13 @@ class ClipBoard { const copyButton = document.createElement('button'); copyButton.id = 'clipboard-copy-to-dir'; copyButton.className = 'btn btn-primary'; - copyButton.textContent = 'Copy'; + copyButton.textContent = filesLabels.labelClipboardCopy; actionsBody.appendChild(copyButton); const moveButton = document.createElement('button'); moveButton.id = 'clipboard-move-to-dir'; moveButton.className = 'btn btn-danger float-end'; - moveButton.textContent = 'Move'; + moveButton.textContent = filesLabels.labelClipboardMove; actionsBody.appendChild(moveButton); card.appendChild(actionsBody); diff --git a/apps/dashboard/app/javascript/files/data_table.js b/apps/dashboard/app/javascript/files/data_table.js index 6f167580fc..20c402cdf6 100644 --- a/apps/dashboard/app/javascript/files/data_table.js +++ b/apps/dashboard/app/javascript/files/data_table.js @@ -15,8 +15,12 @@ const CONTENTID = '#directory-contents'; const SPINNERID = '#tloading_spinner'; let table = null; +let filesLabels = null; jQuery(function () { + const configEl = document.getElementById('files_page_load_config'); + if (!configEl) return; + filesLabels = configEl.dataset; table = new DataTable(); /* END BUTTON ACTIONS */ @@ -189,7 +193,7 @@ class DataTable { }).DataTable({ autoWidth: false, language: { - search: 'Filter:', + search: filesLabels.labelFilter, infoFiltered: "" }, order: [[1, "asc"], [2, "asc"]], @@ -221,10 +225,10 @@ class DataTable { orderable: false, defaultContent: '', render: (data, type, row, meta) => { - return ``; + return ``; } }, - { data: 'type', render: (data, type, row, meta) => data == 'd' ? 'directory' : 'file' }, // type + { data: 'type', render: (data, type, row, meta) => data == 'd' ? `${filesLabels.labelDirectory}` : `${filesLabels.labelFile}` }, // type { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => this.renderNameColumn(data, type, row, meta) }, // name { name: 'actions', orderable: false, searchable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, { @@ -239,7 +243,7 @@ class DataTable { let date = new Date(data * 1000) // Return formatted date "3/23/2021 10:52:28 AM" - return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + return isNaN(data) ? filesLabels.labelInvalidDate : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` } else { return data; @@ -262,8 +266,8 @@ class DataTable { ] }); - $('div.dt-search').prepend(``) - $('div.dt-search').prepend(``) + $('div.dt-search').prepend(``) + $('div.dt-search').prepend(``) this.updateGlobus(); } @@ -303,8 +307,8 @@ class DataTable { $('#select_all').trigger(); } - $(`${CONTENTID}_caption`).text(`Contents of directory ${data.path}`); - ariaNotify(`navigated to ${data.path}`); + $(`${CONTENTID}_caption`).text(filesLabels.labelContentsOfDirectory.replace('__PATH__', data.path)); + ariaNotify(filesLabels.labelNavigatedTo.replace('__PATH__', data.path)); let result = await Promise.resolve(data); $('td input[type=checkbox]').on('keypress', function(event) { @@ -384,7 +388,7 @@ class DataTable { if(disposition === null) { return response; } else { - throw new Error("Cannot navigate to a file."); + throw new Error(filesLabels.labelCannotNavigateToFile); } }) .then(response => response.json()) @@ -444,7 +448,7 @@ class DataTable { button.setAttribute('data-bs-toggle', 'dropdown'); button.setAttribute('aria-haspopup', 'true'); button.setAttribute('aria-expanded', 'false'); - button.setAttribute('title', 'Actions'); + button.setAttribute('title', filesLabels.labelActions); // Create the icon inside the button const icon = document.createElement('span'); @@ -466,7 +470,7 @@ class DataTable { viewLink.href = data.url; viewLink.classList.add('view-file', 'dropdown-item'); viewLink.setAttribute('data-row-index', rowIndex); - viewLink.innerHTML = ' View'; + viewLink.innerHTML = ` ${filesLabels.labelView}`; viewItem.appendChild(viewLink); dropdownMenu.appendChild(viewItem); } @@ -477,7 +481,7 @@ class DataTable { editLink.href = data.edit_url; editLink.classList.add('edit-file', 'dropdown-item'); editLink.setAttribute('data-row-index', rowIndex); - editLink.innerHTML = ' Edit'; + editLink.innerHTML = ` ${filesLabels.labelEdit}`; editItem.appendChild(editLink); dropdownMenu.appendChild(editItem); } @@ -489,7 +493,7 @@ class DataTable { renameLink.href = '#'; renameLink.classList.add('rename-file', 'dropdown-item'); renameLink.setAttribute('data-row-index', rowIndex); - renameLink.innerHTML = ' Rename'; + renameLink.innerHTML = ` ${filesLabels.labelRename}`; renameItem.appendChild(renameLink); dropdownMenu.appendChild(renameItem); @@ -500,7 +504,7 @@ class DataTable { downloadLink.href = data.download_url; downloadLink.classList.add('download-file', 'dropdown-item'); downloadLink.setAttribute('data-row-index', rowIndex); - downloadLink.innerHTML = ' Download'; + downloadLink.innerHTML = ` ${filesLabels.labelDownload}`; downloadItem.appendChild(downloadLink); dropdownMenu.appendChild(downloadItem); } @@ -516,7 +520,7 @@ class DataTable { deleteLink.href = '#'; deleteLink.classList.add('delete-file', 'dropdown-item', 'text-danger'); deleteLink.setAttribute('data-row-index', rowIndex); - deleteLink.innerHTML = ' Delete'; + deleteLink.innerHTML = ` ${filesLabels.labelDelete}`; deleteItem.appendChild(deleteLink); dropdownMenu.appendChild(deleteItem); @@ -532,9 +536,9 @@ class DataTable { let api = this._table; let rows = api.rows({ selected: true }).flatten().length, page_info = api.page.info(), - msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; + msg = page_info.recordsTotal == page_info.recordsDisplay ? filesLabels.labelShowingRows.replace('__SHOWN__', page_info.recordsDisplay) : filesLabels.labelShowingRowsFiltered.replace('__SHOWN__', page_info.recordsDisplay).replace('__TOTAL__', page_info.recordsTotal); - $('#directory-contents_info').html(`${msg} - ${rows} rows selected`); + $('#directory-contents_info').html(`${msg} - ${filesLabels.labelRowsSelected.replace('__COUNT__', rows)}`); } goto(url, pushState = true, show_processing_indicator = true) { diff --git a/apps/dashboard/app/javascript/files/file_ops.js b/apps/dashboard/app/javascript/files/file_ops.js index 451b200c17..2bdf4f62f2 100644 --- a/apps/dashboard/app/javascript/files/file_ops.js +++ b/apps/dashboard/app/javascript/files/file_ops.js @@ -1,11 +1,11 @@ -import {CONTENTID, EVENTNAME as DATATABLE_EVENTNAME} from './data_table.js'; -import {EVENTNAME as CLIPBOARD_EVENTNAME} from './clip_board.js'; -import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; +import { CONTENTID, EVENTNAME as DATATABLE_EVENTNAME } from './data_table.js'; +import { EVENTNAME as CLIPBOARD_EVENTNAME } from './clip_board.js'; +import { EVENTNAME as SWAL_EVENTNAME } from './sweet_alert.js'; import _ from 'lodash'; import { transfersPath, csrfToken } from '../config.js'; import { OODAlertError } from '../alert'; -export {EVENTNAME}; +export { EVENTNAME }; const EVENTNAME = { changeDirectory: 'changeDirectory', @@ -25,56 +25,59 @@ const EVENTNAME = { renameFilePrompt: 'renameFilePrompt', } - +let filesLabels = null; let fileOps = null; -export {fileOps}; +export { fileOps }; -jQuery(function() { - fileOps = new FileOps(); +jQuery(function () { + const configEl = document.getElementById('files_page_load_config'); + if (!configEl) return; + filesLabels = configEl.dataset; + fileOps = new FileOps(); - $('#directory-contents tbody, #path-breadcrumbs, #favorites').on('click', 'a.d', function(event){ - if(fileOps.clickEventIsSignificant(event)){ + $('#directory-contents tbody, #path-breadcrumbs, #favorites').on('click', 'a.d', function (event) { + if (fileOps.clickEventIsSignificant(event)) { event.preventDefault(); event.cancelBubble = true; - if(event.stopPropagation) event.stopPropagation(); + if (event.stopPropagation) event.stopPropagation(); const eventData = { 'path': this.getAttribute("href"), }; - + $(CONTENTID).trigger(DATATABLE_EVENTNAME.goto, eventData); - + } }); $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function (e) { if (this.dataset['dlUrl'] == 'undefined' && this.checked) { $("#download-btn").attr('disabled', true); - } else if ($("input[data-dl-url='undefined']:checked" ).length == 0) { + } else if ($("input[data-dl-url='undefined']:checked").length == 0) { $("#download-btn").attr('disabled', false); } }); - - $('#directory-contents tbody').on('dblclick', 'tr td:not(:first-child)', function(){ + + $('#directory-contents tbody').on('dblclick', 'tr td:not(:first-child)', function () { // handle double-click let a = this.parentElement.querySelector('a'); - if(a.classList.contains('d')) { + if (a.classList.contains('d')) { const eventData = { 'path': a.getAttribute("href"), }; - + $(CONTENTID).trigger(DATATABLE_EVENTNAME.goto, eventData); } }); - $('#directory-contents tbody').on('click', '.download-file', function(e){ + $('#directory-contents tbody').on('click', '.download-file', function (e) { e.preventDefault(); const table = $(CONTENTID).DataTable(); const row = e.currentTarget.dataset.rowIndex; const eventData = { - selection: table.rows(row).data() + selection: table.rows(row).data() }; $(CONTENTID).trigger(EVENTNAME.download, eventData); @@ -89,14 +92,14 @@ jQuery(function() { }); $("#new-dir-btn").on("click", function () { - $(CONTENTID).trigger(EVENTNAME.newDirectoryPrompt); + $(CONTENTID).trigger(EVENTNAME.newDirectoryPrompt); }); $("#download-btn").on("click", function () { let table = $(CONTENTID).DataTable(); let selection = table.rows({ selected: true }).data(); const eventData = { - selection: selection + selection: selection }; $(CONTENTID).trigger(EVENTNAME.download, eventData); @@ -108,7 +111,7 @@ jQuery(function() { let table = $(CONTENTID).DataTable(); let files = table.rows({ selected: true }).data().toArray().map((f) => f.name); const eventData = { - files: files + files: files }; $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); @@ -116,7 +119,7 @@ jQuery(function() { }); $(document).on("click", '#goto-btn', function () { - $(CONTENTID).trigger(EVENTNAME.changeDirectoryPrompt); + $(CONTENTID).trigger(EVENTNAME.changeDirectoryPrompt); }); $(document).on('click', '.rename-file', function (e) { @@ -127,25 +130,25 @@ jQuery(function() { let fileName = $($.parseHTML(row.name)).text(); const eventData = { - file: fileName, + file: fileName, }; - + $(CONTENTID).trigger(EVENTNAME.renameFilePrompt, eventData); }); $(document).on('click', '.delete-file', function (e) { - e.preventDefault(); - let table = $(CONTENTID).DataTable(); - let rowId = e.currentTarget.dataset.rowIndex; - let row = table.row(rowId).data(); - let fileName = $($.parseHTML(row.name)).text(); + e.preventDefault(); + let table = $(CONTENTID).DataTable(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); - const eventData = { - files: [fileName] - }; + const eventData = { + files: [fileName] + }; - $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); + $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); }); @@ -174,22 +177,22 @@ jQuery(function() { }); $(CONTENTID).on(EVENTNAME.download, function (e, options) { - if(options.selection.length == 0) { - OODAlertError('Select a file, files, or directory to download. You have selected none.'); + if (options.selection.length == 0) { + OODAlertError(filesLabels.labelDownloadErrorMsg); } else { fileOps.download(options.selection); } }); $(CONTENTID).on(EVENTNAME.deletePrompt, function (e, options) { - if(options.files.length == 0) { - OODAlertError('Select a file, files, or directory to delete. You have selected none.'); + if (options.files.length == 0) { + OODAlertError(filesLabels.labelDeleteErrorMsg); } else { fileOps.deletePrompt(options.files); } }); - $(CONTENTID).on(EVENTNAME.deleteFile, function (e, options) { + $(CONTENTID).on(EVENTNAME.deleteFile, function (e, options) { fileOps.delete(options.files, options.from_fs); }); @@ -223,7 +226,7 @@ class FileOps { clickEventIsSignificant(event) { return !( // (event.target && (event.target as any).isContentEditable) - event.defaultPrevented + event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey @@ -245,18 +248,18 @@ class FileOps { const eventData = { action: 'changeDirectory', 'inputOptions': { - title: 'Change Directory', + title: filesLabels.labelChangeDirectoryTitle, input: 'text', - inputLabel: 'Path', - inputValue: history.state.currentDirectory, + inputLabel: filesLabels.labelPath, + inputValue: history.state.currentDirectory, inputAttributes: { spellcheck: 'false', }, showCancelButton: true, inputValidator: (value) => { - if (! value || ! value.startsWith('/')) { + if (!value || !value.startsWith('/')) { // TODO: validate filenames against listing - return 'Provide an absolute pathname' + return filesLabels.labelProvideAbsolutePath } } } @@ -271,8 +274,8 @@ class FileOps { action: EVENTNAME.deleteFile, files: files, 'inputOptions': { - title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, - text: 'Are you sure you want to delete the files: ' + files.join(', '), + title: files.length == 1 ? filesLabels.labelDeleteSingle.replace('__FILE__', files[0]) : filesLabels.labelDeleteMultiple.replace('__COUNT__', files.length), + text: filesLabels.labelDeleteConfirm.replace('__FILES__', files.join(', ')), showCancelButton: true, } }; @@ -281,10 +284,10 @@ class FileOps { } } - + removeFiles(files) { this.transferFiles(files, "rm", "remove files", history.state.currentFilesystem) - } + } renameFile(fileName, newFileName) { let files = {}; @@ -297,20 +300,20 @@ class FileOps { action: EVENTNAME.renameFile, files: fileName, 'inputOptions': { - title: 'Rename', + title: filesLabels.labelRename, input: 'text', - inputLabel: 'Filename', + inputLabel: filesLabels.labelFilename, inputValue: fileName, inputAttributes: { spellcheck: 'false', }, showCancelButton: true, inputValidator: (value) => { - if (! value) { + if (!value) { // TODO: validate filenames against listing - return 'Provide a filename to rename this to'; + return filesLabels.labelProvideRenameFilename; } else if (value.includes('/') || value.includes('..')) { - return 'Filename cannot include / or ..'; + return filesLabels.labelFilenameIllegalChars; } } } @@ -327,18 +330,18 @@ class FileOps { const eventData = { action: EVENTNAME.createFile, 'inputOptions': { - title: 'New File', + title: filesLabels.labelNewFile, input: 'text', - inputLabel: 'Filename', + inputLabel: filesLabels.labelFilename, showCancelButton: true, inputValidator: (value) => { if (!value) { // TODO: validate filenames against listing - return 'Provide a non-empty filename.' + return filesLabels.labelProvideNonemptyFilename } else if (value.includes("/")) { // TODO: validate filenames against listing - return 'Illegal character (/) not allowed in filename.' + return filesLabels.labelFilenameIllegalSlash } } } @@ -356,7 +359,7 @@ class FileOps { myFileOp.reloadTable(); }) .catch(function (e) { - OODAlertError(`Error occurred when attempting to create new file: ${e.message}`); + OODAlertError(filesLabels.labelErrorCreateFile.replace('__ERROR__', e.message)); }); } @@ -365,14 +368,14 @@ class FileOps { const eventData = { action: EVENTNAME.createDirectory, 'inputOptions': { - title: 'New Directory', + title: filesLabels.labelNewDirectory, input: 'text', - inputLabel: 'Directory name', + inputLabel: filesLabels.labelDirectoryName, showCancelButton: true, inputValidator: (value) => { if (!value || value.includes("/")) { // TODO: validate filenames against listing - return 'Provide a directory name that does not have / in it' + return filesLabels.labelProvideDirectoryName } } } @@ -384,19 +387,19 @@ class FileOps { newDirectory(filename) { let myFileOp = new FileOps(); - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrfToken() }}) + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, { method: 'put', headers: { 'X-CSRF-Token': csrfToken() } }) .then(response => this.dataFromJsonResponse(response)) .then(function () { myFileOp.reloadTable(); }) .catch(function (e) { - OODAlertError(`Error occurred when attempting to create new directory: ${e.message}`); + OODAlertError(filesLabels.labelErrorCreateDirectory.replace('__ERROR__', e.message)); }); } download(selection) { - selection.toArray().forEach( (f) => { - if(f.type == 'd') { + selection.toArray().forEach((f) => { + if (f.type == 'd') { this.downloadDirectory(f); } else if (f.type == 'f') { this.downloadFile(f); @@ -406,17 +409,17 @@ class FileOps { downloadDirectory(file) { let filename = $($.parseHTML(file.name)).text(), - canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` + canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` + + this.showSwalLoading(filesLabels.labelPreparingDownload.replace('__NAME__', file.name)); - this.showSwalLoading('preparing to download directory: ' + file.name); - fetch(canDownloadReq, { - method: 'GET', - headers: { - 'X-CSRF-Token': csrfToken(), - 'Accept': 'application/json' - } - }) + method: 'GET', + headers: { + 'X-CSRF-Token': csrfToken(), + 'Accept': 'application/json' + } + }) .then(response => this.dataFromJsonResponse(response)) .then(data => { if (data.can_download) { @@ -424,58 +427,58 @@ class FileOps { this.downloadFile(file) } else { this.doneLoading(); - OODAlertError(`Error while downloading: ${data.error_message}`); + OODAlertError(filesLabels.labelErrorDownloading.replace('__ERROR__', data.error_message)); } }) .catch(e => { this.doneLoading(); - OODAlertError(`Error while downloading: ${e.message}`); + OODAlertError(filesLabels.labelErrorDownloading.replace('__ERROR__', e.message)); }) } - - + + downloadFile(file) { // creating the temporary iframe is exactly what the CloudCmd does // so this just repeats the status quo - + let filename = $($.parseHTML(file.name)).text(), - downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, - iframe = document.createElement('iframe'), - TIME = 30 * 1000; - + downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, + iframe = document.createElement('iframe'), + TIME = 30 * 1000; + iframe.setAttribute('class', 'd-none'); iframe.setAttribute('src', downloadUrl); - + document.body.appendChild(iframe); - - setTimeout(function() { + + setTimeout(function () { document.body.removeChild(iframe); }, TIME); } - + dataFromJsonResponse(response) { return new Promise((resolve, reject) => { - Promise.resolve(response) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) - .then(response => response.json()) - .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) - .catch((e) => reject(e)) + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) }); } - - + + delete(files) { - this.showSwalLoading('Deleting files...: '); + this.showSwalLoading(filesLabels.labelDeletingFiles); - this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrfToken() ); + this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrfToken()); } - transferFiles(files, action, summary, from_fs, to_fs){ + transferFiles(files, action, summary, from_fs, to_fs) { this._failures = 0; this.showSwalLoading(_.startCase(summary)); - + return fetch(transfersPath(), { method: 'post', body: JSON.stringify({ @@ -486,61 +489,61 @@ class FileOps { }), headers: { 'X-CSRF-Token': csrfToken() } }) - .then(response => this.dataFromJsonResponse(response)) - .then((data) => { - - if(! data.completed){ - // was async, gotta report on progress and start polling - this.reportTransfer(data); - this.findAndUpdateTransferStatus(data); - } else { - // if(data.target_dir == history.state.currentDirectory){ - // } - // this.findAndUpdateTransferStatus(data); - } - - if(action == 'mv' || action == 'cp') { - this.reloadTable(); - this.clearClipboard(); - this.updateClipboard(); - } + .then(response => this.dataFromJsonResponse(response)) + .then((data) => { - this.fadeOutTransferStatus(data); - this.doneLoading(); - this.reloadTable(); + if (!data.completed) { + // was async, gotta report on progress and start polling + this.reportTransfer(data); + this.findAndUpdateTransferStatus(data); + } else { + // if(data.target_dir == history.state.currentDirectory){ + // } + // this.findAndUpdateTransferStatus(data); + } - }) - .then(() => this.doneLoading()) - .catch(e => { - this.doneLoading(); - OODAlertError(`Error occurred when attempting to ${summary}: ${e.message}`); - }) + if (action == 'mv' || action == 'cp') { + this.reloadTable(); + this.clearClipboard(); + this.updateClipboard(); + } + + this.fadeOutTransferStatus(data); + this.doneLoading(); + this.reloadTable(); + + }) + .then(() => this.doneLoading()) + .catch(e => { + this.doneLoading(); + OODAlertError(filesLabels.labelErrorTransfer.replace('__SUMMARY__', summary).replace('__ERROR__', e.message)); + }) } - + findAndUpdateTransferStatus(data) { let id = `#${data.id}`; - - if($(id).length){ + + if ($(id).length) { $(id).replaceWith(this.reportTransferTemplate(data)); - } else{ + } else { $('.transfers-status').append(this.reportTransferTemplate(data)); } } - - fadeOutTransferStatus(data){ + + fadeOutTransferStatus(data) { let id = `#${data.id}`; $(id).fadeOut(4000); } reportTransferTemplate(data) { let html = ''; - + if (data.completed) { if (data.error_summary) { html += ` - - + + - + diff --git a/apps/dashboard/app/views/projects/_job_details_content.html.erb b/apps/dashboard/app/views/projects/_job_details_content.html.erb index 84a10f9421..404d278acd 100644 --- a/apps/dashboard/app/views/projects/_job_details_content.html.erb +++ b/apps/dashboard/app/views/projects/_job_details_content.html.erb @@ -13,7 +13,7 @@
<%= t(dashboard.files_table_content_of_directory, path: @path) %> <%= t('dashboard.files_table_content_of_directory', path: @path) %>
- t(dashboard.files_table_select) + <%= t('dashboard.files_table_select') %> t(dashboard.files_table_type)t(dashboard.files_table_name)t(dashboard.files_table_actions)t(dashboard.files_table_size)t(dashboard.files_table_modified_at)t(dashboard.files_table_owner)t(dashboard.files_table_mode)<%= t('dashboard.files_table_type') %><%= t('dashboard.files_table_name') %><%= t('dashboard.files_table_actions') %><%= t('dashboard.files_table_size') %><%= t('dashboard.files_table_modified_at') %><%= t('dashboard.files_table_owner') %><%= t('dashboard.files_table_mode') %>
Directory NameApp Details<%= t('dashboard.products_dir_name') %><%= t('dashboard.products_app_details') %> Last Modified<%= t('dashboard.products_last_modified') %>
- + <% job.to_human_display.each do |name, value| %> diff --git a/apps/dashboard/app/views/projects/index.html.erb b/apps/dashboard/app/views/projects/index.html.erb index bc74123325..5506f827d5 100644 --- a/apps/dashboard/app/views/projects/index.html.erb +++ b/apps/dashboard/app/views/projects/index.html.erb @@ -1,7 +1,7 @@ <%= javascript_include_tag('projects_index', nonce: true, type: 'module') %>
@@ -24,21 +24,21 @@
<% if project.deletable? %> - <%= button_to 'Delete', project_path(project.id), class: 'btn btn-danger w-100 mb-1', method: 'delete', + <%= button_to t('dashboard.delete'), project_path(project.id), class: 'btn btn-danger w-100 mb-1', method: 'delete', form: { class: 'button_to d-flex justify-content-center' }, data: { confirm: I18n.t('dashboard.jobs_project_delete_project_confirmation') } %> <% else %> - <%= button_to 'Remove', project_path(project.id), class: 'btn btn-warning w-100 mb-1', method: 'delete', + <%= button_to t('dashboard.remove'), project_path(project.id), class: 'btn btn-warning w-100 mb-1', method: 'delete', form: { class: 'button_to d-flex justify-content-center' } %> <% end %>
<% if project.editable? %>
- <%= button_to 'Edit', edit_project_path(project.id), class: 'btn btn-info w-100', method: 'get', + <%= button_to t('dashboard.edit'), edit_project_path(project.id), class: 'btn btn-info w-100', method: 'get', form: { class: 'd-flex justify-content-center' }, - title: 'Edit project directory' + title: t('dashboard.jobs_project_edit_project_directory') %>
<% end %> diff --git a/apps/dashboard/app/views/projects/new.html.erb b/apps/dashboard/app/views/projects/new.html.erb index 41b318d9e9..d4546a5d7a 100644 --- a/apps/dashboard/app/views/projects/new.html.erb +++ b/apps/dashboard/app/views/projects/new.html.erb @@ -1,7 +1,7 @@ <%= javascript_include_tag('projects_new', nonce: true, type: 'module') %>
diff --git a/apps/dashboard/app/views/projects/show.html.erb b/apps/dashboard/app/views/projects/show.html.erb index c88a5ca1fc..139ea5d5af 100644 --- a/apps/dashboard/app/views/projects/show.html.erb +++ b/apps/dashboard/app/views/projects/show.html.erb @@ -14,7 +14,7 @@

<%= @project.title %>

- <%= link_to 'Back to Projects', projects_path, class: 'btn btn-default align-self-start', title: 'Return to projects page' %> + <%= link_to t('dashboard.jobs_project_back_to_projects'), projects_path, class: 'btn btn-default align-self-start', title: t('dashboard.jobs_project_return_to_projects_page') %>
@@ -36,7 +36,7 @@
- <%= link_to clone_project_launcher_path(@project.id, launcher.id), title: "Clone launcher" do %> + <%= link_to clone_project_launcher_path(@project.id, launcher.id), title: t('dashboard.jobs_project_clone_launcher') do %> <%= icon_tag(URI.parse("fas://clone")) %> <% end %>
@@ -59,12 +59,12 @@
-

Active Jobs

+

<%= t('dashboard.jobs_project_active_jobs') %>

<%= render(partial: 'job_details', collection: @project.active_jobs, as: :job, locals: { project: @project }) %>
-

Completed Jobs

+

<%= t('dashboard.jobs_project_completed_jobs') %>

<%- @project.completed_jobs.each do |job| -%>
"> @@ -87,7 +87,7 @@ %>
- Loading... + <%= t('dashboard.loading') %>
<%- end -%> diff --git a/apps/dashboard/app/views/shared/_path_selector_table.html.erb b/apps/dashboard/app/views/shared/_path_selector_table.html.erb index 791f11f3e9..b16dd2bc3e 100644 --- a/apps/dashboard/app/views/shared/_path_selector_table.html.erb +++ b/apps/dashboard/app/views/shared/_path_selector_table.html.erb @@ -35,7 +35,7 @@ <% if favorites %>
-
Favorites
+
<%= I18n.t('dashboard.path_selector_favorites') %>
<% homedir = FavoritePath.new(Dir.home, title:I18n.t('dashboard.home_directory')) %> <%= render(partial: 'batch_connect/session_contexts/favorites', collection: favorites.unshift(homedir), as: :path) %> @@ -56,15 +56,15 @@
- Loading... + <%= I18n.t('dashboard.loading') %>
Message <%= t('dashboard.jobs_project_message') %> <%= native_message(job.native) %>
- + - - + + diff --git a/apps/dashboard/app/views/system_status/index.html.erb b/apps/dashboard/app/views/system_status/index.html.erb index 6826a67acd..26d3bae2b5 100644 --- a/apps/dashboard/app/views/system_status/index.html.erb +++ b/apps/dashboard/app/views/system_status/index.html.erb @@ -1,2 +1,2 @@ -
Loading
+
<%= t('dashboard.loading') %>
<%= javascript_include_tag 'system_status' %> \ No newline at end of file diff --git a/apps/dashboard/app/views/system_status/index.turbo_stream.erb b/apps/dashboard/app/views/system_status/index.turbo_stream.erb index 1a8675dd14..f6c4933132 100644 --- a/apps/dashboard/app/views/system_status/index.turbo_stream.erb +++ b/apps/dashboard/app/views/system_status/index.turbo_stream.erb @@ -19,7 +19,7 @@
- <%= current_status[:percent] %> in use + <%= t('dashboard.system_status_in_use', percent: current_status[:percent]) %> <% end %> @@ -29,8 +29,8 @@
Choose a file or directory to use in the form.<%= I18n.t('dashboard.path_selector_caption') %>
TypeName<%= I18n.t('dashboard.files_table_type') %><%= I18n.t('dashboard.files_table_name') %>
- - + + diff --git a/apps/dashboard/app/views/widgets/_system_status.html.erb b/apps/dashboard/app/views/widgets/_system_status.html.erb index fd42c65d95..b880c92f00 100644 --- a/apps/dashboard/app/views/widgets/_system_status.html.erb +++ b/apps/dashboard/app/views/widgets/_system_status.html.erb @@ -1,2 +1,2 @@ -
Loading
+
<%= t('dashboard.loading') %>
<%= javascript_include_tag 'system_status' %> diff --git a/apps/dashboard/app/views/workflows/edit.html.erb b/apps/dashboard/app/views/workflows/edit.html.erb index 50dc905e0b..d0cdd818cf 100644 --- a/apps/dashboard/app/views/workflows/edit.html.erb +++ b/apps/dashboard/app/views/workflows/edit.html.erb @@ -1,5 +1,5 @@
diff --git a/apps/dashboard/app/views/workflows/new.html.erb b/apps/dashboard/app/views/workflows/new.html.erb index eb61b53c77..551daede40 100644 --- a/apps/dashboard/app/views/workflows/new.html.erb +++ b/apps/dashboard/app/views/workflows/new.html.erb @@ -1,7 +1,7 @@ <%= javascript_include_tag 'workflow_new', nonce: true, defer: true %>
diff --git a/apps/dashboard/app/views/workflows/show.html.erb b/apps/dashboard/app/views/workflows/show.html.erb index 4c0b1b5704..b118c470c9 100644 --- a/apps/dashboard/app/views/workflows/show.html.erb +++ b/apps/dashboard/app/views/workflows/show.html.erb @@ -6,13 +6,13 @@
-
+
<% hidden_class = @workflow.editable? ? '' : 'd-none' %> <%= select_tag "select_launcher", options_from_collection_for_select(@launchers, :id, :title), include_blank: false, class: "form-control w-25 #{hidden_class}" %> - - - - + + + +
diff --git a/apps/dashboard/config/locales/en.yml b/apps/dashboard/config/locales/en.yml index 62f2fc089c..218f254438 100644 --- a/apps/dashboard/config/locales/en.yml +++ b/apps/dashboard/config/locales/en.yml @@ -42,6 +42,8 @@ en: defaults instead. (%{error_message}) batch_connect_form_choose_template_name: Choose a template name batch_connect_form_data_root: data root directory + batch_connect_form_dynamic_warning: The following form may change dynamically + as options are selected. All changes will be announced and are reversible batch_connect_form_invalid: The form.yml has missing options in the %{id} form field. batch_connect_form_launch: Launch @@ -118,6 +120,22 @@ en: Your session is currently starting... Please be patient as this process can take a few minutes. batch_connect_sessions_word: Session + batch_connect_toggle_notifications: Toggle push notifications + batch_connect_vnc_client_connect_html: Open a VNC client and connect to localhost:%{port} + within the client + batch_connect_vnc_download_html: Download any VNC viewer, %{realvnc_link} is a + good option. + batch_connect_vnc_mac_connection_html: 'Use the code below to establish the VNC + connection:' + batch_connect_vnc_open_terminal: Open a terminal window + batch_connect_vnc_password_html: 'Use the VNC password: %{password}' + batch_connect_vnc_ssh_tunnel: 'Copy/paste in your terminal to establish the SSH + tunnel:' + batch_connect_vnc_ssh_tunnel_replace_host_html: 'Copy/paste in your terminal and + replace SSH_HOST with a valid HPC login server to establish the + SSH tunnel:' + batch_connect_vnc_windows_terminals_html: 'For terminals in Windows you can use: + %{pwsh}, %{putty} and %{wsl} distributions' bc_saved_settings: delete_confirm: Are you sure? delete_title: Delete %{settings_name} saved settings @@ -135,9 +153,6 @@ en: breadcrumbs_home: Home breadcrumbs_my_sessions: My Interactive Sessions breadcrumbs_support_ticket: Support Ticket - browser_warning: OnDemand requires a newer version of the browser you are using. - Current browser requirements include IE Edge, Firefox 19+, Chrome 34+, Safari - 8+. clear: Clear close: Close custom_pages: @@ -263,6 +278,10 @@ en: jobs_launchers_submitted: Successfully submitted job %{job_id}. jobs_launchers_updated: Launcher manifest updated! jobs_new_launcher: New Launcher + jobs_project_active_jobs: Active Jobs + jobs_project_back_to_projects: Back to Projects + jobs_project_clone_launcher: Clone launcher + jobs_project_completed_jobs: Completed Jobs jobs_project_created: Project successfully created! jobs_project_delete_project_confirmation: Delete all contents of project directory? jobs_project_deleted: Project successfully deleted! @@ -271,6 +290,7 @@ en: jobs_project_directory_help_html: 'Leave empty and a new directory will be created.
Default location: %{root_directory}' jobs_project_directory_placeholder: Project directory absolute path + jobs_project_edit_project_directory: Edit project directory jobs_project_generic_error: 'There was an error processing your request: %{error}' jobs_project_group_help: Make sure to choose the group that includes all intended collaborators. If this is not a collaborative project, the default group is @@ -284,22 +304,33 @@ en: jobs_project_job_deleted: Successfully deleted job %{job_id} jobs_project_job_not_deleted: Cannot delete job %{job_id} jobs_project_job_stopped: Successfully stopped job %{job_id} + jobs_project_manager: Project Manager jobs_project_manifest_updated: Project manifest updated! + jobs_project_message: Message jobs_project_name_placeholder: Project name jobs_project_name_validation: Project name may only contain letters, digits, dashes, underscores, and spaces + jobs_project_new: New Project jobs_project_not_found: Cannot find project %{project_id} jobs_project_removed: Project removed from lookup! + jobs_project_return_to_projects_page: Return to projects page jobs_project_save_error: Cannot save manifest to %{path} jobs_project_select_directory: Select project directory jobs_project_setgid_help: Leaving setgid checked ensures project files are created with the selected group. Does not affect projects in your home directory. jobs_project_validation_error: Invalid Request. Please review the errors below jobs_select_directory_placeholder: Click on project to import + jobs_workflow_add_launcher: Add Launcher + jobs_workflow_connect_launchers: Connect Launchers jobs_workflow_created: Workflow successfully created! jobs_workflow_delete_confirmation: Delete all contents of workflow? + jobs_workflow_delete_edge: Delete Edge + jobs_workflow_delete_launcher: Delete Launcher + jobs_workflow_delete_selected_edge: Delete selected edge (Del/Backspace) + jobs_workflow_delete_selected_launcher: Delete selected launcher (Del/Backspace) jobs_workflow_deleted: Workflow successfully deleted! jobs_workflow_description_placeholder: Workflow description + jobs_workflow_editing: 'Editing: %{name}' jobs_workflow_failed: Workflow failed with error %{error} jobs_workflow_help: Click title to select and drag. Connect by clicking two launchers with 'Connect Launchers' selected @@ -308,14 +339,21 @@ en: jobs_workflow_name_placeholder: Workflow name jobs_workflow_name_validation: Workflow name may only contain letters, digits, dashes, underscores, and spaces + jobs_workflow_new: New Workflow jobs_workflow_not_found: Cannot find workflow %{workflow_id} jobs_workflow_submitted: Workflow successfully submitted! jobs_workflows: Workflows launch: Launch - layout_title: Dashboard - %{title} + layout_title: + loading: Loading... logo_alt_text: Welcome to Open OnDemand mode: Mode + module_browser_copy: Copy + module_browser_dependencies: 'Dependencies:' module_browser_last_updated: 'Last updated: %{last_updated}' + module_browser_load_command: 'Load command:' + module_browser_none: None + module_browser_showing_results: Showing %{count} results module_browser_title: Module Browser motd_erb_render_error: 'MOTD was not parsed or rendered correctly: %{error_message}' motd_title: Message of the Day @@ -326,7 +364,9 @@ en: nav_develop_my_sandbox_apps_prod: My Shared Apps (Production) nav_develop_title: Develop nav_group_clusters: Clusters + nav_group_desktops: Desktops nav_group_files: Files + nav_group_guis: GUIs nav_group_interactive_apps: Interactive Apps nav_group_jobs: Jobs nav_help_change_password: Change HPC Password @@ -345,14 +385,21 @@ en: nsf_access_events: NSF ACCESS Events ok: OK owner: Owner + path_selector_caption: Choose a file or directory to use in the form. path_selector_default_popup_title: Select Your Working Directory + path_selector_favorites: Favorites path_selector_home: home path_selector_parent_directory: parent directory pinned_apps_caption_html: A featured subset of all available apps pinned_apps_category: Apps pinned_apps_title: Pinned Apps - powered_by_ood: Powered by Open OnDemand + products_app_details: App Details + products_dir_name: Directory Name + products_last_modified: Last Modified + products_launch_files: Launch Files + products_launch_shell: Launch Shell + products_new_app: New App project: Project project_balances: Project Balances project_zip_error_message: 'Error creating ZIP file: %{error}' @@ -399,7 +446,6 @@ en: skip_navigation: Skip Navigation soft_tabs: Soft Tabs soft_tabs_info: Soft tabs are spaces in the file instead of actual tab characters. - software_license: Software License Notice submit: Submit support_ticket: creation_success: 'Support ticket email sent to: %{to}' @@ -425,14 +471,15 @@ en: attachments_js: Max attachment size is %{max}. Selected file size is %{size} validation_error: Invalid Request. Please review the error messages below system_apps_caption: System Installed App - toggle_navigation: Toggle navigation + system_status_in_use: "%{percent} in use" + system_status_jobs_queued: Jobs Queued + system_status_jobs_running: Jobs Running type: Type unknown: Unknown uppy: Uppy user_configuration: support_ticket_error: support_ticket is misconfigured. Please add a backend implementation in the configuration YAML. - version: 'OnDemand version: %{version}' welcome_html: | %{logo_img_tag}

OnDemand provides an integrated, single access point for all of your HPC resources.

diff --git a/apps/dashboard/config/locales/zh-CN.yml b/apps/dashboard/config/locales/zh-CN.yml index 71fb3d953b..212036c94f 100644 --- a/apps/dashboard/config/locales/zh-CN.yml +++ b/apps/dashboard/config/locales/zh-CN.yml @@ -39,6 +39,7 @@ zh-CN: batch_connect_form_attr_cache_error: 无法使用先前缓存的值,改为使用默认值。(%{error_message}) batch_connect_form_choose_template_name: 选择模版名称 batch_connect_form_data_root: 数据根目录 + batch_connect_form_dynamic_warning: 以下表单可能会随着选项的选择而动态更改。所有更改都将被播报且可逆转 batch_connect_form_invalid: form.yml 中的 %{id} 表单字段缺少选项。 batch_connect_form_launch: 启动 batch_connect_form_prefill: 使用模版预填写 @@ -95,6 +96,17 @@ zh-CN: batch_connect_sessions_status_queued: 由于您的工作目前正在排队,请耐心等待。 等待时间取决于内核数以及请求的时间。 batch_connect_sessions_status_starting: 您的会话目前正在启动...请耐心等待,因为此过程可能需要几分钟。 batch_connect_sessions_word: 会话 + batch_connect_toggle_notifications: 切换推送通知 + batch_connect_vnc_client_connect_html: 打开 VNC 客户端并在客户端内连接到 localhost:%{port} + batch_connect_vnc_download_html: 下载任何 VNC 客户端,%{realvnc_link} 是一个不错的选择。 + batch_connect_vnc_mac_connection_html: 使用以下代码建立 VNC 连接: + batch_connect_vnc_open_terminal: 打开一个终端窗口 + batch_connect_vnc_password_html: 使用 VNC 密码:%{password} + batch_connect_vnc_ssh_tunnel: 在终端中复制/粘贴以建立 SSH 隧道: + batch_connect_vnc_ssh_tunnel_replace_host_html: 在终端中复制/粘贴,并将 SSH_HOST + 替换为有效的 HPC 登录服务器,以建立 SSH 隧道: + batch_connect_vnc_windows_terminals_html: 对于 Windows 终端,您可以使用:%{pwsh}、%{putty} + 和 %{wsl} 发行版 bc_saved_settings: delete_confirm: 你确定吗? delete_title: 删除 %{settings_name} 保存的设置 @@ -225,6 +237,10 @@ zh-CN: jobs_launchers_submitted: 成功提交作业 %{job_id}。 jobs_launchers_updated: 启动器清单已更新! jobs_new_launcher: 新启动器 + jobs_project_active_jobs: 活动作业 + jobs_project_back_to_projects: 返回项目 + jobs_project_clone_launcher: 克隆启动器 + jobs_project_completed_jobs: 已完成作业 jobs_project_created: 项目成功创建! jobs_project_delete_project_confirmation: 是否删除项目目录的所有内容? jobs_project_deleted: 项目成功删除! @@ -232,6 +248,7 @@ zh-CN: jobs_project_directory_error: 此工作流的项目目录路径未设置 jobs_project_directory_help_html: 留空将创建一个新目录。
默认位置: %{root_directory} jobs_project_directory_placeholder: 项目目录绝对路径 + jobs_project_edit_project_directory: 编辑项目目录 jobs_project_generic_error: 处理您的请求时出错:%{error} jobs_project_group_help: 确保选择包含所有预期合作者的组。如果这不是一个协作项目,建议使用默认组。 jobs_project_group_owner: 组 @@ -241,34 +258,52 @@ zh-CN: jobs_project_job_deleted: 成功删除作业 %{job_id} jobs_project_job_not_deleted: 无法删除作业 %{job_id} jobs_project_job_stopped: 成功停止作业 %{job_id} + jobs_project_manager: 项目管理器 jobs_project_manifest_updated: 项目清单已更新! + jobs_project_message: 消息 jobs_project_name_placeholder: 项目名称 jobs_project_name_validation: 项目名称只能包含字母、数字、短横线、下划线和空格 + jobs_project_new: 新建项目 jobs_project_not_found: 找不到项目 %{project_id} jobs_project_removed: 项目已从查找中移除! + jobs_project_return_to_projects_page: 返回项目页面 jobs_project_save_error: 无法保存清单到 %{path} jobs_project_select_directory: 选择项目目录 jobs_project_setgid_help: 保持 setgid 选中状态可确保项目文件以所选组创建。不会影响您主目录中的项目。 jobs_project_validation_error: 无效请求。请查看以下错误 jobs_select_directory_placeholder: 单击下面的项目以导入到您的主页(可选) + jobs_workflow_add_launcher: 添加启动器 + jobs_workflow_connect_launchers: 连接启动器 jobs_workflow_created: 工作流创建成功! jobs_workflow_delete_confirmation: 删除工作流的所有内容? + jobs_workflow_delete_edge: 删除边 + jobs_workflow_delete_launcher: 删除启动器 + jobs_workflow_delete_selected_edge: 删除选定的边 (Del/Backspace) + jobs_workflow_delete_selected_launcher: 删除选定的启动器 (Del/Backspace) jobs_workflow_deleted: 工作流删除成功! jobs_workflow_description_placeholder: 工作流描述 + jobs_workflow_editing: 编辑中:%{name} jobs_workflow_failed: 工作流失败,错误 %{error} jobs_workflow_help: 点击标题以选择并拖动。通过点击两个选中的发射器并选择“连接发射器”来连接 jobs_workflow_manifest_updated: 工作流清单已更新! jobs_workflow_missing_launchers: 至少选择 %{count} jobs_workflow_name_placeholder: 工作流名称 jobs_workflow_name_validation: 工作流名称只能包含字母、数字、短横线、下划线和空格 + jobs_workflow_new: 新建工作流 jobs_workflow_not_found: 找不到工作流 %{workflow_id} jobs_workflow_submitted: 工作流提交成功! jobs_workflows: 工作流 launch: 启动 layout_title: 控制面板 - %{title} + loading: 加载中... logo_alt_text: 欢迎使用 Open OnDemand mode: 模式 + module_browser_copy: 复制 + module_browser_dependencies: 依赖项: module_browser_last_updated: 最后更新:%{last_updated} + module_browser_load_command: 加载命令: + module_browser_none: 无 + module_browser_showing_results: 显示 %{count} 个结果 module_browser_title: 模块浏览器 motd_erb_render_error: MOTD 未正确解析或渲染:%{error_message} motd_title: 每日信息 @@ -279,7 +314,9 @@ zh-CN: nav_develop_my_sandbox_apps_prod: 我的沙盒应用 (Production) nav_develop_title: 开发 nav_group_clusters: 集群 + nav_group_desktops: 桌面系统 nav_group_files: 文件 + nav_group_guis: 图形界面应用 nav_group_interactive_apps: 交互式应用 nav_group_jobs: 作业 nav_help_change_password: 修改HPC密码 @@ -298,13 +335,21 @@ zh-CN: nsf_access_events: NSF 访问事件 ok: 确定 owner: 所有者 + path_selector_caption: 选择在表单中使用的文件或目录。 path_selector_default_popup_title: 选择您的工作目录 + path_selector_favorites: 收藏夹 path_selector_home: 主页 path_selector_parent_directory: 父目录 pinned_apps_caption_html: 一个精选的所有可用应用子集 pinned_apps_category: 应用 pinned_apps_title: 固定应用 powered_by_ood: 由 Open OnDemand 提供支持 + products_app_details: 应用详情 + products_dir_name: 目录名称 + products_last_modified: 最后修改时间 + products_launch_files: 启动文件 + products_launch_shell: 启动 Shell + products_new_app: 新建应用 project: 项目 project_balances: 项目余额 project_zip_error_message: 创建 ZIP 文件时出错:%{error} @@ -370,6 +415,9 @@ zh-CN: attachments_js: 最大附件大小为 %{max}。所选文件大小为 %{size} validation_error: 无效请求。请查看下面的错误信息 system_apps_caption: 系统已安装应用 + system_status_in_use: "%{percent} 使用中" + system_status_jobs_queued: 排队中的作业 + system_status_jobs_running: 正在运行的作业 toggle_navigation: 切换导航栏 type: 类型 unknown: 未知 From e882d0b76b1730dc29b1cb9bd06183e1da47887c Mon Sep 17 00:00:00 2001 From: Simon Xiao Date: Tue, 21 Apr 2026 13:13:46 -0400 Subject: [PATCH 06/12] Fix active jobs datatables i18n initialiation expose global datatables language config from Rails i18n and apply it in application JS so the table is auto-initialized with the correct locale configs. Also localized remaining hard coded strings in this page. --- .../app/controllers/active_jobs_controller.rb | 10 ++-- apps/dashboard/app/javascript/active_jobs.js | 47 +++++++++------ apps/dashboard/app/javascript/application.js | 22 +++++++ .../active_jobs/_extended_data_table.html.erb | 11 ++-- .../app/views/active_jobs/index.html.erb | 46 ++++++++++----- .../app/views/layouts/_config.html.erb | 1 + apps/dashboard/config/locales/en.yml | 59 +++++++++++++++++++ apps/dashboard/config/locales/zh-CN.yml | 59 +++++++++++++++++++ 8 files changed, 212 insertions(+), 43 deletions(-) diff --git a/apps/dashboard/app/controllers/active_jobs_controller.rb b/apps/dashboard/app/controllers/active_jobs_controller.rb index 9672513198..6748ef35de 100644 --- a/apps/dashboard/app/controllers/active_jobs_controller.rb +++ b/apps/dashboard/app/controllers/active_jobs_controller.rb @@ -58,12 +58,12 @@ def delete_job # It takes a couple of seconds for the job to clear out # Using the sleep to wait before reload sleep(2.0) - redirect_to active_jobs_path, :notice => "Successfully deleted #{job_id}" + redirect_to active_jobs_path, :notice => t('dashboard.active_jobs.delete_success', job_id: job_id, default: "Successfully deleted %{job_id}") rescue StandardError - redirect_to active_jobs_path, :alert => "Failed to delete #{job_id}" + redirect_to active_jobs_path, :alert => t('dashboard.active_jobs.delete_failure_with_id', job_id: job_id, default: "Failed to delete %{job_id}") end else - redirect_to active_jobs_path, :alert => 'Failed to delete.' + redirect_to active_jobs_path, :alert => t('dashboard.active_jobs.delete_failure_generic', default: 'Failed to delete.') end end @@ -82,12 +82,12 @@ def get_job(jobid, cluster) ActiveJobs::Jobstatusdata.new(data, cluster, true) rescue OodCore::JobAdapterError - OpenStruct.new(name: jobid, error: 'No job details because job has already left the queue.', + OpenStruct.new(name: jobid, error: t('dashboard.active_jobs.job_details_left_queue', default: 'No job details because job has already left the queue.'), status: 'completed') rescue StandardError => e Rails.logger.info("#{e}:#{e.message}") Rails.logger.info(e.backtrace.join("\n")) - OpenStruct.new(name: jobid, error: "No job details available.\n#{e.backtrace}", status: '') + OpenStruct.new(name: jobid, error: "#{t('dashboard.active_jobs.job_details_unavailable', default: 'No job details available.')}\n#{e.backtrace}", status: '') end # Returns the filter id from the parameter if it is valid diff --git a/apps/dashboard/app/javascript/active_jobs.js b/apps/dashboard/app/javascript/active_jobs.js index a028e0c91a..5376234b37 100644 --- a/apps/dashboard/app/javascript/active_jobs.js +++ b/apps/dashboard/app/javascript/active_jobs.js @@ -84,7 +84,7 @@ function fetch_job_data(tr, row, options) { fetch(jobDataUrl, { headers: { 'Accpet': 'application/json', }}) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error('Login failed: IDP redirect failed'))) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error('Request failed'))) .then(response => response.json()) .then(response => { const ele = document.getElementById('job_details'); @@ -122,13 +122,13 @@ function fetch_table_data(table, options){ } }).fail(function(errorReport){ if(errorReport.statusCode != null){ - show_errors(["Request for jobs failed with status code: " + errorReport.statusCode]); + show_errors([activeJobsConfig.errorStatusCode + errorReport.statusCode]); } else{ //FIXME: this error appears even when the above 404 occurs, for example // that is because a 404 response for json request returns a plain text response // and parsing that as json fails - show_errors(["Request for jobs failed due to body parsing error."]) + show_errors([activeJobsConfig.errorParsing]); } table.processing(false); @@ -138,11 +138,11 @@ function fetch_table_data(table, options){ function status_label(status){ const labelClass = cssBadgeForState(status); - var label = "Undetermined" + var label = activeJobsConfig.labelUndetermined; if(status === "queued_held") { - label = "Hold"; - } else { + label = activeJobsConfig.labelHold; + } else if(status && status !== "undetermined") { label = capitalizeFirstLetter(status); } @@ -158,12 +158,19 @@ function create_datatable(options){ $("#" + filter_id).addClass("active"); var table = $('#job_status_table').DataTable({ autoWidth: true, // Automatically calculate column width - "lengthMenu": [ [10, 25, 50, -1], [10, 25, 50, "All"] ], // Manually set size of particular columns + // Values only. DataTables 2 uses language.lengthLabels for the -1 label. + "lengthMenu": [10, 25, 50, -1], "bStateSave": true, // Save user selected table state "aaSorting": [], // Turn off auto sort. "pageLength": 50, // Set the number of rows - "oLanguage": { - "sSearch": "Filter: " + "language": { + "search": activeJobsConfig.searchFilter, + "emptyTable": activeJobsConfig.emptyTable, + "info": activeJobsConfig.info, + "infoEmpty": activeJobsConfig.infoEmpty, + "infoFiltered": activeJobsConfig.infoFiltered, + "lengthMenu": activeJobsConfig.lengthMenu, + "zeroRecords": activeJobsConfig.zeroRecords }, "fnInitComplete": function( oSettings ) { for ( var i=0, iLen=oSettings.aoData.length ; i`; + let ariaLabel = activeJobsConfig.toggleVisibility.replace('%{jobname}', escapeHtml(jobname)).replace('%{cluster_title}', cluster_title); + return ``; }, }, { @@ -269,14 +277,15 @@ function create_datatable(options){ const support_url = new URL(support_path, document.location); support_url.searchParams.set("job_id", pbsid); support_url.searchParams.set("cluster", cluster); + let ariaSupport = activeJobsConfig.submitTicketAria.replace('%{pbsid}', pbsid); support_ticket = ` @@ -288,17 +297,19 @@ function create_datatable(options){ // This will be empty when support ticket is disabled. return `
${support_ticket}
`; } else { + let confirmText = activeJobsConfig.deleteConfirm.replace('%{jobname}', escapeHtml(jobname)).replace('%{pbsid}', pbsid); + let ariaDelete = activeJobsConfig.deleteAria.replace('%{jobname}', escapeHtml(jobname)).replace('%{pbsid}', pbsid); return `
@@ -311,7 +322,7 @@ function create_datatable(options){ ] }).on( 'error.dt', function ( e, settings, techNote, message ) { // Event is fired when there's an error loading or parsing the datatable. - show_errors(['There was an error getting data from the remote server.']); + show_errors([activeJobsConfig.errorRemote]); } ); // Override the Datatables default error message functionality diff --git a/apps/dashboard/app/javascript/application.js b/apps/dashboard/app/javascript/application.js index fc663208f8..b51f9220a1 100644 --- a/apps/dashboard/app/javascript/application.js +++ b/apps/dashboard/app/javascript/application.js @@ -42,6 +42,28 @@ import initPopovers from './popovers' window.jQuery = jQuery; window.$ = jQuery; +function setGlobalDatatablesLanguageDefaults() { + const configEl = document.getElementById('ood_config'); + if (!configEl) return; + + const raw = configEl.dataset.datatablesLanguage; + if (!raw) return; + + let language; + try { + language = JSON.parse(raw); + } catch (_e) { + // If malformed, avoid breaking the whole app JS. + return; + } + + if (!jQuery?.fn?.dataTable?.defaults) return; + + jQuery.extend(true, jQuery.fn.dataTable.defaults, { language }); +} + +setGlobalDatatablesLanguageDefaults(); + Rails.start(); jQuery(function(){ diff --git a/apps/dashboard/app/views/active_jobs/_extended_data_table.html.erb b/apps/dashboard/app/views/active_jobs/_extended_data_table.html.erb index 842596fd1a..af9e27daf6 100644 --- a/apps/dashboard/app/views/active_jobs/_extended_data_table.html.erb +++ b/apps/dashboard/app/views/active_jobs/_extended_data_table.html.erb @@ -22,26 +22,25 @@ <% end %>
- <% if data.submit_args %>
-

Submit Args:

<%= data.submit_args %>

+

<%= t('dashboard.active_jobs.submit_args', default: 'Submit Args:') %>

<%= data.submit_args %>

<% end %> <% if data.output_path %>
<%# FIXME: can use form element input with label and disabled %> -

Output Location:

<%= data.output_path %>

+

<%= t('dashboard.active_jobs.output_location', default: 'Output Location:') %>

<%= data.output_path %>

<% end %>
<% if CurrentUser.name == data.username %> <% if Configuration.can_access_files? %> - <%= link_to("#{fa_icon("folder-open", classes: nil)} Open in File Manager".html_safe, data.file_explorer_url, :class => "btn btn-outline-dark m-1") if data.file_explorer_url %> + <%= link_to("#{fa_icon('folder-open', classes: nil)} #{t('dashboard.active_jobs.open_in_file_manager', default: 'Open in File Manager')}".html_safe, data.file_explorer_url, :class => "btn btn-outline-dark m-1") if data.file_explorer_url %> <% end %> <% if Configuration.ood_bc_ssh_to_compute_node && Configuration.can_access_shell? %> - <%= link_to("#{fa_icon("terminal", classes: nil)} Open in Terminal".html_safe, data.shell_url, :class => "btn btn-outline-dark m-1") if data.shell_url %> + <%= link_to("#{fa_icon('terminal', classes: nil)} #{t('dashboard.active_jobs.open_in_terminal', default: 'Open in Terminal')}".html_safe, data.shell_url, :class => "btn btn-outline-dark m-1") if data.shell_url %> <% end %> - <%= link_to("#{fa_icon("trash", classes: nil)} Delete".html_safe, delete_job_path(pbsid: data.pbsid, cluster: data.cluster), :class => "btn btn-outline-danger pull-right m-1", data: { method: "delete", confirm: "Are you sure you want to delete #{data.pbsid}" }) %> + <%= link_to("#{fa_icon('trash', classes: nil)} #{t('dashboard.active_jobs.delete', default: 'Delete')}".html_safe, delete_job_path(pbsid: data.pbsid, cluster: data.cluster), :class => "btn btn-outline-danger pull-right m-1", data: { method: "delete", confirm: t('dashboard.active_jobs.delete_confirm', jobname: data.jobname || data.pbsid, pbsid: data.pbsid, default: "Are you sure you want to delete %{jobname} - %{pbsid}") }) %> <% end %>
diff --git a/apps/dashboard/app/views/active_jobs/index.html.erb b/apps/dashboard/app/views/active_jobs/index.html.erb index 6b61a9c76b..ea4f509f3b 100644 --- a/apps/dashboard/app/views/active_jobs/index.html.erb +++ b/apps/dashboard/app/views/active_jobs/index.html.erb @@ -9,7 +9,7 @@ <% filters.each do |filter| %> <%= '
  • '.html_safe if filter == filters.last %>
  • - <%= filter.title %> + <%= t("dashboard.active_jobs.filter_#{filter.filter_id}", default: filter.title) %>
  • <% end %> @@ -26,8 +26,8 @@ <% end %>
  • -
  • - All Clusters +
  • + <%= t('dashboard.active_jobs.all_clusters', default: 'All Clusters') %>
  • @@ -42,20 +42,20 @@
    -

    Active Jobs

    +

    <%= t('dashboard.active_jobs.title', default: 'Active Jobs') %>

    Jobs RunningJobs Queued<%= t('dashboard.system_status_jobs_running') %><%= t('dashboard.system_status_jobs_queued') %>
    - - - - - - - - - - + + + + + + + + + +
    DetailsIDNameUserAccountTime UsedQueueStatusClusterActions<%= t('dashboard.active_jobs.header_details', default: 'Details') %><%= t('dashboard.active_jobs.header_id', default: 'ID') %><%= t('dashboard.active_jobs.header_name', default: 'Name') %><%= t('dashboard.active_jobs.header_user', default: 'User') %><%= t('dashboard.active_jobs.header_account', default: 'Account') %><%= t('dashboard.active_jobs.header_time_used', default: 'Time Used') %><%= t('dashboard.active_jobs.header_queue', default: 'Queue') %><%= t('dashboard.active_jobs.header_status', default: 'Status') %><%= t('dashboard.active_jobs.header_cluster', default: 'Cluster') %><%= t('dashboard.active_jobs.header_actions', default: 'Actions') %>
    @@ -70,6 +70,24 @@ data-console-log-performance-report='<%= Configuration.console_log_performance_report? %>' data-base-uri='<%= controller.relative_url_root %>' data-ood-version='<%= Configuration.ood_version %>' + data-label-hold='<%= t("dashboard.active_jobs.label_hold", default: "Hold") %>' + data-label-undetermined='<%= t("dashboard.active_jobs.label_undetermined", default: "Undetermined") %>' + data-error-status-code='<%= t("dashboard.active_jobs.error_status_code", default: "Request for jobs failed with status code: ") %>' + data-error-parsing='<%= t("dashboard.active_jobs.error_parsing", default: "Request for jobs failed due to body parsing error.") %>' + data-error-remote='<%= t("dashboard.active_jobs.error_remote", default: "There was an error getting data from the remote server.") %>' + data-search-filter='<%= t("dashboard.active_jobs.search_filter", default: "Filter: ") %>' + data-empty-table='<%= t("dashboard.active_jobs.empty_table", default: "No data available in table") %>' + data-info='<%= t("dashboard.active_jobs.info", default: "Showing _START_ to _END_ of _TOTAL_ entries") %>' + data-info-empty='<%= t("dashboard.active_jobs.info_empty", default: "Showing 0 to 0 of 0 entries") %>' + data-info-filtered='<%= t("dashboard.active_jobs.info_filtered", default: "(filtered from _MAX_ total entries)") %>' + data-length-menu='<%= t("dashboard.active_jobs.length_menu", default: "_MENU_ entries per page") %>' + data-zero-records='<%= t("dashboard.active_jobs.zero_records", default: "No matching records found") %>' + data-toggle-visibility='<%= t("dashboard.active_jobs.toggle_visibility", default: "Toggle visibility of job %{jobname} on %{cluster_title}") %>' + data-submit-ticket-title='<%= t("dashboard.active_jobs.submit_ticket_title", default: "Submit Support Ticket") %>' + data-submit-ticket-aria='<%= t("dashboard.active_jobs.submit_ticket_aria", default: "Submit support ticket for job with ID %{pbsid}") %>' + data-delete-confirm='<%= t("dashboard.active_jobs.delete_confirm", default: "Are you sure you want to delete %{jobname} - %{pbsid}") %>' + data-delete-aria='<%= t("dashboard.active_jobs.delete_aria", default: "Delete job %{jobname} with ID %{pbsid}") %>' + data-delete-title='<%= t("dashboard.active_jobs.delete_title", default: "Delete Job") %>' > diff --git a/apps/dashboard/app/views/layouts/_config.html.erb b/apps/dashboard/app/views/layouts/_config.html.erb index 364564865f..25ffb1f90e 100644 --- a/apps/dashboard/app/views/layouts/_config.html.erb +++ b/apps/dashboard/app/views/layouts/_config.html.erb @@ -5,6 +5,7 @@ data-transfers-path="<%= transfers_path(format: "json") if respond_to?(:transfers_path) %>" data-root-path="<%= root_path %>" data-uppy-locale="<%= I18n.t('dashboard.uppy', :default => {}).to_json %>" + data-datatables-language="<%= I18n.t('dashboard.datatables', default: {}).to_json %>" data-bc-dynamic-js="<%= Configuration.bc_dynamic_js? %>" data-xdmod-url="<%= Configuration.xdmod_host %>" data-base-analytics-path="<%= analytics_path('delme').gsub(/[\/]*delme[\/]*/,'') %>" diff --git a/apps/dashboard/config/locales/en.yml b/apps/dashboard/config/locales/en.yml index 218f254438..e0f1be0fe0 100644 --- a/apps/dashboard/config/locales/en.yml +++ b/apps/dashboard/config/locales/en.yml @@ -1,4 +1,7 @@ en: + "Active Jobs": "Active Jobs" + "Job Composer": "Job Composer" + "Project Manager": "Project Manager" activemodel: errors: messages: @@ -7,6 +10,62 @@ en: required: "%{attribute} is required" used: "%{attribute} is already used" dashboard: + datatables: + search: "Filter: " + emptyTable: "No data available in table" + info: "Showing _START_ to _END_ of _TOTAL_ entries" + infoEmpty: "Showing 0 to 0 of 0 entries" + infoFiltered: "(filtered from _MAX_ total entries)" + lengthMenu: "_MENU_ entries per page" + zeroRecords: "No matching records found" + entries: + 1: "entry" + _: "entries" + lengthLabels: + "-1": "All" + active_jobs: + title: "Active Jobs" + header_details: "Details" + header_id: "ID" + header_name: "Name" + header_user: "User" + header_account: "Account" + header_time_used: "Time Used" + header_queue: "Queue" + header_status: "Status" + header_cluster: "Cluster" + header_actions: "Actions" + all_clusters: "All Clusters" + label_hold: "Hold" + label_undetermined: "Undetermined" + error_status_code: "Request for jobs failed with status code: " + error_parsing: "Request for jobs failed due to body parsing error." + error_remote: "There was an error getting data from the remote server." + search_filter: "Filter: " + toggle_visibility: "Toggle visibility of job %{jobname} on %{cluster_title}" + submit_ticket_title: "Submit Support Ticket" + submit_ticket_aria: "Submit support ticket for job with ID %{pbsid}" + delete_confirm: "Are you sure you want to delete %{jobname} - %{pbsid}" + delete_aria: "Delete job %{jobname} with ID %{pbsid}" + delete_title: "Delete Job" + delete_success: "Successfully deleted %{job_id}" + delete_failure_with_id: "Failed to delete %{job_id}" + delete_failure_generic: "Failed to delete." + job_details_left_queue: "No job details because job has already left the queue." + job_details_unavailable: "No job details available." + submit_args: "Submit Args:" + output_location: "Output Location:" + open_in_file_manager: "Open in File Manager" + open_in_terminal: "Open in Terminal" + delete: "Delete" + filter_user: "Your Jobs" + filter_all: "All Jobs" + empty_table: "No data available in table" + info: "Showing _START_ to _END_ of _TOTAL_ entries" + info_empty: "Showing 0 to 0 of 0 entries" + info_filtered: "(filtered from _MAX_ total entries)" + length_menu: "_MENU_ entries per page" + zero_records: "No matching records found" active_sessions_caption_html: view all (%{number_of_sessions}) active_sessions_title: Active interactive sessions add: Add diff --git a/apps/dashboard/config/locales/zh-CN.yml b/apps/dashboard/config/locales/zh-CN.yml index 212036c94f..0a3c6d615b 100644 --- a/apps/dashboard/config/locales/zh-CN.yml +++ b/apps/dashboard/config/locales/zh-CN.yml @@ -1,4 +1,7 @@ zh-CN: + "Active Jobs": "活动作业" + "Job Composer": "作业编辑器" + "Project Manager": "项目管理器" activemodel: errors: messages: @@ -7,6 +10,62 @@ zh-CN: required: "%{attribute} 是必需的" used: "%{attribute} 已被使用" dashboard: + datatables: + search: "筛选: " + emptyTable: "表中无数据可用" + info: "显示第 _START_ 至 _END_ 项,共 _TOTAL_ 项" + infoEmpty: "显示第 0 至 0 项,共 0 项" + infoFiltered: "(从 _MAX_ 项中过滤)" + lengthMenu: "每页显示 _MENU_ 项" + zeroRecords: "没有找到匹配的记录" + entries: + 1: "条" + _: "条" + lengthLabels: + "-1": "全部" + active_jobs: + title: "活动作业" + header_details: "详情" + header_id: "ID" + header_name: "名称" + header_user: "用户" + header_account: "帐户" + header_time_used: "使用时间" + header_queue: "队列" + header_status: "状态" + header_cluster: "集群" + header_actions: "操作" + all_clusters: "所有集群" + label_hold: "保留" + label_undetermined: "未定" + error_status_code: "请求作业失败,状态码:" + error_parsing: "请求作业失败:正文解析错误。" + error_remote: "从远程服务器获取数据时出错。" + search_filter: "筛选: " + toggle_visibility: "切换在 %{cluster_title} 上的作业 %{jobname} 的可见性" + submit_ticket_title: "提交支持票" + submit_ticket_aria: "为 ID 为 %{pbsid} 的作业提交支持票" + delete_confirm: "您确定要删除 %{jobname} - %{pbsid} 吗?" + delete_aria: "删除 ID 为 %{pbsid} 的作业 %{jobname}" + delete_title: "删除作业" + delete_success: "已成功删除 %{job_id}" + delete_failure_with_id: "删除 %{job_id} 失败" + delete_failure_generic: "删除失败。" + job_details_left_queue: "无法获取作业详情,因为该作业已离开队列。" + job_details_unavailable: "无法获取作业详情。" + submit_args: "提交参数:" + output_location: "输出位置:" + open_in_file_manager: "在文件管理器中打开" + open_in_terminal: "在终端中打开" + delete: "删除" + filter_user: "您的作业" + filter_all: "所有作业" + empty_table: "表中无数据可用" + info: "显示第 _START_ 至 _END_ 项,共 _TOTAL_ 项" + info_empty: "显示第 0 至 0 项,共 0 项" + info_filtered: "(从 _MAX_ 项中过滤)" + length_menu: "每页显示 _MENU_ 项" + zero_records: "没有找到匹配的记录" active_sessions_caption_html: 查看全部 (%{number_of_sessions}) active_sessions_title: 活动交互会话 add: 添加 From fd142f72f8cbaa54846e13d5b5f5ab887b2ab74c Mon Sep 17 00:00:00 2001 From: Simon Xiao Date: Tue, 21 Apr 2026 13:48:47 -0400 Subject: [PATCH 07/12] localize module browser page Replaced hard coded strings with I18n and added corresponding string entries in en.yml file --- apps/dashboard/app/javascript/module_browser.js | 13 ++++++++++--- .../app/views/module_browser/_module_list.html.erb | 7 +++++-- .../app/views/module_browser/_toolbar.html.erb | 8 ++++---- .../views/module_browser/_versions_table.html.erb | 6 +++--- .../app/views/module_browser/index.html.erb | 8 ++++++++ apps/dashboard/config/locales/en.yml | 10 ++++++++++ apps/dashboard/config/locales/zh-CN.yml | 10 ++++++++++ 7 files changed, 50 insertions(+), 12 deletions(-) diff --git a/apps/dashboard/app/javascript/module_browser.js b/apps/dashboard/app/javascript/module_browser.js index 6da9e53ef7..fdd0aa04c3 100644 --- a/apps/dashboard/app/javascript/module_browser.js +++ b/apps/dashboard/app/javascript/module_browser.js @@ -2,6 +2,13 @@ import { debounce } from 'lodash'; import { hide, show } from './utils.js'; document.addEventListener('DOMContentLoaded', function () { + const i18nElem = document.getElementById('module_browser_i18n'); + const i18n = { + copyText: i18nElem?.dataset?.copyText || 'Copy', + copiedText: i18nElem?.dataset?.copiedText || 'Copied!', + showingResultsTemplate: i18nElem?.dataset?.showingResultsTemplate || 'Showing %{count} results', + }; + document.querySelectorAll('[data-name]').forEach(card => { const versions = card.querySelectorAll("[data-role='selectable-version']"); const infoBox = card.querySelector("[data-role='module-info']"); @@ -97,8 +104,8 @@ document.addEventListener('DOMContentLoaded', function () { const text = target.textContent; navigator.clipboard.writeText(text) .then(() => { - button.textContent = 'Copied!'; - setTimeout(() => button.textContent = 'Copy', 2000); + button.textContent = i18n.copiedText; + setTimeout(() => (button.textContent = i18n.copyText), 2000); }) .catch(err => { console.error('Clipboard write failed:', err); @@ -154,7 +161,7 @@ document.addEventListener('DOMContentLoaded', function () { // Update visible module count const resultsCountElem = document.getElementById('module_results_count'); if (resultsCountElem) { - resultsCountElem.textContent = `Showing ${resultsCount} results`; + resultsCountElem.textContent = i18n.showingResultsTemplate.replace('%{count}', resultsCount); } } diff --git a/apps/dashboard/app/views/module_browser/_module_list.html.erb b/apps/dashboard/app/views/module_browser/_module_list.html.erb index 4c3647d886..6cf2dc55fe 100644 --- a/apps/dashboard/app/views/module_browser/_module_list.html.erb +++ b/apps/dashboard/app/views/module_browser/_module_list.html.erb @@ -18,7 +18,10 @@
    -
    Available modules: (select version)
    +
    + <%= t('dashboard.module_browser_available_modules') %> + (<%= t('dashboard.module_browser_select_version_hint') %>) +
    <%= render 'versions_table', versions: versions, clusters: versions.map(&:cluster).uniq %>
    @@ -52,7 +55,7 @@ class="btn btn-outline-secondary btn-sm ms-2" data-role="copy-btn" data-clipboard-target="#module_info_<%= name %> [data-role='module-load-command']" - aria-label="Copy load command for <%= name %>" + aria-label="<%= t('dashboard.module_browser_copy_load_command_aria', module: name) %>" > <%= t('dashboard.module_browser_copy') %> diff --git a/apps/dashboard/app/views/module_browser/_toolbar.html.erb b/apps/dashboard/app/views/module_browser/_toolbar.html.erb index 58f38b0bc6..2407c5a2a1 100644 --- a/apps/dashboard/app/views/module_browser/_toolbar.html.erb +++ b/apps/dashboard/app/views/module_browser/_toolbar.html.erb @@ -1,12 +1,12 @@
    - - + +
    - +