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" />
</value>
</option>
<JSCodeStyleSettings version="0">
<option name="USE_EXPLICIT_JS_EXTENSION" value="TRUE" />
</JSCodeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `
@ -32,7 +33,7 @@ const TPL = `
}
</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>`;

View File

@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import utils from "../services/utils.js";
import AttachmentActionsWidget from "./buttons/attachments_actions.js";
import BasicWidget from "./basic_widget.js";
@ -153,19 +154,19 @@ export default class AttachmentDetailWidget extends BasicWidget {
$deletionWarning.show();
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 {
$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 {
this.$wrapper.removeClass("scheduled-for-deletion");
$deletionWarning.hide();
}
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());
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);
toastService.showMessage("Attachment link copied to clipboard.");
toastService.showMessage(t('attachment_detail_2.link_copied'));
} 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 server from "../services/server.js";
import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
export default class BookmarkSwitchWidget extends SwitchWidget {
isEnabled() {
@ -12,11 +13,11 @@ export default class BookmarkSwitchWidget extends SwitchWidget {
doRender() {
super.doRender();
this.$switchOnName.text("Bookmark");
this.$switchOnButton.attr("title", "Bookmark this note to the left side panel");
this.$switchOnName.text(t("bookmark_switch.bookmark"));
this.$switchOnButton.attr("title", t("bookmark_switch.bookmark_this_note"));
this.$switchOffName.text("Bookmark");
this.$switchOffButton.attr("title", "Remove bookmark");
this.$switchOffName.text(t("bookmark_switch.bookmark"));
this.$switchOffButton.attr("title", t("bookmark_switch.remove_bookmark"));
}
async toggle(state) {

View File

@ -1,5 +1,6 @@
import attributeService from '../services/attributes.js';
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { t } from "../services/i18n.js";
const TPL = `
<div class="dropdown editability-select-widget">
@ -15,24 +16,24 @@ const TPL = `
}
</style>
<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>
</button>
<div class="editability-dropdown dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" data-editability="auto">
<span class="check">&check;</span>
Auto
<div>Note is editable if it's not too long.</div>
${t("editability_select.auto")}
<div>${t("editability_select.note_is_editable")}</div>
</a>
<a class="dropdown-item" href="#" data-editability="readOnly">
<span class="check">&check;</span>
Read-only
<div>Note is read-only, but can be edited with a button click.</div>
${t("editability_select.read_only")}
<div>${t("editability_select.note_is_read_only")}</div>
</a>
<a class="dropdown-item" href="#" data-editability="autoReadOnlyDisabled">
<span class="check">&check;</span>
Always editable
<div>Note is always editable, regardless of its length.</div>
${t("editability_select.always_editable")}
<div>${t("editability_select.note_is_always_editable")}</div>
</a>
</div>
</div>
@ -46,20 +47,20 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
this.$widget.on('click', '.dropdown-item',
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()) {
if (['readOnly', 'autoReadOnlyDisabled'].includes(ownedAttr.name)) {
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
for (const ownedAttr of this.note.getOwnedLabels()) {
if (['readOnly', 'autoReadOnlyDisabled'].includes(ownedAttr.name)) {
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
}
}
}
if (editability !== 'auto') {
await attributeService.addLabel(this.noteId, editability);
}
});
if (editability !== 'auto') {
await attributeService.addLabel(this.noteId, editability);
}
});
}
async refreshWithNote(note) {
@ -73,9 +74,9 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
}
const labels = {
"auto": "Auto",
"readOnly": "Read-only",
"autoReadOnlyDisabled": "Always Editable"
"auto": t("editability_select.auto"),
"readOnly": t("editability_select.read_only"),
"autoReadOnlyDisabled": t("editability_select.always_editable")
}
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 toastService from "../../../services/toast.js";
import OptionsWidget from "./options_widget.js";
const TPL = `
<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">
<li>
<label>
<input type="checkbox" class="daily-backup-enabled">
Enable daily backup
${t('backup.enable_daily_backup')}
</label>
</li>
<li>
<label>
<input type="checkbox" class="weekly-backup-enabled">
Enable weekly backup
${t('backup.enable_weekly_backup')}
</label>
</li>
<li>
<label>
<input type="checkbox" class="monthly-backup-enabled">
Enable monthly backup
${t('backup.enable_monthly_backup')}
</label>
</li>
</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 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 class="options-section">
<h4>Existing backups</h4>
<h4>${t('backup.existing_backups')}</h4>
<ul class="existing-backup-list"></ul>
</div>
@ -54,7 +55,7 @@ export default class BackupOptions extends OptionsWidget {
this.$backupDatabaseButton.on('click', async () => {
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();
});
@ -84,7 +85,7 @@ export default class BackupOptions extends OptionsWidget {
this.$existingBackupList.empty();
if (!backupFiles.length) {
backupFiles = [{filePath: "no backup yet", mtime: ''}];
backupFiles = [{filePath: t('backup.no_backup_yet'), mtime: ''}];
}
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 dialogService from "../../../services/dialog.js";
import toastService from "../../../services/toast.js";
@ -5,24 +6,24 @@ import OptionsWidget from "./options_widget.js";
const TPL = `
<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/>
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>
<p>${t("etapi.description")} <br/>
${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;">
<table class="tokens-table table table-stripped">
<thead>
<tr>
<th>Token name</th>
<th>Created</th>
<th>Actions</th>
<th>${t("etapi.token_name")}</th>
<th>${t("etapi.created")}</th>
<th>${t("etapi.actions")}</th>
</tr>
</thead>
<tbody></tbody>
@ -52,21 +53,21 @@ export default class EtapiOptions extends OptionsWidget {
this.$widget.find(".create-etapi-token").on("click", async () => {
const tokenName = await dialogService.prompt({
title: "New ETAPI token",
message: "Please enter new token's name",
defaultValue: "new token"
title: t("etapi.new_token_title"),
message: t("etapi.new_token_message"),
defaultValue: t("etapi.default_token_name")
});
if (!tokenName.trim()) {
toastService.showError("Token name can't be empty");
toastService.showError(t("etapi.error_empty_name"));
return;
}
const {authToken} = await server.post('etapi-tokens', {tokenName});
await dialogService.prompt({
title: "ETAPI token created",
message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.',
title: t("etapi.token_created_title"),
message: t("etapi.token_created_message"),
defaultValue: authToken
});
@ -94,9 +95,9 @@ export default class EtapiOptions extends OptionsWidget {
.append($("<td>").text(token.name))
.append($("<td>").text(token.utcDateCreated))
.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)),
$('<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))
))
);
@ -105,8 +106,8 @@ export default class EtapiOptions extends OptionsWidget {
async renameToken(etapiTokenId, oldName) {
const tokenName = await dialogService.prompt({
title: "Rename token",
message: "Please enter new token's name",
title: t("etapi.rename_token_title"),
message: t("etapi.rename_token_message"),
defaultValue: oldName
});
@ -120,7 +121,7 @@ export default class EtapiOptions extends OptionsWidget {
}
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;
}

View File

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

View File

@ -1,3 +1,4 @@
import { t } from "../../../services/i18n.js";
import server from "../../../services/server.js";
import protectedSessionHolder from "../../../services/protected_session_holder.js";
import toastService from "../../../services/toast.js";
@ -5,42 +6,39 @@ import OptionsWidget from "./options_widget.js";
const TPL = `
<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;">
Please take care to remember your new password. Password is used for logging into the web interface and
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>.
${t("password.alert_message")} <a class="reset-password-button" href="javascript:">${t("password.reset_link")}</a>
</div>
<form class="change-password-form">
<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">
</div>
<div class="form-group">
<label>New password</label>
<label>${t("password.new_password")}</label>
<input class="new-password1 form-control" type="password">
</div>
<div class="form-group">
<label>New password confirmation</label>
<label>${t("password.new_password_confirmation")}</label>
<input class="new-password2 form-control" type="password">
</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>
</div>
<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
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>
<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>
<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">
</div>
</div>`;
@ -58,13 +56,13 @@ export default class PasswordOptions extends OptionsWidget {
this.$resetPasswordButton = this.$widget.find(".reset-password-button");
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");
const options = await server.get('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';
this.$widget.find(".old-password-form-group").toggle(isPasswordSet);
this.$passwordHeading.text(isPasswordSet ? 'Change Password' : 'Set Password');
this.$savePasswordButton.text(isPasswordSet ? 'Change Password' : 'Set Password');
this.$passwordHeading.text(isPasswordSet ? t("password.change_password_heading") : t("password.set_password_heading"));
this.$savePasswordButton.text(isPasswordSet ? t("password.change_password") : t("password.set_password"));
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
}
@ -94,7 +92,7 @@ export default class PasswordOptions extends OptionsWidget {
this.$newPassword2.val('');
if (newPassword1 !== newPassword2) {
toastService.showError("New passwords are not the same.");
toastService.showError(t("password.password_mismatch"));
return false;
}
@ -103,7 +101,7 @@ export default class PasswordOptions extends OptionsWidget {
'new_password': newPassword1
}).then(result => {
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
protectedSessionHolder.resetProtectedSession();

View File

@ -2,6 +2,7 @@ import server from "../../../services/server.js";
import utils from "../../../services/utils.js";
import dialogService from "../../../services/dialog.js";
import OptionsWidget from "./options_widget.js";
import { t } from "../../../services/i18n.js";
const TPL = `
<div class="options-section shortcuts-options-section">
@ -25,25 +26,25 @@ const TPL = `
}
</style>
<h4>Keyboard Shortcuts</h4>
<h4>${t('shortcuts.keyboard_shortcuts')}</h4>
<p>
Multiple shortcuts for the same action can be separated by comma.
See <a href="https://www.electronjs.org/docs/latest/api/accelerator">Electron documentation</a> for available modifiers and key codes.
${t('shortcuts.multiple_shortcuts')}
${t('shortcuts.electron_documentation')}
</p>
<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 class="shortcuts-table-container">
<table class="keyboard-shortcut-table" cellpadding="10">
<thead>
<tr>
<th>Action name</th>
<th>Shortcuts</th>
<th>Default shortcuts</th>
<th>Description</th>
<tr class="text-nowrap">
<th>${t('shortcuts.action_name')}</th>
<th>${t('shortcuts.shortcuts')}</th>
<th>${t('shortcuts.default_shortcuts')}</th>
<th>${t('shortcuts.description')}</th>
</tr>
</thead>
<tbody></tbody>
@ -51,9 +52,9 @@ const TPL = `
</div>
<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>`;
@ -83,10 +84,10 @@ export default class KeyboardShortcutsOptions extends OptionsWidget {
else {
$tr.append($("<td>").text(action.actionName))
.append($("<td>").append(
$(`<input type="text" class="form-control">`)
.val(action.effectiveShortcuts.join(", "))
.attr('data-keyboard-action-name', action.actionName)
.attr('data-default-keyboard-shortcuts', action.defaultShortcuts.join(", "))
$(`<input type="text" class="form-control">`)
.val(action.effectiveShortcuts.join(", "))
.attr('data-keyboard-action-name', action.actionName)
.attr('data-default-keyboard-shortcuts', 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 actionName = $input.attr('data-keyboard-action-name');
const shortcuts = $input.val()
.replace('+,', "+Comma")
.split(",")
.map(shortcut => shortcut.replace("+Comma", "+,"))
.filter(shortcut => !!shortcut);
.replace('+,', "+Comma")
.split(",")
.map(shortcut => shortcut.replace("+Comma", "+,"))
.filter(shortcut => !!shortcut);
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 () => {
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;
}
@ -152,7 +153,7 @@ export default class KeyboardShortcutsOptions extends OptionsWidget {
return;
}
this.$widget.find(el).toggle(!!( // !! to avoid toggle overloads with different behavior
this.$widget.find(el).toggle(!!(
action.actionName.toLowerCase().includes(filter)
|| action.defaultShortcuts.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 OptionsWidget from "./options_widget.js";
import { t } from "../../../services/i18n.js";
const TPL_WEB = `
<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>
`;
const TPL_ELECTRON = `
<div class="options-section">
<h4>Spell Check</h4>
<h4>${t('spellcheck.title')}</h4>
<p>App restart is required after change.</p>
<label>
<input type="checkbox" class="spell-check-enabled">
Enable spellcheck
${t('spellcheck.enable')}
</label>
<br/>
<div class="form-group">
<label>Language code(s)</label>
<input type="text" class="spell-check-language-code form-control" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
<label>${t('spellcheck.language_code_label')}</label>
<input type="text" class="spell-check-language-code form-control" placeholder="${t('spellcheck.language_code_placeholder')}">
</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>`;
export default class SpellcheckOptions extends OptionsWidget {

View File

@ -1,44 +1,45 @@
import server from "../../../services/server.js";
import toastService from "../../../services/toast.js";
import OptionsWidget from "./options_widget.js";
import { t } from "../../../services/i18n.js";
const TPL = `
<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">
<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>">
</div>
<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;">
</div>
<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>">
<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>Another special value is <code>noproxy</code> which forces ignoring even the system proxy and respectes <code>NODE_TLS_REJECT_UNAUTHORIZED</code>.</p>
<p><strong>${t('sync_2.note')}:</strong> ${t('sync_2.note_description')}</p>
<p>${t('sync_2.special_value_description')}</p>
</div>
<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>
</form>
</div>
<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>`;
export default class SyncOptions extends OptionsWidget {
@ -58,9 +59,8 @@ export default class SyncOptions extends OptionsWidget {
if (result.success) {
toastService.showMessage(result.message);
}
else {
toastService.showError(`Sync server handshake failed, error: ${result.message}`);
} else {
toastService.showError(t('sync_2.handshake_failed', { message: result.message }));
}
});
}

View File

@ -1114,5 +1114,133 @@
"title": "自动只读大小",
"description": "自动只读笔记大小是超过该大小后,笔记将以只读模式显示(出于性能考虑)。",
"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