diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 9bf4329..2d35bb8 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -322,15 +322,16 @@ return Promise.reject(new Error('clipboard unavailable')); } - function showExternalFallback(entry, mode) { + function showExternalFallback(entry, mode, labels) { if (!entry) return; + labels = labels || {}; var pathToShow = entry.relativePath; if (mode === 'explorer' && entry.type !== 'folder') { pathToShow = parentPath(entry.relativePath) || entry.relativePath; } - var title = mode === 'explorer' ? 'Show in Explorer' : 'Open External'; - var message = title + ' failed.\nVault-relative path:\n' + pathToShow; - confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) { + var title = mode === 'explorer' ? (labels.showInFolder || 'Show in folder') : (labels.openExternal || 'Open externally'); + var message = (labels.externalFailed || '{action} failed.').replace('{action}', title) + '\n' + (labels.vaultRelativePath || 'Vault-relative path:') + '\n' + pathToShow; + confirmModal(message, { confirmText: labels.copyPath || 'Copy path', cancelText: labels.close || 'Close' }).then(function (copy) { if (!copy) return; copyTextToClipboard(pathToShow).catch(function (err) { console.error('[files] copy path failed:', err); @@ -1026,7 +1027,11 @@ function trashEntry(entry) { var count = selectedCount(); if (entry) { - confirmModal('Move "' + entry.name + '" to trash?', { danger: true }).then(function (ok) { + confirmModal(tr('ui.trashConfirm', { name: entry.name }, 'Move "' + entry.name + '" to trash?'), { + danger: true, + confirmText: tr('ui.trash', null, 'Move to trash'), + cancelText: tr('ui.cancel', null, 'Cancel') + }).then(function (ok) { if (!ok) return; api.files.trash(entry.relativePath).then(function () { loadEntries(); @@ -1035,7 +1040,11 @@ }); }); } else if (count > 1) { - confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) { + confirmModal(tr('ui.trashManyConfirm', { count: count }, 'Move ' + count + ' items to trash?'), { + danger: true, + confirmText: tr('ui.trash', null, 'Move to trash'), + cancelText: tr('ui.cancel', null, 'Cancel') + }).then(function (ok) { if (!ok) return; var paths = Object.keys(selectedPaths); Promise.allSettled(paths.map(function (p) { return api.files.trash(p); })).then(function () { @@ -1080,15 +1089,23 @@ function openExternalEntry(entry, mode) { if (!entry) return; + var fallbackLabels = { + openExternal: tr('ui.openExternal', null, 'Open externally'), + showInFolder: tr('ui.showInFolder', null, 'Show in folder'), + externalFailed: tr('ui.externalFailed', null, '{action} failed.'), + vaultRelativePath: tr('ui.vaultRelativePath', null, 'Vault-relative path:'), + copyPath: tr('ui.copyPath', null, 'Copy path'), + close: tr('ui.close', null, 'Close') + }; var filesApi = api && api.files; var action = mode === 'explorer' ? filesApi && filesApi.showInFolder : filesApi && filesApi.openExternal; if (typeof action !== 'function') { - showExternalFallback(entry, mode); + showExternalFallback(entry, mode, fallbackLabels); return; } action(entry.relativePath).catch(function (err) { console.warn('[verstak.files] external open error:', err); - showExternalFallback(entry, mode); + showExternalFallback(entry, mode, fallbackLabels); }); } @@ -1134,27 +1151,27 @@ renderList(); } var isFolder = entry.type === 'folder'; - ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open')); - ctxMenu.appendChild(ctxItem('Open External', '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external')); - ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer')); + ctxMenu.appendChild(ctxItem(isFolder ? tr('ui.openFolder', null, 'Open folder') : tr('ui.open', null, 'Open'), '', function () { openEntry(entry); }, 'open', 'open')); + ctxMenu.appendChild(ctxItem(tr('ui.openExternal', null, 'Open externally'), '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external')); + ctxMenu.appendChild(ctxItem(tr('ui.showInFolder', null, 'Show in folder'), '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer')); appendContributionMenuItems(entry); ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename')); + ctxMenu.appendChild(ctxItem(tr('ui.rename', null, 'Rename'), '', function () { beginRename(entry); }, 'rename', 'rename')); if (entry.type !== 'folder') { - ctxMenu.appendChild(ctxItem('Duplicate', '', function () { duplicateEntry(entry); }, 'duplicate', 'duplicate')); + ctxMenu.appendChild(ctxItem(tr('ui.duplicate', null, 'Duplicate'), '', function () { duplicateEntry(entry); }, 'duplicate', 'duplicate')); } ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Cut', '', function () { cutSelection(); }, 'cut', 'cut')); - ctxMenu.appendChild(ctxItem('Copy', '', function () { copySelection(); }, 'copy', 'copy')); + ctxMenu.appendChild(ctxItem(tr('ui.cut', null, 'Cut'), '', function () { cutSelection(); }, 'cut', 'cut')); + ctxMenu.appendChild(ctxItem(tr('ui.copy', null, 'Copy'), '', function () { copySelection(); }, 'copy', 'copy')); ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Move to Trash', 'danger', function () { trashEntry(); }, 'trash', 'trash')); + ctxMenu.appendChild(ctxItem(tr('ui.trash', null, 'Move to trash'), 'danger', function () { trashEntry(); }, 'trash', 'trash')); } else { - ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); }, 'new-folder', 'folderAdd')); - ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); }, 'new-markdown', 'markdownAdd')); - ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); }, 'new-text', 'textAdd')); + ctxMenu.appendChild(ctxItem(tr('ui.newFolder', null, 'New folder'), '', function () { startCreate('folder'); }, 'new-folder', 'folderAdd')); + ctxMenu.appendChild(ctxItem(tr('ui.newMarkdown', null, 'New Markdown file'), '', function () { startCreate('markdown'); }, 'new-markdown', 'markdownAdd')); + ctxMenu.appendChild(ctxItem(tr('ui.newText', null, 'New text file'), '', function () { startCreate('text'); }, 'new-text', 'textAdd')); if (window.__filesClipboard && window.__filesClipboard.items && window.__filesClipboard.items.length) { ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); }, 'paste', 'paste')); + ctxMenu.appendChild(ctxItem(tr('ui.paste', null, 'Paste'), '', function () { pasteEntry(); }, 'paste', 'paste')); } } ctxMenu.style.display = 'block'; @@ -1247,7 +1264,7 @@ var clip = window.__filesClipboard; if (!clip || !clip.items || clip.items.length === 0) return; if (clip.workspaceRoot && clip.workspaceRoot !== workspaceRoot) { - window.alert('Clipboard items belong to another workspace.'); + window.alert(tr('ui.clipboardDifferentDeal', null, 'Clipboard items belong to another Deal.')); return; } var destinationDir = scopedPath(currentPath); diff --git a/plugins/files/locales/en.json b/plugins/files/locales/en.json index 6ca34fd..289b9cf 100644 --- a/plugins/files/locales/en.json +++ b/plugins/files/locales/en.json @@ -10,7 +10,11 @@ "ui.newMarkdown": "New markdown file", "ui.newText": "New text file", "ui.open": "Open", + "ui.openFolder": "Open folder", + "ui.openExternal": "Open externally", + "ui.showInFolder": "Show in folder", "ui.rename": "Rename", + "ui.duplicate": "Duplicate", "ui.trash": "Move to trash", "ui.cut": "Cut", "ui.copy": "Copy", @@ -37,6 +41,13 @@ "ui.invalidName": "Enter a valid name.", "ui.nameDiffersOnlyByCase": "The name differs only by letter case.", "ui.nameConflict": "An item with that name already exists.", + "ui.externalFailed": "{action} failed.", + "ui.vaultRelativePath": "Vault-relative path:", + "ui.copyPath": "Copy path", + "ui.close": "Close", + "ui.trashConfirm": "Move \"{name}\" to trash?", + "ui.trashManyConfirm": "Move {count} items to trash?", + "ui.clipboardDifferentDeal": "Clipboard items belong to another Deal.", "ui.emptyFolder": "Empty folder", "ui.noMatches": "No matches", "ui.clearFilter": "Clear filter", diff --git a/plugins/files/locales/ru.json b/plugins/files/locales/ru.json index 5248060..faeeb3f 100644 --- a/plugins/files/locales/ru.json +++ b/plugins/files/locales/ru.json @@ -10,7 +10,11 @@ "ui.newMarkdown": "Новый Markdown-файл", "ui.newText": "Новый текстовый файл", "ui.open": "Открыть", + "ui.openFolder": "Открыть папку", + "ui.openExternal": "Открыть во внешнем приложении", + "ui.showInFolder": "Показать в папке", "ui.rename": "Переименовать", + "ui.duplicate": "Создать копию", "ui.trash": "Переместить в корзину", "ui.cut": "Вырезать", "ui.copy": "Копировать", @@ -37,6 +41,13 @@ "ui.invalidName": "Введите корректное название.", "ui.nameDiffersOnlyByCase": "Название отличается только регистром букв.", "ui.nameConflict": "Элемент с таким названием уже существует.", + "ui.externalFailed": "Не удалось выполнить действие «{action}».", + "ui.vaultRelativePath": "Путь относительно хранилища:", + "ui.copyPath": "Скопировать путь", + "ui.close": "Закрыть", + "ui.trashConfirm": "Переместить «{name}» в корзину?", + "ui.trashManyConfirm": "Переместить в корзину {count} элементов?", + "ui.clipboardDifferentDeal": "Элементы буфера обмена принадлежат другому Делу.", "ui.emptyFolder": "Папка пуста", "ui.noMatches": "Совпадений нет", "ui.clearFilter": "Сбросить фильтр", diff --git a/scripts/smoke-files-plugin.js b/scripts/smoke-files-plugin.js index 5a6cfef..ee6eb43 100755 --- a/scripts/smoke-files-plugin.js +++ b/scripts/smoke-files-plugin.js @@ -195,7 +195,7 @@ function loadFilesComponent(document) { return { component, clipboard: sandbox.navigator.clipboard }; } -function makeApi() { +function makeApi(options = {}) { const externalCalls = []; const contributionCalls = []; const created = []; @@ -317,6 +317,15 @@ function makeApi() { }; }, }, + i18n: { + t: (key, params, fallback) => { + let value = options.translations && options.translations[key] ? options.translations[key] : (fallback || key); + Object.entries(params || {}).forEach(([name, replacement]) => { + value = value.replace(`{${name}}`, String(replacement)); + }); + return value; + }, + }, showExternalFile() { externalVisible = true; }, @@ -444,6 +453,31 @@ async function flush() { throw new Error(`external file row not rendered after file.changed: ${container.textContent}`); } + const russianDocument = makeDocument(); + const { component: russianComponent } = loadFilesComponent(russianDocument); + const russianApi = makeApi({ + translations: { + 'ui.open': 'Открыть', + 'ui.openExternal': 'Открыть во внешнем приложении', + 'ui.showInFolder': 'Показать в папке', + 'ui.rename': 'Переименовать', + 'ui.duplicate': 'Создать копию', + 'ui.cut': 'Вырезать', + 'ui.copy': 'Копировать', + 'ui.trash': 'Переместить в корзину', + }, + }); + const russianContainer = new FakeNode('div'); + russianComponent.mount(russianContainer, {}, russianApi); + await flush(); + const russianList = walk(russianContainer, (node) => node.getAttribute && node.getAttribute('data-files-list') !== undefined); + const russianRow = walk(russianContainer, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/readme.md'); + russianList.dispatchEvent('contextmenu', { target: russianRow, clientX: 20, clientY: 20 }); + const russianMenuText = russianDocument.body.textContent; + ['Открыть во внешнем приложении', 'Показать в папке', 'Переименовать', 'Создать копию', 'Вырезать', 'Копировать', 'Переместить в корзину'].forEach((label) => { + if (!russianMenuText.includes(label)) throw new Error(`context menu label was not localized: ${label}`); + }); + console.log('files frontend smoke passed'); })().catch((err) => { console.error(err);