mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-11-23 22:56:53 +09:00
feat: 노트 자동 번역 기능
- 노트 자동 번역은 번역 서비스의 API 제한을 방지하기 위해 자동으로 활성화되지 않으며, 기본적으로 비활성화되어 있습니다. - `역할`에서 `자동 번역 기능 이용 가능 여부`를 활성화 하면 자동 번역을 사용할 수 있는 상태가 됩니다. - 이후, 각 사용자별로 `설정` - `일반`에서 `노트 자동 번역`을 활성화한 사용자는 자동 번역을 사용할 수 있습니다. - 노트가 아래와 같이 설정된 경우에는 노트 자동 번역을 사용하지 않습니다. - 노트가 `내용 가리기`로 설정되어 있음 - 노트의 내용이 긺 - 노트에 파일이 포함되어 있음 - `자동 번역 기능 이용 가능 여부` 역할의 권한을 상실하게 되면 모든 사용자의 `노트 자동 번역` 설정도 자동으로 비활성화 됩니다.
This commit is contained in:
parent
e81a078a76
commit
40a6d0b7f3
@ -63,6 +63,15 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE
|
||||
- Feat: 모바일 환경에서 하단 내비게이션 바를 개인화할 수 있음
|
||||
- `설정` - `내비게이션 바`의 `하단 내비게이션 바`에서 설정할 수 있습니다.
|
||||
- Feat: 리버시 대전 중에 상대방에게 리액션을 보낼 수 있음 (misskey-dev/misskey#13119)
|
||||
- Feat: 노트 자동 번역 기능
|
||||
- 노트 자동 번역은 번역 서비스의 API 제한을 방지하기 위해 자동으로 활성화되지 않으며, 기본적으로 비활성화되어 있습니다.
|
||||
- `역할`에서 `자동 번역 기능 이용 가능 여부`를 활성화 하면 자동 번역을 사용할 수 있는 상태가 됩니다.
|
||||
- 이후, 각 사용자별로 `설정` - `일반`에서 `노트 자동 번역`을 활성화한 사용자는 자동 번역을 사용할 수 있습니다.
|
||||
- 노트가 아래와 같이 설정된 경우에는 노트 자동 번역을 사용하지 않습니다.
|
||||
- 노트가 `내용 가리기`로 설정되어 있음
|
||||
- 노트의 내용이 긺
|
||||
- 노트에 파일이 포함되어 있음
|
||||
- `자동 번역 기능 이용 가능 여부` 역할의 권한을 상실하게 되면 모든 사용자의 `노트 자동 번역` 설정도 자동으로 비활성화 됩니다.
|
||||
|
||||
### Client
|
||||
- Enhance: CherryPick 업데이트 페이지를 제어판 목록에 추가함
|
||||
@ -88,6 +97,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE
|
||||
- Fix: 이미지 자르기를 할 때 이미지 크기 전체를 표시하지 못할 수 있음
|
||||
- Fix: 페이지에서 페이지 생성 버튼이 본문에 중복으로 표시됨
|
||||
- Fix: 노트 본문의 사용자 멘션 영역을 클릭하면 노트 상세 페이지가 표시됨
|
||||
- Fix: 역할 권한에 의해 번역 기능을 사용할 수 없을 때도 번역 버튼이 표시됨
|
||||
|
||||
### Server
|
||||
- Enhance: 보안 향상을 위해 비밀번호 해싱 알고리즘이 `bcrypt`에서 `argon2`로 변경됨 (kokonect-link/cherrypick#511), (1673beta/cherrypick#88)
|
||||
|
@ -1,5 +1,7 @@
|
||||
---
|
||||
_lang_: "English"
|
||||
useAutoTranslate: "Automatic translation of notes"
|
||||
useAutoTranslateDescription: "The server administrator has disabled this feature.\nContact your server administrator to use the feature."
|
||||
widgets: "Widgets"
|
||||
postNote: "Post note"
|
||||
bottomNavbar: "Bottom navigation bar"
|
||||
|
9
locales/index.d.ts
vendored
9
locales/index.d.ts
vendored
@ -13,6 +13,15 @@ export interface Locale extends ILocale {
|
||||
* 日本語
|
||||
*/
|
||||
"_lang_": string;
|
||||
/**
|
||||
* ノートを自動翻訳
|
||||
*/
|
||||
"useAutoTranslate": string;
|
||||
/**
|
||||
* サーバー管理者がこの機能を無効にしました。
|
||||
* 機能を使用するには、サーバー管理者にお問い合わせください。
|
||||
*/
|
||||
"useAutoTranslateDescription": string;
|
||||
/**
|
||||
* ウィジェット
|
||||
*/
|
||||
|
@ -1,5 +1,7 @@
|
||||
_lang_: "日本語"
|
||||
|
||||
useAutoTranslate: "ノートを自動翻訳"
|
||||
useAutoTranslateDescription: "サーバー管理者がこの機能を無効にしました。\n機能を使用するには、サーバー管理者にお問い合わせください。"
|
||||
widgets: "ウィジェット"
|
||||
postNote: "ノートを作成"
|
||||
bottomNavbar: "下のナビゲーションバー"
|
||||
|
@ -1,5 +1,7 @@
|
||||
---
|
||||
_lang_: "한국어"
|
||||
useAutoTranslate: "노트 자동 번역"
|
||||
useAutoTranslateDescription: "서버 관리자가 이 기능을 사용할 수 없도록 설정했어요.\n기능을 사용하려면 서버 관리자에게 문의해 주세요."
|
||||
widgets: "위젯"
|
||||
postNote: "노트 작성"
|
||||
bottomNavbar: "하단 내비게이션 바"
|
||||
@ -1991,6 +1993,7 @@ _role:
|
||||
canHideAds: "광고 숨기기"
|
||||
canSearchNotes: "노트 검색 이용 가능 여부"
|
||||
canUseTranslator: "번역 기능 이용 가능 여부"
|
||||
canUseAutoTranslate: "자동 번역 기능 이용 가능 여부"
|
||||
avatarDecorationLimit: "최대로 붙일 수 있는 아바타 장식 개수"
|
||||
canImportAntennas: "안테나 가져오기 허용"
|
||||
canImportBlocking: "차단 목록 가져오기 허용"
|
||||
|
@ -45,6 +45,7 @@ export type RolePolicies = {
|
||||
canManageAvatarDecorations: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canUseTranslator: boolean;
|
||||
canUseAutoTranslate: boolean;
|
||||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
@ -80,6 +81,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canManageAvatarDecorations: false,
|
||||
canSearchNotes: false,
|
||||
canUseTranslator: true,
|
||||
canUseAutoTranslate: false,
|
||||
canHideAds: false,
|
||||
driveCapacityMb: 100,
|
||||
alwaysMarkNsfw: false,
|
||||
@ -386,6 +388,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
|
||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||
canUseAutoTranslate: calc('canUseAutoTranslate', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||
|
@ -216,6 +216,10 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canUseAutoTranslate: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canHideAds: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -5174,6 +5174,7 @@ export type components = {
|
||||
canManageAvatarDecorations: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canUseTranslator: boolean;
|
||||
canUseAutoTranslate: boolean;
|
||||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
|
@ -88,6 +88,7 @@ export const ROLE_POLICIES = [
|
||||
'canManageAvatarDecorations',
|
||||
'canSearchNotes',
|
||||
'canUseTranslator',
|
||||
'canUseAutoTranslate',
|
||||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'alwaysMarkNsfw',
|
||||
|
@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && appearNote.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && (!defaultStore.state.useAutoTranslate || (defaultStore.state.useAutoTranslate && isLong)) && instance.translatorAvailable && $i && $i.policies.canUseTranslator && appearNote.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @click.stop="translate()">{{ i18n.ts.translateNote }}</button>
|
||||
<button v-else class="_button" @click.stop="translation = null">{{ i18n.ts.close }}</button>
|
||||
</div>
|
||||
@ -741,6 +741,9 @@ const isForeignLanguage: boolean = appearNote.value.text != null && (() => {
|
||||
return postLang !== '' && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
if (defaultStore.state.useAutoTranslate && !$i.policies.canUseAutoTranslate) defaultStore.set('useAutoTranslate', false);
|
||||
if ($i.policies.canUseTranslator && defaultStore.state.useAutoTranslate && !isLong && appearNote.value.text && isForeignLanguage) translate();
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
|
@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && appearNote.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && !defaultStore.state.useAutoTranslate && instance.translatorAvailable && $i && $i.policies.canUseTranslator && appearNote.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @click="translate()">{{ i18n.ts.translateNote }}</button>
|
||||
<button v-else class="_button" @click="translation = null">{{ i18n.ts.close }}</button>
|
||||
</div>
|
||||
@ -681,6 +681,9 @@ const isForeignLanguage: boolean = appearNote.value.text != null && (() => {
|
||||
return postLang !== '' && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
if (defaultStore.state.useAutoTranslate && !$i.policies.canUseAutoTranslate) defaultStore.set('useAutoTranslate', false);
|
||||
if ($i.policies.canUseTranslator && defaultStore.state.useAutoTranslate && appearNote.value.text && isForeignLanguage) translate();
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && note.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && (!defaultStore.state.useAutoTranslate || (defaultStore.state.useAutoTranslate && isLong)) && instance.translatorAvailable && $i && $i.policies.canUseTranslator && note.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @click.stop="translate()">{{ i18n.ts.translateNote }}</button>
|
||||
<button v-else class="_button" @click.stop="translation = null">{{ i18n.ts.close }}</button>
|
||||
</div>
|
||||
@ -441,6 +441,9 @@ const isForeignLanguage: boolean = note.value.text != null && (() => {
|
||||
return postLang !== '' && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
if (defaultStore.state.useAutoTranslate && !$i.policies.canUseAutoTranslate) defaultStore.set('useAutoTranslate', false);
|
||||
if ($i.policies.canUseTranslator && defaultStore.state.useAutoTranslate && !isLong && note.value.text && isForeignLanguage) translate();
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
|
@ -358,6 +358,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAutoTranslate, 'canUseAutoTranslate'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseAutoTranslate }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canUseAutoTranslate.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canUseAutoTranslate.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseAutoTranslate)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canUseAutoTranslate.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canUseAutoTranslate.value" :disabled="role.policies.canUseAutoTranslate.useDefault || !role.policies.canUseTranslator.value" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canUseAutoTranslate.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>
|
||||
|
@ -121,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canSearchNotes'])">
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canUseTranslator'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUseTranslator">
|
||||
@ -129,6 +129,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAutoTranslate, 'canUseAutoTranslate'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseAutoTranslate }}</template>
|
||||
<template #suffix>{{ policies.canUseAutoTranslate ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUseAutoTranslate">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>{{ policies.driveCapacityMb }}MB</template>
|
||||
|
@ -51,6 +51,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
|
||||
<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
|
||||
<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
|
||||
<MkSwitch v-model="useAutoTranslate" :disabled="!$i.policies.canUseTranslator || !$i.policies.canUseAutoTranslate">
|
||||
{{ i18n.ts.useAutoTranslate }} <span class="_beta">CherryPick</span>
|
||||
<template v-if="!$i.policies.canUseAutoTranslate" #caption>{{ i18n.ts.cannotBeUsedFunc }} <a class="_link" @click="learnMoreAutoTranslate">{{ i18n.ts.learnMore }}</a></template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }} <span class="_beta">CherryPick</span></template>
|
||||
@ -161,6 +165,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import {$i} from "@/account.js";
|
||||
|
||||
const lang = ref(miLocalStorage.getItem('lang'));
|
||||
const dataSaver = ref(defaultStore.state.dataSaver);
|
||||
@ -186,6 +191,7 @@ const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSette
|
||||
const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
|
||||
const newNoteReceivedNotificationBehavior = computed(defaultStore.makeGetterSetter('newNoteReceivedNotificationBehavior'));
|
||||
const requireRefreshBehavior = computed(defaultStore.makeGetterSetter('requireRefreshBehavior'));
|
||||
const useAutoTranslate = computed(defaultStore.makeGetterSetter('useAutoTranslate'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
@ -208,6 +214,7 @@ watch([
|
||||
|
||||
watch([
|
||||
enableInfiniteScroll,
|
||||
useAutoTranslate,
|
||||
], () => {
|
||||
reloadTimeline();
|
||||
});
|
||||
@ -285,6 +292,13 @@ function disableAllDataSaver() {
|
||||
dataSaver.value = g;
|
||||
}
|
||||
|
||||
function learnMoreAutoTranslate() {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts.useAutoTranslateDescription,
|
||||
});
|
||||
}
|
||||
|
||||
watch(dataSaver, (to) => {
|
||||
defaultStore.set('dataSaver', to);
|
||||
}, {
|
||||
|
@ -23,6 +23,7 @@ import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
||||
import { addDividersBetweenMenuSections } from '@/scripts/add-dividers-between-menu-sections.js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
|
||||
export async function getNoteClipMenu(props: {
|
||||
note: Misskey.entities.Note;
|
||||
@ -422,7 +423,8 @@ export function getNoteMenu(props: {
|
||||
action: openInNewTab,
|
||||
});
|
||||
|
||||
if ($i.policies.canUseTranslator && instance.translatorAvailable) {
|
||||
const isLong = shouldCollapsed(appearNote, []);
|
||||
if ($i.policies.canUseTranslator && instance.translatorAvailable && (!defaultStore.state.useAutoTranslate || (defaultStore.state.useAutoTranslate && isLong))) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-language-hiragana',
|
||||
text: i18n.ts.translate,
|
||||
|
@ -576,6 +576,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
useAutoTranslate: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
enableAbsoluteTime: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
|
Loading…
Reference in New Issue
Block a user