Merge pull request #342 from TriliumNext/feature/i18n-part2

i18n support (part 2)
This commit is contained in:
Elian Doran 2024-08-26 21:24:56 +03:00 committed by GitHub
commit ef955a300a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1507 additions and 1246 deletions

View File

@ -6,8 +6,10 @@
<option name="TAB_SIZE" value="2" /> <option name="TAB_SIZE" value="2" />
</value> </value>
</option> </option>
<JSCodeStyleSettings version="0"> <codeStyleSettings language="JSON">
<option name="USE_EXPLICIT_JS_EXTENSION" value="TRUE" /> <indentOptions>
</JSCodeStyleSettings> <option name="INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme> </code_scheme>
</component> </component>

View File

@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = ` const TPL = `
@ -32,7 +33,7 @@ const TPL = `
} }
</style> </style>
<div class="bx bx-x close-api-log-button" title="Close"></div> <div class="bx bx-x close-api-log-button" title="${t('api_log.close')}"></div>
<div class="api-log-container"></div> <div class="api-log-container"></div>
</div>`; </div>`;

View File

@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import AttachmentActionsWidget from "./buttons/attachments_actions.js"; import AttachmentActionsWidget from "./buttons/attachments_actions.js";
import BasicWidget from "./basic_widget.js"; import BasicWidget from "./basic_widget.js";
@ -153,19 +154,19 @@ export default class AttachmentDetailWidget extends BasicWidget {
$deletionWarning.show(); $deletionWarning.show();
if (willBeDeletedInMs >= 60000) { if (willBeDeletedInMs >= 60000) {
$deletionWarning.text(`This attachment will be automatically deleted in ${utils.formatTimeInterval(willBeDeletedInMs)}`); $deletionWarning.text(t('attachment_detail_2.will_be_deleted_in', { time: utils.formatTimeInterval(willBeDeletedInMs) }));
} else { } else {
$deletionWarning.text(`This attachment will be automatically deleted soon`); $deletionWarning.text(t('attachment_detail_2.will_be_deleted_soon'));
} }
$deletionWarning.append(", because the attachment is not linked in the note's content. To prevent deletion, add the attachment link back into the content or convert the attachment into note."); $deletionWarning.append(t('attachment_detail_2.deletion_reason'));
} else { } else {
this.$wrapper.removeClass("scheduled-for-deletion"); this.$wrapper.removeClass("scheduled-for-deletion");
$deletionWarning.hide(); $deletionWarning.hide();
} }
this.$wrapper.find('.attachment-details') this.$wrapper.find('.attachment-details')
.text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`); .text(t('attachment_detail_2.role_and_size', { role: this.attachment.role, size: utils.formatSize(this.attachment.contentLength) }));
this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render()); this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render());
const {$renderedContent} = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail }); const {$renderedContent} = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail });
@ -186,9 +187,9 @@ export default class AttachmentDetailWidget extends BasicWidget {
utils.copyHtmlToClipboard($link[0].outerHTML); utils.copyHtmlToClipboard($link[0].outerHTML);
toastService.showMessage("Attachment link copied to clipboard."); toastService.showMessage(t('attachment_detail_2.link_copied'));
} else { } else {
throw new Error(`Unrecognized attachment role '${this.attachment.role}'.`); throw new Error(t('attachment_detail_2.unrecognized_role', { role: this.attachment.role }));
} }
} }

View File

@ -1,6 +1,7 @@
import SwitchWidget from "./switch.js"; import SwitchWidget from "./switch.js";
import server from "../services/server.js"; import server from "../services/server.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
export default class BookmarkSwitchWidget extends SwitchWidget { export default class BookmarkSwitchWidget extends SwitchWidget {
isEnabled() { isEnabled() {
@ -12,11 +13,11 @@ export default class BookmarkSwitchWidget extends SwitchWidget {
doRender() { doRender() {
super.doRender(); super.doRender();
this.$switchOnName.text("Bookmark"); this.$switchOnName.text(t("bookmark_switch.bookmark"));
this.$switchOnButton.attr("title", "Bookmark this note to the left side panel"); this.$switchOnButton.attr("title", t("bookmark_switch.bookmark_this_note"));
this.$switchOffName.text("Bookmark"); this.$switchOffName.text(t("bookmark_switch.bookmark"));
this.$switchOffButton.attr("title", "Remove bookmark"); this.$switchOffButton.attr("title", t("bookmark_switch.remove_bookmark"));
} }
async toggle(state) { async toggle(state) {

View File

@ -1,5 +1,6 @@
import attributeService from '../services/attributes.js'; import attributeService from '../services/attributes.js';
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { t } from "../services/i18n.js";
const TPL = ` const TPL = `
<div class="dropdown editability-select-widget"> <div class="dropdown editability-select-widget">
@ -15,24 +16,24 @@ const TPL = `
} }
</style> </style>
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle editability-button"> <button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle editability-button">
<span class="editability-active-desc">auto</span> <span class="editability-active-desc">${t("editability_select.auto")}</span>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<div class="editability-dropdown dropdown-menu dropdown-menu-right"> <div class="editability-dropdown dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" data-editability="auto"> <a class="dropdown-item" href="#" data-editability="auto">
<span class="check">&check;</span> <span class="check">&check;</span>
Auto ${t("editability_select.auto")}
<div>Note is editable if it's not too long.</div> <div>${t("editability_select.note_is_editable")}</div>
</a> </a>
<a class="dropdown-item" href="#" data-editability="readOnly"> <a class="dropdown-item" href="#" data-editability="readOnly">
<span class="check">&check;</span> <span class="check">&check;</span>
Read-only ${t("editability_select.read_only")}
<div>Note is read-only, but can be edited with a button click.</div> <div>${t("editability_select.note_is_read_only")}</div>
</a> </a>
<a class="dropdown-item" href="#" data-editability="autoReadOnlyDisabled"> <a class="dropdown-item" href="#" data-editability="autoReadOnlyDisabled">
<span class="check">&check;</span> <span class="check">&check;</span>
Always editable ${t("editability_select.always_editable")}
<div>Note is always editable, regardless of its length.</div> <div>${t("editability_select.note_is_always_editable")}</div>
</a> </a>
</div> </div>
</div> </div>
@ -46,20 +47,20 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
this.$widget.on('click', '.dropdown-item', this.$widget.on('click', '.dropdown-item',
async e => { async e => {
this.$widget.find('.dropdown-toggle').dropdown('toggle'); this.$widget.find('.dropdown-toggle').dropdown('toggle');
const editability = $(e.target).closest("[data-editability]").attr("data-editability"); const editability = $(e.target).closest("[data-editability]").attr("data-editability");
for (const ownedAttr of this.note.getOwnedLabels()) { for (const ownedAttr of this.note.getOwnedLabels()) {
if (['readOnly', 'autoReadOnlyDisabled'].includes(ownedAttr.name)) { if (['readOnly', 'autoReadOnlyDisabled'].includes(ownedAttr.name)) {
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId); await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
}
} }
}
if (editability !== 'auto') { if (editability !== 'auto') {
await attributeService.addLabel(this.noteId, editability); await attributeService.addLabel(this.noteId, editability);
} }
}); });
} }
async refreshWithNote(note) { async refreshWithNote(note) {
@ -73,9 +74,9 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
} }
const labels = { const labels = {
"auto": "Auto", "auto": t("editability_select.auto"),
"readOnly": "Read-only", "readOnly": t("editability_select.read_only"),
"autoReadOnlyDisabled": "Always Editable" "autoReadOnlyDisabled": t("editability_select.always_editable")
} }
this.$widget.find('.dropdown-item').removeClass("selected"); this.$widget.find('.dropdown-item').removeClass("selected");

View File

@ -1,45 +1,46 @@
import { t } from "../../../services/i18n.js";
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsWidget from "./options_widget.js"; import OptionsWidget from "./options_widget.js";
const TPL = ` const TPL = `
<div class="options-section"> <div class="options-section">
<h4>Automatic backup</h4> <h4>${t('backup.automatic_backup')}</h4>
<p>Trilium can back up the database automatically:</p> <p>${t('backup.automatic_backup_description')}</p>
<ul style="list-style: none"> <ul style="list-style: none">
<li> <li>
<label> <label>
<input type="checkbox" class="daily-backup-enabled"> <input type="checkbox" class="daily-backup-enabled">
Enable daily backup ${t('backup.enable_daily_backup')}
</label> </label>
</li> </li>
<li> <li>
<label> <label>
<input type="checkbox" class="weekly-backup-enabled"> <input type="checkbox" class="weekly-backup-enabled">
Enable weekly backup ${t('backup.enable_weekly_backup')}
</label> </label>
</li> </li>
<li> <li>
<label> <label>
<input type="checkbox" class="monthly-backup-enabled"> <input type="checkbox" class="monthly-backup-enabled">
Enable monthly backup ${t('backup.enable_monthly_backup')}
</label> </label>
</li> </li>
</ul> </ul>
<p>It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.</p> <p>${t('backup.backup_recommendation')}</p>
</div> </div>
<div class="options-section"> <div class="options-section">
<h4>Backup now</h4> <h4>${t('backup.backup_now')}</h4>
<button class="backup-database-button btn">Backup database now</button> <button class="backup-database-button btn">${t('backup.backup_database_now')}</button>
</div> </div>
<div class="options-section"> <div class="options-section">
<h4>Existing backups</h4> <h4>${t('backup.existing_backups')}</h4>
<ul class="existing-backup-list"></ul> <ul class="existing-backup-list"></ul>
</div> </div>
@ -54,7 +55,7 @@ export default class BackupOptions extends OptionsWidget {
this.$backupDatabaseButton.on('click', async () => { this.$backupDatabaseButton.on('click', async () => {
const {backupFile} = await server.post('database/backup-database'); const {backupFile} = await server.post('database/backup-database');
toastService.showMessage(`Database has been backed up to ${backupFile}`, 10000); toastService.showMessage(`${t('backup.database_backed_up_to')} ${backupFile}`, 10000);
this.refresh(); this.refresh();
}); });
@ -84,7 +85,7 @@ export default class BackupOptions extends OptionsWidget {
this.$existingBackupList.empty(); this.$existingBackupList.empty();
if (!backupFiles.length) { if (!backupFiles.length) {
backupFiles = [{filePath: "no backup yet", mtime: ''}]; backupFiles = [{filePath: t('backup.no_backup_yet'), mtime: ''}];
} }
for (const {filePath, mtime} of backupFiles) { for (const {filePath, mtime} of backupFiles) {

View File

@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import dialogService from "../../../services/dialog.js"; import dialogService from "../../../services/dialog.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
@ -5,24 +6,24 @@ import OptionsWidget from "./options_widget.js";
const TPL = ` const TPL = `
<div class="options-section"> <div class="options-section">
<h4>ETAPI</h4> <h4>${t("etapi.title")}</h4>
<p>ETAPI is a REST API used to access Trilium instance programmatically, without UI. <br/> <p>${t("etapi.description")} <br/>
See more details on <a href="https://triliumnext.github.io/Docs/Wiki/etapi.html">wiki</a> and <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">ETAPI OpenAPI spec</a>.</p> ${t("etapi.see_more")} <a href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a> ${t("etapi.and")} <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>.</p>
<button type="button" class="create-etapi-token btn btn-sm">Create new ETAPI token</button> <button type="button" class="create-etapi-token btn btn-sm">${t("etapi.create_token")}</button>
<h5>Existing tokens</h5> <h5>${t("etapi.existing_tokens")}</h5>
<div class="no-tokens-yet">There are no tokens yet. Click on the button above to create one.</div> <div class="no-tokens-yet">${t("etapi.no_tokens_yet")}</div>
<div style="overflow: auto; height: 500px;"> <div style="overflow: auto; height: 500px;">
<table class="tokens-table table table-stripped"> <table class="tokens-table table table-stripped">
<thead> <thead>
<tr> <tr>
<th>Token name</th> <th>${t("etapi.token_name")}</th>
<th>Created</th> <th>${t("etapi.created")}</th>
<th>Actions</th> <th>${t("etapi.actions")}</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -52,21 +53,21 @@ export default class EtapiOptions extends OptionsWidget {
this.$widget.find(".create-etapi-token").on("click", async () => { this.$widget.find(".create-etapi-token").on("click", async () => {
const tokenName = await dialogService.prompt({ const tokenName = await dialogService.prompt({
title: "New ETAPI token", title: t("etapi.new_token_title"),
message: "Please enter new token's name", message: t("etapi.new_token_message"),
defaultValue: "new token" defaultValue: t("etapi.default_token_name")
}); });
if (!tokenName.trim()) { if (!tokenName.trim()) {
toastService.showError("Token name can't be empty"); toastService.showError(t("etapi.error_empty_name"));
return; return;
} }
const {authToken} = await server.post('etapi-tokens', {tokenName}); const {authToken} = await server.post('etapi-tokens', {tokenName});
await dialogService.prompt({ await dialogService.prompt({
title: "ETAPI token created", title: t("etapi.token_created_title"),
message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.', message: t("etapi.token_created_message"),
defaultValue: authToken defaultValue: authToken
}); });
@ -94,9 +95,9 @@ export default class EtapiOptions extends OptionsWidget {
.append($("<td>").text(token.name)) .append($("<td>").text(token.name))
.append($("<td>").text(token.utcDateCreated)) .append($("<td>").text(token.utcDateCreated))
.append($("<td>").append( .append($("<td>").append(
$('<span class="bx bx-pen token-table-button" title="Rename this token"></span>') $('<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>')
.on("click", () => this.renameToken(token.etapiTokenId, token.name)), .on("click", () => this.renameToken(token.etapiTokenId, token.name)),
$('<span class="bx bx-trash token-table-button" title="Delete / deactivate this token"></span>') $('<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>')
.on("click", () => this.deleteToken(token.etapiTokenId, token.name)) .on("click", () => this.deleteToken(token.etapiTokenId, token.name))
)) ))
); );
@ -105,8 +106,8 @@ export default class EtapiOptions extends OptionsWidget {
async renameToken(etapiTokenId, oldName) { async renameToken(etapiTokenId, oldName) {
const tokenName = await dialogService.prompt({ const tokenName = await dialogService.prompt({
title: "Rename token", title: t("etapi.rename_token_title"),
message: "Please enter new token's name", message: t("etapi.rename_token_message"),
defaultValue: oldName defaultValue: oldName
}); });
@ -120,7 +121,7 @@ export default class EtapiOptions extends OptionsWidget {
} }
async deleteToken(etapiTokenId, name) { async deleteToken(etapiTokenId, name) {
if (!await dialogService.confirm(`Are you sure you want to delete ETAPI token "${name}"?`)) { if (!await dialogService.confirm(t("etapi.delete_token_confirmation", { name }))) {
return; return;
} }

View File

@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import NoteContextAwareWidget from "../../note_context_aware_widget.js"; import NoteContextAwareWidget from "../../note_context_aware_widget.js";
@ -24,8 +25,8 @@ export default class OptionsWidget extends NoteContextAwareWidget {
showUpdateNotification() { showUpdateNotification() {
toastService.showPersistent({ toastService.showPersistent({
id: "options-change-saved", id: "options-change-saved",
title: "Options status", title: t("options_widget.options_status"),
message: "Options change have been saved.", message: t("options_widget.options_change_saved"),
icon: "slider", icon: "slider",
closeAfter: 2000 closeAfter: 2000
}); });

View File

@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import protectedSessionHolder from "../../../services/protected_session_holder.js"; import protectedSessionHolder from "../../../services/protected_session_holder.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
@ -5,42 +6,39 @@ import OptionsWidget from "./options_widget.js";
const TPL = ` const TPL = `
<div class="options-section"> <div class="options-section">
<h4 class="password-heading"></h4> <h4 class="password-heading">${t("password.heading")}</h4>
<div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;"> <div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
Please take care to remember your new password. Password is used for logging into the web interface and ${t("password.alert_message")} <a class="reset-password-button" href="javascript:">${t("password.reset_link")}</a>
to encrypt protected notes. If you forget your password, then all your protected notes are forever lost.
In case you did forget your password, <a class="reset-password-button" href="javascript:">click here to reset it</a>.
</div> </div>
<form class="change-password-form"> <form class="change-password-form">
<div class="old-password-form-group form-group"> <div class="old-password-form-group form-group">
<label>Old password</label> <label>${t("password.old_password")}</label>
<input class="old-password form-control" type="password"> <input class="old-password form-control" type="password">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>New password</label> <label>${t("password.new_password")}</label>
<input class="new-password1 form-control" type="password"> <input class="new-password1 form-control" type="password">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>New password confirmation</label> <label>${t("password.new_password_confirmation")}</label>
<input class="new-password2 form-control" type="password"> <input class="new-password2 form-control" type="password">
</div> </div>
<button class="save-password-button btn btn-primary">Change password</button> <button class="save-password-button btn btn-primary">${t("password.change_password")}</button>
</form> </form>
</div> </div>
<div class="options-section"> <div class="options-section">
<h4>Protected Session Timeout</h4> <h4>${t("password.protected_session_timeout")}</h4>
<p>Protected session timeout is a time period after which the protected session is wiped from <p>${t("password.protected_session_timeout_description")} <a href="https://triliumnext.github.io/Docs/Wiki/protected-notes.html" class="external">${t("password.wiki")}</a> ${t("password.for_more_info")}</p>
the browser's memory. This is measured from the last interaction with protected notes. See <a href="https://triliumnext.github.io/Docs/Wiki/protected-notes.html" class="external">wiki</a> for more info.</p>
<div class="form-group"> <div class="form-group">
<label>Protected session timeout (in seconds)</label> <label>${t("password.protected_session_timeout_label")}</label>
<input class="protected-session-timeout-in-seconds form-control options-number-input" type="number" min="60"> <input class="protected-session-timeout-in-seconds form-control options-number-input" type="number" min="60">
</div> </div>
</div>`; </div>`;
@ -58,13 +56,13 @@ export default class PasswordOptions extends OptionsWidget {
this.$resetPasswordButton = this.$widget.find(".reset-password-button"); this.$resetPasswordButton = this.$widget.find(".reset-password-button");
this.$resetPasswordButton.on("click", async () => { this.$resetPasswordButton.on("click", async () => {
if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) { if (confirm(t("password.reset_confirmation"))) {
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes"); await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
const options = await server.get('options'); const options = await server.get('options');
this.optionsLoaded(options); this.optionsLoaded(options);
toastService.showError("Password has been reset. Please set new password"); toastService.showError(t("password.reset_success_message"));
} }
}); });
@ -79,8 +77,8 @@ export default class PasswordOptions extends OptionsWidget {
const isPasswordSet = options.isPasswordSet === 'true'; const isPasswordSet = options.isPasswordSet === 'true';
this.$widget.find(".old-password-form-group").toggle(isPasswordSet); this.$widget.find(".old-password-form-group").toggle(isPasswordSet);
this.$passwordHeading.text(isPasswordSet ? 'Change Password' : 'Set Password'); this.$passwordHeading.text(isPasswordSet ? t("password.change_password_heading") : t("password.set_password_heading"));
this.$savePasswordButton.text(isPasswordSet ? 'Change Password' : 'Set Password'); this.$savePasswordButton.text(isPasswordSet ? t("password.change_password") : t("password.set_password"));
this.$protectedSessionTimeout.val(options.protectedSessionTimeout); this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
} }
@ -94,7 +92,7 @@ export default class PasswordOptions extends OptionsWidget {
this.$newPassword2.val(''); this.$newPassword2.val('');
if (newPassword1 !== newPassword2) { if (newPassword1 !== newPassword2) {
toastService.showError("New passwords are not the same."); toastService.showError(t("password.password_mismatch"));
return false; return false;
} }
@ -103,7 +101,7 @@ export default class PasswordOptions extends OptionsWidget {
'new_password': newPassword1 'new_password': newPassword1
}).then(result => { }).then(result => {
if (result.success) { if (result.success) {
toastService.showError("Password has been changed. Trilium will be reloaded after you press OK."); toastService.showError(t("password.password_changed_success"));
// password changed so current protected session is invalid and needs to be cleared // password changed so current protected session is invalid and needs to be cleared
protectedSessionHolder.resetProtectedSession(); protectedSessionHolder.resetProtectedSession();

View File

@ -2,6 +2,7 @@ import server from "../../../services/server.js";
import utils from "../../../services/utils.js"; import utils from "../../../services/utils.js";
import dialogService from "../../../services/dialog.js"; import dialogService from "../../../services/dialog.js";
import OptionsWidget from "./options_widget.js"; import OptionsWidget from "./options_widget.js";
import { t } from "../../../services/i18n.js";
const TPL = ` const TPL = `
<div class="options-section shortcuts-options-section"> <div class="options-section shortcuts-options-section">
@ -25,25 +26,25 @@ const TPL = `
} }
</style> </style>
<h4>Keyboard Shortcuts</h4> <h4>${t('shortcuts.keyboard_shortcuts')}</h4>
<p> <p>
Multiple shortcuts for the same action can be separated by comma. ${t('shortcuts.multiple_shortcuts')}
See <a href="https://www.electronjs.org/docs/latest/api/accelerator">Electron documentation</a> for available modifiers and key codes. ${t('shortcuts.electron_documentation')}
</p> </p>
<div class="form-group"> <div class="form-group">
<input type="text" class="keyboard-shortcut-filter form-control" placeholder="Type text to filter shortcuts..."> <input type="text" class="keyboard-shortcut-filter form-control" placeholder="${t('shortcuts.type_text_to_filter')}">
</div> </div>
<div class="shortcuts-table-container"> <div class="shortcuts-table-container">
<table class="keyboard-shortcut-table" cellpadding="10"> <table class="keyboard-shortcut-table" cellpadding="10">
<thead> <thead>
<tr> <tr class="text-nowrap">
<th>Action name</th> <th>${t('shortcuts.action_name')}</th>
<th>Shortcuts</th> <th>${t('shortcuts.shortcuts')}</th>
<th>Default shortcuts</th> <th>${t('shortcuts.default_shortcuts')}</th>
<th>Description</th> <th>${t('shortcuts.description')}</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -51,9 +52,9 @@ const TPL = `
</div> </div>
<div class="shortcuts-options-buttons"> <div class="shortcuts-options-buttons">
<button class="options-keyboard-shortcuts-reload-app btn btn-primary">Reload app to apply changes</button> <button class="options-keyboard-shortcuts-reload-app btn btn-primary">${t('shortcuts.reload_app')}</button>
<button class="options-keyboard-shortcuts-set-all-to-default btn">Set all shortcuts to the default</button> <button class="options-keyboard-shortcuts-set-all-to-default btn">${t('shortcuts.set_all_to_default')}</button>
</div> </div>
</div>`; </div>`;
@ -83,10 +84,10 @@ export default class KeyboardShortcutsOptions extends OptionsWidget {
else { else {
$tr.append($("<td>").text(action.actionName)) $tr.append($("<td>").text(action.actionName))
.append($("<td>").append( .append($("<td>").append(
$(`<input type="text" class="form-control">`) $(`<input type="text" class="form-control">`)
.val(action.effectiveShortcuts.join(", ")) .val(action.effectiveShortcuts.join(", "))
.attr('data-keyboard-action-name', action.actionName) .attr('data-keyboard-action-name', action.actionName)
.attr('data-default-keyboard-shortcuts', action.defaultShortcuts.join(", ")) .attr('data-default-keyboard-shortcuts', action.defaultShortcuts.join(", "))
) )
) )
.append($("<td>").text(action.defaultShortcuts.join(", "))) .append($("<td>").text(action.defaultShortcuts.join(", ")))
@ -101,10 +102,10 @@ export default class KeyboardShortcutsOptions extends OptionsWidget {
const $input = this.$widget.find(e.target); const $input = this.$widget.find(e.target);
const actionName = $input.attr('data-keyboard-action-name'); const actionName = $input.attr('data-keyboard-action-name');
const shortcuts = $input.val() const shortcuts = $input.val()
.replace('+,', "+Comma") .replace('+,', "+Comma")
.split(",") .split(",")
.map(shortcut => shortcut.replace("+Comma", "+,")) .map(shortcut => shortcut.replace("+Comma", "+,"))
.filter(shortcut => !!shortcut); .filter(shortcut => !!shortcut);
const optionName = `keyboardShortcuts${actionName.substr(0, 1).toUpperCase()}${actionName.substr(1)}`; const optionName = `keyboardShortcuts${actionName.substr(0, 1).toUpperCase()}${actionName.substr(1)}`;
@ -112,7 +113,7 @@ export default class KeyboardShortcutsOptions extends OptionsWidget {
}); });
this.$widget.find(".options-keyboard-shortcuts-set-all-to-default").on('click', async () => { this.$widget.find(".options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
if (!await dialogService.confirm("Do you really want to reset all keyboard shortcuts to the default?")) { if (!await dialogService.confirm(t('shortcuts.confirm_reset'))) {
return; return;
} }
@ -152,7 +153,7 @@ export default class KeyboardShortcutsOptions extends OptionsWidget {
return; return;
} }
this.$widget.find(el).toggle(!!( // !! to avoid toggle overloads with different behavior this.$widget.find(el).toggle(!!(
action.actionName.toLowerCase().includes(filter) action.actionName.toLowerCase().includes(filter)
|| action.defaultShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter)) || action.defaultShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|| action.effectiveShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter)) || action.effectiveShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))

View File

@ -1,35 +1,36 @@
import utils from "../../../services/utils.js"; import utils from "../../../services/utils.js";
import OptionsWidget from "./options_widget.js"; import OptionsWidget from "./options_widget.js";
import { t } from "../../../services/i18n.js";
const TPL_WEB = ` const TPL_WEB = `
<div class="options-section"> <div class="options-section">
<h4>Spell Check</h4> <h4>${t('spellcheck.title')}</h4>
<p>These options apply only for desktop builds, browsers will use their own native spell check.</p> <p>${t('spellcheck.description')}</p>
</div> </div>
`; `;
const TPL_ELECTRON = ` const TPL_ELECTRON = `
<div class="options-section"> <div class="options-section">
<h4>Spell Check</h4> <h4>${t('spellcheck.title')}</h4>
<p>App restart is required after change.</p> <p>App restart is required after change.</p>
<label> <label>
<input type="checkbox" class="spell-check-enabled"> <input type="checkbox" class="spell-check-enabled">
Enable spellcheck ${t('spellcheck.enable')}
</label> </label>
<br/> <br/>
<div class="form-group"> <div class="form-group">
<label>Language code(s)</label> <label>${t('spellcheck.language_code_label')}</label>
<input type="text" class="spell-check-language-code form-control" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;"> <input type="text" class="spell-check-language-code form-control" placeholder="${t('spellcheck.language_code_placeholder')}">
</div> </div>
<p>Multiple languages can be separated by comma, e.g. <code>en-US, de-DE, cs</code>. Changes to the spell check options will take effect after application restart.</p> <p>${t('spellcheck.multiple_languages_info')}</p>
<p><strong>Available language codes: </strong> <span class="available-language-codes"></span></p> <p><strong>${t('spellcheck.available_language_codes_label')} </strong> <span class="available-language-codes"></span></p>
</div>`; </div>`;
export default class SpellcheckOptions extends OptionsWidget { export default class SpellcheckOptions extends OptionsWidget {

View File

@ -1,44 +1,45 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsWidget from "./options_widget.js"; import OptionsWidget from "./options_widget.js";
import { t } from "../../../services/i18n.js";
const TPL = ` const TPL = `
<div class="options-section"> <div class="options-section">
<h4 style="margin-top: 0px;">Sync Configuration</h4> <h4 style="margin-top: 0px;">${t('sync_2.config_title')}</h4>
<form class="sync-setup-form"> <form class="sync-setup-form">
<div class="form-group"> <div class="form-group">
<label>Server instance address</label> <label>${t('sync_2.server_address')}</label>
<input class="sync-server-host form-control" placeholder="https://<host>:<port>"> <input class="sync-server-host form-control" placeholder="https://<host>:<port>">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Sync timeout (milliseconds)</label> <label>${t('sync_2.timeout')}</label>
<input class="sync-server-timeout form-control" min="1" max="10000000" type="number" style="text-align: left;"> <input class="sync-server-timeout form-control" min="1" max="10000000" type="number" style="text-align: left;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Sync proxy server (optional)</label> <label>${t('sync_2.proxy_label')}</label>
<input class="sync-proxy form-control" placeholder="https://<host>:<port>"> <input class="sync-proxy form-control" placeholder="https://<host>:<port>">
<p><strong>Note:</strong> If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only).</p> <p><strong>${t('sync_2.note')}:</strong> ${t('sync_2.note_description')}</p>
<p>Another special value is <code>noproxy</code> which forces ignoring even the system proxy and respectes <code>NODE_TLS_REJECT_UNAUTHORIZED</code>.</p> <p>${t('sync_2.special_value_description')}</p>
</div> </div>
<div style="display: flex; justify-content: space-between;"> <div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button> <button class="btn btn-primary">${t('sync_2.save')}</button>
<button class="btn" type="button" data-help-page="synchronization.html">Help</button> <button class="btn" type="button" data-help-page="synchronization.html">${t('sync_2.help')}</button>
</div> </div>
</form> </form>
</div> </div>
<div class="options-section"> <div class="options-section">
<h4>Sync Test</h4> <h4>${t('sync_2.test_title')}</h4>
<p>This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.</p> <p>${t('sync_2.test_description')}</p>
<button class="test-sync-button btn">Test sync</button> <button class="test-sync-button btn">${t('sync_2.test_button')}</button>
</div>`; </div>`;
export default class SyncOptions extends OptionsWidget { export default class SyncOptions extends OptionsWidget {
@ -58,9 +59,8 @@ export default class SyncOptions extends OptionsWidget {
if (result.success) { if (result.success) {
toastService.showMessage(result.message); toastService.showMessage(result.message);
} } else {
else { toastService.showError(t('sync_2.handshake_failed', { message: result.message }));
toastService.showError(`Sync server handshake failed, error: ${result.message}`);
} }
}); });
} }

View File

@ -1114,5 +1114,133 @@
"title": "自动只读大小", "title": "自动只读大小",
"description": "自动只读笔记大小是超过该大小后,笔记将以只读模式显示(出于性能考虑)。", "description": "自动只读笔记大小是超过该大小后,笔记将以只读模式显示(出于性能考虑)。",
"label": "自动只读大小(文本笔记)" "label": "自动只读大小(文本笔记)"
},
"i18n": {
"title": "本地化",
"language": "语言"
},
"backup": {
"automatic_backup": "自动备份",
"automatic_backup_description": "Trilium 可以自动备份数据库:",
"enable_daily_backup": "启用每日备份",
"enable_weekly_backup": "启用每周备份",
"enable_monthly_backup": "启用每月备份",
"backup_recommendation": "建议打开备份功能,但这可能会使大型数据库和/或慢速存储设备的应用程序启动变慢。",
"backup_now": "立即备份",
"backup_database_now": "立即备份数据库",
"existing_backups": "现有备份",
"database_backed_up_to": "数据库已备份到",
"no_backup_yet": "尚无备份"
},
"etapi": {
"title": "ETAPI",
"description": "ETAPI 是一个 REST API用于以编程方式访问 Trilium 实例,而无需 UI。",
"see_more": "更多详情见",
"wiki": "维基",
"and": "和",
"openapi_spec": "ETAPI OpenAPI 规范",
"create_token": "创建新的 ETAPI 令牌",
"existing_tokens": "现有令牌",
"no_tokens_yet": "目前还没有令牌。点击上面的按钮创建一个。",
"token_name": "令牌名称",
"created": "创建时间",
"actions": "操作",
"new_token_title": "新 ETAPI 令牌",
"new_token_message": "请输入新的令牌名称",
"default_token_name": "新令牌",
"error_empty_name": "令牌名称不能为空",
"token_created_title": "ETAPI 令牌已创建",
"token_created_message": "将创建的令牌复制到剪贴板。Trilium 存储了令牌的哈希值,这是你最后一次看到它。",
"rename_token": "重命名此令牌",
"delete_token": "删除/停用此令牌",
"rename_token_title": "重命名令牌",
"rename_token_message": "请输入新的令牌名称",
"delete_token_confirmation": "你确定要删除 ETAPI 令牌 \"{{name}}\" 吗?"
},
"options_widget": {
"options_status": "选项状态",
"options_change_saved": "选项更改已保存。"
},
"password": {
"heading": "密码",
"alert_message": "请务必记住您的新密码。密码用于登录 Web 界面和加密保护的笔记。如果您忘记了密码,所有保护的笔记将永久丢失。",
"reset_link": "点击这里重置。",
"old_password": "旧密码",
"new_password": "新密码",
"new_password_confirmation": "新密码确认",
"change_password": "更改密码",
"protected_session_timeout": "保护会话超时",
"protected_session_timeout_description": "保护会话超时是一个时间段,超时后保护会话会从浏览器内存中清除。这是从最后一次与保护笔记的交互开始计时的。更多信息请见",
"wiki": "维基",
"for_more_info": "更多信息。",
"protected_session_timeout_label": "保护会话超时(秒)",
"reset_confirmation": "重置密码将永久丧失对所有现受保护笔记的访问。您真的要重置密码吗?",
"reset_success_message": "密码已重置。请设置新密码",
"change_password_heading": "更改密码",
"set_password_heading": "设置密码",
"set_password": "设置密码",
"password_mismatch": "新密码不一致。",
"password_changed_success": "密码已更改。按 OK 后 Trilium 将重新加载。"
},
"shortcuts": {
"keyboard_shortcuts": "快捷键",
"multiple_shortcuts": "同一操作的多个快捷键可以用逗号分隔。",
"electron_documentation": "请参阅 <a href=\"https://www.electronjs.org/docs/latest/api/accelerator\">Electron文档</a>,了解可用的修饰符和键码。",
"type_text_to_filter": "输入文字以过滤快捷键...",
"action_name": "操作名称",
"shortcuts": "快捷键",
"default_shortcuts": "默认快捷键",
"description": "描述",
"reload_app": "重新加载应用以应用更改",
"set_all_to_default": "将所有快捷键重置为默认值",
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?"
},
"spellcheck": {
"title": "拼写检查",
"description": "这些选项仅适用于桌面版本,浏览器将使用其原生的拼写检查功能。更改后需要重启应用。",
"enable": "启用拼写检查",
"language_code_label": "语言代码",
"language_code_placeholder": "例如 \"en-US\", \"de-AT\"",
"multiple_languages_info": "多种语言可以用逗号分隔,例如 \"en-US, de-DE, cs\"。拼写检查选项的更改将在应用重启后生效。",
"available_language_codes_label": "可用的语言代码:"
},
"sync_2": {
"config_title": "同步配置",
"server_address": "服务器地址",
"timeout": "同步超时(单位:毫秒)",
"proxy_label": "同步代理服务器(可选)",
"note": "注意",
"note_description": "代理设置留空则使用系统代理(仅桌面客户端有效)。",
"special_value_description": "另一个特殊值是 <code>noproxy</code>,它强制忽略系统代理并遵守 <code>NODE_TLS_REJECT_UNAUTHORIZED</code>。",
"save": "保存",
"help": "帮助",
"test_title": "同步测试",
"test_description": "测试和同步服务器之间的连接。如果同步服务器没有初始化,会将本地文档同步到同步服务器上。",
"test_button": "测试同步",
"handshake_failed": "同步服务器握手失败,错误:{{message}}"
},
"api_log": {
"close": "关闭"
},
"attachment_detail_2": {
"will_be_deleted_in": "此附件将在 {{time}} 后自动删除",
"will_be_deleted_soon": "该附件将很快被自动删除",
"deletion_reason": ",因为该附件未链接在笔记的内容中。为防止被删除,请将附件链接重新添加到内容中或将附件转换为笔记。",
"role_and_size": "角色: {{role}}, 大小: {{size}}",
"link_copied": "附件链接已复制到剪贴板。",
"unrecognized_role": "无法识别的附件角色 '{{role}}'。"
},
"bookmark_switch": {
"bookmark": "书签",
"bookmark_this_note": "将此笔记添加到左侧面板的书签",
"remove_bookmark": "移除书签"
},
"editability_select": {
"auto": "自动",
"read_only": "只读",
"always_editable": "始终可编辑",
"note_is_editable": "笔记如果不太长则可编辑。",
"note_is_read_only": "笔记为只读,但可以通过点击按钮进行编辑。",
"note_is_always_editable": "无论笔记长度如何,始终可编辑。"
} }
} }

File diff suppressed because it is too large Load Diff