1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-23 22:56:53 +09:00

feat: 노트 자동 번역 기능

- 노트 자동 번역은 번역 서비스의 API 제한을 방지하기 위해 자동으로 활성화되지 않으며, 기본적으로 비활성화되어 있습니다.
  - `역할`에서 `자동 번역 기능 이용 가능 여부`를 활성화 하면 자동 번역을 사용할 수 있는 상태가 됩니다.
  - 이후, 각 사용자별로 `설정` - `일반`에서 `노트 자동 번역`을 활성화한 사용자는 자동 번역을 사용할 수 있습니다.
  - 노트가 아래와 같이 설정된 경우에는 노트 자동 번역을 사용하지 않습니다.
    - 노트가 `내용 가리기`로 설정되어 있음
    - 노트의 내용이 긺
    - 노트에 파일이 포함되어 있음
  - `자동 번역 기능 이용 가능 여부` 역할의 권한을 상실하게 되면 모든 사용자의 `노트 자동 번역` 설정도 자동으로 비활성화 됩니다.
This commit is contained in:
NoriDev 2024-10-07 01:52:21 +09:00
parent e81a078a76
commit 40a6d0b7f3
17 changed files with 97 additions and 5 deletions

View File

@ -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)

View File

@ -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
View File

@ -13,6 +13,15 @@ export interface Locale extends ILocale {
*
*/
"_lang_": string;
/**
*
*/
"useAutoTranslate": string;
/**
*
* 使
*/
"useAutoTranslateDescription": string;
/**
*
*/

View File

@ -1,5 +1,7 @@
_lang_: "日本語"
useAutoTranslate: "ノートを自動翻訳"
useAutoTranslateDescription: "サーバー管理者がこの機能を無効にしました。\n機能を使用するには、サーバー管理者にお問い合わせください。"
widgets: "ウィジェット"
postNote: "ノートを作成"
bottomNavbar: "下のナビゲーションバー"

View File

@ -1,5 +1,7 @@
---
_lang_: "한국어"
useAutoTranslate: "노트 자동 번역"
useAutoTranslateDescription: "서버 관리자가 이 기능을 사용할 수 없도록 설정했어요.\n기능을 사용하려면 서버 관리자에게 문의해 주세요."
widgets: "위젯"
postNote: "노트 작성"
bottomNavbar: "하단 내비게이션 바"
@ -1991,6 +1993,7 @@ _role:
canHideAds: "광고 숨기기"
canSearchNotes: "노트 검색 이용 가능 여부"
canUseTranslator: "번역 기능 이용 가능 여부"
canUseAutoTranslate: "자동 번역 기능 이용 가능 여부"
avatarDecorationLimit: "최대로 붙일 수 있는 아바타 장식 개수"
canImportAntennas: "안테나 가져오기 허용"
canImportBlocking: "차단 목록 가져오기 허용"

View File

@ -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)),

View File

@ -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,

View File

@ -5174,6 +5174,7 @@ export type components = {
canManageAvatarDecorations: boolean;
canSearchNotes: boolean;
canUseTranslator: boolean;
canUseAutoTranslate: boolean;
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;

View File

@ -88,6 +88,7 @@ export const ROLE_POLICIES = [
'canManageAvatarDecorations',
'canSearchNotes',
'canUseTranslator',
'canUseAutoTranslate',
'canHideAds',
'driveCapacityMb',
'alwaysMarkNsfw',

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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);
}, {

View File

@ -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,

View File

@ -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,