diff --git a/src/boot/after_store.js b/src/boot/after_store.js index ce93436f..aab74f2a 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -275,6 +275,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) + store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') }) const uploadLimits = metadata.uploadLimits store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index edc57d89..6d9713ff 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -8,7 +8,6 @@ <button class="emoji-reaction btn button-default" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" - :disabled="!isLocalReaction(reaction.url)" @click="emojiOnClick(reaction.name, $event)" @mouseenter="fetchEmojiReactionsByIfMissing()" > diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index f7045110..97fb7d6e 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -55,6 +55,12 @@ const ExtraButtons = { hideDeleteStatusConfirmDialog () { this.showingDeleteDialog = false }, + + translateStatus () { + this.$store.dispatch('translateStatus', { id: this.status.id, language: this.$store.state.instance.interfaceLanguage }) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) + }, pinStatus () { this.$store.dispatch('pinStatus', this.status.id) .then(() => this.$emit('onSuccess')) @@ -110,6 +116,9 @@ const ExtraButtons = { canMute () { return !!this.currentUser }, + canTranslate () { + return this.$store.state.instance.translationEnabled === true + }, statusLink () { return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` }, diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 2b574bbd..bbb140e4 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -116,6 +116,17 @@ :icon="['far', 'flag']" /><span>{{ $t("user_card.report") }}</span> </button> + <button + v-if="canTranslate" + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="translateStatus" + @click="close" + > + <FAIcon + fixed-width + icon="globe" + /><span>{{ $t("status.translate") }}</span> + </button> </div> </template> <template v-slot:trigger> diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss index 0f946f79..230a27ac 100644 --- a/src/components/status_body/status_body.scss +++ b/src/components/status_body/status_body.scss @@ -4,6 +4,12 @@ display: flex; flex-direction: column; + .translation { + border: 1px solid var(--accent, $fallback--link); + border-radius: var(--panelRadius, $fallback--panelRadius); + margin-top: 1em; + padding: 0.5em; + } .emoji { --_still_image-label-scale: 0.5; --emoji-size: 38px; diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue index 321f3c4b..c1a3d9e7 100644 --- a/src/components/status_body/status_body.vue +++ b/src/components/status_body/status_body.vue @@ -56,6 +56,23 @@ :attentions="status.attentions" @parseReady="onParseReady" /> + <div + v-if="status.translation" + class="translation" + > + <h4>{{ $t('status.translated_from', { language: status.translation.detected_language }) }}</h4> + <RichContent + :class="{ '-single-line': singleLine }" + class="text media-body" + :html="status.translation.text" + :emoji="status.emojis" + :handle-links="true" + :mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')" + :greentext="mergedConfig.greentext" + :attentions="status.attentions" + @parseReady="onParseReady" + /> + </div> </div> <button v-show="hideSubjectStatus" diff --git a/src/i18n/en.json b/src/i18n/en.json index 7d845ca4..3671a2a2 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -875,6 +875,8 @@ "thread_show": "Show this thread", "thread_show_full": "Show everything under this thread ({numStatus} post in total, max depth {depth}) | Show everything under this thread ({numStatus} posts in total, max depth {depth})", "thread_show_full_with_icon": "{icon} {text}", + "translate": "Translate", + "translated_from": "Translated from {language}", "unbookmark": "Unbookmark", "unmute_conversation": "Unmute conversation", "unpin": "Unpin from profile", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index 1858ed92..888afe29 100644 --- a/src/i18n/ja_pedantic.json +++ b/src/i18n/ja_pedantic.json @@ -791,6 +791,8 @@ "status_unavailable": "利用できません", "thread_muted": "ミュートされたスレッド", "thread_muted_and_words": "以下の単語を含むため:", + "translate": "翻訳", + "translated_from": "{language}から翻訳されました", "unbookmark": "ブックマーク解除", "unmute_conversation": "スレッドのミュートを解除", "unpin": "プロフィールのピン留めを外す", diff --git a/src/modules/config.js b/src/modules/config.js index f97e5a8f..789c0640 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -187,6 +187,7 @@ const config = { case 'interfaceLanguage': messages.setLanguage(this.getters.i18n, value) Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value)) + dispatch('setInstanceOption', { name: 'interfaceLanguage', value }) break case 'thirdColumnMode': dispatch('setLayoutWidth', undefined) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 56e48759..8bb8abb9 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -425,6 +425,10 @@ export const mutations = { state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted }) } }, + setTranslatedStatus (state, { id, translation }) { + const newStatus = state.allStatusesObject[id] + newStatus.translation = translation + }, setRetweeted (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -637,6 +641,10 @@ const statuses = { rootState.api.backendInteractor.unpinOwnStatus({ id: statusId }) .then((status) => dispatch('addNewStatuses', { statuses: [status] })) }, + translateStatus ({ rootState, commit }, { id, translation, language }) { + return rootState.api.backendInteractor.translateStatus({ id: id, translation, language }) + .then((translation) => commit('setTranslatedStatus', { id, translation })) + }, muteConversation ({ rootState, commit }, statusId) { return rootState.api.backendInteractor.muteConversation({ id: statusId }) .then((status) => commit('setMutedStatus', status)) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 686689cd..cc2da9c5 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -31,6 +31,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' +const AKKOMA_TRANSLATE_URL = (id, lang) => `/api/v1/statuses/${id}/translations/${lang}` const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss` const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` @@ -738,6 +739,13 @@ const unretweet = ({ id, credentials }) => { .then((data) => parseStatus(data)) } +const translateStatus = ({ id, credentials, language }) => { + return promisedRequest({ url: AKKOMA_TRANSLATE_URL(id, language), method: 'GET', credentials }) + .then((data) => { + return data + }) +} + const bookmarkStatus = ({ id, credentials }) => { return promisedRequest({ url: MASTODON_BOOKMARK_STATUS_URL(id), @@ -1576,7 +1584,8 @@ const apiService = { postAnnouncement, editAnnouncement, deleteAnnouncement, - adminFetchAnnouncements + adminFetchAnnouncements, + translateStatus } export default apiService