6 Commits

Author SHA1 Message Date
nkanaev
14b06dcbaf i18n: tweak language selection ui 2026-06-22 21:46:39 +01:00
nkanaev
3a75e61c7d i18n: more autotranslated languages 2026-06-22 21:46:35 +01:00
nkanaev
8fb7702e6d i18n: translation string improvements 2026-06-22 21:46:31 +01:00
nkanaev
6202451c7c i18n: switch to fluent in login page 2026-06-22 21:46:28 +01:00
nkanaev
9e46014787 i18n: switch to fluent 2026-06-22 21:46:24 +01:00
nkanaev
2de9772e4b i18n: add fluent.js 2026-06-22 21:46:17 +01:00
6 changed files with 1673 additions and 69 deletions

View File

@@ -126,11 +126,12 @@
{{ $t('shortcuts') }}
</button>
<div class="dropdown-divider"></div>
<header class="dropdown-header" role="heading" aria-level="2"> / 𐎠 / 𑖀</header>
<div class="d-flex">
<header class="dropdown-header" role="heading" aria-level="2">A / / </header>
<div class="container">
<div class="row">
<button
v-for="lang in languages"
class="dropdown-item text-center"
class="dropdown-item text-center col-3 px-0"
:aria-label="lang.name"
:title="lang.name"
:class="{active: language==lang.code}"
@@ -138,6 +139,7 @@
{{ lang.code }}
</button>
</div>
</div>
<div class="dropdown-divider" v-if="authenticated"></div>
<button class="dropdown-item" v-if="authenticated" @click="logout()">
<span class="icon mr-1">{% inline "log-out.svg" %}</span>
@@ -193,7 +195,7 @@
</div>
<div class="p-2 toolbar d-flex align-items-center border-top flex-shrink-0" v-if="loading.feeds">
<span class="icon loading mx-2"></span>
<span class="text-truncate cursor-default noselect">{{ $t('refreshing') }} ({{ loading.feeds }} {{ $t('left') }})</span>
<span class="text-truncate cursor-default noselect">{{ $t('refreshing_progress', {count: loading.feeds}) }}</span>
</div>
</div>
<!-- item list -->
@@ -453,6 +455,7 @@
</div>
<!-- external -->
<script src="./static/javascripts/vue.min.js"></script>
<script src="./static/javascripts/fluent.js"></script>
<!-- internal -->
<script src="./static/javascripts/i18n.js"></script>
<script src="./static/javascripts/api.js"></script>

View File

@@ -276,8 +276,13 @@ var vm = new Vue({
'language': s.language,
'languages': [
{code: 'en', name: 'English' },
{code: 'zh', name: '简体中文'},
{code: 'de', name: 'Deutsch'},
{code: 'es', name: 'Español'},
{code: 'fr', name: 'Français'},
{code: 'ja', name: '日本語'},
{code: 'pt', name: 'Português'},
{code: 'ru', name: 'Русский'},
{code: 'zh', name: '简体中文'},
]
}
},
@@ -555,7 +560,7 @@ var vm = new Vue({
})
},
moveFeedToNewFolder: function(feed) {
var title = prompt('Enter folder name:')
var title = prompt(this.$t('prompt_folder_name'))
if (!title) return
api.folders.create({'title': title}).then(function(folder) {
api.feeds.update(feed.id, {folder_id: folder.id}).then(function() {
@@ -566,7 +571,7 @@ var vm = new Vue({
})
},
createNewFeedFolder: function() {
var title = prompt('Enter folder name:')
var title = prompt(this.$t('prompt_folder_name'))
if (!title) return
api.folders.create({'title': title}).then(function(result) {
vm.refreshFeeds().then(function() {
@@ -579,7 +584,7 @@ var vm = new Vue({
})
},
renameFolder: function(folder) {
var newTitle = prompt('Enter new title', folder.title)
var newTitle = prompt(this.$t('prompt_new_title'), folder.title)
if (newTitle) {
api.folders.update(folder.id, {title: newTitle}).then(function() {
folder.title = newTitle
@@ -590,7 +595,7 @@ var vm = new Vue({
}
},
deleteFolder: function(folder) {
if (confirm('Are you sure you want to delete ' + folder.title + '?')) {
if (confirm(this.$t('confirm_delete', {name: folder.title}))) {
api.folders.delete(folder.id).then(function() {
vm.feedSelected = null
vm.refreshStats()
@@ -599,7 +604,7 @@ var vm = new Vue({
}
},
updateFeedLink: function(feed) {
var newLink = prompt('Enter feed link', feed.feed_link)
var newLink = prompt(this.$t('prompt_feed_link'), feed.feed_link)
if (newLink) {
api.feeds.update(feed.id, {feed_link: newLink}).then(function() {
feed.feed_link = newLink
@@ -607,7 +612,7 @@ var vm = new Vue({
}
},
renameFeed: function(feed) {
var newTitle = prompt('Enter new title', feed.title)
var newTitle = prompt(this.$t('prompt_new_title'), feed.title)
if (newTitle) {
api.feeds.update(feed.id, {title: newTitle}).then(function() {
feed.title = newTitle
@@ -615,7 +620,7 @@ var vm = new Vue({
}
},
deleteFeed: function(feed) {
if (confirm('Are you sure you want to delete ' + feed.title + '?')) {
if (confirm(this.$t('confirm_delete', {name: feed.title}))) {
api.feeds.delete(feed.id).then(function() {
vm.feedSelected = null
vm.refreshStats()

File diff suppressed because it is too large Load Diff

View File

@@ -2,371 +2,715 @@
const translations = {
"unread": {
"en": "Unread",
"de": "Ungelesene",
"fr": "Non lus",
"es": "No leídos",
"ja": "未読",
"pt": "Não lidos",
"zh": "未读",
"ru": "Непрочитанные"
},
"starred": {
"en": "Starred",
"de": "Markierte",
"fr": "Favoris",
"es": "Destacados",
"ja": "スター付き",
"pt": "Favoritos",
"zh": "星标",
"ru": "Избранные"
},
"all": {
"en": "All",
"de": "Alle",
"fr": "Tout",
"es": "Todo",
"ja": "すべて",
"pt": "Tudo",
"zh": "全部",
"ru": "Все"
},
"settings": {
"en": "Settings",
"de": "Einstellungen",
"fr": "Paramètres",
"es": "Ajustes",
"ja": "設定",
"pt": "Configurações",
"zh": "设置",
"ru": "Настройки"
},
"new_feed": {
"en": "New Feed",
"de": "Neuer Feed",
"fr": "Nouveau flux",
"es": "Nueva fuente",
"ja": "新規フィード",
"pt": "Novo feed",
"zh": "新建订阅",
"ru": "Новая лента"
},
"refresh_feeds": {
"en": "Refresh Feeds",
"de": "Feeds aktualisieren",
"fr": "Actualiser les flux",
"es": "Actualizar fuentes",
"ja": "フィードを更新",
"pt": "Atualizar feeds",
"zh": "刷新订阅",
"ru": "Обновить ленты"
},
"theme": {
"en": "Theme",
"de": "Design",
"fr": "Thème",
"es": "Tema",
"ja": "テーマ",
"pt": "Tema",
"zh": "主题",
"ru": "Тема"
},
"auto_refresh": {
"en": "Auto Refresh",
"de": "Automatisch aktualisieren",
"fr": "Actualisation automatique",
"es": "Actualización automática",
"ja": "自動更新",
"pt": "Atualização automática",
"zh": "自动刷新",
"ru": "Автообновление"
},
"show_first": {
"en": "Show first",
"de": "Zuerst anzeigen",
"fr": "Afficher d'abord",
"es": "Mostrar primero",
"ja": "表示順",
"pt": "Mostrar primeiro",
"zh": "优先显示",
"ru": "Сначала"
},
"new": {
"en": "New",
"de": "Neue",
"fr": "Récents",
"es": "Nuevos",
"ja": "新しい順",
"pt": "Novos",
"zh": "最新",
"ru": "Новые"
},
"old": {
"en": "Old",
"de": "Alte",
"fr": "Anciens",
"es": "Antiguos",
"ja": "古い順",
"pt": "Antigos",
"zh": "最旧",
"ru": "Старые"
},
"subscriptions": {
"en": "Subscriptions",
"de": "Abonnements",
"fr": "Abonnements",
"es": "Suscripciones",
"ja": "購読管理",
"pt": "Assinaturas",
"zh": "订阅管理",
"ru": "Подписки"
},
"import": {
"en": "Import",
"de": "Importieren",
"fr": "Importer",
"es": "Importar",
"ja": "インポート",
"pt": "Importar",
"zh": "导入",
"ru": "Импорт"
},
"export": {
"en": "Export",
"de": "Exportieren",
"fr": "Exporter",
"es": "Exportar",
"ja": "エクスポート",
"pt": "Exportar",
"zh": "导出",
"ru": "Экспорт"
},
"shortcuts": {
"en": "Shortcuts",
"de": "Tastenkürzel",
"fr": "Raccourcis",
"es": "Atajos",
"ja": "ショートカット",
"pt": "Atalhos",
"zh": "快捷键",
"ru": "Горячие клавиши"
},
"log_out": {
"en": "Log out",
"de": "Abmelden",
"fr": "Déconnexion",
"es": "Cerrar sesión",
"ja": "ログアウト",
"pt": "Sair",
"zh": "登出",
"ru": "Выйти"
},
"all_unread": {
"en": "All Unread",
"de": "Alle ungelesenen",
"fr": "Tous les non lus",
"es": "Todos los no leídos",
"ja": "すべての未読",
"pt": "Todos os não lidos",
"zh": "全部未读",
"ru": "Все непрочитанные"
},
"all_starred": {
"en": "All Starred",
"de": "Alle markierten",
"fr": "Tous les favoris",
"es": "Todos los destacados",
"ja": "すべてのスター付き",
"pt": "Todos os favoritos",
"zh": "全部星标",
"ru": "Все избранные"
},
"all_feeds": {
"en": "All Feeds",
"de": "Alle Feeds",
"fr": "Tous les flux",
"es": "Todas las fuentes",
"ja": "すべてのフィード",
"pt": "Todos os feeds",
"zh": "全部订阅",
"ru": "Все ленты"
},
"refreshing": {
"en": "Refreshing",
"zh": "正在刷新",
"ru": "Обновление"
},
"left": {
"en": "left",
"zh": "剩余",
"ru": "осталось"
"refreshing_progress": {
"en": "Refreshing ({ $count } left)",
"de": "Aktualisiere ({ $count } übrig)",
"fr": "Actualisation ({ $count } restantes)",
"es": "Actualizando ({ $count } restantes)",
"ja": "更新中(残り{ $count }",
"pt": "Atualizando ({ $count } restantes)",
"zh": "正在刷新(剩余{ $count }",
"ru": "Обновление: осталось { $count }"
},
"show_feeds": {
"en": "Show Feeds",
"de": "Feeds anzeigen",
"fr": "Afficher les flux",
"es": "Mostrar fuentes",
"ja": "フィードを表示",
"pt": "Mostrar feeds",
"zh": "显示订阅",
"ru": "Показать ленты"
},
"mark_all_read": {
"en": "Mark All Read",
"de": "Alle als gelesen markieren",
"fr": "Tout marquer comme lu",
"es": "Marcar todo como leído",
"ja": "すべて既読にする",
"pt": "Marcar todos como lidos",
"zh": "全部标记为已读",
"ru": "Отметить все как прочитанные"
},
"feed_settings": {
"en": "Feed Settings",
"de": "Feed-Einstellungen",
"fr": "Paramètres du flux",
"es": "Ajustes de fuente",
"ja": "フィード設定",
"pt": "Configurações do feed",
"zh": "订阅设置",
"ru": "Настройки ленты"
},
"folder_settings": {
"en": "Folder Settings",
"de": "Ordner-Einstellungen",
"fr": "Paramètres du dossier",
"es": "Ajustes de carpeta",
"ja": "フォルダ設定",
"pt": "Configurações da pasta",
"zh": "文件夹设置",
"ru": "Настройки папки"
},
"website": {
"en": "Website",
"de": "Webseite",
"fr": "Site web",
"es": "Sitio web",
"ja": "ウェブサイト",
"pt": "Site",
"zh": "网站",
"ru": "Сайт"
},
"feed_link": {
"en": "Feed Link",
"de": "Feed-Link",
"fr": "Lien du flux",
"es": "Enlace de la fuente",
"ja": "フィードリンク",
"pt": "Link do feed",
"zh": "订阅链接",
"ru": "Ссылка на ленту"
},
"rename": {
"en": "Rename",
"de": "Umbenennen",
"fr": "Renommer",
"es": "Renombrar",
"ja": "名前変更",
"pt": "Renomear",
"zh": "重命名",
"ru": "Переименовать"
},
"change_link": {
"en": "Change Link",
"de": "Link ändern",
"fr": "Changer le lien",
"es": "Cambiar enlace",
"ja": "リンク変更",
"pt": "Alterar link",
"zh": "修改链接",
"ru": "Изменить ссылку"
},
"move_to": {
"en": "Move to...",
"de": "Verschieben nach...",
"fr": "Déplacer vers...",
"es": "Mover a...",
"ja": "移動...",
"pt": "Mover para...",
"zh": "移动到...",
"ru": "Переместить в..."
},
"new_folder": {
"en": "new folder",
"de": "neuer Ordner",
"fr": "nouveau dossier",
"es": "nueva carpeta",
"ja": "新規フォルダ",
"pt": "nova pasta",
"zh": "新建文件夹",
"ru": "новая папка"
},
"delete": {
"en": "Delete",
"de": "Löschen",
"fr": "Supprimer",
"es": "Eliminar",
"ja": "削除",
"pt": "Excluir",
"zh": "删除",
"ru": "Удалить"
},
"mark_starred": {
"en": "Mark Starred",
"de": "Als markiert kennzeichnen",
"fr": "Marquer comme favori",
"es": "Marcar como destacado",
"ja": "スターを付ける",
"pt": "Marcar como favorito",
"zh": "标记星标",
"ru": "Пометить избранным"
},
"mark_unread": {
"en": "Mark Unread",
"de": "Als ungelesen kennzeichnen",
"fr": "Marquer comme non lu",
"es": "Marcar como no leído",
"ja": "未読にする",
"pt": "Marcar como não lido",
"zh": "标记未读",
"ru": "Пометить непрочитанным"
},
"appearance": {
"en": "Appearance",
"de": "Darstellung",
"fr": "Apparence",
"es": "Apariencia",
"ja": "表示設定",
"pt": "Aparência",
"zh": "外观",
"ru": "Внешний вид"
},
"read_here": {
"en": "Read Here",
"de": "Hier lesen",
"fr": "Lire ici",
"es": "Leer aquí",
"ja": "ここで読む",
"pt": "Ler aqui",
"zh": "在此阅读",
"ru": "Читать здесь"
},
"open_link": {
"en": "Open Link",
"de": "Link öffnen",
"fr": "Ouvrir le lien",
"es": "Abrir enlace",
"ja": "リンクを開く",
"pt": "Abrir link",
"zh": "打开链接",
"ru": "Открыть ссылку"
},
"previous_article": {
"en": "Previous Article",
"de": "Vorheriger Artikel",
"fr": "Article précédent",
"es": "Artículo anterior",
"ja": "前の記事",
"pt": "Artigo anterior",
"zh": "上一篇",
"ru": "Предыдущая статья"
},
"next_article": {
"en": "Next Article",
"de": "Nächster Artikel",
"fr": "Article suivant",
"es": "Artículo siguiente",
"ja": "次の記事",
"pt": "Próximo artigo",
"zh": "下一篇",
"ru": "Следующая статья"
},
"close_article": {
"en": "Close Article",
"de": "Artikel schließen",
"fr": "Fermer l'article",
"es": "Cerrar artículo",
"ja": "記事を閉じる",
"pt": "Fechar artigo",
"zh": "关闭文章",
"ru": "Закрыть статью"
},
"untitled": {
"en": "untitled",
"de": "unbenannt",
"fr": "sans titre",
"es": "sin título",
"ja": "無題",
"pt": "sem título",
"zh": "无标题",
"ru": "без названия"
},
"sans_serif": {
"en": "sans-serif",
"de": "serifenlos",
"fr": "sans empattement",
"es": "sans-serif",
"ja": "ゴシック体",
"pt": "sem serifa",
"zh": "无衬线",
"ru": "sans-serif"
},
"serif": {
"en": "serif",
"de": "Serife",
"fr": "empattement",
"es": "serifa",
"ja": "明朝体",
"pt": "com serifa",
"zh": "衬线",
"ru": "serif"
},
"monospace": {
"en": "monospace",
"de": "monospace",
"fr": "monospace",
"es": "monoespacio",
"ja": "等幅",
"pt": "monoespaçada",
"zh": "等宽",
"ru": "monospace"
},
"url": {
"en": "URL",
"de": "URL",
"fr": "URL",
"es": "URL",
"ja": "URL",
"pt": "URL",
"zh": "网址",
"ru": "URL"
},
"folder": {
"en": "Folder",
"de": "Ordner",
"fr": "Dossier",
"es": "Carpeta",
"ja": "フォルダ",
"pt": "Pasta",
"zh": "文件夹",
"ru": "Папка"
},
"add": {
"en": "Add",
"de": "Hinzufügen",
"fr": "Ajouter",
"es": "Añadir",
"ja": "追加",
"pt": "Adicionar",
"zh": "添加",
"ru": "Добавить"
},
"keyboard_shortcuts": {
"en": "Keyboard Shortcuts",
"de": "Tastenkürzel",
"fr": "Raccourcis clavier",
"es": "Atajos de teclado",
"ja": "キーボードショートカット",
"pt": "Atalhos do teclado",
"zh": "键盘快捷键",
"ru": "Горячие клавиши"
},
"multiple_feeds_found": {
"en": "Multiple feeds found. Choose one below:",
"de": "Mehrere Feeds gefunden. Bitte wählen Sie einen aus:",
"fr": "Plusieurs flux trouvés. Choisissez-en un ci-dessous :",
"es": "Múltiples fuentes encontradas. Elija una:",
"ja": "複数のフィードが見つかりました。以下から選択してください:",
"pt": "Múltiplos feeds encontrados. Escolha um abaixo:",
"zh": "找到多个订阅源,请选择一个:",
"ru": "Найдено несколько лент. Выберите одну:"
},
"cancel": {
"en": "cancel",
"de": "abbrechen",
"fr": "annuler",
"es": "cancelar",
"ja": "キャンセル",
"pt": "cancelar",
"zh": "取消",
"ru": "отмена"
},
"kb_show_filters": {
"en": "show unread / starred / all feeds",
"de": "ungelesene / markierte / alle Feeds anzeigen",
"fr": "afficher les flux non lus / favoris / tous",
"es": "mostrar fuentes no leídas / destacadas / todas",
"ja": "未読/スター付き/すべてのフィードを表示",
"pt": "mostrar feeds não lidos / favoritos / todos",
"zh": "显示未读/星标/全部订阅",
"ru": "показать непрочитанные / избранные / все ленты"
},
"kb_focus_search": {
"en": "focus the search bar",
"de": "Suchleiste fokussieren",
"fr": "focus sur la barre de recherche",
"es": "enfocar la barra de búsqueda",
"ja": "検索バーにフォーカス",
"pt": "focar na barra de pesquisa",
"zh": "聚焦搜索栏",
"ru": "фокус на строку поиска"
},
"kb_next_prev_article": {
"en": "next / prev article",
"de": "nächster / vorheriger Artikel",
"fr": "article suivant / précédent",
"es": "artículo siguiente / anterior",
"ja": "次の/前の記事",
"pt": "próximo / artigo anterior",
"zh": "下一篇/上一篇文章",
"ru": "следующая / предыдущая статья"
},
"kb_next_prev_feed": {
"en": "next / prev feed",
"de": "nächster / vorheriger Feed",
"fr": "flux suivant / précédent",
"es": "fuente siguiente / anterior",
"ja": "次の/前のフィード",
"pt": "próximo / feed anterior",
"zh": "下一个/上一个订阅",
"ru": "следующая / предыдущая лента"
},
"kb_close_article": {
"en": "close article",
"de": "Artikel schließen",
"fr": "fermer l'article",
"es": "cerrar artículo",
"ja": "記事を閉じる",
"pt": "fechar artigo",
"zh": "关闭文章",
"ru": "закрыть статью"
},
"kb_mark_all_read": {
"en": "mark all read",
"de": "alle als gelesen markieren",
"fr": "tout marquer comme lu",
"es": "marcar todo como leído",
"ja": "すべて既読にする",
"pt": "marcar todos como lidos",
"zh": "全部标记为已读",
"ru": "отметить все как прочитанные"
},
"kb_mark_read": {
"en": "mark read / unread",
"de": "als gelesen / ungelesen markieren",
"fr": "marquer comme lu / non lu",
"es": "marcar como leído / no leído",
"ja": "既読/未読を切り替え",
"pt": "marcar como lido / não lido",
"zh": "标记已读/未读",
"ru": "отметить как прочитанное / непрочитанное"
},
"kb_mark_starred": {
"en": "mark starred / unstarred",
"de": "als markiert / nicht markiert kennzeichnen",
"fr": "marquer comme favori / non favori",
"es": "marcar como destacado / no destacado",
"ja": "スターを付ける/外す",
"pt": "marcar como favorito / não favorito",
"zh": "标记星标/取消星标",
"ru": "пометить избранным / убрать из избранного"
},
"kb_open_link": {
"en": "open link",
"de": "Link öffnen",
"fr": "ouvrir le lien",
"es": "abrir enlace",
"ja": "リンクを開く",
"pt": "abrir link",
"zh": "打开链接",
"ru": "открыть ссылку"
},
"kb_read_here": {
"en": "read here",
"de": "hier lesen",
"fr": "lire ici",
"es": "leer aquí",
"ja": "ここで読む",
"pt": "ler aqui",
"zh": "在此阅读",
"ru": "читать здесь"
},
"kb_scroll_content": {
"en": "scroll content forward / backward",
"de": "Inhalt vorwärts / rückwärts scrollen",
"fr": "faire défiler le contenu avant / arrière",
"es": "desplazar contenido hacia adelante / atrás",
"ja": "コンテンツを前/後にスクロール",
"pt": "rolar conteúdo para frente / trás",
"zh": "向前/向后滚动内容",
"ru": "прокрутка вперед / назад"
},
"prompt_folder_name": {
"en": "Enter folder name:",
"de": "Ordnernamen eingeben:",
"fr": "Entrez le nom du dossier :",
"es": "Introduzca el nombre de la carpeta:",
"ja": "フォルダ名を入力してください:",
"pt": "Digite o nome da pasta:",
"zh": "请输入文件夹名称:",
"ru": "Введите имя папки:"
},
"prompt_new_title": {
"en": "Enter new title",
"de": "Neuen Titel eingeben",
"fr": "Entrez un nouveau titre",
"es": "Introduzca un nuevo título",
"ja": "新しいタイトルを入力してください",
"pt": "Digite o novo título",
"zh": "请输入新标题",
"ru": "Введите новый заголовок"
},
"prompt_feed_link": {
"en": "Enter feed link",
"de": "Feed-Link eingeben",
"fr": "Entrez le lien du flux",
"es": "Introduzca el enlace de la fuente",
"ja": "フィードリンクを入力してください",
"pt": "Digite o link do feed",
"zh": "请输入订阅链接",
"ru": "Введите ссылку на ленту"
},
"confirm_delete_folder": {
"en": "Are you sure you want to delete",
"zh": "确定要删除",
"ru": "Вы уверены, что хотите удалить"
},
"confirm_delete_feed": {
"en": "Are you sure you want to delete",
"zh": "确定要删除",
"ru": "Вы уверены, что хотите удалить"
"confirm_delete": {
"en": "Are you sure you want to delete { $name }?",
"de": "Möchten Sie { $name } wirklich löschen?",
"fr": "Voulez-vous vraiment supprimer { $name } ?",
"es": "¿Está seguro de que quiere eliminar { $name }?",
"ja": "{ $name }を削除してもよろしいですか?",
"pt": "Tem certeza que deseja excluir { $name }?",
"zh": "确定要删除{ $name }",
"ru": "Вы уверены, что хотите удалить { $name }?"
},
"alert_no_feeds": {
"en": "No feeds found at the given url.",
"de": "Keine Feeds unter der angegebenen URL gefunden.",
"fr": "Aucun flux trouvé à cette URL.",
"es": "No se encontraron fuentes en la URL proporcionada.",
"ja": "指定されたURLにフィードが見つかりませんでした。",
"pt": "Nenhum feed encontrado no URL fornecido.",
"zh": "在指定的网址未找到订阅源。",
"ru": "Лент по данному адресу не найдено."
},
"login": {
"en": "Login",
"de": "Anmelden",
"fr": "Connexion",
"es": "Iniciar sesión",
"ja": "ログイン",
"pt": "Entrar",
"zh": "登录",
"ru": "Вход"
},
"login_error": {
"en": "Invalid username or password",
"de": "Ungültiger Benutzername oder Passwort",
"fr": "Nom d'utilisateur ou mot de passe invalide",
"es": "Nombre de usuario o contraseña inválidos",
"ja": "ユーザー名またはパスワードが無効です",
"pt": "Nome de usuário ou senha inválidos",
"zh": "用户名或密码错误",
"ru": "Неверное имя пользователя или пароль"
},
"username": {
"en": "Username",
"de": "Benutzername",
"fr": "Nom d'utilisateur",
"es": "Nombre de usuario",
"ja": "ユーザー名",
"pt": "Nome de usuário",
"zh": "用户名",
"ru": "Имя пользователя"
},
"password": {
"en": "Password",
"de": "Passwort",
"fr": "Mot de passe",
"es": "Contraseña",
"ja": "パスワード",
"pt": "Senha",
"zh": "密码",
"ru": "Пароль"
},
};
class i18n {
constructor() {
this.lang = 'en'
}
setLang(lang) {
this.lang = lang
}
$t(code) {
return translations[code][this.lang]
}
function ftlFrom(lang) {
return Object.entries(translations)
.map(([key, langs]) => `${key} = ${langs[lang]}`)
.join('\n')
}
exports.i18n = {
install(Vue, opts) {
const x = new i18n();
Vue.prototype.$t = x.$t
Vue.prototype.$setLang = x.setLang
install(Vue) {
let bundle = null
Vue.prototype.$setLang = function (lang) {
const ftl = ftlFrom(lang)
const resource = new FluentBundle.FluentResource(ftl)
bundle = new FluentBundle.FluentBundle(lang)
bundle.addResource(resource)
}
Vue.prototype.$t = function (code, args) {
if (!bundle) return
const msg = bundle.getMessage(code)
if (!msg || !msg.value) return
return bundle.formatPattern(msg.value, args)
}
}
}
})(window)

View File

@@ -9,6 +9,7 @@
<link rel="alternate icon" href="./static/graphicarts/favicon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
[v-cloak] { display: none }
form {
max-width: 300px;
margin: 0 auto;
@@ -23,21 +24,33 @@
</style>
</head>
<body class="theme-{% .settings.theme_name %}">
<div id="app" v-cloak>
<form action="" method="post">
<img src="./static/graphicarts/anchor.svg" alt="">
{% if .error %}
<div class="text-danger text-center my-3">{% .error %}</div>
{% end %}
<div class="text-danger text-center my-3" v-if="hasError">{{ $t('login_error') }}</div>
<div class="form-group">
<label for="username">Username</label>
<label for="username">{{ $t('username') }}</label>
<input name="username" class="form-control" id="username" autocomplete="off"
value="{% if .username %}{% .username %}{% end %}" required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<label for="password">{{ $t('password') }}</label>
<input name="password" class="form-control" id="password" type="password" required>
</div>
<button class="btn btn-block btn-default" type="submit">Login</button>
<button class="btn btn-block btn-default" type="submit">{{ $t('login') }}</button>
</form>
</div>
<script src="./static/javascripts/vue.min.js"></script>
<script src="./static/javascripts/fluent.js"></script>
<script src="./static/javascripts/i18n.js"></script>
<script>
Vue.use(i18n)
new Vue({
data: { hasError: {% .hasError %} },
created: function () {
this.$setLang('{% .settings.language %}')
}
}).$mount('#app')
</script>
</body>
</html>

View File

@@ -46,13 +46,14 @@ func (m *Middleware) Handler(c *router.Context) {
} else {
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]any{
"username": username,
"error": "Invalid username/password",
"hasError": true,
"settings": m.DB.GetSettings().Map(),
})
return
}
}
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]any{
"hasError": false,
"settings": m.DB.GetSettings().Map(),
})
}