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

View File

@@ -276,8 +276,13 @@ var vm = new Vue({
'language': s.language, 'language': s.language,
'languages': [ 'languages': [
{code: 'en', name: 'English' }, {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: 'ru', name: 'Русский'},
{code: 'zh', name: '简体中文'},
] ]
} }
}, },
@@ -555,7 +560,7 @@ var vm = new Vue({
}) })
}, },
moveFeedToNewFolder: function(feed) { moveFeedToNewFolder: function(feed) {
var title = prompt('Enter folder name:') var title = prompt(this.$t('prompt_folder_name'))
if (!title) return if (!title) return
api.folders.create({'title': title}).then(function(folder) { api.folders.create({'title': title}).then(function(folder) {
api.feeds.update(feed.id, {folder_id: folder.id}).then(function() { api.feeds.update(feed.id, {folder_id: folder.id}).then(function() {
@@ -566,7 +571,7 @@ var vm = new Vue({
}) })
}, },
createNewFeedFolder: function() { createNewFeedFolder: function() {
var title = prompt('Enter folder name:') var title = prompt(this.$t('prompt_folder_name'))
if (!title) return if (!title) return
api.folders.create({'title': title}).then(function(result) { api.folders.create({'title': title}).then(function(result) {
vm.refreshFeeds().then(function() { vm.refreshFeeds().then(function() {
@@ -579,7 +584,7 @@ var vm = new Vue({
}) })
}, },
renameFolder: function(folder) { renameFolder: function(folder) {
var newTitle = prompt('Enter new title', folder.title) var newTitle = prompt(this.$t('prompt_new_title'), folder.title)
if (newTitle) { if (newTitle) {
api.folders.update(folder.id, {title: newTitle}).then(function() { api.folders.update(folder.id, {title: newTitle}).then(function() {
folder.title = newTitle folder.title = newTitle
@@ -590,7 +595,7 @@ var vm = new Vue({
} }
}, },
deleteFolder: function(folder) { 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() { api.folders.delete(folder.id).then(function() {
vm.feedSelected = null vm.feedSelected = null
vm.refreshStats() vm.refreshStats()
@@ -599,7 +604,7 @@ var vm = new Vue({
} }
}, },
updateFeedLink: function(feed) { 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) { if (newLink) {
api.feeds.update(feed.id, {feed_link: newLink}).then(function() { api.feeds.update(feed.id, {feed_link: newLink}).then(function() {
feed.feed_link = newLink feed.feed_link = newLink
@@ -607,7 +612,7 @@ var vm = new Vue({
} }
}, },
renameFeed: function(feed) { renameFeed: function(feed) {
var newTitle = prompt('Enter new title', feed.title) var newTitle = prompt(this.$t('prompt_new_title'), feed.title)
if (newTitle) { if (newTitle) {
api.feeds.update(feed.id, {title: newTitle}).then(function() { api.feeds.update(feed.id, {title: newTitle}).then(function() {
feed.title = newTitle feed.title = newTitle
@@ -615,7 +620,7 @@ var vm = new Vue({
} }
}, },
deleteFeed: function(feed) { 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() { api.feeds.delete(feed.id).then(function() {
vm.feedSelected = null vm.feedSelected = null
vm.refreshStats() vm.refreshStats()

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -9,6 +9,7 @@
<link rel="alternate icon" href="./static/graphicarts/favicon.png" type="image/png"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style> <style>
[v-cloak] { display: none }
form { form {
max-width: 300px; max-width: 300px;
margin: 0 auto; margin: 0 auto;
@@ -23,21 +24,33 @@
</style> </style>
</head> </head>
<body class="theme-{% .settings.theme_name %}"> <body class="theme-{% .settings.theme_name %}">
<div id="app" v-cloak>
<form action="" method="post"> <form action="" method="post">
<img src="./static/graphicarts/anchor.svg" alt=""> <img src="./static/graphicarts/anchor.svg" alt="">
{% if .error %} <div class="text-danger text-center my-3" v-if="hasError">{{ $t('login_error') }}</div>
<div class="text-danger text-center my-3">{% .error %}</div>
{% end %}
<div class="form-group"> <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" <input name="username" class="form-control" id="username" autocomplete="off"
value="{% if .username %}{% .username %}{% end %}" required autofocus> value="{% if .username %}{% .username %}{% end %}" required autofocus>
</div> </div>
<div class="form-group"> <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> <input name="password" class="form-control" id="password" type="password" required>
</div> </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> </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> </body>
</html> </html>

View File

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