diff --git a/.drone.yml b/.drone.yml index 38ff2044f..c4e9e67e0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -528,3 +528,34 @@ steps: status: - success - failure +--- +kind: pipeline +name: ping-weblate + +depends_on: + - build + +trigger: + branch: + - main + event: + - push + +steps: + - name: update-translation-base + image: appleboy/drone-git-push + failure: ignore + settings: + branch: translations + remote: ssh://git@kolaente.dev:9022/vikunja/frontend.git + ssh_key: + from_secret: translations_branch_update_ssh_key + - name: notify-weblate + image: curlimages/curl + depends_on: + - update-translation-base + environment: + WEBLATE_TOKEN: + from_secret: weblate_token + commands: + - ./ping-weblate.sh diff --git a/README.md b/README.md index 1cf4e8bef..0e1a8615c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE) [![Download](https://img.shields.io/badge/download-v0.17.0-brightgreen.svg)](https://dl.vikunja.io) +[![Translation](https://hosted.weblate.org/widgets/vikunja/-/frontend/svg-badge.svg)](https://hosted.weblate.org/engage/vikunja/) This is the web frontend for Vikunja, written in Vue.js. diff --git a/cypress/integration/list/list.spec.js b/cypress/integration/list/list.spec.js index 3c8c0d6fc..843d82913 100644 --- a/cypress/integration/list/list.spec.js +++ b/cypress/integration/list/list.spec.js @@ -388,7 +388,7 @@ describe('Lists', () => { .first() .click() cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item') - .contains('Limit: Not set') + .contains('Limit: Not Set') .click() cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input') .first() diff --git a/cypress/integration/list/namespaces.spec.js b/cypress/integration/list/namespaces.spec.js index 9d3125b59..e8c15443e 100644 --- a/cypress/integration/list/namespaces.spec.js +++ b/cypress/integration/list/namespaces.spec.js @@ -24,7 +24,7 @@ describe('Namepaces', () => { cy.visit('/namespaces') cy.get('a.button') - .contains('Create namespace') + .contains('Create a new namespace') .click() cy.url() diff --git a/cypress/integration/sharing/team.spec.js b/cypress/integration/sharing/team.spec.js index 094556355..7c48a59aa 100644 --- a/cypress/integration/sharing/team.spec.js +++ b/cypress/integration/sharing/team.spec.js @@ -11,7 +11,7 @@ describe('Team', () => { const newTeamName = 'New Team' cy.get('a.button') - .contains('New Team') + .contains('Create a new team') .click() cy.url() .should('contain', '/teams/new') @@ -113,7 +113,7 @@ describe('Team', () => { cy.get('.card') .contains('Team Members') .get('.card-content .button') - .contains('Add To Team') + .contains('Add to team') .click() cy.get('table.table td') diff --git a/cypress/integration/task/task.spec.js b/cypress/integration/task/task.spec.js index e07936375..ac9481156 100644 --- a/cypress/integration/task/task.spec.js +++ b/cypress/integration/task/task.spec.js @@ -27,7 +27,7 @@ describe('Task', () => { it('Should be created new', () => { cy.visit('/lists/1/list') - cy.get('input.input[placeholder="Add a new task..."') + cy.get('input.input[placeholder="Add a new task…"') .type('New Task') cy.get('.button') .contains('Add') @@ -43,7 +43,7 @@ describe('Task', () => { cy.visit('/lists/1/list') cy.get('.list-is-empty-notice') .should('not.exist') - cy.get('input.input[placeholder="Add a new task..."') + cy.get('input.input[placeholder="Add a new task…"') .type('New Task') cy.get('.button') .contains('Add') diff --git a/package.json b/package.json index de3b34283..e7aaef5c0 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ }, "dependencies": { "browserslist": "4.16.6", - "bulma": "0.9.2", + "bulma": "0.9.3", "camel-case": "4.1.2", "copy-to-clipboard": "3.3.1", "date-fns": "2.22.1", "dompurify": "2.2.9", "highlight.js": "11.0.1", "lodash": "4.17.21", - "marked": "2.0.7", + "marked": "2.1.3", "register-service-worker": "1.7.2", "sass": "1.35.1", "snake-case": "3.0.4", @@ -30,6 +30,7 @@ "vue-advanced-cropper": "1.7.0", "vue-drag-resize": "1.5.4", "vue-easymde": "1.4.0", + "vue-i18n": "8.24.5", "vue-shortkey": "3.1.7", "vue-smooth-dnd": "0.8.1", "vuex": "3.6.2" @@ -46,18 +47,18 @@ "@vue/cli-service": "4.5.13", "axios": "0.21.1", "babel-eslint": "10.1.0", - "cypress": "7.5.0", - "cypress-file-upload": "5.0.7", - "eslint": "7.28.0", - "eslint-plugin-vue": "7.11.1", + "cypress": "7.6.0", + "cypress-file-upload": "5.0.8", + "eslint": "7.29.0", + "eslint-plugin-vue": "7.12.1", "faker": "5.5.3", - "jest": "27.0.4", + "jest": "27.0.5", "sass-loader": "10.2.0", "vue-flatpickr-component": "8.1.6", "vue-notification": "1.3.20", - "vue-router": "3.5.1", + "vue-router": "3.5.2", "vue-template-compiler": "2.6.14", - "wait-on": "5.3.0" + "wait-on": "6.0.0" }, "eslintConfig": { "root": true, diff --git a/ping-weblate.sh b/ping-weblate.sh new file mode 100755 index 000000000..af0e2e019 --- /dev/null +++ b/ping-weblate.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +# Shell script because yaml doesn't understand the header is a string literal and not a yaml symbol + +curl -d operation=pull -H "Authorization: Token $WEBLATE_TOKEN" https://hosted.weblate.org/api/projects/vikunja/repository/ +curl -d operation=push -H "Authorization: Token $WEBLATE_TOKEN" https://hosted.weblate.org/api/projects/vikunja/repository/ diff --git a/src/App.vue b/src/App.vue index cb017b6b7..774d81617 100644 --- a/src/App.vue +++ b/src/App.vue @@ -34,6 +34,7 @@ import TopNavigation from '@/components/home/topNavigation' import ContentAuth from '@/components/home/contentAuth' import ContentLinkShare from '@/components/home/contentLinkShare' import ContentNoAuth from '@/components/home/contentNoAuth' +import {setLanguage} from '@/i18n/setup' export default { name: 'app', @@ -53,6 +54,8 @@ export default { beforeCreate() { this.$store.dispatch('config/update') this.$store.dispatch('auth/checkAuth') + + setLanguage() }, created() { // Make sure to always load the home route when running with electron diff --git a/src/components/home/contentAuth.vue b/src/components/home/contentAuth.vue index 576d77bf0..55ab90f8d 100644 --- a/src/components/home/contentAuth.vue +++ b/src/components/home/contentAuth.vue @@ -130,7 +130,7 @@ export default { loadLabels() { this.$store.dispatch('labels/loadAllLabels') .catch(e => { - this.error(e, this) + this.error(e) }) }, }, diff --git a/src/components/home/contentLinkShare.vue b/src/components/home/contentLinkShare.vue index 0cf685568..e3602c561 100644 --- a/src/components/home/contentLinkShare.vue +++ b/src/components/home/contentLinkShare.vue @@ -10,12 +10,12 @@

- {{ currentList.title === '' ? 'Loading...' : currentList.title }} + {{ currentList.title === '' ? $t('misc.loading') : currentList.title }}

- Logout + {{ $t('user.auth.logout') }} @@ -23,7 +23,7 @@
- Powered by Vikunja + {{ $t('misc.poweredBy') }}
diff --git a/src/components/home/contentNoAuth.vue b/src/components/home/contentNoAuth.vue index ad8dc789e..f8349fbdb 100644 --- a/src/components/home/contentNoAuth.vue +++ b/src/components/home/contentNoAuth.vue @@ -4,7 +4,7 @@ Vikunja
-

Info

+

{{ $t('misc.info') }}

{{ motd }} diff --git a/src/components/home/navigation.vue b/src/components/home/navigation.vue index a2933d759..65b045088 100644 --- a/src/components/home/navigation.vue +++ b/src/components/home/navigation.vue @@ -10,7 +10,7 @@ - Overview + {{ $t('navigation.overview') }}
  • @@ -18,7 +18,7 @@ - Upcoming + {{ $t('navigation.upcoming') }}
  • @@ -26,7 +26,7 @@ - Namespaces & Lists + {{ $t('namespace.title') }}
  • @@ -34,7 +34,7 @@ - Labels + {{ $t('label.title') }}
  • @@ -42,7 +42,7 @@ - Teams + {{ $t('team.title') }}
  • @@ -109,7 +109,9 @@
    - Powered by Vikunja + + {{ $t('misc.poweredBy') }} +
    @@ -161,7 +163,7 @@ export default { return } this.$store.dispatch('lists/toggleListFavorite', list) - .catch(e => this.error(e, this)) + .catch(e => this.error(e)) }, resize() { // Hide the menu by default on mobile diff --git a/src/components/home/topNavigation.vue b/src/components/home/topNavigation.vue index 5a475f154..385f971a2 100644 --- a/src/components/home/topNavigation.vue +++ b/src/components/home/topNavigation.vue @@ -25,14 +25,16 @@ > -
    -

    - {{ currentList.title === '' ? 'Loading...' : currentList.title }} -

    +
    +
    @@ -129,112 +129,112 @@ export default { { name: 'heading-1', action: EasyMDE.toggleHeading1, - title: 'Heading 1', + title: this.$t('input.editor.heading1'), icon: '', }, { name: 'heading-2', action: EasyMDE.toggleHeading2, - title: 'Heading 2', + title: this.$t('input.editor.heading2'), icon: '', }, { name: 'heading-3', action: EasyMDE.toggleHeading3, - title: 'Heading 3', + title: this.$t('input.editor.heading3'), icon: '', }, { name: 'heading-smaller', action: EasyMDE.toggleHeadingSmaller, - title: 'Heading Smaller', + title: this.$t('input.editor.headingSmaller'), icon: '', }, { name: 'heading-bigger', action: EasyMDE.toggleHeadingBigger, - title: 'Heading Bigger', + title: this.$t('input.editor.headingBigger'), icon: '', }, '|', { name: 'bold', action: EasyMDE.toggleBold, - title: 'Bold', + title: this.$t('input.editor.bold'), icon: '', }, { name: 'italic', action: EasyMDE.toggleItalic, - title: 'Italic', + title: this.$t('input.editor.italic'), icon: '', }, { name: 'strikethrough', action: EasyMDE.toggleStrikethrough, - title: 'Strikethrough', + title: this.$t('input.editor.strikethrough'), icon: '', }, { name: 'code', action: EasyMDE.toggleCodeBlock, - title: 'Code', + title: this.$t('input.editor.code'), icon: '', }, { name: 'quote', action: EasyMDE.toggleBlockquote, - title: 'Quote', + title: this.$t('input.editor.quote'), icon: '', }, { name: 'unordered-list', action: EasyMDE.toggleUnorderedList, - title: 'Unordered List', + title: this.$t('input.editor.unorderedList'), icon: '', }, { name: 'ordered-list', action: EasyMDE.toggleOrderedList, - title: 'Ordered List', + title: this.$t('input.editor.orderedList'), icon: '', }, '|', { name: 'clean-block', action: EasyMDE.cleanBlock, - title: 'Clean Block', + title: this.$t('input.editor.cleanBlock'), icon: '', }, { name: 'link', action: EasyMDE.drawLink, - title: 'Link', + title: this.$t('input.editor.link'), icon: '', }, { name: 'image', action: EasyMDE.drawImage, - title: 'Image', + title: this.$t('input.editor.image'), icon: '', }, { name: 'table', action: EasyMDE.drawTable, - title: 'Table', + title: this.$t('input.editor.table'), icon: '', }, { name: 'horizontal-rule', action: EasyMDE.drawHorizontalRule, - title: 'Horizontal Rule', + title: this.$t('input.editor.horizontalRule'), icon: '', }, '|', { name: 'side-by-side', action: EasyMDE.toggleSideBySide, - title: 'Side By Side', + title: this.$t('input.editor.sideBySide'), icon: '', }, { @@ -242,7 +242,7 @@ export default { action: () => { window.open('https://www.markdownguide.org/basic-syntax/', '_blank') }, - title: 'Guide', + title: this.$t('input.editor.guide'), icon: '', }, ], diff --git a/src/components/input/multiselect.vue b/src/components/input/multiselect.vue index 220113d72..a6ac83b6d 100644 --- a/src/components/input/multiselect.vue +++ b/src/components/input/multiselect.vue @@ -28,7 +28,7 @@ :placeholder="placeholder" @keydown.down.exact.prevent="() => preSelect(0)" ref="searchInput" - @focus="() => showSearchResults = true" + @focus="handleFocus" />
    @@ -149,15 +149,15 @@ export default { createPlaceholder: { type: String, default() { - return 'Create new' - }, + return this.$t('input.multiselect.createPlaceholder') + } }, // The text shown next to an option. selectPlaceholder: { type: String, default() { - return 'Click or press enter to select' - }, + return this.$t('input.multiselect.selectPlaceholder') + } }, // If true, allows for selecting multiple items. v-model will be an array with all selected values in that case. multiple: { @@ -258,6 +258,13 @@ export default { closeSearchResults() { this.showSearchResults = false }, + handleFocus() { + // We need the timeout to avoid the hideSearchResultsHandler hiding the search results right after the input + // is focused. That would lead to flickering pre-loaded search results and hiding them right after showing. + setTimeout(() => { + this.showSearchResults = true + }, 10) + }, select(object) { if (this.multiple) { if (this.internalValue === null) { diff --git a/src/components/list/list-settings-dropdown.vue b/src/components/list/list-settings-dropdown.vue index 090defb1f..e44bec3a1 100644 --- a/src/components/list/list-settings-dropdown.vue +++ b/src/components/list/list-settings-dropdown.vue @@ -5,13 +5,13 @@ :to="{ name: `${listRoutePrefix}.settings.edit`, params: { listId: list.id } }" icon="pen" > - Edit + {{ $t('menu.edit') }} - Delete + {{ $t('misc.delete') }} diff --git a/src/components/list/partials/filters.vue b/src/components/list/partials/filters.vue index a28f339e6..1bb586593 100644 --- a/src/components/list/partials/filters.vue +++ b/src/components/list/partials/filters.vue @@ -1,28 +1,30 @@