diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index a028e2685e..8b11c8413c 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -215,20 +215,9 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - allowedPrivateNetworks: [ '127.0.0.1/32' ] -# Disable automatic redirect for ActivityPub object lookup. (default: false) -# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. -# However it will make it impossible for other instances to lookup third-party user and notes through your URL. -#disallowExternalApRedirect: true - # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 4be1352bd7..dc354324dc 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -227,12 +227,6 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). @@ -240,11 +234,6 @@ signToActivityPubGet: true # '127.0.0.1/32' #] -# Disable automatic redirect for ActivityPub object lookup. (default: false) -# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. -# However it will make it impossible for other instances to lookup third-party user and notes through your URL. -#disallowExternalApRedirect: true - # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/example.yml b/.config/example.yml index d4584215c9..c127eaae22 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -319,19 +319,12 @@ proxyBypassHosts: # * Perform image compression (on a different server resource than the main process) #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. -proxyRemoteFiles: true - # Movie Thumbnail Generation URL # There is no reference implementation. # For example, Misskey will point to the following URL: # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). @@ -339,11 +332,6 @@ signToActivityPubGet: true # '127.0.0.1/32' #] -# Disable automatic redirect for ActivityPub object lookup. (default: false) -# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. -# However it will make it impossible for other instances to lookup third-party user and notes through your URL. -#disallowExternalApRedirect: true - # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 6d904e87b9..fb0d25c214 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -202,12 +202,6 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - allowedPrivateNetworks: [ '127.0.0.1/32' ] diff --git a/.editorconfig b/.editorconfig index def7baa1a8..ccf388f06e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,3 +13,7 @@ trim_trailing_whitespace = false [*.{yml,yaml}] indent_style = space + +[packages/backend/migration/*.js] +indent_style = space +indent_size = 4 diff --git a/.github/min.node-version b/.github/min.node-version index d5a159609d..b8ffd70759 100644 --- a/.github/min.node-version +++ b/.github/min.node-version @@ -1 +1 @@ -20.10.0 +22.15.0 diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml index 3c807e8b9e..513bfb1ac0 100644 --- a/.github/misskey/test.yml +++ b/.github/misskey/test.yml @@ -15,3 +15,5 @@ redis: host: 127.0.0.1 port: 56312 id: aidx + +proxyRemoteFiles: true diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml index bc6be308d1..cf1fd6007d 100644 --- a/.github/workflows/check-spdx-license-id.yml +++ b/.github/workflows/check-spdx-license-id.yml @@ -50,6 +50,7 @@ jobs: "packages/backend/test" "packages/frontend-shared/@types" "packages/frontend-shared/js" + "packages/frontend-builder" "packages/frontend/.storybook" "packages/frontend/@types" "packages/frontend/lib" @@ -58,6 +59,7 @@ jobs: "packages/frontend/test" "packages/frontend-embed/@types" "packages/frontend-embed/src" + "packages/icons-subsetter/src" "packages/misskey-bubble-game/src" "packages/misskey-reversi/src" "packages/sw/src" diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index 3054607913..f006a45ea4 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -25,7 +25,7 @@ jobs: cp ./compose_example.yml ./compose.yml - run: | docker compose up -d web - docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest + docker tag "$(docker compose images --format json web | jq -r '.[] | .ID')" misskey-web:latest - run: | cmd="dockle --exit-code 1 misskey-web:latest ${image_name}" echo "> ${cmd}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f27cce5b97..550438e308 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,9 @@ on: - packages/backend/** - packages/frontend/** - packages/frontend-shared/** + - packages/frontend-builder/** - packages/frontend-embed/** + - packages/icons-subsetter/** - packages/sw/** - packages/misskey-js/** - packages/misskey-bubble-game/** @@ -21,7 +23,9 @@ on: - packages/backend/** - packages/frontend/** - packages/frontend-shared/** + - packages/frontend-builder/** - packages/frontend-embed/** + - packages/icons-subsetter/** - packages/sw/** - packages/misskey-js/** - packages/misskey-bubble-game/** @@ -54,7 +58,9 @@ jobs: - backend - frontend - frontend-shared + - frontend-builder - frontend-embed + - icons-subsetter - sw - misskey-js - misskey-bubble-game diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index c156de1a8b..7787d6055b 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -26,6 +26,8 @@ jobs: with: node-version-file: '.node-version' cache: 'pnpm' + # see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry + registry-url: 'https://registry.npmjs.org' - name: Publish package run: | pnpm i --frozen-lockfile diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 9d611c9964..5358df3dc4 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -18,6 +18,14 @@ on: - packages/misskey-js/** - .github/workflows/test-backend.yml - .github/misskey/test.yml + workflow_dispatch: + inputs: + force_ffmpeg_cache_update: + description: 'Force update ffmpeg cache' + required: false + default: false + type: boolean + jobs: unit: name: Unit tests (backend) @@ -47,7 +55,22 @@ jobs: submodules: true - name: Setup pnpm uses: pnpm/action-setup@v4.1.0 + - name: Get current date + id: current-date + run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - name: Setup and Restore ffmpeg/ffprobe Cache + id: cache-ffmpeg + uses: actions/cache@v4 + with: + path: | + /usr/local/bin/ffmpeg + /usr/local/bin/ffprobe + # daily cache + key: ${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }} + restore-keys: | + ${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }} - name: Install FFmpeg + if: steps.cache-ffmpeg.outputs.cache-hit != 'true' || github.event.inputs.force_ffmpeg_cache_update == true run: | for i in {1..3}; do echo "Attempt $i: Installing FFmpeg..." @@ -86,6 +109,7 @@ jobs: name: E2E tests (backend) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: node-version-file: - .node-version @@ -129,3 +153,47 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} files: ./packages/backend/coverage/coverage-final.json + + migration: + name: Migration tests (backend) + runs-on: ubuntu-latest + strategy: + matrix: + node-version-file: + - .node-version + #- .github/min.node-version + + services: + postgres: + image: postgres:15 + ports: + - 54312:5432 + env: + POSTGRES_DB: test-misskey + POSTGRES_HOST_AUTH_METHOD: trust + + steps: + - uses: actions/checkout@v4.2.2 + with: + submodules: true + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 + - name: Get current date + id: current-date + run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - name: Use Node.js + uses: actions/setup-node@v4.4.0 + with: + node-version-file: ${{ matrix.node-version-file }} + cache: 'pnpm' + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .github/misskey/test.yml .config + - name: Build + run: pnpm build + - name: Run migrations + run: MISSKEY_CONFIG_YML=test.yml pnpm --filter backend migrate + - name: Check no migrations are remaining + run: MISSKEY_CONFIG_YML=test.yml pnpm --filter backend check-migrations diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml index 737b543a73..873396f622 100644 --- a/.github/workflows/test-federation.yml +++ b/.github/workflows/test-federation.yml @@ -14,6 +14,13 @@ on: - packages/backend/** - packages/misskey-js/** - .github/workflows/test-federation.yml + workflow_dispatch: + inputs: + force_ffmpeg_cache_update: + description: 'Force update ffmpeg cache' + required: false + default: false + type: boolean jobs: test: @@ -30,7 +37,22 @@ jobs: submodules: true - name: Setup pnpm uses: pnpm/action-setup@v4.1.0 + - name: Get current date + id: current-date + run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - name: Setup and Restore ffmpeg/ffprobe Cache + id: cache-ffmpeg + uses: actions/cache@v4 + with: + path: | + /usr/local/bin/ffmpeg + /usr/local/bin/ffprobe + # daily cache + key: ${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }} + restore-keys: | + ${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }} - name: Install FFmpeg + if: steps.cache-ffmpeg.outputs.cache-hit != 'true' || github.event.inputs.force_ffmpeg_cache_update == true run: | for i in {1..3}; do echo "Attempt $i: Installing FFmpeg..." diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ceec23acd..5f36a32af4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,8 +6,12 @@ "files.associations": { "*.test.ts": "typescript" }, - "jest.jestCommandLine": "pnpm run jest", "jest.runMode": "on-demand", + "jest.virtualFolders": [ + { "name": "backend unit", "jestCommandLine": "pnpm -F backend run test" }, + { "name": "backend e2e", "jestCommandLine": "pnpm -F backend run test:e2e"}, + { "name": "misskey-js", "jestCommandLine": "pnpm -F misskey-js run jest" } + ], "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index cd84f830cc..2702189568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,170 @@ +## 2025.8.0 + +### Note +- サポートされるNode.jsの最小バージョンが**22.15.0**になりました + +### General +- ノートを削除した際、関連するノートが同時に削除されないようになりました + - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります +- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning) + - 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります + - データベースの肥大化を防止することが可能です + - 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。 + - 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。 +- サーバーの初期設定が完了するまでは連合がオンにならないようになりました +- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました +- mfm.jsをアップデートしました + - Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応 + - Enhance: acctに `.` が入っているユーザーのメンションに対応 + - Fix: Unicode絵文字に隣接する異体字セレクタ(`U+FE0F`)が絵文字として認識される問題を修正 +- Enhance: ユーザー検索をロールポリシーで制限できるように + +### Client +- Feat: AiScriptが1.0に更新されました + - プラグインは1.0に対応したものが必要です + - Playはそのまま動作しますが、新規に作られるプリセットは1.0になります + - 以前のバージョンから無効化されていた note_view_interruptor が有効になりました +- Feat: セーフモード + - プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます + - 以下の方法でセーフモードを起動できます + - `g` キーを連打する + - URLに`?safemode=true`を付ける + - PWAのショートカットで Safemode を選択して起動する +- Feat: ページのタブバーを下部に表示できるように +- Enhance: コントロールパネルを検索できるように +- Enhance: トルコ語 (tr-TR) に対応 +- Enhance: 不必要な翻訳データを読み込まなくなり、パフォーマンスが向上しました +- Enhance: 画像エフェクトのパラメータ名の多言語対応 +- Enhance: 依存ソフトウェアの更新 +- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正 +- Fix: 一部の設定検索結果が存在しないパスになる問題を修正 + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) +- Fix: テーマエディタが動作しない問題を修正 +- Fix: チャンネルのハイライトページにノートが表示されない問題を修正 +- Fix: カラムの名前が正しくリスト/チャンネルの名前にならない問題を修正 + +### Server +- Enhance: ノートの削除処理の効率化 +- Enhance: 全体的なパフォーマンスの向上 +- Enhance: 依存ソフトウェアの更新 +- Fix: SystemWebhook設定でsecretを空に出来ない問題を修正 + + +## 2025.7.0 + +### Note +- Node.jsの最小バージョンを20.10.0から20.18.1に引き上げました + - なお、特に必要がない限りNode.jsは推奨バージョンであるv22を使用するようにしてください + +### General +- Feat: ノートの下書き機能 +- Feat: クリップ内でノートを検索できるように +- Feat: Playを検索できるように +- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように +- Enhance: ウォーターマーク機能をロールで制御可能に + +### Client +- Note: 「自動でもっと見る」オプションは無効になっています +- Feat: モデログを検索できるように +- Enhance: 設定の自動バックアップをオンにした直後に自動バックアップするように +- Enhance: ファイルアップロード前にキャプション設定を行えるように +- Enhance: ファイルアップロード時にセンシティブ設定されているか表示するように +- Enhance: 投稿フォームにファイルをペースト/ドロップした際のUXを改善 +- Enhance: ページネーション(一覧表示)の並び順を逆にできるように +- Enhance: ページネーション(一覧表示)の基準日時を指定できるように +- Enhance: レンダリングパフォーマンスの向上 +- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正 +- Fix: プラグインをアンインストールしてもセーブデータが残る問題を修正 +- Fix: 数時間後Misskeyのタブに戻った際に、タブがスロットリングされている間の更新アニメーションを延々見せ続けられる問題を修正 +- Fix: 非ログイン時のハイライトノートの画像がCWの有無を考慮せず表示される問題を修正 +- Fix: レンジ選択・ドロップダウンにて、操作を無効にすべきところで無効にならない問題を修正 +- Fix: Pull to refreshが有効なときに横スクロールができない問題を修正 + +### Server +- Enhance: sinceId/untilIdが指定可能なエンドポイントにおいて、sinceDate/untilDateも指定可能に +- Enhance: メールの送信者としてサーバー名を表示するように (サーバー名が設定されている場合) +- Fix: ジョブキューのProgressの値を正しく計算する + + +## 2025.6.3 + +### Client +- Fix: キャッシュを削除しないとクライアントが使用できないことがある問題を修正 + +## 2025.6.2 + +### Client +- Fix: キャッシュを削除しないとクライアントが使用できないことがある問題を修正 +- 翻訳の更新 + +## 2025.6.1 + +### Note +- AiScript Misskey拡張API(Misskey Webプラグイン)の[note_view_interruptor](https://misskey-hub.net/ja/docs/for-developers/plugin/plugin-api-reference/#pluginregister_note_view_interruptorfn)は不具合の影響により現在一時的に無効化されています。 +- Misskey Web投稿フォームのプレビュー切り替えは「...」メニュー内に配置されました + +### Client +- Feat: 画像にウォーターマークを付与できるようになりました +- Feat: 画像の加工ができるようになりました(実験的) +- Enhance: ノートのリアクション一覧で、押せるリアクションを優先して表示できるようにするオプションを追加 +- Enhance: 全てのチャットメッセージを既読にできるように(設定→その他) +- Enhance: ミュートした絵文字をデバイス間で同期できるように +- Fix: ドライブファイルの選択が不安定な問題を修正 +- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正 +- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正 +- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正 +- Fix: ウェルカムタイムラインでリアクションが表示されない問題を修正 +- Fix: デッキのタイムラインカラムで新着ノート時のサウンドが再生されない問題を修正 + +### Server +- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all) +- Fix: アカウント削除が正常に行われないことがあった問題を修正 +- Fix: outboxのページネーションが正しく行われない問題を修正 + +### Misskey.js +- Fix: misskey-jsの drive/file/create でファイルアップロードができない問題を修正 + +## 2025.6.0 + +### Client +- Enhance: 非同期的なコンポーネントの読み込み時のハンドリングを強化 +- Fix: リアクションの一部の絵文字が重複して表示されることがある問題を修正 +- Fix: 非利用者に対するユーザー作成コンテンツの公開範囲が全て非公開になっている場合にログインできない問題を修正 + +### Server +- Fix: 非利用者に対するユーザー作成コンテンツの公開範囲が全て非公開になっている場合でもusers/showを許可するように + ## 2025.5.1 +### Note +- 設定ファイルの以下の項目がコントロールパネルから設定するようになりました + - signToActivityPubGet + - proxyRemoteFiles + - disallowExternalApRedirect + - 許可しないかどうかではなく、許可するかどうかの設定(allowExternalApRedirect)になりました + ### General - Feat: 非ログインでサーバーを閲覧された際に、サーバー内のコンテンツを非公開にすることができるようになりました - モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます - 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます - デフォルト値は「ローカルのコンテンツだけ公開」になっています +- Feat: ロールでアップロード可能なファイル種別を設定可能になりました + - デフォルトは**テキスト、JSON、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。 + - 場合によってはファイル種別を正しく検出できないことがあります(特にテキストフォーマット)。その場合、ファイル種別は application/octet-stream と見做されます。 + - したがって、それらの種別不明ファイルを許可したい場合は application/octet-stream を指定に追加してください。 +- Feat: プレビュー先がリダイレクトを伴う場合、リダイレクト先のコンテンツを取得しに行くか否かを設定できるように(#16043) +- Enhance: UIのアイコンデータの読み込みを軽量化 ### Client +- Feat: ドライブのUIが強化されました + - 複数のファイルをまとめて移動できるようになりました +- Feat: ファイルのアップロードUIが一新されました + - アップロード前にファイル情報を確認できるようになりました + - 圧縮の品質を選択できるようになりました + - アップロードに失敗したときに再試行できるようになりました + - アップロード前に画像のクロッピングを行えるようになりました + - ファイルサイズのチェックは圧縮後の実際にアップロードされるサイズで行われるようになりました + - ファイルのアップロードを中断できるようになりました - Feat: サーバー初期設定ウィザードが実装されました - 簡単なウィザードに従うだけで、サーバーに最適な設定が適用されます - Feat: Websocket接続を行わずにMisskeyを利用するNo Websocketモードが実装されました(beta) @@ -14,18 +172,43 @@ - 何らの理由によりWebsocket接続が行えない環境でも快適に利用可能です - 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました - チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます +- Feat: 絵文字をミュート可能にする機能 + - 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました +- Feat: モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように(実験的) +- Enhance: 設定の同期をオンにするときに競合したときに値をマージできるように - Enhance: メモリ使用量を軽減しました +- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加 +- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように - Enhance: リプライ元にアンケートがあることが表示されるように - Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上 (Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283) +- Enhance: ユーザー設定でURLプレビューを無効化できるように +- Enhance: ヒントとコツを追加 +- Enhance: ヒントとコツを再表示できるように +- Enhance: AiScriptからtoastを表示する関数 `Mk:toast` を追加 +- Enhance: シンタックスハイライトのエンジンをJavaScriptベースのものに変更 + - フロントエンドの読み込みサイズを軽量化しました + - ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。 +- Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正 +- Fix: アカウント依存かつ初期状態である設定値をサーバー同期しようとした際に正しくコンフリクト検出されない問題を修正 - Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 +- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように +- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正 ### Server +- Enhance: リストやフォローをエクスポートする際にリプライを含むかどうかの情報を含むように +- Enhance: チャットルームの最大メンバー数を30人から50人に調整 - Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加 +- Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加 +- Enhance: レートリミットの計算方法を調整 (#13997) +- Enhance: 外部サイトのOGPのキャッシュ期間を調整 - Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正 - Fix: ユーザ除外アンテナをインポートできない問題を修正 - Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正 - +- Fix: ミュート対象ユーザーが引用されているノートがRNされたときにミュートを貫通してしまう問題を修正 #16009 +- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように +- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正 +- Fix: コントロールパネルのジョブキューページからPausedなジョブ一覧を閲覧できない問題を修正 ## 2025.5.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3aedfa9eb..95e6077d93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -258,6 +258,12 @@ Misskey uses Vue(v3) as its front-end framework. - **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.** - Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome. +## Tabler Icons +アイコンは、Production Build時に使用されていないものが削除されるようになっています。 + +**アイコンを動的に設定する際には、 `ti-${someVal}` のような、アイコン名のみを動的に変化させる実装を行わないでください。** +必ず `ti-xxx` のような完全なクラス名を含めるようにしてください。 + ## nirax niraxは、Misskeyで使用しているオリジナルのフロントエンドルーティングシステムです。 **vue-routerから影響を多大に受けているので、まずはvue-routerについて学ぶことをお勧めします。** @@ -575,27 +581,6 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o - 生成後、ファイルをmigration下に移してください - 作成されたスクリプトは不必要な変更を含むため除去してください -### JSON SchemaのobjectでanyOfを使うとき -JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 -バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) -https://github.com/misskey-dev/misskey/pull/10082 - -テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合: - -```ts -export const paramDef = { - type: 'object', - properties: { - hoge: { type: 'string', minLength: 1 }, - fuga: { type: 'string', minLength: 1 }, - }, - anyOf: [ - { required: ['hoge'] }, - { required: ['fuga'] }, - ], -} as const; -``` - ### コネクションには`markRaw`せよ **Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。 @@ -633,3 +618,23 @@ color: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); color: color(from var(--MI_THEME-accent) srgb r g b / 0.5); ``` +## 考え方 +### DRYに囚われるな +必要なのは一般化ではなく抽象化と考えます。 +盲信せず、誤った・不必要な共通化は避け、それが自然だと感じる場合は重複させる勇気を持ちましょう。 + +### Misskeyを複雑にしない実装 +それがいくら複雑であっても、Misskey固有のコンテキストと関心が分離されている(もしくは事実上分離されていると見做すことができる)実装であれば、それはMisskeyのコードベースに対する複雑性に影響を与えないと考えます。 + +例えるなら、VueやAiScriptといったMisskeyが使用しているライブラリの内部実装がいくら複雑だったとしても、「それを使用しているからMisskeyの実装は複雑である」ということにはならないのと同じです。 + +Misskeyのドメイン知識から関心が分離されているということは、Misskeyの実装について考える時にそれらの内部実装を考慮する必要が無く、認知負荷を増やさないからです。 + +また重要な点は、その実装が、Misskeyリポジトリの外部にあるか・内部にあるかということや、Misskeyがメンテナンスするものか・第三者がメンテナンスするものかといったことは複雑性を考える上ではほとんど無視できるという点です。 + +もちろんその実装がMisskeyリポジトリにあり、Misskeyがメンテナンスしなければならないものは、保守のコストはかかります。 +しかし、Misskeyの本質的な設計・実装という観点で見たときは、その実装は実質的に外部ライブラリのように振る舞います。 +換言すれば「たまたまMisskeyの開発者と同じ人たちがメンテナンスしているし、たまたまMisskeyのリポジトリ内に置いてあるだけの外部ライブラリ」です。 + +そのため、実装をなるべくMisskeyのドメイン知識から独立したものにすれば、Misskeyのコードベースの複雑性を上げることなく機能実装を行うことができ、お得であると言えます。 +もちろんそれにこだわって、些細な実装でもそのように分離してしまうとかえって認知負荷が増えたり、実装量が増えてメリットをデメリットが上回る場合もあるので、ケースバイケースではあります。 diff --git a/Dockerfile b/Dockerfile index aafaa9dc6e..370bed5751 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,10 +18,13 @@ WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] COPY --link ["scripts", "./scripts"] +COPY --link ["patches", "./patches"] COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"] +COPY --link ["packages/frontend-builder/package.json", "./packages/frontend-builder/"] +COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"] COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] @@ -52,6 +55,7 @@ WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] COPY --link ["scripts", "./scripts"] +COPY --link ["patches", "./patches"] COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] diff --git a/chart/files/default.yml b/chart/files/default.yml index 06f762aafa..8fa0b39eff 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -221,9 +221,6 @@ id: "aidx" # Media Proxy #mediaProxy: https://example.com/proxy -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - #allowedPrivateNetworks: [ # '127.0.0.1/32' #] diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index bd4021d2e3..4f2f700146 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -34,7 +34,6 @@ describe('Before setup instance', () => { cy.intercept('POST', '/api/admin/update-meta').as('update-meta'); - cy.get('[data-cy-next]').click(); cy.get('[data-cy-next]').click(); cy.get('[data-cy-server-name] input').type('Testskey'); cy.get('[data-cy-server-setup-wizard-apply]').click(); @@ -79,6 +78,8 @@ describe('After setup instance', () => { cy.get('[data-cy-signup-password] input').type('alice1234'); cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-password-retype] input').type('alice1234'); + cy.get('[data-cy-signup-submit]').should('be.disabled'); + cy.get('[data-cy-signup-invitation-code] input').type('test-invitation-code'); cy.get('[data-cy-signup-submit]').should('not.be.disabled'); cy.get('[data-cy-signup-submit]').click(); diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 9bd5e2fcc2..b5b832080f 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -68,7 +68,7 @@ receiveFollowRequest: "تلقيت طلب متابعة" followRequestAccepted: "قُبل طلب المتابعة" mention: "أشر الى" mentions: "الإشارات" -directNotes: "الملاحظات المباشرة" +directNotes: "رسالة خاصة" importAndExport: "إستورد / صدر" import: "استيراد" export: "تصدير" @@ -1008,6 +1008,8 @@ lastNDays: "آخر {n} أيام" surrender: "ألغِ" postForm: "أنشئ ملاحظة" information: "عن" +inMinutes: "د" +inDays: "ي" _chat: invitations: "دعوة" noHistory: "السجل فارغ" @@ -1252,7 +1254,6 @@ _theme: buttonBg: "خلفية الأزرار" buttonHoverBg: "خلفية الأزرار (عند التمرير فوقها)" inputBorder: "حواف حقل الإدخال" - driveFolderBg: "خلفية مجلد قرص التخزين" messageBg: "خلفية المحادثة" _sfx: note: "الملاحظات" @@ -1590,3 +1591,11 @@ _search: searchScopeAll: "الكل" searchScopeLocal: "المحلي" searchScopeUser: "مستخدم محدد" +_watermarkEditor: + opacity: "الشفافية" + scale: "الحجم" + text: "نص" + position: "الموضع" + type: "نوع" + image: "صور" + advanced: "متقدم" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 556f780298..93ee2b1cb5 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -848,6 +848,8 @@ sourceCode: "সোর্স কোড" flip: "উল্টান" postForm: "নোট লিখুন" information: "আপনার সম্পর্কে" +inMinutes: "মিনিট" +inDays: "দিন" _chat: invitations: "আমন্ত্রণ" noHistory: "কোনো ইতিহাস নেই" @@ -1017,7 +1019,6 @@ _theme: buttonBg: "বাটনের পটভূমি" buttonHoverBg: "বাটনের পটভূমি (হভার)" inputBorder: "ইনপুট ফিল্ডের বর্ডার" - driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি" badge: "ব্যাজ" messageBg: "চ্যাটের পটভূমি" fgHighlighted: "হাইলাইট করা পাঠ্য" @@ -1350,3 +1351,9 @@ _remoteLookupErrors: _search: searchScopeAll: "সবগুলো" searchScopeLocal: "স্থানীয়" +_watermarkEditor: + opacity: "অস্বচ্ছতা" + scale: "আকার" + text: "লেখা" + image: "ছবি" + advanced: "উন্নত" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 02b50fdb97..7f26a96d37 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -298,6 +298,7 @@ uploadFromUrl: "Carrega des d'un enllaç" uploadFromUrlDescription: "Enllaç del fitxer que vols carregar" uploadFromUrlRequested: "Càrrega sol·licitada" uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot trigar un temps" +uploadNFiles: "Pujar {n} arxius" explore: "Explora" messageRead: "Vist" noMoreHistory: "No hi ha res més per veure" @@ -326,6 +327,7 @@ dark: "Fosc" lightThemes: "Temes clars" darkThemes: "Temes foscos" syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu" +switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" es troba activat. Vols desactivar la sincronització i canviar de mode manualment?" drive: "Disc" fileName: "Nom del Fitxer" selectFile: "Selecciona un fitxer" @@ -575,8 +577,10 @@ showFixedPostForm: "Mostrar el formulari per escriure a l'inici de la línia de showFixedPostFormInChannel: "Mostrar el formulari d'escriptura al principi de la línia de temps (Canals)" withRepliesByDefaultForNewlyFollowed: "Inclou les respostes d'usuaris nous que segueixes a la línia de temps per defecte." newNoteRecived: "Hi ha publicacions noves" +newNote: "Notes noves" sounds: "Sons" sound: "So" +notificationSoundSettings: "Configuració del so de notificació" listen: "Escoltar" none: "Res" showInPage: "Mostrar a la pàgina " @@ -791,6 +795,7 @@ wide: "Gran" narrow: "Estret" reloadToApplySetting: "Aquest ajust només s'aplicarà després de recarregar la pàgina. Vols fer-ho ara?" needReloadToApply: "Es requereix recarregar per reflectir aquesta opció " +needToRestartServerToApply: "És necessari reiniciar el servidor perquè tinguin efecte els canvis." showTitlebar: "Mostra la barra del títol " clearCache: "Esborra la memòria cau" onlineUsersCount: "{n} Usuaris es troben en línia " @@ -891,7 +896,7 @@ searchResult: "Resultats de la cerca" hashtags: "Etiquetes" troubleshooting: "Solucionar problemes" useBlurEffect: "Fes servir efectes de desenfocament a la interfície" -learnMore: "Saber més " +learnMore: "Saber-ne més " misskeyUpdated: "Misskey s'ha actualitzat " whatIsNew: "Mostra canvis" translate: "Traduir " @@ -997,6 +1002,7 @@ failedToUpload: "Ha fallat la pujada" cannotUploadBecauseInappropriate: "Aquest fitxer no es pot pujar perquè s'ha trobat que algunes parts són inapropiades." cannotUploadBecauseNoFreeSpace: "Ha fallat la pujada del fitxer perquè no hi ha capacitat al Disc." cannotUploadBecauseExceedsFileSizeLimit: "Aquest fitxer no es pot pujar perquè supera la mida permesa." +cannotUploadBecauseUnallowedFileType: "Impossible pujar l'arxiu no és un tipus de fitxer autoritzat." beta: "Proves" enableAutoSensitive: "Marcar com a sensible automàticament " enableAutoSensitiveDescription: "Permet la detecció i el marcat automàtic dels mitjans sensibles fent servir aprenentatge automàtic quan sigui possible. Si aquesta opció es troba desactivada potser que estigui activada per a tota la instància. " @@ -1307,6 +1313,7 @@ availableRoles: "Roles disponibles " acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perills." federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador." federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors." +draft: "Esborrany " confirmOnReact: "Confirmar en reaccionar" reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?" markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?" @@ -1324,6 +1331,7 @@ restore: "Restaurar " syncBetweenDevices: "Sincronització entre dispositius" preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu" preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?" +preferenceSyncConflictChoiceMerge: "Integració " preferenceSyncConflictChoiceServer: "Valors de configuració del servidor" preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu " preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització " @@ -1343,9 +1351,32 @@ embed: "Incrustar" settingsMigrating: "Estem migrant la teva configuració. Si us plau espera un moment... (També pots fer la migració més tard, manualment, anant a Preferències → Altres → Migrar configuració antiga)" readonly: "Només lectura" goToDeck: "Tornar al tauler" -federationJobs: "Treballs sindicats " +federationJobs: "Treballs de federació" driveAboutTip: "Al Disc veure's una llista de tots els arxius que has anat pujant.
\nPots tornar-los a fer servir adjuntant-los a notes noves o pots adelantar-te i pujar arxius per publicar-los més tard!
\nTingués en compte que si esborres un arxiu també desapareixerà de tots els llocs on l'has fet servir (notes, pàgines, avatars, imatges de capçalera, etc.)
\nTambé pots crear carpetes per organitzar les." scrollToClose: "Desplaçar per tancar" +advice: "Consell" +realtimeMode: "Mode en temps real" +turnItOn: "Activar" +turnItOff: "Desactivar" +emojiMute: "Silenciar emojis" +emojiUnmute: "Deixar de silenciar emojis" +muteX: "Silenciar {x}" +unmuteX: "Deixar de silenciar {x}" +abort: "Cancel·lar" +tip: "Trucs i consells" +redisplayAllTips: "Torna ha mostrat tots els trucs i consells" +hideAllTips: "Amagar tots els trucs i consells" +defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte" +defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran.
Alta, redueix la mida de l'arxiu però també la qualitat de la imatge." +inMinutes: "Minut(s)" +inDays: "Di(a)(es)" +safeModeEnabled: "Mode segur activat" +pluginsAreDisabledBecauseSafeMode: "Els afegits no estan activats perquè el mode segur està activat." +customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el mode segur es troba activat." +themeIsDefaultBecauseSafeMode: "El tema predeterminat es farà servir mentre el mode segur estigui activat. Una vegada es desactivi el mode segur es restablirà el tema escollit." +_order: + newest: "Més recent" + oldest: "Antigues primer" _chat: noMessagesYet: "Encara no tens missatges " newMessage: "Missatge nou" @@ -1379,6 +1410,8 @@ _chat: chatNotAvailableInOtherAccount: "La funció de xat es troba desactivada al compte de l'altre usuari." cannotChatWithTheUser: "No pots xatejar amb aquest usuari" cannotChatWithTheUser_description: "El xat està desactivat o l'altra part encara no l'ha obert." + youAreNotAMemberOfThisRoomButInvited: "No participes en aquesta sala, però has rebut una invitació. Per participar accepta la invitació." + doYouAcceptInvitation: "Acceptes la invitació?" chatWithThisUser: "Xateja amb aquest usuari" thisUserAllowsChatOnlyFromFollowers: "Aquest usuari només accepta xats d'usuaris que el segueixen." thisUserAllowsChatOnlyFromFollowing: "Aquest usuari només accepta xats d'usuaris que segueix." @@ -1418,12 +1451,21 @@ _settings: makeEveryTextElementsSelectable: "Fes que tots els elements del text siguin seleccionables" makeEveryTextElementsSelectable_description: "L'activació pot reduir la usabilitat en determinades ocasions." useStickyIcons: "Utilitza icones fixes" + enableHighQualityImagePlaceholders: "Mostrar marcadors de posició per imatges d'alta qualitat" + uiAnimations: "Animacions de la interfície" showNavbarSubButtons: "Mostrar sub botons a la barra de navegació " ifOn: "Quan s'activa" ifOff: "Quan es desactiva" enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius" enablePullToRefresh: "Lliscar i actualitzar " enablePullToRefresh_description: "Amb el ratolí, llisca mentre prems la roda." + realtimeMode_description: "Estableix una connexió amb el servidor i actualitza el contingut en temps real. Pot consumir més dades i bateria." + contentsUpdateFrequency: "Freqüència d'adquisició del contingut" + contentsUpdateFrequency_description: "Com més alt sigui l'adquisició de contingut en temps real, més baixa el rendiment i més consum de dades i bateria." + contentsUpdateFrequency_description2: "Quan s'activa el mode en temps real, el contingut s'actualitza en temps real, independentment d'aquesta configuració." + showUrlPreview: "Mostrar vista prèvia d'URL" + showAvailableReactionsFirstInNote: "Mostra les reacciones que pots fer servir al damunt" + showPageTabBarBottom: "Mostrar les pestanyes de les línies de temps a la part inferior" _chat: showSenderName: "Mostrar el nom del remitent" sendOnEnter: "Introdueix per enviar" @@ -1597,6 +1639,10 @@ _serverSettings: fanoutTimelineDbFallback: "Carregar de la base de dades" fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir." reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat." + remoteNotesCleaning: "Neteja automàtica de notes remotes" + remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se" + remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja" + remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes" inquiryUrl: "URL de consulta " inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació." openRegistration: "Registres oberts" @@ -1604,6 +1650,23 @@ _serverSettings: thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un període de temps, aquesta opció es desactiva automàticament per evitar el correu brossa." deliverSuspendedSoftware: "Programari que ja no es distribueix" deliverSuspendedSoftwareDescription: "Pots especificar un rang de noms i versions del programari del servidor per detenir l'entrega, per exemple, degut a vulnerabilitats. Aquesta informació la proporciona el servidor i la seva fiabilitat no es garantitzada. Es pot fer servir una especificació de rang sencer per especificar una versió, però es recomana especificar una versió anterior, com >= 2024.3.1-0, perquè especificar >= 2024.3.1 no incloure versions personalitzades com 2024.3.1-custom.0." + singleUserMode: "Mode un usuari" + singleUserMode_description: "Si ets l'únic usuari d'aquesta instància, activant aquest mode optimitzaràs el funcionament." + signToActivityPubGet: "Formar sol·licituds GET" + signToActivityPubGet_description: " Això normalment hauria d'estar activat. Desactivar aquesta opció pot millorar els problemes de comunicació amb algunes de les instàncies federades, però també pot fer impossibles les comunicacions amb altres servidors." + proxyRemoteFiles: "Proxy d'arxius remots" + proxyRemoteFiles_description: "Quan està habilitat, fa de proxy i serveix arxius remots. Això ajuda a generar les miniatures de les imatges i a protegir la privacitat dels usuaris." + allowExternalApRedirect: "Permetre el reencaminament per consultes fent servir ActivityPub." + allowExternalApRedirect_description: "Si aquesta opció s'activa, altres servidors poden consultar continguts de tercers mitjançant aquest servidor, però això pot donar peu a la suplantació de continguts." + userGeneratedContentsVisibilityForVisitor: "L'abast de la publicació del contingut generat per l'usuari" + userGeneratedContentsVisibilityForVisitor_description: "Això ajuda a evitar problemes com que continguts remots inadequats que no hagin estat moderats correctament es publiquin a internet mitjançant el teu servidor." + userGeneratedContentsVisibilityForVisitor_description2: "La publicació incondicional de tots els continguts del servidor a internet, incloent-hi els continguts remots rebuts pel servidor, comporta riscos. Això és extremadament important per els espectadors que desconeixen el caràcter descentralitzat dels continguts, ja que poden percebre erroneament els continguts remots com contingut generat per el propi servidor." + restartServerSetupWizardConfirm_title: "Vols tornar a executar l'assistent de configuració inicial del servidor?" + restartServerSetupWizardConfirm_text: "Algunes configuracions actuals seran restablertes." + _userGeneratedContentsVisibilityForVisitor: + all: "Tot obert al públic " + localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat" + none: "Tot privat" _accountMigration: moveFrom: "Migrar un altre compte a aquest" moveFromSub: "Crear un àlies per un altre compte" @@ -1944,6 +2007,11 @@ _role: canImportMuting: "Autoritza la importació de silenciats" canImportUserLists: "Autoritza la importació de llistes d'usuaris " chatAvailability: "Es permet xatejar" + uploadableFileTypes: "Tipus de fitxers que en podeu pujar" + uploadableFileTypes_caption: "Especifica el tipus MIME. Es poden especificar diferents tipus MIME separats amb una nova línia, i es poden especificar comodins amb asteriscs (*). (Per exemple: image/*)" + uploadableFileTypes_caption2: "Pot que no sigui possible determinar el tipus MIME d'alguns arxius. Per permetre aquests tipus d'arxius afegeix {x} a les especificacions." + noteDraftLimit: "Nombre possible d'esborranys de notes al servidor" + watermarkAvailable: "Pots fer servir la marca d'aigua" _condition: roleAssignedTo: "Assignat a rols manuals" isLocal: "Usuari local" @@ -2103,6 +2171,7 @@ _theme: install: "Instal·lar un tema" manage: "Gestionar els temes " code: "Codi del tema" + copyThemeCode: "Copiar el codi del tema" description: "Descripció" installed: "{name} Instal·lat " installedThemes: "Temes instal·lats " @@ -2161,7 +2230,6 @@ _theme: buttonBg: "Fons botó " buttonHoverBg: "Fons botó (en passar-hi per sobre)" inputBorder: "Contorn del cap d'introducció " - driveFolderBg: "Fons de la carpeta Disc" badge: "Insígnia " messageBg: "Fons del xat" fgHighlighted: "Text ressaltat" @@ -2417,6 +2485,8 @@ _visibility: disableFederation: "Sense federar" disableFederationDescription: "No enviar a altres servidors" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "Hi ha arxius que no s'han carregat, vols descartar-los i tancar el formulari?" + uploaderTip: "L'arxiu encara no s'ha carregat. Des del menú arxiu pots canviar el nom, retallar imatges, posar marques d'aigua i comprimir o no l'arxiu. Els arxius es carreguen automàticament quan públiques una nota." replyPlaceholder: "Contestar..." quotePlaceholder: "Citar..." channelPlaceholder: "Publicar a un canal..." @@ -2750,6 +2820,7 @@ _fileViewer: url: "URL" uploadedAt: "Pujat el" attachedNotes: "Notes amb aquest fitxer" + usage: "Ús " thisPageCanBeSeenFromTheAuthor: "Aquesta pàgina només la pot veure l'usuari que ha pujat aquest fitxer." _externalResourceInstaller: title: "Instal·lar des d'un lloc extern" @@ -2797,9 +2868,12 @@ _dataSaver: _avatar: title: "Avatars animats" description: "Detenir l'animació dels avatars animats. Les imatges animades solen tenir un pes més gran que les imatges normals, reduint el tràfic disponible." - _urlPreview: - title: "Miniatures vista prèvia de l'URL" - description: "Les imatges en miniatura que serveixen com a vista prèvia de les URLs no es tornaran a carregar." + _urlPreviewThumbnail: + title: "Amagar les miniatures de la vista prèvia d'URL" + description: "Les imatges en miniatura de la vista prèvia d'URL ja no es carreguen" + _disableUrlPreview: + title: "Desactivar la vista prèvia d'URL" + description: "Desactiva la funció de previsualització d'URL. A diferència de les imatges en miniatura soles, això redueix la càrrega de la mateixa informació vinculada." _code: title: "Ressaltat del codi " description: "Quan s'utilitza codi MFM, no es llegeix fins que es copiï. En els punts destacats del codi s'han de llegir els fitxers definits per a cada llengua que resulti alt, però no es poden llegir automàticament, per la qual cosa es poden reduir les quantitats de comunicació." @@ -2857,6 +2931,8 @@ _offlineScreen: _urlPreviewSetting: title: "Configuració per a la previsualització de l'URL" enable: "Activa la previsualització de l'URL" + allowRedirect: "Permet la redirecció de la visualització prèvia " + allowRedirectDescription: "Estableix si es mostra o no la redirecció a la vista prèvia quan l'adreça URL introduïda té una redirecció. Si es desactiva s'estalvien recursos del servidor, però no es mostrarà el contingut de la redirecció." timeout: "Temps màxim per carregar la previsualització de l'URL (ms)" timeoutDescription: "Si l'obtenció de la previsualització triga més que el temps establert, no es generarà la vista prèvia." maximumContentLength: "Longitud màxima del contingut (bytes)" @@ -2930,10 +3006,6 @@ _customEmojisManager: uploadSettingDescription: "En aquesta pantalla pots configurar el que s'ha de fer quan es puja un Emoji." directoryToCategoryLabel: "Escriu el nom del directori al camp de \"categoria\"" directoryToCategoryCaption: "Quan arrossegues un directori, escriu el nom del directori al camp categoria." - emojiInputAreaCaption: "Selecciona els Emojis que vols registrar gent servir un dels mètodes." - emojiInputAreaList1: "Arrossega i deixar anar fitxers o directoris dintre del quadrat." - emojiInputAreaList2: "Clica l'enllaç per seleccionar un fitxer des del teu ordinador." - emojiInputAreaList3: "Clica aquest enllaç per seleccionar del Disc" confirmRegisterEmojisDescription: "Registrar els Emojis de la llista com a nous Emojis personalitzats. Vols continuar? (Per evitar una sobrecàrrega només {count} Emojis es poden registrar d'una sola vegada)" confirmClearEmojisDescription: "Descartar els canvis i esborrar els Emojis de la llista. Vols continuar?" confirmUploadEmojisDescription: "Pujar els {count} fitxers que has arrossegat al disc. Vols continuar?" @@ -3001,6 +3073,7 @@ _bootErrors: otherOption1: "Esborrar la configuració i la memòria cau del client" otherOption2: "Iniciar client senzill" otherOption3: "Iniciar l'eina de reparació " + otherOption4: "Iniciar Misskey en mode segur" _search: searchScopeAll: "Tot" searchScopeLocal: "Local" @@ -3009,3 +3082,132 @@ _search: pleaseEnterServerHost: "Introdueix l'adreça de la instància " pleaseSelectUser: "Selecciona un usuari" serverHostPlaceholder: "Ex: misskey.example.com" +_serverSetupWizard: + installCompleted: "La instal·lació de Misskey ha finalitzat!" + firstCreateAccount: "Primer crea un compte d'administrador." + accountCreated: "Compte d'administrador creat." + serverSetting: "Configuració del servidor" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Aquest assistent t'ajuda a fer una configuració òptima del servidor." + settingsYouMakeHereCanBeChangedLater: "Els canvis que facis ara poden modificar-se més tard." + howWillYouUseMisskey: "Com es fa servir Misskey?" + _use: + single: "Servidor per una sola persona" + single_description: "Fes-ho servir com el teu propi servidor dedicat" + single_youCanCreateMultipleAccounts: "Es poden crear diferents comptes segons siguin les teves necessitats, inclús quan es fa servir com a servidor unipersonal." + group: "Servidor per a grups" + group_description: "Invita altres usuaris de la teva confiança i fes-ho servir amb més d'una persona." + open: "Servidor obert" + open_description: "Operar per donar cabuda a un nombre no determinat d'usuaris." + openServerAdvice: "Acceptar un nombre no determinat d'usuaris comporta alguns riscos. Es recomana operar amb un sistema de moderació fiable per fer front als problemes." + openServerAntiSpamAdvice: "També s'ha de tenir molta cura amb la seguretat, per exemple habilitant funcions anti-bot com reCAPTCHA, per assegurar-te que el teu servidor no es converteix en un trampolí per contingut brossa." + howManyUsersDoYouExpect: "Quantes persones preveus?" + _scale: + small: "Menys de 100 (petita escala)" + medium: "Més de 100 i menys de 1000 (mida mitjana)" + large: "Més de 1000 persones (gran escala)" + largeScaleServerAdvice: "Els grans servidors poden requerir coneixements avançats d'infraestructures, com balanceig de càrregues i replicació de base de dades." + doYouConnectToFediverse: "Desitges connectar-te amb el Fedivers?" + doYouConnectToFediverse_description1: "Quan es connecta amb una xarxa de servidors distribuïts (Fedivers), els continguts poden intercanviar-se amb altres servidors i entre ells." + doYouConnectToFediverse_description2: "La connexió amb el Fedivers també es coneix com a \"federació\"." + youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard." + remoteContentsCleaning: "Neteja automàtica del contingut rebut" + remoteContentsCleaning_description: "Quan es comença a federar es rep un munt de contingut, quan s'activa la neteja automàtica el contingut antic que no es consulta serà eliminat del servidor, el que permet estalviar espai d'emmagatzematge." + adminInfo: "Informació de l'administrador " + adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes." + adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada." + followingSettingsAreRecommended: "Es recomana la següent configuració " + applyTheseSettings: "Aplicar aquesta configuració " + skipSettings: "Saltar la configuració " + settingsCompleted: "Configuració finalitzada " + settingsCompleted_description: "Gràcies per la teva ajuda. Ara que ja està tot llest, pots començar a fer servir el servidor immediatament." + settingsCompleted_description2: "La configuració avançada del servidor també poden fer-se des del \"Tauler de control\"." + donationRequest: "Una donació, si us plau" + _donationRequest: + text1: "Misskey és un programari gratuït fet per voluntaris." + text2: "Si ho desitges, agrairíem molt la teva donació per poder seguir desenvolupant el projecte." + text3: "També hi ha privilegis especials per als donants!" +_uploader: + editImage: "Edició d'imatges" + compressedToX: "Comprimit a {x}" + savedXPercent: "{x}% d'estalvi " + abortConfirm: "Hi ha un arxiu que no s'ha pujat, vols cancel·lar?" + doneConfirm: "Hi han fitxers no pujats, vols completar-los?" + maxFileSizeIsX: "La mida màxima d'arxiu que es pot pujar és {x}." + allowedTypes: "Tipus de fitxers que en podeu pujar" + tip: "L'arxiu encara no s'ha carregat. En aquest quadre de diàleg, pots comprovar, canviar el nom, comprimir i retallar l'arxiu abans de pujar-lo. Quan estigui llest pots iniciar la càrrega polsant el boto \"Pujar\"" +_clientPerformanceIssueTip: + title: "Si creus que el consum de bateria és molt alt" + makeSureDisabledAdBlocker: "Desactiva els bloquejadors de publicitat" + makeSureDisabledAdBlocker_description: "Els bloquejadors d'anuncis pot afectar el rendiment, comprova que no estiguin activats per característiques del sistema operatiu o del navegador." + makeSureDisabledCustomCss: "Desactiva CSS personalitzat" + makeSureDisabledCustomCss_description: "L'anul·lació dels estils pot afectar el rendiment. Comprova que el CSS personalitzat o les extensions que reescriuen estils no estiguin activats." + makeSureDisabledAddons: "Desactiva extensions" + makeSureDisabledAddons_description: "Algunes extensions poden interferir en el comportament del client i afectar el rendiment. Desactiva les extensions del navegador i comprovar-ho." +_clip: + tip: "Clip és una funció que permet organitzar les teves notes." +_userLists: + tip: "Es poden crear llistes amb qualsevol usuari. La llista creada es pot mostrar com una línia de temps." +watermark: "Marca d'aigua " +defaultPreset: "Per defecte" +_watermarkEditor: + tip: "A la imatge es pot afegir una marca d'aigua com informació sobre drets." + quitWithoutSaveConfirm: "Sortir sense desar?" + driveFileTypeWarn: "Aquest arxiu no és compatible" + driveFileTypeWarnDescription: "Selecciona un arxiu d'imatge " + title: "Editar la marca d'aigua " + cover: "Cobrir-ho tot" + repeat: "Repetir" + opacity: "Opacitat" + scale: "Mida" + text: "Text" + position: "Posició " + type: "Tipus" + image: "Imatges" + advanced: "Avançat" + stripe: "Bandes" + stripeWidth: "Amplada de la banda" + stripeFrequency: "Freqüència de la banda" + angle: "Angle" + polkadot: "Lunars" + checker: "Escacs" + polkadotMainDotOpacity: "Opacitat del lunar principal" + polkadotMainDotRadius: "Mida del lunar principal" + polkadotSubDotOpacity: "Opacitat del lunar secundari" + polkadotSubDotRadius: "Mida del lunar secundari" + polkadotSubDotDivisions: "Nombre de punts secundaris" +_imageEffector: + title: "Efecte" + addEffect: "Afegeix un efecte" + discardChangesConfirm: "Vols descartar els canvis i sortir?" + _fxs: + chromaticAberration: "Aberració cromàtica" + glitch: "Glitch" + mirror: "Mirall" + invert: "Inversió cromàtica " + grayscale: "Monocrom " + colorAdjust: "Correcció de color" + colorClamp: "Compressió cromàtica " + colorClampAdvanced: "Compressió de cromàtica avançada " + distort: "Distorsió " + threshold: "Binarització" + zoomLines: "Saturació de línies " + stripe: "Bandes" + polkadot: "Lunars" + checker: "Escacs" + blockNoise: "Bloqueig de soroll" + tearing: "Trencament d'imatge " +drafts: "Esborrany " +_drafts: + select: "Seleccionar esborrany" + cannotCreateDraftAnymore: "S'ha sobrepassat el nombre màxim d'esborranys que es poden crear." + cannotCreateDraft: "Amb aquest contingut no es poden crear esborranys." + delete: "Esborrar esborranys" + deleteAreYouSure: "Vols esborrar els esborranys?" + noDrafts: "No hi ha esborranys" + replyTo: "Respondre a {user}" + quoteOf: "Citar les notes de {user}" + postTo: "Destinat a {channel}" + saveToDraft: "Desar com a esborrany" + restoreFromDraft: "Restaurar des dels esborranys" + restore: "Restaurar esborrany" + listDrafts: "Llistat d'esborranys" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index c23b0263b8..5945ceaf96 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1,7 +1,7 @@ --- _lang_: "Čeština" headlineMisskey: "Síť propojená poznámkami" -introMisskey: "Vítejte! Misskey je otevřený a decentralizovaný microblogový servis.\n\"Poznámkami\" můžete sdílet co se zrovna děje se všemi ve Vašem okolí. 📡\nPomocí \"reakcí\" můžete sdílet své názory a pocity na ostatní poznámky. 👍\nPojďte objevovat nový svět! 🚀" +introMisskey: "Vítejte! Misskey je otevřená a decentralizovaná microblogovací služba.\n\"Poznámkami\" můžete sdílet co se zrovna děje se všemi ve Vašem okolí. 📡\nPomocí \"reakcí\" můžete sdílet své názory a pocity na ostatní poznámky. 👍\nPojďte objevovat nový svět! 🚀" poweredByMisskeyDescription: "{name} je jeden ze serverů využívající open source platformu Misskey (nazývaná \"Misskey instance\")." monthAndDay: "{day}. {month}." search: "Vyhledávání" @@ -19,7 +19,7 @@ gotIt: "Rozumím!" cancel: "Zrušit" noThankYou: "Ne děkuji" enterUsername: "Zadej uživatelské jméno" -renotedBy: "{user} přeposla/a" +renotedBy: "{user} přeposlal*a" noNotes: "Žádné poznámky" noNotifications: "Žádná oznámení" instance: "Instance" @@ -65,6 +65,7 @@ copyFileId: "Kopírovat ID souboru" copyFolderId: "Kopírovat ID složky" copyProfileUrl: "Kopírovat URL profilu" searchUser: "Vyhledat uživatele" +searchThisUsersNotes: "Prohledat poznámky uživatele" reply: "Odpovědět" loadMore: "Zobrazit více" showMore: "Zobrazit více" @@ -1106,6 +1107,8 @@ lastNDays: "Posledních {n} dnů" surrender: "Zrušit" postForm: "Formulář pro odeslání" information: "Informace" +inMinutes: "Minut" +inDays: "Dnů" _chat: invitations: "Pozvat" noHistory: "Žádná historie" @@ -1645,7 +1648,6 @@ _theme: buttonBg: "Pozadí tlačítka" buttonHoverBg: "Pozadí tlačítka (Hover)" inputBorder: "Ohraničení vstupního pole" - driveFolderBg: "Pozadí složky disku" badge: "Odznak" messageBg: "Pozadí chatu" fgHighlighted: "Zvýrazněný text" @@ -2002,7 +2004,7 @@ _deck: list: "Seznamy" channel: "Kanály" mentions: "Zmínění" - direct: "Přímý" + direct: "Přímé poznámky" roleTimeline: "Časová osa role" _dialog: charactersExceeded: "Překročili jste maximální počet znaků! V současné době je na hodnotě {current} z {max}." @@ -2043,3 +2045,11 @@ _search: searchScopeAll: "Vše" searchScopeLocal: "Místní" searchScopeUser: "Upřesnit uživatele" +_watermarkEditor: + opacity: "Průhlednost" + scale: "Velikost" + text: "Text" + position: "Pozice" + type: "Typ" + image: "Obrázky" + advanced: "Pokročilé" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 9899276075..e3897613a0 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -298,6 +298,7 @@ uploadFromUrl: "Von einer URL hochladen" uploadFromUrlDescription: "URL der hochzuladenden Datei" uploadFromUrlRequested: "Upload angefordert" uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen ist." +uploadNFiles: "Lade {n} Dateien hoch" explore: "Erkunden" messageRead: "Gelesen" noMoreHistory: "Kein weiterer Verlauf vorhanden" @@ -326,6 +327,7 @@ dark: "Dunkel" lightThemes: "Helle Farbschemata" darkThemes: "Dunkle Farbschemata" syncDeviceDarkMode: "Einstellung deines Geräts übernehmen" +switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" ist eingeschaltet. Möchtest du die Synchronisation ausschalten und den Modus manuell wechseln?" drive: "Drive" fileName: "Dateiname" selectFile: "Datei auswählen" @@ -575,8 +577,10 @@ showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik an showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)" withRepliesByDefaultForNewlyFollowed: "Standardmäßig Antworten von neu gefolgten Benutzern in der Chronik anzeigen" newNoteRecived: "Es gibt neue Notizen" +newNote: "Neue Notiz" sounds: "Töne" sound: "Töne" +notificationSoundSettings: "Benachrichtigungston festlegen" listen: "Anhören" none: "Nichts" showInPage: "In einer Seite anzeigen" @@ -791,6 +795,7 @@ wide: "Breit" narrow: "Schmal" reloadToApplySetting: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft. Jetzt aktualisieren?" needReloadToApply: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft." +needToRestartServerToApply: "Diese Einstellung tritt nach einem Neustart des Servers in Kraft." showTitlebar: "Titelleiste anzeigen" clearCache: "Cache leeren" onlineUsersCount: "{n} Benutzer sind online" @@ -997,6 +1002,7 @@ failedToUpload: "Hochladen fehlgeschlagen" cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise unangebracht festgestellt wurden." cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist." cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden." +cannotUploadBecauseUnallowedFileType: "Hochladen nicht möglich wegen unzulässigem Dateityp." beta: "Beta" enableAutoSensitive: "Automarkierung sensibler Medien" enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch Markierungen für sensible Medien. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert." @@ -1307,6 +1313,7 @@ availableRoles: "Verfügbare Rollen" acknowledgeNotesAndEnable: "Schalten Sie dies erst ein, wenn Sie die Vorsichtsmaßnahmen verstanden haben." federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren." federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren." +draft: "Entwurf" confirmOnReact: "Reagieren bestätigen" reactAreYouSure: "Willst du eine \"{emoji}\"-Reaktion hinzufügen?" markAsSensitiveConfirm: "Möchtest du dieses Medium als sensibel kennzeichnen?" @@ -1324,6 +1331,7 @@ restore: "Wiederherstellen" syncBetweenDevices: "Zwischen Geräten synchronisieren" preferenceSyncConflictTitle: "Der konfigurierte Wert ist auf dem Server bereits vorhanden." preferenceSyncConflictText: "Die Einstellungen mit aktivierter Synchronisierung werden ihre Werte auf dem Server speichern. Es gibt jedoch bereits Werte auf dem Server. Welche Einstellungswerte sollen überschrieben werden?" +preferenceSyncConflictChoiceMerge: "Zusammenführen" preferenceSyncConflictChoiceServer: "Konfigurierte Werte auf dem Server" preferenceSyncConflictChoiceDevice: "Konfigurierte Werte auf dem Gerät" preferenceSyncConflictChoiceCancel: "Einrichten der Synchronisierung abbrechen" @@ -1340,12 +1348,31 @@ right: "Rechts" bottom: "Unten" top: "Oben" embed: "Einbetten" -settingsMigrating: "Ihre Einstellungen werden gerade migriert, Bitte warten Sie einen Moment... (Sie können die Einstellungen später auch manuell migrieren, indem Sie zu Einstellungen → Sonstiges → Alte Einstellungen migrieren gehen)" +settingsMigrating: "Deine Einstellungen werden gerade migriert. Bitte warte einen Moment... (Du kannst die Einstellungen später auch manuell migrieren, indem du zu Einstellungen → Anderes → Alte Einstellungen migrieren gehst)" readonly: "Nur Lesezugriff" goToDeck: "Zurück zum Deck" federationJobs: "Föderation Jobs" driveAboutTip: "In Drive sehen Sie eine Liste der Dateien, die Sie in der Vergangenheit hochgeladen haben.
\nSie können diese Dateien wiederverwenden um sie zu beispiel an Notizen anzuhängen, oder sie können Dateien vorab hochzuladen, um sie später zu versenden!
\nWenn Sie eine Datei löschen, verschwindet sie auch von allen Stellen, an denen Sie sie verwendet haben (Notizen, Seiten, Avatare, Banner usw.).
\nSie können auch Ordner erstellen, um sie zu organisieren." scrollToClose: "Zum Schließen scrollen" +advice: "Tipps" +realtimeMode: "Echtzeit-Modus" +turnItOn: "Einschalten" +turnItOff: "Ausschalten" +emojiMute: "Emoji stummschalten" +emojiUnmute: "Emoji-Stummschaltung aufheben" +muteX: "{x} stummschalten" +unmuteX: "Stummschaltung von {x} aufheben" +abort: "Abbrechen" +tip: "Tipps und Tricks" +redisplayAllTips: "Alle „Tipps und Tricks“ wieder anzeigen" +hideAllTips: "Alle „Tipps und Tricks“ ausblenden" +defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe" +defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße.
Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität." +inMinutes: "Minute(n)" +inDays: "Tag(en)" +_order: + newest: "Neueste zuerst" + oldest: "Älteste zuerst" _chat: noMessagesYet: "Noch keine Nachrichten" newMessage: "Neue Nachricht" @@ -1379,6 +1406,8 @@ _chat: chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert." cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich" cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert." + youAreNotAMemberOfThisRoomButInvited: "Du bist kein Teilnehmer in diesem Raum, aber du hast eine Einladung erhalten. Bitte nimm die Einladung an, um beizutreten." + doYouAcceptInvitation: "Nimmst du die Einladung an?" chatWithThisUser: "Mit dem Benutzer chatten" thisUserAllowsChatOnlyFromFollowers: "Dieser Benutzer nimmt nur Chats von Followern an." thisUserAllowsChatOnlyFromFollowing: "Dieser Benutzer nimmt nur Chats von Benutzern an, denen er folgt." @@ -1418,12 +1447,20 @@ _settings: makeEveryTextElementsSelectable: "Alle Textelemente auswählbar machen" makeEveryTextElementsSelectable_description: "Die Aktivierung kann in manchen Situationen die Benutzerfreundlichkeit beeinträchtigen." useStickyIcons: "Icons beim Scrollen folgen lassen" + enableHighQualityImagePlaceholders: "Zeige Platzhalter für Bilder in hoher Qualität an" + uiAnimations: "Animationen der Benutzeroberfläche" showNavbarSubButtons: "Unterschaltflächen in der Navigationsleiste anzeigen" ifOn: "Wenn eingeschaltet" ifOff: "Wenn ausgeschaltet" enableSyncThemesBetweenDevices: "Synchronisierung von installierten Themen auf verschiedenen Endgeräten" enablePullToRefresh: "Ziehen zum Aktualisieren" enablePullToRefresh_description: "Bei Benutzung einer Maus, mit gedrücktem Mausrad ziehen" + realtimeMode_description: "Stellt eine Verbindung mit dem Server her und aktualisiert die Inhalte in Echtzeit. Kann zu mehr Datenverkehr einem höheren Akkuverbrauch führen." + contentsUpdateFrequency: "Häufigkeit des Abrufs von Inhalten" + contentsUpdateFrequency_description: "Je höher der Wert, desto häufiger werden die Inhalte aktualisiert, aber die Leistung sinkt und der Datenverkehr und der Akkuverbrauch steigen." + contentsUpdateFrequency_description2: "Wenn der Echtzeitmodus aktiviert ist, werden die Inhalte unabhängig von dieser Einstellung in Echtzeit aktualisiert." + showUrlPreview: "URL-Vorschau anzeigen" + showAvailableReactionsFirstInNote: "Zeige die verfügbaren Reaktionen im oberen Bereich an." _chat: showSenderName: "Name des Absenders anzeigen" sendOnEnter: "Eingabetaste sendet Nachricht" @@ -1604,6 +1641,21 @@ _serverSettings: thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern." deliverSuspendedSoftware: "Software, die nicht mehr beliefert wird" deliverSuspendedSoftwareDescription: "Sie können eine Auswahl von Namen und Versionen verschiedener Serversoftware angeben, um die Zustellung zu stoppen, z. B. aufgrund von Sicherheitslücken. Diese Versionsinformationen werden vom Server bereitgestellt und ihre Zuverlässigkeit ist nicht garantiert. Es wird jedoch empfohlen, eine Vorabversion anzugeben, wie z. B. >= 2024.3.1-0, da die Angabe >= 2024.3.1 keine benutzerdefinierten Versionen wie 2024.3.1-custom.0 einschließt." + singleUserMode: "Einzelbenutzermodus" + singleUserMode_description: "Wenn du der einzige Benutzer dieses Servers bist, optimiert die Aktivierung dieses Modus die Leistung des Servers." + signToActivityPubGet: "ActivityPub-GET-Anfragen signieren" + signToActivityPubGet_description: "Normalerweise sollte diese Option aktiviert sein. Die Deaktivierung kann Probleme im Zusammenhang mit der Föderation beheben, aber andererseits könnte sie die Föderation mit einigen anderen Servern deaktivieren." + proxyRemoteFiles: "Proxy für Dateien fremder Instanzen" + proxyRemoteFiles_description: "Wenn diese Einstellung aktiviert ist, werden fremde Dateien über einen Proxyserver übertragen und bereitgestellt. Dies hilft bei der Erstellung von Vorschaubildern und schützt die Privatsphäre der Benutzer." + allowExternalApRedirect: "Weiterleitungen für Anfragen über ActivityPub zulassen" + allowExternalApRedirect_description: "Wenn diese Option aktiviert ist, können andere Server Inhalte von Drittanbietern über diesen Server abfragen, was jedoch zu Content-Spoofing führen kann." + userGeneratedContentsVisibilityForVisitor: "Sichtbarkeit von nutzergenerierten Inhalten für Gäste" + userGeneratedContentsVisibilityForVisitor_description: "Dies ist nützlich, um zu verhindern, dass unangemessene Inhalte, die nicht gut moderiert sind, ungewollt über deinen eigenen Server im Internet veröffentlicht werden." + userGeneratedContentsVisibilityForVisitor_description2: "Die uneingeschränkte Veröffentlichung aller Inhalte des Servers im Internet, einschließlich der vom Server empfangenen Fremdinhalte, birgt Risiken. Dies ist besonders wichtig für Betrachter, die sich des dezentralen Charakters der Inhalte nicht bewusst sind, da sie selbst fremde Inhalte fälschlicherweise als auf dem Server erstellte Inhalte wahrnehmen könnten." + _userGeneratedContentsVisibilityForVisitor: + all: "Alles ist öffentlich" + localOnly: "Nur lokale Inhalte werden veröffentlicht, fremde Inhalte bleiben privat" + none: "Alles ist privat" _accountMigration: moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFromSub: "Alias für ein anderes Konto erstellen" @@ -1944,6 +1996,11 @@ _role: canImportMuting: "Importieren von Stummgeschalteten zulassen" canImportUserLists: "Importieren von Listen erlauben" chatAvailability: "Chatten erlauben" + uploadableFileTypes: "Hochladbare Dateitypen" + uploadableFileTypes_caption: "Gibt die zulässigen MIME-/Dateitypen an. Mehrere MIME-Typen können durch einen Zeilenumbruch getrennt angegeben werden, und Platzhalter können mit einem Sternchen (*) angegeben werden. (z. B. image/*)" + uploadableFileTypes_caption2: "Bei manchen Dateien ist es nicht möglich, den Typ zu bestimmen. Um solche Dateien zuzulassen, füge {x} der Spezifikation hinzu." + noteDraftLimit: "Anzahl der möglichen Entwürfe für serverseitige Notizen" + watermarkAvailable: "Kann die Wasserzeichenfunktion verwenden" _condition: roleAssignedTo: "Manuellen Rollen zugewiesen" isLocal: "Lokaler Benutzer" @@ -2103,6 +2160,7 @@ _theme: install: "Farbschemata installieren" manage: "Farbschemaverwaltung" code: "Farbschemencode" + copyThemeCode: "Farbschemencode kopieren" description: "Beschreibung" installed: "{name} wurde installiert" installedThemes: "Installierte Farbschemata" @@ -2161,7 +2219,6 @@ _theme: buttonBg: "Hintergrund von Schaltflächen" buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)" inputBorder: "Rahmen von Eingabefeldern" - driveFolderBg: "Hintergrund von Drive-Ordnern" badge: "Wappen" messageBg: "Hintergrund von Chats" fgHighlighted: "Hervorgehobener Text" @@ -2417,6 +2474,8 @@ _visibility: disableFederation: "Deföderieren" disableFederationDescription: "Nicht an andere Instanzen übertragen" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "Es gibt Dateien, die nicht hochgeladen wurden. Möchtest du diese verwerfen und das Formular schließen?" + uploaderTip: "Die Datei wurde noch nicht hochgeladen. Über das Dateimenü kannst du sie umbenennen, das Bild zuschneiden, ein Wasserzeichen hinzufügen, komprimieren usw. Die Datei wird automatisch hochgeladen, wenn du eine Notiz veröffentlichst." replyPlaceholder: "Dieser Notiz antworten …" quotePlaceholder: "Diese Notiz zitieren …" channelPlaceholder: "In einen Kanal senden" @@ -2797,9 +2856,12 @@ _dataSaver: _avatar: title: "Animierte Profilbilder deaktivieren" description: "Die Animation von Profilbildern wird angehalten. Da animierte Bilder eine größere Dateigröße haben können als normale Bilder, kann dies den Datenverkehr weiter reduzieren." - _urlPreview: + _urlPreviewThumbnail: title: "URL-Vorschaubilder ausblenden" description: "URL-Vorschaubilder werden nicht mehr geladen." + _disableUrlPreview: + title: "URL-Vorschau deaktivieren" + description: "Deaktiviert die URL-Vorschaufunktion. Anders als bei reinen Vorschaubildern wird dadurch das Laden der verlinkten Informationen selbst reduziert." _code: title: "Code-Hervorhebungen ausblenden" description: "Wenn Code-Hervorhebungen in MFM usw. verwendet werden, werden sie erst geladen, wenn sie angetippt werden. Die Syntaxhervorhebung erfordert das Herunterladen der Definitionsdateien für jede Programmiersprache. Es ist daher zu erwarten, dass die Deaktivierung des automatischen Ladens dieser Dateien die Menge des Datenverkehrs reduziert." @@ -2857,6 +2919,8 @@ _offlineScreen: _urlPreviewSetting: title: "Einstellungen der URL-Vorschau" enable: "URL-Vorschau aktivieren" + allowRedirect: "Umleitung von URL-Vorschauen erlauben" + allowRedirectDescription: "Wenn für eine URL eine Umleitung festgelegt ist, kann diese Funktion aktiviert werden, um der Umleitung zu folgen und eine Vorschau des umgeleiteten Inhalts anzuzeigen. Die Deaktivierung spart Serverressourcen, aber der Inhalt des Weiterleitungsziels wird nicht angezeigt." timeout: "Zeitüberschreitung beim Abrufen der Vorschau (ms)" timeoutDescription: "Übersteigt die für die Vorschau benötigte Zeit diesen Wert, wird keine Vorschau generiert." maximumContentLength: "Maximale Content-Length (Bytes)" @@ -2930,10 +2994,6 @@ _customEmojisManager: uploadSettingDescription: "Hier kannst du das Verhalten beim Hochladen von Emojis konfigurieren." directoryToCategoryLabel: "Gib den Namen des Verzeichnisses in das Feld „Kategorie“ ein" directoryToCategoryCaption: "Wenn du ein Verzeichnis ziehst und ablegst, gib den Verzeichnisnamen in das Feld „Kategorie“ ein." - emojiInputAreaCaption: "Wählen Sie die Emojis aus, die Sie mit einer der folgenden Methoden speichern möchten." - emojiInputAreaList1: "Ziehe Bilddateien oder Verzeichnisse per Drag-and-drop in diesen Rahmen" - emojiInputAreaList2: "Klicke auf diesen Link, um von deinem PC aus zu wählen" - emojiInputAreaList3: "Klicke auf diesen Link, um vom Drive aus zu wählen" confirmRegisterEmojisDescription: "Füge die in der Liste aufgeführten Emojis als neue benutzerdefinierte Emojis hinzu. Bist du sicher? (Um eine Überlastung zu vermeiden, können nur {count} Emoji(s) in einem Vorgang hinzugefügt werden)" confirmClearEmojisDescription: "Verwerfe die Bearbeitungen und lösche die Emojis aus der Liste. Bist du sicher, dass du fortfahren möchtest?" confirmUploadEmojisDescription: "Lade die {count} abgelegte(n) Datei(en) in das Drive hoch. Bist du sicher, dass du fortfahren möchtest?" @@ -3009,3 +3069,121 @@ _search: pleaseEnterServerHost: "Gib den Server-Host ein" pleaseSelectUser: "Benutzer auswählen" serverHostPlaceholder: "Beispiel: misskey.example.com" +_serverSetupWizard: + installCompleted: "Die Installation von Misskey ist abgeschlossen!" + firstCreateAccount: "Erstelle zunächst ein Administratorkonto." + accountCreated: "Ein Administratorkonto wurde angelegt!" + serverSetting: "Servereinstellungen" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Mit diesem Assistenten lässt sich die optimale Serverkonfiguration leicht einrichten." + settingsYouMakeHereCanBeChangedLater: "Die Einstellungen hier können später geändert werden." + howWillYouUseMisskey: "Wie wirst du Misskey verwenden?" + _use: + single: "Ein-Personen-Server" + single_description: "Verwende den Server alleine als deinen eigenen." + single_youCanCreateMultipleAccounts: "Bei Bedarf können mehrere Konten eingerichtet werden, auch wenn es sich um einen Ein-Personen-Server handelt." + group: "Gruppenserver" + group_description: "Lade andere vertrauenswürdige Benutzer ein und verwende es mit mehreren Personen." + open: "Offener Server" + open_description: "Registrierung für alle öffnen." + openServerAdvice: "Die Aufnahme einer unbestimmten Anzahl von Nutzern birgt Risiken. Es wird empfohlen, mit einem zuverlässigen Moderationssystem zu arbeiten, um eventuell auftretende Probleme behandeln zu können." + openServerAntiSpamAdvice: "Große Sorgfalt muss auch auf die Sicherheit gelegt werden, z. B. durch die Aktivierung von Anti-Bot-Funktionen wie reCAPTCHA, um sicherzustellen, dass der Server nicht zum Verbreiten von Spam genutzt wird." + howManyUsersDoYouExpect: "Mit wie vielen Benutzern rechnest du?" + _scale: + small: "Weniger als 100 (kleiner Maßstab)" + medium: "Mehr als 100 und weniger als 1000 Benutzer (mittelgroß)" + large: "Mehr als 1000 (großer Maßstab)" + largeScaleServerAdvice: "Für große Server sind unter Umständen fortgeschrittene Kenntnisse erforderlich, z. B. Lastverteilung und Datenbankreplikation." + doYouConnectToFediverse: "Mit dem Fediverse verbinden?" + doYouConnectToFediverse_description1: "Bei Anschluss an ein Netz von verteilten Servern (Fediverse) können Inhalte mit anderen Servern ausgetauscht werden." + doYouConnectToFediverse_description2: "Die Verbindung mit dem Fediverse wird auch als „Föderation“ bezeichnet." + youCanConfigureMoreFederationSettingsLater: "Erweiterte Einstellungen, wie z. B. die Angabe von föderierbaren Servern, können später vorgenommen werden." + adminInfo: "Administrator-Informationen" + adminInfo_description: "Legt die Administrator-Informationen fest, die für den Empfang von Anfragen verwendet werden." + adminInfo_mustBeFilled: "Dies ist auf einem offenen Server oder bei aktivierter Föderation erforderlich." + followingSettingsAreRecommended: "Die folgenden Einstellungen werden empfohlen" + applyTheseSettings: "Diese Einstellungen anwenden" + skipSettings: "Konfiguration überspringen" + settingsCompleted: "Einrichtung abgeschlossen!" + settingsCompleted_description: "Vielen Dank für deine Zeit. Jetzt, wo alles fertig ist, kannst du den Server sofort benutzen." + settingsCompleted_description2: "Detaillierte Servereinstellungen können über die „Systemsteuerung“ vorgenommen werden." + donationRequest: "Spendenaufruf" + _donationRequest: + text1: "Misskey ist eine freie Software, die von Freiwilligen entwickelt wird." + text2: "Wir würden uns über deine Unterstützung freuen, damit wir dieses Projekt auch in Zukunft weiterentwickeln können." + text3: "Für Unterstützer gibt es auch besondere Vorteile!" +_uploader: + editImage: "Bild bearbeiten" + compressedToX: "Komprimiert zu {x}" + savedXPercent: "{x}% gespart" + abortConfirm: "Einige Dateien wurden nicht hochgeladen. Möchtest du den Vorgang abbrechen?" + doneConfirm: "Einige Dateien wurden nicht hochgeladen. Möchtest du den Vorgang fortsetzen?" + maxFileSizeIsX: "Die maximale Dateigröße, die hochgeladen werden kann, beträgt {x}." + allowedTypes: "Hochladbare Dateitypen" + tip: "Die Datei ist noch nicht hochgeladen worden. In diesem Dialog kannst du die Datei vor dem Hochladen anzeigen, umbenennen, komprimieren und zuschneiden. Wenn du fertig bist, klicke auf „Hochladen“, um den Upload zu starten." +_clientPerformanceIssueTip: + makeSureDisabledAdBlocker: "Deaktiviere deinen Adblocker" + makeSureDisabledAdBlocker_description: "Adblocker können die Leistung beeinträchtigen; vergewissere dich, ob in deinem Betriebssystem, Browser oder deinen Add-ons Adblocker aktiviert sind." + makeSureDisabledCustomCss: "Benutzerdefiniertes CSS deaktivieren" + makeSureDisabledCustomCss_description: "Das Überschreiben von Stilen kann die Leistung beeinträchtigen. Stelle daher sicher, dass du kein benutzerdefiniertes CSS oder Erweiterungen aktiviert hast, die Stile überschreiben." + makeSureDisabledAddons: "Erweiterungen deaktivieren" + makeSureDisabledAddons_description: "Einige Erweiterungen können das Verhalten des Clients stören und die Leistung beeinträchtigen. Deaktiviere die Browser-Erweiterungen und prüfe, ob sich die Situation dadurch verbessert." +_clip: + tip: "Clips sind eine Funktion, mit der du Notizen gruppieren kannst." +_userLists: + tip: "Es können Listen mit beliebigen Benutzern erstellt werden. Die erstellte Liste kann als eigene Chronik angezeigt werden." +watermark: "Wasserzeichen" +defaultPreset: "Standard-Voreinstellungen" +_watermarkEditor: + tip: "Dem Bild kann ein Wasserzeichen, z. B. eine Quellenangabe, hinzugefügt werden." + quitWithoutSaveConfirm: "Nicht gespeicherte Änderungen verwerfen?" + driveFileTypeWarn: "Diese Datei wird nicht unterstützt" + driveFileTypeWarnDescription: "Bilddatei auswählen" + title: "Wasserzeichen bearbeiten" + cover: "Alles bedecken" + opacity: "Transparenz" + scale: "Größe" + text: "Text" + position: "Position" + type: "Art" + image: "Bilder" + advanced: "Fortgeschritten" + stripe: "Streifen" + stripeWidth: "Linienbreite" + stripeFrequency: "Linienanzahl" + angle: "Winkel" + polkadot: "Punktmuster" + polkadotMainDotOpacity: "Deckkraft des Hauptpunktes" + polkadotMainDotRadius: "Größe des Hauptpunktes" + polkadotSubDotOpacity: "Deckkraft des Unterpunktes" + polkadotSubDotRadius: "Größe des Unterpunktes" + polkadotSubDotDivisions: "Anzahl der Unterpunkte" +_imageEffector: + title: "Effekte" + addEffect: "Effekte hinzufügen" + discardChangesConfirm: "Änderungen verwerfen und beenden?" + _fxs: + chromaticAberration: "Chromatische Abweichung" + glitch: "Glitch" + mirror: "Spiegeln" + invert: "Farben umkehren" + grayscale: "Schwarzweiß" + colorAdjust: "Farbkorrektur" + colorClamp: "Farbkomprimierung" + colorClampAdvanced: "Farbkomprimierung (erweitert)" + distort: "Verzerrung" + stripe: "Streifen" + polkadot: "Punktmuster" +drafts: "Entwurf" +_drafts: + select: "Entwurf auswählen" + cannotCreateDraftAnymore: "Die Anzahl der Entwürfe, die erstellt werden können, wurde überschritten." + cannotCreateDraft: "Mit diesem Inhalt kann kein Entwurf erstellt werden." + delete: "Entwurf löschen" + deleteAreYouSure: "Entwurf löschen?" + noDrafts: "Keine Entwürfe" + replyTo: "Antwort an {user}" + quoteOf: "Zitat von {user}s Notiz" + saveToDraft: "Als Entwurf speichern" + restoreFromDraft: "Aus Entwurf wiederherstellen" + restore: "Wiederherstellen" + listDrafts: "Liste der Entwürfe" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index c8aff304d2..5fc2bd7221 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -353,6 +353,7 @@ _visibility: home: "Κεντρικό" homeDescription: "Δημοσίευση στο κεντρικό χρονολόγιο μόνο" followers: "Ακολουθούν" + specified: "Απευθείας σημειώματα" _profile: name: "Όνομα" username: "Όνομα μέλους" @@ -395,6 +396,7 @@ _deck: antenna: "Αντένες" list: "Λίστα" mentions: "Επισημάνσεις" + direct: "Απευθείας σημειώματα" _webhookSettings: name: "Όνομα" _moderationLogTypes: @@ -403,3 +405,5 @@ _reversi: total: "Σύνολο" _search: searchScopeLocal: "Τοπικό" +_watermarkEditor: + image: "Εικόνες" diff --git a/locales/en-US.yml b/locales/en-US.yml index a7fd93d9b0..f5c0f25ad0 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -81,7 +81,7 @@ import: "Import" export: "Export" files: "Files" download: "Download" -driveFileDeleteConfirm: "Do you want to remove the file \"{name}\"? Some content using this file will also be removed." +driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted." unfollowConfirm: "Are you sure you want to unfollow {name}?" exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." importRequested: "You've requested an import. This may take a while." @@ -298,6 +298,7 @@ uploadFromUrl: "Upload from a URL" uploadFromUrlDescription: "URL of the file you want to upload" uploadFromUrlRequested: "Upload requested" uploadFromUrlMayTakeTime: "It may take some time until the upload is complete." +uploadNFiles: "Upload {n} files" explore: "Explore" messageRead: "Read" noMoreHistory: "There is no further history" @@ -326,6 +327,7 @@ dark: "Dark" lightThemes: "Light themes" darkThemes: "Dark themes" syncDeviceDarkMode: "Sync Dark Mode with your device settings" +switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" is turned on. Would you like to turn off synchronization and switch modes manually?" drive: "Drive" fileName: "Filename" selectFile: "Select a file" @@ -575,8 +577,10 @@ showFixedPostForm: "Display the posting form at the top of the timeline" showFixedPostFormInChannel: "Display the posting form at the top of the timeline (Channels)" withRepliesByDefaultForNewlyFollowed: "Include replies by newly followed users in the timeline by default" newNoteRecived: "There are new notes" +newNote: "New Note" sounds: "Sounds" sound: "Sounds" +notificationSoundSettings: "Notification sound settings" listen: "Listen" none: "None" showInPage: "Show in page" @@ -791,6 +795,7 @@ wide: "Wide" narrow: "Narrow" reloadToApplySetting: "This setting will only apply after a page reload. Reload now?" needReloadToApply: "A reload is required for this to be reflected." +needToRestartServerToApply: "A Misskey restart is required to reflect the change." showTitlebar: "Show title bar" clearCache: "Clear cache" onlineUsersCount: "{n} users are online" @@ -997,6 +1002,7 @@ failedToUpload: "Upload failed" cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially inappropriate." cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity." cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit." +cannotUploadBecauseUnallowedFileType: "Unable to upload due to unauthorized file type." beta: "Beta" enableAutoSensitive: "Automatic marking as sensitive" enableAutoSensitiveDescription: "Allows automatic detection and marking of sensitive media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide." @@ -1296,7 +1302,7 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification messageToFollower: "Message to followers" target: "Target" testCaptchaWarning: "This function is intended for CAPTCHA testing purposes.\nDo not use in a production environment." -prohibitedWordsForNameOfUser: "Prohibited words for user names" +prohibitedWordsForNameOfUser: "Prohibited words for usernames" prohibitedWordsForNameOfUserDescription: "If any of the strings in this list are included in the user's name, the name will be denied. Users with moderator privileges are not affected by this restriction." yourNameContainsProhibitedWords: "Your name contains prohibited words" yourNameContainsProhibitedWordsDescription: "If you wish to use this name, please contact your server administrator." @@ -1307,6 +1313,7 @@ availableRoles: "Available roles" acknowledgeNotesAndEnable: "Turn on after understanding the precautions." federationSpecified: "This server is operated in a whitelist federation. Interacting with servers other than those designated by the administrator is not allowed." federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers." +draft: "Drafts" confirmOnReact: "Confirm when reacting" reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?" markAsSensitiveConfirm: "Do you want to set this media as sensitive?" @@ -1324,6 +1331,7 @@ restore: "Restore" syncBetweenDevices: "Sync between devices" preferenceSyncConflictTitle: "The configured value exists on the server." preferenceSyncConflictText: "The sync enabled settings will save their values to the server. However, there are existing values on the server. Which set of values would you like to overwrite?" +preferenceSyncConflictChoiceMerge: "Merge" preferenceSyncConflictChoiceServer: "Configured value on server" preferenceSyncConflictChoiceDevice: "Configured value on device" preferenceSyncConflictChoiceCancel: "Cancel enabling sync" @@ -1346,6 +1354,29 @@ goToDeck: "Return to Deck" federationJobs: "Federation Jobs" driveAboutTip: "In Drive, a list of files you've uploaded in the past will be displayed.
\nYou can reuse these files when attaching them to notes, or you can upload files in advance to post later.
\nBe careful when deleting a file, as it will not be available in all places where it was used (such as notes, pages, avatars, banners, etc.).
\nYou can also create folders to organize your files." scrollToClose: "Scroll to close" +advice: "Advice" +realtimeMode: "Real-time mode" +turnItOn: "Turn on" +turnItOff: "Turn off" +emojiMute: "Mute emoji" +emojiUnmute: "Unmute emoji" +muteX: "Mute {x}" +unmuteX: "Unmute {x}" +abort: "Abort" +tip: "Tips & Tricks" +redisplayAllTips: "Show all “Tips & Tricks” again" +hideAllTips: "Hide all \"Tips & Tricks\"" +defaultImageCompressionLevel: "Default image compression level" +defaultImageCompressionLevel_description: "Lower level preserves image quality but increases file size.
Higher level reduce file size, but reduce image quality." +inMinutes: "Minute(s)" +inDays: "Day(s)" +safeModeEnabled: "Safe mode is enabled" +pluginsAreDisabledBecauseSafeMode: "All plugins are disabled because safe mode is enabled." +customCssIsDisabledBecauseSafeMode: "Custom CSS is not applied because safe mode is enabled." +themeIsDefaultBecauseSafeMode: "While safe mode is active, the default theme is used. Disabling safe mode will revert these changes." +_order: + newest: "Newest First" + oldest: "Oldest First" _chat: noMessagesYet: "No messages yet" newMessage: "New message" @@ -1375,10 +1406,12 @@ _chat: muteThisRoom: "Mute room" deleteRoom: "Delete room" chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account." - chatIsReadOnlyForThisAccountOrServer: "Chat is read-only on this instance or this account. You cannot write new messages or create/join chat rooms." + chatIsReadOnlyForThisAccountOrServer: "Chat is read-only on this server or this account. You cannot write new messages or create/join chat rooms." chatNotAvailableInOtherAccount: "The chat function is disabled for the other user." cannotChatWithTheUser: "Cannot start a chat with this user" cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat." + youAreNotAMemberOfThisRoomButInvited: "You are not a participant in this room, but you have received an invitation. Please accept the invitation to join." + doYouAcceptInvitation: "Do you accept the invitation?" chatWithThisUser: "Chat with user" thisUserAllowsChatOnlyFromFollowers: "This user accepts chats from followers only." thisUserAllowsChatOnlyFromFollowing: "This user accepts chats only from users they follow." @@ -1418,12 +1451,20 @@ _settings: makeEveryTextElementsSelectable: "Make all text elements selectable" makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations." useStickyIcons: "Make icons follow while scrolling" + enableHighQualityImagePlaceholders: "Display placeholders for high quality images" + uiAnimations: "UI Animations" showNavbarSubButtons: "Show sub-buttons on the navigation bar" ifOn: "When turned on" ifOff: "When turned off" enableSyncThemesBetweenDevices: "Synchronize installed themes across devices" enablePullToRefresh: "Pull to Refresh" enablePullToRefresh_description: "When using a mouse, drag while pressing in the scroll wheel." + realtimeMode_description: "Establishes a connection with the server and updates content in real time. This may increase traffic and memory consumption." + contentsUpdateFrequency: "Frequency of content retrieval" + contentsUpdateFrequency_description: "The higher the value the more the content updates but it lowers the performance and increases the traffic and memory consumption." + contentsUpdateFrequency_description2: "When real-time mode is on, content is updated in real time regardless of this setting." + showUrlPreview: "Show URL preview" + showAvailableReactionsFirstInNote: "Show available reactions at the top." _chat: showSenderName: "Show sender's name" sendOnEnter: "Press Enter to send" @@ -1463,7 +1504,7 @@ _abuseUserReport: resolveTutorial: "If the report's content is legitimate, select \"Accept\" to mark it as resolved.\nIf the report's content is illegitimate, select \"Reject\" to ignore it." _delivery: status: "Delivery status" - stop: "Suspended" + stop: "Suspend" resume: "Delivery resume" _type: none: "Publishing" @@ -1597,6 +1638,10 @@ _serverSettings: fanoutTimelineDbFallback: "Fallback to database" fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved." reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase." + remoteNotesCleaning: "Automatic cleanup of remote notes" + remoteNotesCleaning_description: "When enabled, unused and outdated remote notes will be periodically cleaned up to prevent database bloat." + remoteNotesCleaningMaxProcessingDuration: "Maximum cleanup processing time" + remoteNotesCleaningExpiryDaysForEachNotes: "Minimum days to retain notes" inquiryUrl: "Inquiry URL" inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." openRegistration: "Make the account creation open" @@ -1604,6 +1649,23 @@ _serverSettings: thisSettingWillAutomaticallyOffWhenModeratorsInactive: "If no moderator activity is detected for a while, this setting will be automatically turned off to prevent spam." deliverSuspendedSoftware: "Suspended Software" deliverSuspendedSoftwareDescription: "You can specify a range of names and versions of the server's software to stop delivery for vulnerability or other reasons. This version information is provided by the server and is not guaranteed to be reliable. A semver range specification can be used to specify the version, but specifying >= 2024.3.1 will not include custom versions such as 2024.3.1-custom.0, so it is recommended that a prerelease specification be used, such as >= 2024.3.1-0" + singleUserMode: "Single user mode" + singleUserMode_description: "If you are the only user of this server, enabling this mode will optimize its performance." + signToActivityPubGet: "Sign ActivityPub GET requests" + signToActivityPubGet_description: "Normally, this should be enabled. Disabling it may improve issues related to federation, but on the other hand it could disable federation towards some other servers." + proxyRemoteFiles: "Proxy remote files" + proxyRemoteFiles_description: "When enabled, the server will proxy and serve remote files. This is useful for generating image thumbnails and protecting user privacy." + allowExternalApRedirect: "Allow redirects for queries via ActivityPub" + allowExternalApRedirect_description: "If enabled, other servers can query third-party content through this server but this may result in content spoofing." + userGeneratedContentsVisibilityForVisitor: "Visibility of user-generated content to guests" + userGeneratedContentsVisibilityForVisitor_description: "This is useful for preventing problems caused by inappropriate remote content that is not well moderated from being unintentionally published on the Internet via your own server." + userGeneratedContentsVisibilityForVisitor_description2: "Unconditionally publishing all content on the server to the Internet, including remote content received by the server is risky. This is especially important for guests who are unaware of the distributed nature of the content, as they may mistakenly believe that even remote content is content created by users on the server." + restartServerSetupWizardConfirm_title: "Restart server setup wizard?" + restartServerSetupWizardConfirm_text: "Some current settings will be reset." + _userGeneratedContentsVisibilityForVisitor: + all: "Everything is public" + localOnly: "Only local content is published, remote content is kept private" + none: "Everything is private" _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -1944,6 +2006,11 @@ _role: canImportMuting: "Allow importing muting" canImportUserLists: "Allow importing lists" chatAvailability: "Allow Chat" + uploadableFileTypes: "Uploadable file types" + uploadableFileTypes_caption: "Specifies the allowed MIME/file types. Multiple MIME types can be specified by separating them with a new line, and wildcards can be specified with an asterisk (*). (e.g., image/*)" + uploadableFileTypes_caption2: "Some files types might fail to be detected. To allow such files, add {x} to the specification." + noteDraftLimit: "Number of possible drafts of server notes" + watermarkAvailable: "Availability of watermark function" _condition: roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" @@ -2103,6 +2170,7 @@ _theme: install: "Install a theme" manage: "Manage themes" code: "Theme code" + copyThemeCode: "Copy theme code" description: "Description" installed: "{name} has been installed" installedThemes: "Installed themes" @@ -2161,7 +2229,6 @@ _theme: buttonBg: "Button background" buttonHoverBg: "Button background (Hover)" inputBorder: "Input field border" - driveFolderBg: "Drive folder background" badge: "Badge" messageBg: "Chat background" fgHighlighted: "Highlighted Text" @@ -2275,7 +2342,7 @@ _permissions: "read:admin:index-stats": "View database index stats" "read:admin:table-stats": "View database table stats" "read:admin:user-ips": "View user IP addresses" - "read:admin:meta": "View instance metadata" + "read:admin:meta": "View server metadata" "write:admin:reset-password": "Reset user password" "write:admin:resolve-abuse-user-report": "Resolve user report" "write:admin:send-email": "Send email" @@ -2286,7 +2353,7 @@ _permissions: "write:admin:unset-user-avatar": "Remove user avatar" "write:admin:unset-user-banner": "Remove user banner" "write:admin:unsuspend-user": "Unsuspend user" - "write:admin:meta": "Manage instance metadata" + "write:admin:meta": "Manage server metadata" "write:admin:user-note": "Manage moderation note" "write:admin:roles": "Manage roles" "read:admin:roles": "View roles" @@ -2417,6 +2484,8 @@ _visibility: disableFederation: "Defederate" disableFederationDescription: "Don't transmit to other instances" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "There are files that have not been uploaded, do you want to discard them and close the form?" + uploaderTip: "The file has not yet been uploaded. From the file menu, you can rename, crop images, watermark and compress or uncompress the file. Files are automatically uploaded when you publish a note." replyPlaceholder: "Reply to this note..." quotePlaceholder: "Quote this note..." channelPlaceholder: "Post to a channel..." @@ -2716,7 +2785,7 @@ _moderationLogTypes: resetPassword: "Password reset" suspendRemoteInstance: "Remote instance suspended" unsuspendRemoteInstance: "Remote instance unsuspended" - updateRemoteInstanceNote: "Moderation note updated for remote instance." + updateRemoteInstanceNote: "Updated moderation note for remote servers" markSensitiveDriveFile: "File marked as sensitive" unmarkSensitiveDriveFile: "File unmarked as sensitive" resolveAbuseReport: "Report resolved" @@ -2750,6 +2819,7 @@ _fileViewer: url: "URL" uploadedAt: "Uploaded at" attachedNotes: "Attached notes" + usage: "Used" thisPageCanBeSeenFromTheAuthor: "This page can only be seen by the user who uploaded this file." _externalResourceInstaller: title: "Install from external site" @@ -2797,9 +2867,12 @@ _dataSaver: _avatar: title: "Avatar image" description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." - _urlPreview: - title: "URL preview thumbnails" + _urlPreviewThumbnail: + title: "Hide URL preview thumbnails" description: "URL preview thumbnail images will no longer be loaded." + _disableUrlPreview: + title: "Disable URL preview" + description: "Disables the URL preview function. Unlike thumbnail images, this function reduces the loading of the linked information itself." _code: title: "Code highlighting" description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data." @@ -2857,6 +2930,8 @@ _offlineScreen: _urlPreviewSetting: title: "URL preview settings" enable: "Enable URL preview" + allowRedirect: "Allow URL preview redirection" + allowRedirectDescription: "If a URL has a redirection set, you can enable this feature to follow the redirection and display a preview of the redirected content. Disabling this will save server resources, but redirected content will not be displayed." timeout: "Time out when getting preview (ms)" timeoutDescription: "If it takes longer than this value to get the preview, the preview won’t be generated." maximumContentLength: "Maximum Content-Length (bytes)" @@ -2918,7 +2993,7 @@ _customEmojisManager: markAsDeleteTargetRanges: "Mark rows in the selection as a target to delete" alertUpdateEmojisNothingDescription: "There are no updated Emojis." alertDeleteEmojisNothingDescription: "There are no Emojis to be deleted." - confirmMovePage: "" + confirmMovePage: "Would you like to move pages?" confirmChangeView: "" confirmUpdateEmojisDescription: "Update {count} Emoji(s). Are you sure to continue?" confirmDeleteEmojisDescription: "Delete checked {count} Emoji(s). Are you sure to continue?" @@ -2930,10 +3005,6 @@ _customEmojisManager: uploadSettingDescription: "On this screen, you can configure the behavior when uploading Emojis." directoryToCategoryLabel: "Enter the directory name in the \"category\" field" directoryToCategoryCaption: "When you drag and drop a directory, enter the directory name in the \"category\" field." - emojiInputAreaCaption: "Select the Emojis you wish to register using one of the methods." - emojiInputAreaList1: "Drag and drop image files or a directory into this frame" - emojiInputAreaList2: "Click this link to select from your computer" - emojiInputAreaList3: "Click this link to select from the drive" confirmRegisterEmojisDescription: "Register the Emojis from the list as new custom Emojis. Are you sure to continue? (To avoid overload, only {count} Emoji(s) can be registered in a single operation)" confirmClearEmojisDescription: "Discard the edits and clear the Emojis from the list. Are you sure to continue?" confirmUploadEmojisDescription: "Upload the dragged and dropped {count} file(s) to the drive. Are you sure to continue?" @@ -3001,6 +3072,7 @@ _bootErrors: otherOption1: "Delete client settings and cache" otherOption2: "Start the simple client" otherOption3: "Launch the repair tool" + otherOption4: "Launch Misskey in safe mode" _search: searchScopeAll: "All" searchScopeLocal: "Local" @@ -3009,3 +3081,132 @@ _search: pleaseEnterServerHost: "Enter the server host" pleaseSelectUser: "Select user" serverHostPlaceholder: "Example: misskey.example.com" +_serverSetupWizard: + installCompleted: "Misskey installation is now complete!" + firstCreateAccount: "To begin, create an administrator account." + accountCreated: "Administrator account has been created!" + serverSetting: "Server Settings" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "This wizard makes it easier to configure the server settings." + settingsYouMakeHereCanBeChangedLater: "The settings that were changed via this wizard can be adjusted later." + howWillYouUseMisskey: "How will you use Misskey?" + _use: + single: "Single User server" + single_description: "Use it alone as your own server." + single_youCanCreateMultipleAccounts: "Multiple accounts can be created as needed, even when operated as a single user server." + group: "Group server" + group_description: "Invite other trusted users to use it with more than one user." + open: "Public server" + open_description: "Allow anyone to register." + openServerAdvice: "Accepting a large number of unknown users involves risk. We recommend that you operate with a reliable moderation system to handle any problems." + openServerAntiSpamAdvice: "To prevent your server from becoming a stepping stone for spam, you should also pay close attention to security by enabling anti-bot functions such as reCAPTCHA." + howManyUsersDoYouExpect: "How many users do you expect?" + _scale: + small: "Less than 100 (small scale)" + medium: "More than 100 and less than 1000 users (medium size)" + large: "More than 1000 (Large scale)" + largeScaleServerAdvice: "Large servers may require advanced infrastructure knowledge, such as load balancing and database replication." + doYouConnectToFediverse: "Do you want to connect to the Fediverse?" + doYouConnectToFediverse_description1: "When connected to a network of distributed servers (Fediverse) content can be exchanged with other servers." + doYouConnectToFediverse_description2: "Connecting with the Fediverse is also called \"federation\"" + youCanConfigureMoreFederationSettingsLater: "Advanced settings such as specifying federated servers can be configured later." + remoteContentsCleaning: "Automatic cleanup of received contents" + remoteContentsCleaning_description: "Federation may result in a continuous inflow of content. Enabling automatic cleanup will remove outdated and unreferenced content from the server to save storage." + adminInfo: "Administrator information" + adminInfo_description: "Sets the administrator information used to receive inquiries." + adminInfo_mustBeFilled: "Must be entered if public server or federation is on." + followingSettingsAreRecommended: "The following settings are recommended" + applyTheseSettings: "Apply these settings" + skipSettings: "Skip settings" + settingsCompleted: "Setup is now complete!" + settingsCompleted_description: "Thank you for your time. Now that everything is ready, you can start using the server right away." + settingsCompleted_description2: "The server settings can be changed from the “Control Panel”" + donationRequest: "Donation Request" + _donationRequest: + text1: "Misskey is a free software developed by volunteers." + text2: "We would appreciate your support so that we can continue to develop this software further into the future." + text3: "There are also special benefits for supporters!" +_uploader: + editImage: "Edit Image" + compressedToX: "Compressed to {x}" + savedXPercent: "Saving {x}%" + abortConfirm: "Some files have not been uploaded, do you want to abort?" + doneConfirm: "Some files have not been uploaded, do you want to continue anyway?" + maxFileSizeIsX: "The maximum file size that can be uploaded is {x}" + allowedTypes: "Uploadable file types" + tip: "The file has not yet been uploaded so this dialog allows you to confirm, rename, compress, and crop the file before uploading. When ready, you can start uploading by pressing the “Upload” button." +_clientPerformanceIssueTip: + title: "Performance tips" + makeSureDisabledAdBlocker: "Disable your adblocker" + makeSureDisabledAdBlocker_description: "Adblockers can affect performance, please make sure that adblockers are not enabled by your system or browser features/extensions." + makeSureDisabledCustomCss: "Disable custom CSS" + makeSureDisabledCustomCss_description: "Overriding styles can affect performance. Please make sure that custom CSS or extensions that override styles are not enabled." + makeSureDisabledAddons: "Disable extensions" + makeSureDisabledAddons_description: "Some extensions may interfere with client behavior and affect performance. Please disable your browser extensions and see if this improves the situation." +_clip: + tip: "Clip is a feature that allows you to organize your notes." +_userLists: + tip: "Lists can contain any user you specify when creating, the created list can then be displayed as a timeline showing only the specified users." +watermark: "Watermark" +defaultPreset: "Default Preset" +_watermarkEditor: + tip: "A watermark, such as credit information, can be added to the image." + quitWithoutSaveConfirm: "Discard unsaved changes?" + driveFileTypeWarn: "This file is not supported" + driveFileTypeWarnDescription: "Choose an image file" + title: "Edit Watermark" + cover: "Cover everything" + repeat: "spread all over" + opacity: "Opacity" + scale: "Size" + text: "Text" + position: "Position" + type: "Type" + image: "Images" + advanced: "Advanced" + stripe: "Stripes" + stripeWidth: "Line width" + stripeFrequency: "Lines count" + angle: "Angle" + polkadot: "Polkadot" + checker: "Checker" + polkadotMainDotOpacity: "Opacity of the main dot" + polkadotMainDotRadius: "Size of the main dot" + polkadotSubDotOpacity: "Opacity of the secondary dot" + polkadotSubDotRadius: "Size of the secondary dot" + polkadotSubDotDivisions: "Number of sub-dots." +_imageEffector: + title: "Effects" + addEffect: "Add Effects" + discardChangesConfirm: "Are you sure you want to leave? You have unsaved changes." + _fxs: + chromaticAberration: "Chromatic Aberration" + glitch: "Glitch" + mirror: "Mirror" + invert: "Invert Colors" + grayscale: "Grayscale" + colorAdjust: "Color Correction" + colorClamp: "Color Compression" + colorClampAdvanced: "Color Compression (Advanced)" + distort: "Distortion" + threshold: "Binarize" + zoomLines: "Saturated lines" + stripe: "Stripes" + polkadot: "Polkadot" + checker: "Checker" + blockNoise: "Block Noise" + tearing: "Tearing" +drafts: "Drafts" +_drafts: + select: "Select Draft" + cannotCreateDraftAnymore: "The number of drafts that can be created has been exceeded." + cannotCreateDraft: "You cannot create a draft with this content." + delete: "Delete Draft" + deleteAreYouSure: "Delete draft?" + noDrafts: "No drafts" + replyTo: "Reply to {user}" + quoteOf: "Citation to {user}'s note" + postTo: "Posting to {channel}" + saveToDraft: "Save to Draft" + restoreFromDraft: "Restore from Draft" + restore: "Restore" + listDrafts: "List of Drafts" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index b3f7c5ff3d..20289f605c 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -61,11 +61,11 @@ copyRSS: "Copiar RSS" copyUsername: "Copiar nombre de usuario" copyUserId: "Copiar ID del usuario" copyNoteId: "Copiar ID de la nota" -copyFileId: "Copiar ID del archivo" +copyFileId: "Copiar ID de archivo" copyFolderId: "Copiar ID de carpeta" copyProfileUrl: "Copiar la URL del perfil" searchUser: "Buscar un usuario" -searchThisUsersNotes: "" +searchThisUsersNotes: "Buscar en las notas de este usuario" reply: "Responder" loadMore: "Ver más" showMore: "Ver más" @@ -83,10 +83,10 @@ files: "Archivos" download: "Descargar" driveFileDeleteConfirm: "¿Desea borrar el archivo \"{name}\"? Las notas que tengan este archivo como adjunto serán eliminadas" unfollowConfirm: "¿Desea dejar de seguir a {name}?" -exportRequested: "Se ha solicitado la exportación. Puede tomar un tiempo. Cuando termine la exportación, se añadirá en el drive" -importRequested: "Se ha solicitado la importación. Puede tomar un tiempo." +exportRequested: "Has solicitado la exportación. Puede llevar un tiempo. Cuando termine la exportación, se añadirá al drive" +importRequested: "Has solicitado la importación. Puede llevar un tiempo." lists: "Listas" -noLists: "No tiene listas" +noLists: "No tienes ninguna lista" note: "Notas" notes: "Notas" following: "Siguiendo" @@ -99,9 +99,9 @@ somethingHappened: "Ocurrió un error" retry: "Reintentar" pageLoadError: "Error al leer la página" pageLoadErrorDescription: "Normalmente es debido a la red o al caché del navegador. Por favor limpie el caché o intente más tarde." -serverIsDead: "No hay respuesta del servidor. Espere un momento y vuelva a intentarlo." -youShouldUpgradeClient: "Para ver esta página, por favor refrezca el navegador y utiliza una versión más reciente del cliente." -enterListName: "Ingrese nombre de lista" +serverIsDead: "No hay respuesta del servidor. Espera un momento y vuelve a intentarlo." +youShouldUpgradeClient: "Para ver esta página, recarga el navegador para actualizar el cliente." +enterListName: "Introduce un nombre para la lista" privacy: "Privacidad" makeFollowManuallyApprove: "Aprobar manualmente las solicitudes de seguimiento" defaultNoteVisibility: "Visibilidad por defecto" @@ -125,7 +125,7 @@ renoteToOtherChannel: "Renotar a otro canal" pinnedNote: "Nota fijada" pinned: "Fijar al perfil" you: "Tú" -clickToShow: "Click para ver" +clickToShow: "Haz clic para verlo" sensitive: "Marcado como sensible" add: "Agregar" reaction: "Reacción" @@ -134,28 +134,28 @@ emojiPicker: "Selector de emojis" pinnedEmojisForReactionSettingDescription: "Puedes seleccionar reacciones para fijarlos en el selector" pinnedEmojisSettingDescription: "Puedes seleccionar emojis para fijarlos en el selector" emojiPickerDisplay: "Mostrar el selector de emojis" -overwriteFromPinnedEmojisForReaction: "Sobreescribir las reacciones fijadas" -overwriteFromPinnedEmojis: "Sobreescribir los emojis fijados" -reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir." +overwriteFromPinnedEmojisForReaction: "Sobreescribir los ajustes de reacciones" +overwriteFromPinnedEmojis: "Sobreescribir los ajustes generales" +reactionSettingDescription2: "Arrastra para reordenar, click para borrar, pulsa \"+\" para añadir." rememberNoteVisibility: "Recordar visibilidad" attachCancel: "Quitar adjunto" -deleteFile: "Archivo eliminado" +deleteFile: "Eliminar archivo" markAsSensitive: "Marcar como sensible" unmarkAsSensitive: "Desmarcar como sensible" -enterFileName: "Ingrese el nombre del archivo" +enterFileName: "Introduce el nombre del archivo" mute: "Silenciar" unmute: "Dejar de silenciar" renoteMute: "Silenciar renota" renoteUnmute: "Desilenciar renota" block: "Bloquear" -unblock: "Dejar de bloquear" +unblock: "Desbloquear" suspend: "Suspender" unsuspend: "Dejar de suspender" -blockConfirm: "¿Quiere bloquear esta cuenta?" -unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?" -suspendConfirm: "¿Quiere suspender esta cuenta?" -unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?" -selectList: "Seleccione una lista" +blockConfirm: "¿Quieres bloquear esta cuenta?" +unblockConfirm: "¿Quieres desbloquear esta cuenta?" +suspendConfirm: "¿Quieres suspender esta cuenta?" +unsuspendConfirm: "¿Quieres dejar de suspender esta cuenta?" +selectList: "Selecciona una lista" editList: "Editar lista" selectChannel: "Seleccionar canal" selectAntenna: "Seleccionar antena" @@ -163,55 +163,55 @@ editAntenna: "Editar antena" createAntenna: "Crear una antena" selectWidget: "Seleccionar widget" editWidgets: "Editar widgets" -editWidgetsExit: "Terminar edición" +editWidgetsExit: "Hecho" customEmojis: "Emojis personalizados" emoji: "Emoji" -emojis: "Emoji" +emojis: "Emojis" emojiName: "Nombre del emoji" -emojiUrl: "URL de la imágen del emoji" -addEmoji: "Agregar emoji" -settingGuide: "Configuración sugerida" -cacheRemoteFiles: "Mantener en cache los archivos remotos" -cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas." +emojiUrl: "URL del emoji" +addEmoji: "Añadir emoji" +settingGuide: "Configuración recomendada" +cacheRemoteFiles: "Mantener los archivos remotos en caché" +cacheRemoteFilesDescription: "Si desactivas esta configuración, los archivos remotos se cargarán directamente de los servidores remotos. Desactivar esto reducirá el uso de almacenamiento, pero incrementará el uso de tráfico, ya que no se generarán miniaturas." youCanCleanRemoteFilesCache: "Puedes vaciar la caché pulsando en el botón 🗑️ en el administrador de archivos." -cacheRemoteSensitiveFiles: "Cachear archivos remotos sensibles" -cacheRemoteSensitiveFilesDescription: "Cuando esta opción está desactivada, los archivos remotos sensibles son cargador directamente de la instancia origen sin ser cacheados." +cacheRemoteSensitiveFiles: "Mantener los archivos remotos sensibles en caché" +cacheRemoteSensitiveFilesDescription: "Cuando esta opción está desactivada, los archivos remotos sensibles se cargarán directamente desde los servidores remotos." flagAsBot: "Esta cuenta es un bot" -flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de Misskey para que trate a esta cuenta como un bot." -flagAsCat: "Esta cuenta es un gato" -flagAsCatDescription: "En caso de que declare que esta cuenta es de un gato, active esta opción." -flagShowTimelineReplies: "Mostrar respuestas a las notas en la biografía" -flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas a otras notas además de las notas del usuario" +flagAsBotDescription: "Activa esta opción si la cuenta es utilizada por un programa. Si se activa, actuará como una etiqueta para otros desarrolladores para prevenir cadenas eternas de interacción con otros bots, y ajustará los sistemas internos de Misskey para tratar esta cuenta de manera acorde." +flagAsCat: "Marcar esta cuenta como gato" +flagAsCatDescription: "Activa esta opción para marcar esta cuenta como un gato." +flagShowTimelineReplies: "Mostrar respuestas en la línea de tiempo" +flagShowTimelineRepliesDescription: "Muestra respuestas de los usuarios a las notas de otros usuarios en la línea de tiempo al activar esta opción." autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues" -addAccount: "Agregar Cuenta" +addAccount: "Agregar cuenta" reloadAccountsList: "Recargar lista de cuentas" loginFailed: "Error al iniciar sesión." -showOnRemote: "Ver en una instancia remota" -continueOnRemote: "Ver en una instancia remota" +showOnRemote: "Ver en instancia remota" +continueOnRemote: "Continuar en una instancia remota" chooseServerOnMisskeyHub: "Elegir un servidor en Misskey Hub" specifyServerHost: "Especifica una instancia directamente" -inputHostName: "Introduzca el dominio" +inputHostName: "Introduce el dominio" general: "General" wallpaper: "Fondo de pantalla" setWallpaper: "Establecer fondo de pantalla" removeWallpaper: "Quitar fondo de pantalla" searchWith: "Buscar: {q}" -youHaveNoLists: "No tienes listas" -followConfirm: "¿Desea seguir a {name}?" +youHaveNoLists: "No tienes ninguna lista" +followConfirm: "¿Quieres seguir a {name}?" proxyAccount: "Cuenta proxy" -proxyAccountDescription: "Una cuenta proxy es una cuenta que actúa como un seguidor remoto de un usuario bajo ciertas condiciones. Por ejemplo, cuando un usuario añade un usuario remoto a una lista, si ningún usuario local sigue al usuario agregado a la lista, la instancia no puede obtener su actividad. Así que la cuenta proxy sigue al usuario añadido a la lista" -host: "Host" +proxyAccountDescription: "Una cuenta proxy es una cuenta que actúa como un seguidor remoto de un usuario bajo ciertas condiciones. Por ejemplo, cuando un usuario añade un usuario remoto a una lista, si ningún usuario local sigue al usuario agregado a la lista, la instancia no puede obtener su actividad, así que la cuenta proxy sigue al usuario añadido a la lista" +host: "Instancia" selectSelf: "Elígete a ti mismo" selectUser: "Elegir usuario" -recipient: "Recipiente" +recipient: "Receptor" annotation: "Anotación" federation: "Federación" -instances: "Instancia" +instances: "Instancias" registeredAt: "Registrado en" -latestRequestReceivedAt: "Ultimo pedido recibido" -latestStatus: "Último status" +latestRequestReceivedAt: "Última petición recibida" +latestStatus: "Último estado" storageUsage: "Almacenamiento usado" -charts: "Chat" +charts: "Métricas" perHour: "por hora" perDay: "por día" stopActivityDelivery: "Dejar de enviar actividades" @@ -220,45 +220,46 @@ silenceThisInstance: "Silenciar esta instancia" mediaSilenceThisInstance: "Silencia la Multimedia(Imágenes,videos...) para este servidor" operations: "Operaciones" software: "Software" +softwareName: "Nombre del software" version: "Versión" metadata: "Metadatos" withNFiles: "{n} archivos" monitor: "Monitor" jobQueue: "Cola de trabajos" -cpuAndMemory: "CPU y Memoria" +cpuAndMemory: "CPU y memoria" network: "Red" disk: "Disco" -instanceInfo: "información de la instancia" +instanceInfo: "Información de la instancia" statistics: "Estadísticas" clearQueue: "Limpiar cola" -clearQueueConfirmTitle: "¿Desea limpiar la cola?" +clearQueueConfirmTitle: "¿Quieres limpiar la cola?" clearQueueConfirmText: "Las notas aún no entregadas no se federarán. Normalmente no se necesita ejecutar esta operación" clearCachedFiles: "Limpiar caché" -clearCachedFilesConfirm: "¿Desea borrar todos los archivos remotos cacheados?" +clearCachedFilesConfirm: "¿Quieres borrar todos los archivos remotos en caché?" blockedInstances: "Instancias bloqueadas" -blockedInstancesDescription: "Seleccione los hosts de las instancias que desea bloquear, separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse con esta instancia." +blockedInstancesDescription: "La lista de los dominios de las instancias que quieres bloquear, separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse con esta instancia." silencedInstances: "Instancias silenciadas" -silencedInstancesDescription: "Listar los hostname de las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas." -mediaSilencedInstances: "Servidores silenciados (Multimedia)" -mediaSilencedInstancesDescription: "Listar las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas." +silencedInstancesDescription: "La lista de los dominios de las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas." +mediaSilencedInstances: "Servidores con multimedia silenciada" +mediaSilencedInstancesDescription: "La lista de los dominios de las instancias cuya multimedia quieres silenciar. Todas las cuentas que pertenezcan a estas instancias serán marcadas como sensibles, y no podrán usar sus emojis personalizados. Esto no afectará a las instancias bloqueadas" federationAllowedHosts: "Servidores federados" -federationAllowedHostsDescription: "Establezca los nombres de los servidores que pueden federarse, separados por una nueva línea." +federationAllowedHostsDescription: "La lista de los dominios de las instancias cuya federación está permitida, separadas por saltos de línea." muteAndBlock: "Silenciar y bloquear" mutedUsers: "Usuarios silenciados" blockedUsers: "Usuarios bloqueados" noUsers: "No hay usuarios" editProfile: "Editar perfil" -noteDeleteConfirm: "¿Desea borrar esta nota?" -pinLimitExceeded: "Ya no se pueden fijar más posts" +noteDeleteConfirm: "¿Quieres borrar esta nota?" +pinLimitExceeded: "Ya no se pueden fijar más notas" done: "Terminado" -processing: "Procesando" +processing: "Procesando..." preview: "Vista previa" default: "Predeterminado" defaultValueIs: "Por defecto: {value}" noCustomEmojis: "No hay emojis personalizados" noJobs: "No hay trabajos" federating: "Federando" -blocked: "Bloqueando" +blocked: "Bloqueado" suspended: "Suspendido" all: "Todo" subscribing: "Suscribiendo" @@ -279,8 +280,8 @@ featured: "Destacados" usernameOrUserId: "Nombre o ID del usuario" noSuchUser: "No se encuentra el usuario" lookup: "Búsqueda" -announcements: "Anuncios" -imageUrl: "URL de la imágen" +announcements: "Avisos" +imageUrl: "URL de la imagen." remove: "Borrar" removed: "Borrado" removeAreYouSure: "¿Desea borrar \"{x}\"?" @@ -297,6 +298,7 @@ uploadFromUrl: "Subir desde una URL" uploadFromUrlDescription: "URL del fichero que quieres subir" uploadFromUrlRequested: "Subida solicitada" uploadFromUrlMayTakeTime: "Subir el fichero puede tardar un tiempo." +uploadNFiles: "Subir {n} archivos" explore: "Explorar" messageRead: "Ya leído" noMoreHistory: "El historial se ha acabado" @@ -325,6 +327,7 @@ dark: "Oscuro" lightThemes: "Tema claro" darkThemes: "Tema oscuro" syncDeviceDarkMode: "Sincronice el Modo Oscuro con la configuración de su dispositivo" +switchDarkModeManuallyWhenSyncEnabledConfirm: "{x} está activado ¿Te gustaría desactivar la sincronización y cambiar al modo manual?" drive: "Drive" fileName: "Nombre de archivo" selectFile: "Elegir archivo" @@ -574,8 +577,10 @@ showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)" withRepliesByDefaultForNewlyFollowed: "Incluir por defecto respuestas de usuarios recién seguidos en la línea de tiempo" newNoteRecived: "Tienes una nota nueva" +newNote: "Nueva nota" sounds: "Sonidos" sound: "Sonidos" +notificationSoundSettings: "Configuración del sonido de las notificaciones" listen: "Escuchar" none: "Ninguna" showInPage: "Mostrar en la página" @@ -790,6 +795,7 @@ wide: "Ancho" narrow: "Estrecho" reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la página. ¿Recargar ahora?" needReloadToApply: "Se requiere un reinicio para la aplicar los cambios" +needToRestartServerToApply: "Se requiere un reinicio para la aplicar los cambios" showTitlebar: "Mostrar la barra de título" clearCache: "Limpiar caché" onlineUsersCount: "{n} usuarios en línea" @@ -836,13 +842,13 @@ unlikeConfirm: "¿Quitar como favorito?" fullView: "Vista completa" quitFullView: "quitar vista completa" addDescription: "Agregar descripción" -userPagePinTip: "Puede mantener sus notas visibles aquí seleccionando Pin en el menú de notas individuales" +userPagePinTip: "Puede mantener sus notas visibles aquí seleccionando 'Fijar al perfil' en el menú de notas individuales" notSpecifiedMentionWarning: "Algunas menciones no están incluidas en el destino" info: "Información" userInfo: "Información del usuario" unknown: "Desconocido" onlineStatus: "En línea" -hideOnlineStatus: "mostrarse como desconectado" +hideOnlineStatus: "Mostrarse como desconectado" hideOnlineStatusDescription: "Ocultar su estado en línea puede reducir la eficacia de algunas funciones, como la búsqueda" online: "En línea" active: "Activo" @@ -871,7 +877,7 @@ popularPosts: "Más vistos" shareWithNote: "Compartir con una nota" ads: "Anuncios" expiration: "Termina el" -startingperiod: "periodo de inicio" +startingperiod: "Comienzo" memo: "Notas" priority: "Prioridad" high: "Alta" @@ -996,6 +1002,7 @@ failedToUpload: "La subida falló" cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas partes han sido detectadas comoNSFW." cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en la unidad del usuario." cannotUploadBecauseExceedsFileSizeLimit: "Este archivo supera el peso máximo y no puede ser subido." +cannotUploadBecauseUnallowedFileType: "Incapaz de subir el archivo debido a que es un tipo de archivo no autorizado." beta: "Beta" enableAutoSensitive: "Marcar automáticamente contenido NSFW" enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está desactivada, puede ser activado para toda la instancia." @@ -1136,7 +1143,7 @@ channelArchiveConfirmTitle: "¿Seguro de archivar {name}?" channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas." thisChannelArchived: "El canal ha sido archivado." displayOfNote: "Mostrar notas" -initialAccountSetting: "Configración inicial de su cuenta\nか\nConfigración de inicio" +initialAccountSetting: "Configración inicial de su cuenta" youFollowing: "Siguiendo" preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generativa)" preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada." @@ -1306,6 +1313,7 @@ availableRoles: "Roles disponibles " acknowledgeNotesAndEnable: "Activar después de comprender las precauciones" federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador." federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores" +draft: "Borrador" confirmOnReact: "Confirmar la reacción" reactAreYouSure: "¿Quieres añadir una reacción «{emoji}»?" markAsSensitiveConfirm: "¿Desea establecer este medio multimedia(Imagen,vídeo...) como sensible?" @@ -1323,14 +1331,19 @@ restore: "Restaurar" syncBetweenDevices: "Sincronizar entre dispositivos" preferenceSyncConflictTitle: "Los valores configurados existen en el servidor." preferenceSyncConflictText: "Los ajustes de sincronización activados guardarán sus valores en el servidor. Sin embargo, hay valores existentes en el servidor. ¿Qué conjunto de valores desea sobrescribir?" +preferenceSyncConflictChoiceMerge: "Fusionar" preferenceSyncConflictChoiceServer: "Valores de configuración del servidor" preferenceSyncConflictChoiceDevice: "Valor configurado en el dispositivo" +preferenceSyncConflictChoiceCancel: "Cancelar la activación de la sincronización" paste: "Pegar" emojiPalette: "Paleta emoji" postForm: "Formulario" +textCount: "caracteres" information: "Información" chat: "Chat" migrateOldSettings: "Migrar la configuración anterior" +migrateOldSettings_description: "Esto debería hacerse automáticamente, pero si por alguna razón la migración no ha tenido éxito, puede activar usted mismo el proceso de migración manualmente. Se sobrescribirá la información de configuración actual." +compress: "Comprimir" right: "Derecha" bottom: "Abajo" top: "Arriba" @@ -1339,6 +1352,31 @@ settingsMigrating: "La configuración está siendo migrada, por favor espera un readonly: "Solo Lectura" goToDeck: "Volver al Deck" federationJobs: "Trabajos de Federación" +driveAboutTip: "En Drive, aparecerá una lista de los archivos que has subido en el pasado.
\nPuedes reutilizar estos archivos al adjuntarlos a notas, o puedes subir archivos por adelantado para publicarlos más tarde.
\nTen cuidado al eliminar un archivo, ya que no estará disponible en todos los lugares donde se utilizó (como notas, páginas, avatares, banners, etc.).
\nTambién puedes crear carpetas para organizar tus archivos." +scrollToClose: "Desliza para cerrar" +advice: "Consejos" +realtimeMode: "Modo en tiempo real" +turnItOn: "Activar" +turnItOff: "Desactivar" +emojiMute: "Silenciar emoji" +emojiUnmute: "No silenciar emoji" +muteX: "Silenciar {x}" +unmuteX: "Dejar de silenciar {x}" +abort: "Abortar" +tip: "Consejos y trucos" +redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\"" +hideAllTips: "Ocultar todos los \"Trucos y consejos\"" +defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto" +defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande.
Alta, reduce la medida del archivo pero también la calidad de la imagen." +inMinutes: "Minutos" +inDays: "Días" +safeModeEnabled: "El modo seguro está activado" +pluginsAreDisabledBecauseSafeMode: "El modo seguro está activado, por lo que todos los plugins están desactivados." +customCssIsDisabledBecauseSafeMode: "El modo seguro está activado, por lo que no se aplica el CSS personalizado." +themeIsDefaultBecauseSafeMode: "Mientras el modo seguro esté activado, se utilizará el tema predeterminado. Cuando se desactive el modo seguro, se volverá al tema original." +_order: + newest: "Los más recientes primero" + oldest: "Los más antiguos primero" _chat: noMessagesYet: "Aún no hay mensajes" newMessage: "Mensajes nuevos" @@ -1354,7 +1392,7 @@ _chat: noInvitations: "No hay invitación." history: "Historial" noHistory: "No hay datos en el historial" - noRooms: "Sala no encontrada" + noRooms: "No te has unido a ninguna sala " inviteUser: "Invitar usuarios" sentInvitations: "Invitaciones enviadas" join: "Unirse" @@ -1372,6 +1410,8 @@ _chat: chatNotAvailableInOtherAccount: "La función de chat está desactivada para el otro usuario." cannotChatWithTheUser: "No se puede iniciar un chat con este usuario" cannotChatWithTheUser_description: "El chat no está disponible o la otra parte no ha habilitado el chat." + youAreNotAMemberOfThisRoomButInvited: "No eres participante en esta sala, pero has recibido una invitación. Por favor, acepta la invitación para unirte." + doYouAcceptInvitation: "¿Aceptas la invitación?" chatWithThisUser: "Chatear" thisUserAllowsChatOnlyFromFollowers: "Este usuario sólo acepta chats de seguidores." thisUserAllowsChatOnlyFromFollowing: "Este usuario sólo acepta chats de los usuarios a los que sigue." @@ -1388,15 +1428,44 @@ _chat: _emojiPalette: palettes: "Paleta\n" enableSyncBetweenDevicesForPalettes: "Activar la sincronización de paletas entre dispositivos" + paletteForMain: "Paleta principal" + paletteForReaction: "Paleta de reacción" _settings: + driveBanner: "Puedes gestionar y configurar la unidad, comprobar su uso y configurar los ajustes de carga de archivos." + pluginBanner: "Puedes ampliar las funciones del cliente con plugins. Puedes instalar plugins, configurarlos y gestionarlos individualmente." + notificationsBanner: "Puede configurar los tipos y el alcance de las notificaciones del servidor y las notificaciones push." api: "API" webhook: "Webhook" + serviceConnection: "Integraciones" + serviceConnectionBanner: "Gestione y configure tokens de acceso y Webhooks para integrarse con aplicaciones o servicios externos." + accountData: "Datos de la cuenta" + accountDataBanner: "Exportación e importación para gestionar los datos de la cuenta." + muteAndBlockBanner: "Puedes configurar y gestionar ajustes para ocultar contenidos y restringir acciones a usuarios específicos." + accessibilityBanner: "Puedes personalizar los visuales y el comportamiento del cliente, y configurar los ajustes para optimizar el uso." + privacyBanner: "Puedes configurar opciones relacionadas con la privacidad de la cuenta, como la visibilidad del contenido, la posibilidad de descubrir la cuenta y la aprobación de seguimiento." + securityBanner: "Puedes configurar opciones relacionadas con la seguridad de la cuenta, como la contraseña, los métodos de inicio de sesión, las aplicaciones de autenticación y Passkeys." + preferencesBanner: "Puedes configurar el comportamiento general del cliente según tus preferencias." + appearanceBanner: "Puedes configurar el aspecto y la visualización del cliente según tus preferencias." + soundsBanner: "Puedes configurar los ajustes de sonido para la reproducción en el cliente." timelineAndNote: "Líneas del tiempo y notas" + makeEveryTextElementsSelectable: "Hacer que todos los elementos de texto sean seleccionables" makeEveryTextElementsSelectable_description: "Activar esta opción puede reducir la usabilidad en algunas situaciones." useStickyIcons: "Hacer que los iconos te sigan cuando desplaces" + enableHighQualityImagePlaceholders: "Mostrar marcadores de posición para imágenes de alta calidad" + uiAnimations: "Animaciones de la interfaz de usuario" showNavbarSubButtons: "Mostrar los sub-botones en la barra de navegación." ifOn: "Si está activado" + ifOff: "Si está desactivado" enableSyncThemesBetweenDevices: "Sincronizar los temas instalados entre dispositivos." + enablePullToRefresh: "Tirar para actualizar" + enablePullToRefresh_description: "Si utiliza un ratón, arrastre mientras pulsa la rueda de desplazamiento." + realtimeMode_description: "Establece una conexión con el servidor y actualiza el contenido en tiempo real. Esto puede aumentar el tráfico y el consumo de memoria." + contentsUpdateFrequency: "Frecuencia de adquisición del contenido." + contentsUpdateFrequency_description: "Cuanto mayor sea el valor, más se actualiza el contenido, pero disminuye el rendimiento y aumenta el tráfico y el consumo de memoria." + contentsUpdateFrequency_description2: "Cuando el modo en tiempo real está activado, el contenido se actualiza en tiempo real independientemente de esta configuración." + showUrlPreview: "Mostrar la vista previa de la URL" + showAvailableReactionsFirstInNote: "Mostrar las reacciones disponibles en la parte superior." + showPageTabBarBottom: "Mostrar la barra de pestañas de la página en la parte inferior." _chat: showSenderName: "Mostrar el nombre del remitente" sendOnEnter: "Intro para enviar" @@ -1404,20 +1473,46 @@ _preferencesProfile: profileName: "Nombre de perfil" profileNameDescription: "Establece un nombre que identifique al dispositivo" profileNameDescription2: "Por ejemplo: \"PC Principal\",\"Teléfono\"" + manageProfiles: "Administrar perfiles" _preferencesBackup: autoBackup: "Respaldo automático" restoreFromBackup: "Restaurar desde copia de seguridad" noBackupsFoundTitle: "No se encontró una copia de seguridad" + noBackupsFoundDescription: "No se han encontrado copias de seguridad creadas automáticamente, pero si has guardado manualmente un archivo de copia de seguridad, puedes importarlo y restaurarlo." + selectBackupToRestore: "Selecciona una copia de seguridad para restaurar" + youNeedToNameYourProfileToEnableAutoBackup: "Se debe establecer un nombre de perfil para activar la copia de seguridad automática." + autoPreferencesBackupIsNotEnabledForThisDevice: "La copia de seguridad automática de los ajustes no está activada en este dispositivo." + backupFound: "Copia de seguridad de los ajustes encontrada " _accountSettings: requireSigninToViewContents: "Se requiere iniciar sesión para ver el contenido" requireSigninToViewContentsDescription1: "Requiere iniciar sesión para ver todas las notas y otros contenidos que hayas creado. Se espera que esto evite que los rastreadores recopilen información." + requireSigninToViewContentsDescription2: "El contenido no se mostrará en vistas previas de URL (OGP), incrustado en páginas web o en servidores que no admitan citas de notas." + requireSigninToViewContentsDescription3: "Estas restricciones pueden no aplicarse a los contenidos federados de otros servidores remotos." + makeNotesFollowersOnlyBefore: "Hacer que las notas antiguas sólo se muestren a los seguidores" + makeNotesFollowersOnlyBeforeDescription: "Mientras esta función esté activada, sólo los seguidores podrán ver las notas que hayan superado la fecha y hora establecidas o que hayan estado visibles durante un tiempo determinado. Cuando se desactive, también se restablecerá el estado de publicación de la nota." + makeNotesHiddenBefore: "Hacer privadas las notas antiguas " + makeNotesHiddenBeforeDescription: "Mientras esta función esté activada, las notas que hayan pasado la fecha y hora fijadas o hayan transcurrido el tiempo establecido sólo serán visibles para ti (se harán privadas). Si la desactivas, también se restablecerá el estado público de las notas." + mayNotEffectForFederatedNotes: "Notas federadas por un servidor remoto pueden no verse afectadas." + mayNotEffectSomeSituations: "Estas restricciones son simplificadas. Pueden no aplicarse en algunas situaciones, como cuando se visualiza en un servidor remoto o durante la moderación." + notesHavePassedSpecifiedPeriod: "Notas publicadas durante el siguiente tiempo específico" + notesOlderThanSpecifiedDateAndTime: "Notas antes de la fecha y hora especificadas" _abuseUserReport: + forward: "Reenviar" + forwardDescription: "Reenvía el informe a un servidor/instancia remoto como cuenta anónima del sistema." + resolve: "Resuelto" accept: "Acepte" reject: "repudio" + resolveTutorial: "Si el contenido del informe es legítimo, selecciona \"Aceptar\" para marcarlo como resuelto.\nSi el contenido del informe es ilegítimo, selecciona \"Rechazar\" para ignorarlo." _delivery: + status: "Estado de la entrega" stop: "Suspendido" + resume: "Resumen de entrega" _type: none: "Publicando" + manuallySuspended: "Suspendido manualmente" + goneSuspended: "El servidor se ha suspendido debido a la eliminación del servidor" + autoSuspendedForNotResponding: "El servidor se suspende debido a que el servidor no responde." + softwareSuspended: "Suspendido porque este software ya no se distribuye a" _bubbleGame: howToPlay: "Cómo jugar" hold: "Mantener" @@ -1442,7 +1537,7 @@ _announcement: tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos." readConfirmTitle: "¿Marcar como leído?" readConfirmText: "Esto marcará el contenido de \"{title}\" como leído." - shouldNotBeUsedToPresentPermanentInfo: "Dado que puede impactar en la experiencia de usuario de forma significativa, es recomendable usar notificaciones en el flujo de información en vez de información persistente." + shouldNotBeUsedToPresentPermanentInfo: "Se recomienda utilizar los avisos para publicar información que requiera inmediatez, en lugar de hacerlo constantemente, ya que esto perjudica especialmente la UX de los nuevos usuarios." dialogAnnouncementUxWarn: "Mostrar dos o más notificaciones en formato diálogo a la vez puede impactar en la experiencia de usuario de forma significativa, úsalos con cuidado." silence: "Silenciar notificaciones" silenceDescription: "Si lo activas, no enviarás notificación sobre este anuncio y el usuario no tendrá que leerlo." @@ -1543,6 +1638,35 @@ _serverSettings: fanoutTimelineDescription: "Incrementa el rendimiento de forma significativa cuando se obtienen las líneas de tiempo y reduce la carga en la base de datos. A cambio, el uso de la memoria en Redis incrementará. Considera desactivar esta opción en caso de que tu servidor tenga poca memoria o detectes inestabilidad." fanoutTimelineDbFallback: "Cargar desde la base de datos" fanoutTimelineDbFallbackDescription: "Cuando esta opción está habilitada, la carga de peticiones adicionales de la línea de tiempo se hará desde la base de datos cuando éstas no se encuentren en la caché. Al deshabilitar esta opción se reduce la carga del servidor, pero limita el número de líneas de tiempo que pueden obtenerse." + reactionsBufferingDescription: "Cuando se activa, el rendimiento durante la creación de reacciones mejorará considerablemente, reduciendo la carga de la base de datos. Sin embargo, aumentará el uso de memoria de Redis." + remoteNotesCleaning: "Limpieza automática de notas (publicaciones) remotas" + remoteNotesCleaning_description: "Al habilitar esta opción, se limpiarán periódicamente las entradas remotas antiguas que no se consultan, lo que evitará que la base de datos se sature." + remoteNotesCleaningMaxProcessingDuration: "Tiempo máximo de funcionamiento continuo del proceso de limpieza" + remoteNotesCleaningExpiryDaysForEachNotes: "Días mínimos para conservar las notas" + inquiryUrl: "URL de consulta " + inquiryUrlDescription: "Especifica una URL para el formulario de consulta al responsable del servidor o una página web para la información de contacto." + openRegistration: "Registros Abiertos" + openRegistrationWarning: "Abrir registros conlleva riesgos. Se recomienda solo habilitarlos si tienes un sistema en el cual puedes monitorear continuamente el servidor y respondes inmediatamente en caso de que haya cualquier problema." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no se ha detectado por un tiempo actividad de un moderador, este ajuste será automáticamente desactivado para prevenir el spam. " + deliverSuspendedSoftware: "Software suspendido." + deliverSuspendedSoftwareDescription: "Puede especificar un rango de nombres y versiones del software del servidor para detener la entrega, por ejemplo, debido a vulnerabilidades. Esta información sobre la versión la proporciona el servidor y su fiabilidad no está garantizada. Se puede utilizar una especificación de rango para especificar una versión, pero se recomienda especificar una versión previa, como >= 2024.3.1-0, ya que especificar >= 2024.3.1 no incluirá versiones personalizadas como 2024.3.1-custom.0." + singleUserMode: "Modo de usuario único" + singleUserMode_description: "Si eres el único usuario de este servidor, activar este modo optimizará su rendimiento." + signToActivityPubGet: "Firmar solicitudes GET de Activitypub." + signToActivityPubGet_description: "Normalmente, debería estar activada. Deshabilitarlo puede mejorar los problemas relacionados con la federación, pero por otro lado podría deshabilitar la federación hacia otros servidores." + proxyRemoteFiles: "Proxy de archivos remotos" + proxyRemoteFiles_description: "Cuando se activa, el servidor proxy sirve archivos remotos. Esto es útil para generar miniaturas de imágenes y proteger la privacidad del usuario." + allowExternalApRedirect: "Permitir redirecciones para consultas vía ActivityPub" + allowExternalApRedirect_description: "Si se activa, otros servidores pueden consultar contenidos de terceros a través de este servidor, pero esto puede dar lugar a la suplantación de contenidos." + userGeneratedContentsVisibilityForVisitor: "Visibilidad de contenido generado por un usuario a invitados" + userGeneratedContentsVisibilityForVisitor_description: "Esto es útil para evitar problemas causados por contenidos remotos inapropiados que no estén bien moderados y que se publiquen involuntariamente en Internet a través de su propio servidor." + userGeneratedContentsVisibilityForVisitor_description2: "Publicar incondicionalmente todo el contenido del servidor en Internet, incluido el contenido remoto recibido por el servidor, es arriesgado. Esto es especialmente importante para los invitados que desconocen la naturaleza distribuida del contenido, ya que pueden creer erróneamente que incluso el contenido remoto es contenido creado por usuarios en el servidor." + restartServerSetupWizardConfirm_title: "¿Reiniciar el asistente de configuración del servidor?" + restartServerSetupWizardConfirm_text: "Algunas configuraciones actuales se restablecerán" + _userGeneratedContentsVisibilityForVisitor: + all: "Todo es público." + localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado" + none: "Todo es privado" _accountMigration: moveFrom: "Trasladar de otra cuenta a ésta" moveFromSub: "Crear un alias para otra cuenta." @@ -1839,6 +1963,8 @@ _role: descriptionOfIsExplorable: "La línea de tiempo de éste rol y la lista de usuarios serán públicos si se activa.." displayOrder: "Posición" descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz." + preserveAssignmentOnMoveAccount: "Preservar los roles asignados durante la migración" + preserveAssignmentOnMoveAccount_description: "Si está activada, este rol se transferirá a la cuenta de destino cuando se migre una cuenta con este rol." canEditMembersByModerator: "Permitir a los moderadores editar los miembros" descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo." priority: "Prioridad" @@ -1858,7 +1984,9 @@ _role: canManageCustomEmojis: "Administrar emojis personalizados" canManageAvatarDecorations: "Administrar decoraciones de avatar" driveCapacity: "Capacidad del drive" + maxFileSize: "Tamaño máximo de archivo que se puede cargar." alwaysMarkNsfw: "Siempre marcar archivos como NSFW" + canUpdateBioMedia: "Puede editar un icono o una imagen de fondo (banner)" pinMax: "Máximo de notas fijadas" antennaMax: "Máximo de antenas" wordMuteMax: "Máximo de caracteres en palabras silenciadas" @@ -1873,6 +2001,17 @@ _role: canSearchNotes: "Uso de la búsqueda de notas" canUseTranslator: "Uso de traductor" avatarDecorationLimit: "Número máximo de decoraciones de avatar" + canImportAntennas: "Permitir la importación de antenas" + canImportBlocking: "Permitir la importación de bloqueos" + canImportFollowing: "Permitir la importación de seguidos" + canImportMuting: "Permitir la importación de silenciados" + canImportUserLists: "Permitir la importación de listas" + chatAvailability: "Permitir Chats" + uploadableFileTypes: "Tipos de archivos que se pueden cargar." + uploadableFileTypes_caption: "Especifica los tipos MIME/archivos permitidos. Se pueden especificar varios tipos MIME separándolos con una nueva línea, y se pueden especificar comodines con un asterisco (*). (por ejemplo, image/*)" + uploadableFileTypes_caption2: "Es posible que no se detecten algunos tipos de archivos. Para permitir estos archivos, añade {x} a la especificación." + noteDraftLimit: "Número de posibles borradores de notas del servidor" + watermarkAvailable: "Disponibilidad de la función de marca de agua" _condition: roleAssignedTo: "Asignado a roles manuales" isLocal: "Usuario local" @@ -1881,6 +2020,7 @@ _role: isBot: "Usuarios Bot" isSuspended: "Usuario suspendido" isLocked: "Cuentas privadas" + isExplorable: "Hacer que la cuenta sea visible en las búsquedas" createdLessThan: "Menos de X han pasado desde la creación de la cuenta" createdMoreThan: "Más de X han pasado desde la creación de la cuenta" followersLessThanOrEq: "Tiene X o menos seguidores" @@ -2031,10 +2171,12 @@ _theme: install: "Instalar tema" manage: "Gestor de temas" code: "Código del tema" + copyThemeCode: "Copiar el código del tema" description: "Descripción" installed: "{name} ha sido instalado" installedThemes: "Temas instalados" builtinThemes: "Temas integrados" + instanceTheme: "Tema del servidor (o también denominado: tema de la instancia)" alreadyInstalled: "Este tema ya está instalado" invalid: "El formato del tema no es válido" make: "Crear tema" @@ -2088,7 +2230,6 @@ _theme: buttonBg: "Fondo de botón" buttonHoverBg: "Fondo de botón (hover)" inputBorder: "Borde de los campos de entrada" - driveFolderBg: "Fondo de capeta del drive" badge: "Medalla" messageBg: "Fondo de chat" fgHighlighted: "Texto resaltado" @@ -2097,6 +2238,7 @@ _sfx: noteMy: "Nota (a mí mismo)" notification: "Notificaciones" reaction: "Al seleccionar una reacción" + chatMessage: "Mensajes del Chat" _soundSettings: driveFile: "Usar un archivo de audio en Drive" driveFileWarn: "Selecciona un archivo de audio en Drive." @@ -2104,6 +2246,7 @@ _soundSettings: driveFileTypeWarnDescription: "Selecciona un archivo de audio" driveFileDurationWarn: "La duración del audio es demasiado larga." driveFileDurationWarnDescription: "Usar un audio de larga duración puede llegar a molestar mientras usas Misskey. ¿Quieres continuar?" + driveFileError: "No puedo cargar el sonido. Por favor cambia la configuración." _ago: future: "Futuro" justNow: "Justo ahora" @@ -2139,7 +2282,7 @@ _2fa: setupCompleted: "Configuración completada" step4: "Ahora cuando inicie sesión, ingrese el mismo token" securityKeyNotSupported: "Tu navegador no soporta claves de autenticación." - registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key.\npor favor. configura una aplicación de autenticación para registrar una llave de seguridad." + registerTOTPBeforeKey: "Por favor. configura una aplicación de autenticación para registrar una llave de seguridad." securityKeyInfo: "Se puede configurar el inicio de sesión usando una clave de seguridad de hardware que soporte FIDO2 o con un certificado de huella digital o con un PIN" registerSecurityKey: "Registrar una llave de seguridad" securityKeyName: "Ingresa un nombre para la clave" @@ -2243,6 +2386,7 @@ _permissions: "read:federation": "Ver instancias federadas" "write:report-abuse": "Crear reportes de usuario" "write:chat": "Administrar chat" + "read:chat": "Explorar Chats" _auth: shareAccessTitle: "Permisos de la aplicación" shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?" @@ -2251,8 +2395,11 @@ _auth: permissionAsk: "Esta aplicación requiere los siguientes permisos" pleaseGoBack: "Por favor, vuelve a la aplicación" callback: "Volviendo a la aplicación" + accepted: "Acceso concedido." denied: "Acceso denegado" + scopeUser: "Operar como el siguiente usuario" pleaseLogin: "Se requiere un inicio de sesión para darle permisos a la aplicación" + byClickingYouWillBeRedirectedToThisUrl: "Cuando el acceso es concedido, serás automáticamente redireccionado a la siguiente URL" _antennaSources: all: "Todas las notas" homeTimeline: "Notas de los usuarios que sigues" @@ -2338,6 +2485,8 @@ _visibility: disableFederation: "No federado" disableFederationDescription: "No enviar a otras instancias" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "Hay archivos que no se han cargado, ¿deseas descartarlos y cerrar el formulario?" + uploaderTip: "El archivo aún no se ha cargado. Desde el menú Archivo, puedes cambiar el nombre, recortar imágenes, poner marcas de agua y comprimir o no el archivo. Los archivos se cargan automáticamente al publicar una nota." replyPlaceholder: "Responder a esta nota" quotePlaceholder: "Citar esta nota" channelPlaceholder: "Publicar en el canal" @@ -2362,6 +2511,9 @@ _profile: changeBanner: "Cambiar banner" verifiedLinkDescription: "Introduciendo una URL que contiene un enlace a tu perfil, se puede mostrar un icono de verificación de propiedad al lado del campo." avatarDecorationMax: "Puedes añadir un máximo de {max} decoraciones de avatar." + followedMessage: "Mensaje cuando te han seguido" + followedMessageDescription: "Puedes establecer un mensaje de bienvenida para nuevos seguidores." + followedMessageDescriptionForLockedAccount: "Si apruebas manualmente seguidores, el mensaje se mostrará al seguidor en el momento de la aprobación." _exportOrImport: allNotes: "Todas las notas" favoritedNotes: "Notas favoritas" @@ -2451,6 +2603,7 @@ _pages: eyeCatchingImageSet: "Elegir imagen llamativa" eyeCatchingImageRemove: "Borrar imagen llamativa" chooseBlock: "Agregar bloque" + enterSectionTitle: "Escribe el título de la sección" selectType: "Elegir tipo" contentBlocks: "Contenido" inputBlocks: "Entrada" @@ -2485,6 +2638,7 @@ _notification: newNote: "Nueva nota" unreadAntennaNote: "Antena {name}" roleAssigned: "Rol asignado" + chatRoomInvitationReceived: "Invitado a la sala de chat." emptyPushNotificationMessage: "Se han actualizado las notificaciones push" achievementEarned: "Logro desbloqueado" testNotification: "Notificación de prueba" @@ -2492,8 +2646,14 @@ _notification: sendTestNotification: "Enviar notificación de prueba" notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto" reactedBySomeUsers: "{n} usuarios han reaccionado" + likedBySomeUsers: "{n} usuarios les gustó tu nota" renotedBySomeUsers: "{n} usuarios han renotado" followedBySomeUsers: "Seguido por {n} usuarios" + flushNotification: "Limpiar notificaciones" + exportOfXCompleted: "La exportación de {x} ha sido completada." + login: "Alguien ha iniciado sesión" + createToken: "Token de acceso creado" + createTokenDescription: "Si no tienes ni idea, elimina el token de acceso a través de \"{text}\"." _types: all: "Todo" note: "Nuevas notas" @@ -2507,8 +2667,11 @@ _notification: receiveFollowRequest: "Recibió una solicitud de seguimiento" followRequestAccepted: "El seguimiento fue aceptado" roleAssigned: "Rol asignado" + chatRoomInvitationReceived: "Invitado a la sala de chat." achievementEarned: "Logro desbloqueado" + exportCompleted: "La exportación se ha completado" login: "Iniciar sesión" + createToken: "Crear tokens de acceso" test: "Pruebas de nofiticaciones" app: "Notificaciones desde aplicaciones" _actions: @@ -2518,7 +2681,11 @@ _notification: _deck: alwaysShowMainColumn: "Siempre mostrar la columna principal" columnAlign: "Alinear columnas" + columnGap: "Margen entre columnas" + deckMenuPosition: "Posición del menú Deck" + navbarPosition: "Posición de la barra de navegación" addColumn: "Agregar columna" + newNoteNotificationSettings: "Configuración de las notificaciones para notas nuevas" configureColumn: "Ajustes de columna" swapLeft: "Mover a la izquierda" swapRight: "Mover a la derecha" @@ -2535,6 +2702,7 @@ _deck: useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple" usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada" flexible: "Autoajustar ancho" + enableSyncBetweenDevicesForProfiles: "Activar la sincronización de la información de perfiles entre dispositivos." _columns: main: "Principal" widgets: "Widgets" @@ -2558,8 +2726,10 @@ _drivecleaner: orderByCreatedAtAsc: "Fecha ascendente" _webhookSettings: createWebhook: "Crear Webhook" + modifyWebhook: "Editar webhook" name: "Nombre" secret: "Secreto" + trigger: "Disparador" active: "Activado" _events: follow: "Cuando se sigue a alguien" @@ -2570,13 +2740,28 @@ _webhookSettings: reaction: "Cuando se recibe una reacción" mention: "Cuando hay una mención" _systemEvents: + abuseReport: "Cuando se recibe un nuevo informe de moderación" + abuseReportResolved: "Cuando se resuelve un informe de moderación" userCreated: "Cuando se crea el usuario." + inactiveModeratorsWarning: "Cuando un moderador ha estado inactivo por un tiempo" + inactiveModeratorsInvitationOnlyChanged: "Cuando un moderador ha estado inactivo durante un tiempo, y el servidor se cambia a sólo por invitación" + deleteConfirm: "¿Estás seguro de querer eliminar el Webhook?" + testRemarks: "Haz clic en el botón de la derecha del switch para mandar una prueba Webhook con datos ficticios" _abuseReport: _notificationRecipient: + createRecipient: "Añadir destinatario a los informes" + modifyRecipient: "Editar un destinatario en el informe de moderación\n" + recipientType: "Tipo de notificación" _recipientType: mail: "Correo" webhook: "Webhook" + _captions: + mail: "Enviar un correo electrónico a todos los moderadores cuando reciban un informe de moderación" + webhook: "Enviar una notificación al SystemWebhook cuando se reciba o se resuelva un informe de moderación" keywords: "Palabras Clave" + notifiedUser: "Usuarios a notificar" + notifiedWebhook: "Webhook a utilizar" + deleteConfirm: "¿Estás seguro de que deseas borrar el destinatario del informe de moderación?" _moderationLogTypes: createRole: "Rol creado" deleteRole: "Rol eliminado" @@ -2601,9 +2786,12 @@ _moderationLogTypes: resetPassword: "Resetear contraseña" suspendRemoteInstance: "Instancia remota suspendida" unsuspendRemoteInstance: "Suspensión de instancia remota retirada" + updateRemoteInstanceNote: "Nota de moderación de una instancia remota actualizada" markSensitiveDriveFile: "Archivo marcado como sensible" unmarkSensitiveDriveFile: "Archivo marcado como no sensible" resolveAbuseReport: "Reporte resuelto" + forwardAbuseReport: "Informe reenviado" + updateAbuseReportNote: "Nota de moderación de un informe actualizada" createInvitation: "Generar invitación" createAd: "Anuncio creado" deleteAd: "Anuncio eliminado" @@ -2613,6 +2801,18 @@ _moderationLogTypes: deleteAvatarDecoration: "Decoración de avatar eliminada" unsetUserAvatar: "Quitar decoración de avatar de este usuario" unsetUserBanner: "Quitar banner de este usuario" + createSystemWebhook: "Crear un SystemWebhook" + updateSystemWebhook: "Actualizar SystemWebhook " + deleteSystemWebhook: "Borrar SystemWebHook" + createAbuseReportNotificationRecipient: "Crear un destinatario para el informe de moderación" + updateAbuseReportNotificationRecipient: "Destinatario de los informes actualizados" + deleteAbuseReportNotificationRecipient: "Destinatario de los informes borrado" + deleteAccount: "Cuenta Borrada" + deletePage: "Página borrada" + deleteFlash: "Juego borrado" + deleteGalleryPost: "Publicación de la galería, eliminada" + deleteChatRoom: "Borrar sala del chat" + updateProxyAccountDescription: "Actualizar la descripción de la cuenta proxy" _fileViewer: title: "Detalles del archivo" type: "Tipo de archivo" @@ -2620,6 +2820,7 @@ _fileViewer: url: "URL" uploadedAt: "Subido el" attachedNotes: "Notas adjuntas" + usage: "Utilizado" thisPageCanBeSeenFromTheAuthor: "Esta página solo puede ser vista por el autor." _externalResourceInstaller: title: "Instalar desde sitio externo" @@ -2667,35 +2868,346 @@ _dataSaver: _avatar: title: "Avatares animados" description: "Desactiva la animación de los avatares. Las imágenes animadas pueden llegar a ser de mayor tamaño que las normales, por lo que al desactivarlas puedes reducir el consumo de datos." - _urlPreview: - title: "Vista previa de URLs" - description: "Desactiva la carga de vistas previas de las URLs." + _urlPreviewThumbnail: + title: "Ocultar las miniaturas de las vistas previas de URL" + description: "Las imágenes en miniatura de la vista previa de URL no se pueden cargar " + _disableUrlPreview: + title: "Desactivar la vista previa de las URL" + description: "Desactiva la función de previsualización de la URL. A diferencia de solo las imágenes en miniatura, esta función reduce la carga de la propia información vinculada." _code: title: "Resaltar código" description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." _hemisphere: N: "Hemisferio norte" S: "Hemisferio sur" + caption: "Usado en algunos clientes para determinar la estación del año" _reversi: reversi: "Reversi" + gameSettings: "Configuración del juego" + chooseBoard: "Elegir tablero" + blackOrWhite: "Negras/Blancas" + blackIs: "{name} juega con negras" rules: "Reglas" + thisGameIsStartedSoon: "El juego comenzará en breve" + waitingForOther: "Esperando el turno del adversario" + waitingForMe: "Esperando tu turno" + waitingBoth: "Prepárate" + ready: "Listo" + cancelReady: "No estoy listo" + opponentTurn: "Turno del oponente" + myTurn: "¡Tu turno!" + turnOf: "Le toca a {name}" + pastTurnOf: "Turno de {name}" + surrender: "Rendirse" + surrendered: "Te has rendido" + timeout: "Se acabó el tiempo" + drawn: "Empate" won: "{name} ha ganado" + black: "Negras" + white: "Blancas" total: "Total" + turnCount: "Turno {count}" + myGames: "Mis rondas" + allGames: "Todos los juegos" + ended: "Finalizado" + playing: "Jugando actualmente" + isLlotheo: "El que tenga menos fichas gana (LLoTheO)" + loopedMap: "Mapa en bucle" + canPutEverywhere: "Las fichas se pueden poner a cualquier lugar\n" + timeLimitForEachTurn: "Tiempo límite por jugada." + freeMatch: "Partida libre." + lookingForPlayer: "Buscando oponente" + gameCanceled: "La partida ha sido cancelada." + shareToTlTheGameWhenStart: "Compartir la partida en la línea de tiempo cuando comience " + iStartedAGame: "¡La partida ha comenzado!" + opponentHasSettingsChanged: "El oponente ha cambiado su configuración" + allowIrregularRules: "Reglas irregulares (completamente libre)" + disallowIrregularRules: "Sin reglas irregulares " + showBoardLabels: "Mostrar el número de línea y la letra de columna en el tablero de juego." + useAvatarAsStone: "Usar los avatares de los usuarios como fichas\n" +_offlineScreen: + title: "Fuera de línea. No se puede conectar con el servidor" + header: "Incapaz de conectar con el servidor" _urlPreviewSetting: + title: "Configuración para la previsualización de la URL" + enable: "Activar la vista previa de URL" + allowRedirect: "Permitir la redirección de la visualización previa" + allowRedirectDescription: "Si una URL tiene una redirección establecida, puede activar esta función para seguir la redirección y mostrar una vista previa del contenido redirigido. Si se desactiva, se ahorrarán recursos del servidor, pero no se mostrará el contenido redirigido." timeout: "Timeout de la carga de vista previa de las URLs (ms)" + timeoutDescription: "Si se tarda más de este valor en obtener la vista previa, ésta no se generará." maximumContentLength: "Content-Length Máximo (bytes)" + maximumContentLengthDescription: "Si Content-Length es superior a este valor, no se generará la vista previa." + requireContentLength: "Genere la vista previa sólo si puede obtener Content-Length" + requireContentLengthDescription: "Si el otro servidor no devuelve Content-Length, no se generará la vista previa." userAgent: "User-Agent" + userAgentDescription: "Establece el User-Agent que se utilizará al recuperar vistas previas. Si se deja en blanco, se utilizará el User-Agent por defecto." + summaryProxy: "Proxy endpoints para generar vistas previas" + summaryProxyDescription: "La vista previa se genera usando Summaly proxy, no la genera el mismo Misskey." + summaryProxyDescription2: "Los siguientes parámetros se vinculan al proxy como cadena de consulta (query string). Si el proxy no los admite, los valores se ignoran." _mediaControls: pip: "Picture in Picture" playbackRate: "Velocidad de reproducción" loop: "Reproducción en bucle" +_contextMenu: + title: "Menú contextual" + app: "Aplicación" + appWithShift: "Aplicación con la tecla shift" + native: "Interfaz nativa (del navegador web)" +_gridComponent: + _error: + requiredValue: "Este valor es obligatorio" + columnTypeNotSupport: "La validación con expresión regular sólo se admite para columnas de tipo:texto." + patternNotMatch: "Este valor no coincide con el patrón en {pattern}" + notUnique: "Este valor debe ser único" +_roleSelectDialog: + notSelected: "No seleccionado" +_customEmojisManager: + _gridCommon: + copySelectionRows: "Copiar filas seleccionadas" + copySelectionRanges: "Copiar selección" + deleteSelectionRows: "Borrar las líneas seleccionadas" + deleteSelectionRanges: "Borrar las filas de la selección" + searchSettings: "Ajustes de búsqueda" + searchSettingCaption: "Establecer criterios de búsqueda detallados." + searchLimit: "Límite de resultados" + sortOrder: "Ordenar" + registrationLogs: "Log de registros " + registrationLogsCaption: "Los registros se mostrarán al actualizar o borrar Emojis. Desaparecerán después de actualizarlos o eliminarlos, pasar a una nueva página o recargar." + alertEmojisRegisterFailedDescription: "No se ha podido actualizar o borrar el emoji. Por favor comprueba el log del registro para más detalles." + _logs: + showSuccessLogSwitch: "Mostrar registro de éxito" + failureLogNothing: "No hay log de fallos" + logNothing: "No hay logs" + _remote: + selectionRowDetail: "Detalle de la línea seleccionada" + importSelectionRows: "Importar las líneas seleccionadas" + importSelectionRangesRows: "Importar las filas seleccionadas" + importEmojisButton: "Importar los Emojis marcados" + confirmImportEmojisTitle: "Importar Emojis" + confirmImportEmojisDescription: "Importar {count} Emoji(s) recibidos del servidor remoto. Por favor, presta mucha atención a la licencia del Emoji. ¿Estás seguro de continuar?" + _local: + tabTitleList: "Lista de emojis registrados" + tabTitleRegister: "Registro de Emojis" + _list: + emojisNothing: "No hay Emojis registrados" + markAsDeleteTargetRows: "Marcar las filas seleccionadas como objetivo a eliminar" + markAsDeleteTargetRanges: "Selección de filas para su eliminación" + alertUpdateEmojisNothingDescription: "No hay Emojis actualizados" + alertDeleteEmojisNothingDescription: "No hay Emojis para borrar" + confirmMovePage: "¿Quieres cambiar de página?" + confirmChangeView: "¿De verdad quieres cambiar la vista?" + confirmUpdateEmojisDescription: "Actualizar {count} Emoji(s). ¿Deseas continuar?" + confirmDeleteEmojisDescription: "Borrar {count} Emoji(s) seleccionados. ¿Deseas continuar?" + confirmResetDescription: "Se restablecerán todos los cambios hechos hasta ahora." + confirmMovePageDesciption: "Se han realizado cambios en los Emojis de esta página.\nSi abandonas la página sin guardar, se descartarán todos los cambios realizados en esta página." + dialogSelectRoleTitle: "Buscar Emojis por rol" + _register: + uploadSettingTitle: "Ajustes de carga" + uploadSettingDescription: "En esta pantalla, puedes configurar el comportamiento al cargar Emojis." + directoryToCategoryLabel: "Introduce el nombre del directorio en el campo \"categoría\"" + directoryToCategoryCaption: "Cuando arrastres y sueltes un directorio, introduce el nombre del directorio en el campo \"categoría\"." + confirmRegisterEmojisDescription: "Registra los Emojis de la lista como nuevos Emojis personalizados. ¿Estás seguro de continuar? (Para evitar sobrecargas, sólo {count} Emoji(s) en una sola operación)" + confirmClearEmojisDescription: "Descartar las ediciones y borrar los Emojis de la lista. ¿Estás seguro de continuar?" + confirmUploadEmojisDescription: "Cargar los {count} archivo(s) arrastrado(s) y soltado(s) en la unidad. ¿Estás seguro de continuar?" +_embedCodeGen: + title: "Personalizar el código de incrustación" + header: "Mostrar encabezados" + autoload: "Cargar más automáticamente (no recomendado)" + maxHeight: "Altura máxima" + maxHeightDescription: "0 desactiva el ajuste del valor máximo. Para evitar que el widget siga creciendo verticalmente, especifica algún valor." + maxHeightWarn: "El límite de altura máxima está desactivado (0). Si esto no estaba previsto, establece la altura máxima en algún valor." + previewIsNotActual: "La visualización difiere de la incrustación real porque excede el rango mostrado en la pantalla de vista previa." + rounded: "Bordes Redondeados" + border: "Añadir un borde al marco exterior" + applyToPreview: "Aplicar a la vista previa" + generateCode: "Crear el código para incrustar" + codeGenerated: "El código ha sido generado" + codeGeneratedDescription: "Pegue el código generado en su sitio web para incrustar el contenido." +_selfXssPrevention: + warning: "Advertencia" + title: "\"Pegar algo en esta pantalla\" es un timo." + description1: "Si pegas algo aquí, un usuario malintencionado podría secuestrar tu cuenta o robar tu información personal." + description2: "Si no entiendes que estás pegando exactamente, %cdetente ahora mismo y cierra esta ventana" + description3: "Para más información visita esto {link}" _followRequest: recieved: "Petición de seguimiento recibida" sent: "Petición de seguimiento enviada" _remoteLookupErrors: + _federationNotAllowed: + title: "Incapaz de comunicarse con este servidor." + description: "Es posible que se haya desactivado la comunicación con este servidor o que haya sido bloqueado.\nPonte en contacto con el administrador del servidor.." + _uriInvalid: + title: "La URI es inválida" + description: "Ha habido un problema con la dirección introducida. Comprueba que no hayas escrito caracteres que no pueden ser usados en la URI" + _requestFailed: + title: "Solicitud fallida." + description: "Ha fallado la comunicación con este servidor. Es posible que el servidor no funcione. Asegúrese también de que no ha introducido un URI no válido o inexistente." + _responseInvalid: + title: "La respuesta no es válida" + description: "Has podido comunicarte con este servidor, pero los datos obtenidos eran incorrectos. Si estás consultando contenidos remotos a través de un servidor de terceros, vuelve a realizar la consulta utilizando un URI que pueda obtenerse del servidor de origen." _noSuchObject: title: "No se encuentra" + description: "No se ha encontrado el recurso solicitado, por favor, vuelve a comprobar el URI." +_captcha: + verify: "Por favor verifica el CAPTCHA" + testSiteKeyMessage: "Puedes comprobar la vista previa introduciendo los valores de prueba para el sitio y las claves secretas.\nPara más detalles, consulta la página siguiente.\n" + _error: + _requestFailed: + title: "Ha fallado la solicitud del CAPTCHA" + text: "Por favor, ejecútalo después de un rato o comprueba los ajustes de nuevo." + _verificationFailed: + title: "Ha fallado la validación del CAPTCHA" + text: "Comprueba que los ajustes son los correctos." + _unknown: + title: "Error en el CAPTCHA." + text: "Se ha producido un error inesperado." +_bootErrors: + title: "Fallo al cargar" + serverError: "Si el problema persiste después de esperar un momento y volver a cargar, póngase en contacto con el administrador del servidor con el siguiente ID de error." + solution: "Lo siguiente puede resolver el problema." + solution1: "Actualiza tu navegador web y el sistema operativo a la última versión" + solution2: "Desactiva el AdBlocker" + solution3: "Borra la memoria caché del navegador web " + solution4: "(Navegador Tor) configura dom.webaudio.enabled a true" + otherOption: "Otras opciones" + otherOption1: "Borra la configuración y la memoria caché del cliente" + otherOption2: "Iniciar el cliente simple" + otherOption3: "Iniciar la herramienta de reparación" + otherOption4: "Iniciar Misskey en modo seguro" _search: searchScopeAll: "Todo" searchScopeLocal: "Local" + searchScopeServer: "Especifica el servidor (Instancia)" searchScopeUser: "Especificar usuario" + pleaseEnterServerHost: "Introduce la dirección del servidor/Instancia" + pleaseSelectUser: "Selecciona un usuario, por favor" + serverHostPlaceholder: "Ejemplo: misskey.example.com" +_serverSetupWizard: + installCompleted: "¡La instalación de Misskey se ha completado!" + firstCreateAccount: "Para comenzar, crea una cuenta de administrador" + accountCreated: "¡La cuenta de administrador se ha creado! " + serverSetting: "Configuración del servidor" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Este asistente te facilita una configuración óptima del servidor." + settingsYouMakeHereCanBeChangedLater: "Los ajustes que han sido cambiados a través de este asistente pueden ser modificados más tarde." + howWillYouUseMisskey: "¿Cómo vas a usar Misskey?" + _use: + single: "Servidor para un único usuario." + single_description: "Utilízalo como tu propio servidor dedicado." + single_youCanCreateMultipleAccounts: "Se pueden crear múltiples cuentas según sea necesario, incluso cuando se opera como servidor unipersonal." + group: "Servidor de grupo" + group_description: "Invita otros usuarios de confianza y úsalo con más de una persona.\n" + open: "Servidor público" + open_description: "Permite a cualquiera registrarse" + openServerAdvice: "Aceptar un número no determinado de usuarios comporta algunos riesgos. Se recomienda operar con un sistema de moderación fiable para hacer frente a los problemas." + openServerAntiSpamAdvice: "Para evitar que su servidor se convierta en un trampolín para el spam, también debe prestar mucha atención a la seguridad habilitando funciones anti-bot como reCAPTCHA." + howManyUsersDoYouExpect: "¿Cuántas personas esperas?" + _scale: + small: "Menos de 100 (escala pequeña)" + medium: "Más de 100 y menos de 1000 (escala media)\n" + large: "Más de 1000(escala grande)" + largeScaleServerAdvice: "Los grandes servidores pueden requerir conocimientos avanzados de infraestructura, como equilibrio de carga y replicación de bases de datos." + doYouConnectToFediverse: "¿Quieres conectarte al Fediverso?" + doYouConnectToFediverse_description1: "Cuando se conecta a una red de servidores distribuidos (Fediverso), el contenido puede intercambiarse con otros servidores." + doYouConnectToFediverse_description2: "Conectarse con el Fediverso también se conoce como \"federación\"." + youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante." + remoteContentsCleaning: "Limpieza automática de los contenidos recibidos" + remoteContentsCleaning_description: "La federación puede dar lugar a un flujo continuo de contenido. Al habilitar la limpieza automática, se eliminará del servidor el contenido obsoleto y sin referencias para ahorrar espacio de almacenamiento." + adminInfo: "Información del administrador" + adminInfo_description: "Establece la información del administrador para recibir consultas." + adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada." + followingSettingsAreRecommended: "Se recomienda los siguientes ajustes" + applyTheseSettings: "Aplicar estos ajustes" + skipSettings: "Omitir configuración" + settingsCompleted: "¡Configuración inicial del servidor completada!" + settingsCompleted_description: "Gracias por tu tiempo. Ahora que está todo listo puedes empezar a utilizar el servidor inmediatamente." + settingsCompleted_description2: "La configuración avanzada del servidor pueden realizarse a través del \"Panel de control\"." + donationRequest: "Por favor Dona" + _donationRequest: + text1: "Misskey es un software libre desarrollado por voluntarios." + text2: "Agradeceríamos su apoyo para que podamos seguir desarrollando este software en el futuro." + text3: "También hay beneficios especiales para los donantes" +_uploader: + editImage: "Editar la imagen" + compressedToX: "Comprimir a {x}" + savedXPercent: "Guardando {x}%" + abortConfirm: "Algunos archivos no se han cargado, ¿deseas cancelar?" + doneConfirm: "Algunos archivos no se han cargado, ¿deseas continuar de todos modos?" + maxFileSizeIsX: "El tamaño máximo de archivo que se puede cargar es de {x}" + allowedTypes: "Tipos de archivos que se pueden cargar." + tip: "El archivo aún no se ha cargado, por lo que este cuadro de diálogo te permite confirmar, renombrar, comprimir y recortar el archivo antes de cargarlo. Cuando esté listo, puedes iniciar la carga pulsando el botón \"Cargar\"." +_clientPerformanceIssueTip: + title: "Si crees que el consumo de batería es demasiado alto" + makeSureDisabledAdBlocker: "Por favor, desactiva el bloqueador de publicidad." + makeSureDisabledAdBlocker_description: "Los bloqueadores de anuncios pueden afectar al rendimiento. Asegúrate de que no están activados en tu sistema o en las funciones/extensiones de tu navegador." + makeSureDisabledCustomCss: "Desactiva el CSS personalizado" + makeSureDisabledCustomCss_description: "Anular estilos puede afectar al rendimiento. Asegúrate de que el CSS personalizado o las extensiones que sobrescriben estilos no están activados." + makeSureDisabledAddons: "Desactiva las extensiones " + makeSureDisabledAddons_description: "Algunas extensiones pueden interferir con el comportamiento del cliente y afectar al rendimiento. Por favor, deshabilita las extensiones de tu navegador y comprueba si esto mejora la situación." +_clip: + tip: "Clip es una función que permite organizar varias notas." +_userLists: + tip: "Las listas pueden contener cualquier usuario que especifiques al crearlas, la lista creada puede mostrarse entonces como una línea de tiempo mostrando solo los usuarios especificados." +watermark: "Marca de Agua" +defaultPreset: "Por defecto" +_watermarkEditor: + tip: "Se puede añadir a la imagen una marca de agua, como información crediticia." + quitWithoutSaveConfirm: "¿Descartar cambios no guardados?" + driveFileTypeWarn: "Este archivo es incompatible" + driveFileTypeWarnDescription: "Elegir una imagen" + title: "Editar la marca de agua" + cover: "Cubrir todo" + repeat: "Repetir" + opacity: "Opacidad" + scale: "Tamaño" + text: "Texto" + position: "Posición" + type: "Tipo" + image: "Imágenes" + advanced: "Avanzado" + stripe: "Rayas" + stripeWidth: "Anchura de línea" + stripeFrequency: "Número de líneas." + angle: "Ángulo" + polkadot: "Lunares" + checker: "verificador" + polkadotMainDotOpacity: "Opacidad del círculo principal" + polkadotMainDotRadius: "Tamaño del círculo principal." + polkadotSubDotOpacity: "Opacidad del círculo secundario" + polkadotSubDotRadius: "Tamaño del círculo secundario." + polkadotSubDotDivisions: "Número de subpuntos." +_imageEffector: + title: "Efecto" + addEffect: "Añadir Efecto" + discardChangesConfirm: "¿Ignorar cambios y salir?" + _fxs: + chromaticAberration: "Aberración Cromática" + glitch: "Glitch" + mirror: "Espejo" + invert: "Invertir colores" + grayscale: "Blanco y negro" + colorAdjust: "Corrección de Color" + colorClamp: "Compresión cromática" + colorClampAdvanced: "Compresión cromática avanzada" + distort: "Distorsión" + threshold: "umbral" + zoomLines: "Saturación de Líneas" + stripe: "Rayas" + polkadot: "Lunares" + checker: "Corrector" + blockNoise: "Bloquear Ruido" + tearing: "Rasgado de Imagen (Tearing)" +drafts: "Borrador" +_drafts: + select: "Seleccionar borradores" + cannotCreateDraftAnymore: "Se ha superado el número de borradores que se pueden crear." + cannotCreateDraft: "No se pueden crear borradores con este contenido." + delete: "Eliminar borrador" + deleteAreYouSure: "¿Quieres borrar el borrador?" + noDrafts: "No hay borradores disponibles." + replyTo: "Responder a {user}" + quoteOf: "Citar las notas de {user}" + postTo: "Destino a {channel}" + saveToDraft: "Guardar como borrador" + restoreFromDraft: "Restaurar desde los borradores" + restore: "Restaurar" + listDrafts: "Listar los borradores" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index f07c08bd3d..d68e7dfde4 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1272,6 +1272,8 @@ pleaseSelectAccount: "Sélectionner un compte" availableRoles: "Rôles disponibles" postForm: "Formulaire de publication" information: "Informations" +inMinutes: "min" +inDays: "j" _chat: invitations: "Inviter" noHistory: "Pas d'historique" @@ -1834,7 +1836,6 @@ _theme: buttonBg: "Arrière-plan du bouton" buttonHoverBg: "Arrière-plan du bouton (survolé)" inputBorder: "Cadre de la zone de texte" - driveFolderBg: "Arrière-plan du dossier de disque" badge: "Badge" messageBg: "Arrière plan de la discussion" fgHighlighted: "Texte mis en évidence" @@ -2333,9 +2334,6 @@ _dataSaver: _avatar: title: "Animation d'avatars" description: "Arrête l'animation d'avatars. Comme les images animées peuvent être plus volumineuses que les images normales, cela permet de réduire davantage le trafic de données." - _urlPreview: - title: "Vignettes d'aperçu des URL" - description: "Les vignettes d'aperçu des URL ne seront plus chargées." _code: title: "Mise en évidence du code" description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." @@ -2364,3 +2362,13 @@ _search: searchScopeAll: "Tous" searchScopeLocal: "Local" searchScopeUser: "Spécifier l'utilisateur·rice" +_watermarkEditor: + driveFileTypeWarn: "Ce fichier n'est pas pris en charge" + opacity: "Transparence" + scale: "Taille" + text: "Texte" + position: "Position" + type: "Type" + image: "Images" + advanced: "Avancé" + angle: "Angle" diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 49807144ec..ab0613cc82 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -73,7 +73,7 @@ export default function generateDTS() { ts.NodeFlags.Const, ), ), - ts.factory.createInterfaceDeclaration( + ts.factory.createTypeAliasDeclaration( [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier('ParameterizedString'), [ @@ -84,20 +84,22 @@ export default function generateDTS() { ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ), ], - undefined, - [ - ts.factory.createPropertySignature( - undefined, - ts.factory.createComputedPropertyName( - ts.factory.createIdentifier('kParameters'), - ), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('T'), + ts.factory.createIntersectionTypeNode([ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ts.factory.createTypeLiteralNode([ + ts.factory.createPropertySignature( undefined, + ts.factory.createComputedPropertyName( + ts.factory.createIdentifier('kParameters'), + ), + undefined, + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('T'), + undefined, + ), ), - ), - ], + ]) + ]), ), ts.factory.createInterfaceDeclaration( [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 3ad9afdc22..009142a4ad 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -5,9 +5,13 @@ introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersif poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka Misskey." monthAndDay: "{day} {month}" search: "Penelusuran" +reset: "Reset" notifications: "Notifikasi" username: "Nama Pengguna" password: "Kata sandi" +initialPasswordForSetup: "Kata sandi untuk memulai konfigurasi awal" +initialPasswordIsIncorrect: "Kata sandi untuk memulai konfigurasi awal salah." +initialPasswordForSetupDescription: "Jika Anda menginstal Misskey sendiri, gunakan kata sandi yang Anda masukkan di berkas konfigurasi.\nJika Anda menggunakan layanan hosting Misskey, gunakan kata sandi yang diberikan.\nJika Anda belum mengatur kata sandi, biarkan kosong dan lanjutkan." forgotPassword: "Lupa Kata Sandi" fetchingAsApObject: "Mengambil data dari Fediverse..." ok: "OK" @@ -45,6 +49,7 @@ pin: "Sematkan ke profil" unpin: "Lepas sematan dari profil" copyContent: "Salin konten" copyLink: "Salin tautan" +copyRemoteLink: "Salin tautan jarak jauh" copyLinkRenote: "Salin tautan renote" delete: "Hapus" deleteAndEdit: "Hapus dan sunting" @@ -212,8 +217,10 @@ perDay: "per Hari" stopActivityDelivery: "Berhenti mengirim aktivitas" blockThisInstance: "Blokir instansi ini" silenceThisInstance: "Senyapkan instansi ini" +mediaSilenceThisInstance: "Server media senyap" operations: "Tindakan" software: "Perangkat lunak" +softwareName: "Nama Perangkat Lunak" version: "Versi" metadata: "Metadata" withNFiles: "{n} berkas" @@ -1040,7 +1047,7 @@ disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi disableFederationOk: "Matikan federasi" invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar." emailNotSupported: "Instansi ini tidak mendukung mengirim surel" -postToTheChannel: "Catat ke kanal" +postToTheChannel: "Buat Catatan ke Kanal" cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi." reactionAcceptance: "Penerimaan reaksi" likeOnly: "Hanya suka" @@ -1256,6 +1263,8 @@ thereAreNChanges: "Ada {n} perubahan" prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna" postForm: "Buat catatan" information: "Informasi" +inMinutes: "menit" +inDays: "hari" _chat: invitations: "Undang" noHistory: "Tidak ada riwayat" @@ -1949,7 +1958,6 @@ _theme: buttonBg: "Latar belakang tombol" buttonHoverBg: "Latar belakang tombol (Mengambang)" inputBorder: "Batas bidang masukan" - driveFolderBg: "Latar belakang folder drive" badge: "Lencana" messageBg: "Latar belakang obrolan" fgHighlighted: "Teks yang disorot" @@ -2401,7 +2409,7 @@ _deck: main: "Utama" widgets: "Widget" notifications: "Notifikasi" - tl: "Lini masa" + tl: "Beranda" antenna: "Antena" list: "Daftar" channel: "Kanal" @@ -2530,9 +2538,6 @@ _dataSaver: _avatar: title: "Gambar avatar" description: "Hentikan animasi gambar avatar. Gambar animasi dapat berukuran lebih besar dari gambar biasa, berpotensi pada pengurangan lalu lintas data lebih jauh." - _urlPreview: - title: "Gambar kecil URL pratinjau" - description: "Gambar kecil URL pratinjau tidak akan dimuat lagi." _code: title: "Penyorotan kode" description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." @@ -2612,3 +2617,13 @@ _search: searchScopeAll: "Semua" searchScopeLocal: "Lokal" searchScopeUser: "Pengguna spesifik" +_watermarkEditor: + driveFileTypeWarn: "Berkas ini tidak didukung" + opacity: "Opasitas" + scale: "Ukuran" + text: "Teks" + position: "Posisi" + type: "Tipe" + image: "Gambar" + advanced: "Tingkat lanjut" + angle: "Sudut" diff --git a/locales/index.d.ts b/locales/index.d.ts index 6bd5c8c3d7..028db4043f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2,9 +2,9 @@ // This file is generated by locales/generateDTS.js // Do not edit this file directly. declare const kParameters: unique symbol; -export interface ParameterizedString { +export type ParameterizedString = string & { [kParameters]: T; -} +}; export interface ILocale { [_: string]: string | ParameterizedString | ILocale; } @@ -315,11 +315,11 @@ export interface Locale extends ILocale { */ "mention": string; /** - * あなた宛て + * メンション */ "mentions": string; /** - * ダイレクト投稿 + * 指名 */ "directNotes": string; /** @@ -707,7 +707,7 @@ export interface Locale extends ILocale { */ "cacheRemoteFiles": string; /** - * この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします。 + * この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。 */ "cacheRemoteFilesDescription": string; /** @@ -1210,6 +1210,10 @@ export interface Locale extends ILocale { * アップロードが完了するまで時間がかかる場合があります。 */ "uploadFromUrlMayTakeTime": string; + /** + * {n}個のファイルをアップロード + */ + "uploadNFiles": ParameterizedString<"n">; /** * みつける */ @@ -1322,6 +1326,10 @@ export interface Locale extends ILocale { * デバイスのダークモードと同期する */ "syncDeviceDarkMode": string; + /** + * 「{x}」がオンになっています。同期をオフにして手動でモードを切り替えますか? + */ + "switchDarkModeManuallyWhenSyncEnabledConfirm": ParameterizedString<"x">; /** * ドライブ */ @@ -2330,6 +2338,10 @@ export interface Locale extends ILocale { * サウンド */ "sound": string; + /** + * 通知音の設定 + */ + "notificationSoundSettings": string; /** * 聴く */ @@ -2555,11 +2567,11 @@ export interface Locale extends ILocale { */ "serviceworkerInfo": string; /** - * 削除された投稿 + * 削除されたノート */ "deletedNote": string; /** - * 非公開の投稿 + * 非公開のノート */ "invisibleNote": string; /** @@ -3186,6 +3198,10 @@ export interface Locale extends ILocale { * 反映には再起動が必要です。 */ "needReloadToApply": string; + /** + * 反映にはサーバーの再起動が必要です。 + */ + "needToRestartServerToApply": string; /** * タイトルバーを表示する */ @@ -4010,6 +4026,10 @@ export interface Locale extends ILocale { * ファイルサイズの制限を超えているためアップロードできません。 */ "cannotUploadBecauseExceedsFileSizeLimit": string; + /** + * 許可されていないファイル種別のためアップロードできません。 + */ + "cannotUploadBecauseUnallowedFileType": string; /** * ベータ */ @@ -4366,6 +4386,10 @@ export interface Locale extends ILocale { * ノート検索は利用できません。 */ "notesSearchNotAvailable": string; + /** + * ユーザー検索は利用できません。 + */ + "usersSearchNotAvailable": string; /** * ライセンス */ @@ -5211,7 +5235,7 @@ export interface Locale extends ILocale { */ "prohibitedWordsForNameOfUser": string; /** - * このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。 + * このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。ユーザー名(username)に対しても全て小文字に置き換えて検査します。 */ "prohibitedWordsForNameOfUserDescription": string; /** @@ -5250,6 +5274,10 @@ export interface Locale extends ILocale { * このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。 */ "federationDisabled": string; + /** + * 下書き + */ + "draft": string; /** * リアクションする際に確認する */ @@ -5315,15 +5343,19 @@ export interface Locale extends ILocale { */ "preferenceSyncConflictTitle": string; /** - * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか? + * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか? */ "preferenceSyncConflictText": string; /** - * サーバーの設定値 + * 統合する + */ + "preferenceSyncConflictChoiceMerge": string; + /** + * サーバーの設定値で上書き */ "preferenceSyncConflictChoiceServer": string; /** - * デバイスの設定値 + * デバイスの設定値で上書き */ "preferenceSyncConflictChoiceDevice": string; /** @@ -5425,6 +5457,80 @@ export interface Locale extends ILocale { * オフにする */ "turnItOff": string; + /** + * 絵文字ミュート + */ + "emojiMute": string; + /** + * 絵文字ミュート解除 + */ + "emojiUnmute": string; + /** + * {x}をミュート + */ + "muteX": ParameterizedString<"x">; + /** + * {x}のミュートを解除 + */ + "unmuteX": ParameterizedString<"x">; + /** + * 中止 + */ + "abort": string; + /** + * ヒントとコツ + */ + "tip": string; + /** + * 全ての「ヒントとコツ」を再表示 + */ + "redisplayAllTips": string; + /** + * 全ての「ヒントとコツ」を非表示 + */ + "hideAllTips": string; + /** + * デフォルトの画像圧縮度 + */ + "defaultImageCompressionLevel": string; + /** + * 低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。 + */ + "defaultImageCompressionLevel_description": string; + /** + * 分 + */ + "inMinutes": string; + /** + * 日 + */ + "inDays": string; + /** + * セーフモードが有効です + */ + "safeModeEnabled": string; + /** + * セーフモードが有効なため、プラグインはすべて無効化されています。 + */ + "pluginsAreDisabledBecauseSafeMode": string; + /** + * セーフモードが有効なため、カスタムCSSは適用されていません。 + */ + "customCssIsDisabledBecauseSafeMode": string; + /** + * セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。 + */ + "themeIsDefaultBecauseSafeMode": string; + "_order": { + /** + * 新しい順 + */ + "newest": string; + /** + * 古い順 + */ + "oldest": string; + }; "_chat": { /** * まだメッセージはありません @@ -5555,6 +5661,14 @@ export interface Locale extends ILocale { * チャットが使えない状態になっているか、相手がチャットを開放していません。 */ "cannotChatWithTheUser_description": string; + /** + * あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。 + */ + "youAreNotAMemberOfThisRoomButInvited": string; + /** + * 招待を承認しますか? + */ + "doYouAcceptInvitation": string; /** * チャットする */ @@ -5705,6 +5819,14 @@ export interface Locale extends ILocale { * アイコンをスクロールに追従させる */ "useStickyIcons": string; + /** + * 高品質な画像のプレースホルダを表示 + */ + "enableHighQualityImagePlaceholders": string; + /** + * UIのアニメーション + */ + "uiAnimations": string; /** * ナビゲーションバーに副ボタンを表示 */ @@ -5745,6 +5867,18 @@ export interface Locale extends ILocale { * リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。 */ "contentsUpdateFrequency_description2": string; + /** + * URLプレビューを表示する + */ + "showUrlPreview": string; + /** + * 利用できるリアクションを先頭に表示 + */ + "showAvailableReactionsFirstInNote": string; + /** + * ページのタブバーを下部に表示 + */ + "showPageTabBarBottom": string; "_chat": { /** * 送信者の名前を表示 @@ -6227,7 +6361,7 @@ export interface Locale extends ILocale { */ "followers": string; /** - * 指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。 + * 指定したユーザーにのみ公開され、また相手に通知が入ります。 */ "direct": string; /** @@ -6235,7 +6369,7 @@ export interface Locale extends ILocale { */ "doNotSendConfidencialOnDirect1": string; /** - * 送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。 + * 送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーが含まれる限定公開のノートを作成する際は、機密情報の扱いに注意が必要です。 */ "doNotSendConfidencialOnDirect2": string; /** @@ -6384,6 +6518,22 @@ export interface Locale extends ILocale { * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。 */ "reactionsBufferingDescription": string; + /** + * リモート投稿の自動クリーニング + */ + "remoteNotesCleaning": string; + /** + * 有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。 + */ + "remoteNotesCleaning_description": string; + /** + * 最大クリーニング処理継続時間 + */ + "remoteNotesCleaningMaxProcessingDuration": string; + /** + * 最低ノート保持日数 + */ + "remoteNotesCleaningExpiryDaysForEachNotes": string; /** * 問い合わせ先URL */ @@ -6420,6 +6570,30 @@ export interface Locale extends ILocale { * このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。 */ "singleUserMode_description": string; + /** + * GETリクエストに署名する + */ + "signToActivityPubGet": string; + /** + * 通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。 + */ + "signToActivityPubGet_description": string; + /** + * リモートファイルをプロキシする + */ + "proxyRemoteFiles": string; + /** + * 有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。 + */ + "proxyRemoteFiles_description": string; + /** + * ActivityPub経由の照会にリダイレクトを許可する + */ + "allowExternalApRedirect": string; + /** + * 有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。 + */ + "allowExternalApRedirect_description": string; /** * 非利用者に対するユーザー作成コンテンツの公開範囲 */ @@ -6432,6 +6606,14 @@ export interface Locale extends ILocale { * サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。 */ "userGeneratedContentsVisibilityForVisitor_description2": string; + /** + * サーバーの初期設定ウィザードをやり直しますか? + */ + "restartServerSetupWizardConfirm_title": string; + /** + * 現在の一部の設定はリセットされます。 + */ + "restartServerSetupWizardConfirm_text": string; "_userGeneratedContentsVisibilityForVisitor": { /** * 全て公開 @@ -7621,6 +7803,10 @@ export interface Locale extends ILocale { * ノート検索の利用 */ "canSearchNotes": string; + /** + * ユーザー検索の利用 + */ + "canSearchUsers": string; /** * 翻訳機能の利用 */ @@ -7653,6 +7839,26 @@ export interface Locale extends ILocale { * チャットを許可 */ "chatAvailability": string; + /** + * アップロード可能なファイル種別 + */ + "uploadableFileTypes": string; + /** + * MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*) + */ + "uploadableFileTypes_caption": string; + /** + * ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。 + */ + "uploadableFileTypes_caption2": ParameterizedString<"x">; + /** + * サーバーサイドのノートの下書きの作成可能数 + */ + "noteDraftLimit": string; + /** + * ウォーターマーク機能の使用可否 + */ + "watermarkAvailable": string; }; "_condition": { /** @@ -8242,6 +8448,10 @@ export interface Locale extends ILocale { * テーマコード */ "code": string; + /** + * テーマコードをコピー + */ + "copyThemeCode": string; /** * 説明 */ @@ -8471,10 +8681,6 @@ export interface Locale extends ILocale { * 入力ボックスの縁取り */ "inputBorder": string; - /** - * ドライブフォルダーの背景 - */ - "driveFolderBg": string; /** * バッジ */ @@ -9447,7 +9653,7 @@ export interface Locale extends ILocale { */ "followersDescription": string; /** - * ダイレクト + * 指名 */ "specified": string; /** @@ -9464,6 +9670,14 @@ export interface Locale extends ILocale { "disableFederationDescription": string; }; "_postForm": { + /** + * アップロードされていないファイルがありますが、破棄してフォームを閉じますか? + */ + "quitInspiteOfThereAreUnuploadedFilesConfirm": string; + /** + * ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。 + */ + "uploaderTip": string; /** * このノートに返信... */ @@ -9607,7 +9821,7 @@ export interface Locale extends ILocale { */ "excludeInactiveUsers": string; /** - * インポートした人による返信をTLに含むようにする + * 返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする */ "withReplies": string; }; @@ -10328,11 +10542,11 @@ export interface Locale extends ILocale { */ "channel": string; /** - * あなた宛て + * メンション */ "mentions": string; /** - * ダイレクト + * 指名 */ "direct": string; /** @@ -10740,6 +10954,10 @@ export interface Locale extends ILocale { * 添付されているノート */ "attachedNotes": string; + /** + * 利用 + */ + "usage": string; /** * このページは、このファイルをアップロードしたユーザーしか閲覧できません。 */ @@ -10894,7 +11112,7 @@ export interface Locale extends ILocale { */ "description": string; }; - "_urlPreview": { + "_urlPreviewThumbnail": { /** * URLプレビューのサムネイルを非表示 */ @@ -10904,6 +11122,16 @@ export interface Locale extends ILocale { */ "description": string; }; + "_disableUrlPreview": { + /** + * URLプレビューを無効化 + */ + "title": string; + /** + * URLプレビュー機能を無効化します。サムネイル画像だけと違い、リンク先の情報の読み込み自体を削減できます。 + */ + "description": string; + }; "_code": { /** * コードハイライトを非表示 @@ -11122,6 +11350,14 @@ export interface Locale extends ILocale { * URLプレビューを有効にする */ "enable": string; + /** + * プレビュー先のリダイレクトを許可 + */ + "allowRedirect": string; + /** + * 入力されたURLがリダイレクトされる場合に、そのリダイレクト先をたどってプレビューを表示するかどうかを設定します。無効にするとサーバーリソースの節約になりますが、リダイレクト先の内容は表示されなくなります。 + */ + "allowRedirectDescription": string; /** * プレビュー取得時のタイムアウト(ms) */ @@ -11389,22 +11625,6 @@ export interface Locale extends ILocale { * ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を"category"に入力します。 */ "directoryToCategoryCaption": string; - /** - * いずれかの方法で登録する絵文字を選択してください。 - */ - "emojiInputAreaCaption": string; - /** - * この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ - */ - "emojiInputAreaList1": string; - /** - * このリンクをクリックしてPCから選択する - */ - "emojiInputAreaList2": string; - /** - * このリンクをクリックしてドライブから選択する - */ - "emojiInputAreaList3": string; /** * リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです) */ @@ -11647,6 +11867,10 @@ export interface Locale extends ILocale { * 修復ツールを起動 */ "otherOption3": string; + /** + * Misskeyをセーフモードで起動 + */ + "otherOption4": string; }; "_search": { /** @@ -11783,6 +12007,14 @@ export interface Locale extends ILocale { * 連合可能なサーバーの指定など、高度な設定も後ほど可能です。 */ "youCanConfigureMoreFederationSettingsLater": string; + /** + * 受信コンテンツの自動クリーニング + */ + "remoteContentsCleaning": string; + /** + * 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。 + */ + "remoteContentsCleaning_description": string; /** * 管理者情報 */ @@ -11838,6 +12070,460 @@ export interface Locale extends ILocale { "text3": string; }; }; + "_uploader": { + /** + * 画像の編集 + */ + "editImage": string; + /** + * {x}に圧縮 + */ + "compressedToX": ParameterizedString<"x">; + /** + * {x}%節約 + */ + "savedXPercent": ParameterizedString<"x">; + /** + * アップロードされていないファイルがありますが、中止しますか? + */ + "abortConfirm": string; + /** + * アップロードされていないファイルがありますが、完了しますか? + */ + "doneConfirm": string; + /** + * アップロード可能な最大ファイルサイズは{x}です。 + */ + "maxFileSizeIsX": ParameterizedString<"x">; + /** + * アップロード可能なファイル種別 + */ + "allowedTypes": string; + /** + * ファイルはまだアップロードされていません。このダイアログで、アップロード前の確認・リネーム・圧縮・クロッピングなどが行えます。準備が出来たら、「アップロード」ボタンを押してアップロードを開始できます。 + */ + "tip": string; + }; + "_clientPerformanceIssueTip": { + /** + * バッテリー消費が多いと感じたら + */ + "title": string; + /** + * アドブロッカーを無効にしてください + */ + "makeSureDisabledAdBlocker": string; + /** + * アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。 + */ + "makeSureDisabledAdBlocker_description": string; + /** + * カスタムCSSを無効にしてください + */ + "makeSureDisabledCustomCss": string; + /** + * スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。 + */ + "makeSureDisabledCustomCss_description": string; + /** + * 拡張機能を無効にしてください + */ + "makeSureDisabledAddons": string; + /** + * 一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。 + */ + "makeSureDisabledAddons_description": string; + }; + "_clip": { + /** + * クリップは、ノートをまとめることができる機能です。 + */ + "tip": string; + }; + "_userLists": { + /** + * 任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。 + */ + "tip": string; + }; + /** + * ウォーターマーク + */ + "watermark": string; + /** + * デフォルトのプリセット + */ + "defaultPreset": string; + "_watermarkEditor": { + /** + * 画像にクレジット情報などのウォーターマークを追加することができます。 + */ + "tip": string; + /** + * 保存せずに終了しますか? + */ + "quitWithoutSaveConfirm": string; + /** + * このファイルは対応していません + */ + "driveFileTypeWarn": string; + /** + * 画像ファイルを選択してください + */ + "driveFileTypeWarnDescription": string; + /** + * ウォーターマークの編集 + */ + "title": string; + /** + * 全体に被せる + */ + "cover": string; + /** + * 敷き詰める + */ + "repeat": string; + /** + * 不透明度 + */ + "opacity": string; + /** + * サイズ + */ + "scale": string; + /** + * テキスト + */ + "text": string; + /** + * 位置 + */ + "position": string; + /** + * タイプ + */ + "type": string; + /** + * 画像 + */ + "image": string; + /** + * 高度 + */ + "advanced": string; + /** + * 角度 + */ + "angle": string; + /** + * ストライプ + */ + "stripe": string; + /** + * ラインの幅 + */ + "stripeWidth": string; + /** + * ラインの数 + */ + "stripeFrequency": string; + /** + * ポルカドット + */ + "polkadot": string; + /** + * チェッカー + */ + "checker": string; + /** + * メインドットの不透明度 + */ + "polkadotMainDotOpacity": string; + /** + * メインドットの大きさ + */ + "polkadotMainDotRadius": string; + /** + * サブドットの不透明度 + */ + "polkadotSubDotOpacity": string; + /** + * サブドットの大きさ + */ + "polkadotSubDotRadius": string; + /** + * サブドットの数 + */ + "polkadotSubDotDivisions": string; + }; + "_imageEffector": { + /** + * エフェクト + */ + "title": string; + /** + * エフェクトを追加 + */ + "addEffect": string; + /** + * 変更を破棄して終了しますか? + */ + "discardChangesConfirm": string; + /** + * 設定項目はありません + */ + "nothingToConfigure": string; + "_fxs": { + /** + * 色収差 + */ + "chromaticAberration": string; + /** + * グリッチ + */ + "glitch": string; + /** + * ミラー + */ + "mirror": string; + /** + * 色の反転 + */ + "invert": string; + /** + * 白黒 + */ + "grayscale": string; + /** + * 色調補正 + */ + "colorAdjust": string; + /** + * 色の圧縮 + */ + "colorClamp": string; + /** + * 色の圧縮(高度) + */ + "colorClampAdvanced": string; + /** + * 歪み + */ + "distort": string; + /** + * 二値化 + */ + "threshold": string; + /** + * 集中線 + */ + "zoomLines": string; + /** + * ストライプ + */ + "stripe": string; + /** + * ポルカドット + */ + "polkadot": string; + /** + * チェッカー + */ + "checker": string; + /** + * ブロックノイズ + */ + "blockNoise": string; + /** + * ティアリング + */ + "tearing": string; + }; + "_fxProps": { + /** + * 角度 + */ + "angle": string; + /** + * サイズ + */ + "scale": string; + /** + * サイズ + */ + "size": string; + /** + * 色 + */ + "color": string; + /** + * 不透明度 + */ + "opacity": string; + /** + * 正規化 + */ + "normalize": string; + /** + * 量 + */ + "amount": string; + /** + * 明るさ + */ + "lightness": string; + /** + * コントラスト + */ + "contrast": string; + /** + * 色相 + */ + "hue": string; + /** + * 輝度 + */ + "brightness": string; + /** + * 彩度 + */ + "saturation": string; + /** + * 最大値 + */ + "max": string; + /** + * 最小値 + */ + "min": string; + /** + * 方向 + */ + "direction": string; + /** + * 位相 + */ + "phase": string; + /** + * 頻度 + */ + "frequency": string; + /** + * 強さ + */ + "strength": string; + /** + * ズレ + */ + "glitchChannelShift": string; + /** + * シード値 + */ + "seed": string; + /** + * 赤色成分 + */ + "redComponent": string; + /** + * 緑色成分 + */ + "greenComponent": string; + /** + * 青色成分 + */ + "blueComponent": string; + /** + * しきい値 + */ + "threshold": string; + /** + * 中心X + */ + "centerX": string; + /** + * 中心Y + */ + "centerY": string; + /** + * スムージング + */ + "zoomLinesSmoothing": string; + /** + * スムージングと集中線の幅の設定は併用できません。 + */ + "zoomLinesSmoothingDescription": string; + /** + * 集中線の幅 + */ + "zoomLinesThreshold": string; + /** + * 中心径 + */ + "zoomLinesMaskSize": string; + /** + * 黒色にする + */ + "zoomLinesBlack": string; + }; + }; + /** + * 下書き + */ + "drafts": string; + "_drafts": { + /** + * 下書きを選択 + */ + "select": string; + /** + * 下書きの作成可能数を超えています。 + */ + "cannotCreateDraftAnymore": string; + /** + * この内容では下書きを作成できません。 + */ + "cannotCreateDraft": string; + /** + * 下書きを削除 + */ + "delete": string; + /** + * 下書きを削除しますか? + */ + "deleteAreYouSure": string; + /** + * 下書きはありません + */ + "noDrafts": string; + /** + * {user}への返信 + */ + "replyTo": ParameterizedString<"user">; + /** + * {user}のノートへの引用 + */ + "quoteOf": ParameterizedString<"user">; + /** + * {channel}への投稿 + */ + "postTo": ParameterizedString<"channel">; + /** + * 下書きへ保存 + */ + "saveToDraft": string; + /** + * 下書きから復元 + */ + "restoreFromDraft": string; + /** + * 復元 + */ + "restore": string; + /** + * 下書き一覧 + */ + "listDrafts": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/index.js b/locales/index.js index 091d216dee..6d9cf4796b 100644 --- a/locales/index.js +++ b/locales/index.js @@ -36,6 +36,7 @@ const languages = [ 'ru-RU', 'sk-SK', 'th-TH', + 'tr-TR', 'ug-CN', 'uk-UA', 'vi-VN', diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 259fe6edd3..fb32deec50 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -220,6 +220,7 @@ silenceThisInstance: "Silenziare l'istanza" mediaSilenceThisInstance: "Silenzia i media dell'istanza" operations: "Operazioni" software: "Software" +softwareName: "Nome del software" version: "Versione" metadata: "Metadato" withNFiles: "{n} file in allegato" @@ -297,6 +298,7 @@ uploadFromUrl: "Incolla URL immagine" uploadFromUrlDescription: "URL del file che vuoi caricare" uploadFromUrlRequested: "Caricamento richiesto" uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo." +uploadNFiles: "Caricare {n} file singolarmente" explore: "Esplora" messageRead: "Visualizzato" noMoreHistory: "Non c'è più cronologia da visualizzare" @@ -325,6 +327,7 @@ dark: "Scuro" lightThemes: "Tema Chiaro" darkThemes: "Tema Scuro" syncDeviceDarkMode: "Sincronizza il tema scuro con le impostazioni del dispositivo" +switchDarkModeManuallyWhenSyncEnabledConfirm: "({x}) è attiva. Vuoi disattivare la sincronizzazione e passare alla modalità manuale?" drive: "Drive" fileName: "Nome dell'allegato" selectFile: "Scelta allegato" @@ -574,10 +577,12 @@ showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timel showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita" newNoteRecived: "Nuove Note da leggere" +newNote: "Nuova Nota" sounds: "Impostazioni suoni" sound: "Suono" +notificationSoundSettings: "Preferenze di notifica" listen: "Ascolta" -none: "Nessuno" +none: "Nessuna" showInPage: "Visualizza in pagina" popout: "Finestra pop-out" volume: "Volume" @@ -790,6 +795,7 @@ wide: "Largo" narrow: "Stretto" reloadToApplySetting: "Le tue preferenze verranno impostate dopo il ricaricamento della pagina. Vuoi ricaricare adesso?" needReloadToApply: "È necessario riavviare per rendere effettive le modifiche." +needToRestartServerToApply: "Per attivare le modifiche, occorre riavviare il server." showTitlebar: "Visualizza la barra del titolo" clearCache: "Svuota la cache" onlineUsersCount: "{n} persone attive adesso" @@ -996,6 +1002,7 @@ failedToUpload: "errore di caricamento" cannotUploadBecauseInappropriate: "Non è possibile caricarlo perché è stato stabilito che potrebbe contenere contenuti inappropriati." cannotUploadBecauseNoFreeSpace: "Impossibile caricare a causa della mancanza di spazio libero sul drive." cannotUploadBecauseExceedsFileSizeLimit: "Il file non può essere caricato perché eccede le dimensioni consentite." +cannotUploadBecauseUnallowedFileType: "Impossibile caricare a causa di un tipo file non autorizzato." beta: "Versione beta" enableAutoSensitive: "Determinazione automatica del NSFW" enableAutoSensitiveDescription: "Se disponibile, il flag NSFW viene impostato automaticamente sui media utilizzando l'apprendimento automatico. Anche se questa funzione è disattivata, in alcuni casi può essere impostata automaticamente." @@ -1191,7 +1198,7 @@ replies: "Risposte" renotes: "Rinota" loadReplies: "Leggi le risposte" loadConversation: "Leggi la conversazione" -pinnedList: "Elenco in primo piano" +pinnedList: "Lista in primo piano" keepScreenOn: "Mantenere lo schermo acceso" verifiedLink: "Abbiamo confermato la validità di questo collegamento" notifyNotes: "Notifica nuove Note" @@ -1306,6 +1313,7 @@ availableRoles: "Ruoli disponibili" acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento." federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione." federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server." +draft: "Bozza" confirmOnReact: "Confermare le reazioni" reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?" @@ -1323,6 +1331,7 @@ restore: "Ripristina" syncBetweenDevices: "Sincronizzazione tra i dispositivi" preferenceSyncConflictTitle: "Sul server esiste già il valore impostato" preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?" +preferenceSyncConflictChoiceMerge: "Integra" preferenceSyncConflictChoiceServer: "Valore del server" preferenceSyncConflictChoiceDevice: "Valore del dispositivo" preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione" @@ -1334,7 +1343,7 @@ information: "Informazioni" chat: "Chat" migrateOldSettings: "Migrare le vecchie impostazioni" migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali." -compress: "Comprimi" +compress: "Compressione" right: "Destra" bottom: "Sotto" top: "Sopra" @@ -1342,6 +1351,32 @@ embed: "Incorporare" settingsMigrating: "Migrazione delle impostazioni. Attendere prego ... (Puoi anche migrare manualmente in un secondo momento, nel menu: Impostazioni → Altro → Migrazione delle impostazioni)" readonly: "Sola lettura" goToDeck: "Torna al Deck" +federationJobs: "Coda di federazione" +driveAboutTip: "Il Drive mostra l'elenco di file caricati in passato. Puoi organizzarli in cartelle, riusarli allegandoli ad altre note, o caricarli in anticipo e poi pubblicarli in un secondo momento. Tieni presente che se elimini un file, non sarà più visibile in nessuno degli oggetti a cui è allegato (Note, pagine, avatar, banner, ecc.)" +scrollToClose: "Scorri per chiudere" +advice: "Consiglio" +realtimeMode: "Modalità in tempo reale" +turnItOn: "Attivare" +turnItOff: "Disattivare" +emojiMute: "Silenzia emoji" +emojiUnmute: "De silenzia emoji" +muteX: "Silenzia {x}" +unmuteX: "De silenzia {x}" +abort: "Annulla" +tip: "Suggerimento" +redisplayAllTips: "Mostra tutti i suggerimenti" +hideAllTips: "Nascondi tutti i suggerimenti" +defaultImageCompressionLevel: "Livello predefinito di compressione immagini" +defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine." +inMinutes: "min" +inDays: "giorni" +safeModeEnabled: "La modalità sicura è attiva" +pluginsAreDisabledBecauseSafeMode: "Tutti i plugin sono disattivati, poiché la modalità sicura è attiva." +customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicato, poiché la modalità sicura è attiva." +themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente." +_order: + newest: "Prima i più recenti" + oldest: "Meno recenti prima" _chat: noMessagesYet: "Ancora nessun messaggio" newMessage: "Nuovo messaggio" @@ -1375,6 +1410,8 @@ _chat: chatNotAvailableInOtherAccount: "La chat non è disponibile nel profilo dell'altra persona." cannotChatWithTheUser: "Impossibile chattare con questa persona" cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo." + youAreNotAMemberOfThisRoomButInvited: "Non partecipi a questa stanza di chat, ma hai ricevuto un invito. Per partecipare, accetta l'invito." + doYouAcceptInvitation: "Intendi accettare l'invito?" chatWithThisUser: "Chatta con questa persona" thisUserAllowsChatOnlyFromFollowers: "Questa persona permette di chattare soltanto i propri Follower." thisUserAllowsChatOnlyFromFollowing: "Questa persona permette di chattare soltanto ai suoi Follow." @@ -1414,10 +1451,21 @@ _settings: makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile" makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni." useStickyIcons: "Fissa le icone durante lo scorrimento" + enableHighQualityImagePlaceholders: "Mostra un segnaposto per immagini in alta qualità" + uiAnimations: "Animazione dell'interfaccia" showNavbarSubButtons: "Mostra i pulsanti secondari nella barra di navigazione" ifOn: "Quando attivato" ifOff: "Quando disattivato" enableSyncThemesBetweenDevices: "Sincronizzare il tema tra i dispositivi" + enablePullToRefresh: "Scorri e aggiorna" + enablePullToRefresh_description: "Clicca col mouse e gira la rotella." + realtimeMode_description: "Connette al server e aggiorna il contenuto in tempo reale. Potrebbe aumentare l'uso dei dati e il consumo della batteria." + contentsUpdateFrequency: "Frequenza di ricezione contenuti" + contentsUpdateFrequency_description: "Se l'impostazione è alta, verranno aggiornati più frequentemente, consumando più dati e più batteria." + contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere." + showUrlPreview: "Mostra anteprima dell'URL" + showAvailableReactionsFirstInNote: "Mostra le reazioni disponibili in alto" + showPageTabBarBottom: "Visualizza le schede della pagina nella parte inferiore" _chat: showSenderName: "Mostra il nome del mittente" sendOnEnter: "Invio spedisce" @@ -1425,6 +1473,7 @@ _preferencesProfile: profileName: "Nome del profilo" profileNameDescription: "Impostare il nome che indentifica questo dispositivo." profileNameDescription2: "Es: \"PC principale\" o \"Cellulare\"" + manageProfiles: "Gestione profili" _preferencesBackup: autoBackup: "Backup automatico" restoreFromBackup: "Ripristinare da backup" @@ -1463,6 +1512,7 @@ _delivery: manuallySuspended: "Sospesa manualmente" goneSuspended: "Sospensione server a causa dell'eliminazione" autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta" + softwareSuspended: "Attualmente non disponibile perché il software non è più distribuito" _bubbleGame: howToPlay: "Come giocare" hold: "Tieni" @@ -1589,11 +1639,34 @@ _serverSettings: fanoutTimelineDbFallback: "Elaborazione dati alternativa" fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline." reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis." + remoteNotesCleaning: "Pulizia automatica dei contenuti remoti" + remoteNotesCleaning_description: "Se abilitata, verranno periodicamente rimosse le vecchie Note remote senza relazioni, per ridurre il sovraccarico del sistema." + remoteNotesCleaningMaxProcessingDuration: "Durata massima del processo di pulizia" + remoteNotesCleaningExpiryDaysForEachNotes: "Periodo minimo di conservazione delle note" inquiryUrl: "URL di contatto" inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." openRegistration: "Registrazioni aperte" openRegistrationWarning: "L’apertura della registrazione comporta dei rischi. Ti consigliamo di attivarla solo se hai predisposto il monitoraggio continuo del tuo server e puoi rispondere immediatamente se si verifica un problema." thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo." + deliverSuspendedSoftware: "Software fuori produzione" + deliverSuspendedSoftwareDescription: "A causa di vulnerabilità o altri motivi, puoi interrompere la distribuzione di un software da un server specificandone il nome e la versione. Le informazioni sono fornite dall'altro server e l'autenticità non è garantita. Puoi indicare un intervallo di versione semantica, ma specificando >= 2024.3.1 non verranno incluse le versioni personalizzate come ad esempio 2024.3.1-custom.0, pertanto ti consigliamo di specificare una versione come >= 2024.3.1-0." + singleUserMode: "Modalità utente singolo" + singleUserMode_description: "Se sei l'unica persona a utilizzare questo server, l'abilitazione di questa modalità ottimizzerà le prestazioni." + signToActivityPubGet: "Firma delle richieste GET" + signToActivityPubGet_description: "Normalmente questa opzione dovrebbe essere abilitata. Se si verificano problemi con la comunicazione federata, disabilitarla potrebbe migliorare la situazione, ma d'altro canto potrebbe rendere impossibile la comunicazione, a seconda del server." + proxyRemoteFiles: "Proxy di file remoti" + proxyRemoteFiles_description: "Se abilitato, i file remoti verranno serviti tramite proxy. Utile per generare miniature delle immagini e proteggere la privacy degli utenti." + allowExternalApRedirect: "Consenti reindirizzamenti per le query tramite ActivityPub" + allowExternalApRedirect_description: "Se abilitata, consente ad altri server di interrogare contenuti di terze parti tramite il tuo server, con conseguente potenziale falsificazione dei contenuti." + userGeneratedContentsVisibilityForVisitor: "Visibilità dei contenuti generati dagli utenti ai non utenti" + userGeneratedContentsVisibilityForVisitor_description: "Questa funzionalità è utile per impedire che contenuti remoti inappropriati e difficili da moderare vengano inavvertitamente resi pubblici su Internet tramite il proprio server." + userGeneratedContentsVisibilityForVisitor_description2: "Esistono dei rischi nell'esporre incondizionatamente su internet tutto il contenuto del tuo server, incluso il contenuto remoto ricevuto da altri server. In particolare, occorre prestare attenzione, perché le persone non consapevoli della federazione potrebbero erroneamente credere che il contenuto remoto sia stato invece creato all'interno del proprio server." + restartServerSetupWizardConfirm_title: "Vuoi ripetere la procedura guidata di configurazione iniziale del server?" + restartServerSetupWizardConfirm_text: "Verranno ripristinate alcune tue impostazioni personalizzate." + _userGeneratedContentsVisibilityForVisitor: + all: "Tutto pubblico" + localOnly: "Pubblica solo contenuti locali, mantieni privati ​​i contenuti remoti" + none: "Tutto privato" _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" @@ -1911,6 +1984,7 @@ _role: canManageCustomEmojis: "Gestire le emoji personalizzate" canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo" driveCapacity: "Capienza del Drive" + maxFileSize: "Dimensione massima del file caricabile" alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" canUpdateBioMedia: "Può aggiornare foto profilo e di testata" pinMax: "Quantità massima di Note in primo piano" @@ -1933,6 +2007,11 @@ _role: canImportMuting: "Può importare Silenziati" canImportUserLists: "Può importare liste di Profili" chatAvailability: "Chat consentita" + uploadableFileTypes: "Tipi di file caricabili" + uploadableFileTypes_caption: "Specifica il tipo MIME. Puoi specificare più valori separandoli andando a capo, oppure indicare caratteri jolly con un asterisco (*). Ad esempio: image/*" + uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica." + noteDraftLimit: "Numero massimo di Note in bozza, lato server" + watermarkAvailable: "Disponibilità della funzione filigrana" _condition: roleAssignedTo: "Assegnato a ruoli manualmente" isLocal: "Profilo locale" @@ -2092,6 +2171,7 @@ _theme: install: "Installa un tema" manage: "Gestione dei temi" code: "Codice tema" + copyThemeCode: "Copia il codice del Tema" description: "Descrizione" installed: "{name} è installato" installedThemes: "Temi installati" @@ -2150,7 +2230,6 @@ _theme: buttonBg: "Sfondo del pulsante" buttonHoverBg: "Sfondo del pulsante (sorvolato)" inputBorder: "Inquadra casella di testo" - driveFolderBg: "Sfondo della cartella di disco" badge: "Distintivo" messageBg: "Sfondo della chat" fgHighlighted: "Testo in evidenza." @@ -2406,6 +2485,8 @@ _visibility: disableFederation: "Senza federazione" disableFederationDescription: "Non spedire attività alle altre istanze remote" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?" + uploaderTip: "Il file non è ancora stato caricato. Nel menu file (tre puntini), puoi ritagliare l'immagine, mettere la filigrana, decidere la presenza o l'assenza di compressione... Il file verrà caricato automaticamente quando pubblichi la Nota." replyPlaceholder: "Rispondi a questa nota..." quotePlaceholder: "Cita questa nota..." channelPlaceholder: "Pubblica sul canale..." @@ -2739,6 +2820,7 @@ _fileViewer: url: "URL" uploadedAt: "Caricato il" attachedNotes: "Note a cui è allegato" + usage: "In uso" thisPageCanBeSeenFromTheAuthor: "Questa pagina può essere vista solo da chi ha caricato il file." _externalResourceInstaller: title: "Installa da sito esterno" @@ -2786,9 +2868,12 @@ _dataSaver: _avatar: title: "Immagine del profilo" description: "Impedire l'animazione per l'immagine del profilo. Le immagini animate possono avere dimensioni file maggiori rispetto a quelle normali, puoi ridurre ulteriormente l'utilizzo dei dati." - _urlPreview: - title: "Anteprime delle URL" - description: "Impedire il caricamento delle anteprime URL." + _urlPreviewThumbnail: + title: "Nascondi le miniature nell'anteprima URL" + description: "Le immagini in miniatura nell'anteprima URL non verranno più caricate." + _disableUrlPreview: + title: "Disabilita l'anteprima URL" + description: "Disabilita la funzione di anteprima URL. A differenza di una semplice immagine in miniatura, questo riduce il tempo necessario per caricare le informazioni collegate." _code: title: "Codice evidenziato" description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato." @@ -2846,6 +2931,8 @@ _offlineScreen: _urlPreviewSetting: title: "Impostazioni per l'anteprima delle URL" enable: "Attiva l'anteprima delle URL" + allowRedirect: "Segui i reindirizzamenti per visualizzare le anteprime" + allowRedirectDescription: "Se la URL inserita contiene un reindirizzamento, decidi di seguire il reindirizzamento fino alla destinazione, visualizzandone l'anteprima. Disabilitando questa opzione si risparmiano risorse del server, ma il contenuto effettivo dal reindirizzamento, non verrà visualizzato." timeout: "Timeout dell'anteprima in millisecondi" timeoutDescription: "Impegna al massimo il tempo indicato, altrimenti ignora l'anteprima" maximumContentLength: "Grandezza del contenuto (Content-Length in byte)" @@ -2919,10 +3006,6 @@ _customEmojisManager: uploadSettingDescription: "Questa schermata ti permette di scegliere il comportamento durante il caricamento delle emoji." directoryToCategoryLabel: "Inseriscile in una cartella omonima alla categoria" directoryToCategoryCaption: "Crea il campo categoria in base alla cartella." - emojiInputAreaCaption: "Seleziona l'emoji da registrare utilizzando uno dei metodi." - emojiInputAreaList1: "Trascinare una immagine o una cartella in quest'area" - emojiInputAreaList2: "Clicca per scegliere file dal tuo dispositivo" - emojiInputAreaList3: "Clicca per selezionare dal Drive" confirmRegisterEmojisDescription: "Registrazione delle emoji elencate come nuove emoji personalizzate. Vuoi davvero procedere? (Per evitare sovraccarichi, puoi registrare al massimo {count} emoji per volta)" confirmClearEmojisDescription: "Annullare le modifiche e cancella le emoji nell'elenco. Confermi?" confirmUploadEmojisDescription: "Caricamento sul Drive di {count} file locali. Vuoi davvero procedere?" @@ -2990,6 +3073,7 @@ _bootErrors: otherOption1: "Nelle impostazioni, cancellare le impostazioni del client e svuotare la cache" otherOption2: "Avviare il client predefinito" otherOption3: "Avviare lo strumento di riparazione" + otherOption4: "Avvia Misskey in modalità sicura" _search: searchScopeAll: "Tutte" searchScopeLocal: "Locale" @@ -2998,3 +3082,132 @@ _search: pleaseEnterServerHost: "Inserire il nome host" pleaseSelectUser: "Per favore, seleziona un profilo" serverHostPlaceholder: "Es: misskey.example.com" +_serverSetupWizard: + installCompleted: "L'installazione di Misskey è completata!" + firstCreateAccount: "Per prima cosa, crea un account amministratore." + accountCreated: "Il tuo account amministratore è stato creato!" + serverSetting: "Configurazione del server" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Questa procedura guidata ti aiuterà a configurare facilmente il tuo server in modo ottimale." + settingsYouMakeHereCanBeChangedLater: "Potrai anche modificare le impostazioni in seguito." + howWillYouUseMisskey: "Come si usa Misskey?" + _use: + single: "Modalità utenza singola" + single_description: "Se intendi usarlo come tuo server personale" + single_youCanCreateMultipleAccounts: "Anche se lo utilizzi come server per una sola persona, puoi creare più account in base alle tue esigenze." + group: "Modalità multi utentza" + group_description: "Invita altre persone fidate ad usare il server insieme a te" + open: "Server aperto" + open_description: "Per ospitare un numero imprecisato di persone" + openServerAdvice: "Ospitare un numero imprecisato di persone comporta dei rischi. Ti consigliamo di adottare un solido sistema di moderazione, in modo da poter gestire eventuali problemi che potrebbero presentarsi pubblicando contenuti proposti da altre persone, che potrebbero essere sconosciute." + openServerAntiSpamAdvice: "Presta molta attenzione alla sicurezza, ad esempio attivando funzionalità anti-bot (iscrizioni automatiche) come reCAPTCHA. Questo può evitare che il tuo server diventi un trampolino di lancio per lo spam di altri." + howManyUsersDoYouExpect: "Quante persone pensi che parteciperanno?" + _scale: + small: "100 persone o meno (piccolo)" + medium: "Da 100 a 1000 persone (medio)" + large: "Oltre 1000 persone (grande)" + largeScaleServerAdvice: "Configurare grandi server potrebbe richiedere conoscenze infrastrutturali avanzate, ad esempio, il bilanciamento del carico e la replicazione del database." + doYouConnectToFediverse: "Vuoi connetterti al Fediverso?" + doYouConnectToFediverse_description1: "Collegandosi a una rete di server distribuiti, denominata Fediverso, potrai scambiare contenuti con altri server, tramite il protocollo di comunicazione ActivityPub." + doYouConnectToFediverse_description2: "Connettersi al Fediverso è anche detto \"federazione\"." + youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi." + remoteContentsCleaning: "Pulizia automatica dei contenuti in arrivo" + remoteContentsCleaning_description: "Con la federazione funzionante, riceverai sempre più contenuti. Abilitando la pulizia automatica, i contenuti non referenziati e obsoleti verranno rimossi automaticamente dai tuoi server, risparmiando spazio di archiviazione." + adminInfo: "Informazioni sull'amministratore" + adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste." + adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione." + followingSettingsAreRecommended: "Si consigliano le seguenti impostazioni:" + applyTheseSettings: "Applica questa impostazione" + skipSettings: "Salta l'installazione" + settingsCompleted: "Installazione completata!" + settingsCompleted_description: "Grazie per il tuo impegno. Adesso che hai completato la configurazione, puoi iniziare a utilizzare il tuo server." + settingsCompleted_description2: "Le impostazioni dettagliate del server possono essere effettuate tramite il Pannello di controllo." + donationRequest: "Per favore Fai una donazione" + _donationRequest: + text1: "Misskey è un software libero sviluppato da volontari." + text2: "Se puoi, ti preghiamo di prendere in considerazione l'idea di fare una donazione, così potremo continuare a sviluppare." + text3: "Sono previsti anche dei vantaggi speciali per i sostenitori!" +_uploader: + editImage: "Modifica immagine" + compressedToX: "Compresso in {x}" + savedXPercent: "{x}% risparmiati" + abortConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?" + doneConfirm: "Alcuni file non sono stati caricati. Vuoi completarli?" + maxFileSizeIsX: "La dimensione massima del file che puoi caricare è {x}." + allowedTypes: "Tipi di file caricabili" + tip: "Il file non è ancora stato caricato. Puoi controllare, rinominare, comprimere, ritagliare, prima del caricamento. Quando hai finito, premi il bottone \"Carica\" ​​per completare." +_clientPerformanceIssueTip: + title: "Se ritieni che la batteria si stia scaricando troppo" + makeSureDisabledAdBlocker: "Disattiva il tuo AdBlocker" + makeSureDisabledAdBlocker_description: "Gli AdBlocker possono influire sulle prestazioni. Controlla se nel tuo sistema operativo, nel browser o nei componenti aggiuntivi è abilitato un AdBlocker." + makeSureDisabledCustomCss: "Disabilita CSS personalizzato" + makeSureDisabledCustomCss_description: "La riscrittura degli stili CSS può influire sulle prestazioni. Assicurati di non avere CSS personalizzati o estensioni abilitate che sovrascrivano i tuoi stili." + makeSureDisabledAddons: "Disabilitare le estensioni" + makeSureDisabledAddons_description: "Alcune estensioni potrebbero interferire con il funzionamento del client e comprometterne le prestazioni. Prova a disattivare le estensioni del browser e vedi se il problema persiste." +_clip: + tip: "Le clip sono una funzionalità che consente di raggruppare le Note." +_userLists: + tip: "Puoi creare un elenco di Note create da qualsiasi profilo. L'elenco è visualizzato come una sequenza temporale." +watermark: "Filigrana" +defaultPreset: "Impostazioni predefinite" +_watermarkEditor: + tip: "Puoi aggiungere una filigrana, ad esempio con i crediti alle tue immagini." + quitWithoutSaveConfirm: "Uscire senza salvare?" + driveFileTypeWarn: "Formato file non supportato" + driveFileTypeWarnDescription: "Per favore seleziona un file immagine" + title: "Modifica la filigrana" + cover: "Coprire tutto" + repeat: "Disposizione" + opacity: "Opacità" + scale: "Dimensioni" + text: "Testo" + position: "Posizione" + type: "Tipo" + image: "Immagini" + advanced: "Avanzato" + stripe: "Strisce" + stripeWidth: "Larghezza della linea" + stripeFrequency: "Il numero di linee" + angle: "Angolo" + polkadot: "A pallini" + checker: "revisore" + polkadotMainDotOpacity: "Opacità del punto principale" + polkadotMainDotRadius: "Dimensione del punto principale" + polkadotSubDotOpacity: "Opacità del punto secondario" + polkadotSubDotRadius: "Dimensione del punto secondario" + polkadotSubDotDivisions: "Quantità di punti secondari" +_imageEffector: + title: "Effetto" + addEffect: "Aggiungi effetto" + discardChangesConfirm: "Scarta le modifiche ed esci?" + _fxs: + chromaticAberration: "Aberrazione cromatica" + glitch: "Glitch" + mirror: "Specchio" + invert: "Inversione colore" + grayscale: "Bianco e nero" + colorAdjust: "Correzione Colore" + colorClamp: "Compressione del colore" + colorClampAdvanced: "Compressione del colore (avanzata)" + distort: "Distorsione" + threshold: "Soglia" + zoomLines: "Linea di saturazione" + stripe: "Strisce" + polkadot: "A pallini" + checker: "revisore" + blockNoise: "Attenua rumore" + tearing: "Strappa immagine" +drafts: "Bozza" +_drafts: + select: "Selezionare bozza" + cannotCreateDraftAnymore: "Hai superato il numero massimo di bozze ammissibili." + cannotCreateDraft: "Impossibile creare una bozza di questo contenuto." + delete: "Elimina bozza" + deleteAreYouSure: "Vuoi davvero eliminare la bozza?" + noDrafts: "Non c'è nessuna bozza." + replyTo: "Rispondere a {user}" + quoteOf: "Citare la nota di {user}" + postTo: "Inserire in {channel}" + saveToDraft: "Salva come bozza" + restoreFromDraft: "Recuperare dalle bozze" + restore: "Ripristina" + listDrafts: "Elenco bozze" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6c73285295..7aa88f399d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -74,8 +74,8 @@ youGotNewFollower: "フォローされました" receiveFollowRequest: "フォローリクエストされました" followRequestAccepted: "フォローが承認されました" mention: "メンション" -mentions: "あなた宛て" -directNotes: "ダイレクト投稿" +mentions: "メンション" +directNotes: "指名" importAndExport: "インポートとエクスポート" import: "インポート" export: "エクスポート" @@ -172,7 +172,7 @@ emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" settingGuide: "おすすめ設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" -cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします。" +cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。" youCanCleanRemoteFilesCache: "ファイル管理の🗑️ボタンで全てのキャッシュを削除できます。" cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする" cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。" @@ -298,6 +298,7 @@ uploadFromUrl: "URLアップロード" uploadFromUrlDescription: "アップロードしたいファイルのURL" uploadFromUrlRequested: "アップロードをリクエストしました" uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。" +uploadNFiles: "{n}個のファイルをアップロード" explore: "みつける" messageRead: "既読" noMoreHistory: "これより過去の履歴はありません" @@ -326,6 +327,7 @@ dark: "ダーク" lightThemes: "明るいテーマ" darkThemes: "暗いテーマ" syncDeviceDarkMode: "デバイスのダークモードと同期する" +switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」がオンになっています。同期をオフにして手動でモードを切り替えますか?" drive: "ドライブ" fileName: "ファイル名" selectFile: "ファイルを選択" @@ -578,6 +580,7 @@ newNoteRecived: "新しいノートがあります" newNote: "新しいノート" sounds: "サウンド" sound: "サウンド" +notificationSoundSettings: "通知音の設定" listen: "聴く" none: "なし" showInPage: "ページで表示" @@ -634,8 +637,8 @@ addRelay: "リレーの追加" inboxUrl: "inboxのURL" addedRelays: "追加済みのリレー" serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。" -deletedNote: "削除された投稿" -invisibleNote: "非公開の投稿" +deletedNote: "削除されたノート" +invisibleNote: "非公開のノート" enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" @@ -792,6 +795,7 @@ wide: "広い" narrow: "狭い" reloadToApplySetting: "設定はページリロード後に反映されます。" needReloadToApply: "反映には再起動が必要です。" +needToRestartServerToApply: "反映にはサーバーの再起動が必要です。" showTitlebar: "タイトルバーを表示する" clearCache: "キャッシュをクリア" onlineUsersCount: "{n}人がオンライン" @@ -998,6 +1002,7 @@ failedToUpload: "アップロード失敗" cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" +cannotUploadBecauseUnallowedFileType: "許可されていないファイル種別のためアップロードできません。" beta: "ベータ" enableAutoSensitive: "自動センシティブ判定" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" @@ -1087,6 +1092,7 @@ prohibitedWordsDescription2: "スペースで区切るとAND指定になり、 hiddenTags: "非表示ハッシュタグ" hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" +usersSearchNotAvailable: "ユーザー検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" @@ -1298,7 +1304,7 @@ messageToFollower: "フォロワーへのメッセージ" target: "対象" testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。本番環境で使用しないでください。" prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)" -prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。" +prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。ユーザー名(username)に対しても全て小文字に置き換えて検査します。" yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています" yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。" thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています" @@ -1308,6 +1314,7 @@ availableRoles: "利用可能なロール" acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" +draft: "下書き" confirmOnReact: "リアクションする際に確認する" reactAreYouSure: "\" {emoji} \" をリアクションしますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" @@ -1324,9 +1331,10 @@ skip: "スキップ" restore: "復元" syncBetweenDevices: "デバイス間で同期" preferenceSyncConflictTitle: "サーバーに設定値が存在します" -preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?" -preferenceSyncConflictChoiceServer: "サーバーの設定値" -preferenceSyncConflictChoiceDevice: "デバイスの設定値" +preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか?" +preferenceSyncConflictChoiceMerge: "統合する" +preferenceSyncConflictChoiceServer: "サーバーの設定値で上書き" +preferenceSyncConflictChoiceDevice: "デバイスの設定値で上書き" preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル" paste: "ペースト" emojiPalette: "絵文字パレット" @@ -1351,6 +1359,26 @@ advice: "アドバイス" realtimeMode: "リアルタイムモード" turnItOn: "オンにする" turnItOff: "オフにする" +emojiMute: "絵文字ミュート" +emojiUnmute: "絵文字ミュート解除" +muteX: "{x}をミュート" +unmuteX: "{x}のミュートを解除" +abort: "中止" +tip: "ヒントとコツ" +redisplayAllTips: "全ての「ヒントとコツ」を再表示" +hideAllTips: "全ての「ヒントとコツ」を非表示" +defaultImageCompressionLevel: "デフォルトの画像圧縮度" +defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。" +inMinutes: "分" +inDays: "日" +safeModeEnabled: "セーフモードが有効です" +pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プラグインはすべて無効化されています。" +customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。" +themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。" + +_order: + newest: "新しい順" + oldest: "古い順" _chat: noMessagesYet: "まだメッセージはありません" @@ -1385,6 +1413,8 @@ _chat: chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。" cannotChatWithTheUser: "このユーザーとのチャットを開始できません" cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" + youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。" + doYouAcceptInvitation: "招待を承認しますか?" chatWithThisUser: "チャットする" thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。" thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。" @@ -1426,6 +1456,8 @@ _settings: makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする" makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。" useStickyIcons: "アイコンをスクロールに追従させる" + enableHighQualityImagePlaceholders: "高品質な画像のプレースホルダを表示" + uiAnimations: "UIのアニメーション" showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示" ifOn: "オンのとき" ifOff: "オフのとき" @@ -1436,6 +1468,9 @@ _settings: contentsUpdateFrequency: "コンテンツの取得頻度" contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。" contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。" + showUrlPreview: "URLプレビューを表示する" + showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示" + showPageTabBarBottom: "ページのタブバーを下部に表示" _chat: showSenderName: "送信者の名前を表示" @@ -1576,9 +1611,9 @@ _initialTutorial: public: "すべてのユーザーに公開。" home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。" followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。" - direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。" + direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。" doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。" - doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。" + doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーが含まれる限定公開のノートを作成する際は、機密情報の扱いに注意が必要です。" localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。" _cw: title: "内容を隠す(CW)" @@ -1622,6 +1657,10 @@ _serverSettings: fanoutTimelineDbFallback: "データベースへのフォールバック" fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" + remoteNotesCleaning: "リモート投稿の自動クリーニング" + remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。" + remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間" + remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数" inquiryUrl: "問い合わせ先URL" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" openRegistration: "アカウントの作成をオープンにする" @@ -1631,9 +1670,17 @@ _serverSettings: deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" singleUserMode: "お一人様モード" singleUserMode_description: "このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。" + signToActivityPubGet: "GETリクエストに署名する" + signToActivityPubGet_description: "通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。" + proxyRemoteFiles: "リモートファイルをプロキシする" + proxyRemoteFiles_description: "有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。" + allowExternalApRedirect: "ActivityPub経由の照会にリダイレクトを許可する" + allowExternalApRedirect_description: "有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。" userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲" userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。" userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。" + restartServerSetupWizardConfirm_title: "サーバーの初期設定ウィザードをやり直しますか?" + restartServerSetupWizardConfirm_text: "現在の一部の設定はリセットされます。" _userGeneratedContentsVisibilityForVisitor: all: "全て公開" @@ -1974,6 +2021,7 @@ _role: descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" canHideAds: "広告の非表示" canSearchNotes: "ノート検索の利用" + canSearchUsers: "ユーザー検索の利用" canUseTranslator: "翻訳機能の利用" avatarDecorationLimit: "アイコンデコレーションの最大取付個数" canImportAntennas: "アンテナのインポートを許可" @@ -1982,6 +2030,11 @@ _role: canImportMuting: "ミュートのインポートを許可" canImportUserLists: "リストのインポートを許可" chatAvailability: "チャットを許可" + uploadableFileTypes: "アップロード可能なファイル種別" + uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)" + uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。" + noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数" + watermarkAvailable: "ウォーターマーク機能の使用可否" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" @@ -2162,6 +2215,7 @@ _theme: install: "テーマのインストール" manage: "テーマの管理" code: "テーマコード" + copyThemeCode: "テーマコードをコピー" description: "説明" installed: "{name}をインストールしました" installedThemes: "インストールされたテーマ" @@ -2221,7 +2275,6 @@ _theme: buttonBg: "ボタンの背景" buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" - driveFolderBg: "ドライブフォルダーの背景" badge: "バッジ" messageBg: "チャットの背景" fgHighlighted: "強調された文字" @@ -2486,12 +2539,14 @@ _visibility: homeDescription: "ホームタイムラインのみに公開" followers: "フォロワー" followersDescription: "自分のフォロワーのみに公開" - specified: "ダイレクト" + specified: "指名" specifiedDescription: "指定したユーザーのみに公開" disableFederation: "連合なし" disableFederationDescription: "他サーバーへの配信を行いません" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?" + uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。" replyPlaceholder: "このノートに返信..." quotePlaceholder: "このノートを引用..." channelPlaceholder: "チャンネルに投稿..." @@ -2531,7 +2586,7 @@ _exportOrImport: userLists: "リスト" excludeMutingUsers: "ミュートしているユーザーを除外" excludeInactiveUsers: "使われていないアカウントを除外" - withReplies: "インポートした人による返信をTLに含むようにする" + withReplies: "返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする" _charts: federation: "連合" @@ -2730,8 +2785,8 @@ _deck: antenna: "アンテナ" list: "リスト" channel: "チャンネル" - mentions: "あなた宛て" - direct: "ダイレクト" + mentions: "メンション" + direct: "指名" roleTimeline: "ロールタイムライン" chat: "チャット" @@ -2846,6 +2901,7 @@ _fileViewer: url: "URL" uploadedAt: "追加日" attachedNotes: "添付されているノート" + usage: "利用" thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。" _externalResourceInstaller: @@ -2895,9 +2951,12 @@ _dataSaver: _avatar: title: "アイコン画像のアニメーションを無効化" description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。" - _urlPreview: + _urlPreviewThumbnail: title: "URLプレビューのサムネイルを非表示" description: "URLプレビューのサムネイル画像が読み込まれなくなります。" + _disableUrlPreview: + title: "URLプレビューを無効化" + description: "URLプレビュー機能を無効化します。サムネイル画像だけと違い、リンク先の情報の読み込み自体を削減できます。" _code: title: "コードハイライトを非表示" description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" @@ -2959,6 +3018,8 @@ _offlineScreen: _urlPreviewSetting: title: "URLプレビューの設定" enable: "URLプレビューを有効にする" + allowRedirect: "プレビュー先のリダイレクトを許可" + allowRedirectDescription: "入力されたURLがリダイレクトされる場合に、そのリダイレクト先をたどってプレビューを表示するかどうかを設定します。無効にするとサーバーリソースの節約になりますが、リダイレクト先の内容は表示されなくなります。" timeout: "プレビュー取得時のタイムアウト(ms)" timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。" maximumContentLength: "Content-Lengthの最大値(byte)" @@ -3037,10 +3098,6 @@ _customEmojisManager: uploadSettingDescription: "この画面で絵文字アップロードを行う際の動作を設定できます。" directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する" directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。" - emojiInputAreaCaption: "いずれかの方法で登録する絵文字を選択してください。" - emojiInputAreaList1: "この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ" - emojiInputAreaList2: "このリンクをクリックしてPCから選択する" - emojiInputAreaList3: "このリンクをクリックしてドライブから選択する" confirmRegisterEmojisDescription: "リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)" confirmClearEmojisDescription: "編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?" confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?" @@ -3114,6 +3171,7 @@ _bootErrors: otherOption1: "クライアント設定とキャッシュを削除" otherOption2: "簡易クライアントを起動" otherOption3: "修復ツールを起動" + otherOption4: "Misskeyをセーフモードで起動" _search: searchScopeAll: "全て" @@ -3152,6 +3210,8 @@ _serverSetupWizard: doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" + remoteContentsCleaning: "受信コンテンツの自動クリーニング" + remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。" adminInfo: "管理者情報" adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" @@ -3166,3 +3226,130 @@ _serverSetupWizard: text1: "Misskeyは有志によって開発されている無料のソフトウェアです。" text2: "今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。" text3: "支援者向け特典もあります!" + +_uploader: + editImage: "画像の編集" + compressedToX: "{x}に圧縮" + savedXPercent: "{x}%節約" + abortConfirm: "アップロードされていないファイルがありますが、中止しますか?" + doneConfirm: "アップロードされていないファイルがありますが、完了しますか?" + maxFileSizeIsX: "アップロード可能な最大ファイルサイズは{x}です。" + allowedTypes: "アップロード可能なファイル種別" + tip: "ファイルはまだアップロードされていません。このダイアログで、アップロード前の確認・リネーム・圧縮・クロッピングなどが行えます。準備が出来たら、「アップロード」ボタンを押してアップロードを開始できます。" + +_clientPerformanceIssueTip: + title: "バッテリー消費が多いと感じたら" + makeSureDisabledAdBlocker: "アドブロッカーを無効にしてください" + makeSureDisabledAdBlocker_description: "アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。" + makeSureDisabledCustomCss: "カスタムCSSを無効にしてください" + makeSureDisabledCustomCss_description: "スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。" + makeSureDisabledAddons: "拡張機能を無効にしてください" + makeSureDisabledAddons_description: "一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。" + +_clip: + tip: "クリップは、ノートをまとめることができる機能です。" + +_userLists: + tip: "任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。" + +watermark: "ウォーターマーク" +defaultPreset: "デフォルトのプリセット" +_watermarkEditor: + tip: "画像にクレジット情報などのウォーターマークを追加することができます。" + quitWithoutSaveConfirm: "保存せずに終了しますか?" + driveFileTypeWarn: "このファイルは対応していません" + driveFileTypeWarnDescription: "画像ファイルを選択してください" + title: "ウォーターマークの編集" + cover: "全体に被せる" + repeat: "敷き詰める" + opacity: "不透明度" + scale: "サイズ" + text: "テキスト" + position: "位置" + type: "タイプ" + image: "画像" + advanced: "高度" + angle: "角度" + stripe: "ストライプ" + stripeWidth: "ラインの幅" + stripeFrequency: "ラインの数" + polkadot: "ポルカドット" + checker: "チェッカー" + polkadotMainDotOpacity: "メインドットの不透明度" + polkadotMainDotRadius: "メインドットの大きさ" + polkadotSubDotOpacity: "サブドットの不透明度" + polkadotSubDotRadius: "サブドットの大きさ" + polkadotSubDotDivisions: "サブドットの数" + +_imageEffector: + title: "エフェクト" + addEffect: "エフェクトを追加" + discardChangesConfirm: "変更を破棄して終了しますか?" + nothingToConfigure: "設定項目はありません" + + _fxs: + chromaticAberration: "色収差" + glitch: "グリッチ" + mirror: "ミラー" + invert: "色の反転" + grayscale: "白黒" + colorAdjust: "色調補正" + colorClamp: "色の圧縮" + colorClampAdvanced: "色の圧縮(高度)" + distort: "歪み" + threshold: "二値化" + zoomLines: "集中線" + stripe: "ストライプ" + polkadot: "ポルカドット" + checker: "チェッカー" + blockNoise: "ブロックノイズ" + tearing: "ティアリング" + + _fxProps: + angle: "角度" + scale: "サイズ" + size: "サイズ" + color: "色" + opacity: "不透明度" + normalize: "正規化" + amount: "量" + lightness: "明るさ" + contrast: "コントラスト" + hue: "色相" + brightness: "輝度" + saturation: "彩度" + max: "最大値" + min: "最小値" + direction: "方向" + phase: "位相" + frequency: "頻度" + strength: "強さ" + glitchChannelShift: "ズレ" + seed: "シード値" + redComponent: "赤色成分" + greenComponent: "緑色成分" + blueComponent: "青色成分" + threshold: "しきい値" + centerX: "中心X" + centerY: "中心Y" + zoomLinesSmoothing: "スムージング" + zoomLinesSmoothingDescription: "スムージングと集中線の幅の設定は併用できません。" + zoomLinesThreshold: "集中線の幅" + zoomLinesMaskSize: "中心径" + zoomLinesBlack: "黒色にする" + +drafts: "下書き" +_drafts: + select: "下書きを選択" + cannotCreateDraftAnymore: "下書きの作成可能数を超えています。" + cannotCreateDraft: "この内容では下書きを作成できません。" + delete: "下書きを削除" + deleteAreYouSure: "下書きを削除しますか?" + noDrafts: "下書きはありません" + replyTo: "{user}への返信" + quoteOf: "{user}のノートへの引用" + postTo: "{channel}への投稿" + saveToDraft: "下書きへ保存" + restoreFromDraft: "下書きから復元" + restore: "復元" + listDrafts: "下書き一覧" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index ca5a046b90..235f11e197 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -234,7 +234,7 @@ clearQueue: "キューをほかす" clearQueueConfirmTitle: "キューをほかしとこか?" clearQueueConfirmText: "未配達の投稿は配送されんなるで。ふつうこの操作を行う必要は無いんやけどな。" clearCachedFiles: "キャッシュをほかす" -clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?" +clearCachedFilesConfirm: "キャッシュされとるリモートファイルを全部ほかしてええか?" blockedInstances: "ブロックしたサーバー" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。" silencedInstances: "サーバーサイレンスされてんねん" @@ -300,6 +300,7 @@ uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間か explore: "みつける" messageRead: "もう読んだ" noMoreHistory: "これより昔のんはあらへんで" +startChat: "チャットを始めよか" nUsersRead: "{n}人が読んでもうた" agreeTo: "{0}に同意したで" agree: "せやな" @@ -324,6 +325,7 @@ dark: "ダーク" lightThemes: "デイゲーム" darkThemes: "ナイトゲーム" syncDeviceDarkMode: "デバイスのダークモードと一緒にする" +switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」がオンになってるで。同期をオフにして手動でモードを切り替えることにします?" drive: "ドライブ" fileName: "ファイル名" selectFile: "ファイル選んでや" @@ -422,6 +424,7 @@ antennaExcludeBots: "Botアカウントを除外" antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や" notifyAntenna: "新しいノートを通知すんで" withFileAntenna: "なんか添付されたノートだけ" +excludeNotesInSensitiveChannel: "センシティブなチャンネルのノートは入れんとくわ" enableServiceworker: "ブラウザにプッシュ通知が行くようにする" antennaUsersDescription: "ユーザー名を改行で区切ったってな" caseSensitive: "大文字と小文字は別もんや" @@ -693,6 +696,7 @@ userSaysSomethingAbout: "{name}が「{word}」についてなんか言うてた makeActive: "使うで" display: "表示" copy: "コピー" +copiedToClipboard: "クリップボードにコピーされたで" metrics: "メトリクス" overview: "概要" logs: "ログ" @@ -787,6 +791,7 @@ wide: "広い" narrow: "狭い" reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?" needReloadToApply: "反映には再起動せなあかんで" +needToRestartServerToApply: "反映にはサーバーを再起動せなあかんのよ。" showTitlebar: "タイトルバーを見せる" clearCache: "キャッシュをほかす" onlineUsersCount: "{n}人が起きとるで" @@ -974,6 +979,7 @@ document: "ドキュメント" numberOfPageCache: "ページ、どんだけキャッシュすんの?" numberOfPageCacheDescription: "増やすと使いやすくなるけど、負荷とメモリ使用量が増えてくで。一長一短やな。" logoutConfirm: "ログアウトしまっか?" +logoutWillClearClientData: "ログアウトするとクライアントの設定情報がブラウザから消されてまうで。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にするとええで。" lastActiveDate: "最後に使った日時" statusbar: "ステータスバー" pleaseSelect: "選んだってやー" @@ -992,6 +998,7 @@ failedToUpload: "アップロードに失敗してもうたわ…" cannotUploadBecauseInappropriate: "きわどい内容を含むかもしれへんって言われたからアップロードできへんわ。" cannotUploadBecauseNoFreeSpace: "ドライブがもうパンパンやからアップロードできへんわ。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルが思うたよりも大きいさかいアップロードできへんでこれ。" +cannotUploadBecauseUnallowedFileType: "許可されてへんファイル種別やからアップロードできへんっぽい。" beta: "ベータ" enableAutoSensitive: "自動できわどいか判断する" enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。" @@ -1304,16 +1311,97 @@ federationSpecified: "このサーバーはホワイトリスト連合で運用 federationDisabled: "このサーバーは連合が無効化されてるで。他のサーバーのユーザーとやり取りすることはできひんで。" confirmOnReact: "ツッコむときに確認とる" reactAreYouSure: "\" {emoji} \" でツッコむ?" +markAsSensitiveConfirm: "このメディアをきわどい扱いしときますか?" +unmarkAsSensitiveConfirm: "このメディアはやっぱきわどくなかったってことでええんか?" +noName: "名前はあらへんで" +preferenceSyncConflictTitle: "サーバーに設定値があるみたいやわ" +preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存するねんけど、この設定項目はサーバーに保存されたやつがあるみたいやわ。どないするん?" +preferenceSyncConflictChoiceMerge: "ガッチャンコしよか" +preferenceSyncConflictChoiceCancel: "同期の有効化はやめとくわ" postForm: "投稿フォーム" information: "情報" +migrateOldSettings: "旧設定情報をお引っ越し" +migrateOldSettings_description: "通常これは自動で行われるはずなんやけど、なんかの理由で上手く移行できへんかったときは手動で移行処理をポチっとできるで。今の設定情報は上書きされるで。" +settingsMigrating: "設定を移行しとるで。ちょっと待っとってな... (後で、設定→その他→旧設定情報を移行 で手動で移行することもできるで)" +driveAboutTip: "ドライブでは、今までアップロードしたファイルがずらーっと表示されるで。
\nノートにファイルをもっかいのっけたり、あとで投稿するファイルをその辺に置いとくこともできるねん。
\nファイルをほかすと、前にそのファイルをのっけた全部の場所(ノート、ページ、アバター、バナー等)からも見えんくなるから気いつけてな。
\nフォルダを作って整理することもできるで。" +turnItOn: "オンにしとこ" +turnItOff: "オフでええわ" +emojiUnmute: "絵文字ミュートやめたる" +unmuteX: "{x}のミュートやめたる" +redisplayAllTips: "全部の「ヒントとコツ」をもっかい見して" +hideAllTips: "「ヒントとコツ」は全部表示せんでええ" +defaultImageCompressionLevel_description: "低くすると画質は保てるんやけど、ファイルサイズが増えるで。
高くするとファイルサイズは減らせるんやけど、画質が落ちるで。" +inMinutes: "分" +inDays: "日" +safeModeEnabled: "セーフモードがオンになってるで" +pluginsAreDisabledBecauseSafeMode: "セーフモードがオンやから、プラグインは全部無効化されてるで。" +customCssIsDisabledBecauseSafeMode: "セーフモードがオンやから、カスタムCSSは適用されてへんで。" +themeIsDefaultBecauseSafeMode: "セーフモードがオンの間はデフォルトのテーマを使うで。セーフモードをオフにれば元に戻るで。" _chat: + noMessagesYet: "まだメッセージはあらへんで" + individualChat_description: "特定のユーザーと一対一でチャットができるで。" + roomChat_description: "複数人でチャットできるで。\nあと、個人チャットを許可してへんユーザーとでも、相手がええって言うならチャットできるで。" + inviteUserToChat: "ユーザーを招待してチャットを始めてみ" invitations: "来てや" + noInvitations: "招待はあらへんで" noHistory: "履歴はないわ。" + noRooms: "ルームはあらへんで" members: "メンバーはん" home: "ホーム" send: "送信" + deleteRoom: "ルームをほかす" + chatNotAvailableForThisAccountOrServer: "このサーバー、もしくはこのアカウントでチャットが有効にされてへんで。" + chatIsReadOnlyForThisAccountOrServer: "このサーバー、もしくはこのアカウントでチャットが読み取り専用になっとるわ。新しく書き込んだり、チャットルームを作ったり参加したりはできへんで。" + chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えんくなっとるみたいやわ。" + cannotChatWithTheUser: "このユーザーとのチャットを開始できへんみたいやわ" + cannotChatWithTheUser_description: "チャットが使えん状態になっとるか、相手がチャットを開放してへんみたいやわ。" + youAreNotAMemberOfThisRoomButInvited: "あんたはこのルームの参加者ちゃうけど、招待が届いとるで。参加するんやったら、招待を承認してな。" + doYouAcceptInvitation: "招待を承認してもええんか?" + chatWithThisUser: "チャットしよか" + thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのチャットしか受け付けとらんみたいやわ。" + thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしとるユーザーからのチャットしか受け付けとらんみたいやわ。" + thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのチャットしか受け付けとらんみたいやわ。" + thisUserNotAllowedChatAnyone: "このユーザーは誰からのチャットも受け付けとらんみたいやわ。" + chatAllowedUsers: "チャットしてもええ相手" + chatAllowedUsers_note: "自分からチャットメッセージを送った相手やったらこの設定に関わらずチャットできるで。" + _chatAllowedUsers: + followers: "自分のフォロワーだけ" + following: "自分がフォローしとるユーザーだけ" + mutual: "相互フォローのユーザーだけ" + none: "誰もかもあかん" +_emojiPalette: + enableSyncBetweenDevicesForPalettes: "パレットのデバイス間同期をつけとく" + paletteForMain: "メインで使うパレット" + paletteForReaction: "リアクションで使うパレット" _settings: + driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードするときの設定ができるで。" + pluginBanner: "プラグインを使うとクライアントの機能を拡張できるねん。プラグインのインストール、個別の設定と管理ができるで。" + notificationsBanner: "サーバーから受け取る通知の種類とか範囲、プッシュ通知の設定ができるで。" webhook: "Webhook" + serviceConnectionBanner: "外部のアプリ・サービスと連携するのに使うとるアクセストークンとかWebhookの管理と設定ができるで。" + accountDataBanner: "アカウントデータのアーカイブをエクスポート/インポートして管理できるで。" + muteAndBlockBanner: "見せんでええコンテンツの設定とか、特定のユーザーからのアクションを制限する設定と管理ができるで。" + accessibilityBanner: "クライアントの視覚や動作に関わるパーソナライズをして、よりええ感じに使えるように設定できるで。" + privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制とかアカウントのプライバシーに関わる設定ができるで。" + securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーとかアカウントのセキュリティに関わる設定ができるで。" + preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定ができるで。" + appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関わる設定ができるで。" + soundsBanner: "クライアントで流すサウンドの設定ができるで。" + makeEveryTextElementsSelectable: "全部のテキスト要素を選択できるようにする" + makeEveryTextElementsSelectable_description: "これをつけると、一部のシチュエーションでユーザビリティが低下するかもしれん。" + enablePullToRefresh_description: "マウスやったら、ホイールを押し込みながらドラッグしてな。" + realtimeMode_description: "サーバーと接続を確立して、リアルタイムでコンテンツを更新するで。通信量とバッテリーの消費が多くなるかもしれへん。" + contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されるんやけど、そのぶんパフォーマンスが低くなるし、通信量とバッテリーの消費も増えるねん。" + contentsUpdateFrequency_description2: "リアルタイムモードをつけてるんやったら、この設定がどうであれリアルタイムでコンテンツが更新されるで。" +_preferencesProfile: + profileNameDescription: "このデバイスはなんて呼んだらええんや?" +_preferencesBackup: + noBackupsFoundTitle: "バックアップが見つからへんね" + noBackupsFoundDescription: "自動で作られたバックアップは見つからんかったけど、バックアップファイルを手動で保存してるんやったら、それをインポートして復元できるで。" + selectBackupToRestore: "復元するバックアップを選んでや" + youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効するんやったらプロファイル名の設定が必要やな。" + autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になってへんで。" + backupFound: "設定のバックアップがあるみたいやわ" _accountSettings: requireSigninToViewContents: "ログインしてもらってからコンテンツ見てもらう" requireSigninToViewContentsDescription1: "あなたが作成した全部のノートとかのコンテンツを見れるようにするのにログインがいるようにするで。クローラーにいろいろ収集されるんを防げるかもしれん。" @@ -1324,6 +1412,7 @@ _accountSettings: makeNotesHiddenBefore: "昔のノートを見れんようにする" makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。" + mayNotEffectSomeSituations: "これらの制限は簡易的なものやで。リモートサーバーでの閲覧とかモデレーション時とか、一部のシチュエーションでは適用されへんかもしれん。" notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート" notesOlderThanSpecifiedDateAndTime: "決めた日時より前のノート" _abuseUserReport: @@ -1342,6 +1431,7 @@ _delivery: manuallySuspended: "手動停止中" goneSuspended: "サーバー削除のため停止中" autoSuspendedForNotResponding: "サーバー応答せえへんから停止中" + softwareSuspended: "配信停止中のソフトウェアやから停止中" _bubbleGame: howToPlay: "遊び方" hold: "ホールド" @@ -1468,11 +1558,21 @@ _serverSettings: fanoutTimelineDbFallback: "データベースにフォールバックする" fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。" reactionsBufferingDescription: "有効にしたら、リアクション作るときのパフォーマンスがすっごい上がって、データベースへの負荷が減るで。代わりに、Redisのメモリ使用は増えるで。" + remoteNotesCleaning_description: "つけると、参照されてへん古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑えてくれるで。" inquiryUrl: "問い合わせ先URL" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。" openRegistration: "アカウントの作成をオープンにする" openRegistrationWarning: "登録を解放するのはリスクが伴うで。サーバーをいっつも監視して、なんか起きたらすぐに対応できるんやったら、オンにしてもええと思う。" thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターがおらんかったら、スパムを防ぐためにこの設定は勝手に切られるで。" + deliverSuspendedSoftwareDescription: "脆弱性とかの理由で、サーバーのソフトウェアの名前とバージョンの範囲を決めて配信を止められるで。このバージョン情報はサーバーが提供したものやから、信頼性は保証されへん。バージョン指定には semver の範囲指定が使えるねんけど、>= 2024.3.1と指定すると 2024.3.1-custom.0 みたいなカスタムバージョンが含まれへんから、>= 2024.3.1-0 みたいに prerelease を指定するとええかもしれへんな。" + singleUserMode_description: "このサーバーを使うとるんが自分だけなんやったら、このモードを有効にすると動作がええ感じになるで。" + signToActivityPubGet_description: "通常はつけといてな。連合の通信に関わる問題があるんやったら、無効にすると改善するかもしれへんけど、逆にサーバーによっては通信ができんくなることがあるで。" + proxyRemoteFiles_description: "つけると、リモートのファイルをプロキシして提供するで。画像のサムネイル生成とかユーザーのプライバシー保護にええな。" + allowExternalApRedirect_description: "つけると、他のサーバーがうちのサーバーを通して第三者のコンテンツを照会できるようになるんやけど、コンテンツのなりすましが発生するかもしれへん。" + userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツとかが、うちのサーバー経由で図らずもインターネットに公開されてまうことによるトラブルを防止できたりするで。" + userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受け取ったリモートのコンテンツを含め、サーバー内の全部のコンテンツを何でもかんでもインターネットに公開するのはリスクを伴うねん。特に、分散型の特性を知らん閲覧者にとっては、リモートのコンテンツやったとしてもサーバー内で作られたコンテンツやと誤認してまうかもしれへんから、注意が必要やな。" + restartServerSetupWizardConfirm_title: "サーバーの初期設定ウィザードをやり直すん?" + restartServerSetupWizardConfirm_text: "現在の一部の設定はリセットされるで。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに引っ越す" moveFromSub: "別のアカウントへエイリアスを作る" @@ -1769,6 +1869,7 @@ _role: descriptionOfIsExplorable: "オンにしたらロールの面子一覧が「みつける」で公開されるし、ロールのタイムラインが使えるようになるで。" displayOrder: "表示順" descriptionOfDisplayOrder: "数がでかいほど、UI上で先に表示されるで。" + preserveAssignmentOnMoveAccount_description: "つけると、このロールがのっかったアカウントが引っ越したときに、引っ越し先アカウントにもこのロールがのっかるようになるで。" canEditMembersByModerator: "モデレーターがメンバーいじるのを許す" descriptionOfCanEditMembersByModerator: "オンにすると、管理者だけやなくてモデレーターもこのロールにユーザーを入れたり抜いたりできるで。オフにすると管理者だけしかやれへんくなるで。" priority: "優先度" @@ -1809,6 +1910,8 @@ _role: canImportFollowing: "フォローのインポートを許す" canImportMuting: "ミュートのインポートを許す" canImportUserLists: "リストのインポートを許す" + uploadableFileTypes_caption: "MIMEタイプを指定してや。改行で区切って複数指定もできるし、アスタリスク(*)でワイルドカード指定もできるで。(例: image/*)" + uploadableFileTypes_caption2: "ファイルによっては種別がわからんこともあるで。そないなファイルを許可するんやったら {x} を指定に追加してな。" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" @@ -2008,7 +2111,7 @@ _theme: navIndicator: "サイドバーのインジケーター" link: "リンク" hashtag: "ハッシュタグ" - mention: "メンション" + mention: "あんた宛て" mentionMe: "うち宛てのメンション" renote: "Renote" modalBg: "モーダルの背景" @@ -2025,7 +2128,6 @@ _theme: buttonBg: "ボタンの背景" buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" - driveFolderBg: "ドライブフォルダーの背景" badge: "バッジ" messageBg: "チャットの背景" fgHighlighted: "強調されとる文字" @@ -2278,6 +2380,8 @@ _visibility: disableFederation: "連合なし" disableFederationDescription: "他サーバーへは送らんとくわ" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされてへんファイルがあるんやけど、ほかしてフォームを閉じてもええんか?" + uploaderTip: "ファイルはまだアップロードされてへんで。ファイルのメニューから、リネームとか画像のクロップ、ウォーターマークをのっける、圧縮するかどうかなんかを設定できるで。ファイルはノートを投稿するときに自動でアップロードされるで。" replyPlaceholder: "このノートに返信..." quotePlaceholder: "このノートを引用..." channelPlaceholder: "チャンネルに投稿..." @@ -2429,6 +2533,7 @@ _notification: newNote: "さらの投稿" unreadAntennaNote: "アンテナ {name}" roleAssigned: "ロールが付与されたで" + chatRoomInvitationReceived: "チャットルームへ招待されたで" emptyPushNotificationMessage: "プッシュ通知の更新をしといたで" achievementEarned: "実績を獲得しとるで" testNotification: "通知テスト" @@ -2448,7 +2553,7 @@ _notification: all: "すべて" note: "あんたらの新規投稿" follow: "フォロー" - mention: "メンション" + mention: "あんた宛て" reply: "リプライ" renote: "リノート" quote: "引用" @@ -2618,7 +2723,7 @@ _externalResourceInstaller: _errors: _invalidParams: title: "" - description: "" + description: "外部サイトからデータを持ってくるのに欲しい情報が足らへんみたいやわ。URLは合っとる?" _resourceTypeNotSupported: title: "" description: "" @@ -2648,11 +2753,12 @@ _dataSaver: _avatar: title: "アイコンの絵" description: "アイコン画像のアニメが止まるで。普通の画像よりもデータ量がでかいから、もっと通信量を節約できるねん。" - _urlPreview: - title: "URLプレビューのサムネイル画像" - description: "URLプレビューのサムネイル画像が読み込まへんなるで。" + _urlPreviewThumbnail: + description: "URLプレビューのサムネイル画像が読み込まれへんくなるで。" + _disableUrlPreview: + description: "URLプレビュー機能を切るで。サムネイル画像だけと違って、リンク先の情報の読み込み自体を削減できるで。" _code: - title: "コードハイライト" + title: "コードハイライトは表示せんでええ" description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。" _hemisphere: N: "北半球" @@ -2708,6 +2814,7 @@ _offlineScreen: _urlPreviewSetting: title: "URLプレビューの設定" enable: "URLプレビューを有効にする" + allowRedirectDescription: "入力されたURLがリダイレクトされるとき、そのリダイレクト先をたどってプレビューを表示するかどうかを設定できるで。無効にするとサーバーリソースを節約できるんやけど、リダイレクト先の内容は表示されへんくなるで。" timeout: "プレビュー取得時のタイムアウト(ms)" timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されへんで。" maximumContentLength: "Content-Lengthの最大値(byte)" @@ -2781,10 +2888,6 @@ _customEmojisManager: uploadSettingDescription: "この画面で絵文字アップロードするときの動きを設定できるで。" directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する" directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。" - emojiInputAreaCaption: "どれかの方法で登録する絵文字を選択して。" - emojiInputAreaList1: "この枠に画像ファイルかディレクトリをドラッグ&ドロップ" - emojiInputAreaList2: "このリンクをクリックしてPCから選択する" - emojiInputAreaList3: "このリンクをクリックしてドライブから選択する" confirmRegisterEmojisDescription: "リストに表示されてる絵文字を新たなカスタム絵文字として登録するで。ほんまにええか? (サーバーがしんどくなるから、一回で登録できる絵文字は{count}件までやで)" confirmClearEmojisDescription: "編集内容をほかして、リストに表示されている絵文字をクリアするで。ほんまにええか?" confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードするで。ほんまにええか?" @@ -2856,3 +2959,70 @@ _search: searchScopeAll: "みんな" searchScopeLocal: "ローカル" searchScopeUser: "ユーザー指定" + pleaseEnterServerHost: "サーバーのホストはどないするん?" + pleaseSelectUser: "ユーザーを選んでや" +_serverSetupWizard: + installCompleted: "Misskeyのインストールが終わったで!" + firstCreateAccount: "最初は、管理者アカウントを作成しよか。" + accountCreated: "管理者アカウントができたで!" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "このウィザードで簡単にええ感じのサーバーの設定ができるで。" + settingsYouMakeHereCanBeChangedLater: "ここでの設定は、あとからでも変えられるで。" + howWillYouUseMisskey: "Misskeyをどんな感じに使うん?" + _use: + single_youCanCreateMultipleAccounts: "お一人様サーバーとして運用するとしても、アカウントは必要に応じて複数作れるで。" + openServerAdvice: "不特定多数の利用者を受け入れるには相応のリスクがあるで。トラブルに対処できるよう、ちゃんとしたモデレーション体制で運営しいや。" + openServerAntiSpamAdvice: "うちのサーバーがスパムの踏み台にならへんように、reCAPTCHAとかのアンチボット機能を使う、みたいなセキュリティ対策もしっかり考えてな。" + howManyUsersDoYouExpect: "どれくらいの人数を考えとるん?" + largeScaleServerAdvice: "大規模なサーバーやったら、ロードバランシングとかデータベースのレプリケーションみたいな、高度なインフラストラクチャーの知識が必要になるかもしれへんわ。" + doYouConnectToFediverse: "Fediverseと接続するんやっけ?" + doYouConnectToFediverse_description1: "分散型サーバーでできたネットワーク(Fediverse)に繋げると、他のサーバーと相互にコンテンツのやり取りができるようになるで。" + doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれるな。" + youCanConfigureMoreFederationSettingsLater: "連合してもええサーバーの指定とか、高度な設定も後でできるで。" + remoteContentsCleaning_description: "連合すると、ぎょうさんコンテンツを受け取り続けることになるねん。自動クリーニングをつけると、参照されてない古いコンテンツを自動でサーバーからほかして、ストレージを節約できるで。" + adminInfo_description: "問い合わせを受け付けるのに使う管理者情報を設定しよか。" + adminInfo_mustBeFilled: "オープンサーバー、もしくは連合を入れとるんやったら必ず入力せなあかんで。" + followingSettingsAreRecommended: "こういう設定がええかもな" + settingsCompleted: "設定が終わったで!" + settingsCompleted_description: "お疲れさん。準備ができたから、さっそくサーバーを使い始められるで。" + settingsCompleted_description2: "細かいサーバー設定は、「コントロールパネル」を見てみてな。" + _donationRequest: + text1: "Misskeyは有志で開発されとる無料のソフトウェアやで。" + text2: "今後も開発を続けられるように、よかったらぜひカンパをお願いするわ。" + text3: "支援者向け特典もあるで!" +_uploader: + abortConfirm: "アップロードされてへんファイルがあるんやけど、やめてもええんか?" + doneConfirm: "アップロードされてへんファイルがあるんやけど、完了してもええんか?" + maxFileSizeIsX: "アップロードできるファイルサイズは{x}までやで。" + tip: "ファイルはまだアップロードされてへんで。このダイアログで、アップロードする前に確認・リネーム・圧縮・クロッピングとかをできるで。準備が出来たら、「アップロード」ボタンを押してアップロードしてな。" +_clientPerformanceIssueTip: + makeSureDisabledAdBlocker: "アドブロッカーを切ってみてや" + makeSureDisabledAdBlocker_description: "アドブロッカーはパフォーマンスに影響があるかもしれへん。OSの機能とかブラウザの機能・アドオンとかでアドブロッカーが有効になってないか確認してや。" + makeSureDisabledCustomCss: "カスタムCSSを無効にしてみてや" + makeSureDisabledCustomCss_description: "スタイルを上書きするとパフォーマンスに影響があるかもしれへん。カスタムCSSとか、スタイルを上書きする拡張機能が有効になってないか確認してや。" + makeSureDisabledAddons: "拡張機能を無効にしてみてや" + makeSureDisabledAddons_description: "なんかの拡張機能がクライアントの動作にちょっかいをかけてパフォーマンスに影響を与えてるかもしれへん。ブラウザの拡張機能を無効にして良くなるか確認してや。" +_clip: + tip: "クリップは、ノートをまとめられる機能やで。" +_userLists: + tip: "好きなユーザーを含むリストを作れるねん。作ったリストはタイムラインとして表示できるで。" +_watermarkEditor: + tip: "画像にクレジット情報とかのウォーターマークをのっけられるで。" + quitWithoutSaveConfirm: "保存せずに終わってもええんか?" + driveFileTypeWarn: "このファイルは対応しとらへん" + driveFileTypeWarnDescription: "画像ファイルを選んでや" + opacity: "不透明度" + scale: "大きさ" + text: "テキスト" + position: "位置" + type: "タイプ" + image: "画像" + advanced: "高度" + angle: "角度" +_imageEffector: + discardChangesConfirm: "変更をせんで終わるか?" +_drafts: + cannotCreateDraftAnymore: "下書きはこれ以上は作れへんな。" + cannotCreateDraft: "この内容で下書きは作れへんな。" + delete: "下書きをほかす" + deleteAreYouSure: "下書きをほかしてもええか?" + noDrafts: "下書きはあらへん" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index 222599572a..cdd9a5b2ae 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -44,6 +44,7 @@ showMore: "ಇನ್ನಷ್ಟು ನೋಡು" youGotNewFollower: "ಹಿಂಬಾಲಿಸಿದರು" receiveFollowRequest: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಬಂದಿದೆ" followRequestAccepted: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಸ್ವೀಕರಿಸಲಾಯಿತು" +mention: "ಹೆಸರಿಸಿದ" mentions: "ಹೆಸರಿಸಿದ" directNotes: "ನೇರ ಟಿಪ್ಪಣಿಗಳು" importAndExport: "ಆಮದು/ರಫ್ತು" @@ -65,6 +66,9 @@ replies: "ಉತ್ತರಿಸು" _email: _follow: title: "ಹಿಂಬಾಲಿಸಿದರು" +_theme: + keys: + mention: "ಹೆಸರಿಸಿದ" _sfx: notification: "ಅಧಿಸೂಚನೆಗಳು" _widgets: @@ -73,11 +77,14 @@ _widgets: timeline: "ಸಮಯಸಾಲು" _cw: show: "ಇನ್ನಷ್ಟು ನೋಡು" +_visibility: + specified: "ನೇರ ಟಿಪ್ಪಣಿಗಳು" _profile: username: "ಬಳಕೆಹೆಸರು" _notification: youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು" _types: + mention: "ಹೆಸರಿಸಿದ" login: "ಪ್ರವೇಶ" _actions: reply: "ಉತ್ತರಿಸು" @@ -86,3 +93,4 @@ _deck: notifications: "ಅಧಿಸೂಚನೆಗಳು" tl: "ಸಮಯಸಾಲು" mentions: "ಹೆಸರಿಸಿದ" + direct: "ನೇರ ಟಿಪ್ಪಣಿಗಳು" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 361d90d8fa..59fe63be6c 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -745,7 +745,7 @@ _menuDisplay: _theme: description: "설멩" keys: - mention: "멘션" + mention: "받언 멘션" renote: "리노트" _sfx: note: "새 노트" @@ -775,6 +775,7 @@ _cw: _visibility: home: "덜머리" followers: "팔로워" + specified: "쪽지 서기" _postForm: _placeholders: e: "옇다 서 주이소" @@ -809,7 +810,7 @@ _notification: newNote: "새 걸" _types: follow: "팔로잉" - mention: "멘션" + mention: "받언 멘션" renote: "리노트" quote: "따오기" reaction: "반엉" @@ -824,6 +825,7 @@ _deck: antenna: "안테나" list: "리스트" mentions: "받언 멘션" + direct: "쪽지 서기" _webhookSettings: name: "이럼" _abuseReport: @@ -848,3 +850,5 @@ _remoteLookupErrors: _search: searchScopeAll: "말캉" searchScopeUser: "사용자 지정" +_watermarkEditor: + image: "이미지" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 9d3b130bf2..4950f78b4d 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -298,6 +298,7 @@ uploadFromUrl: "URL 업로드" uploadFromUrlDescription: "업로드하려는 파일의 URL" uploadFromUrlRequested: "업로드를 요청했습니다" uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다." +uploadNFiles: "{n}개의 파일을 업로" explore: "둘러보기" messageRead: "읽음" noMoreHistory: "이것보다 과거의 기록이 없습니다" @@ -326,6 +327,7 @@ dark: "다크" lightThemes: "밝은 테마" darkThemes: "어두운 테마" syncDeviceDarkMode: "디바이스의 다크 모드 설정과 동기화" +switchDarkModeManuallyWhenSyncEnabledConfirm: "'{x}'가 켜져 있습니다. 동기화를 끄고 수동으로 모드를 변경하겠습니까?" drive: "드라이브" fileName: "파일명" selectFile: "파일 선택" @@ -575,8 +577,10 @@ showFixedPostForm: "타임라인 상단에 글 입력란을 표시" showFixedPostFormInChannel: "채널 타임라인 상단에 글 입력란을 표시" withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답글을 타임라인에 나오게 하기" newNoteRecived: "새 노트가 있습니다" +newNote: "새로운 노트" sounds: "소리" sound: "소리" +notificationSoundSettings: "알림 설정" listen: "듣기" none: "없음" showInPage: "페이지로 보기" @@ -791,6 +795,7 @@ wide: "넓게" narrow: "좁게" reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?" needReloadToApply: "변경 사항은 새로고침하면 적용됩니다." +needToRestartServerToApply: "변경 사항은 새로고침이 필요합니다." showTitlebar: "타이틀 바를 표시하기" clearCache: "캐시 비우기" onlineUsersCount: "{n}명이 접속 중" @@ -997,6 +1002,7 @@ failedToUpload: "업로드 실패" cannotUploadBecauseInappropriate: "이 파일은 부적절한 내용을 포함한다고 판단되어 업로드할 수 없습니다." cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다." cannotUploadBecauseExceedsFileSizeLimit: "파일 크기가 너무 크기 때문에 업로드할 수 없습니다." +cannotUploadBecauseUnallowedFileType: "허가되지 않은 유형의 파일이기에 업로드할 수 없습니다." beta: "베타" enableAutoSensitive: "자동 NSFW 탐지" enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 서버 정책에 따라 자동으로 설정될 수 있습니다." @@ -1307,6 +1313,7 @@ availableRoles: "사용 가능한 역할" acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다." federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다." federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다." +draft: "초안" confirmOnReact: "리액션할 때 확인" reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?" markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?" @@ -1324,6 +1331,7 @@ restore: "복원" syncBetweenDevices: "장치간 동기화" preferenceSyncConflictTitle: "서버에 설정값이 존재합니다." preferenceSyncConflictText: "동기화를 활성화 한 항목의 설정 값은 서버에 저장되지만, 해당 항목은 이미 서버에 설정 값이 저장되어져 있습니다. 어느 쪽의 설정 값을 덮어씌울까요?" +preferenceSyncConflictChoiceMerge: "병합" preferenceSyncConflictChoiceServer: "서버 설정값" preferenceSyncConflictChoiceDevice: "장치 설정값" preferenceSyncConflictChoiceCancel: "동기화 취소" @@ -1346,6 +1354,29 @@ goToDeck: "덱으로 돌아가기" federationJobs: "연합 작업" driveAboutTip: "드라이브는 이전에 업로드한 파일 목록을 표시해요.
\n노트에 첨부할 때 다시 사용하거나 나중에 게시할 파일을 미리 업로드할 수 있어요.
\n파일을 삭제하면, 지금까지 그 파일을 사용한 모든 장소(노트, 페이지, 아바타, 배너 등)에서도 보이지 않게 되므로 주의해 주세요. 폴더를 만들고 정리할 수도 있어요.
" scrollToClose: "스크롤하여 닫기" +advice: "참고" +realtimeMode: "실시간 모드" +turnItOn: "켜기" +turnItOff: "끄기" +emojiMute: "이모티콘 뮤트" +emojiUnmute: "이모티콘 뮤트 해제" +muteX: "{x}를 뮤트" +unmuteX: "{x}의 뮤트를 해제" +abort: "중지" +tip: "팁과 유용한 정보" +redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시" +hideAllTips: "모든 '팁과 유용한 정보'를 비표시" +defaultImageCompressionLevel: "기본 이미지 압축 정도" +defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다.
높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다." +inMinutes: "분" +inDays: "일" +safeModeEnabled: "세이프 모드가 활성화돼있습니다" +pluginsAreDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 플러그인은 전부 비활성화됩니다." +customCssIsDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 커스텀 CSS는 적용되지 않습니다." +themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다." +_order: + newest: "최신 순" + oldest: "오래된 순" _chat: noMessagesYet: "아직 메시지가 없습니다" newMessage: "새로운 메시지" @@ -1379,6 +1410,8 @@ _chat: chatNotAvailableInOtherAccount: "상대방 계정에서 채팅 기능을 사용할 수 없는 상태입니다." cannotChatWithTheUser: "이 유저와 채팅을 시작할 수 없습니다" cannotChatWithTheUser_description: "채팅을 사용할 수 없는 상태이거나 상대방이 채팅을 열지 않은 상태입니다." + youAreNotAMemberOfThisRoomButInvited: "당신은 이 룸의 참가자가 아닙니다만 초대 신청을 받으셨습니다. 참가하려면 초대를 수락해주십시오." + doYouAcceptInvitation: "초대를 수락하시겠습니까?" chatWithThisUser: "채팅하기" thisUserAllowsChatOnlyFromFollowers: "이 유저는 팔로워만 채팅을 할 수 있습니다." thisUserAllowsChatOnlyFromFollowing: "이 유저는 이 유저가 팔로우하는 유저만 채팅을 허용합니다." @@ -1418,12 +1451,21 @@ _settings: makeEveryTextElementsSelectable: "모든 텍스트 요소를 선택할 수 있도록 함" makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 유저의 접근성이 나빠질 수도 있습니다." useStickyIcons: "아이콘이 스크롤을 따라가도록 하기" + enableHighQualityImagePlaceholders: "고화질 이미지의 플레이스홀더를 표시" + uiAnimations: "UI 애니메이션" showNavbarSubButtons: "내비게이션 바에 보조 버튼 표시" ifOn: "켜져 있을 때" ifOff: "꺼져 있을 때" enableSyncThemesBetweenDevices: "기기 간 설치한 테마 동기화" enablePullToRefresh: "계속해서 갱신" enablePullToRefresh_description: "마우스에서 휠을 누르면서 드래그해요." + realtimeMode_description: "서버에 접속하고 실시간으로 콘텐츠를 업데이트합니다. 데이터 사용량과 배터리의 소비가 증가할 수 있습니다." + contentsUpdateFrequency: "콘텐츠의 업데이트 빈도" + contentsUpdateFrequency_description: "높을수록 실시간으로 콘텐츠가 업데이트됩니다만, 성능이 저하되고 데이터 사용량과 배터리의 소비가 증가합니다." + contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다." + showUrlPreview: "URL 미리보기 표시" + showAvailableReactionsFirstInNote: "이용 가능한 리액션을 선두로 표시" + showPageTabBarBottom: "페이지의 탭 바를 아래쪽에 표시" _chat: showSenderName: "발신자 이름 표시" sendOnEnter: "엔터로 보내기" @@ -1597,6 +1639,10 @@ _serverSettings: fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기" fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다." reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다." + remoteNotesCleaning: "리모트 서버 노트 자동 정리 " + remoteNotesCleaning_description: "더 이상 사용되지 않는 오래된 리모트 노트를 정기적으로 정리하여, 데이터 베이스의 사용량을 절약할 수 있습니다." + remoteNotesCleaningMaxProcessingDuration: "리모트 노트 자동 정리 최대 실행 시간" + remoteNotesCleaningExpiryDaysForEachNotes: "리모트 노트 저장 최소 일수" inquiryUrl: "문의처 URL" inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다." openRegistration: "회원 가입을 활성화 하기" @@ -1604,6 +1650,23 @@ _serverSettings: thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다." deliverSuspendedSoftware: "전달 정지 중인 소프트웨어" deliverSuspendedSoftwareDescription: "취약성 등의 이유로 서버의 소프트웨어 이름 및 버전 범위를 지정하여 전달을 정지할 수 있어요. 이 버전 정보는 서버가 제공한 것이며 신뢰성은 보장되지 않아요. 버전 지정에는 semver의 범위 지정을 사용할 수 있지만, >= 2024.3.1로 지정하면 2024.3.1-custom.0과 같은 custom.0과 같은 custom 버전이 포함되지 않기 때문에 >= 2024.3.1-0과 같이 prerelease를 지정하는 것이 좋아요." + singleUserMode: "1인 모드" + singleUserMode_description: "이 서버의 이용자가 자신 뿐인 경우, 이 모드를 활성화하면 동작이 최적화됩니다." + signToActivityPubGet: "GET 요청에 사인" + signToActivityPubGet_description: "보통의 경우 활성화해 주십시오. 연합의 통신에 관한 문제가 있는 경우, 비활성화하면 개선되는 경우도 있습니다만, 서버에 따라서는 통신이 불가능해지는 경우도 있습니다." + proxyRemoteFiles: "리모트 파일 프록시" + proxyRemoteFiles_description: "활성화하면 리모트 파일을 프록시로 제공합니다. 이미지의 섬네일 생성이나 유저의 개인정보 보호에 도움을 줍니다." + allowExternalApRedirect: "ActivityPub 경유 조회에 리디렉션 허가" + allowExternalApRedirect_description: "활성화하면 다른 서버가 이 서버를 통해 제3자의 콘텐츠를 조회할 수 있습니다만, 콘텐츠의 사칭 문제가 생길 수 있습니다." + userGeneratedContentsVisibilityForVisitor: "비이용자에 대한 유저 작성 콘텐츠의 공개 범위" + userGeneratedContentsVisibilityForVisitor_description: "조정을 하기 힘든 부적절한 리모트 콘텐츠 등이 자신의 서버 경유로 의도치 않게 인터넷에 공개되는 문제의 방지 등에 도움을 줍니다." + userGeneratedContentsVisibilityForVisitor_description2: "서버에서 받은 리모트 콘텐츠를 포함해 서버 내의 모든 콘텐츠를 무조건 인터넷에 공개하는 것에는 위험이 따릅니다. 특히, 분산형 특성에 대해 모르는 열람자에게는 리모트 콘텐츠여도 서버 내에서 작성된 콘텐츠라고 잘못 인식할 수 있기에 주의가 필요합니다." + restartServerSetupWizardConfirm_title: "서버의 초기 설정 위자드를 재시도하시겠습니까?" + restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다." + _userGeneratedContentsVisibilityForVisitor: + all: "모두 공개" + localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개" + none: "모두 비공개" _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" @@ -1944,6 +2007,11 @@ _role: canImportMuting: "뮤트 목록 가져오기 허용" canImportUserLists: "리스트 목록 가져오기 허용" chatAvailability: "채팅을 허락" + uploadableFileTypes: "업로드 가능한 파일 유형" + uploadableFileTypes_caption: "MIME 유형을 " + uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오." + noteDraftLimit: "서버측 노트 초안 작성 가능 수" + watermarkAvailable: "워터마크 기능의 사용 여부" _condition: roleAssignedTo: "수동 역할에 이미 할당됨" isLocal: "로컬 유저" @@ -2103,6 +2171,7 @@ _theme: install: "테마 설치" manage: "테마 관리" code: "테마 코드" + copyThemeCode: "테마 코드 복사" description: "설명" installed: "{name} 테마가 설치되었습니다" installedThemes: "설치된 테마" @@ -2161,7 +2230,6 @@ _theme: buttonBg: "버튼 배경" buttonHoverBg: "버튼 배경 (호버)" inputBorder: "입력 필드 테두리" - driveFolderBg: "드라이브 폴더 배경" badge: "배지" messageBg: "대화 배경" fgHighlighted: "강조된 텍스트" @@ -2417,6 +2485,8 @@ _visibility: disableFederation: "연합에 보내지 않기" disableFederationDescription: "다른 서버로 보내지 않습니다" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "업로드되지 않은 파일이 있습니다만, 없애고 폼을 닫겠습니까?" + uploaderTip: "파일이 아직 업로드돼있지 않습니다. 파일 메뉴에서 이름 바꾸기나 이미지의 자르기, 워터마크 넣기, 압축의 유무 등을 설정할 수 있습니다. 파일은 노트 게시 시 자동으로 업로드됩니다." replyPlaceholder: "이 노트에 답글..." quotePlaceholder: "이 노트를 인용..." channelPlaceholder: "채널에 게시하기..." @@ -2750,6 +2820,7 @@ _fileViewer: url: "URL" uploadedAt: "업로드 날짜" attachedNotes: "첨부된 노트" + usage: "이용" thisPageCanBeSeenFromTheAuthor: "이 페이지는 파일 소유자만 열람할 수 있습니다" _externalResourceInstaller: title: "외부 사이트로부터 설치" @@ -2797,9 +2868,12 @@ _dataSaver: _avatar: title: "아이콘 이미지" description: "아이콘 이미지의 애니메이션을 멈춥니다. 애니메이션 이미지는 일반 이미지보다 파일 크기가 클 수 있으므로 데이터 사용량을 더 줄일 수 있습니다." - _urlPreview: - title: "URL 미리보기의 섬네일" - description: "URL 미리보기의 섬네일 이미지를 불러오지 않게 됩니다." + _urlPreviewThumbnail: + title: "URL 미리보기의 섬네일을 비표시" + description: "URL 미리보기의 섬네일 이미지를 불러올 수 없게 됩니다." + _disableUrlPreview: + title: "URL 미리보기 비활성화" + description: "URL 미리보기 기능을 비활성화합니다. 섬네일 이미지와 달리 링크 정보 불러오기 자체를 줄일 수 있습니다." _code: title: "문자열 강조" description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다." @@ -2857,6 +2931,8 @@ _offlineScreen: _urlPreviewSetting: title: "URL 미리보기 설정" enable: "URL 미리보기 활성화" + allowRedirect: "미리보기 위치의 리디렉션 허가" + allowRedirectDescription: "입력된 URL이 리디렉션될 경우, 그 리디렉션 위치를 따라 미리보기를 표시할 것인지 설정합니다. 비활성화하면 서버 리소스를 절약할 수 있습니다만, 리디렉션 위치의 내용은 표시되지 않습니다." timeout: "미리보기를 불러올 때의 타임아웃 (ms)" timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 정한 시간보다 오래 걸리는 경우, 미리보기를 생성하지 않습니다." maximumContentLength: "Content-Length의 최대치 (byte)" @@ -2930,10 +3006,6 @@ _customEmojisManager: uploadSettingDescription: "여기서 이모지를 업로드 할 때의 동작을 설정할 수 있습니다." directoryToCategoryLabel: "디렉토리 이름을 \"category\"로 입력하기" directoryToCategoryCaption: "디렉토리를 드래그 앤 드롭한 경우, 디렉토리 이름을 \"category\"로 입력합니다." - emojiInputAreaCaption: "이모지를 등록할 방법을 선택해주세요." - emojiInputAreaList1: "이 틀 안에 이미지 파일 또는 디렉토리를 끌어서 가져오기" - emojiInputAreaList2: "이 링크를 클릭해서 PC에서 선택하기" - emojiInputAreaList3: "이 링크를 클릭해서 드라이브에서 선택하기" confirmRegisterEmojisDescription: "리스트에 표시되어진 이모지를 새로운 커스텀 이모지로 등록합니다. 실행할까요? (부하를 피하기 위해, 한 번에 등록할 수 있는 이모지는 {count}건까지 입니다.)" confirmClearEmojisDescription: "편집 내용을 지우고, 목록에 표시되어진 이모지를 지웁니다. 실행할까요?" confirmUploadEmojisDescription: "드래그 앤 드롭한 {count}개의 파일을 드라이브에 업로드 합니다. 실행할까요?" @@ -3001,6 +3073,7 @@ _bootErrors: otherOption1: "클라이언트 설정 및 캐시 삭제" otherOption2: "간편 클라이언트 실행" otherOption3: "복구 툴 실행" + otherOption4: "Misskey를 세이프 모드로 열기" _search: searchScopeAll: "전체" searchScopeLocal: "로컬" @@ -3009,3 +3082,132 @@ _search: pleaseEnterServerHost: "서버의 호스트를 입력해 주세요." pleaseSelectUser: "유저를 선택해주세요" serverHostPlaceholder: "예: misskey.example.com" +_serverSetupWizard: + installCompleted: "Misskey의 설치가 완료됐습니다!" + firstCreateAccount: "먼저 관리자 계정을 만듭시다." + accountCreated: "관리자 계정이 만들어졌습니다!" + serverSetting: "서버 설정" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "이 위자드로 쉽게 최적화된 서버의 설정을 할 수 있습니다." + settingsYouMakeHereCanBeChangedLater: "이 설정은 나중에 변경 가능합니다." + howWillYouUseMisskey: "Misskey를 어떻게 사용하십니까?" + _use: + single: "1인 서버" + single_description: "자신 전용 서버로 혼자서 사용" + single_youCanCreateMultipleAccounts: "1인 서버로 운영하는 경우에도 계정은 필요에 따라 여러 개 만들 수 있습니다." + group: "그룹 서버" + group_description: "신뢰 가능한 다른 유저를 초대해 여러 명이 사용" + open: "오픈 서버" + open_description: "불특정 다수의 유저를 받아들이는 운영을 함" + openServerAdvice: "불특정 다수의 유저를 받아들이는 것에는 위험이 따릅니다. 문제에 대처할 수 있도록 확실한 조정 체제로 운영하는 것을 권장합니다." + openServerAntiSpamAdvice: "자신의 서버가 스팸으로 사용되지 않게끔 reCAPTCHA라는 안티 봇 기능을 활성화하는 등 보안에 대해서도 세심한 주의가 필요합니다." + howManyUsersDoYouExpect: "어느 정도의 인원으로 생각 중이십니까?" + _scale: + small: "100명 이하(소규모)" + medium: "100명 이상 1000명 이하(중간 규모)" + large: "1000명 이상(대규모)" + largeScaleServerAdvice: "대규모 서버에서는 부하분산이나 데이터베이스의 복제 등 높은 인프라스트럭처 지식이 필요할 수 있습니다." + doYouConnectToFediverse: "Fediverse에 접속하시겠습니까?" + doYouConnectToFediverse_description1: "분산형 서버로 구성된 네트워크(Fediverse)에 접속하면 다른 서버와 서로 콘텐츠의 주고받기를 할 수 있습니다." + doYouConnectToFediverse_description2: "Fediverse에 접속하는 것을 '연합'이라고도 부릅니다." + youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다." + remoteContentsCleaning: "리모트 콘텐츠 자동 정리" + remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다." + adminInfo: "관리자 정보" + adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다." + adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다." + followingSettingsAreRecommended: "아래의 설정이 권장됩니다." + applyTheseSettings: "이 설정을 적용" + skipSettings: "설정 건너뛰기" + settingsCompleted: "설정이 완료됐습니다!" + settingsCompleted_description: "수고하셨습니다. 준비를 마쳤으므로 바로 서버의 이용을 시작하실 수 있습니다." + settingsCompleted_description2: "상세한 서버 설정은 '제어판'에서 하실 수 있습니다." + donationRequest: "기부 요청" + _donationRequest: + text1: "Misskey는 자원봉사자들에 의해 개발되는 무료 소프트웨어입니다." + text2: "앞으로도 계속해서 개발을 할 수 있도록 괜찮으시다면 부디 기부를 부탁드립니다." + text3: "지원자 대상 특전도 있습니다!" +_uploader: + editImage: "이미지 편집" + compressedToX: "{x}로 압축" + savedXPercent: "{x}% 절약" + abortConfirm: "업로드되지 않은 파일이 있습니다만, 그만 두시겠습니까?" + doneConfirm: "업로드되지 않은 파일이 있습니다만, 완료하시겠습니까?" + maxFileSizeIsX: "업로드 가능한 최대 파일 크기는 {x}입니다." + allowedTypes: "업로드 가능한 파일 유형" + tip: "파일은 아직 업로드되지 않았습니다. 이 다이얼로그에서 업로드 전의 확인, 이름 바꾸기, 압축, 자르기 등을 하실 수 있습니다. 준비가 되셨다면 '업로드' 버튼을 클릭해 업로드를 시작하실 수 있습니다." +_clientPerformanceIssueTip: + title: "배터리 소비가 심하다고 생각되시면" + makeSureDisabledAdBlocker: "광고 차단을 비활성화해 주십시오." + makeSureDisabledAdBlocker_description: "광고 차단은 성능에 영향을 미칠 수 있습니다. OS의 기능이나 브라우저의 기능, 애드온 등으로 광고 차단이 활성화돼있지 않은지 확인해 주십시오." + makeSureDisabledCustomCss: "커스텀 CSS를 무효로 해주십시오." + makeSureDisabledCustomCss_description: "스타일을 덮어쓰기하면 성능에 영향을 미칠 수 있습니다. 커스텀 CSS나 스타일을 덮어쓰기하는 확장 기능이 유효로 돼있는지 확인해주십시오." + makeSureDisabledAddons: "확장 기능을 비활성화해 주십시오." + makeSureDisabledAddons_description: "일부 확장 기능은 클라이언트의 동작에 간섭해 성능에 영향을 미칠 수 있습니다. 브라우저의 확장 기능을 비활성화해 개선할지 확인해주십시오." +_clip: + tip: "클립은 노트를 정리할 수 있는 기능입니다." +_userLists: + tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다." +watermark: "워터마크" +defaultPreset: "기본 프리셋" +_watermarkEditor: + tip: "이미지에 크레딧 정보 등의 워터마크를 추가할 수 있습니다." + quitWithoutSaveConfirm: "보존하지 않고 종료하시겠습니까?" + driveFileTypeWarn: "이 파이" + driveFileTypeWarnDescription: "이미지 파일을 선택해주십시오." + title: "워터마크 편집" + cover: "전체에 붙이기" + repeat: "전면에 깔기" + opacity: "불투명도" + scale: "크기" + text: "텍스트" + position: "위치" + type: "종류" + image: "이미지" + advanced: "고급" + stripe: "줄무늬" + stripeWidth: "라인의 폭" + stripeFrequency: "라인의 수" + angle: "각도" + polkadot: "물방울 무늬" + checker: "체크 무늬" + polkadotMainDotOpacity: "주요 물방울의 불투명도" + polkadotMainDotRadius: "주요 물방울의 크기" + polkadotSubDotOpacity: "서브 물방울의 불투명도" + polkadotSubDotRadius: "서브 물방울의 크기" + polkadotSubDotDivisions: "서브 물방울의 수" +_imageEffector: + title: "이펙트" + addEffect: "이펙트를 추가" + discardChangesConfirm: "변경을 취소하고 종료하시겠습니까?" + _fxs: + chromaticAberration: "색수차" + glitch: "글리치" + mirror: "미러" + invert: "색 반전" + grayscale: "흑백" + colorAdjust: "색조 보정" + colorClamp: "색 압축" + colorClampAdvanced: "색 압축(고급)" + distort: "뒤틀림" + threshold: "이진화" + zoomLines: "집중선" + stripe: "줄무늬" + polkadot: "물방울 무늬" + checker: "체크 무늬" + blockNoise: "노이즈 방지" + tearing: "티어링" +drafts: "초안" +_drafts: + select: "초안 선택" + cannotCreateDraftAnymore: "초안 작성 가능 수를 초과했습니다." + cannotCreateDraft: "이 내용으로는 초안을 작성할 수 없습니다. " + delete: "초안 삭제\n" + deleteAreYouSure: "초안을 삭제하시겠습니까?" + noDrafts: "초안 없음\n" + replyTo: "{user}에 회신" + quoteOf: "{user} 노트에 인용" + postTo: "{channel}에 게시" + saveToDraft: "초안에 저장" + restoreFromDraft: "초안에서 복원\n" + restore: "복원" + listDrafts: "초안 목록" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 455a71f302..aec49a2965 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -433,6 +433,7 @@ _cw: _visibility: home: "ໜ້າຫຼັກ" followers: "ຜູ້ຕິດຕາມ" + specified: "ໂພສ Direct note" _profile: name: "ຊື່" username: "ຊື່ຜູ້ໃຊ້" @@ -470,6 +471,7 @@ _deck: list: "ລາຍການ" channel: "ຊ່ອງ" mentions: "ກ່າວເຖິງເຈົ້າ" + direct: "ໂພສ Direct note" _webhookSettings: name: "ຊື່" _abuseReport: @@ -483,3 +485,5 @@ _remoteLookupErrors: title: "ບໍ່ພົບ" _search: searchScopeAll: "ທັງໝົດ" +_watermarkEditor: + image: "ຮູບພາບ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 1fc4342e92..e4efbc7e39 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -1019,6 +1019,7 @@ _cw: _visibility: home: "Startpagina" followers: "Volgers" + specified: "Directe notities" _profile: name: "Naam" username: "Gebruikersnaam" @@ -1061,6 +1062,7 @@ _deck: list: "Lijsten" channel: "Kanalen" mentions: "Vermeldingen" + direct: "Directe notities" _webhookSettings: name: "Naam" active: "Ingeschakeld" @@ -1078,3 +1080,6 @@ _remoteLookupErrors: title: "Niet gevonden" _search: searchScopeAll: "Alle" +_watermarkEditor: + image: "Afbeeldingen" + advanced: "Geavanceerd" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 578183efa5..a38237208a 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -461,6 +461,8 @@ replies: "Svar" renotes: "Renote" surrender: "Avbryt" information: "Informasjon" +inMinutes: "Minutter" +inDays: "Dager" _chat: invitations: "Inviter" members: "Medlemmer" @@ -735,3 +737,8 @@ _remoteLookupErrors: title: "Ikke funnet" _search: searchScopeAll: "Alle" +_watermarkEditor: + scale: "Størrelse" + text: "Tekst" + type: "Type" + image: "Bilder" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 38ccad9835..1edd803972 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1040,6 +1040,8 @@ surrender: "Odrzuć" gameRetry: "Spróbuj ponownie" postForm: "Formularz tworzenia wpisu" information: "Informacje" +inMinutes: "minuta" +inDays: "dzień" _chat: invitations: "Zaproś" noHistory: "Brak historii" @@ -1231,7 +1233,6 @@ _theme: buttonBg: "Tło przycisku" buttonHoverBg: "Tło przycisku (po najechaniu)" inputBorder: "Obramowanie pola wejścia" - driveFolderBg: "Tło folderu na dysku" badge: "Odznaka" messageBg: "Tło czatu" fgHighlighted: "Wyróżniony tekst" @@ -1585,3 +1586,10 @@ _remoteLookupErrors: _search: searchScopeAll: "Wszystkie" searchScopeLocal: "Lokalne" +_watermarkEditor: + opacity: "Przezroczystość" + scale: "Rozmiar" + text: "Tekst" + type: "Typ" + image: "Zdjęcia" + advanced: "Zaawansowane" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 57978509d3..5fdf4f8258 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -143,9 +143,9 @@ deleteFile: "Excluir arquivo" markAsSensitive: "Marcar como sensível" unmarkAsSensitive: "Desmarcar como sensível" enterFileName: "Digite o nome do arquivo" -mute: "Mutar" +mute: "Silenciar" unmute: "Desmutar" -renoteMute: "Mutar repostagens" +renoteMute: "Silenciar repostagens" renoteUnmute: "Reativar repostagens" block: "Bloquear" unblock: "Desbloquear" @@ -220,6 +220,7 @@ silenceThisInstance: "Silenciar essa instância" mediaSilenceThisInstance: "Silenciar a mídia dessa instância" operations: "Operações" software: "Software" +softwareName: "Software" version: "Versão" metadata: "Metadados" withNFiles: "{n} arquivo(s)" @@ -297,6 +298,7 @@ uploadFromUrl: "Enviar por URL" uploadFromUrlDescription: "URL do arquivo que você deseja enviar" uploadFromUrlRequested: "Upload solicitado" uploadFromUrlMayTakeTime: "Pode levar algum tempo para que o upload seja concluído." +uploadNFiles: "Enviar {n} arquivos" explore: "Explorar" messageRead: "Lida" noMoreHistory: "Não existe histórico anterior" @@ -325,6 +327,7 @@ dark: "Escuro" lightThemes: "Tema claro" darkThemes: "Tema escuro" syncDeviceDarkMode: "Sincronize com o modo escuro do dispositivo" +switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" está ativado. Você gostaria de desligar a sincronização e alterar manualmente?" drive: "Drive" fileName: "Nome do Ficheiro" selectFile: "Selecione os arquivos" @@ -443,7 +446,7 @@ exploreUsersCount: "Há um utilizador de {count}" exploreFediverse: "Explorar Fediverse" popularTags: "Tags populares" userList: "Listas" -about: "Informações" +about: "Sobre" aboutMisskey: "Sobre Misskey" administrator: "Administrador" token: "Símbolo" @@ -574,8 +577,10 @@ showFixedPostForm: "Exibir o formulário de postagem na parte superior da linha showFixedPostFormInChannel: "Exibir o campo de postagem na parte superior da linha do tempo (canais)" withRepliesByDefaultForNewlyFollowed: "Incluir respostas por usuários recém-seguidos na linha do tempo por padrão" newNoteRecived: "Nova nota recebida" +newNote: "Nova Nota" sounds: "Sons" sound: "Sons" +notificationSoundSettings: "Configurações de som de notificações" listen: "Ouvir" none: "Nenhum" showInPage: "Ver na página" @@ -790,6 +795,7 @@ wide: "Largo" narrow: "Estreito" reloadToApplySetting: "As configurações serão refletidas após recarregar a página. Deseja recarregar agora?" needReloadToApply: "É necessário recarregar a página para refletir as alterações." +needToRestartServerToApply: "É necessário reiniciar o servidor para aplicar as mudanças." showTitlebar: "Exibir barra de título" clearCache: "Limpar o cache" onlineUsersCount: "{n} Pessoas Online" @@ -996,6 +1002,7 @@ failedToUpload: "Falha ao enviar" cannotUploadBecauseInappropriate: "Esse arquivo não pôde ser enviado porque partes dele foram detectadas como potencialmente inapropriadas." cannotUploadBecauseNoFreeSpace: "Envio falhou devido à falta de capacidade no Drive." cannotUploadBecauseExceedsFileSizeLimit: "Não é possível realizar o upload deste arquivo porque ele excede o tamanho máximo permitido." +cannotUploadBecauseUnallowedFileType: "Não foi possível fazer o envio, pois o formato do arquivo não foi autorizado." beta: "Beta" enableAutoSensitive: "Marcar automaticamente como conteúdo sensível" enableAutoSensitiveDescription: "Quando disponível, a marcação de mídia sensível será automaticamente atribuído ao conteúdo de mídia usando aprendizado de máquina. Mesmo que você desative essa função, em alguns servidores, isso pode ser configurado automaticamente." @@ -1306,6 +1313,7 @@ availableRoles: "Cargos disponíveis" acknowledgeNotesAndEnable: "Ative após compreender as precauções." federationSpecified: "Esse servidor opera com uma lista branca de federação. Interagir com servidores diferentes daqueles designados pela administração não é permitido." federationDisabled: "Federação está desabilitada nesse servidor. Você não pode interagir com usuários de outros servidores." +draft: "Rascunhos" confirmOnReact: "Confirmar ao reagir" reactAreYouSure: "Você deseja adicionar uma reação \"{emoji}\"?" markAsSensitiveConfirm: "Você deseja definir essa mídia como sensível?" @@ -1323,6 +1331,7 @@ restore: "Redefinir" syncBetweenDevices: "Sincronizar entre dispositivos" preferenceSyncConflictTitle: "O valor configurado já existe no servidor." preferenceSyncConflictText: "As preferências com a sincronização ativada irão salvar os seus valores no servidor. Porém, já existem valores no servidor. Qual conjunto de valores você deseja sobrescrever?" +preferenceSyncConflictChoiceMerge: "Combinar" preferenceSyncConflictChoiceServer: "Valor configurado no servidor" preferenceSyncConflictChoiceDevice: "Valor configurado no dispositivo" preferenceSyncConflictChoiceCancel: "Cancelar a habilitação de sincronização" @@ -1330,7 +1339,7 @@ paste: "Colar" emojiPalette: "Paleta de emojis" postForm: "Campo de postagem" textCount: "Contagem de caracteres" -information: "Informações" +information: "Sobre" chat: "Conversas" migrateOldSettings: "Migrar configurações antigas de cliente" migrateOldSettings_description: "Isso deve ser feito automaticamente. Caso o processo de migração tenha falhado, você pode acioná-lo manualmente. As informações atuais de migração serão substituídas." @@ -1343,6 +1352,27 @@ settingsMigrating: "Configurações estão sendo migradas, aguarde... (Você pod readonly: "Ler apenas" goToDeck: "Voltar ao Deck" federationJobs: "Tarefas de Federação" +driveAboutTip: "No Drive, uma lista de arquivos enviados no passado será exibida.
\nVocê pode reutilizar esses arquivos anexando-os às notas, ou você pode enviar arquivos para publicar posteriormente.
\nCuidado ao excluir um arquivo, pois ele será removido de quaisquer outros lugares onde está sendo utilizado (notas, páginas, avatares, banners, etc.)
\nVocê também pode criar pastas para organizar seus arquivos." +scrollToClose: "Role a página para fechar" +advice: "Dica" +realtimeMode: "Modo tempo-real" +turnItOn: "Ativar" +turnItOff: "Desativar" +emojiMute: "Silenciar emoji" +emojiUnmute: "Reativar emoji" +muteX: "Silenciar {x}" +unmuteX: "Reativar {x}" +abort: "Abortar" +tip: "Dicas e Truques" +redisplayAllTips: "Mostrar todas as \"Dicas e Truques\" novamente" +hideAllTips: "Ocultas todas as \"Dicas e Truques\"" +defaultImageCompressionLevel: "Nível de compressão de imagem padrão" +defaultImageCompressionLevel_description: "Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem.
Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem." +inMinutes: "Minuto(s)" +inDays: "Dia(s)" +_order: + newest: "Priorizar Mais Novos" + oldest: "Priorizar Mais Antigos" _chat: noMessagesYet: "Ainda não há mensagens" newMessage: "Nova mensagem" @@ -1376,6 +1406,8 @@ _chat: chatNotAvailableInOtherAccount: "A função de conversas está desabilitadas para o outro usuário." cannotChatWithTheUser: "Não é possível conversar com esse usuário." cannotChatWithTheUser_description: "Conversas estão indisponíveis ou o outro usuário não as habilitou." + youAreNotAMemberOfThisRoomButInvited: "Você não é um participante da sala, mas recebeu um convite. Por favor, aceite o convite para entrar." + doYouAcceptInvitation: "Aceita o convite?" chatWithThisUser: "Conversar com usuário" thisUserAllowsChatOnlyFromFollowers: "Esse usuário aceita conversar apenas com seguidores." thisUserAllowsChatOnlyFromFollowing: "Esse usuário aceita conversar apenas com quem segue." @@ -1415,10 +1447,20 @@ _settings: makeEveryTextElementsSelectable: "Tornar todos os elementos de texto selecionáveis" makeEveryTextElementsSelectable_description: "Habilitar isso pode reduzir a usabilidade em algumas situações" useStickyIcons: "Fazer ícones acompanharem a rolagem da tela" + enableHighQualityImagePlaceholders: "Exibir prévias para imagens de alta qualidade" + uiAnimations: "Animações de UI" showNavbarSubButtons: "Mostrar sub-botões na barra de navegação" ifOn: "Quando ligado" ifOff: "Quando desligado" enableSyncThemesBetweenDevices: "Sincronizar temas instalados entre dispositivos" + enablePullToRefresh: "Puxe para atualizar" + enablePullToRefresh_description: "Quando estiver utilizando um mouse, arraste enquanto aperta a roda de rolagem." + realtimeMode_description: "Estabelece uma conexão com o servidor e atualiza o conteúdo em tempo real. Isso pode aumentar o tráfego e uso de memória." + contentsUpdateFrequency: "Frequência da obtenção de conteúdo" + contentsUpdateFrequency_description: "Quanto maior o valor, mais o conteúdo atualiza. Porém, há uma diminuição do desempenho e aumento do tráfego e consumo de memória." + contentsUpdateFrequency_description2: "Quando o modo tempo-real está ativado, o conteúdo é atualizado em tempo real, ignorando essa opção." + showUrlPreview: "Exibir prévia de URL" + showAvailableReactionsFirstInNote: "Exibir reações disponíveis no topo." _chat: showSenderName: "Exibir nome de usuário do remetente" sendOnEnter: "Pressionar Enter para enviar" @@ -1426,6 +1468,7 @@ _preferencesProfile: profileName: "Nome do perfil" profileNameDescription: "Defina o nome que identifica esse dispositivo." profileNameDescription2: "Exemplo: \"Computador Principal\", \"Celular\"" + manageProfiles: "Gerenciar Perfis" _preferencesBackup: autoBackup: "Backup automático" restoreFromBackup: "Restaurar backup" @@ -1464,6 +1507,7 @@ _delivery: manuallySuspended: "Suspenso manualmente" goneSuspended: "Servidor foi suspenso devido ao seu apagamento" autoSuspendedForNotResponding: "Servidor foi suspenso por não responder" + softwareSuspended: "Suspenso, pois esse software não está recebendo conteúdo" _bubbleGame: howToPlay: "Como jogar" hold: "Próximos" @@ -1595,6 +1639,23 @@ _serverSettings: openRegistration: "Abrir a criação de contas" openRegistrationWarning: "Abrir cadastros contém riscos. É recomendado apenas habilitá-los se houver um sistema de monitoramento contínuo e resolução imediata de problemas." thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Se nenhuma atividade da moderação for detectada por um tempo, essa configuração será desativada para prevenir spam." + deliverSuspendedSoftware: "Software Suspenso" + deliverSuspendedSoftwareDescription: "Você pode especificar uma faixa de nomes e versões do software de servidores para cancelar o envio de conteúdo por motivos como vulnerabilidades. Essa informação da versão é providenciada pelo servidor e pode não ser confiável. Uma faixa semver pode ser utilizada para especificar a versão, mas colocar '>= 2024.3.1' não incluirá versões personalizadas como '2024.3.1-custom.0'. Logo, é recomendado inserir uma especificação como '>= 2024.3.1-0'" + singleUserMode: "Modo de usuário único" + singleUserMode_description: "Se você é o único usuário desse servidor, habilitar esse modo irá otimizar a performance." + signToActivityPubGet: "Assinar solicitações GET do ActivityPub" + signToActivityPubGet_description: "Normalmente, isso deve ser habilitado. Desabilitar pode melhorar o desempenho na federação, mas também pode cortar a federação com alguns servidores." + proxyRemoteFiles: "Passar arquivos remotos por proxy" + proxyRemoteFiles_description: "Se habilitado, o servidor irá servir arquivos remotos através de um proxy. Isso é útil para gerar prévias de imagens e proteger a privacidade do usuário." + allowExternalApRedirect: "Permitir redirecionamento de conteúdo pelo ActivityPub" + allowExternalApRedirect_description: "Se habilitado, outros servidores podem solicitar conteúdo de terceiros através desse servidor, o que pode resultar em falsificação de conteúdo (spoofing)." + userGeneratedContentsVisibilityForVisitor: "Visibilidade de conteúdo dos usuários para visitantes" + userGeneratedContentsVisibilityForVisitor_description: "Isso é útil para prevenir problemas causados por conteúdo inapropriado de usuários remotos de servidores com pouca ou nenhuma moderação, que pode ser hospedado na internet a partir desse servidor." + userGeneratedContentsVisibilityForVisitor_description2: "Publicar todo o conteúdo do servidor para a internet pode ser arriscado. Isso é especialmente importante para visitantes que desconhecem a natureza distribuída do conteúdo na internet, pois eles podem acreditar que o conteúdo remoto é criado por usuários desse servidor." + _userGeneratedContentsVisibilityForVisitor: + all: "Tudo é público" + localOnly: "Conteúdo local é publicado, conteúdo remoto é privado" + none: "Tudo é privado" _accountMigration: moveFrom: "Migrar outra conta para essa" moveFromSub: "Criar um 'alias' a outra conta" @@ -1912,6 +1973,7 @@ _role: canManageCustomEmojis: "Permitir gerenciar emojis personalizados" canManageAvatarDecorations: "Gerenciar decorações de avatar" driveCapacity: "Capacidade do drive" + maxFileSize: "Tamanho máximo de envio de arquivos" alwaysMarkNsfw: "Sempre marcar arquivos como NSFW" canUpdateBioMedia: "Permitir a edição de ícone ou imagem do banner." pinMax: "Número máximo de notas fixadas" @@ -1934,6 +1996,11 @@ _role: canImportMuting: "Permitir importação de silenciamentos" canImportUserLists: "Permitir importação de listas" chatAvailability: "Permitir Conversas" + uploadableFileTypes: "Tipos de arquivo enviáveis" + uploadableFileTypes_caption: "Especifica tipos MIME permitidos. Múltiplos tipos MIME podem ser especificados separando-os por linha. Curingas podem ser especificados com um asterisco (*). (exemplo, image/*)" + uploadableFileTypes_caption2: "Alguns tipos de arquivos podem não ser detectados. Para permiti-los, adicione {x} à especificação." + noteDraftLimit: "Limite de rascunhos possíveis" + watermarkAvailable: "Disponibilidade da função de marca d'água" _condition: roleAssignedTo: "Atribuído a cargos manuais" isLocal: "Usuário local" @@ -2093,6 +2160,7 @@ _theme: install: "Instalar um tema" manage: "Gerenciar temas" code: "Código do tema" + copyThemeCode: "Copiar código do tema" description: "Descrição" installed: "{name} foi instalado" installedThemes: "Temas instalados" @@ -2151,7 +2219,6 @@ _theme: buttonBg: "Plano de fundo de botão" buttonHoverBg: "Plano de fundo de botão (Selecionado)" inputBorder: "Borda de campo digitável" - driveFolderBg: "Plano de fundo da pasta no Drive" badge: "Emblema" messageBg: "Plano de fundo do chat" fgHighlighted: "Texto Destacado" @@ -2407,6 +2474,8 @@ _visibility: disableFederation: "Defederar" disableFederationDescription: "Não transmitir às outras instâncias" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "Há arquivos que não foram enviados, gostaria de descartá-los e fechar o editor?" + uploaderTip: "O arquivo ainda não foi enviado. No menu do arquivo, você pode renomear, cortar, adicionar uma marca d'água, comprimir ou descomprimir um arquivo. Arquivos serão enviados automaticamente ao publicar a nota." replyPlaceholder: "Responder a essa nota..." quotePlaceholder: "Citar essa nota..." channelPlaceholder: "Postar em canal..." @@ -2740,6 +2809,7 @@ _fileViewer: url: "URL" uploadedAt: "Adicionado em" attachedNotes: "Notas anexadas" + usage: "Usado" thisPageCanBeSeenFromTheAuthor: "Essa página só pode ser vista pelo usuário que enviou esse arquivo." _externalResourceInstaller: title: "Instalar de site externo" @@ -2787,9 +2857,12 @@ _dataSaver: _avatar: title: "Imagem do avatar" description: "Parar animação de avatares. Imagens animadas podem ter um arquivo mais pesado do que imagens normais, potencialmente levando a reduções no tráfego de dados." - _urlPreview: - title: "Miniaturas na prévia de URLs" - description: "Miniaturas na prévia de URLs não serão mais carregadas." + _urlPreviewThumbnail: + title: "Esconder miniaturas em prévias de URL" + description: "Miniaturas em prévias de URL não serão carregadas." + _disableUrlPreview: + title: "Desabilitar prévias de URL" + description: "Desabilita a função de prévias de URL. Diferente das miniaturas, essa função impede o carregamento de toda informação do link." _code: title: "Destaque de código" description: "Se as notações de formatação de código forem utilizadas em MFM, elas não irão carregar até serem selecionadas. Destaque de código exige baixar arquivos de alta definição para cada linguagem de programação. Logo, desabilitar o carregamento automático desses arquivos diminui a quantidade de informação comunicada." @@ -2847,6 +2920,8 @@ _offlineScreen: _urlPreviewSetting: title: "Configurações da prévia de URL" enable: "Habilitar prévia de URL" + allowRedirect: "Permitir redirecionamentos de URL em prévias." + allowRedirectDescription: "Se um URL tem um redirecionamento, você pode habilitar essa função para segui-lo e exibir a prévia do conteúdo redirecionado. Desabilitar isso irá economizar recursos, mas o conteúdo não será exibido." timeout: "Tempo máximo para obter a prévia (ms)" timeoutDescription: "Se demorar mais que esse valor para obter uma prévia, ela não será gerada." maximumContentLength: "Content-Length máximo (em bytes)" @@ -2920,10 +2995,6 @@ _customEmojisManager: uploadSettingDescription: "Nessa tela, você pode configurar o comportamento ao enviar Emojis." directoryToCategoryLabel: "Transformar as pastas em categorias" directoryToCategoryCaption: "Quando você arrastar um diretório, converter o caminho das pastas no campo \"categoria\"." - emojiInputAreaCaption: "Selecione Emojis que você deseja registrar utilizando um dos métodos." - emojiInputAreaList1: "Arraste arquivos de imagem ou diretórios dentro desse quadro" - emojiInputAreaList2: "Clique nesse link para abrir a seleção de arquivos" - emojiInputAreaList3: "Clique nesse link para selecionar do drive" confirmRegisterEmojisDescription: "Registrando os Emojis da lista como novos Emojis personalizados. Deseja continuar? (Para evitar sobrecarga, apenas {count} Emoji(s) podem ser registrados em uma única operação)" confirmClearEmojisDescription: "Descartando edições e limpando Emojis da lista. Deseja continuar?" confirmUploadEmojisDescription: "Enviando {count} arquivo(s) arrastados ao drive. Deseja continuar?" @@ -2999,3 +3070,130 @@ _search: pleaseEnterServerHost: "Insira o endereço do servidor" pleaseSelectUser: "Selecione um usuário" serverHostPlaceholder: "Exemplo: misskey.example.com" +_serverSetupWizard: + installCompleted: "Instalação do Misskey concluída!" + firstCreateAccount: "Para iniciar, crie uma conta de administrador." + accountCreated: "Conta de administrador foi criada!" + serverSetting: "Configurações de Servidor" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "O assistente facilita a configuração do servidor." + settingsYouMakeHereCanBeChangedLater: "Configurações alteradas pelo assistente podem ser ajustadas posteriormente." + howWillYouUseMisskey: "Como você usará o Misskey?" + _use: + single: "Servidor de Usuário Único" + single_description: "Utilizar servidor sozinho." + single_youCanCreateMultipleAccounts: "Múltiplas contas podem ser criadas se necessário, mesmo operando como servidor de usuário único." + group: "Servidor de Grupo" + group_description: "Convide outros usuários confiáveis para utilizar com mais de um usuário" + open: "Servidor Público" + open_description: "Permitir registro de todos." + openServerAdvice: "Aceitar um número alto de pessoas desconhecidas pode envolve um risco. Recomendamos que você opere com um sistema de moderação confiável para resolver quaisquer problemas." + openServerAntiSpamAdvice: "Para prevenir que o seu servidor se torne alvo de spam, é essencial cuidar da segurança habilitando recursos antibot como o reCAPTCHA." + howManyUsersDoYouExpect: "Quantos usuários você espera?" + _scale: + small: "Menos que 100 (pequeno porte)" + medium: "Entre 100 e 1000 usuários (médio porte)" + large: "Mais que 1000 usuários (larga escala)" + largeScaleServerAdvice: "Servidores de larga escala podem precisar de conhecimento avançado de infraestrutura, como balanceamento de carga e replicação de banco de dados." + doYouConnectToFediverse: "Você deseja conectar-se com o Fediverso?" + doYouConnectToFediverse_description1: "Quando conectado com uma rede distribuída de servidores (Fediverso), o conteúdo pode ser trocado com outros servidores." + doYouConnectToFediverse_description2: "Conectar com o Fediverso também é chamado de \"federação\"" + youCanConfigureMoreFederationSettingsLater: "Configurações adicionais como especificar servidores para conectar-se com podem ser feitas posteriormente" + adminInfo: "Informações da administração" + adminInfo_description: "Define as informações do administrador usadas para receber consultas." + adminInfo_mustBeFilled: "Deve ser preenchido se o servidor é público ou se a federação está ativa." + followingSettingsAreRecommended: "As configurações a seguir são recomendadas" + applyTheseSettings: "Aplicar essas configurações" + skipSettings: "Pular configuração" + settingsCompleted: "Instalação concluída!" + settingsCompleted_description: "Obrigado pelo seu tempo. Agora que tudo está pronto, você pode começar a utilizar o servidor." + settingsCompleted_description2: "As configurações do servidor podem ser alteradas no \"Painel de Controle\"" + donationRequest: "Solicitação de Doação" + _donationRequest: + text1: "Misskey é software aberto desenvolvido por voluntários." + text2: "Nós apreciaríamos o seu apoio para podermos continuar o desenvolvimento desse software no futuro." + text3: "Também há benefícios especiais para apoiadores!" +_uploader: + editImage: "Editar Imagem" + compressedToX: "Comprimido para {x}" + savedXPercent: "Salvando {x}%" + abortConfirm: "Alguns arquivos não foram enviados, deseja abortar?" + doneConfirm: "Alguns arquivos não foram enviados, deseja continuar mesmo assim?" + maxFileSizeIsX: "O tamanho máximo de arquivos enviados é {x}" + allowedTypes: "Tipos de arquivo enviáveis" + tip: "O arquivo não foi enviado. Então, esse diálogo permite que você confirme, renomeie, comprima e recorte o arquivo antes de enviar. Quando estiver pronto, você pode enviar apertando o botão \"Enviar\"." +_clientPerformanceIssueTip: + title: "Dicas de desempenho" + makeSureDisabledAdBlocker: "Desative o seu bloqueador de anúncios" + makeSureDisabledAdBlocker_description: "Bloqueadores de anúncios podem afetar o desempenho. Certifique-se que eles não estão habilitados no seu sistema ou nos recursos/extensões do navegador. " + makeSureDisabledCustomCss: "Desabilite CSS personalizado" + makeSureDisabledCustomCss_description: "Substituir o estilo da página pode afetar o desempenho. Certifique-se que o CSS personalizado ou extensões que modifiquem o estilo da página estejam desabilitados." + makeSureDisabledAddons: "Desabilite extensões" + makeSureDisabledAddons_description: "Algumas extensões podem afetar comportamentos do cliente e afetar o desempenho. Por favor, desative as extensões do seu navegador e veja se isso melhora a situação." +_clip: + tip: "Clip é uma função que permite organização das suas notas." +_userLists: + tip: "Listas podem conter qualquer usuário que você especificar em sua criação. A lista criada aparece como uma linha do tempo exibindo usuários selecionados." +watermark: "Marca d'água" +defaultPreset: "Predefinição Padrão" +_watermarkEditor: + tip: "Uma marca d'água, como informação de autoria, pode ser adicionada à imagem." + quitWithoutSaveConfirm: "Descartar mudanças?" + driveFileTypeWarn: "Esse arquivo não é compatível" + driveFileTypeWarnDescription: "Escolha um arquivo de imagem" + title: "Editar marca d'água" + cover: "Cobrir tudo" + repeat: "Espalhar pelo conteúdo" + opacity: "Opacidade" + scale: "Tamanho" + text: "Texto" + position: "Posição" + type: "Tipo" + image: "imagem" + advanced: "Avançado" + stripe: "Listras" + stripeWidth: "Largura da linha" + stripeFrequency: "Número de linhas" + angle: "Ângulo" + polkadot: "Bolinhas" + checker: "Xadrez" + polkadotMainDotOpacity: "Opacidade da bolinha principal" + polkadotMainDotRadius: "Raio da bolinha principal" + polkadotSubDotOpacity: "Opacidade da bolinha secundária" + polkadotSubDotRadius: "Raio das bolinhas adicionais" + polkadotSubDotDivisions: "Número de bolinhas adicionais" +_imageEffector: + title: "Efeitos" + addEffect: "Adicionar efeitos" + discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas." + _fxs: + chromaticAberration: "Aberração cromática" + glitch: "Glitch" + mirror: "Espelho" + invert: "Inverter Cores" + grayscale: "Tons de Cinza" + colorAdjust: "Correção de Cores" + colorClamp: "Compressão de Cores" + colorClampAdvanced: "Compressão Avançada de Cores" + distort: "Distorção" + threshold: "Limiarização Binária" + zoomLines: "Linhas de Ação" + stripe: "Listras" + polkadot: "Bolinhas" + checker: "Xadrez" + blockNoise: "Bloquear Ruído" + tearing: "Descontinuidade" +drafts: "Rascunhos" +_drafts: + select: "Selecionar Rascunho" + cannotCreateDraftAnymore: "O número máximo de rascunhos foi excedido." + cannotCreateDraft: "Você não pode criar um rascunho com esse conteúdo." + delete: "Excluir Rascunho" + deleteAreYouSure: "Excluir rascunho?" + noDrafts: "Sem rascunhos" + replyTo: "Resposta a {user}" + quoteOf: "Citação à nota de {user}" + postTo: "Publicando em {channel}" + saveToDraft: "Salvar como Rascunho" + restoreFromDraft: "Restaurar de Rascunho" + restore: "Redefinir" + listDrafts: "Lista de Rascunhos" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index abfaac7121..f07e4d8d2f 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -1302,6 +1302,7 @@ _cw: _visibility: home: "Acasă" followers: "Urmăritori" + specified: "Note directe" _postForm: replyPlaceholder: "Răspunde la această notă..." quotePlaceholder: "Citează aceasta nota..." @@ -1356,6 +1357,7 @@ _deck: list: "Liste" channel: "Canale" mentions: "Mențiuni" + direct: "Note directe" roleTimeline: "Cronologia rolului" _webhookSettings: name: "Nume" @@ -1391,3 +1393,10 @@ _search: searchScopeLocal: "Local" searchScopeUser: "Utilizator specific" serverHostPlaceholder: "Exemplu: misskey.example.com" +_watermarkEditor: + scale: "Dimensiune" + text: "Text" + position: "Poziție" + type: "Tip" + image: "Imagini" + advanced: "Avansat" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 5851530786..375b46c3e9 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -2,7 +2,7 @@ _lang_: "Русский" headlineMisskey: "Сеть, сплетённая из заметок" introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀" -poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом Misskey, называемый экземпляром Misskey." +poweredByMisskeyDescription: "{name} – один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом Misskey." monthAndDay: "{day}.{month}" search: "Поиск" reset: "Сброс" @@ -82,7 +82,7 @@ export: "Экспорт" files: "Файлы" download: "Скачать" driveFileDeleteConfirm: "Удалить файл «{name}»? Заметки с ним также будут удалены." -unfollowConfirm: "Удалить из подписок пользователя {name}?" +unfollowConfirm: "Отписаться от {name} ?" exportRequested: "Вы запросили экспорт. Это может занять некоторое время. Результат будет добавлен на «Диск»." importRequested: "Вы запросили импорт. Это может занять некоторое время." lists: "Списки" @@ -298,6 +298,7 @@ uploadFromUrl: "Загрузить по ссылке" uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить" uploadFromUrlRequested: "Загрузка выбранного" uploadFromUrlMayTakeTime: "Загрузка может занять некоторое время." +uploadNFiles: "Загрузить {n} файл" explore: "Обзор" messageRead: "Прочитали" noMoreHistory: "История закончилась" @@ -575,8 +576,10 @@ showFixedPostForm: "Показывать поле для ввода новой showFixedPostFormInChannel: "Показывать поле для ввода новой заметки наверху ленты (каналы)" withRepliesByDefaultForNewlyFollowed: "По умолчанию включайте ответы новых пользователей, на которых вы подписались, во временную шкалу" newNoteRecived: "Появилась новая заметка" +newNote: "Новая заметка" sounds: "Звуки" sound: "Звуки" +notificationSoundSettings: "Настройки звука уведомлений" listen: "Слушать" none: "Ничего" showInPage: "Показать страницу" @@ -791,6 +794,7 @@ wide: "Толстый" narrow: "Тонкий" reloadToApplySetting: "Это настройка вступает в силу при загрузке страницы. Перезагрузить сейчас?" needReloadToApply: "Изменения вступят в силу после перезагрузки страницы." +needToRestartServerToApply: "Для вступления изменений в силу необходимо перезапустить сервер." showTitlebar: "Показать заголовок" clearCache: "Очистить кэш" onlineUsersCount: "Пользователей сейчас в сети: {n}" @@ -1176,13 +1180,25 @@ unused: "Неиспользованное" used: "Использован" expired: "Срок действия приглашения истёк" doYouAgree: "Согласны?" +beSureToReadThisAsItIsImportant: "Это важно, поэтому, пожалуйста, прочтите это." +iHaveReadXCarefullyAndAgree: "Я прочитал(а) и согласен(сна) с условиями \"{x}" +dialog: "Диалог" icon: "Аватар" +currentAnnouncements: "Текущие новости" +pastAnnouncements: "Предыдущие новости" +youHaveUnreadAnnouncements: "У вас есть непрочитанные уведомления" replies: "Ответы" renotes: "Репост" loadReplies: "Показать ответы" +loadConversation: "Загрузить беседу" pinnedList: "Закреплённый список" keepScreenOn: "Держать экран включённым" +unnotifyNotes: "Отписаться от сообщений" +authentication: "Аутентификация" +authenticationRequiredToContinue: "Пожалуйста, пройдите аутентификацию, чтобы продолжить" +dateAndTime: "Дата и время" showRenotes: "Показывать репосты" +edited: "Изменено" mutualFollow: "Взаимные подписки" followingOrFollower: "Подписки или подписчики" fileAttachedOnly: "Только заметки с файлами" @@ -1193,30 +1209,71 @@ sourceCode: "Исходный код" sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему." repositoryUrl: "Ссылка на репозиторий" repositoryUrlDescription: "Если вы используете Misskey как есть (без изменений в исходном коде), введите https://github.com/misskey-dev/misskey" +feedback: "Обратная связь" privacyPolicy: "Политика Конфиденциальности" privacyPolicyUrl: "Ссылка на Политику Конфиденциальности" +tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности" +avatarDecorations: "Украшения для аватара" attach: "Прикрепить" angle: "Угол" flip: "Переворот" +showAvatarDecorations: "Показать украшения для аватара" +pullDownToRefresh: "Опустите что бы обновить" useGroupedNotifications: "Отображать уведомления сгруппировано" +signupPendingError: "Возникла проблема с подтверждением вашего адреса электронной почты. Возможно, срок действия ссылки истёк." +cwNotationRequired: "Если включена опция «Скрыть содержимое», необходимо написать аннотацию." doReaction: "Добавить реакцию" code: "Код" +reloadRequiredToApplySettings: "Для применения настроек необходима обновить страницу." remainingN: "Остаётся: {n}" +overwriteContentConfirm: "Текущее содержимое будет перезаписано. Вы уверены?" seasonalScreenEffect: "Эффект времени года на экране" decorate: "Украсить" addMfmFunction: "Добавить MFM" +bubbleGame: "BubbleGame" +sfx: "Звуковые эффекты" +soundWillBePlayed: "Будет воспроизведен звук" +showReplay: "Показать повтор" +endReplay: "Конец повтора" lastNDays: "Последние {n} сут" hemisphere: "Место проживания" +userSaysSomethingSensitive: "Сообщение, содержит конфиденциальные файлы от {name}" enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки" surrender: "Этот пост не может быть отменен." +gameRetry: "Повторить попытку" +notUsePleaseLeaveBlank: "Если не используется, оставьте пустым" useNativeUIForVideoAudioPlayer: "Использовать интерфейс браузера при проигрывании видео и звука" keepOriginalFilename: "Сохранять исходное имя файла" keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке." alwaysConfirmFollow: "Всегда подтверждать подписку" inquiry: "Связаться" +fromX: "Из {x}" +genEmbedCode: "Сгенерировать код для " +noteOfThisUser: "Список заметок этого пользователя" +clipNoteLimitExceeded: "К этому клипу больше нельзя добавить заметки" +performance: "Производительность" +modified: "Изменено" +signinWithPasskey: "Войдите в систему, используя свой пароль" +unknownWebAuthnKey: "Не известный ключ " +passkeyVerificationFailed: "Ошибка проверка ключа доступа " messageToFollower: "Сообщение подписчикам" +testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. Не использовать это в рабочей среде" +prohibitedWordsForNameOfUser: "Запрещенные слова (имя пользователя)" +prohibitedWordsForNameOfUserDescription: "Если имя пользователя содержит строку из этого списка, изменение имени пользователя будет запрещено. На пользователей с правами модератора это ограничение не распространяется. Имена пользователей также проверяются путём замены всех букв в нижнем регистре" +yourNameContainsProhibitedWords: "Имя, которое вы пытаетесь изменить, содержит запрещенную строку символов" +yourNameContainsProhibitedWordsDescription: "Имя содержит запрещённую строку символов. Если вы хотите использовать это имя, обратитесь к администратору сервера" +thisContentsAreMarkedAsSigninRequiredByAuthor: "Автор сообщения установил требование в виде авторизации для просмотра" +lockdown: "Доступ ограничен" +pleaseSelectAccount: "Выберите свой аккаунт" +availableRoles: "Доступные роли" +federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах." +draft: "Черновик" +markAsSensitiveConfirm: "Отметить контент как чувствительный?" +resetToDefaultValue: "Сбросить настройки до стандартных" postForm: "Форма отправки" information: "Описание" +inMinutes: "мин" +inDays: "сут" _chat: invitations: "Пригласить" noHistory: "История пока пуста" @@ -1748,7 +1805,6 @@ _theme: buttonBg: "Фон кнопки" buttonHoverBg: "Текст кнопки" inputBorder: "Рамка поля ввода" - driveFolderBg: "Фон папки «Диска»" badge: "Значок" messageBg: "Фон беседы" fgHighlighted: "Подсвеченный текст" @@ -2192,3 +2248,13 @@ _search: searchScopeAll: "Все" searchScopeLocal: "Местная" searchScopeUser: "Указанный пользователь" +_watermarkEditor: + opacity: "Непрозрачность" + scale: "Размер" + text: "Текст" + position: "Позиция" + type: "Тип" + image: "Изображения" + advanced: "Для продвинутых" + angle: "Угол" +drafts: "Черновик" diff --git a/locales/si-LK.yml b/locales/si-LK.yml index c43f3d860d..841fb10585 100644 --- a/locales/si-LK.yml +++ b/locales/si-LK.yml @@ -1,10 +1,18 @@ --- _lang_: "සිංහල" monthAndDay: "{month}-{day}" +search: "සොයන්න" +reset: "යළි සකසන්න" +notifications: "දැනුම්දීම්" username: "පරිශීලක නාමය" password: "මුරපදය" +ok: "හරි" +gotIt: "තේරුණා" cancel: "අවලංගු කරන්න" +noThankYou: "එපා, ස්තුතියි" +noNotifications: "දැනුම්දීම් නැත" instance: "සර්වර්" +settings: "සැකසුම්" login: "පිවිසෙන්න" users: "පරිශීලක" note: "නෝට්" @@ -13,10 +21,19 @@ instances: "සර්වර්" smtpUser: "පරිශීලක නාමය" smtpPass: "මුරපදය" user: "පරිශීලක" +searchByGoogle: "සොයන්න" _sfx: note: "නෝට්" + notification: "දැනුම්දීම්" +_2fa: + renewTOTPCancel: "එපා, ස්තුතියි" +_widgets: + notifications: "දැනුම්දීම්" _profile: username: "පරිශීලක නාමය" _notification: _types: login: "පිවිසෙන්න" +_deck: + _columns: + notifications: "දැනුම්දීම්" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 9877080f0c..80a1f2f0a9 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -913,6 +913,8 @@ flip: "Preklopiť" lastNDays: "Posledných {n} dní" postForm: "Napísať poznámku" information: "Informácie" +inMinutes: "min" +inDays: "dní" _chat: invitations: "Pozvať" noHistory: "Žiadna história" @@ -1108,7 +1110,6 @@ _theme: buttonBg: "Pozadie tlačidla" buttonHoverBg: "Pozadie tlačidla (pod kurzorom)" inputBorder: "Okraj vstupného poľa" - driveFolderBg: "Pozadie priečinu disku" badge: "Odznak" messageBg: "Pozadie chatu" fgHighlighted: "Zvýraznený text" @@ -1451,3 +1452,10 @@ _remoteLookupErrors: _search: searchScopeAll: "Všetko" searchScopeLocal: "Lokálne" +_watermarkEditor: + opacity: "Priehľadnosť" + scale: "Veľkosť" + text: "Text" + type: "Typ" + image: "Obrázky" + advanced: "Rozšírené" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index ba6d8a93d2..95947607cb 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -646,6 +646,7 @@ _poll: _visibility: home: "Hem" followers: "Följare" + specified: "Direktnoter" _profile: name: "Namn" username: "Användarnamn" @@ -692,6 +693,7 @@ _deck: list: "Listor" channel: "kanal" mentions: "Omnämningar" + direct: "Direktnoter" _webhookSettings: name: "Namn" active: "Aktiverad" @@ -711,3 +713,6 @@ _selfXssPrevention: warning: "VARNING" _search: searchScopeAll: "Allt" +_watermarkEditor: + scale: "Storlek" + image: "Bilder" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 44148b714b..519d10daa6 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -146,7 +146,7 @@ enterFileName: "พิมพ์ชื่อไฟล์" mute: "ปิดเสียง" unmute: "ยกเลิกการปิดเสียง" renoteMute: "ปิดเสียงรีโน้ต" -renoteUnmute: "เปิดเสียง รีโน้ต" +renoteUnmute: "เลิกปิดเสียงรีโน้ต" block: "บล็อก" unblock: "เลิกบล็อก" suspend: "ระงับ" @@ -220,6 +220,7 @@ silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้" operations: "ดำเนินการ" software: "ซอฟต์แวร์" +softwareName: "ชื่อซอฟต์แวร์" version: "เวอร์ชั่น" metadata: "Metadata" withNFiles: "{n} ไฟล์" @@ -241,8 +242,8 @@ silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้ silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ" mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" -federationAllowedHosts: "เซิร์ฟเวอร์ที่เปิดให้บริการแบบเฟเดอเรชั่น" -federationAllowedHostsDescription: "ระบุชื่อโฮสต์ของเซิร์ฟเวอร์ที่คุณต้องการอนุญาตให้เชื่อมต่อแบบเฟเดอเรชั่น โดยต้องเว้นวรรคแต่ละบรรทัด" +federationAllowedHosts: "เซิร์ฟเวอร์ที่อนุญาตให้เชื่อมกับสหพันธ์" +federationAllowedHostsDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่อนุญาตให้เชื่อมกับสหพันธ์ โดยแยกแต่ละรายการด้วยบรรทัดใหม่" muteAndBlock: "ปิดเสียงและบล็อก" mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง" blockedUsers: "ผู้ใช้ที่ถูกบล็อก" @@ -297,9 +298,11 @@ uploadFromUrl: "อัปโหลดจาก URL" uploadFromUrlDescription: "URL ของไฟล์ที่คุณต้องการอัปโหลด" uploadFromUrlRequested: "ร้องขอการอัปโหลดแล้ว" uploadFromUrlMayTakeTime: "การอัปโหลดอาจใช้เวลาสักครู่จึงจะเสร็จสมบูรณ์" +uploadNFiles: "อัปโหลด {n} ไฟล์" explore: "สำรวจ" messageRead: "อ่านแล้ว" noMoreHistory: "ไม่มีประวัติเพิ่มเติม" +startChat: "เริ่มแชต" nUsersRead: "อ่านโดย {n}" agreeTo: "ฉันยอมรับ {0}" agree: "ยอมรับ" @@ -324,6 +327,7 @@ dark: "มืด" lightThemes: "ธีมสว่าง" darkThemes: "ธีมมืด" syncDeviceDarkMode: "ซิงค์โหมดมืดกับการตั้งค่าอุปกรณ์ของคุณ" +switchDarkModeManuallyWhenSyncEnabledConfirm: "“{x}” เปิดอยู่ ต้องการปิดการซิงค์และสลับโหมดด้วยตนเองหรือไม่?" drive: "ไดรฟ์" fileName: "ชื่อไฟล์" selectFile: "เลือกไฟล์" @@ -364,7 +368,7 @@ reject: "ปฏิเสธ" normal: "ปกติ" instanceName: "ชื่อเซิร์ฟเวอร์" instanceDescription: "คำอธิบายแนะนำเซิร์ฟเวอร์" -maintainerName: "ผู้ดูแล" +maintainerName: "ชื่อผู้ดูแลระบบ" maintainerEmail: "อีเมลผู้ดูแลระบบ" tosUrl: "URL เงื่อนไขการให้บริการ" thisYear: "ปีนี้" @@ -422,6 +426,7 @@ antennaExcludeBots: "ยกเว้นบัญชีบอต" antennaKeywordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR" notifyAntenna: "แจ้งเตือนเกี่ยวกับโน้ตใหม่" withFileAntenna: "เฉพาะโน้ตที่มีไฟล์" +excludeNotesInSensitiveChannel: "ไม่รวมโน้ตจากช่องเนื้อหาละเอียดอ่อน" enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์ของคุณ" antennaUsersDescription: "ระบุหนึ่งชื่อผู้ใช้ต่อบรรทัด" caseSensitive: "อักษรพิมพ์ใหญ่-พิมพ์เล็กความหมายต่างกัน" @@ -452,17 +457,17 @@ totpDescription: "ใช้แอปยืนยันตัวตนเพื moderator: "ผู้ควบคุม" moderation: "การกลั่นกรอง" moderationNote: "โน้ตการกลั่นกรอง" -moderationNoteDescription: "คุณสามารถใส่โน้ตส่วนตัวที่เฉพาะผู้ดูแลระบบเท่านั้นที่สามารถเข้าถึงได้" +moderationNoteDescription: "สามารถจดเมโมที่จะแบ่งปันเฉพาะระหว่างผู้ควบคุมได้" addModerationNote: "เพิ่มโน้ตการกลั่นกรอง" moderationLogs: "ปูมการควบคุมดูแล" nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย" -securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน" -securityKey: "กุญแจความปลอดภัย" +securityKeyAndPasskey: "Security key และ Passkey" +securityKey: "Security Key" lastUsed: "ใช้ล่าสุด" lastUsedAt: "ใช้งานครั้งล่าสุด: {t}" unregister: "เลิกติดตาม" passwordLessLogin: "เข้าสู่ระบบแบบไม่ใช้รหัสผ่าน" -passwordLessLoginDescription: "อนุญาตให้เข้าสู่ระบบโดยไม่ต้องใช้รหัสผ่านโดยใช้รหัสรักษาความปลอดภัยหรือรหัสผ่านเท่านั้น" +passwordLessLoginDescription: "เข้าสู่ระบบโดยไม่ใช้รหัสผ่าน โดยใช้เฉพาะ Security Key หรือ Passkey เท่านั้น" resetPassword: "รีเซ็ตรหัสผ่าน" newPasswordIs: "รหัสผ่านใหม่คือ “{password}”" reduceUiAnimation: "ลดภาพเคลื่อนไหว UI" @@ -572,8 +577,10 @@ showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ showFixedPostFormInChannel: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนของไทม์ไลน์ (ช่อง)" withRepliesByDefaultForNewlyFollowed: "แสดงการตอบกลับจากผู้ใช้ที่คุณเพิ่งติดตามลงไทม์ไลน์ตามค่าเริ่มต้น" newNoteRecived: "มีโน้ตใหม่" +newNote: "โน้ตใหม่" sounds: "เสียง" sound: "เสียง" +notificationSoundSettings: "ตั้งค่าเสียงแจ้งเตือน" listen: "ฟัง" none: "ไม่มี" showInPage: "แสดงในเพจ" @@ -583,6 +590,7 @@ masterVolume: "ระดับเสียงหลัก" notUseSound: "ไม่ใช้เสียง" useSoundOnlyWhenActive: "มีเสียงออกเฉพาะตอนกำลังใช้ Misskey อยู่เท่านั้น" details: "รายละเอียด" +renoteDetails: "รายละเอียดรีโน้ต" chooseEmoji: "เลือกเอโมจิ" unableToProcess: "ไม่สามารถดำเนินการให้เสร็จสิ้นได้" recentUsed: "ใช้ล่าสุด" @@ -604,8 +612,8 @@ output: "เอาท์พุต" script: "สคริปต์" disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ" updateRemoteUser: "อัปเดตข้อมูลผู้ใช้งานระยะไกล" -unsetUserAvatar: "เลิกตั้งอวตาร" -unsetUserAvatarConfirm: "ต้องการเลิกตั้งอวตารใข่ไหม?" +unsetUserAvatar: "เลิกตั้งไอคอน" +unsetUserAvatarConfirm: "ต้องการเลิกตั้งไอคอนประจำตัวหรือไม่?" unsetUserBanner: "เลิกตั้งแบนเนอร์" unsetUserBannerConfirm: "ต้องการเลิกตั้งแบนเนอร์?" deleteAllFiles: "ลบไฟล์ทั้งหมด" @@ -680,16 +688,19 @@ smtpSecure: "ใช้โดยนัย SSL/TLS สำหรับการเ smtpSecureInfo: "ปิดสิ่งนี้เมื่อใช้ STARTTLS" testEmail: "ทดสอบการส่งอีเมล" wordMute: "ปิดเสียงคำ" +wordMuteDescription: "ย่อโน้ตที่มีวลีที่ระบุ สามารถดูโน้ตที่ย่อแล้วได้โดยคลิกที่โน้ตเหล่านั้น" hardWordMute: "ปิดเสียงคำแบบแข็งโป๊ก" -hardWordMuteDescription: "ซ่อนหมายเหตุที่มีวลีที่ระบุ ต่างจากการปิดเสียงคำ โน้ตต่างๆ จะถูกซ่อนไว้อย่างสมบูรณ์" +showMutedWord: "แสดงคำที่ถูกปิดเสียง" +hardWordMuteDescription: "จะซ่อนโน้ตที่มีคำที่ระบุไว้ ซึ่งไม่เหมือนการปิดเสียงคำ ในกรณีนี้โน้ตจะไม่แสดงเลย" regexpError: "เกิดข้อผิดพลาดใน regular expression" regexpErrorDescription: "เกิดข้อผิดพลาดใน regular expression บรรทัดที่ {line} ของการปิดเสียงคำ {tab} :" instanceMute: "ปิดเสียงเซิร์ฟเวอร์" userSaysSomething: "{name} พูดอะไรบางอย่าง" -userSaysSomethingAbout: "{name} พูดอะไรบางอย่างเกี่ยวกับ \"{word}\"" +userSaysSomethingAbout: "{name} พูดบางอย่างเกี่ยวกับ “{word}”" makeActive: "เปิดใช้งาน" display: "แสดงผล" copy: "คัดลอก" +copiedToClipboard: "คัดลอกไปยังคลิปบอร์ดแล้ว" metrics: "เมตริก" overview: "ภาพรวม" logs: "ปูม" @@ -755,7 +766,7 @@ yes: "ใช่" no: "ไม่" driveFilesCount: "จำนวนไฟล์ไดรฟ์" driveUsage: "การใช้พื้นที่ไดรฟ์" -noCrawle: "ปฏิเสธการจัดทำดัชนีของโปรแกรมรวบรวมข้อมูล" +noCrawle: "ปฏิเสธการจัดทำดัชนีของ Crawler (โปรแกรมรวบรวมข้อมูล)" noCrawleDescription: "ขอให้เครื่องมือค้นหาไม่จัดทำดัชนีหน้าโปรไฟล์ โน้ต หน้าเพจ ฯลฯ" lockedAccountInfo: "แม้ว่าการอนุมัติการติดตามถูกเปิดใช้งานอยู่ทุกคนก็ยังคงสามารถเห็นโน้ตของคุณได้ เว้นแต่ว่าคุณจะเปลี่ยนการเปิดเผยโน้ตของคุณเป็น “เฉพาะผู้ติดตาม”" alwaysMarkSensitive: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนเป็นค่าเริ่มต้น" @@ -765,7 +776,7 @@ highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น" notSet: "ไม่ได้ตั้งค่า" emailVerified: "อีเมลได้รับการยืนยันแล้ว" -noteFavoritesCount: "จำนวนโน้ตที่ชื่นชอบ" +noteFavoritesCount: "จำนวนโน้ตโปรด" pageLikesCount: "จำนวนเพจที่ถูกใจ" pageLikedCount: "จำนวนการกดถูกใจเพจที่ได้รับแล้ว" contact: "ติดต่อ" @@ -784,6 +795,7 @@ wide: "กว้าง" narrow: "ชิด" reloadToApplySetting: "การตั้งค่านี้จะมีผลหลังจากโหลดหน้าซ้ำเท่านั้น ต้องการที่จะโหลดใหม่เลยไหม?" needReloadToApply: "ต้องรีโหลดเพื่อให้การเปลี่ยนแปลงมีผล" +needToRestartServerToApply: "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์เพื่อให้การเปลี่ยนแปลงมีผล" showTitlebar: "แสดงแถบชื่อ" clearCache: "ล้างแคช" onlineUsersCount: "{n} รายกำลังออนไลน์" @@ -877,7 +889,7 @@ previewNoteText: "แสดงตัวอย่าง" customCss: "CSS ที่กำหนดเอง" customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้มันใช้ทำอะไร การตั้งค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์ไม่สามารถใช้งานได้อย่างถูกต้อง" global: "ทั่วโลก" -squareAvatars: "แสดงผลอวตารเป็นสี่เหลี่ยม" +squareAvatars: "แสดงไอคอนประจำตัวเป็นสี่เหลี่ยม" sent: "ส่ง" received: "ได้รับแล้ว" searchResult: "ผลการค้นหา" @@ -944,6 +956,9 @@ oneHour: "1 ชั่วโมง" oneDay: "1 วัน" oneWeek: "1 สัปดาห์" oneMonth: "หนึ่งเดือน" +threeMonths: "3 เดือน" +oneYear: "1 ปี" +threeDays: "3 วัน" reflectMayTakeTime: "อาจจำเป็นต้องใช้เวลาสักระยะหนึ่งจึงจะเห็นแสดงผลได้นะ" failedToFetchAccountInformation: "ไม่สามารถเรียกดึงข้อมูลบัญชีได้" rateLimitExceeded: "เกินขีดจำกัดอัตรา" @@ -968,6 +983,7 @@ document: "เอกสาร" numberOfPageCache: "จำนวนหน้าเพจที่แคช" numberOfPageCacheDescription: "การเพิ่มจำนวนนี้จะช่วยเพิ่มความสะดวกให้กับผู้ใช้งาน แต่จะทำให้เซิร์ฟเวอร์โหลดมากขึ้นและต้องใช้หน่วยความจำมากขึ้นอีกด้วย" logoutConfirm: "ต้องการออกจากระบบใช่ไหม?" +logoutWillClearClientData: "เมื่อออกจากระบบ ข้อมูลการตั้งค่าของไคลเอนต์จะถูกลบออกจากเบราว์เซอร์ เพื่อให้สามารถกู้คืนข้อมูลการตั้งค่าได้เมื่อกลับมาเข้าสู่ระบบอีกครั้ง โปรดเปิดใช้งานการสำรองข้อมูลการตั้งค่าอัตโนมัติ" lastActiveDate: "ใช้งานล่าสุดเมื่อ" statusbar: "แถบสถานะ" pleaseSelect: "ตัวเลือก" @@ -986,6 +1002,7 @@ failedToUpload: "การอัปโหลดล้มเหลว" cannotUploadBecauseInappropriate: "ไม่สามารถอัปโหลดไฟล์นี้ได้เนื่องจากระบบตรวจพบบางส่วนของไฟล์ว่านี้อาจจะเป็น NSFW" cannotUploadBecauseNoFreeSpace: "ไม่สามารถอัปโหลดได้เนื่องจากไม่มีพื้นที่ว่างในไดรฟ์เหลือแล้ว" cannotUploadBecauseExceedsFileSizeLimit: "ไม่สามารถอัปโหลดไฟล์นี้ได้แล้วเนื่องจากเกินขีดจำกัดของขนาดไฟล์แล้ว" +cannotUploadBecauseUnallowedFileType: "ไม่สามารถอัปโหลดได้เนื่องจากเป็นชนิดไฟล์ที่ไม่ได้รับอนุญาต" beta: "เบต้า" enableAutoSensitive: "ทำเครื่องหมายว่ามีเนื้อหาที่ละเอียดอ่อนโดยอัตโนมัติ" enableAutoSensitiveDescription: "อนุญาตให้ตรวจหาและทำเครื่องหมายสื่อว่ามีเนื้อหาโดยละเอียดอ่อนโดยอัตโนมัติ ผ่าน Machine Learning หากเป็นไปได้ แม้ว่าคุณจะปิดคุณสมบัตินี้ ก็อาจถูกตั้งค่าโดยอัตโนมัติ ทั้งนี้ขึ้นอยู่กับเซิร์ฟเวอร์" @@ -1005,7 +1022,7 @@ windowMaximize: "ขยายใหญ่สุด" windowMinimize: "ย่อเล็กที่สุด" windowRestore: "เลิกทำ" caption: "คำอธิบาย" -loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้" +loggedInAsBot: "เข้าสู่ระบบเป็นบอตอยู่ในขณะนี้" tools: "เครื่องมือ" cannotLoad: "ไม่สามารถโหลดได้" numberOfProfileView: "มุมมองโปรไฟล์" @@ -1054,7 +1071,7 @@ exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่ letsLookAtTimeline: "มาดูไทม์ไลน์กัน" disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?" disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น" -disableFederationOk: "ปิดการใช้งาน" +disableFederationOk: "ปิดการใช้งานสหพันธ์" invitationRequiredToRegister: "เซิร์ฟเวอร์นี้เป็นแบบรับเชิญ เฉพาะผู้มีรหัสเชิญเท่านั้นถึงสามารถลงทะเบียนได้" emailNotSupported: "เซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล" postToTheChannel: "โพสต์ลงช่อง" @@ -1084,7 +1101,7 @@ retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริ retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ" enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล" enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล" -enableStatsForFederatedInstances: "ดึงข้อมูลสถิติจากเซิร์ฟเวอร์ที่อยู่ห่างไกล" +enableStatsForFederatedInstances: "ดึงข้อมูลจากเซิร์ฟเวอร์ระยะไกล" showClipButtonInNoteFooter: "เพิ่ม “คลิป” ไปยังเมนูสั่งการของโน้ต" reactionsDisplaySize: "ขนาดของรีแอคชั่น" limitWidthOfReaction: "จำกัดความกว้างสูงสุดของรีแอคชั่นและแสดงให้เล็กลง" @@ -1215,13 +1232,13 @@ impressumDescription: "การติดป้ายกำกับ (Impressum) privacyPolicy: "นโยบายความเป็นส่วนตัว" privacyPolicyUrl: "URL นโยบายความเป็นส่วนตัว" tosAndPrivacyPolicy: "เงื่อนไขในการให้บริการและนโยบายความเป็นส่วนตัว" -avatarDecorations: "การตกแต่งอวตาร" +avatarDecorations: "ของตกแต่งไอคอน" attach: "แนบ" detach: "นำออก" detachAll: "เอาออกทั้งหมด" angle: "แองเกิล" flip: "พลิก" -showAvatarDecorations: "แสดงตกแต่งอวตาร" +showAvatarDecorations: "แสดงของตกแต่งไอคอน" releaseToRefresh: "ปล่อยเพื่อรีเฟรช" refreshing: "กำลังรีเฟรช..." pullDownToRefresh: "ดึงลงเพื่อรีเฟรช" @@ -1277,46 +1294,211 @@ clipNoteLimitExceeded: "ไม่สามารถเพิ่มโน้ต performance: "ประสิทธิภาพ​" modified: "แก้ไข" discard: "ละทิ้ง" -thereAreNChanges: "มีอยู่ {n} เปลี่ยนแปลง(s)" +thereAreNChanges: "มีการเปลี่ยนแปลง {n} รายการ" signinWithPasskey: "ลงชื่อเข้าใช้ด้วย Passkey" -unknownWebAuthnKey: "พาสคีย์ไม่ถูกต้องค่ะ" -passkeyVerificationFailed: "การยืนยันกุญแจดิจิทัลไม่สำเร็จค่ะ" -passkeyVerificationSucceededButPasswordlessLoginDisabled: "การยืนยันพาสคีย์สำเร็จแล้ว แต่การลงชื่อเข้าใช้แบบไม่ต้องใส่รหัสผ่านถูกปิดใช้งานแล้ว" +unknownWebAuthnKey: "เป็น Passkey ที่ยังไม่ได้ลงทะเบียน" +passkeyVerificationFailed: "การยืนยัน Passkey ล้มเหลว" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "การยืนยัน Passkey สำเร็จ แต่การเข้าสู่ระบบแบบไม่ใช้รหัสผ่านถูกปิดใช้งานอยู่" messageToFollower: "ข้อความถึงผู้ติดตาม" target: "เป้า" testCaptchaWarning: "ฟังก์ชันนี้มีไว้สำหรับทดสอบ CAPTCHA เท่านั้น\nห้ามนำไปใช้ในระบบจริงโดยเด็ดขาด" prohibitedWordsForNameOfUser: "คำนี้ไม่สามารถใช้เป็นชื่อผู้ใช้ได้" -prohibitedWordsForNameOfUserDescription: "หากมีสตริงใดๆ ในรายการนี้ปรากฏอยู่ในชื่อของผู้ใช้ ชื่อนั้นจะถูกปฏิเสธ ผู้ใช้ที่มีสิทธิ์แต่ผู้ดูแลระบบนั้นจะไม่ได้รับผลกระทบใดๆจากข้อจำกัดนี้ค่ะ" +prohibitedWordsForNameOfUserDescription: "จะไม่อนุญาตให้เปลี่ยนชื่อผู้ใช้หากชื่อของผู้ใช้มีข้อความที่อยู่ในรายการนี้ แต่ผู้ใช้ที่มีสิทธิ์เป็นผู้ควบคุมจะไม่ได้รับผลกระทบจากข้อจำกัดนี้" yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม" yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ" -federationDisabled: "เซิร์ฟเวอร์นี้ปิดการใช้งานการรวมกลุ่ม คุณไม่สามารถโต้ตอบกับผู้ใช้บนเซิร์ฟเวอร์อื่นได้" -reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?" -markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?" -unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?" +thisContentsAreMarkedAsSigninRequiredByAuthor: "ผู้โพสต์ได้ตั้งค่าว่าต้องเข้าสู่ระบบจึงจะสามารถดูได้" +lockdown: "ล็อกดาวน์" +pleaseSelectAccount: "โปรดเลือกบัญชี" +availableRoles: "บทบาทที่ใช้ได้" +acknowledgeNotesAndEnable: "เปิดใช้งานหลังจากที่เข้าใจข้อควรระวังแล้ว" +federationSpecified: "เซิร์ฟเวอร์นี้ดำเนินงานในระบบกลุ่มไวท์ลิสต์ ไม่สามารถติดต่อกับเซิร์ฟเวอร์อื่นที่ไม่ได้รับอนุญาตจากผู้ดูแลระบบได้" +federationDisabled: "เซิร์ฟเวอร์นี้ปิดใช้งานสหพันธ์ ไม่สามารถติดต่อหรือแลกเปลี่ยนข้อมูลกับผู้ใช้จากเซิร์ฟเวอร์อื่นได้" +draft: "ร่าง" +confirmOnReact: "ยืนยันเมื่อทำการรีแอคชั่น" +reactAreYouSure: "ต้องการใส่รีแอคชั่นด้วย \"{emoji}\" หรือไม่?" +markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อนี้ว่าเป็นเนื้อหาละเอียดอ่อนหรือไม่?" +unmarkAsSensitiveConfirm: "ต้องการยกเลิกการระบุว่าสื่อนี้มีเนื้อหาละเอียดอ่อนหรือไม่?" +preferences: "การตั้งค่าสภาพแวดล้อม" +accessibility: "การช่วยการเข้าถึง" +preferencesProfile: "โปรไฟล์การกำหนดค่า" +copyPreferenceId: "คัดลือก ID การตั้งค่า" +resetToDefaultValue: "คืนค่าเป็นค่าเริ่มต้น" +overrideByAccount: "เขียนทับด้วยบัญชี" +untitled: "ไม่มีชื่อ" +noName: "ไม่มีชื่อ" +skip: "ข้าม" +restore: "กู้คืน" +syncBetweenDevices: "ซิงค์ระหว่างอุปกรณ์" +preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์" +preferenceSyncConflictText: "การตั้งค่าที่เปิดใช้งานการซิงค์จะบันทึกค่าลงในเซิร์ฟเวอร์ อย่างไรก็ดี พบว่ามีค่าการตั้งค่านี้ที่เคยบันทึกไว้ในเซิร์ฟเวอร์แล้ว ต้องการดำเนินการอย่างไร?" +preferenceSyncConflictChoiceMerge: "รวมเข้าด้วยกัน" +preferenceSyncConflictChoiceServer: "เขียนทับด้วยค่าการตั้งค่าเซิร์ฟเวอร์" +preferenceSyncConflictChoiceDevice: "เขียนทับด้วยค่าการตั้งค่าอุปกรณ์" +preferenceSyncConflictChoiceCancel: "ยกเลิกการเปิดใช้งานการซิงค์" +paste: "วาง" +emojiPalette: "จานสีเอโมจิ" postForm: "แบบฟอร์มการโพสต์" +textCount: "จำนวนอักขระ" information: "เกี่ยวกับ" +chat: "แชต" +migrateOldSettings: "ย้ายข้อมูลการตั้งค่าเก่า" +migrateOldSettings_description: "โดยปกติจะทำโดยอัตโนมัติ แต่หากด้วยเหตุผลบางประการที่ไม่สามารถย้ายได้สำเร็จ สามารถสั่งย้ายด้วยตนเองได้ การตั้งค่าปัจจุบันจะถูกเขียนทับ" +compress: "บีบอัด" right: "ขวา" bottom: "ภายใต้" +top: "บน" +embed: "ฝัง" +settingsMigrating: "กำลังย้ายการตั้งค่า กรุณารอสักครู่... (สามารถย้ายด้วยตนเองภายหลังได้ที่ การตั้งค่า → อื่นๆ → ย้ายข้อมูลการตั้งค่าเก่า)" +readonly: "อ่านได้อย่างเดียว" +goToDeck: "กลับไปยังเด็ค" +federationJobs: "งานสหพันธ์" +driveAboutTip: "ในไดรฟ์จะแสดงรายการไฟล์ที่เคยอัปโหลดไว้ก่อนหน้า
\nสามารถนำมาใช้ซ้ำเมื่อแนบไฟล์ในโน้ต หรือตั้งค่าให้อัปโหลดไฟล์ล่วงหน้าเพื่อนำไปโพสต์ทีหลังได้
\nโปรดระวัง เมื่อลบไฟล์ ไฟล์นั้นจะไม่แสดงในทุกที่ที่เคยใช้ไฟล์นี้ (โน้ต, หน้าเพจ, อวตาร, แบนเนอร์ ฯลฯ)
\nสามารถสร้างโฟลเดอร์เพื่อจัดระเบียบได้" +scrollToClose: "เลื่อนเพื่อปิด" +advice: "คำแนะนำ" +realtimeMode: "โหมดเรียลไทม์" +turnItOn: "เปิดใช้งาน" +turnItOff: "ปิดใช้งาน" +emojiMute: "ปิดเสียงเอโมจิ" +emojiUnmute: "เลิกปิดเสียงเอโมจิ" +muteX: "ปิดเสียง {x}" +unmuteX: "เลิกปิดเสียง {x}" +abort: "หยุดและยกเลิก" +tip: "คำแนะนำและเคล็ดลับ" +redisplayAllTips: "แสดงคำแนะนำและเคล็ดลับทั้งหมดอีกครั้ง" +hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด" +defaultImageCompressionLevel: "ความละเอียดเริ่มต้นสำหรับการบีบอัดภาพ" +defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น
หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง" +inMinutes: "นาที" +inDays: "วัน" +_order: + newest: "เรียงจากใหม่ไปเก่า" + oldest: "เรียงจากเก่าไปใหม่" _chat: + noMessagesYet: "ยังไม่มีข้อความ" + newMessage: "ข้อความใหม่" + individualChat: "แชตส่วนตัว" + individualChat_description: "สามารถแชตแบบตัวต่อตัวกับผู้ใช้ที่ระบุไว้ได้" + roomChat: "ห้องแชต" + roomChat_description: "สามารถแชตแบบกลุ่มหลายคนได้\nและสามารถแชตกับผู้ใช้ที่ไม่ได้อนุญาตแชตส่วนตัวได้ หากอีกฝ่ายยอมรับ" + createRoom: "สร้างห้อง" + inviteUserToChat: "เชิญผู้ใช้และเริ่มแชตได้เลย" + yourRooms: "ห้องที่สร้างไว้" + joiningRooms: "ห้องที่เข้าร่วมอยู่" invitations: "คำเชิญ" + noInvitations: "ไม่มีคำเชิญ" + history: "ประวัติ" noHistory: "ไม่มีประวัติ" + noRooms: "ไม่มีห้อง" + inviteUser: "เชิญผู้ใช้" + sentInvitations: "คำเชิญที่ส่งไปแล้ว" + join: "เข้าร่วม" + ignore: "ไม่สนใจ" + leave: "ออกจากห้อง" members: "สมาชิก" + searchMessages: "ค้นหาข้อความ" home: "หน้าหลัก" send: "ส่ง" + newline: "ขึ้นบรรทัดใหม่" + muteThisRoom: "ปิดเสียงห้องนี้" + deleteRoom: "ลบห้อง" + chatNotAvailableForThisAccountOrServer: "แชตไม่ได้เปิดใช้งานบนเซิร์ฟเวอร์นี้ หรือบัญชีนี้" + chatIsReadOnlyForThisAccountOrServer: "แชตบนเซิร์ฟเวอร์นี้ หรือบัญชีนี้ เป็นแบบอ่านอย่างเดียว ไม่สามารถส่งข้อความใหม่ สร้างหรือเข้าร่วมห้องแชตได้" + chatNotAvailableInOtherAccount: "บัญชีคู่สนทนาไม่สามารถใช้ฟังก์ชันแชตได้" + cannotChatWithTheUser: "ไม่สามารถเริ่มแชตกับผู้ใช้นี้ได้" + cannotChatWithTheUser_description: "แชตใช้งานไม่ได้ หรือคู่สนทนายังไม่ได้เปิดแชต" + youAreNotAMemberOfThisRoomButInvited: "คุณไม่ได้เป็นผู้เข้าร่วมห้องนี้ แต่มีคำเชิญส่งมา หากต้องการเข้าร่วม กรุณายืนยันคำเชิญ" + doYouAcceptInvitation: "ต้องการยอมรับคำเชิญหรือไม่?" + chatWithThisUser: "แชตเลย" + thisUserAllowsChatOnlyFromFollowers: "ผู้ใช้นี้รับแชตเฉพาะจากผู้ติดตามเท่านั้น" + thisUserAllowsChatOnlyFromFollowing: "ผู้ใช้นี้รับแชตเฉพาะจากผู้ที่เขาติดตามเท่านั้น" + thisUserAllowsChatOnlyFromMutualFollowing: "ผู้ใช้นี้รับแชตเฉพาะจากผู้ที่ติดตามซึ่งกันและกันทั้งสองฝ่ายเท่านั้น" + thisUserNotAllowedChatAnyone: "ผู้ใช้นี้ไม่รับแชตจากใครเลย" + chatAllowedUsers: "ผู้ที่อนุญาตให้แชตด้วย" + chatAllowedUsers_note: "ไม่ว่าจะตั้งค่ายังไง คุณยังสามารถแชตกับคนที่คุณส่งข้อความไปหาได้" + _chatAllowedUsers: + everyone: "ใครก็ได้หมด" + followers: "เฉพาะผู้ติดตามเท่านั้น" + following: "เฉพาะผู้ที่ตัวเองติดตามเท่านั้น" + mutual: "เฉพาะผู้ใช้ที่ติดตามซึ่งกันและกันทั้งสองฝ่ายเท่านั้น" + none: "ไม่อนุญาตให้ใครเลย" +_emojiPalette: + palettes: "จานสี" + enableSyncBetweenDevicesForPalettes: "เปิดใช้งานการซิงค์จานสีระหว่างอุปกรณ์" + paletteForMain: "จานสีหลักที่ใช้" + paletteForReaction: "จานสีที่ใช้ในการรีแอคชั่น" _settings: + driveBanner: "สามารถจัดการและตั้งค่าไดรฟ์ ตรวจสอบการใช้งาน และตั้งค่าการอัปโหลดไฟล์ได้" + pluginBanner: "สามารถขยายความสามารถของไคลเอนต์ด้วยปลั๊กอินได้ ติดตั้ง ตั้งค่า และจัดการปลั๊กอินแต่ละตัวได้" + notificationsBanner: "สามารถตั้งค่าประเภทและขอบเขตของการแจ้งเตือนที่รับจากเซิร์ฟเวอร์ รวมถึงการแจ้งเตือนแบบพุช" + api: "API" webhook: "Webhook" + serviceConnection: "การเชื่อมต่อกับบริการ" + serviceConnectionBanner: "สามารถจัดการและตั้งค่าโทเค็นการเข้าถึงและ Webhook เพื่อเชื่อมต่อกับแอปหรือบริการภายนอกได้" + accountData: "ข้อมูลบัญชี" + accountDataBanner: "สามารถจัดการข้อมูลบัญชีได้โดยส่งออกหรือนำเข้าไฟล์เก็บถาวร" + muteAndBlockBanner: "สามารถตั้งค่าการซ่อนเนื้อหา และจำกัดการกระทำจากผู้ใช้เฉพาะรายได้" + accessibilityBanner: "สามารถปรับแต่งรูปลักษณ์และพฤติกรรมของไคลเอนต์เพื่อให้เหมาะกับการใช้งานของตนเองมากขึ้น" + privacyBanner: "สามารถตั้งค่าความเป็นส่วนตัวของบัญชี เช่น ขอบเขตการเผยแพร่เนื้อหา ความสามารถในการค้นหา และการอนุมัติผู้ติดตาม" + securityBanner: "สามารถตั้งค่าความปลอดภัยของบัญชี เช่น รหัสผ่าน วิธีการเข้าสู่ระบบ แอปยืนยันตัวตน Passkey เป็นต้น" + preferencesBanner: "คุณสามารถกำหนดค่าพฤติกรรมโดยรวมของไคลเอนต์ได้ตามความต้องการของคุณ" + appearanceBanner: "สามารถตั้งค่ารูปลักษณ์และวิธีการแสดงผลของไคลเอนต์ตามความชอบได้" + soundsBanner: "สามารถตั้งค่าเสียงที่จะเล่นบนไคลเอนต์ได้" + timelineAndNote: "ไทม์ไลน์และโน้ต" + makeEveryTextElementsSelectable: "อนุญาตให้เลือกข้อความทั้งหมดได้" + makeEveryTextElementsSelectable_description: "หากเปิดใช้งาน อาจทำให้ความสะดวกในการใช้งานลดลงในบางสถานการณ์" + useStickyIcons: "ทำให้ไอคอนเคลื่อนตามการเลื่อน" + enableHighQualityImagePlaceholders: "แสดงภาพตัวแทนคุณภาพสูง" + uiAnimations: "ภาพเคลื่อนไหวของ UI" + showNavbarSubButtons: "แสดงปุ่มรองบนแถบนำทาง" + ifOn: "เมื่อเปิดใช้งาน" + ifOff: "เมื่อปิดใช้งาน" + enableSyncThemesBetweenDevices: "ซิงค์ธีมที่ติดตั้งระหว่างอุปกรณ์" + enablePullToRefresh: "ดึงเพื่ออัปเดต" + enablePullToRefresh_description: "สำหรับเมาส์ ให้กดปุ่มล้อกลางค้างไว้แล้วลาก" + realtimeMode_description: "เชื่อมต่อกับเซิร์ฟเวอร์และอัปเดตเนื้อหาแบบเรียลไทม์ อาจทำให้ใช้ปริมาณข้อมูลและแบตเตอรี่มากขึ้นได้" + contentsUpdateFrequency: "ความถี่ในการดึงข้อมูลเนื้อหา" + contentsUpdateFrequency_description: "ยิ่งตั้งค่าสูง เนื้อหาจะอัปเดตแบบเรียลไทม์มากขึ้น แต่ประสิทธิภาพอาจลดลง และการใช้ข้อมูลกับแบตเตอรี่จะเพิ่มมากขึ้น" + contentsUpdateFrequency_description2: "เมื่อโหมดเรียลไทม์เปิดอยู่ เนื้อหาจะอัปเดตแบบเรียลไทม์โดยไม่ขึ้นกับการตั้งค่านี้" + showUrlPreview: "แสดงตัวอย่าง URL" + showAvailableReactionsFirstInNote: "แสดงรีแอคชั่นที่ใช้ได้ไว้หน้าสุด" + showPageTabBarBottom: "แสดงแท็บบาร์ของเพจที่ด้านล่าง" + _chat: + showSenderName: "แสดงชื่อผู้ส่ง" + sendOnEnter: "กด Enter เพื่อส่ง" +_preferencesProfile: + profileName: "ชื่อโปรไฟล์" + profileNameDescription: "กรุณาตั้งชื่อเพื่อระบุอุปกรณ์นี้" + profileNameDescription2: "เช่น: “คอมเครื่องหลัก”, “มือถือ” ฯลฯ" + manageProfiles: "จัดการโปรไฟล์" +_preferencesBackup: + autoBackup: "สำรองโดยอัตโนมัติ" + restoreFromBackup: "คืนค่าจากข้อมูลสำรอง" + noBackupsFoundTitle: "ไม่พบข้อมูลสำรอง" + noBackupsFoundDescription: "ไม่พบข้อมูลสำรองที่สร้างโดยอัตโนมัติ แต่หากมีข้อมูลสำรองที่บันทึกด้วยตนเอง สามารถนำเข้ามาเพื่อกู้คืนได้" + selectBackupToRestore: "กรุณาเลือกข้อมูลสำรองที่ต้องการกู้คืน" + youNeedToNameYourProfileToEnableAutoBackup: "จำเป็นต้องตั้งชื่อโปรไฟล์ก่อนจึงจะเปิดใช้งานการสำรองข้อมูลอัตโนมัติได้" + autoPreferencesBackupIsNotEnabledForThisDevice: "ยังไม่ได้เปิดใช้งานการสำรองข้อมูลอัตโนมัติบนอุปกรณ์นี้" + backupFound: "พบข้อมูลสำรองของการตั้งค่าแล้ว" _accountSettings: requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา" - requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล" - requireSigninToViewContentsDescription2: "นอกจากนี้ จะไม่สามารถดูจากเซิร์ฟเวอร์ที่ไม่รองรับการดูตัวอย่าง URL (OGP), การฝังในหน้าเว็บ หรือการอ้างอิงหมายเหตุได้" - requireSigninToViewContentsDescription3: "เนื้อหาที่ถูกรวมเข้ากับเซิร์ฟเวอร์ระยะไกลอาจไม่อยู่ภายใต้ข้อจำกัดเหล่านี้" + requireSigninToViewContentsDescription1: "กำหนดให้ต้องเข้าสู่ระบบก่อนจึงจะสามารถดูโน้ตหรือเนื้อหาทั้งหมดที่สร้างไว้ได้ ซึ่งช่วยป้องกันไม่ให้ข้อมูลถูกเก็บโดยบอตหรือ Crawler (โปรแกรมรวบรวมข้อมูล)" + requireSigninToViewContentsDescription2: "จะไม่สามารถแสดงผลจากเซิร์ฟเวอร์ที่ไม่รองรับการแสดงตัวอย่าง URL (OGP), การฝังในหน้าเว็บ, หรือการอ้างอิงโน้ตได้" + requireSigninToViewContentsDescription3: "เนื้อหาที่ถูกรวมผ่านสหพันธ์จากเซิร์ฟเวอร์ระยะไกลอาจไม่อยู่ภายใต้ข้อจำกัดเหล่านี้" + makeNotesFollowersOnlyBefore: "แสดงโน้ตเก่าเฉพาะกับผู้ติดตามเท่านั้น" + makeNotesFollowersOnlyBeforeDescription: "ขณะที่เปิดฟังก์ชันนี้ โน้ตที่เก่ากว่าหรือเลยเวลาที่กำหนดจะแสดงเฉพาะกับผู้ติดตามเท่านั้น หากปิดใช้งาน สถานะการเปิดเผยจะกลับไปเป็นแบบเดิม" + makeNotesHiddenBefore: "ทำให้โน้ตเก่าทั้งหมดเป็นแบบส่วนตัว" + makeNotesHiddenBeforeDescription: "ขณะที่เปิดฟังก์ชันนี้ โน้ตที่เก่ากว่าหรือเลยเวลาที่กำหนดจะแสดงเฉพาะกับตนเอง (กลายเป็นแบบส่วนตัว) หากปิดใช้งาน สถานะการเปิดเผยจะกลับไปเป็นแบบเดิม" + mayNotEffectForFederatedNotes: "โน้ตที่ถูกรวมผ่านสหพันธ์จากเซิร์ฟเวอร์ระยะไกลอาจไม่ได้รับผลจากการตั้งค่านี้" + mayNotEffectSomeSituations: "ข้อจำกัดเหล่านี้เป็นเพียงการกรองเบื้องต้น ในบางกรณี เช่น การดูจากเซิร์ฟเวอร์อื่นหรือในระหว่างการตรวจสอบโดยผู้ดูแล อาจไม่สามารถใช้งานได้" + notesHavePassedSpecifiedPeriod: "โน้ตที่เลยเวลาที่กำหนดไว้แล้ว" + notesOlderThanSpecifiedDateAndTime: "โน้ตก่อนเวลาที่กำหนดไว้" _abuseUserReport: forward: "ส่ง​ต่อ" forwardDescription: "ส่งรายงานไปยังเซิร์ฟเวอร์ระยะไกลโดยใช้บัญชีระบบที่ไม่ระบุตัวตน" resolve: "แก้ไข" accept: "ยอมรับ" reject: "ปฏิเสธ" - resolveTutorial: "ถ้าหากรายงานนี้มีเนื้อหาถูกต้อง ให้เลือก \"ยอมรับ\" เพื่อปิดเคสกรณีนี้โดยถือว่าได้รับการแก้ไขแล้ว\nถ้าหากเนื้อหาในรายงานนี้นั้นไม่ถูกต้อง ให้เลือก \"ปฏิเสธ\" เพื่อปิดเคสกรณีนี้โดยถือว่าไม่ได้รับการแก้ไข" + resolveTutorial: "ให้เลือก “ยอมรับ” หากรายงานนี้มีเนื้อหาชอบธรรม เพื่อทำเครื่องหมายว่ากรณีนี้ได้รับการแก้ไขในทางบวก\nให้เลือก “ปฏิเสธ” หากรายงานนี้มีเนื้อหาไม่สมเหตุผล เพื่อทำเครื่องหมายว่ากรณีนี้ได้รับการแก้ไขในทางลบ" _delivery: status: "สถานะการจัดส่ง" stop: "ระงับการส่ง" @@ -1326,6 +1508,7 @@ _delivery: manuallySuspended: "หยุดชั่วคราวด้วยตนเอง" goneSuspended: "เซิร์ฟเวอร์ถูกระงับเนื่องจากมีการลบเซิร์ฟเวอร์นี้" autoSuspendedForNotResponding: "เซิร์ฟเวอร์ถูกระงับเนื่องจากไม่ตอบสนอง" + softwareSuspended: "หยุดให้บริการ เนื่องจากเป็นซอฟต์แวร์ที่ถูกระงับการเผยแพร่" _bubbleGame: howToPlay: "วิธีเล่น" hold: "ถือไว้" @@ -1440,7 +1623,7 @@ _timelineDescription: _serverRules: description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" _serverSettings: - iconUrl: "URL ไอคอน" + iconUrl: "URL ของไอคอน" appIconDescription: "ระบุไอคอนที่จะใช้เมื่อ {host} แสดงเป็นแอป" appIconUsageExample: "ตัวอย่างเช่น เมื่อถูกเพิ่มเป็น PWA หรือบุ๊กมาร์กบนหน้าจอหลักในสมาร์ทโฟน" appIconStyleRecommendation: "เนื่องจากอาจถูกครอบตัดเป็นสี่เหลี่ยมหรือวงกลม จึงแนะนำให้ใช้ภาพที่เผื่อพื้นที่รอบๆ ตัวโลโก้ไอคอนไว้" @@ -1452,9 +1635,34 @@ _serverSettings: fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล" fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้" reactionsBufferingDescription: "เมื่อเปิดใช้งานฟังก์ชันนี้ก็จะช่วยลด latency ในการสร้างปฏิกิริยา แต่อาจจะส่งผลให้ memory footprint ของ Redis เพิ่มขึ้นนะ" + remoteNotesCleaning: "การล้างข้อมูลโพสต์จากระยะไกลโดยอัตโนมัติ" + remoteNotesCleaning_description: "เมื่อเปิดใช้งาน จะทำการล้างโพสต์จากระยะไกลเก่าที่ไม่ถูกอ้างอิง เป็นระยะ เพื่อลดการขยายตัวของฐานข้อมูล" + remoteNotesCleaningMaxProcessingDuration: "ระยะเวลาสูงสุดของการประมวลผลการล้างข้อมูล" + remoteNotesCleaningExpiryDaysForEachNotes: "จำนวนวันที่ต้องเก็บโน้ตไว้อย่างน้อย" inquiryUrl: "URL สำหรับการติดต่อสอบถาม" inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์" - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "ถ้าหากไม่มีการตรวจสอบจากผู้ดูแลระบบหรือไม่มีความเคลื่อนไหวมาเป็นระยะเวลาหนึ่ง ระบบจะทำการปิดใช้งานฟังก์ชันนี้โดยอัตโนมัติ เพื่อลดความเสี่ยงในการถูกโจมตีด้วยสแปมและอื่นๆ" + openRegistration: "เปิดให้สร้างบัญชีได้" + openRegistrationWarning: "การเปิดให้ลงทะเบียนมีความเสี่ยง แนะนำให้เปิดใช้งานเฉพาะในกรณีที่สามารถตรวจสอบเซิร์ฟเวอร์อย่างสม่ำเสมอและมีระบบรับมือกับปัญหาได้ทันท่วงที" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "หากไม่พบกิจกรรมของผู้ควบคุมในช่วงระยะเวลาหนึ่ง การตั้งค่านี้จะถูกปิดโดยอัตโนมัติเพื่อป้องกันสแปม" + deliverSuspendedSoftware: "ซอฟต์แวร์ที่หยุดการเผยแพร่" + deliverSuspendedSoftwareDescription: "เนื่องจากเหตุผลด้านช่องโหว่ เป็นต้น สามารถหยุดการแจกจ่ายโดยระบุชื่อซอฟต์แวร์ของเซิร์ฟเวอร์และช่วงของเวอร์ชันได้ ข้อมูลเวอร์ชันนี้เป็นข้อมูลที่เซิร์ฟเวอร์ให้มา จึงไม่สามารถรับประกันความน่าเชื่อถือได้ สามารถใช้การระบุช่วงเวอร์ชันแบบ semver ได้ แต่ถ้าระบุเป็น >= 2024.3.1 จะไม่รวมเวอร์ชันแบบกำหนดเอง เช่น 2024.3.1-custom.0 จึงแนะนำให้ระบุเป็น >= 2024.3.1-0 ซึ่งเป็นการระบุแบบ prerelease" + singleUserMode: "โหมดผู้ใช้คนเดียว" + singleUserMode_description: "หากมีเพียงตัวเองคนเดียวที่ใช้เซิร์ฟเวอร์นี้ การเปิดใช้งานโหมดนี้จะช่วยปรับการทำงานให้เหมาะสมที่สุด" + signToActivityPubGet: "ลงนามในคำขอ GET" + signToActivityPubGet_description: "โดยปกติควรเปิดใช้งาน แต่หากพบปัญหาเกี่ยวกับการสื่อสารในสหพันธ์ การปิดใช้งานอาจช่วยแก้ไขได้ แต่ในบางกรณี เซิร์ฟเวอร์อาจไม่สามารถสื่อสารได้เลยหากปิดใช้งานนี้" + proxyRemoteFiles: "พร็อกซีไฟล์ระยะไกล" + proxyRemoteFiles_description: "เมื่อเปิดใช้งาน จะทำหน้าที่เป็นพร็อกซีสำหรับไฟล์จากระยะไกล ช่วยในการสร้างภาพขนาดย่อและปกป้องความเป็นส่วนตัวของผู้ใช้" + allowExternalApRedirect: "อนุญาตการเปลี่ยนเส้นทางการสืบค้นผ่าน ActivityPub" + allowExternalApRedirect_description: "เมื่อเปิดใช้งาน จะอนุญาตให้เซิร์ฟเวอร์อื่นสืบค้นเนื้อหาของบุคคลที่สามผ่านเซิร์ฟเวอร์นี้ได้ แต่มีความเสี่ยงที่อาจเกิดการปลอมแปลงเนื้อหา" + userGeneratedContentsVisibilityForVisitor: "ขอบเขตการเปิดเผยเนื้อหาที่ผู้ใช้สร้างต่อบุคคลที่ไม่ได้เข้าร่วม (แขก)" + userGeneratedContentsVisibilityForVisitor_description: "ช่วยป้องกันปัญหาที่อาจเกิดขึ้นจากเนื้อหาระยะไกลที่ไม่เหมาะสม ซึ่งอาจถูกเผยแพร่ออกสู่อินเทอร์เน็ตโดยไม่ตั้งใจผ่านเซิร์ฟเวอร์ของตนเอง โดยเฉพาะในกรณีที่การดูแลควบคุมไม่ทั่วถึง" + userGeneratedContentsVisibilityForVisitor_description2: "การเปิดเผยเนื้อหาทั้งหมดในเซิร์ฟเวอร์รวมทั้งเนื้อหาที่รับมาจากระยะไกลสู่สาธารณะบนอินเทอร์เน็ตโดยไม่มีข้อจำกัดใดๆ มีความเสี่ยงโดยเฉพาะอย่างยิ่งสำหรับผู้ชมที่ไม่เข้าใจลักษณะของระบบแบบกระจาย อาจทำให้เกิดความเข้าใจผิดคิดว่าเนื้อหาที่มาจากระยะไกลนั้นเป็นเนื้อหาที่สร้างขึ้นภายในเซิร์ฟเวอร์นี้ จึงควรใช้ความระมัดระวังอย่างมาก" + restartServerSetupWizardConfirm_title: "ต้องการเริ่มวิซาร์ดการตั้งค่าเซิร์ฟเวอร์ใหม่หรือไม่?" + restartServerSetupWizardConfirm_text: "การตั้งค่าบางส่วนในปัจจุบันจะถูกรีเซ็ต" + _userGeneratedContentsVisibilityForVisitor: + all: "ทั้งหมดสาธารณะ" + localOnly: "เผยแพร่เป็นสาธารณะเฉพาะเนื้อหาท้องถิ่น เนื้อหาระยะไกลให้เป็นส่วนตัว" + none: "ทั้งหมดส่วนตัว" _accountMigration: moveFrom: "ย้ายจากบัญชีอื่นมาที่บัญชีนี้" moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น" @@ -1744,13 +1952,15 @@ _role: baseRole: "แม่แบบบทบาท" useBaseValue: "ใช้ตามแม่แบบบทบาท" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" - iconUrl: "URL ไอคอน" + iconUrl: "URL ของไอคอน" asBadge: "แสดงเป็นตรา" - descriptionOfAsBadge: "เมื่อเปิดใช้งาน ไอคอนบทบาทจะปรากฏถัดจากชื่อผู้ใช้" + descriptionOfAsBadge: "หากเปิดใช้งาน จะมีไอคอนของบทบาท แสดงถัดจากชื่อผู้ใช้" isExplorable: "ค้นหาผู้ใช้ได้ง่ายขึ้นโดยดูจากบทบาท" descriptionOfIsExplorable: "เมื่อเปิดใช้งาน ไทมไลน์บทบาทนี้และสมาชิกที่มีบทบาทนี้จะเปิดเผยเป็นสาธารณะ" displayOrder: "ลำดับการแสดงผล" descriptionOfDisplayOrder: "เลขที่สูงกว่าจะแสดงบน UI ก่อน" + preserveAssignmentOnMoveAccount: "โอนสถานะการมอบหมายไปยังบัญชีที่ย้ายไป" + preserveAssignmentOnMoveAccount_description: "เมื่อเปิดใช้งาน บัญชีที่ได้รับบทบาทนี้เมื่อถูกย้ายไปบัญชีใหม่ บทบาทนี้จะถูกถ่ายทอดไปยังบัญชีปลายทางด้วย" canEditMembersByModerator: "อนุญาตให้ผู้ควบคุมแก้ไขสมาชิก" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ นอกเหนือจากผู้ควบคุมและผู้ดูแลระบบแล้ว จะสามารถเพิ่มถอนบทบาทนี้แก่ผู้ใช้ได้ แต่เมื่อปิดใช้ จะมีเฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถดำเนินการได้" priority: "ลำดับความสำคัญ" @@ -1770,8 +1980,9 @@ _role: canManageCustomEmojis: "จัดการเอโมจิที่กำหนดเอง" canManageAvatarDecorations: "จัดการตกแต่งอวตาร" driveCapacity: "ความจุของไดรฟ์" + maxFileSize: "ขนาดไฟล์สูงสุดที่สามารถอัปโหลดได้" alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" - canUpdateBioMedia: "อนุญาตให้ปรับปรุงไอคอนและแบนเนอร์" + canUpdateBioMedia: "อนุญาตให้เปลี่ยนไอคอนประจำตัวและแบนเนอร์" pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้" antennaMax: "จำนวนสูงสุดของเสาอากาศ" wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ" @@ -1785,12 +1996,18 @@ _role: canHideAds: "ซ่อนโฆษณา" canSearchNotes: "การใช้การค้นหาโน้ต" canUseTranslator: "การใช้งานแปล" - avatarDecorationLimit: "จำนวนการตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้" + avatarDecorationLimit: "จำนวนของตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้" canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ" canImportBlocking: "อนุญาตให้นำเข้าการบล็อก" canImportFollowing: "อนุญาตให้นำเข้ารายการต่อไปนี้" - canImportMuting: "อนุญาตให้นำเข้าการปิดกั้น" + canImportMuting: "อนุญาตให้นำเข้าการปิดเสียง" canImportUserLists: "อนุญาตให้นำเข้ารายการ" + chatAvailability: "อนุญาตให้แชต" + uploadableFileTypes: "ประเภทไฟล์ที่สามารถอัปโหลดได้" + uploadableFileTypes_caption: "สามารถระบุ MIME type ได้ โดยใช้การขึ้นบรรทัดใหม่เพื่อแยกหลายรายการ และสามารถใช้ดอกจัน (*) เพื่อระบุแบบไวลด์การ์ดได้ (เช่น: image/*)" + uploadableFileTypes_caption2: "ไฟล์บางประเภทอาจไม่สามารถระบุชนิดได้ หากต้องการอนุญาตไฟล์ลักษณะนั้น กรุณาเพิ่ม {x} ลงในรายการที่อนุญาต" + noteDraftLimit: "จำนวนโน้ตฉบับร่างที่สามารถสร้างได้บนฝั่งเซิร์ฟเวอร์" + watermarkAvailable: "มีฟังก์ชั่นลายน้ำให้เลือกใช้" _condition: roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ" isLocal: "ผู้ใช้ท้องถิ่น" @@ -1950,10 +2167,12 @@ _theme: install: "ติดตั้งธีม" manage: "จัดการธีม" code: "โค้ดธีม" - description: "รายละเอียด" + copyThemeCode: "คัดลอกรหัสธีม" + description: "คำอธิบาย" installed: "{name} ได้รับการติดตั้ง" installedThemes: "ธีมที่ติดตั้ง" builtinThemes: "ธีมในตัว" + instanceTheme: "ธีมของเซิร์ฟเวอร์" alreadyInstalled: "ธีมนี้ได้รับการติดตั้งแล้ว" invalid: "รูปแบบของธีมนี้ไม่ถูกต้องนะ" make: "ทำธีม" @@ -1981,7 +2200,7 @@ _theme: fg: "ข้อความ" focus: "โฟกัส" indicator: "ตัวบ่งชี้" - panel: "แผงควบคุม" + panel: "แผง" shadow: "เงา" header: "ส่วนหัว" navBg: "พื้นหลังแถบด้านข้าง" @@ -1991,7 +2210,7 @@ _theme: link: "ลิงก์" hashtag: "แฮชแท็ก" mention: "กล่าวถึง" - mentionMe: "ได้กล่าวถึง (ฉัน)" + mentionMe: "ได้กล่าวถึงคุณ" renote: "รีโน้ต" modalBg: "พื้นหลังโมดอล" divider: "ตัวแบ่ง" @@ -2007,7 +2226,6 @@ _theme: buttonBg: "ปุ่มพื้นหลัง" buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)" inputBorder: "เส้นขอบของช่องป้อนข้อมูล" - driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์" badge: "ตรา" messageBg: "พื้นหลังแชท" fgHighlighted: "ข้อความที่ไฮไลต์" @@ -2016,6 +2234,7 @@ _sfx: noteMy: "โน้ตของตัวเอง" notification: "การเเจ้งเตือน" reaction: "เมื่อเลือกรีแอคชั่น" + chatMessage: "ข้อความของแชต" _soundSettings: driveFile: "ใช้เสียงจากไดรฟ์" driveFileWarn: "เลือกไฟล์ในไดรฟ์ของคุณ" @@ -2058,15 +2277,15 @@ _2fa: step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า" setupCompleted: "ตั้งค่าสำเร็จแล้ว" step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว" - securityKeyNotSupported: "เบราว์เซอร์ของคุณไม่รองรับคีย์ความปลอดภัยนะ" - registerTOTPBeforeKey: "กรุณาตั้งค่าแอปยืนยันตัวตนเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" - securityKeyInfo: "นอกจากนี้การตรวจสอบความถูกต้องด้วยลายนิ้วมือหรือ PIN แล้ว คุณยังสามารถตั้งค่าการตรวจสอบสิทธิ์ผ่านคีย์ความปลอดภัยของฮาร์ดแวร์ที่รองรับ FIDO2 เพื่อเพิ่มความปลอดภัยให้กับบัญชีของคุณ" - registerSecurityKey: "ลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" + securityKeyNotSupported: "เว็บเบราว์เซอร์ที่ใช้งานอยู่ไม่รองรับ Security Key" + registerTOTPBeforeKey: "ก่อนลงทะเบียน Security Key หรือ Passkey กรุณาตั้งค่าแอปยืนยันตัวตนก่อน" + securityKeyInfo: "ลงทะเบียนกุญแจที่มาจาก WebAuthn เช่น Security Key แบบฮาร์ดแวร์ที่รองรับ FIDO2 การยืนยันตัวตนด้วยชีวมิติหรือ PIN บนอุปกรณ์ และ Passkey" + registerSecurityKey: "ลงทะเบียน Security Key หรือ Passkey" securityKeyName: "ป้อนชื่อคีย์" - tapSecurityKey: "กรุณาทำตามเบราว์เซอร์ของคุณเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" - removeKey: "ลบคีย์ความปลอดภัยออก" + tapSecurityKey: "กรุณาทำตามคำแนะนำของเบราว์เซอร์เพื่อลงทะเบียน Security Key หรือ Passkey" + removeKey: "ลบ Security Key ออก" removeKeyConfirm: "ลบข้อมูลสำรอง {name} มั้ย?" - whyTOTPOnlyRenew: "ไม่สามารถลบแอปตัวรับรองความถูกต้องได้ตราบใดที่มีการลงทะเบียนคีย์ความปลอดภัยไว้แล้ว" + whyTOTPOnlyRenew: "ไม่สามารถลบแอปตัวรับรองความถูกต้องได้ตราบใดที่ยังมีการลงทะเบียน Security Key อยู่" renewTOTP: "ตั้งค่าแอปยืนยันตัวตน" renewTOTPConfirm: "วิธีการแบบนี้จะทําให้รหัสยืนยันจากแอพก่อนหน้าของคุณหยุดทํางานเลยนะ" renewTOTPOk: "ตั้งค่าคอนฟิกใหม่" @@ -2163,6 +2382,7 @@ _permissions: "read:federation": "รับข้อมูลเกี่ยวกับสหพันธ์" "write:report-abuse": "รายงานการละเมิด" "write:chat": "เขียนหรือลบข้อความแชท" + "read:chat": "อ่านแชต" _auth: shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน" shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?" @@ -2171,8 +2391,11 @@ _auth: permissionAsk: "แอปพลิเคชันนี้ขอสิทธิ์ดังต่อไปนี้" pleaseGoBack: "กรุณากลับไปที่แอปพลิเคชัน" callback: "กำลังกลับไปที่แอปพลิเคชัน" + accepted: "การเข้าถึงได้รับอนุญาต" denied: "ปฏิเสธการเข้าใช้" + scopeUser: "กำลังดำเนินการในฐานะผู้ใช้ต่อไปนี้" pleaseLogin: "กรุณาเข้าสู่ระบบเพื่ออนุมัติแอปพลิเคชัน" + byClickingYouWillBeRedirectedToThisUrl: "หากอนุญาตการเข้าถึง ระบบจะเปลี่ยนเส้นทางไปยัง URL ด้านล่างโดยอัตโนมัติ" _antennaSources: all: "โน้ตทั้งหมด" homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม" @@ -2218,6 +2441,7 @@ _widgets: chooseList: "เลือกรายชื่อ" clicker: "คลิกเกอร์" birthdayFollowings: "วันเกิดผู้ใช้ในวันนี้" + chat: "แชต" _cw: hide: "ซ่อน" show: "โหลดเพิ่มเติม" @@ -2257,6 +2481,8 @@ _visibility: disableFederation: "การปิดใช้งานสหพันธ์" disableFederationDescription: "อย่าส่งข้อมูลไปยังเซิร์ฟเวอร์อื่น" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "มีไฟล์ที่ยังไม่ได้อัปโหลด ต้องการละทิ้งและปิดฟอร์มหรือไม่?" + uploaderTip: "ไฟล์ยังไม่ได้อัปโหลด สามารถตั้งค่าต่างๆ ได้จากเมนูของไฟล์ เช่น การเปลี่ยนชื่อ การครอปรูป การใส่ลายน้ำ และการบีบอัด ไฟล์จะถูกอัปโหลดโดยอัตโนมัติเมื่อโพสต์โน้ต" replyPlaceholder: "ตอบกลับโน้ตนี้..." quotePlaceholder: "อ้างโน้ตนี้..." channelPlaceholder: "โพสต์ลงช่อง..." @@ -2277,7 +2503,7 @@ _profile: metadataDescription: "ใช้สิ่งเหล่านี้ คุณสามารถแสดงฟิลด์ข้อมูลเพิ่มเติมในโปรไฟล์ของคุณ" metadataLabel: "ป้ายชื่อ" metadataContent: "เนื้อหา" - changeAvatar: "เปลี่ยนอวาตาร์" + changeAvatar: "เปลี่ยนไอคอนประจำตัว" changeBanner: "เปลี่ยนแบนเนอร์" verifiedLinkDescription: "หากป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณ ไอคอนการยืนยันความเป็นเจ้าของจะแสดงถัดจากฟิลด์นั้น ๆ" avatarDecorationMax: "คุณสามารถเพิ่มการตกแต่งได้สูงสุด {max}" @@ -2290,7 +2516,7 @@ _exportOrImport: clips: "คลิป" followingList: "กำลังติดตาม" muteList: "ปิดเสียง" - blockingList: "บล็อค" + blockingList: "บล็อก" userLists: "รายชื่อ" excludeMutingUsers: "ยกเว้นผู้ใช้ที่ปิดเสียง" excludeInactiveUsers: "ยกเว้นผู้ใช้ที่ไม่ได้ใช้งาน" @@ -2360,7 +2586,7 @@ _pages: featured: "เป็นที่นิยม" inspector: "ตัวตรวจสอบ" contents: "เนื้อหา" - content: "บล็อคหน้าเพจ" + content: "บล็อกหน้าเพจ" variables: "ตัวแปร" title: "หัวข้อ" url: "URL ของหน้า" @@ -2372,7 +2598,7 @@ _pages: fontSansSerif: "Sans Serif" eyeCatchingImageSet: "ตั้งค่าภาพขนาดย่อ" eyeCatchingImageRemove: "ลบภาพขนาดย่อ" - chooseBlock: "เพิ่มบล็อค" + chooseBlock: "เพิ่มบล็อก" enterSectionTitle: "ป้อนชื่อหัวข้อ" selectType: "เลือกชนิด" contentBlocks: "เนื้อหา" @@ -2408,6 +2634,7 @@ _notification: newNote: "โพสต์ใหม่" unreadAntennaNote: "เสาอากาศ {name}" roleAssigned: "ได้รับบทบาท" + chatRoomInvitationReceived: "ได้รับคำเชิญเข้าร่วมห้องแชต" emptyPushNotificationMessage: "อัปเดตการแจ้งเตือนแบบพุชแล้ว" achievementEarned: "รับความสำเร็จ" testNotification: "ทดสอบการแจ้งเตือน" @@ -2420,7 +2647,9 @@ _notification: followedBySomeUsers: "มีผู้ติดตาม {n} ราย" flushNotification: "ล้างประวัติการแจ้งเตือน" exportOfXCompleted: "การดำเนินการส่งออก {x} ได้เสร็จสิ้นลงแล้ว" - login: "มีคนล็อกอิน" + login: "มีการเข้าสู่ระบบ" + createToken: "สร้างโทเค็นการเข้าถึงแล้ว" + createTokenDescription: "หากไม่ทราบสาเหตุของคำเชิญ กรุณาลบโทเค็นการเข้าถึงผ่านทาง “{text}”" _types: all: "ทั้งหมด" note: "โน้ตใหม่" @@ -2434,9 +2663,11 @@ _notification: receiveFollowRequest: "ได้รับคำร้องขอติดตาม" followRequestAccepted: "อนุมัติให้ติดตามแล้ว" roleAssigned: "ให้บทบาท" + chatRoomInvitationReceived: "เชิญเข้าห้องแชต" achievementEarned: "ปลดล็อกความสำเร็จแล้ว" exportCompleted: "กระบวนการส่งออกข้อมูลได้เสร็จสิ้นสมบูรณ์แล้ว" login: "เข้าสู่ระบบ" + createToken: "สร้างโทเค็นการเข้าถึง" test: "ทดสอบระบบแจ้งเตือน" app: "การแจ้งเตือนจากแอปที่มีลิงก์" _actions: @@ -2446,6 +2677,9 @@ _notification: _deck: alwaysShowMainColumn: "แสดงคอลัมน์หลักเสมอ" columnAlign: "จัดแนวคอลัมน์" + columnGap: "ช่องห่างระว่างคอลัมน์" + deckMenuPosition: "ตำแหน่งเมนูเด็ค" + navbarPosition: "ตำแหน่งของแถบนำทาง" addColumn: "เพิ่มคอลัมน์" newNoteNotificationSettings: "ตั้งค่าการแจ้งเตือนเมื่อมีโน้ตใหม่" configureColumn: "ตั้งค่าคอลัมน์" @@ -2464,6 +2698,7 @@ _deck: useSimpleUiForNonRootPages: "แสดง UI ของ Root Page อย่างง่าย " usedAsMinWidthWhenFlexible: "ความกว้างขั้นต่ำนั้นจะถูกใช้งานสำหรับสิ่งนี้เมื่อเปิดใช้งานตัวเลือก \"ปรับความกว้างอัตโนมัติ\" หากเลือกเปิดใช้งานแล้ว" flexible: "ปรับความกว้างอัตโนมัติ" + enableSyncBetweenDevicesForProfiles: "เปิดใช้งานการซิงค์ข้อมูลโปรไฟล์ระหว่างอุปกรณ์" _columns: main: "หลัก" widgets: "วิดเจ็ต" @@ -2475,6 +2710,7 @@ _deck: mentions: "กล่าวถึงคุณ" direct: "ไดเร็กต์" roleTimeline: "บทบาทไทม์ไลน์" + chat: "แชต" _dialog: charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}" @@ -2503,8 +2739,8 @@ _webhookSettings: abuseReport: "เมื่อมีการรายงานจากผู้ใช้" abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้" userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น" - inactiveModeratorsWarning: "เมื่อผู้ดูแลระบบไม่ได้ใช้งานมานานระยะหนึ่ง" - inactiveModeratorsInvitationOnlyChanged: "เมื่อผู้ดูแลระบบที่ไม่ได้ใช้งานมานาน และเซิร์ฟเวอร์เปลี่ยนเป็นแบบเชิญเข้าร่วมเท่านั้น" + inactiveModeratorsWarning: "เมื่อผู้ควบคุมไม่มีความเคลื่อนไหวในช่วงระยะเวลาหนึ่ง" + inactiveModeratorsInvitationOnlyChanged: "เมื่อผู้ควบคุมไม่มีความเคลื่อนไหวในช่วงระยะเวลาหนึ่ง ระบบจะเปลี่ยนเป็นแบบใช้คำเชิญโดยอัตโนมัติ" deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?" testRemarks: "คลิกปุ่มทางด้านขวาของสวิตช์เพื่อส่ง Webhook ทดสอบที่มีข้อมูลจำลอง" _abuseReport: @@ -2556,10 +2792,10 @@ _moderationLogTypes: createAd: "สร้างโฆษณาแล้ว" deleteAd: "ลบโฆษณาออกแล้ว" updateAd: "อัปเดตโฆษณาแล้ว" - createAvatarDecoration: "สร้างการตกแต่งไอคอนแล้ว" - updateAvatarDecoration: "อัปเดตการตกแต่งไอคอนแล้ว" - deleteAvatarDecoration: "ลบการตกแต่งไอคอนแล้ว" - unsetUserAvatar: "ลบไอคอนผู้ใช้" + createAvatarDecoration: "สร้างของตกแต่งไอคอนแล้ว" + updateAvatarDecoration: "อัปเดตของตกแต่งไอคอนแล้ว" + deleteAvatarDecoration: "ลบของตกแต่งไอคอนแล้ว" + unsetUserAvatar: "เลิกตั้งไอคอนประจำตัวแล้ว" unsetUserBanner: "ลบแบนเนอร์ผู้ใช้" createSystemWebhook: "สร้าง SystemWebhook" updateSystemWebhook: "อัปเดต SystemWebhook" @@ -2571,6 +2807,8 @@ _moderationLogTypes: deletePage: "เพจถูกลบออกไปแล้ว" deleteFlash: "Play ถูกลบออกไปแล้ว" deleteGalleryPost: "โพสต์แกลเลอรี่ถูกลบออกแล้ว" + deleteChatRoom: "ลบห้องแชต" + updateProxyAccountDescription: "อัปเดตคำอธิบายของบัญชีพร็อกซี" _fileViewer: title: "รายละเอียดไฟล์" type: "ประเภทไฟล์" @@ -2578,6 +2816,7 @@ _fileViewer: url: "URL" uploadedAt: "วันที่เข้าร่วม" attachedNotes: "โน้ตที่แนบมาด้วย" + usage: "ใช้แล้ว" thisPageCanBeSeenFromTheAuthor: "หน้าเพจนี้จะสามารถปรากฏได้โดยผู้ใช้ที่อัปโหลดไฟล์นี้เท่านั้น" _externalResourceInstaller: title: "ติดตั้งจากไซต์ภายนอก" @@ -2623,11 +2862,14 @@ _dataSaver: title: "โหลดสื่อ" description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด" _avatar: - title: "รูปไอคอน" - description: "ระงับการเคลื่อนไหวของภาพไอคอน ภาพเคลื่อนไหวอาจมีขนาดไฟล์ใหญ่กว่าภาพปกติ ดังนั้นจึงสามารถช่วยในการลดการใช้ข้อมูล" - _urlPreview: - title: "ธัมบ์เนลแสดงตัวอย่าง URL" - description: "ธัมบ์เนลแสดงตัวอย่าง URL จะไม่โหลดโดยอัตโนมัติ" + title: "ปิดใช้งานภาพเคลื่อนไหวของไอคอนประจำตัว" + description: "ภาพเคลื่อนไหวของไอคอนประจำตัวจะหยุดทำงาน ภาพแบบเคลื่อนไหวมักมีขนาดไฟล์ใหญ่กว่าภาพปกติ จึงช่วยลดปริมาณการใช้ข้อมูลได้มากขึ้น" + _urlPreviewThumbnail: + title: "ซ่อนภาพขนาดย่อของการแสดงตัวอย่าง URL" + description: "ภาพขนาดย่อของการตัวอย่าง URL จะไม่ถูกโหลดอีกต่อไป" + _disableUrlPreview: + title: "ปิดการใช้งานแสดงตัวอย่าง URL" + description: "ปิดฟังก์ชันแสดงตัวอย่าง URL แตกต่างจากการซ่อนเพียงภาพขนาดย่อ ฟังก์ชันนี้จะช่วยลดการโหลดข้อมูลจากลิงก์ปลายทางทั้งหมด" _code: title: "ไฮไลต์โค้ด" description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" @@ -2678,13 +2920,15 @@ _reversi: allowIrregularRules: "อนุญาตกฎที่ไม่ปรกติ (โหมดฟรีทุกอย่าง)" disallowIrregularRules: "ไม่อนุญาตกฎที่ไม่ปรกติ" showBoardLabels: "แสดงหมายเลขแถว/คอลัมน์บนกระดาน" - useAvatarAsStone: "ใช้รูปอวตารเป็นหมาก" + useAvatarAsStone: "ใช้ไอคอนประจำตัวเป็นหมาก" _offlineScreen: title: "ออฟไลน์ - ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" header: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" _urlPreviewSetting: title: "การตั้งค่าการแสดงตัวอย่าง URL" enable: "เปิดใช้งานการแสดงตัวอย่าง URL" + allowRedirect: "อนุญาตการเปลี่ยนเส้นทางไปยังปลายทางของการแสดงตัวอย่าง" + allowRedirectDescription: "ตั้งค่าว่าจะติดตามลิงก์ที่เปลี่ยนเส้นทาง (redirect) เพื่อแสดงตัวอย่างหรือไม่ เมื่อมีการป้อน URL ที่มีการเปลี่ยนเส้นทาง หากปิดการใช้งาน จะช่วยประหยัดทรัพยากรของเซิร์ฟเวอร์ แต่จะไม่สามารถแสดงเนื้อหาจากปลายทางที่เปลี่ยนเส้นทางได้" timeout: "เวลาจำกัดในการโหลดตัวอย่าง URL (ms)" timeoutDescription: "หากเวลาที่ใช้ในการโหลดเกินค่านี้ จะไม่มีการสร้างการแสดงตัวอย่าง" maximumContentLength: "ค่าสูงสุดของ Content-Length (byte)" @@ -2705,6 +2949,62 @@ _contextMenu: app: "แอปพลิเคชัน" appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)" native: "UI ของเบราว์เซอร์" +_gridComponent: + _error: + requiredValue: "ค่านี้จำเป็นต้องกรอก" + columnTypeNotSupport: "การตรวจสอบค่าด้วย regex รองรับเฉพาะคอลัมน์ที่เป็น type:text" + patternNotMatch: "ค่านี้ไม่ตรงกับรูปแบบ {pattern}" + notUnique: "ค่านี้ต้องไม่ซ้ำกับค่าที่มีอยู่" +_roleSelectDialog: + notSelected: "ยังไม่มีการเลือก" +_customEmojisManager: + _gridCommon: + copySelectionRows: "คัดลอกแถวที่เลือกไว้" + copySelectionRanges: "คัดลือกที่เลือกไว้" + deleteSelectionRows: "ลบแถวที่เลือกไว้" + deleteSelectionRanges: "ล้างค่าช่วงที่เลือก" + searchSettings: "ตั้งค่าการค้นหา" + searchSettingCaption: "ตั้งค่าเงื่อนไขการค้นหาอย่างละเอียด" + searchLimit: "จำนวนรายการที่แสดง" + sortOrder: "ลำดับการเรียง" + registrationLogs: "ปูมการลงทะเบียน" + registrationLogsCaption: "จะแสดงปูมเมื่อมีการอัปเดตหรือลบเอโมจิ หากดำเนินการอัปเดต/ลบ หรือเปลี่ยนหน้า/รีโหลด หน้านี้ ปูมจะหายไป" + alertEmojisRegisterFailedDescription: "การอัปเดตหรือลบเอโมจิล้มเหลว กรุณาตรวจสอบรายละเอียดในปูมการลงทะเบียน" + _logs: + showSuccessLogSwitch: "แสดงปูมที่สำเร็จ" + failureLogNothing: "ไม่มีปูมความล้มเหลว" + logNothing: "ไม่มีปูม" + _remote: + selectionRowDetail: "รายละเอียดของแถวที่เลือก" + importSelectionRows: "นำเข้าแถวที่เลือก" + importSelectionRangesRows: "นำเข้าแถวในช่วงที่เลือก" + importEmojisButton: "นำเข้าเอโมจิที่ทำเครื่องหมายไว้" + confirmImportEmojisTitle: "นำเข้าเอโมจิ" + confirmImportEmojisDescription: "จะนำเข้าเอโมจิ {count} รายการที่ได้รับจากระยะไกล ทั้งนี้โปรดระมัดระวังเรื่องสิทธิ์การใช้งานเอโมจิ ดำเนินการหรือไม่?" + _local: + tabTitleList: "รายการเอโมจิที่ลงทะเบียนไว้แล้ว" + tabTitleRegister: "ลงทะเบียนเอโมจิ" + _list: + emojisNothing: "ยังไม่มีเอโมจิที่ลงทะเบียนไว้" + markAsDeleteTargetRows: "กำหนดแถวที่เลือกให้เป็นรายการสำหรับลบ" + markAsDeleteTargetRanges: "กำหนดช่วงแถวที่เลือกให้เป็นรายการสำหรับลบ" + alertUpdateEmojisNothingDescription: "ไม่มีการเปลี่ยนแปลงเอโมจิ" + alertDeleteEmojisNothingDescription: "ไม่มีเอโมจิที่อยู่ในรายการสำหรับลบ" + confirmMovePage: "ต้องการเปลี่ยนหน้าหรือไม่?" + confirmChangeView: "ต้องการเปลี่ยนการแสดงผลหรือไม่?" + confirmUpdateEmojisDescription: "จะอัปเดตเอโมจิ {count} รายการ ดำเนินการหรือไม่?" + confirmDeleteEmojisDescription: "จะลบเอโมจิที่ถูกทำเครื่องหมายไว้ {count} รายการ ดำเนินการหรือไม่?" + confirmResetDescription: "การเปลี่ยนแปลงทั้งหมดที่ทำมาจะถูกรีเซ็ต" + confirmMovePageDesciption: "มีการเปลี่ยนแปลงเอโมจิในหน้านี้ หากเปลี่ยนหน้าโดยไม่บันทึก การเปลี่ยนแปลงทั้งหมดจะถูกละทิ้ง" + dialogSelectRoleTitle: "ค้นหาบทบาทที่ตั้งค่าไว้ด้วยเอโมจิ" + _register: + uploadSettingTitle: "ตั้งค่าการอัปโหลด" + uploadSettingDescription: "สามารถกำหนดพฤติกรรมขณะอัปโหลดเอโมจิจากหน้าจอนี้ได้" + directoryToCategoryLabel: "ป้อนชื่อไดเรกทอรีเป็น \"category\"" + directoryToCategoryCaption: "เมื่อทำการลากและวางไดเรกทอรี ชื่อจะถูกป้อนเป็น \"category\"" + confirmRegisterEmojisDescription: "จะลงทะเบียนเอโมจิที่แสดงในรายการเป็นเอโมจิแบบกำหนดเองใหม่\nดำเนินการต่อหรือไม่? (เพื่อหลีกเลี่ยงภาระโหลดหนัก ระบบจะสามารถลงทะเบียนอีโมจิได้สูงสุด {count} รายการต่อครั้ง)" + confirmClearEmojisDescription: "ต้องการยกเลิกการแก้ไขและล้างรายการเอโมจิที่แสดงอยู่หรือไม่?" + confirmUploadEmojisDescription: "จะอัปโหลดไฟล์ {count} รายการที่ลากและวางไปยังไดรฟ์ ดำเนินการหรือไม่?" _embedCodeGen: title: "ปรับแต่งโค้ดฝัง" header: "แสดงส่วนหัว" @@ -2719,10 +3019,190 @@ _embedCodeGen: generateCode: "สร้างโค้ดสำหรับการฝัง" codeGenerated: "รหัสถูกสร้างขึ้นแล้ว" codeGeneratedDescription: "นำโค้ดที่สร้างแล้วไปวางในเว็บไซต์ของคุณเพื่อฝังเนื้อหา" +_selfXssPrevention: + warning: "คำเตือน" + title: "“ข้อความที่บอกให้วางบางอย่างในหน้าจอนี้” ทั้งหมดเป็นการหลอกลวง" + description1: "ถ้าวางบางอย่างที่นี่ อาจทำให้ผู้ไม่หวังดีเข้าควบคุมบัญชี หรือขโมยข้อมูลส่วนตัวได้" + description2: "ถ้าไม่เข้าใจอย่างชัดเจนว่าสิ่งที่กำลังจะวางคืออะไร %cให้หยุดการทำงานทันทีแล้วปิดหน้าต่างนี้" + description3: "ดูรายละเอียดเพิ่มเติมได้ที่นี่: {link}" +_followRequest: + recieved: "คำขอที่ได้รับ" + sent: "คำที่ส่งไป" _remoteLookupErrors: + _federationNotAllowed: + title: "ไม่สามารถสื่อสารกับเซิร์ฟเวอร์นี้ได้" + description: "การสื่อสารกับเซิร์ฟเวอร์นี้อาจถูกปิดใช้งาน หรือเซิร์ฟเวอร์นี้อาจจะได้บล็อกคุณ หรือคุณอาจจะได้บล็อกเซิร์ฟเวอร์นี้อยู่\nกรุณาติดต่อผู้ดูแลระบบเซิร์ฟเวอร์เพื่อสอบถามรายละเอียดเพิ่มเติม" + _uriInvalid: + title: "URI ไม่ถูกต้อง" + description: "มีปัญหาเกี่ยวกับ URI ที่ป้อน โปรดตรวจสอบว่าไม่มีอักขระที่ไม่สามารถใช้กับ URI" + _requestFailed: + title: "การร้องขอล้มเหลว" + description: "การสื่อสารกับเซิร์ฟเวอร์นี้ล้มเหลว เซิร์ฟเวอร์ปลายทางอาจล่ม หรืออาจป้อน URI ที่ไม่ถูกต้องหรือไม่มีอยู่" + _responseInvalid: + title: "ข้อมูลตอบสนองกลับไม่ถูกต้อง" + description: "สามารถเชื่อมต่อกับเซิร์ฟเวอร์นี้ได้ แต่ข้อมูลที่ได้รับไม่ถูกต้อง หากกำลังดึงข้อมูลจากเซิร์ฟเวอร์บุคคลที่สาม โปรดใช้ URI ที่สามารถดึงข้อมูลได้จากเซิร์ฟเวอร์ต้นทางโดยตรง" _noSuchObject: title: "ไม่พบหน้าที่ต้องการ" + description: "ไม่พบทรัพยากรที่ร้องขอ กรุณาตรวจสอบ URI อีกครั้ง" +_captcha: + verify: "กรุณาผ่าน CAPTCHA" + testSiteKeyMessage: "สามารถดูตัวอย่างได้โดยป้อนค่าทดสอบใน site key และ secret key\nดูรายละเอียดเพิ่มเติมได้ที่หน้าด้านล่างนี้" + _error: + _requestFailed: + title: "การร้องขอ CAPTCHA ล้มเหลว" + text: "โปรดลองใหม่ภายหลัง หรือ ตรวจสอบการตั้งค่าอีกครั้ง" + _verificationFailed: + title: "การยืนยัน CAPTCHA ล้มเหลว" + text: "กรุณาตรวจสอบอีกครั้งว่าการตั้งค่าถูกต้องหรือไม่" + _unknown: + title: "CAPTCHA เกิดข้อผิดพลาด" + text: "เกิดข้อผิดพลาดที่ไม่คาดคิด" +_bootErrors: + title: "การโหลดล้มเหลว" + serverError: "หากปัญหายังคงอยู่แม้ว่าจะรอสักครู่แล้วโหลดหน้าใหม่อีกครั้ง โปรดติดต่อผู้ดูแลระบบเซิร์ฟเวอร์พร้อมรหัสข้อผิดพลาดต่อไปนี้" + solution: "สิ่งต่อไปนี้อาจช่วยแก้ไขปัญหาได้" + solution1: "อัปเดตเบราว์เซอร์และระบบปฏิบัติการเป็นรุ่นล่าสุด" + solution2: "ปิดใช้งานตัวบล็อกโฆษณา" + solution3: "ล้างแคชเบราว์เซอร์" + solution4: "(Tor Browser) ตั้งค่า dom.webaudio.enabled เป็น true" + otherOption: "ตัวเลือกเพิ่มเติม" + otherOption1: "ลบการตั้งค่าและแคชของไคลเอนต์" + otherOption2: "เริ่มใช้งานไคลเอนต์แบบง่าย" + otherOption3: "เปิดเครื่องมือซ่อมแซม" _search: searchScopeAll: "ทั้งหมด" searchScopeLocal: "ท้องถิ่น" + searchScopeServer: "ระบุเซิร์ฟเวอร์" searchScopeUser: "ผู้ใช้เฉพาะ" + pleaseEnterServerHost: "กรุณากรอกโฮสต์ของเซิร์ฟเวอร์" + pleaseSelectUser: "กรุณาเลือกผู้ใช้" + serverHostPlaceholder: "ตัวอย่าง: misskey.example.com" +_serverSetupWizard: + installCompleted: "การติดตั้ง Misskey เสร็จสมบูรณ์แล้ว!" + firstCreateAccount: "ขั้นแรก ให้สร้างบัญชีผู้ดูแลระบบ" + accountCreated: "บัญชีผู้ดูแลระบบถูกสร้างขึ้นแล้ว!" + serverSetting: "การตั้งค่าเซิร์ฟเวอร์" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "สามารถตั้งค่าเซิร์ฟเวอร์ได้อย่างง่ายดายด้วยวิซาร์ดนี้" + settingsYouMakeHereCanBeChangedLater: "สามารถเปลี่ยนแปลงการตั้งค่าเหล่านี้ในภายหลังได้" + howWillYouUseMisskey: "ต้องการใช้ Misskey อย่างไร?" + _use: + single: "เซิร์ฟเวอร์คนเดียว" + single_description: "ใช้งานเป็นเซิร์ฟเวอร์ส่วนตัวสำหรับตัวเองคนเดียว" + single_youCanCreateMultipleAccounts: "แม้จะใช้งานเป็นเซิร์ฟเวอร์ส่วนตัวสำหรับคนเดียว ก็สามารถสร้างบัญชีผู้ใช้หลายบัญชีได้ตามความจำเป็น" + group: "เซิร์ฟเวอร์กลุ่ม" + group_description: "เชิญผู้ใช้ที่เชื่อถือได้ มาเข้าร่วมใช้งานแบบหลายคน" + open: "เซิร์ฟเวอร์สาธารณะ" + open_description: "เปิดรับผู้ใช้จำนวนมากแบบไม่จำกัด" + openServerAdvice: "การเปิดรับผู้ใช้จำนวนมากมีความเสี่ยง ควรบริหารจัดการด้วยระบบดูแลที่เข้มงวดเพื่อรับมือกับปัญหาที่อาจเกิดขึ้น" + openServerAntiSpamAdvice: "เพื่อป้องกันไม่ให้เซิร์ฟเวอร์ของตนกลายเป็นแหล่งส่งสแปม ควรเปิดใช้งานฟีเจอร์ป้องกันบอต เช่น reCAPTCHA และใส่ใจเรื่องความปลอดภัยอย่างเคร่งครัด" + howManyUsersDoYouExpect: "คาดว่าจะมีผู้ใช้งานประมาณกี่คน?" + _scale: + small: "น้อยกว่า 100 คน (ขนาดเล็ก)" + medium: "เกิน 100 คน แต่น้อยกว่า 1000 คน (ขนาดกลาง)" + large: "เกิน 1000 คน (ขนาดใหญ่)" + largeScaleServerAdvice: "เซิร์ฟเวอร์ขนาดใหญ่อาจต้องการความรู้ด้านโครงสร้างพื้นฐานขั้นสูง เช่น การบาลานซ์โหลด หรือการทำสำเนาฐานข้อมูล" + doYouConnectToFediverse: "เชื่อมต่อกับ Fediverse หรือไม่?" + doYouConnectToFediverse_description1: "หากเชื่อมต่อกับเครือข่ายที่ประกอบด้วยเซิร์ฟเวอร์แบบกระจาย (Fediverse) จะสามารถแลกเปลี่ยนเนื้อหากับเซิร์ฟเวอร์อื่นๆ ได้" + doYouConnectToFediverse_description2: "การเชื่อมต่อกับ Fediverse เรียกว่า “สหพันธ์”" + youCanConfigureMoreFederationSettingsLater: "หลังจากนี้ยังสามารถตั้งค่าแบบขั้นสูง เช่น การกำหนดเซิร์ฟเวอร์ที่อนุญาตให้สหพันธ์ต่อกันได้เพิ่มเติม" + remoteContentsCleaning: "การล้างข้อมูลเนื้อหาที่ได้รับโดยอัตโนมัติ" + remoteContentsCleaning_description: "เมื่อมีการเชื่อมโยงสหพันธ์ จะได้รับเนื้อหาเป็นจำนวนมากอย่างต่อเนื่อง เมื่อเปิดใช้งานการล้างข้อมูลอัตโนมัติ จะทำการลบเนื้อหาเก่าที่ไม่ถูกอ้างอิง ไปจากเซิร์ฟเวอร์โดยอัตโนมัติ เพื่อประหยัดพื้นที่จัดเก็บข้อมูล" + adminInfo: "ข้อมูลผู้ดูแลระบ" + adminInfo_description: "ตั้งค่าข้อมูลผู้ดูแลระบบที่จะใช้รับคำถามและติดต่อ" + adminInfo_mustBeFilled: "หากเปิดใช้เซิร์ฟเวอร์สาธารณะ หรือเปิดใช้งานสหพันธ์ จะต้องกรอกข้อมูลนี้" + followingSettingsAreRecommended: "แนะนำให้ตั้งค่าตามด้านล่างนี้" + applyTheseSettings: "ใช้การตั้งค่านี้" + skipSettings: "ข้ามการตั้งค่า" + settingsCompleted: "การตั้งค่าเสร็จสมบูรณ์แล้ว!" + settingsCompleted_description: "ขอบคุณที่สละเวลามาตั้งค่า ตอนนี้เซิร์ฟเวอร์พร้อมใช้งานได้ทันที" + settingsCompleted_description2: "การตั้งค่าเซิร์ฟเวอร์อย่างละเอียดสามารถทำได้จาก “แผงควบคุม”" + donationRequest: "คำขอรับบริจาค" + _donationRequest: + text1: "Misskey เป็นซอฟต์แวร์ฟรีที่พัฒนาโดยอาสาสมัคร" + text2: "เพื่อให้การพัฒนางานนี้สามารถดำเนินต่อไปได้ในอนาคต หากไม่เป็นการรบกวน รบกวนพิจารณาร่วมสมทบทุนด้วยนะคะ" + text3: "นอกจากนี้ยังมีสิทธิพิเศษสำหรับผู้สนับสนุนอีกด้วยค่ะ" +_uploader: + editImage: "แก้ไขรูปภาพ" + compressedToX: "บีบอัดเป็น {x}" + savedXPercent: "ประหยัดไป {x}%" + abortConfirm: "มีไฟล์ที่ยังไม่ได้อัปโหลด ต้องการยกเลิกหรือไม่?" + doneConfirm: "มีไฟล์ที่ยังไม่ได้อัปโหลด ต้องการดำเนินการให้เสร็จสิ้นหรือไม่?" + maxFileSizeIsX: "ขนาดไฟล์สูงสุดที่สามารถอัปโหลดได้คือ {x}" + allowedTypes: "ประเภทไฟล์ที่สามารถอัปโหลดได้" + tip: "ยังไม่มีไฟล์ถูกอัปโหลด สามารถ ตรวจสอบ ลบชื่อไฟล์ บีบอัด หรือครอปตัดภาพ ก่อนอัปโหลดได้ในหน้านี้ เมื่อพร้อมแล้วให้กดปุ่ม “อัปโหลด” เพื่อเริ่มการอัปโหลด" +_clientPerformanceIssueTip: + title: "หากรู้สึกว่าแบตเตอรี่หมดเร็ว" + makeSureDisabledAdBlocker: "โปรดปิดการใช้งานตัวบล็อกโฆษณา" + makeSureDisabledAdBlocker_description: "ตัวบล็อกโฆษณาอาจส่งผลต่อประสิทธิภาพ โปรดตรวจสอบว่าไม่ได้เปิดใช้งานผ่านฟังก์ชันของระบบปฏิบัติการ เบราว์เซอร์ หรือส่วนเสริมใดๆ" + makeSureDisabledCustomCss: "โปรดปิดการใช้งาน CSS แบบกำหนดเอง" + makeSureDisabledCustomCss_description: "การเขียนทับสไตล์อาจส่งผลต่อประสิทธิภาพ โปรดตรวจสอบว่าไม่มี CSS แบบกำหนดเองหรือส่วนเสริมที่แก้ไขสไตล์เปิดใช้งานอยู่" + makeSureDisabledAddons: "โปรดปิดการใช้งานส่วนเสริม" + makeSureDisabledAddons_description: "ส่วนเสริมบางตัวอาจรบกวนการทำงานของไคลเอนต์และทำให้ประสิทธิภาพลดลง กรุณาลองปิดส่วนเสริมในเบราว์เซอร์แล้วตรวจสอบอีกครั้ง" +_clip: + tip: "คลิปเป็นฟังก์ชันที่สามารถรวมโน้ตเข้าด้วยกัน" +_userLists: + tip: "สามารถสร้างรายชื่อที่มีผู้ใช้ใดก็ได้ เมื่อสร้างแล้ว รายชื่อนั้นจะแสดงเป็นไทม์ไลน์ได้" +watermark: "ลายน้ำ" +defaultPreset: "พรีเซ็ตเริ่มต้น" +_watermarkEditor: + tip: "สามารถเพิ่มลายน้ำ เช่น ข้อมูลเครดิต ลงในภาพได้" + quitWithoutSaveConfirm: "ต้องการออกโดยไม่บันทึกหรือไม่?" + driveFileTypeWarn: "ไม่รองรับไฟล์นี้" + driveFileTypeWarnDescription: "กรุณาเลือกไฟล์ภาพ" + title: "แก้ไขลายน้ำ" + cover: "ซ้อนทับทั่วทั้งพื้นที่" + repeat: "ปูให้เต็มพื้นที่" + opacity: "ความทึบแสง" + scale: "ขนาด" + text: "ข้อความ" + position: "ตำแหน่ง" + type: "รูปแบบ" + image: "รูปภาพ" + advanced: "ขั้นสูง" + stripe: "ริ้ว" + stripeWidth: "ความกว้างเส้น" + stripeFrequency: "จำนวนเส้น" + angle: "แองเกิล" + polkadot: "ลายจุด" + checker: "ช่องตาราง" + polkadotMainDotOpacity: "ความทึบของจุดหลัก" + polkadotMainDotRadius: "ขนาดของจุดหลัก" + polkadotSubDotOpacity: "ความทึบของจุดรอง" + polkadotSubDotRadius: "ขนาดของจุดรอง" + polkadotSubDotDivisions: "จำนวนจุดรอง" +_imageEffector: + title: "เอฟเฟกต์" + addEffect: "เพิ่มเอฟเฟกต์" + discardChangesConfirm: "ต้องการทิ้งการเปลี่ยนแปลงแล้วออกหรือไม่?" + _fxs: + chromaticAberration: "ความคลาดสี" + glitch: "กลิตช์" + mirror: "กระจก" + invert: "กลับสี" + grayscale: "ขาวดำเทา" + colorAdjust: "ปรับแก้สี" + colorClamp: "บีบอัดสี" + colorClampAdvanced: "บีบอัดสี (ขั้นสูง)" + distort: "บิดเบี้ยว" + threshold: "สองสี" + zoomLines: "เส้นความเข้มข้น" + stripe: "ริ้ว" + polkadot: "ลายจุด" + checker: "ช่องตาราง" + blockNoise: "บล็อกที่มีการรบกวน" + tearing: "ฉีกขาด" +drafts: "ร่าง" +_drafts: + select: "เลือกฉบับร่าง" + cannotCreateDraftAnymore: "ถึงจำนวนจำกัดที่ฉบับร่างที่สามารถสร้างได้แล้ว" + cannotCreateDraft: "ไม่สามารถสร้างฉบับร่างด้วยเนื้อหานี้ได้" + delete: "ลบฉบับร่าง" + deleteAreYouSure: "ต้องการลบฉบับร่างหรือไม่?" + noDrafts: "ไม่มีฉบับร่าง" + replyTo: "ตอบกลับ {user}" + quoteOf: "อ้างอิงถึงโน้ตของ {user}" + postTo: "โพสต์ไปยัง {channel}" + saveToDraft: "บันทึกเป็นฉบับร่าง" + restoreFromDraft: "คืนค่าจากฉบับร่าง" + restore: "กู้คืน" + listDrafts: "รายการฉบับร่าง" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index f63dcc9467..8604cf1af6 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1,130 +1,145 @@ --- _lang_: "Türkçe" -headlineMisskey: "Notlarla bağlanmış bir ağ" -introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." -poweredByMisskeyDescription: "name}Açık kaynak bir platform\nMisskeyDünya'nın en sunucularında biri。" -monthAndDay: "{month}Ay {day}Gün" +headlineMisskey: "Notlarla birbirine bağlı bir ağ" +introMisskey: "Hoş geldiniz! Misskey, açık kaynaklı, merkezi olmayan bir mikroblog hizmetidir.\nDüşüncelerinizi çevrenizdeki herkesle paylaşmak için “notlar” oluşturun. 📡\n“Tepkiler” ile herkesin notları hakkındaki duygularınızı hızlıca ifade edebilirsiniz. 👍\nYeni bir dünyayı keşfedelim! 🚀" +poweredByMisskeyDescription: "{name}, açık kaynak platformu Misskey (kısaca “Misskey örneği” olarak anılır) tarafından desteklenen hizmetlerden biridir." +monthAndDay: "{month}/{day}" search: "Arama" -notifications: "Bildirim" +reset: "Sıfırla" +notifications: "Bildirimler" username: "Kullanıcı Adı" password: "Şifre" -initialPasswordForSetup: "" -forgotPassword: "şifremi unuttum" -fetchingAsApObject: "從聯邦宇宙取得中..." -ok: "TAMAM" -gotIt: "Anladım" +initialPasswordForSetup: "Kurulum için ilk şifre" +initialPasswordIsIncorrect: "Kurulum için ilk parola yanlış" +initialPasswordForSetupDescription: "Misskey'i kendiniz kurduysanız, yapılandırma dosyasına girdiğiniz parolayı kullanın.\nMisskey barındırma hizmeti kullanıyorsanız, verilen parolayı kullanın.\nParola belirlemediyseniz, devam etmek için boş bırakın." +forgotPassword: "Şifremi unuttum" +fetchingAsApObject: "Fediverse'den getiriliyor..." +ok: "Tamam" +gotIt: "Anladım!" cancel: "İptal" -noThankYou: "Hayır, teşekkürler" -enterUsername: "Kullanıcı adınızı giriniz" -renotedBy: "{user} tarafından Renotelandı" -noNotes: "Notlar mevcut değil." -noNotifications: "Bildirim bulunmuyor" +noThankYou: "Şimdi değil" +enterUsername: "Kullanıcı adını girin" +renotedBy: "{user} tarafından renot edildi" +noNotes: "Not yok" +noNotifications: "Bildirim yok" instance: "Sunucu" settings: "Ayarlar" notificationSettings: "Bildirim Ayarları" basicSettings: "Temel Ayarlar" otherSettings: "Diğer Ayarlar" -openInWindow: "Bir pencere ile aç" +openInWindow: "Pencerede aç" profile: "Profil" -timeline: "Zaman çizelgesi" -noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı" -login: "Giriş Yap " -loggingIn: "Oturum aç" +timeline: "Timeline" +noAccountDescription: "Bu kullanıcı henüz biyografisini yazmamış." +login: "Giriş Yap" +loggingIn: "Giriş yapılıyor" logout: "Çıkış Yap" -signup: "Kayıt Ol" -uploading: "Yükleniyor" +signup: "Kaydol" +uploading: "Yükleniyor..." save: "Kaydet" -users: "Kullanıcı" -addUser: "Kullanıcı Ekle" -favorite: "Favoriler" +users: "Kullanıcılar" +addUser: "Kullanıcı ekle" +favorite: "Favorilere ekle" favorites: "Favoriler" -unfavorite: "Favorilerden Kaldır" -favorited: "Favorilerime eklendi." -alreadyFavorited: "Zaten favorilerinizde kayıtlı." -cantFavorite: "Favorilere kayıt yapılamadı" -pin: "Sabitlenmiş" -unpin: "Sabitlemeyi kaldır" +unfavorite: "Favorilerden kaldır" +favorited: "Favorilere eklendi." +alreadyFavorited: "Zaten favorilere eklendi" +cantFavorite: "Favorilere ekleyemedim." +pin: "Profiline sabitle" +unpin: "Profilden sabitlemeyi kaldır" copyContent: "İçeriği kopyala" -copyLink: "Bağlantıyı Kopyala" -copyLinkRenote: "Turkish" +copyLink: "Linki kopyala" +copyRemoteLink: "Uzak linki kopyala" +copyLinkRenote: "Renote linkini kopyala" delete: "Sil" deleteAndEdit: "Sil ve yeniden düzenle" -deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." +deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek ister misiniz? Bu notla ilgili tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." addToList: "Listeye ekle" -addToAntenna: "Antene ekle" -sendMessage: "Mesaj Gönder" -copyRSS: "RSSKopyala" -copyUsername: "Kullanıcı Adını Kopyala" -copyUserId: "KullanıcıyıKopyala" -copyNoteId: "Kimlik notunu kopyala" -copyFileId: "Dosya ID'sini kopyala" -copyFolderId: "Klasör ID'sini kopyala" -copyProfileUrl: "Profil URL'sini kopyala" -searchUser: "Kullanıcıları ara" -reply: "yanıt" -loadMore: "Devamını yükle" -showMore: "Devamını yükle" +addToAntenna: "Anten'e ekle" +sendMessage: "Mesaj gönder" +copyRSS: "RSS kopyala" +copyUsername: "Kullanıcı adını kopyala" +copyUserId: "Kullanıcı ID'yi kopyala" +copyNoteId: "Not ID'yi kopyala" +copyFileId: "Dosya ID'yi kopyala" +copyFolderId: "Klasör ID'yi kopyala" +copyProfileUrl: "Profil URL kopyala" +searchUser: "Kullanıcı ara" +searchThisUsersNotes: "Bu kullanıcının notlarını ara" +reply: "Yanıtla" +loadMore: "Daha fazla yükle" +showMore: "Daha fazlasını göster" showLess: "Kapat" youGotNewFollower: "seni takip etti" receiveFollowRequest: "Takip isteği alındı" followRequestAccepted: "Takip isteği kabul edildi" mention: "Bahset" mentions: "Bahsetmeler" -directNotes: "Kişisel mesajlar" +directNotes: "Doğrudan notlar" importAndExport: "İçeri/Dışarı aktar" import: "İçeri aktar" export: "Dışa aktar" files: "Dosyalar" download: "İndir" -driveFileDeleteConfirm: "\"{name}\" dosyası silinsin mi? Dosya kullanıldığı tüm notlardan kaybolacaktır." -unfollowConfirm: "{name} takipten çıkarılsın mı?" -exportRequested: "Dışa aktarım talep ettiniz. Bu biraz zaman alabilir. İşlem bitince Sürücünüze eklenecektir." -importRequested: "Dışa aktarım talep ettiniz. Bu işlem biraz zaman alabilir." +driveFileDeleteConfirm: "“{name}” dosyasını silmek istediğinizden emin misiniz? Bu dosyaya ekli tüm notlar da silinecektir." +unfollowConfirm: "{name}'yi takipten çıkarmak istediğinizden emin misiniz?" +exportRequested: "Dışa aktarma işlemi talep ettiniz. Bu işlem biraz zaman alabilir. İşlem tamamlandığında Drive'ınıza eklenecektir." +importRequested: "İçe aktarma talebinde bulundunuz. Bu işlem biraz zaman alabilir." lists: "Listeler" -noLists: "Liste yok" -note: "not" -notes: "notlar" -following: "takipçi" -followers: "takipçi" -followsYou: "seni takip ediyor" +noLists: "Hiçbir listeniz yok." +note: "Not" +notes: "Notlar" +following: "Takip eden" +followers: "Takipçiler" +followsYou: "Sizi takip ediyor" createList: "Liste oluştur" -manageLists: "Yönetici Listeleri" -error: "hata" +manageLists: "Listeleri yönet" +error: "Hata" somethingHappened: "Bir hata oluştu" retry: "Tekrar dene" -pageLoadError: "Sayfa yüklenemedi." -pageLoadErrorDescription: "Bu genelde ağ veya tarayıcı ön belleği hatalarından olur. Lütfen ön belleği temizlemeyi veya birkaç dakika beklemeyi ve sayfayı yenilemeyi deneyin." -serverIsDead: "Sunucu yanıt vermiyor. Birkaç dakika sonra tekrar deneyin." -youShouldUpgradeClient: "Sayfayı görüntülemek için yenileyin." -enterListName: "Liste ismi" +pageLoadError: "Sayfa yüklenirken bir hata oluştu." +pageLoadErrorDescription: "Bu durum genellikle ağ hataları veya tarayıcının önbelleği nedeniyle oluşur. Önbelleği temizleyin ve bir süre bekledikten sonra tekrar deneyin." +serverIsDead: "Bu sunucu yanıt vermiyor. Lütfen bir süre bekleyin ve tekrar deneyin." +youShouldUpgradeClient: "Bu sayfayı görüntülemek için lütfen yenileyerek istemcinizi güncelleyin." +enterListName: "Listeye bir ad girin" privacy: "Gizlilik" -makeFollowManuallyApprove: "Takip istekleri elle onaylansın" +makeFollowManuallyApprove: "Takip istekleri onay gerektirir" defaultNoteVisibility: "Varsayılan görünürlük" -follow: "takipçi" -followRequest: "Takip isteği" +follow: "Takip et" +followRequest: "Takip isteği gönder" followRequests: "Takip istekleri" -unfollow: "takip etmeyi bırak" -followRequestPending: "Bekleyen Takip Etme Talebi" -enterEmoji: "Emoji Giriniz" -renote: "vazgeçme" -unrenote: "not alma" -renoted: "yeniden adlandırılmış" -cantRenote: "Ayrılamama" -cantReRenote: "not alabilirmiyim" -quote: "alıntı" -inChannelRenote: "Kanal içi Renote" -inChannelQuote: "Kanal içi Alıntı" -pinnedNote: "Sabitlenen" -pinned: "Sabitlenmiş" -you: "sen" -clickToShow: "Görüntülemek için tıkla" -sensitive: "Hassas içerik" +unfollow: "Takibi bırak" +followRequestPending: "Takip isteği beklemede" +enterEmoji: "Bir emoji girin" +renote: "Renote" +unrenote: "Renote'u kaldır" +renoted: "Renote edildi" +renotedToX: "{name} adına kayıtlıdır." +cantRenote: "Bu gönderi renote edilemez." +cantReRenote: "Bir renote yeniden renote edilemez." +quote: "Alıntı" +inChannelRenote: "Kanal içi renote" +inChannelQuote: "Kanal içi alıntı" +renoteToChannel: "Kanala not et" +renoteToOtherChannel: "Diğer kanala not edin\n" +pinnedNote: "Sabit not" +pinned: "Profiline sabitle" +you: "Sen" +clickToShow: "Göstermek için tıklayın" +sensitive: "Hassas" add: "Ekle" -reaction: "Tepkiler" -reactions: "Tepkiler" -reactionSettingDescription2: "Sıralamak için sürükleyin, silmek için tıklayın, eklemek için \"+\" tuşuna tıklayın." -rememberNoteVisibility: "Görünürlük ayarlarını hatırla" -attachCancel: "Eki sil" +reaction: "Tepki" +reactions: "Tepki" +emojiPicker: "Emoji seçici" +pinnedEmojisForReactionSettingDescription: "Tepki verirken sabitlenecek ve görüntülenecek emojileri ayarlayın." +pinnedEmojisSettingDescription: "Emoji seçiciyi görüntülerken sabitlenecek ve görüntülenecek emojileri ayarlayın" +emojiPickerDisplay: "Emoji seçici ekranı" +overwriteFromPinnedEmojisForReaction: "Tepki ayarlarından geçersiz kılma" +overwriteFromPinnedEmojis: "Genel ayarlardan geçersiz kılma" +reactionSettingDescription2: "Sıralamayı değiştirmek için sürükleyin, silmek için tıklayın, eklemek için “+” tuşuna basın." +rememberNoteVisibility: "Not görünürlük ayarlarını hatırla" +attachCancel: "Eki kaldır" +deleteFile: "Dosyayı sil" markAsSensitive: "Hassas içerik olarak işaretle" unmarkAsSensitive: "Hassas içerik işaretini kaldır" enterFileName: "Dosya ismini gir" @@ -145,6 +160,7 @@ editList: "Listeyi düzenle" selectChannel: "Kanal seç" selectAntenna: "Bir anten seç" editAntenna: "Anteni düzenle" +createAntenna: "Bir anten oluşturun" selectWidget: "Araç seç" editWidgets: "Araçları düzenle" editWidgetsExit: "Tamam" @@ -157,306 +173,3041 @@ addEmoji: "Emoji ekle" settingGuide: "Önerilen ayarlar" cacheRemoteFiles: "Uzak dosyalar ön belleğe alınsın" cacheRemoteFilesDescription: "Bu ayar açık olduğunda diğer sitelerin dosyaları doğrudan uzak sunucudan yüklenecektir. Bu ayarı kapatmak depolama kullanımını azaltacak ama küçük resimler oluşturulmadığından trafiği arttıracaktır." -youCanCleanRemoteFilesCache: "" +youCanCleanRemoteFilesCache: "Dosya yönetimi görünümünde 🗑️ düğmesine tıklayarak önbelleği temizleyebilirsiniz." cacheRemoteSensitiveFiles: "Hassas uzak dosyalar ön belleğe alınsın" cacheRemoteSensitiveFilesDescription: "Bu ayar kapalı olduğunda hassas uzak dosyalar ön belleğe alınmadan doğrudan uzak sunucudan yüklenecektir." flagAsBot: "Bot olarak işaretle" -flagAsBotDescription: "Bu seçeneği hesap bir program tarafından kontrol ediliyorsa işaretleyin. Bu, diğer geliştiricilerin sonsuz etkileşim zincirleri oluşturmasını engellemeye yardımcı olur ve Misskey'in iç sisteminin hesaba bir bot gibi davranmasını sağlar." +flagAsBotDescription: "Bu hesap bir program tarafından kontrol ediliyorsa bu seçeneği etkinleştirin. Etkinleştirildiğinde, diğer geliştiriciler için bir işaret görevi görerek diğer botlarla sonsuz etkileşim zincirlerini önleyecek ve Misskey'in iç sistemlerini bu hesabı bir bot olarak ele alacak şekilde ayarlayacaktır." flagAsCat: "Kedi hesabı" flagAsCatDescription: "Kedi hesabı" -flagShowTimelineReplies: "Zaman akışında notlara gelen cevapları göster" -flagShowTimelineRepliesDescription: "Açık olduğu durumda, zaman akışında kullanıcıların başkalarına verdiği cevaplar gözükür." +flagShowTimelineReplies: "Timeline'da notlara gelen cevapları göster" +flagShowTimelineRepliesDescription: "Açık olduğu durumda, Timeline'da kullanıcıların başkalarına verdiği cevaplar gözükür." autoAcceptFollowed: "Takip edilen hesapların takip isteklerini kabul et" addAccount: "Hesap ekle" reloadAccountsList: "Hesap listesini güncelle" loginFailed: "Giriş başarısız oldu" showOnRemote: "Uzak sunucuda görüntüle" +continueOnRemote: "Uzak bir sunucuda devam edin" +chooseServerOnMisskeyHub: "Misskey Hub'dan bir sunucu seçin." +specifyServerHost: "Doğrudan bir sunucu ana bilgisayarı belirtin" +inputHostName: "Alan adını girin" general: "Genel" wallpaper: "Duvar kağıdı" setWallpaper: "Duvar kağıdını ayarla" -removeWallpaper: "Duvar kağıdını sil" +removeWallpaper: "Duvar kağıdını kaldır" searchWith: "Arama: {q}" -youHaveNoLists: "Hiç listeniz yok" -followConfirm: "{name} takip edilsin mi?" -proxyAccount: "Vekil hesabı" -proxyAccountDescription: "Proxy hesabı, belirli koşullar altında kullanıcılar için uzaktan takipçi işlevi gören bir hesaptır. Örneğin, bir kullanıcı listeye bir uzak kullanıcı eklediğinde, o kullanıcıyı takip eden yerel bir kullanıcı yoksa uzak kullanıcının etkinliği örneğe teslim edilmeyecektir, dolayısıyla bunun yerine proxy hesabı takip edilecektir." -host: "Sağlayıcı" -selectUser: "Kullanıcı seç" -recipient: "Kime" -annotation: "Açıklamalar" +youHaveNoLists: "Hiçbir listeniz yok." +followConfirm: "{name}'i takip etmek istediğinizden emin misiniz?" +proxyAccount: "Proxy hesabı" +proxyAccountDescription: "Proxy hesabı, belirli koşullar altında kullanıcılar için uzaktan takipçi görevi gören bir hesaptır. Örneğin, bir kullanıcı listeye uzaktan bir kullanıcı eklediğinde, o kullanıcıyı takip eden yerel kullanıcı yoksa uzaktan kullanıcının etkinliği örneğe iletilmez, bunun yerine proxy hesabı takip eder." +host: "Host" +selectSelf: "Kendimi seç" +selectUser: "Bir kullanıcı seçin" +recipient: "Alıcı" +annotation: "Yorumlar" federation: "Federasyon" -instances: "Sunucu" -registeredAt: "Katılma tarihi" -latestRequestReceivedAt: "Alınan son talep" -latestStatus: "En son durum" +instances: "Sunucular" +registeredAt: "Kayıtlı" +latestRequestReceivedAt: "Son talep alındı" +latestStatus: "Son durum" storageUsage: "Depolama kullanımı" -charts: "Çizelgeler" +charts: "Grafikler" perHour: "Saatlik" perDay: "Günlük" -stopActivityDelivery: "Durum güncellemelerini gönderme" +stopActivityDelivery: "Etkinlik göndermeyi durdur" blockThisInstance: "Bu sunucuyu engelle" -silenceThisInstance: "" -operations: "İşlemler" -software: "Yazılımlar" +silenceThisInstance: "Bu sunucuyu sustur" +mediaSilenceThisInstance: "Medya bu sunucuyu sustursun" +operations: "Operasyonlar" +software: "Yazılım" +softwareName: "Yazılım" version: "Sürüm" -metadata: "Meta Verileri" -withNFiles: "{n} tane dosya" +metadata: "Meta veri" +withNFiles: "{n} dosya(lar)" monitor: "Monitör" jobQueue: "İşlem sırası" -cpuAndMemory: "İşlemci ve Hafıza" +cpuAndMemory: "CPU ve Bellek" network: "Ağ" disk: "Disk" instanceInfo: "Sunucu Bilgisi" statistics: "İstatistikler" -clearQueue: "Sırayı temizle" -clearQueueConfirmTitle: "Sıra silinsin mi?" -clearQueueConfirmText: "Sırada kalan hiçbir şey iletilmeyecek. Genelde bu işlem gerekli değildir." -clearCachedFiles: "Ön belleği temizle" -clearCachedFilesConfirm: "Ön belleğe alınmış tüm uzak sunucu dosyaları silinsin mi?" -blockedInstances: "Engellenen sunucular" -blockedInstancesDescription: "Engellemek istediğiniz sunucuların alan adlarını satır sonlarıyla ayırarak yazın. Yazılan sunucular bu sunucuyla iletişime geçemeyecek." -silencedInstances: "Turkısh" -silencedInstancesDescription: "" -muteAndBlock: "Susturma ve Engelleme" -mutedUsers: "Susturulan kullanıcılar" +clearQueue: "Kuyruğu temizle" +clearQueueConfirmTitle: "Kuyruğu silmek istediğinizden emin misiniz?" +clearQueueConfirmText: "Kuyrukta kalan teslim edilmemiş notlar birleştirilmeyecektir. Genellikle bu işlem gerekli değildir." +clearCachedFiles: "Clear cache" +clearCachedFilesConfirm: "Tüm önbelleğe alınmış uzak dosyaları silmek istediğinizden emin misiniz?" +blockedInstances: "Engellenen Sunucu" +blockedInstancesDescription: "Engellemek istediğiniz sunucuların ana bilgisayar adlarını satır sonlarıyla ayırarak listeleyin. Listelenen örnekler artık bu örnekle iletişim kuramayacaktır." +silencedInstances: "Susturulmuş sunucular" +silencedInstancesDescription: "Sessize almak istediğiniz sunucuların ana bilgisayar adlarını yeni bir satırla ayırarak listeleyin. Listelenen sunuculara ait tüm hesaplar sessize alınmış olarak kabul edilecek ve yalnızca takip isteklerinde bulunabilecek, takip edilmedikleri takdirde yerel hesapları etiketleyemeyeceklerdir. Bu, engellenen sunucuları etkilemeyecektir." +mediaSilencedInstances: "Medya susturulmuş sunucular" +mediaSilencedInstancesDescription: "Medya sessize almak istediğiniz sunucuların ana bilgisayar adlarını yeni bir satırla ayırarak listeleyin. Listelenen sunuculara ait tüm hesaplar hassas hesap olarak değerlendirilecek ve özel emojiler kullanılamayacaktır. Bu durum, engellenen sunucuları etkilemeyecektir." +federationAllowedHosts: "Federasyona izin verilen sunucular" +federationAllowedHostsDescription: "Federasyona izin vermek istediğiniz sunucuların ana bilgisayar adlarını satır sonlarıyla ayırın." +muteAndBlock: "Sessize Alma ve Engelleme" +mutedUsers: "Sessize alınan kullanıcılar" blockedUsers: "Engellenen kullanıcılar" noUsers: "Kullanıcı yok" editProfile: "Profili düzenle" noteDeleteConfirm: "Bu notu silmek istediğinizden emin misiniz?" -pinLimitExceeded: "Daha fazla not sabitlenemez" -done: "Tamamlandı" +pinLimitExceeded: "Artık daha fazla not sabitleyemezsiniz" +done: "Tamam" +processing: "İşleme..." preview: "Önizleme" default: "Varsayılan" defaultValueIs: "Varsayılan: {value}" -noCustomEmojis: "Emoji bulunamadı" -noJobs: "Hiç işlem yok" -federating: "Federe ediliyor" +noCustomEmojis: "Emoji yok" +noJobs: "Hiç iş yok" +federating: "Birleştirme" blocked: "Engellenmiş" suspended: "Askıya alınmış" all: "Tümü" subscribing: "Abonelik" publishing: "Paylaşım" -notResponding: "Cevap yok" -instanceFollowing: "Sunucuda takip edenler" +notResponding: "Yanıt vermiyor" +instanceFollowing: "Sunucuda takip" instanceFollowers: "Sunucu takipçileri" -instanceUsers: "Sunucu kullanıcıları" +instanceUsers: "Bu sunucunun kullanıcıları" changePassword: "Şifreyi değiştir" security: "Güvenlik" -retypedNotMatch: "Girişler uyuşmuyor." -currentPassword: "Geçerli şifre" +retypedNotMatch: "Girişler eşleşmiyor." +currentPassword: "Mevcut şifre" newPassword: "Yeni şifre" -newPasswordRetype: "Yeni şifre (tekrar)" -attachFile: "Dosya ekle" -more: "Daha!" -featured: "Öne Çıkan" +newPasswordRetype: "Yeni şifreyi tekrar girin" +attachFile: "Dosyaları ekle" +more: "Daha fazlası!" +featured: "Öne çıkan" usernameOrUserId: "Kullanıcı adı veya ID'si" noSuchUser: "Kullanıcı bulunamadı" lookup: "Sorgu" announcements: "Duyurular" -imageUrl: "Görsel URL'si" +imageUrl: "Görsel URL" remove: "Sil" removed: "Silindi" -removeAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?" -deleteAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?" -resetAreYouSure: "Sıfırlansın mı?" +removeAreYouSure: "“{x}” öğesini kaldırmak istediğinizden emin misiniz?" +deleteAreYouSure: "“{x}” öğesini silmek istediğinizden emin misiniz?" +resetAreYouSure: "Gerçekten sıfırlansın mı?" +areYouSure: "Emin misiniz?" saved: "Kaydedildi" upload: "Yükle" -keepOriginalUploading: "Orijinal görseli koru" -keepOriginalUploadingDescription: "Orijinal olarak yüklenen görüntüyü olduğu gibi kaydeder. Kapatılırsa, yükleme sırasında web'de görüntülenecek bir sürüm oluşturulur." -fromDrive: "Drive Dosyasından" -fromUrl: "Bağlantıdan" -uploadFromUrl: "Bağlantıdan yükle" -uploadFromUrlDescription: "Yüklemek istediğiniz dosyanın bağlantısı" -uploadFromUrlRequested: "Yükleme talep edildi" -uploadFromUrlMayTakeTime: "Yüklemenin tamamlanması biraz süre alabilir." +keepOriginalUploading: "Orijinal görüntüyü koru" +keepOriginalUploadingDescription: "Orijinal olarak yüklenen görüntüyü olduğu gibi kaydeder. Kapalıysa, yükleme sırasında web'de görüntülenecek bir sürüm oluşturulur." +fromDrive: "Sürücüden" +fromUrl: "URL'den" +uploadFromUrl: "Bir URL'den yükle" +uploadFromUrlDescription: "Yüklemek istediğiniz dosyanın URL'si" +uploadFromUrlRequested: "Yükleme istendi" +uploadFromUrlMayTakeTime: "Yükleme işleminin tamamlanması biraz zaman alabilir." +uploadNFiles: "{n} dosya yükle" explore: "Keşfet" -messageRead: "Okundu" -noMoreHistory: "Bundan öncesi yok" -nUsersRead: "{n} kişi okudu" -agreeTo: "Kabul Ediyorum: {0}" -agree: "Kabul Et" -agreeBelow: "Aşağıdakileri kabul ederim" +messageRead: "Oku" +noMoreHistory: "Daha fazla geçmiş bilgisi yoktur." +startChat: "Sohbete başla" +nUsersRead: "{n} tarafından okundu" +agreeTo: "{0}'ı kabul ediyorum." +agree: "Kabul ediyorum" +agreeBelow: "Aşağıdakileri kabul ediyorum" basicNotesBeforeCreateAccount: "Önemli notlar" -termsOfService: "Şartlar ve Koşullar" +termsOfService: "Hizmet Şartları" start: "Başla" home: "Ana sayfa" -remoteUserCaution: "Bu kullanıcı bir uzak sunucudan olduğu için alınan bilgiler tam olmayabilir." +remoteUserCaution: "Bu kullanıcı uzak bir sunucudan geldiği için, gösterilen bilgiler eksik olabilir." activity: "Etkinlik" images: "Görseller" -image: "Görseller" +image: "Görsel" birthday: "Doğum günü" yearsOld: "{age} yaşında" -registeredDate: "Kayıt tarihi" +registeredDate: "Katılım tarihi" location: "Konum" theme: "Temalar" -themeForLightMode: "Aydınlık Tema" -themeForDarkMode: "Karanlık Tema" +themeForLightMode: "Aydınlık Mod'da kullanılacak tema" +themeForDarkMode: "Karanlık Mod'da kullanılacak tema" light: "Aydınlık" dark: "Karanlık" -lightThemes: "Aydınlık Temalar" -darkThemes: "Karanlık Temalar" -syncDeviceDarkMode: "Sistem Koyu Modu ile senkronize et" +lightThemes: "Aydınlık temalar" +darkThemes: "Karanlık temalar" +syncDeviceDarkMode: "Karanlık Modu cihaz ayarlarınızla senkronize edin" +switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" açık. Senkronizasyonu kapatıp modları manuel olarak değiştirmek ister misiniz?" drive: "Sürücü" fileName: "Dosya adı" -selectFile: "Dosya seç" -selectFiles: "Dosya seç" -selectFolder: "Klasör seç" -selectFolders: "Klasör seç" +selectFile: "Bir dosya seçin" +selectFiles: "Dosyaları seçin" +selectFolder: "Bir klasör seçin" +selectFolders: "Klasörleri seçin" +fileNotSelected: "Hiçbir dosya seçilmedi" renameFile: "Dosyayı yeniden adlandır" folderName: "Klasör adı" -createFolder: "Klasör oluştur" -renameFolder: "Klasörü Yeniden Adlandır" -deleteFolder: "Klasörü sil" -addFile: "Dosya ekle" -emptyDrive: "Sürücü boş" +createFolder: "Bir klasör oluşturun" +renameFolder: "Bu klasörü yeniden adlandırın" +deleteFolder: "Bu klasörü sil" +folder: "Dosya" +addFile: "Bir dosya ekle" +showFile: "Dosyaları göster" +emptyDrive: "Sürücünüz boş" emptyFolder: "Bu klasör boş" -unableToDelete: "Silme mümkün değil" -inputNewFileName: "Yeni dosya ismini girin" -inputNewDescription: "Yeni bir başlık gir" -inputNewFolderName: "Yeni klasör ismini girin" -circularReferenceFolder: "Hedef klasör taşınan klasörün bir alt klasörü." -hasChildFilesOrFolders: "Klasör boş olmadığından silinemiyor" +unableToDelete: "Silinemiyor" +inputNewFileName: "Yeni bir dosya adı girin" +inputNewDescription: "Yeni alternatif metin girin" +inputNewFolderName: "Yeni bir klasör adı girin" +circularReferenceFolder: "Hedef klasör, taşımak istediğiniz klasörün bir alt klasörüdür." +hasChildFilesOrFolders: "Bu klasör boş olmadığı için silinemez." copyUrl: "URL'yi kopyala" rename: "Yeniden adlandır" avatar: "Avatar" banner: "Banner" -displayOfSensitiveMedia: "Hassas içerik gösterimi" -whenServerDisconnected: "Sunucu bağlantısı kesildiğinde" -disconnectedFromServer: "Sunucu bağlantısı koptu" +displayOfSensitiveMedia: "Hassas ortamların görüntülenmesi" +whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde" +disconnectedFromServer: "Sunucu bağlantısı kesildi" reload: "Yenile" -doNothing: "Bir şey yapma" -reloadConfirm: "Zaman akışı yenilensin mi?" +doNothing: "Yoksay" +reloadConfirm: "Zaman çizelgesini yenilemek ister misiniz?" watch: "İzle" unwatch: "İzlemeyi bırak" accept: "Kabul et" reject: "Reddet" normal: "Normal" -instanceName: "Sunucu ismi" +instanceName: "Sunucu adı" instanceDescription: "Sunucu açıklaması" -maintainerName: "Yönetici ismi" -maintainerEmail: "Yöneticinin e-postası" -tosUrl: "Hizmet Koşulları Bağlantısı" -thisYear: "Bu yıl" -thisMonth: "Bu ay" +maintainerName: "Bakım sorumlusu" +maintainerEmail: "Bakım sorumlusu E-Posta adresi" +tosUrl: "Hizmet Şartları URL'si" +thisYear: "Yıl" +thisMonth: "Ay" today: "Bugün" -monthX: "{month} ay" +dayX: "{day}" +monthX: "{month}" +yearX: "{year}" pages: "Sayfalar" integration: "Entegrasyon" +connectService: "Bağlan" +disconnectService: "Bağlantıyı kes" +enableLocalTimeline: "Yerel Timeline'ı etkinleştir" +enableGlobalTimeline: "Küresel Timeline'ı etkinleştir" +disablingTimelinesInfo: "Yöneticiler ve Moderatörler, etkinleştirilmemiş olsalar bile her zaman tüm Timeline'a erişebilecekler." +registration: "Kaydol" +invite: "Davet et" +driveCapacityPerLocalAccount: "Yerel kullanıcı başına sürücü kapasitesi" +driveCapacityPerRemoteAccount: "Uzak kullanıcı başına sürücü kapasitesi" +inMb: "Megabayt cinsinden" +bannerUrl: "Banner görseli URL'si" +backgroundImageUrl: "Arka plan görseli URL'si" basicInfo: "Temel bilgiler" pinnedUsers: "Sabitlenmiş kullanıcılar" -pinnedNotes: "Sabitlenen" -manageAntennas: "Anten ayarları" +pinnedUsersDescription: "“Keşfet” sekmesinde sabitlenecek kullanıcı adlarını satır sonlarıyla ayırarak listeleyin." +pinnedPages: "Sabitlenmiş Sayfalar" +pinnedPagesDescription: "Bu örneğin üst sayfasına sabitlemek istediğiniz Sayfaların yollarını satır sonlarıyla ayırarak girin." +pinnedClipId: "Sabitlenecek klibin ID" +pinnedNotes: "Sabitlenmiş notlar" +hcaptcha: "hCaptcha" +enableHcaptcha: "hCaptcha'yı etkinleştir" +hcaptchaSiteKey: "Site anahtar" +hcaptchaSecretKey: "Gizli anahtar" +mcaptcha: "mCaptcha" +enableMcaptcha: "mCaptcha'yı etkinleştir" +mcaptchaSiteKey: "Site anahtarı" +mcaptchaSecretKey: "Gizli anahtar" +mcaptchaInstanceUrl: "mCaptcha sunucu URL'si" +recaptcha: "reCAPTCHA" +enableRecaptcha: "reCAPTCHA'yı etkinleştir" +recaptchaSiteKey: "Site anahtar" +recaptchaSecretKey: "Gizli anahtar" +turnstile: "Turnstile" +enableTurnstile: "Turnstile'yi etkinleştir" +turnstileSiteKey: "Site anahtar" +turnstileSecretKey: "Gizli anahtar" +avoidMultiCaptchaConfirm: "Birden fazla Captcha sistemi kullanmak, aralarında çakışmaya neden olabilir. Şu anda etkin olan diğer Captcha sistemlerini devre dışı bırakmak ister misiniz? Etkin kalmalarını istiyorsanız, iptal düğmesine basın." +antennas: "Antenler" +manageAntennas: "Antenleri Yönet" +name: "İsim" +antennaSource: "Anten kaynağı" +antennaKeywords: "Dinlenecek anahtar kelimeler" +antennaExcludeKeywords: "Hariç tutulacak anahtar kelimeler" +antennaExcludeBots: "Bot hesaplarını hariç tut" +antennaKeywordsDescription: "VE koşulu için boşluklarla, VEYA koşulu için satır sonlarıyla ayırın." +notifyAntenna: "Yeni notlar hakkında bildirimde bulunun" +withFileAntenna: "Sadece dosyalı notlar" +excludeNotesInSensitiveChannel: "Hassas kanallardan gelen notları hariç tutun" +enableServiceworker: "Tarayıcınız için Push Bildirimlerini Etkinleştirin" +antennaUsersDescription: "Satır başına bir kullanıcı adı listeleyin" +caseSensitive: "Harfe duyarlı" +withReplies: "Yanıtları ekle" +connectedTo: "Aşağıdaki hesap(lar) bağlı" +notesAndReplies: "Notlar ve yanıtlar" +withFiles: "Dosyalar dahil" +silence: "Sessize al" +silenceConfirm: "Bu kullanıcıyı susturmak istediğinizden emin misiniz?" +unsilence: "Sessize almayı geri al" +unsilenceConfirm: "Bu kullanıcının sessize alınmasını geri almak istediğinizden emin misiniz?" +popularUsers: "Popüler kullanıcılar" +recentlyUpdatedUsers: "Son zamanlarda aktif olan kullanıcılar" +recentlyRegisteredUsers: "Yeni katılan kullanıcılar" +recentlyDiscoveredUsers: "Yeni keşfedilen kullanıcılar" +exploreUsersCount: "{count} kullanıcı var" +exploreFediverse: "Fediverse'i keşfedin" +popularTags: "Popüler etiketler" userList: "Listeler" -resetPassword: "Şifre sıfırlama" -details: "Detaylar" -deck: "Güverte" -smtpHost: "Sağlayıcı" -smtpUser: "Kullanıcı Adı" +about: "Hakkında" +aboutMisskey: "Misskey Hakkında" +administrator: "Yönetici" +token: "Token" +2fa: "İki faktörlü kimlik doğrulama" +setupOf2fa: "İki faktörlü kimlik doğrulamayı ayarlayın" +totp: "Authenticator Uygulaması" +totpDescription: "Tek seferlik şifreleri girmek için bir kimlik doğrulama uygulaması kullanın" +moderator: "Moderatör" +moderation: "Moderasyon" +moderationNote: "Moderasyon notu" +moderationNoteDescription: "Moderatörler arasında paylaşılacak notları girebilirsiniz." +addModerationNote: "Moderasyon notu ekle" +moderationLogs: "Moderasyon günlükleri" +nUsersMentioned: "{n} kullanıcı tarafından bahsedildi" +securityKeyAndPasskey: "Güvenlik ve geçiş anahtarları" +securityKey: "Güvenlik anahtarı" +lastUsed: "Son kullanılan" +lastUsedAt: "Son kullanım: {t}" +unregister: "Kayıttan çık" +passwordLessLogin: "Şifresiz giriş" +passwordLessLoginDescription: "Yalnızca güvenlik anahtarı veya şifre anahtarı kullanarak şifresiz oturum açmaya izin verir." +resetPassword: "Şifreyi sıfırla" +newPasswordIs: "Yeni şifre \"{password}\"" +reduceUiAnimation: "UI animasyonlarını azaltın." +share: "Paylaş" +notFound: "Bulunamadı" +notFoundDescription: "Bu URL'ye karşılık gelen sayfa bulunamadı." +uploadFolder: "Yüklemeler için varsayılan klasör" +markAsReadAllNotifications: "Tüm bildirimleri okundu olarak işaretle" +markAsReadAllUnreadNotes: "Tüm notları okundu olarak işaretle" +markAsReadAllTalkMessages: "Tüm mesajları okundu olarak işaretle" +help: "Yardım" +inputMessageHere: "Mesajınızı buraya girin" +close: "Kapat" +invites: "Davetler" +members: "Üyeler" +transfer: "Transfer" +title: "Başlık" +text: "Metin" +enable: "Etkin" +next: "Sonraki" +retype: "Tekrar girin" +noteOf: "{user} tarafından not" +quoteAttached: "Alıntı" +quoteQuestion: "Alıntı olarak ekle?" +attachAsFileQuestion: "Panodaki metin uzun. Metin dosyası olarak eklemek ister misiniz?" +onlyOneFileCanBeAttached: "Bir mesaja yalnızca bir dosya ekleyebilirsiniz." +signinRequired: "Devam etmeden önce lütfen kayıt olun veya giriş yapın." +signinOrContinueOnRemote: "Devam etmek için sunucunuzu taşıyın veya bu sunucuya kaydolun / giriş yapın." +invitations: "Davetler" +invitationCode: "Davet kodu" +checking: "Kontrol ediliyor..." +available: "Kullanılabilir" +unavailable: "Kullanılamaz" +usernameInvalidFormat: "Büyük ve küçük harfler, rakamlar ve alt çizgi kullanabilirsiniz. (a~z、A~Z、0~9)" +tooShort: "Çok kısa" +tooLong: "Çok uzun" +weakPassword: "Zayıf şifre" +normalPassword: "Ortalama şifre" +strongPassword: "Güçlü şifre" +passwordMatched: "Eşleşti" +passwordNotMatched: "Eşleşmedi" +signinWith: "{x} ile giriş yapın" +signinFailed: "Giriş yapılamıyor. Girilen kullanıcı adı veya şifre yanlış." +or: "veya" +language: "Dil" +uiLanguage: "Kullanıcı arayüzü dili" +aboutX: "{x} hakkında" +emojiStyle: "Emoji stili" +native: "Yerli" +menuStyle: "Menü stili" +style: "Stil" +drawer: "Çekmece" +popup: "Pop-up" +showNoteActionsOnlyHover: "Not eylemlerini yalnızca üzerine gelindiğinde göster" +showReactionsCount: "Notlardaki tepki sayısını gör" +noHistory: "Geçmiş mevcut değil" +signinHistory: "Giriş geçmişi" +enableAdvancedMfm: "Gelişmiş MFM'yi etkinleştir" +enableAnimatedMfm: "Animasyonlu MFM'yi etkinleştir" +doing: "İşleniyor..." +category: "Kategori" +tags: "Takma adlar" +docSource: "Bu belgenin kaynağı" +createAccount: "Hesap oluştur" +existingAccount: "Mevcut hesap" +regenerate: "Yeniden oluştur" +fontSize: "Yazı tipi boyutu" +mediaListWithOneImageAppearance: "Tek bir resim içeren medya listelerinin yüksekliği" +limitTo: "{x} ile sınırlandır" +noFollowRequests: "Bekleyen takip istekleriniz yok." +openImageInNewTab: "Görüntüleri yeni sekmede aç" +dashboard: "Gösterge paneli" +local: "Yerel" +remote: "Uzaktan" +total: "Toplam" +weekOverWeekChanges: "Geçen haftadan bu yana yapılan değişiklikler" +dayOverDayChanges: "Dünkü değişiklikler" +appearance: "Görünüm" +clientSettings: "İstemci Ayarları" +accountSettings: "Hesap Ayarları" +promotion: "Tanıtım" +promote: "Tanıtıldı" +numberOfDays: "Gün sayısı" +hideThisNote: "Bu notu gizle" +showFeaturedNotesInTimeline: "Timeline'da öne çıkan notları göster" +objectStorage: "Nesne Depolama" +useObjectStorage: "Nesne depolamayı kullanın" +objectStorageBaseUrl: "Temel URL" +objectStorageBaseUrlDesc: "Referans olarak kullanılan URL. CDN veya Proxy kullanıyorsanız, bunların URL'sini belirtin.\nS3 için ‘https://.s3.amazonaws.com’ ve GCS veya eşdeğer hizmetler için ‘https://storage.googleapis.com/’ vb. kullanın." +objectStorageBucket: "Kova" +objectStorageBucketDesc: "Lütfen sağlayıcınızda kullanılan kova adını belirtin." +objectStoragePrefix: "Önek" +objectStoragePrefixDesc: "Dosyalar bu öneke sahip dizinler altında saklanacaktır." +objectStorageEndpoint: "Uç nokta" +objectStorageEndpointDesc: "AWS S3 kullanıyorsanız bu alanı boş bırakın, aksi takdirde kullandığınız hizmete bağlı olarak uç noktayı ‘’ veya ‘:’ olarak belirtin." +objectStorageRegion: "Bölge" +objectStorageRegionDesc: "'xx-east-1' gibi bir bölge belirtin. Hizmetiniz bölgeler arasında ayrım yapmıyorsa, ‘us-east-1’ girin. AWS yapılandırma dosyalarını veya ortam değişkenlerini kullanıyorsanız boş bırakın." +objectStorageUseSSL: "SSL kullanın" +objectStorageUseSSLDesc: "API bağlantıları için HTTPS kullanmayacaksanız bunu kapatın." +objectStorageUseProxy: "Proxy üzerinden bağlan" +objectStorageUseProxyDesc: "API bağlantıları için Proxy kullanmayacaksanız bunu kapatın." +objectStorageSetPublicRead: "Yükleme sırasında \"genel-okuma\" ayarını yapın" +s3ForcePathStyleDesc: "s3ForcePathStyle etkinleştirilirse, kova adı URL'nin ana bilgisayar adı yerine URL yoluna eklenmelidir. Kendi kendine barındırılan bir Minio örneği gibi hizmetleri kullanırken bu ayarı etkinleştirmeniz gerekebilir." +serverLogs: "Sunucu log kayıtları" +deleteAll: "Tümünü sil" +showFixedPostForm: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle" +showFixedPostFormInChannel: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle (Kanallar)" +withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak zaman çizelgesine dahil et" +newNoteRecived: "Yeni notlar var" +newNote: "Yeni Not" +sounds: "Sesler" +sound: "Ses" +notificationSoundSettings: "Bildirim sesi ayarları" +listen: "Dinle" +none: "Hiçbiri" +showInPage: "Sayfada göster" +popout: "Açılır pencere" +volume: "Ses hacmi" +masterVolume: "Ana ses seviyesi" +notUseSound: "Sesi kapat" +useSoundOnlyWhenActive: "Misskey etkin olduğunda ses çıkarılır." +details: "Ayrıntılar" +renoteDetails: "Renote detayları" +chooseEmoji: "Bir emoji seçin" +unableToProcess: "İşlem tamamlanamadı." +recentUsed: "Son kullanılan" +install: "Yükle" +uninstall: "Kaldır" +installedApps: "Yetkili Uygulamalar" +nothing: "Burada görülecek bir şey yok." +installedDate: "Yetkili" +lastUsedDate: "En son kullanıldığı tarih" +state: "Durum" +sort: "Sıralama düzeni" +ascendingOrder: "Yükselme" +descendingOrder: "Alçalma" +scratchpad: "Not defteri" +scratchpadDescription: "Scratchpad, AiScript deneyleri için bir ortam sağlar. Misskey ile etkileşim halindeyken yazabilir, çalıştırabilir ve sonuçlarını kontrol edebilirsiniz." +uiInspector: "UI denetçisi" +uiInspectorDescription: "Bellekteki UI bileşeni sunucu listesini görebilirsiniz. UI bileşeni, Ui:C: işlevi tarafından oluşturulacaktır." +output: "Çıktı" +script: "Script" +disablePagesScript: "Sayfalarda AiScript'i devre dışı bırak" +updateRemoteUser: "Uzak kullanıcı bilgilerini güncelle" +unsetUserAvatar: "Avatar'ı kaldır" +unsetUserAvatarConfirm: "Avatarı silmek istediğinizden emin misiniz?" +unsetUserBanner: "Banner'ı kaldır" +unsetUserBannerConfirm: "Banner'ı kaldırmak istediğinizden emin misiniz?" +deleteAllFiles: "Tüm dosyaları sil" +deleteAllFilesConfirm: "Tüm dosyaları silmek istediğinizden emin misiniz?" +removeAllFollowing: "Takip ettiğiniz tüm kullanıcıları takipten çıkarın" +removeAllFollowingDescription: "Bu komutu çalıştırmak, {host} adresindeki tüm hesapları takipten çıkarır. Örneğin, sunucu artık mevcut değilse bu komutu çalıştırın." +userSuspended: "Bu kullanıcı askıya alınmıştır." +userSilenced: "Bu kullanıcı susturuluyor." +yourAccountSuspendedTitle: "Bu hesap askıya alınmıştır." +yourAccountSuspendedDescription: "Bu hesap, sunucunun hizmet şartlarını veya benzerlerini ihlal ettiği için askıya alınmıştır. Daha ayrıntılı bir neden öğrenmek isterseniz yöneticiyle iletişime geçin. Lütfen yeni bir hesap oluşturmayın." +tokenRevoked: "Geçersiz jeton" +tokenRevokedDescription: "Bu jetonun süresi doldu. Lütfen tekrar giriş yapın." +accountDeleted: "Hesap silindi" +accountDeletedDescription: "Bu hesap silinmiştir." +menu: "Menü" +divider: "Bölücü" +addItem: "Öğe Ekle" +rearrange: "Yeniden düzenle" +relays: "Röleler" +addRelay: "Röle ekle" +inboxUrl: "Gelen Kutusu URL'si" +addedRelays: "Eklenen Röleler" +serviceworkerInfo: "Push bildirimleri için etkinleştirilmelidir." +deletedNote: "Silinen not" +invisibleNote: "Görünmez not" +enableInfiniteScroll: "Otomatik olarak daha fazlasını yükle" +visibility: "Görünürlük" +poll: "Anket" +useCw: "İçeriği gizle" +enablePlayer: "Video oynatıcıyı aç" +disablePlayer: "Video oynatıcıyı kapat" +expandTweet: "Notu genişlet" +themeEditor: "Tema düzenleyici" +description: "Açıklama" +describeFile: "Alternatif metin ekle" +enterFileDescription: "Alternatif metin girin" +author: "Yazar" +leaveConfirm: "Kaydedilmemiş değişiklikler var. Bunları silmek istiyor musunuz?" +manage: "Yönetim" +plugins: "Eklentiler" +preferencesBackups: "Tercih yedeklemeleri" +deck: "Deck" +undeck: "Güverteden Ayrıl" +useBlurEffectForModal: "Modaller için bulanıklaştırma efekti kullanın" +useFullReactionPicker: "Tam boy tepki seçiciyi kullanın" +width: "Genişlik" +height: "Yükseklik" +large: "Büyük" +medium: "Orta" +small: "Küçük" +generateAccessToken: "Erişim jetonu oluştur" +permission: "İzinler" +adminPermission: "Yönetici İzinleri" +enableAll: "Tümünü etkinleştir" +disableAll: "Tümünü devre dışı bırak" +tokenRequested: "Hesaba erişim izni ver" +pluginTokenRequestedDescription: "Bu eklenti, burada ayarlanan izinleri kullanabilecektir." +notificationType: "Bildirim türü" +edit: "Düzenle" +emailServer: "E-posta sunucusu" +enableEmail: "E-posta dağıtımını etkinleştir" +emailConfigInfo: "Kayıt sırasında veya şifrenizi unuttuğunuzda E-postanızı doğrulamak için kullanılır." +email: "E-Posta" +emailAddress: "E-Posta adresi" +smtpConfig: "SMTP Sunucu yapılandırması" +smtpHost: "Host" +smtpPort: "Port" +smtpUser: "Kullanıcı adı" smtpPass: "Şifre" +emptyToDisableSmtpAuth: "SMTP kimlik doğrulamasını devre dışı bırakmak için kullanıcı adı ve şifre alanlarını boş bırakın." +smtpSecure: "SMTP bağlantıları için örtük SSL/TLS kullanın" +smtpSecureInfo: "STARTTLS kullanırken bunu kapatın." +testEmail: "Test E-postası gönderimi" +wordMute: "Kelime sustur" +wordMuteDescription: "Belirtilen kelime veya kelime öbeğini içeren notları küçültün. Küçültülmüş notlar, üzerlerine tıklanarak görüntülenebilir." +hardWordMute: "Zorla kelime sustur" +showMutedWord: "Sessize alınan kelimeleri göster" +hardWordMuteDescription: "Belirtilen kelime veya kelime öbeğini içeren notları gizleyin. Kelime sessize alma özelliğinden farklı olarak, not tamamen görünmez hale gelir." +regexpError: "Düzenli ifade hatası" +regexpErrorDescription: "{tab} kelimesinin {line} satırındaki düzenli ifadede bir hata oluştu:" +instanceMute: "Sunucu Sessizleştirme" +userSaysSomething: "{name} bir şey söyledi." +userSaysSomethingAbout: "{name} “{word}” hakkında bir şey söyledi." +makeActive: "Etkinleştir" +display: "Ekran" +copy: "Kopyala" +copiedToClipboard: "Panoya kopyalandı" +metrics: "Metrikler" +overview: "Genel Bakış" +logs: "Günlükler" +delayed: "Gecikmeli" +database: "Veritabanı" +channel: "Kanallar" +create: "Oluştur" notificationSetting: "Bildirim ayarları" +notificationSettingDesc: "Görüntülemek istediğiniz bildirim türlerini seçin." +useGlobalSetting: "Genel ayarları kullan" +useGlobalSettingDesc: "Etkinleştirildiğinde, hesabınızın bildirim ayarları kullanılır. Devre dışı bırakıldığında, bireysel yapılandırmalar yapılabilir." +other: "Diğer" +regenerateLoginToken: "Giriş jetonunu yeniden oluştur" +regenerateLoginTokenDescription: "Giriş sırasında dahili olarak kullanılan jetonu yeniden oluşturur. Normalde bu işlem gerekli değildir. Yeniden oluşturulursa, tüm cihazlar oturumu kapatılır." +theKeywordWhenSearchingForCustomEmoji: "Bu, kendi emojilerini ararken kullanılan anahtar kelimedir." +setMultipleBySeparatingWithSpace: "Birden fazla girişi boşluklarla ayırın." +fileIdOrUrl: "Dosya ID veya URL" +behavior: "Davranış" +sample: "Örnek" +abuseReports: "Raporlar" +reportAbuse: "Rapor" +reportAbuseRenote: "Raporu yeniden gönder" +reportAbuseOf: "{name} raporu" +fillAbuseReportDescription: "Bu raporla ilgili ayrıntıları lütfen doldurun. Belirli bir notla ilgiliyse, lütfen URL'sini de ekleyin." +abuseReported: "Raporunuz gönderildi. Çok teşekkür ederiz." +reporter: "Raporlayan" +reporteeOrigin: "Bildirim Kaynağı" +reporterOrigin: "Bildirenin Kaynağı" +send: "Gönder" +openInNewTab: "Yeni sekmede aç" +openInSideView: "Yan görünümde aç" +defaultNavigationBehaviour: "Varsayılan gezinme davranışı" +editTheseSettingsMayBreakAccount: "Bu ayarları düzenlemek hesabınıza zarar verebilir." instanceTicker: "Notların sunucu bilgileri" +waitingFor: "{x} bekleniyor" +random: "Rastgele" +system: "Sistem" +switchUi: "UI değiştir" +desktop: "Masaüstü " +clip: "Klip" +createNew: "Yeni oluştur" +optional: "Opsiyonel" +createNewClip: "Klip oluştur" +unclip: "Klip kaldır" +confirmToUnclipAlreadyClippedNote: "Bu not zaten “{name}” klibinin bir parçasıdır. Bu klipten silmek ister misiniz?" +public: "Halka açık" +private: "Özel" +i18nInfo: "Misskey, gönüllüler tarafından çeşitli dillere çevrilmektedir. {link} adresinden yardımcı olabilirsiniz." +manageAccessTokens: "Manage access tokens" +accountInfo: "Erişim belirteçlerini yönetme" +notesCount: "Not sayısı" +repliesCount: "Gönderilen yanıt sayısı" +renotesCount: "Gönderilen renote sayısı" +repliedCount: "Alınan yanıt sayısı" +renotedCount: "Alınan renot sayısı" +followingCount: "Takip edilen hesap sayısı" +followersCount: "Takipçi sayısı" +sentReactionsCount: "Gönderilen tepki sayısı" +receivedReactionsCount: "Alınan tepki sayısı" +pollVotesCount: "Gönderilen anket oylarının sayısı" +pollVotedCount: "Alınan anket oylarının sayısı" +yes: "Evet" +no: "Hayır" +driveFilesCount: "Sürücü dosya sayısı" +driveUsage: "Sürücü alanı kullanımı" +noCrawle: "Tarayıcı indekslemesini reddet" noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et." -clearCache: "Ön belleği temizle" +lockedAccountInfo: "Notunuzun görünürlüğünü “Yalnızca takipçiler” olarak ayarlamadığınız sürece, takipçilerin manuel olarak onaylanmasını gerektirse bile notlarınız herkes tarafından görülebilir." +alwaysMarkSensitive: "Varsayılan olarak hassas olarak işaretle" +loadRawImages: "Küçük resimleri göstermek yerine orijinal resimleri yükle" +disableShowingAnimatedImages: "Animasyonlu görüntüleri oynatmayın" +highlightSensitiveMedia: "Hassas medyayı vurgulayın" +verificationEmailSent: "Doğrulama e-postası gönderildi. Doğrulamayı tamamlamak için e-postadaki bağlantıyı takip edin." +notSet: "Ayarlı değil" +emailVerified: "E-posta adresi doğrulanmıştır." +noteFavoritesCount: "Favori notların sayısı" +pageLikesCount: "Beğenilen Sayfa Sayısı" +pageLikedCount: "Alınan sayfa beğenileri sayısı" +contact: "Alınan Sayfa beğenileri sayısı" +useSystemFont: "Sistemin varsayılan yazı tipini kullanın" +clips: "Klipler" +experimentalFeatures: "Deneysel özellikler" +experimental: "Deneysel" +thisIsExperimentalFeature: "Bu deneysel bir özelliktir. İşlevselliği değişebilir ve amaçlandığı gibi çalışmayabilir." +developer: "Geliştirici" +makeExplorable: "Hesabı “Keşfet” bölümünde görünür hale getirin" +makeExplorableDescription: "Bunu kapatırsanız, hesabınız “Keşfet” bölümünde görünmez." +duplicate: "Çoğalt" +left: "Sol" +center: "Merkez" +wide: "Geniş" +narrow: "Dar" +reloadToApplySetting: "Bu ayar, sayfa yeniden yüklendikten sonra geçerli olacaktır. Şimdi yeniden yüklemek ister misiniz?" +needReloadToApply: "Bunun yansıtılması için yeniden yükleme yapılması gerekir." +needToRestartServerToApply: "Değişikliğin yansıtılması için Misskey'in yeniden başlatılması gerekir." +showTitlebar: "Başlık çubuğunu göster" +clearCache: "Clear cache" onlineUsersCount: "{n} kullanıcı çevrim içi" +nUsers: "{n} Kullanıcı" +nNotes: "{n} Not" +sendErrorReports: "Hata raporları gönder" +sendErrorReportsDescription: "Etkinleştirildiğinde, bir sorun oluştuğunda ayrıntılı hata bilgileri Misskey ile paylaşılacak ve bu da Misskey'in kalitesinin iyileştirilmesine yardımcı olacaktır.\nBu bilgiler arasında işletim sisteminizin sürümü, kullandığınız tarayıcı, Misskey'deki faaliyetleriniz vb. yer alacaktır." +myTheme: "Benim temam" +backgroundColor: "Arka plan rengi" +accentColor: "Vurgu rengi" +textColor: "Metin rengi" +saveAs: "Farklı kaydet..." +advanced: "Gelişmiş" +advancedSettings: "Gelişmiş ayarlar" +value: "Değer" +createdAt: "Oluşturuldu" +updatedAt: "Güncellendi" +saveConfirm: "Değişiklikleri kaydetmek ister misiniz?" +deleteConfirm: "Gerçekten silmek istiyor musunuz?" +invalidValue: "Geçersiz değer." +registry: "Kayıt Defteri" +closeAccount: "Hesabı kapat" +currentVersion: "Şu anki sürüm" +latestVersion: "En yeni sürüm" +youAreRunningUpToDateClient: "Müşteri yazılımınızın en yeni sürümünü kullanıyorsunuz." +newVersionOfClientAvailable: "İstemcinin daha yeni bir sürümü var." +usageAmount: "Kullanım" +capacity: "Kapasite" +inUse: "Kullanılmış" +editCode: "Kodu düzenle" +apply: "Uygula" +receiveAnnouncementFromInstance: "Bu sunucudan bildirimler alın" +emailNotification: "E-posta bildirimleri" +publish: "Yayınla" +inChannelSearch: "Kanalda ara" +useReactionPickerForContextMenu: "Sağ tıklama ile tepki seçiciyi aç" +typingUsers: "{users} yazıyor..." +jumpToSpecifiedDate: "Belirli bir tarihe atla" +showingPastTimeline: "Şu anda eski bir Timeline görüntüleniyor." +clear: "Geri dön" +markAllAsRead: "Tümünü okundu olarak işaretle" +goBack: "Geri" +unlikeConfirm: "Gerçekten beğenini kaldırmak mı istiyorsun?" +fullView: "Tam görünüm" +quitFullView: "Tam ekranı kapat" +addDescription: "Açıklama ekle" +userPagePinTip: "Bireysel notların menüsünden “Profiline sabitle” seçeneğini seçerek notları burada görüntüleyebilirsiniz." +notSpecifiedMentionWarning: "Bu notta, alıcılar arasında yer almayan kullanıcılar hakkında bilgiler bulunmaktadır." +info: "Hakkında" +userInfo: "Kullanıcı bilgileri" +unknown: "Bilinmiyor" +onlineStatus: "Çevrimiçi durumu" +hideOnlineStatus: "Çevrimiçi durumunu gizle" +hideOnlineStatusDescription: "Çevrimiçi durumunuzu gizlemek, arama gibi bazı özelliklerin kullanışlılığını azaltır." +online: "Çevrimiçi" +active: "Aktif" +offline: "Çevrimdışı" +notRecommended: "Tavsiye edilmez" +botProtection: "Bot Koruması" +instanceBlocking: "Blocked/Silenced Instances" +selectAccount: "Hesap seç" +switchAccount: "Hesap değiştir" +enabled: "Aktif" +disabled: "Devre Dışı" +quickAction: "Hızlı eylemler" user: "Kullanıcı" +administration: "Yönetim" +accounts: "Hesaplar" +switch: "Anahtar" +noMaintainerInformationWarning: "Bakımcı bilgileri yapılandırılmamıştır." +noInquiryUrlWarning: "Sorgu URL'si ayarlanmadı" +noBotProtectionWarning: "Bot koruması yapılandırılmamıştır." +configure: "Yeniden Yapılandır" +postToGallery: "Yeni galeri gönderisi oluştur" +postToHashtag: "Bu hashtag'e gönder" +gallery: "Galeri" +recentPosts: "Son gönderiler" +popularPosts: "Popüler gönderiler" +shareWithNote: "Notla paylaş" +ads: "Reklamlar" +expiration: "Son tarih" +startingperiod: "Başla" +memo: "Hatırlatıcı" +priority: "Öncelik" +high: "Yüksek" +middle: "Orta" +low: "Düşük" +emailNotConfiguredWarning: "E-posta adresi ayarlanmamış." +ratio: "Oran" +previewNoteText: "Önizlemeyi göster" +customCss: "Özel CSS" +customCssWarn: "Bu ayar, yalnızca ne işe yaradığını biliyorsanız kullanılmalıdır. Yanlış değerler girilmesi, istemcinin normal şekilde çalışmamasına neden olabilir." global: "Küresel" squareAvatars: "Kare avatarlar" +sent: "Gönderilen" +received: "Alınan" +searchResult: "Arama sonuçları" +hashtags: "Hashtag'ler" +troubleshooting: "Sorun Giderme" +useBlurEffect: "UI'da bulanıklık efektleri kullanın" +learnMore: "Daha fazla bilgi edinin" +misskeyUpdated: "Misskey güncellendi!" +whatIsNew: "Değişiklikleri göster" +translate: "Çevir" +translatedFrom: "{x}'ten çevrilmiştir." +accountDeletionInProgress: "Hesap silme işlemi şu anda devam ediyor." +usernameInfo: "Bu sunucudaki diğer hesaplardan hesabınızı ayıran bir isim. Alfabe (a~z, A~Z), rakamlar (0~9) veya alt çizgi (_) kullanabilirsiniz. Kullanıcı adları daha sonra değiştirilemez." +aiChanMode: "Ai Modu" +devMode: "Geliştirici modu" +keepCw: "İçerik uyarılarını sakla" +pubSub: "Yayın/Abonelik Hesapları" +lastCommunication: "Son iletişim" +resolved: "Çözülmüş" +unresolved: "Çözülmemiş" +breakFollow: "Takipçiyi kaldır" +breakFollowConfirm: "Bu takipçiyi gerçekten silmek istiyor musun?" +itsOn: "Etkin" +itsOff: "Devre Dışı" +on: "Açık" +off: "Kapalı" +emailRequiredForSignup: "Kayıt için E-posta adresi gereklidir." +unread: "Okunmamış" +filter: "Filtre" +controlPanel: "Kontrol Paneli" +manageAccounts: "Hesapları Yönet" +makeReactionsPublic: "Tepki geçmişini herkese açık olarak ayarla" +makeReactionsPublicDescription: "Bu, geçmişteki tüm tepkilerinizin listesini herkese açık hale getirecektir." +classic: "Klasik" +muteThread: "Konuyu sessize al" +unmuteThread: "Konuyu sessizden çıkar" +followingVisibility: "Takip edilenlerin görünürlüğü" +followersVisibility: "Takipçilerin görünürlüğü" +continueThread: "Konunun devamını görüntüle" +deleteAccountConfirm: "Bu, hesabınızı geri dönüşü olmayan bir şekilde silecektir. Devam etmek istiyor musunuz?" +incorrectPassword: "Yanlış şifre." +incorrectTotp: "Tek kullanımlık şifre yanlış veya süresi dolmuş." +voteConfirm: "\"{choice}\" için oyunuzu onaylıyor musunuz?" +hide: "Gizle" +useDrawerReactionPickerForMobile: "Mobil cihazlarda tepki seçiciyi çekmece olarak göster" +welcomeBackWithName: "Hoş geldin, {name}" +clickToFinishEmailVerification: "E-posta doğrulamasını tamamlamak için lütfen [{ok}] düğmesine tıklayın." +overridedDeviceKind: "Cihaz türü" +smartphone: "Akıllı telefon" +tablet: "Tablet" +auto: "Otomatik" +themeColor: "Örnek Ticker Rengi" +size: "Boyut" +numberOfColumn: "Sütun sayısı" searchByGoogle: "Arama" +instanceDefaultLightTheme: "Sunucu genelinde varsayılan açık tema" +instanceDefaultDarkTheme: "Sunucu genelinde varsayılan koyu tema" +instanceDefaultThemeDescription: "Tema kodunu nesne biçiminde girin." +mutePeriod: "Sessiz kalma süresi" +period: "Zaman sınırı" +indefinitely: "Kalıcı olarak" +tenMinutes: "10 dakika" +oneHour: "1 saat" +oneDay: "1 gün" +oneWeek: "1 hafta" +oneMonth: "1 ay" +threeMonths: "3 ay" +oneYear: "1 yıl" +threeDays: "3 gün" +reflectMayTakeTime: "Bunun yansıtılması biraz zaman alabilir." +failedToFetchAccountInformation: "Hesap bilgileri alınamadı" +rateLimitExceeded: "Hız sınırı aşıldı" +cropImage: "Görüntüyü kırp" +cropImageAsk: "Bu görüntüyü kırpmak ister misiniz?" +cropYes: "Kırp" +cropNo: "Olduğu gibi kullanın" file: "Dosyalar" +recentNHours: "Son {n} saat" +recentNDays: "Son {n} gün" +noEmailServerWarning: "E-posta sunucusu yapılandırılmamış." +thereIsUnresolvedAbuseReportWarning: "Çözülmemiş raporlar var." +recommended: "Önerilen" +check: "Kontrol" +driveCapOverrideLabel: "Bu kullanıcının sürücü kapasitesini değiştirin" +driveCapOverrideCaption: "Kapasiteyi varsayılan değere sıfırlamak için 0 veya daha düşük bir değer girin." +requireAdminForView: "Bunu görüntülemek için yönetici hesabıyla oturum açmanız gerekir." +isSystemAccount: "Sistem tarafından oluşturulan ve otomatik olarak işletilen bir hesap." +typeToConfirm: "Onaylamak için lütfen {x} girin." +deleteAccount: "Hesabı sil" +document: "Dokümantasyon" +numberOfPageCache: "Önbelleğe alınmış sayfa sayısı" +numberOfPageCacheDescription: "Bu sayıyı artırmak, kullanıcının cihazında daha fazla bellek kullanımı nedeniyle daha fazla yük oluşturmakla birlikte, kullanıcının rahatlığını artıracaktır." +logoutConfirm: "Çıkmak istediğinizden emin misiniz?" +logoutWillClearClientData: "Oturumu kapatmak, tarayıcıdan istemcinin ayarlarını siler. Tekrar oturum açtığınızda ayarları geri yükleyebilmek için, ayarlarınızın otomatik yedeklenmesini etkinleştirmeniz gerekir." +lastActiveDate: "Son kullanımı" +statusbar: "Durum çubuğu" +pleaseSelect: "Bir seçenek seçin" +reverse: "Tersine" +colored: "Renkli" +refreshInterval: "Güncelleme aralığı" +label: "Etiket" +type: "Tür" +speed: "Hız" +slow: "Yavaş" +fast: "Hızlı" +sensitiveMediaDetection: "Hassas ortamların tespiti" +localOnly: "Yalnızca yerel" +remoteOnly: "Sadece uzaktan" +failedToUpload: "Yükleme başarısız" +cannotUploadBecauseInappropriate: "Bu dosya, dosyanın bazı kısımlarının uygunsuz olabileceği tespit edildiği için yüklenemiyor." +cannotUploadBecauseNoFreeSpace: "Sürücü kapasitesi yetersiz olduğu için yükleme başarısız oldu." +cannotUploadBecauseExceedsFileSizeLimit: "Bu dosya, dosya boyutu sınırını aştığı için yüklenemiyor." +cannotUploadBecauseUnallowedFileType: "Yetkisiz dosya türü nedeniyle yükleme yapılamıyor." +beta: "Beta" +enableAutoSensitive: "Otomatik olarak hassas olarak işaretleme" +enableAutoSensitiveDescription: "Mümkün olduğunda, Makine Öğrenimi yoluyla hassas ortamların otomatik olarak algılanmasını ve işaretlenmesini sağlar. Bu seçenek devre dışı bırakılmış olsa bile, örnek genelinde etkinleştirilebilir." +activeEmailValidationDescription: "E-posta adreslerinin daha sıkı bir şekilde doğrulanmasını sağlar. Bu, tek kullanımlık adreslerin kontrol edilmesini ve adresin gerçekten iletişim kurulabilir olup olmadığının kontrol edilmesini içerir. İşaretlenmediğinde, yalnızca e-postanın biçimi doğrulanır." +navbar: "Gezinti çubuğu" +shuffle: "Karıştır" +account: "Hesap" +move: "Taşı" pushNotification: "Push bildirimleri" subscribePushNotification: "Push bildirimlerini etkinleştir" unsubscribePushNotification: "Push bildirimlerini kapat" pushNotificationAlreadySubscribed: "Push bildirimleri zaten açık" pushNotificationNotSupported: "Push bildirimleri sunucu veya tarayıcı tarafından desteklenmiyor" +sendPushNotificationReadMessage: "Okunduktan sonra push bildirimlerini silin" +sendPushNotificationReadMessageCaption: "Bu, cihazınızın güç tüketimini artırabilir." +windowMaximize: "Maksimize et" +windowMinimize: "Minimize et" +windowRestore: "Geri yükle" +caption: "Alternatif metin" +loggedInAsBot: "Şu anda bot olarak oturum açmış durumdasınız." +tools: "Araçlar" +cannotLoad: "Yüklenemiyor" +numberOfProfileView: "Profil görüntülemeleri" +like: "Beğen" +unlike: "Beğenme" +numberOfLikes: "Beğeniler" +show: "Göster" +neverShow: "Bir daha gösterme" +remindMeLater: "Belki daha sonra" +didYouLikeMisskey: "Misskey'i sevdiniz mi?" +pleaseDonate: "{host} ücretsiz yazılım Misskey kullanmaktadır. Misskey'in geliştirilmesinin devam edebilmesi için bağışlarınızı çok takdir ederiz!" +correspondingSourceIsAvailable: "İlgili kaynak kodu {anchor} adresinde mevcuttur." +roles: "Roller" +role: "Rol" noRole: "Rol bulunamadı" +normalUser: "Normal kullanıcı" +undefined: "Tanımlanmamış" +assign: "Atama" +unassign: "Atamayı kaldır" color: "Renk" +manageCustomEmojis: "Özel Emojileri Yönet" +manageAvatarDecorations: "Avatar süslemelerini yönet" +youCannotCreateAnymore: "Oluşturma sınırına ulaştınız." +cannotPerformTemporary: "Geçici olarak kullanılamıyor" +cannotPerformTemporaryDescription: "Bu işlem, yürütme sınırını aştığı için geçici olarak gerçekleştirilememektedir. Lütfen bir süre bekleyin ve tekrar deneyin." +invalidParamError: "Geçersiz parametreler" +invalidParamErrorDescription: "İstek parametreleri geçersiz. Bu durum genellikle bir hata nedeniyle oluşur, ancak boyut sınırlarını aşan girdiler veya benzer nedenlerden de kaynaklanabilir." +permissionDeniedError: "İşlem reddedildi" +permissionDeniedErrorDescription: "Bu hesap bu işlemi gerçekleştirmek için gerekli izne sahip değildir." +preset: "Ön ayar" +selectFromPresets: "Ön ayarlardan seçim yapın" +achievements: "Başarılar" +gotInvalidResponseError: "Geçersiz sunucu yanıtı" +gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar deneyin." +thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir." +thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder" +thisPostMayBeAnnoyingCancel: "İptal" +thisPostMayBeAnnoyingIgnore: "Yine de gönder" +collapseRenotes: "Zaten gördüğünüz notları daraltın" +collapseRenotesDescription: "Daha önce tepki verdiğiniz veya yeniden not aldığınız notları daraltın." +internalServerError: "İç Sunucu Hatası" +internalServerErrorDescription: "Sunucu beklenmedik bir hatayla karşılaştı." +copyErrorInfo: "Hata ayrıntılarını kopyala" +joinThisServer: "Bu sunucuda kaydolun" +exploreOtherServers: "Başka bir sunucu arayın" +letsLookAtTimeline: "Timeline'a bir göz atın" +disableFederationConfirm: "Federasyonu gerçekten devre dışı bırakmak mı?" +disableFederationConfirmWarn: "Federasyondan ayrılsa bile, aksi belirtilmedikçe gönderiler herkese açık olmaya devam edecektir. Genellikle bunu yapmanız gerekmez." +disableFederationOk: "Devre Dışı" +invitationRequiredToRegister: "Bu etkinlik davetle katılımlıdır. Geçerli bir davet kodu girerek kaydolmanız gerekir." +emailNotSupported: "Bu sunucu, E-Posta göndermeyi desteklemiyor." +postToTheChannel: "Kanalına gönder" +cannotBeChangedLater: "Bu daha sonra değiştirilemez." +reactionAcceptance: "Tepki Kabulü" +likeOnly: "Sadece beğeniler" +likeOnlyForRemote: "Tüm (Yalnızca uzak sunucu için beğeniler)" +nonSensitiveOnly: "Hassas olmayanlar için" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Yalnızca hassas olmayanlar (Yalnızca uzaktan beğeniler)" +rolesAssignedToMe: "Bana atanan roller" +resetPasswordConfirm: "Şifrenizi gerçekten sıfırlamak istiyor musunuz?" +sensitiveWords: "Hassas kelimeler" +sensitiveWordsDescription: "Yapılandırılan kelimelerden herhangi birini içeren tüm notların görünürlüğü otomatik olarak “Ana Sayfa” olarak ayarlanacaktır. Satır sonları ile ayırarak birden fazla not listeleyebilirsiniz." +sensitiveWordsDescription2: "Boşluk kullanmak AND ifadeleri oluşturur ve anahtar kelimeleri eğik çizgi ile çevrelemek bunları düzenli ifadeye dönüştürür." +prohibitedWords: "Yasaklanmış kelimeler" +prohibitedWordsDescription: "Belirlenen kelime(ler)i içeren bir not göndermeye çalışıldığında hata verir. Birden fazla kelime, yeni satırla ayrılmış olarak ayarlanabilir." +prohibitedWordsDescription2: "Boşluk kullanmak AND ifadeleri oluşturur ve anahtar kelimeleri eğik çizgi ile çevrelemek bunları düzenli ifadeye dönüştürür." +hiddenTags: "Gizli hashtag'ler" +hiddenTagsDescription: "Trend listesinde gösterilmeyecek etiketleri seçin.\nSatırlarla birden fazla etiket kaydedilebilir." +notesSearchNotAvailable: "Not arama özelliği kullanılamıyor." +license: "Lisans" +unfavoriteConfirm: "Gerçekten favorilerden kaldırmak istiyor musunuz?" +myClips: "Kliplerim" +drivecleaner: "Sürücü Temizleyici" +retryAllQueuesNow: "Tüm kuyrukları yeniden çalıştırmayı deneyin" +retryAllQueuesConfirmTitle: "Gerçekten hepsini tekrar denemek istiyor musunuz?" +retryAllQueuesConfirmText: "Bu, sunucu yükünü geçici olarak artıracaktır." +enableChartsForRemoteUser: "Uzak kullanıcı veri grafikleri oluşturun" +enableChartsForFederatedInstances: "Uzak sunucu veri grafikleri oluşturun" +enableStatsForFederatedInstances: "Uzak sunucu istatistiklerini alın" +showClipButtonInNoteFooter: "Not eylem menüsüne “Klip” ekle" +reactionsDisplaySize: "Tepki ekran boyutu" +limitWidthOfReaction: "Tepkilerin maksimum genişliğini sınırlayın ve bunları küçültülmüş boyutta görüntüleyin." +noteIdOrUrl: "Not ID veya URL" +video: "Video" +videos: "Videolar" +audio: "Ses" +audioFiles: "Ses Dosyası" +dataSaver: "Veri Tasarrufu" +accountMigration: "Hesap Taşıma" +accountMoved: "Bu kullanıcı yeni bir hesaba taşındı:" +accountMovedShort: "Bu hesap taşınmıştır." +operationForbidden: "İşlem yasak" +forceShowAds: "Her zaman reklamları göster" addMemo: "Kısa not ekle" +editMemo: "Kısa not düzenle" +reactionsList: "Tepkiler" +renotesList: "Renote'lar" +notificationDisplay: "Bildirimler" +leftTop: "Sol üst" +rightTop: "Sağ üst" +leftBottom: "Sol alt" +rightBottom: "Sağ alt" +stackAxis: "Yığınlama yönü" +vertical: "Dikey" +horizontal: "Yatay" +position: "Pozisyon" +serverRules: "Sunucu kuralları" +pleaseConfirmBelowBeforeSignup: "Bu sunucuya kaydolmak için aşağıdakileri gözden geçirip kabul etmelisiniz:" +pleaseAgreeAllToContinue: "Devam etmek için yukarıdaki tüm alanları kabul etmelisiniz." +continue: "Devam et" +preservedUsernames: "Rezerve edilmiş kullanıcı adları" +preservedUsernamesDescription: "Rezervasyon yapmak için kullanıcı adlarını satır sonlarıyla ayırarak listeleyin. Bu kullanıcı adları normal hesap oluşturma sırasında kullanılamaz hale gelir, ancak yöneticiler tarafından manuel olarak hesap oluşturmak için kullanılabilir. Bu kullanıcı adlarını kullanan mevcut hesaplar etkilenmez." +createNoteFromTheFile: "Bu dosyadan not oluşturun" +archive: "Arşiv" +archived: "Arşivle" +unarchive: "Arşivden çıkar" +channelArchiveConfirmTitle: "Gerçekten {name} arşivlemek mi istiyorsunuz?" +channelArchiveConfirmDescription: "Arşivlenmiş bir kanal artık kanal listesinde veya arama sonuçlarında görünmeyecektir. Ayrıca, bu kanala yeni gönderiler eklenemeyecektir." +thisChannelArchived: "Bu kanal arşivlenmiştir." +displayOfNote: "Not ekranı" +initialAccountSetting: "Profil ayarları" +youFollowing: "Takip edildi" +preventAiLearning: "Makine Öğreniminde (Üretken Ai) kullanımını reddet" +preventAiLearningDescription: "Tarayıcılardan, makine öğrenimi (Tahminsel / Üretken Ai) veri kümelerinde yayınlanan metin veya görsel materyalleri vb. kullanmamalarını talep eder. Bu, ilgili içeriğe “noai” HTML-Response bayrağı eklenerek gerçekleştirilir. Ancak, bu bayrakla tam bir önleme sağlanamaz, çünkü bu bayrak basitçe göz ardı edilebilir." +options: "Seçenekler" +specifyUser: "Belirli kullanıcı" +lookupConfirm: "Yukarı bakmak ister misiniz?" +openTagPageConfirm: "Bir hashtag sayfası açmak ister misiniz?" +specifyHost: "Belirli ana bilgisayar" +failedToPreviewUrl: "Önizleme yapılamadı" +update: "Güncelle" +rolesThatCanBeUsedThisEmojiAsReaction: "Bu emojiyi tepki olarak kullanabileceğiniz roller" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Herhangi bir rol belirtilmezse, herkes bu emojiyi tepki olarak kullanabilir." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Bu roller herkese açık olmalıdır." +cancelReactionConfirm: "Tepkinizi gerçekten silmek istiyor musunuz?" +changeReactionConfirm: "Tepkinizi gerçekten değiştirmek istiyor musunuz?" +later: "Daha sonra" +goToMisskey: "Misskey'e" +additionalEmojiDictionary: "Ek emoji sözlükleri" +installed: "Yüklendi" +branding: "Markalaşma" +enableServerMachineStats: "Sunucu donanım istatistiklerini yayınla" +enableIdenticonGeneration: "Kullanıcı identicon oluşturmayı etkinleştir" +turnOffToImprovePerformance: "Devre dışı bırakma, daha yüksek performansa yol açabilir." +createInviteCode: "Davet Kodu oluştur" +createWithOptions: "Seçeneklerle oluştur" +createCount: "Davet sayısı" +inviteCodeCreated: "Davet oluşturuldu" +inviteLimitExceeded: "Oluşturulabilecek davetiyelerin maksimum sayısına ulaştınız." +createLimitRemaining: "{limit} Davet limiti kaldı" +inviteLimitResetCycle: "Bu limit {time} tarihinde {limit} değerine sıfırlanacaktır." +expirationDate: "Son kullanma tarihi" +noExpirationDate: "Son kullanma tarihi yok" +inviteCodeUsedAt: "Kullanılan davet kodu" +registeredUserUsingInviteCode: "Kullanılan davet" +waitingForMailAuth: "E-Posta doğrulama beklemede" +inviteCodeCreator: "Davet oluşturuldu" +usedAt: "Kullanıldığı yer" +unused: "Kullanılmamış" +used: "Kullanılmış" +expired: "Süresi dolmuş" +doYouAgree: "Katılıyor musunuz?" +beSureToReadThisAsItIsImportant: "Lütfen bu önemli bilgileri okuyun." +iHaveReadXCarefullyAndAgree: "“{x}” metnini okudum ve kabul ediyorum." +dialog: "Diyalog" icon: "Avatar" -replies: "yanıt" -renotes: "vazgeçme" +forYou: "Senin için" +currentAnnouncements: "Güncel duyurular" +pastAnnouncements: "Geçmiş duyurular" +youHaveUnreadAnnouncements: "Okunmamış duyurular var." +useSecurityKey: "Güvenlik anahtarınızı veya şifrenizi kullanmak için lütfen tarayıcınızın veya cihazınızın talimatlarını izleyin." +replies: "Yanıtla" +renotes: "Renote'lar" +loadReplies: "Yanıtları göster" +loadConversation: "Konuşmayı göster" +pinnedList: "Sabitlenmiş liste" +keepScreenOn: "Ekranı açık tut" +verifiedLink: "Bağlantı sahipliği doğrulanmıştır." +notifyNotes: "Yeni notlar hakkında bildirimde bulun" +unnotifyNotes: "Yeni notlar hakkında bildirim almayı durdur" +authentication: "Kimlik doğrulama" +authenticationRequiredToContinue: "Devam etmek için lütfen kimlik doğrulaması yapın." +dateAndTime: "Zaman damgası" +showRenotes: "Renot'ları göster" +edited: "Düzenlendi" +notificationRecieveConfig: "Bildirim Ayarları" +mutualFollow: "Karşılıklı takip" +followingOrFollower: "Takip eden veya takipçi" +fileAttachedOnly: "Yalnızca dosya içeren notlar" +showRepliesToOthersInTimeline: "Timeline'da diğer kişilere verilen yanıtları göster" +hideRepliesToOthersInTimeline: "Timeline'dan diğer kişilerin yanıtlarını gizle" +showRepliesToOthersInTimelineAll: "Timeline'da takip ettiğiniz herkesin diğerlerine verdiği yanıtları göster" +hideRepliesToOthersInTimelineAll: "Timeline'de takip ettiğiniz herkesten diğer kişilere verilen yanıtları gizleyin" +confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğiniz herkesin yanıtlarını zaman çizelgenizde diğer kullanıcılara göstermek istiyor musunuz?" +confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğiniz tüm kullanıcıların yanıtlarını zaman tünelinde gerçekten göstermeyecek misiniz?" +externalServices: "Dış Hizmetler" +sourceCode: "Kaynak kodu" +sourceCodeIsNotYetProvided: "Kaynak kodu henüz mevcut değildir. Bu sorunu gidermek için yöneticiyle iletişime geçin." +repositoryUrl: "Depo URL'si" +repositoryUrlDescription: "Misskey'i olduğu gibi kullanıyorsanız (kaynak kodunda herhangi bir değişiklik yapmadan), https://github.com/misskey-dev/misskey adresini girin." +repositoryUrlOrTarballRequired: "Bir depo yayınlamadıysanız, bunun yerine bir tarball sağlamalısınız. Daha fazla bilgi için .config/example.yml dosyasına bakın." +feedback: "Feedback" +feedbackUrl: "Geri Bildirim URL'si" +impressum: "Yayıncı Bilgileri" +impressumUrl: "Yayıncı Bilgileri URL'si" +impressumDescription: "Almanya gibi bazı ülkelerde, ticari web sitelerinde işletmeci iletişim bilgilerinin (Yayıncı) yer alması yasal olarak zorunludur." +privacyPolicy: "Gizlilik Politikası" +privacyPolicyUrl: "Gizlilik Politikası URL'si" +tosAndPrivacyPolicy: "Hizmet Şartları ve Gizlilik Politikası" +avatarDecorations: "Avatar süslemeleri" +attach: "Ek" +detach: "Kaldır" +detachAll: "Tümünü Kaldır" +angle: "Açı" +flip: "Çevir" +showAvatarDecorations: "Avatar süslemelerini göster" +releaseToRefresh: "Yenilemek için serbest bırak" +refreshing: "Yenileniyor..." +pullDownToRefresh: "Yenilemek için aşağı çekin" +useGroupedNotifications: "Gruplandırılmış bildirimleri göster" +signupPendingError: "E-posta adresini doğrulamada bir sorun oluştu. Bağlantının süresi dolmuş olabilir." +cwNotationRequired: "“İçeriği gizle” seçeneği etkinleştirilirse, bir açıklama sağlanmalıdır." +doReaction: "Tepki ekle" +code: "Kod" +reloadRequiredToApplySettings: "Ayarları uygulamak için yeniden yükleme gereklidir." +remainingN: "Kalan: {n}" +overwriteContentConfirm: "Mevcut içeriği üzerine yazmak istediğinizden emin misiniz?" +seasonalScreenEffect: "Mevsimsel Ekran Efekti" +decorate: "Süsle" +addMfmFunction: "MFM ekle" +enableQuickAddMfmFunction: "Gelişmiş MFM seçiciyi göster" +bubbleGame: "Kabarcık Oyunu" +sfx: "Ses Efektleri" +soundWillBePlayed: "Ses çalınacaktır" +showReplay: "Tekrarı izle" +replay: "Tekrar oynat" +replaying: "Tekrar gösteriliyor" +endReplay: "Tekrardan çık" +copyReplayData: "Tekrar oynatma verilerini kopyala" +ranking: "Sıralama" +lastNDays: "Son {n} gün" +backToTitle: "Başlığa geri dön" +hemisphere: "Yaşadığınız yer" +withSensitive: "Hassas dosyalara notlar ekleyin" +userSaysSomethingSensitive: "{name} tarafından gönderilen mesaj hassas içerik barındırmaktadır." +enableHorizontalSwipe: "Kaydırarak sekmeler arasında geçiş yapın" +loading: "Yükleniyor" +surrender: "İptal" +gameRetry: "Tekrar dene" +notUsePleaseLeaveBlank: "Kullanılmıyorsa boş bırakın." +useTotp: "Tek Kullanımlık Şifreyi Girin" +useBackupCode: "Yedek kodları kullanın" +launchApp: "Uygulamayı başlatın" +useNativeUIForVideoAudioPlayer: "Video ve ses oynatımı için tarayıcı kullanıcı arayüzünü kullan" +keepOriginalFilename: "Orijinal dosya adını koru" +keepOriginalFilenameDescription: "Bu ayarı kapatırsanız, dosya yüklediğinizde dosya adları otomatik olarak rastgele bir dizeyle değiştirilecektir." +noDescription: "Açıklama yok" +alwaysConfirmFollow: "Takip ederken her zaman onaylayın" +inquiry: "İletişim" +tryAgain: "Lütfen daha sonra tekrar deneyin." +confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media" +sensitiveMediaRevealConfirm: "Bu hassas bir medya olabilir. Açıklamakta emin misiniz?" +createdLists: "Oluşturulan listeler" +createdAntennas: "Oluşturulan antenler" +fromX: "{x}'den" +genEmbedCode: "Gömme kodu oluştur" +noteOfThisUser: "Bu kullanıcının notları" +clipNoteLimitExceeded: "Bu klibe daha fazla not eklenemez." +performance: "Başarım" +modified: "Değiştirilmiş" +discard: "At" +thereAreNChanges: "{n} değişiklik var." +signinWithPasskey: "Geçiş Anahtarı ile giriş yapın" +unknownWebAuthnKey: "Bilinmeyen Geçiş Anahtarı" +passkeyVerificationFailed: "Geçiş Anahtarı doğrulama başarısız oldu." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "Geçiş anahtarı doğrulaması başarılı oldu ancak şifresiz oturum açma devre dışıdır." +messageToFollower: "Takipçilere mesaj" +target: "Hedef" +testCaptchaWarning: "Bu işlev CAPTCHA testi amacıyla tasarlanmıştır.\nÜretim ortamında kullanmayın." +prohibitedWordsForNameOfUser: "Kullanıcı adları için yasaklanmış kelimeler" +prohibitedWordsForNameOfUserDescription: "Bu listedeki dizilerden herhangi biri kullanıcının adında yer alıyorsa, ad reddedilecektir. Moderatör ayrıcalıklarına sahip kullanıcılar bu kısıtlamadan etkilenmez." +yourNameContainsProhibitedWords: "Adınız yasaklanmış kelimeler içeriyor" +yourNameContainsProhibitedWordsDescription: "Bu adı kullanmak istiyorsanız, lütfen sunucu yöneticinizle iletişime geçin." +thisContentsAreMarkedAsSigninRequiredByAuthor: "Yazar tarafından görüntülemek için oturum açma gerektirir." +lockdown: "Karantina" +pleaseSelectAccount: "Hesap seçin" +availableRoles: "Mevcut roller" +acknowledgeNotesAndEnable: "Önlemleri anladıktan sonra açın." +federationSpecified: "Bu sunucu, beyaz liste federasyonunda çalıştırılmaktadır. Yönetici tarafından belirlenen sunucular dışında diğer sunucularla etkileşim kurmak yasaktır." +federationDisabled: "Bu sunucuda federasyon devre dışıdır. Diğer sunuculardaki kullanıcılarla etkileşim kuramazsınız." +draft: "Taslaklar" +confirmOnReact: "Tepki verirken onaylayın" +reactAreYouSure: "“{emoji}” tepkisini eklemek ister misiniz?" +markAsSensitiveConfirm: "Bu medyayı hassas olarak ayarlamak ister misiniz?" +unmarkAsSensitiveConfirm: "Bu medya için hassas işaretini kaldırmak ister misiniz?" +preferences: "Tercihler" +accessibility: "Erişilebilirlik" +preferencesProfile: "Tercihler profili" +copyPreferenceId: "Tercih ID kopyala" +resetToDefaultValue: "Varsayılana dön" +overrideByAccount: "Hesap tarafından geçersiz kılma" +untitled: "İsimsiz" +noName: "İsim yok" +skip: "Atla" +restore: "Geri yükle" +syncBetweenDevices: "Cihazlar arasında senkronizasyon" +preferenceSyncConflictTitle: "Yapılandırılan değer sunucuda mevcuttur." +preferenceSyncConflictText: "Senkronizasyon etkin ayarlar, değerlerini sunucuya kaydeder. Ancak, sunucuda mevcut değerler bulunmaktadır. Hangi değerleri üzerine yazmak istersiniz?" +preferenceSyncConflictChoiceMerge: "Birleştir" +preferenceSyncConflictChoiceServer: "Sunucuda yapılandırılan değer" +preferenceSyncConflictChoiceDevice: "Cihazda yapılandırılan değer" +preferenceSyncConflictChoiceCancel: "Senkronizasyonu etkinleştirmeyi iptal et" +paste: "Yapıştır" +emojiPalette: "Emoji paleti" +postForm: "Gönderim formu" +textCount: "Karakter sayısı" +information: "Hakkında" +chat: "Sohbet" +migrateOldSettings: "Eski istemci ayarlarını taşıma" +migrateOldSettings_description: "Bu işlem otomatik olarak yapılmalıdır, ancak herhangi bir nedenle geçiş başarısız olursa, geçiş işlemini manuel olarak kendiniz başlatabilirsiniz. Mevcut yapılandırma bilgileri üzerine yazılacaktır." +compress: "Sıkıştır" +right: "Sağ" +bottom: "Alt" +top: "Üst" +embed: "Göm" +settingsMigrating: "Ayarlar taşınıyor, lütfen bir dakika bekleyin... (Daha sonra Ayarlar→Diğerler→Eski ayarları taşı seçeneğine giderek manuel olarak da taşıyabilirsiniz)" +readonly: "Sadece okuma" +goToDeck: "Güverteye Dön" +federationJobs: "Federasyon İşleri" +driveAboutTip: "Drive'da, geçmişte yüklediğiniz dosyaların bir listesi görüntülenir.
\nBu dosyaları notlara eklerken yeniden kullanabilir veya daha sonra paylaşmak üzere önceden yükleyebilirsiniz.
\nBir dosyayı silerken dikkatli olun, çünkü kullanıldığı her yerde (notlar, sayfalar, avatarlar, afişler vb.) mevcut olmayacaktır.
\nAyrıca dosyalarınızı düzenlemek için klasörler oluşturabilirsiniz." +scrollToClose: "Kaydırarak kapatın" +advice: "Tavsiye" +realtimeMode: "Gerçek zamanlı mod" +turnItOn: "Aç" +turnItOff: "Kapat" +emojiMute: "Emoji ses kapat" +emojiUnmute: "Emoji ses aç" +muteX: "Sessiz {x}" +unmuteX: "Sesi aç {x}" +abort: "İptal" +tip: "İpuçları & Püf Noktaları" +redisplayAllTips: "Tüm “İpuçları & Püf Noktaları” tekrar göster" +hideAllTips: "Tüm “İpuçları & Püf Noktaları” gizle" +defaultImageCompressionLevel: "Varsayılan görüntü sıkıştırma düzeyi" +defaultImageCompressionLevel_description: "Düşük seviye görüntü kalitesini korur ancak dosya boyutunu artırır.
Yüksek seviye dosya boyutunu azaltır ancak görüntü kalitesini düşürür." +inMinutes: "Dakika(lar)" +inDays: "Gün(ler)" +safeModeEnabled: "Güvenli mod etkinleştirildi" +pluginsAreDisabledBecauseSafeMode: "Güvenli mod etkinleştirildiği için tüm eklentiler devre dışı bırakılmıştır." +customCssIsDisabledBecauseSafeMode: "Güvenli mod etkin olduğu için özel CSS uygulanmıyor." +themeIsDefaultBecauseSafeMode: "Güvenli mod etkinken, varsayılan tema kullanılır. Güvenli modu devre dışı bırakmak bu değişiklikleri geri alır." +_order: + newest: "Önce yeni" + oldest: "Önce eski" _chat: - home: "Ana sayfa" + noMessagesYet: "Henüz mesaj yok" + newMessage: "Yeni mesaj" + individualChat: "Özel Sohbet" + individualChat_description: "Başka bir kişiyle özel sohbet edin." + roomChat: "Sohbet Odası" + roomChat_description: "Birden fazla kişinin katılabileceği bir sohbet odası.\nÖzel sohbetlere izin vermeyen kişileri de davet edebilirsin, ancak davetini kabul etmeleri gerekir." + createRoom: "Oda Oluştur" + inviteUserToChat: "Kullanıcıları sohbete davet edin" + yourRooms: "Oluşturulan odalar" + joiningRooms: "Katıldığı odalar" + invitations: "Davet" + noInvitations: "Davet yok" + history: "Tarih" + noHistory: "Geçmiş bilgisi mevcut değil" + noRooms: "Oda bulunamadı" + inviteUser: "Kullanıcıları Davet Et" + sentInvitations: "Gönderilen Davetler" + join: "Katıl" + ignore: "Yoksay" + leave: "Odadan çık" + members: "Üyeler" + searchMessages: "Mesajları ara" + home: "Ana Sayfa" + send: "Gönder" + newline: "Yeni satır" + muteThisRoom: "Sessiz oda" + deleteRoom: "Odayı sil" + chatNotAvailableForThisAccountOrServer: "Bu sunucuda veya bu hesapta sohbet özelliği etkin değildir." + chatIsReadOnlyForThisAccountOrServer: "Bu sunucuda veya bu hesapta sohbet okunur modundadır. Yeni mesaj yazamaz veya sohbet odası oluşturamaz/katılamazsınız." + chatNotAvailableInOtherAccount: "Sohbet işlevi diğer kullanıcı için devre dışı bırakılmıştır." + cannotChatWithTheUser: "Bu kullanıcıyla sohbet başlatılamıyor." + cannotChatWithTheUser_description: "Sohbet kullanılamıyor veya karşı taraf sohbeti etkinleştirmedi." + youAreNotAMemberOfThisRoomButInvited: "Bu odanın katılımcısı değilsiniz, ancak bir davet aldınız. Lütfen daveti kabul ederek katılın." + doYouAcceptInvitation: "Daveti kabul ediyor musunuz?" + chatWithThisUser: "Kullanıcıyla sohbet et" + thisUserAllowsChatOnlyFromFollowers: "Bu kullanıcı yalnızca takipçilerinden gelen sohbetleri kabul eder." + thisUserAllowsChatOnlyFromFollowing: "Bu kullanıcı, yalnızca takip ettiği kullanıcılardan gelen sohbetleri kabul eder." + thisUserAllowsChatOnlyFromMutualFollowing: "Bu kullanıcı, yalnızca karşılıklı takip eden kullanıcıların sohbetlerini kabul eder." + thisUserNotAllowedChatAnyone: "Bu kullanıcı kimseyle sohbet etmiyor." + chatAllowedUsers: "Sohbet etmesine izin verilecek kişiler" + chatAllowedUsers_note: "Bu ayardan bağımsız olarak, sohbet mesajı gönderdiğiniz herkesle sohbet edebilirsiniz." + _chatAllowedUsers: + everyone: "Herkes" + followers: "Sadece takipçileriniz" + following: "Only users you are following" + mutual: "Sadece takip ettiğiniz kullanıcılar" + none: "Kimse" +_emojiPalette: + palettes: "Palet" + enableSyncBetweenDevicesForPalettes: "Cihazlar arasında palet senkronizasyonunu etkinleştir" + paletteForMain: "Ana palet" + paletteForReaction: "Reaksiyon paleti" +_settings: + driveBanner: "Sürücüyü yönetebilir ve yapılandırabilir, kullanımı kontrol edebilir ve dosya yükleme ayarlarını yapılandırabilirsiniz." + pluginBanner: "Eklentilerle istemci özelliklerini genişletebilirsiniz. Eklentileri yükleyebilir, ayrı ayrı yapılandırabilir ve yönetebilirsiniz." + notificationsBanner: "Sunucudan gelen bildirimlerin türlerini ve kapsamını ve push bildirimlerini yapılandırabilirsiniz." + api: "API" + webhook: "Webhook" + serviceConnection: "Hizmet entegrasyonu" + serviceConnectionBanner: "Dış uygulamalar veya hizmetlerle entegrasyon sağlamak için erişim belirteçlerini ve Webhook'ları yönetin ve yapılandırın." + accountData: "Hesap verileri" + accountDataBanner: "Hesap verilerini yönetmek için dışa ve içe aktarma." + muteAndBlockBanner: "İçeriği gizlemek ve belirli kullanıcıların eylemlerini kısıtlamak için ayarları yapılandırabilir ve yönetebilirsiniz." + accessibilityBanner: "Müşterinin görsellerini ve davranışını kişiselleştirebilir ve kullanımı optimize etmek için ayarları yapılandırabilirsiniz." + privacyBanner: "Hesap gizliliği ile ilgili ayarları, örneğin içerik görünürlüğü, bulunabilirlik ve takip onayı gibi ayarları yapılandırabilirsiniz." + securityBanner: "Şifre, oturum açma yöntemleri, kimlik doğrulama uygulamaları ve Passkeys gibi hesap güvenliği ile ilgili ayarları yapılandırabilirsiniz." + preferencesBanner: "İstediğiniz şekilde istemcinin genel davranışını yapılandırabilirsiniz." + appearanceBanner: "İstemcinin görünüm ve ekran ayarlarını tercihlerinize göre yapılandırabilirsiniz." + soundsBanner: "İstemcide oynatma için ses ayarlarını yapılandırabilirsiniz." + timelineAndNote: "Timeline ve not" + makeEveryTextElementsSelectable: "Tüm metin öğelerini seçilebilir hale getirin" + makeEveryTextElementsSelectable_description: "Bunu etkinleştirmek bazı durumlarda kullanılabilirliği azaltabilir." + useStickyIcons: "Kaydırma sırasında simgeleri takip et" + enableHighQualityImagePlaceholders: "Yüksek kaliteli görüntüler için yer tutucuları göster" + uiAnimations: "UI Animasyonları" + showNavbarSubButtons: "Navigasyon çubuğunda alt düğmeleri göster" + ifOn: "Açıkken" + ifOff: "Kapalıyken" + enableSyncThemesBetweenDevices: "Yüklü temaları cihazlar arasında senkronize edin" + enablePullToRefresh: "Yenilemek için çekin" + enablePullToRefresh_description: "Fareyi kullanırken, kaydırma tekerleğini basılı tutarken sürükleyin." + realtimeMode_description: "Sunucu ile bağlantı kurar ve içeriği gerçek zamanlı olarak günceller. Bu, trafik ve bellek tüketimini artırabilir." + contentsUpdateFrequency: "İçerik erişim sıklığı" + contentsUpdateFrequency_description: "Değer ne kadar yüksek olursa içerik o kadar sık güncellenir, ancak bu durum performansı düşürür ve trafik ile bellek tüketimini artırır." + contentsUpdateFrequency_description2: "Gerçek zamanlı mod açık olduğunda, bu ayardan bağımsız olarak içerik gerçek zamanlı olarak güncellenir." + showUrlPreview: "URL önizlemesini göster" + showAvailableReactionsFirstInNote: "Mevcut tepkileri en üstte göster." + showPageTabBarBottom: "Sayfa sekme çubuğunu aşağıda göster" + _chat: + showSenderName: "Gönderenin adını göster" + sendOnEnter: "Enter tuşuna basarak gönderin" +_preferencesProfile: + profileName: "Profil adı" + profileNameDescription: "Bu cihazı tanımlayan bir ad belirleyin." + profileNameDescription2: "Örnek: “Ana bilgisayar”, “Akıllı telefon”" + manageProfiles: "Profilleri Yönet" +_preferencesBackup: + autoBackup: "Otomatik yedekleme" + restoreFromBackup: "Yedeklemeden geri yükle" + noBackupsFoundTitle: "Yedekleme bulunamadı" + noBackupsFoundDescription: "Otomatik olarak oluşturulan yedekleme bulunamadı, ancak manuel olarak bir yedekleme dosyası kaydettiyseniz, bunu içe aktarabilir ve geri yükleyebilirsiniz." + selectBackupToRestore: "Geri yüklemek için bir yedekleme seçin" + youNeedToNameYourProfileToEnableAutoBackup: "Otomatik yedeklemeyi etkinleştirmek için bir profil adı ayarlanmalıdır." + autoPreferencesBackupIsNotEnabledForThisDevice: "Bu cihazda ayarların otomatik yedeklemesi etkinleştirilmemiştir." + backupFound: "Ayarların yedeği bulundu" +_accountSettings: + requireSigninToViewContents: "İçeriği görüntülemek için oturum açmanız gerekir." + requireSigninToViewContentsDescription1: "Oluşturduğunuz tüm notları ve diğer içeriği görüntülemek için oturum açmanız gerekir. Bu, tarayıcıların bilgilerinizi toplamasına engel olacaktır." + requireSigninToViewContentsDescription2: "İçerik, URL önizlemelerinde (OGP), web sayfalarına gömülü olarak veya not alıntıları desteklemeyen sunucularda görüntülenmeyecektir." + requireSigninToViewContentsDescription3: "Bu kısıtlamalar, diğer uzak sunuculardan gelen birleştirilmiş içerik için geçerli olmayabilir." + makeNotesFollowersOnlyBefore: "Geçmiş notların yalnızca takipçilere gösterilmesini sağlayın" + makeNotesFollowersOnlyBeforeDescription: "Bu özellik etkinleştirildiğinde, yalnızca takipçiler belirlenen tarih ve saatten sonra veya belirlenen süre boyunca görünür olan notları görebilir. Bu özellik devre dışı bırakıldığında, notun yayın durumu da geri yüklenir." + makeNotesHiddenBefore: "Geçmiş notları gizli yap" + makeNotesHiddenBeforeDescription: "Bu özellik etkinleştirildiğinde, belirlenen tarih ve saatten geçmiş olan veya yalnızca sizin görebildiğiniz notlar. Bu özellik devre dışı bırakıldığında, notun yayın durumu da geri yüklenecektir." + mayNotEffectForFederatedNotes: "Uzak sunucuya bağlı notlar etkilenmeyebilir." + mayNotEffectSomeSituations: "Bu kısıtlamalar basitleştirilmiştir. Uzaktaki bir sunucuda görüntüleme veya moderasyon sırasında gibi bazı durumlarda geçerli olmayabilir." + notesHavePassedSpecifiedPeriod: "Belirtilen sürenin geçtiğini unutmayın." + notesOlderThanSpecifiedDateAndTime: "Belirtilen tarih ve saatten önceki notlar" +_abuseUserReport: + forward: "İleri" + forwardDescription: "Raporu, anonim bir sistem hesabı olarak uzak bir sunucuya iletin." + resolve: "Çözüm" + accept: "Kabul et" + reject: "Reddet" + resolveTutorial: "Raporun içeriği meşruysa, “Kabul Et” seçeneğini seçerek sorunu çözülmüş olarak işaretleyin.\nRaporun içeriği meşru değilse, “Reddet” seçeneğini seçerek raporu yok sayın." _delivery: - stop: "Askıya alınmış" + status: "Teslimat durumu" + stop: "Askıya al" + resume: "Teslimat özgeçmişi" _type: none: "Paylaşım" + manuallySuspended: "Manuel olarak askıya alınmış" + goneSuspended: "Sunucu, sunucunun silinmesi nedeniyle askıya alınmıştır." + autoSuspendedForNotResponding: "Sunucu yanıt vermediği için askıya alınmıştır." + softwareSuspended: "Bu yazılım artık dağıtılmadığı için askıya alınmıştır." +_bubbleGame: + howToPlay: "Nasıl oynanır" + hold: "Tut" + _score: + score: "Skor" + scoreYen: "Kazanılan para miktarı" + highScore: "Yüksek puan" + maxChain: "Maksimum zincir sayısı" + yen: "{yen} Yen" + estimatedQty: "{qty} Adet" + scoreSweets: "{onigiriQtyWithUnit} Onigiri" + _howToPlay: + section1: "Konumu ayarlayın ve nesneyi kutuya bırakın." + section2: "Aynı türden iki nesne birbirine dokunduğunda, farklı bir nesneye dönüşür ve puan kazanırsınız." + section3: "Kutu dolduğunda oyun biter. Kutuyu doldurmadan nesneleri birleştirerek yüksek puan almaya çalışın!" +_announcement: + forExistingUsers: "Sadece mevcut kullanıcılar" + forExistingUsersDescription: "Bu duyuru, etkinleştirildiğinde yalnızca yayınlandığı anda mevcut olan kullanıcılara gösterilecektir. Devre dışı bırakıldığında, yayınlandıktan sonra yeni kaydolan kullanıcılar da bu duyuruyu görecektir." + needConfirmationToRead: "Ayrı okuma onayı gerektirir" + needConfirmationToReadDescription: "Etkinleştirildiğinde, bu duyuruyu okundu olarak işaretlemek için ayrı bir onay mesajı görüntülenir. Bu duyuru, “Tümünü okundu olarak işaretle” işlevinden de hariç tutulur." + end: "Arşiv duyurusu" + tooManyActiveAnnouncementDescription: "Çok fazla aktif duyuru olması kullanıcı deneyimini kötüleştirebilir. Artık geçerliliğini yitirmiş duyuruları arşivlemeyi düşünün." + readConfirmTitle: "Okundu olarak işaretle?" + readConfirmText: "Bu, “{title}” içeriğini okundu olarak işaretleyecektir." + shouldNotBeUsedToPresentPermanentInfo: "Duyuruları, uzun vadede geçerli olacak bilgiler için değil, güncel ve zaman sınırlı bilgileri yayınlamak için kullanmak en iyisidir." + dialogAnnouncementUxWarn: "Aynı anda iki veya daha fazla diyalog tarzı bildirim olması, kullanıcı deneyimini önemli ölçüde etkileyebilir, bu nedenle lütfen bunları dikkatli kullanın." + silence: "Bildirim yok" + silenceDescription: "Bu seçeneği etkinleştirdiğinizde, bu duyurunun bildirimi atlanacak ve kullanıcı bunu okumak zorunda kalmayacaktır." +_initialAccountSetting: + accountCreated: "Hesabınız başarıyla oluşturuldu!" + letsStartAccountSetup: "Öncelikle, profilinizi oluşturalım." + letsFillYourProfile: "Öncelikle profilinizi oluşturalım." + profileSetting: "Profil ayarları" + privacySetting: "Gizlilik ayarları" + theseSettingsCanEditLater: "Bu ayarları daha sonra istediğiniz zaman değiştirebilirsiniz." + youCanEditMoreSettingsInSettingsPageLater: "“Ayarlar” sayfasından yapılandırabileceğiniz daha birçok ayar bulunmaktadır. Daha sonra mutlaka ziyaret edin." + followUsers: "İlgilendiğiniz bazı kullanıcıları takip ederek zaman akışınızı oluşturmaya çalışın." + pushNotificationDescription: "Push bildirimlerini etkinleştirdiğinizde, {name} adresinden gelen bildirimleri doğrudan cihazınıza alabilirsiniz." + initialAccountSettingCompleted: "Profil kurulumu tamamlandı!" + haveFun: "{name}'in keyfini çıkarın!" + youCanContinueTutorial: "{name} (Misskey) kullanımına ilişkin bir eğiticiye geçebilir veya buradan kurulumu sonlandırıp hemen kullanmaya başlayabilirsiniz." + startTutorial: "Öğreticiye başla" + skipAreYouSure: "Profil kurulumunu gerçekten atlamak mı istiyorsunuz?" + laterAreYouSure: "Profil ayarlarını gerçekten daha sonra mı yapacaksınız?" +_initialTutorial: + launchTutorial: "Öğreticiyi izle" + title: "Öğretici" + wellDone: "Tebrikler!" + skipAreYouSure: "Öğreticiyi kapatmak mı istiyorsunuz?" + _landing: + title: "Öğreticiye hoş geldiniz" + description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsiniz." + _note: + title: "Not nedir?" + description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar zaman çizelgesinde kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir." + reply: "Bir mesaja yanıt vermek için bu düğmeye tıklayın. Yanıtlara yanıt vermek de mümkündür, böylece konuşma bir konu başlığı gibi devam eder." + renote: "Bu notu kendi zaman çizelgenizde paylaşabilirsiniz. Ayrıca yorumlarınızla birlikte alıntı da yapabilirsiniz." + reaction: "Not'a tepkiler ekleyebilirsiniz. Daha fazla ayrıntı bir sonraki sayfada açıklanacaktır." + menu: "Not ayrıntılarını görüntüleyebilir, bağlantıları kopyalayabilir ve çeşitli diğer işlemleri gerçekleştirebilirsiniz." + _reaction: + title: "Reaksiyonlar nedir?" + description: "Notlara çeşitli emojilerle tepki verilebilir. Tepkiler, sadece bir ‘beğeni’ ile ifade edilemeyen nüansları ifade etmenizi sağlar." + letsTryReacting: "Notun üzerindeki ‘+’ düğmesine tıklayarak tepkiler eklenebilir. Bu örnek nota tepki verin!" + reactToContinue: "Devam etmek için bir tepki ekleyin." + reactNotification: "Birisi notunuza tepki verdiğinde gerçek zamanlı bildirimler alacaksınız." + reactDone: "“-” düğmesine basarak bir tepkiyi geri alabilirsiniz." + _timeline: + title: "Timeline Kavramı" + description1: "Misskey, kullanıma göre birden fazla Timeline sunar (bazı Timeline'lar sunucunun politikalarına bağlı olarak kullanılamayabilir)." + home: "Takip ettiğiniz hesapların notlarını görüntüleyebilirsiniz." + local: "Bu sunucudaki tüm kullanıcıların notlarını görüntüleyebilirsiniz." + social: "Ev ve Yerel Timeline'dan notlar görüntülenecektir." + global: "Bağlı tüm sunuculardan gelen notları görüntüleyebilirsiniz." + description2: "Ekranın üst kısmındaki Timeline'lar arasında istediğiniz zaman geçiş yapabilirsiniz." + description3: "Ayrıca, Liste Timeline'ı ve Kanal Timeline'ı da bulunmaktadır. Daha fazla ayrıntı için lütfen {link} adresine bakın." + _postNote: + title: "Not Yayınlama Ayarları" + description1: "Misskey'de not yayınlarken çeşitli seçenekler mevcuttur. Yayınlama formu şu şekildedir." + _visibility: + description: "Notunuzu kimlerin görüntüleyebileceğini sınırlayabilirsiniz." + public: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır." + home: "Yalnızca Ana zaman akışında herkese açık. Profilinizi ziyaret edenler, takipçileriniz ve yeniden notlar aracılığıyla bunu görebilirler." + followers: "Sadece takipçiler tarafından görülebilir. Sadece takipçiler görebilir, başkaları göremez ve başkaları tarafından yeniden not edilemez." + direct: "Yalnızca belirli kullanıcılar tarafından görülebilir ve alıcıya bildirim gönderilir. Doğrudan mesajlaşma yerine alternatif olarak kullanılabilir." + doNotSendConfidencialOnDirect1: "Hassas bilgileri gönderirken dikkatli olun!" + doNotSendConfidencialOnDirect2: "Sunucu yöneticileri yazdıklarınızı görebilir. Güvenilir olmayan sunuculardaki kullanıcılara doğrudan not gönderirken hassas bilgilere dikkat edin." + localOnly: "Bu bayrakla yayınlamak, notu diğer sunuculara aktarmaz. Diğer sunuculardaki kullanıcılar, yukarıdaki görüntüleme ayarlarından bağımsız olarak bu notları doğrudan görüntüleyemezler." + _cw: + title: "İçerik Uyarısı" + description: "Gövde yerine, “yorumlar” alanına yazılan içerik görüntülenecektir. “Devamını oku” düğmesine basıldığında gövde görüntülenecektir." + _exampleNote: + cw: "Bu kesinlikle sizi acıktıracak!" + note: "Az önce çikolata kaplı bir donut yedim 🍩😋" + useCases: "Bu, sunucu kurallarına uyulurken, gerekli notlar için veya spoiler veya hassas metinlerin kendi kendine kısıtlanması için kullanılır." + _howToMakeAttachmentsSensitive: + title: "Ekleri Hassas Olarak İşaretleme" + description: "Sunucu kuralları gereği gerekli olan veya bozulmaması gereken ekler için “hassas” bayrağı ekleyin." + tryThisFile: "Bu forma ekli resmi hassas olarak işaretlemeyi deneyin!" + _exampleNote: + note: "Oops, natto kapağını açarken berbat ettim..." + method: "Bir eki hassas olarak işaretlemek için, dosya küçük resmini tıklayın, menüyü açın ve “Hassas Olarak İşaretle” seçeneğini tıklayın." + sensitiveSucceeded: "Dosya eklerken, lütfen sunucu kurallarına uygun olarak hassasiyet ayarlarını yapın." + doItToContinue: "Devam etmek için ek dosyayı hassas olarak işaretleyin." + _done: + title: "Eğitimi tamamladınız! 🎉" + description: "Burada tanıtılan işlevler sadece küçük bir kısmıdır. Misskey'i kullanma konusunda daha ayrıntılı bilgi için lütfen şu kaynağa bakın: {link}." +_timelineDescription: + home: "Ana Timeline'da, takip ettiğiniz hesapların notlarını görebilirsiniz." + local: "Yerel Timeline'de, bu sunucudaki tüm kullanıcıların notlarını görebilirsiniz." + social: "Sosyal Timeline, Ana Sayfa ve Yerel Timeline'dan gelen notları görüntüler." + global: "Global Timeline'da, bağlı tüm sunuculardan gelen notları görebilirsiniz." +_serverRules: + description: "Kayıt öncesinde gösterilecek bir dizi kural. Hizmet Şartlarının özetini belirlemeniz önerilir." +_serverSettings: + iconUrl: "Simge URL'si" + appIconDescription: " {host} bir uygulama olarak görüntülendiğinde kullanılacak simgeyi belirtir." + appIconUsageExample: "Örneğin, PWA olarak veya bir telefonda ana ekran yer imi olarak görüntülendiğinde" + appIconStyleRecommendation: "Simge kare veya daire şeklinde kırpılabileceğinden, içeriğin etrafında renkli kenar boşluğu bulunan bir simge kullanılması önerilir." + appIconResolutionMustBe: "Minimum çözünürlük {resolution}'tür." + manifestJsonOverride: "manifest.json Geçersiz Kılma" + shortName: "Kısa ad" + shortNameDescription: "Resmi adın uzun olması durumunda görüntülenebilen, örneğin adının kısaltması." + fanoutTimelineDescription: "Etkinleştirildiğinde Timeline alma performansını büyük ölçüde artırır ve veritabanı yükünü azaltır. Bunun karşılığında Redis'in bellek kullanımı artacaktır. Sunucu belleği düşükse veya sunucu kararsızsa bunu devre dışı bırakmayı düşünün." + fanoutTimelineDbFallback: "Veritabanına geri dön" + fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Timeline önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek zaman çizelgelerinin aralığını sınırlar." + reactionsBufferingDescription: "Etkinleştirildiğinde, reaksiyon oluşturma sırasında performans büyük ölçüde artacak ve veritabanı üzerindeki yük azalacaktır. Ancak, Redis bellek kullanımı artacaktır." + remoteNotesCleaning: "Uzak notların otomatik olarak temizlenmesi" + remoteNotesCleaning_description: "Etkinleştirildiğinde, kullanılmayan ve güncelliğini yitirmiş uzak notlar, veritabanının şişmesini önlemek için periyodik olarak temizlenecektir." + remoteNotesCleaningMaxProcessingDuration: "Maksimum temizleme işlem süresi" + remoteNotesCleaningExpiryDaysForEachNotes: "Notları saklamak için minimum gün sayısı" + inquiryUrl: "Sorgu URL'si" + inquiryUrlDescription: "Sorgu formu için sunucu yöneticisine bir URL veya iletişim bilgileri için bir web sayfası belirtin." + openRegistration: "Hesap oluşturmayı açık hale getirin" + openRegistrationWarning: "Kayıt açma işlemi riskler içerir. Sunucuyu sürekli olarak izleyen ve herhangi bir sorun durumunda hemen müdahale edebilen bir sisteminiz varsa, bu işlemi etkinleştirmeniz önerilir." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Bir süre boyunca moderatör etkinliği algılanmazsa, spam'ı önlemek için bu ayar otomatik olarak kapatılır." + deliverSuspendedSoftware: "Askıya Alınan Yazılım" + deliverSuspendedSoftwareDescription: "Güvenlik açığı veya diğer nedenlerle sunucunun yazılımının belirli bir isim ve sürüm aralığı için teslimatı durdurabilirsiniz. Bu sürüm bilgileri sunucu tarafından sağlanır ve güvenilirliği garanti edilmez. Sürümü belirtmek için semver aralığı belirtilebilir, ancak >= 2024.3.1 belirtildiğinde 2024.3.1-custom.0 gibi özel sürümler dahil edilmez, bu nedenle >= 2024.3.1-0 gibi ön sürüm belirtimi kullanılması önerilir." + singleUserMode: "Tek kullanıcı modu" + singleUserMode_description: "Bu sunucunun tek kullanıcısıysanız, bu modu etkinleştirerek performansını optimize edebilirsiniz." + signToActivityPubGet: "ActivityPub GET isteklerini imzalayın" + signToActivityPubGet_description: "Normalde bu özellik etkinleştirilmiş olmalıdır. Bu özelliği devre dışı bırakmak federasyonla ilgili sorunları iyileştirebilir, ancak diğer yandan bazı diğer sunuculara yönelik federasyonu devre dışı bırakabilir." + proxyRemoteFiles: "Proxy uzak dosyalar" + proxyRemoteFiles_description: "Etkinleştirildiğinde, sunucu uzak dosyaları proxy olarak kullanır ve sunar. Bu, resim küçük resimleri oluşturmak ve kullanıcı gizliliğini korumak için kullanışlıdır." + allowExternalApRedirect: "ActivityPub aracılığıyla yapılan sorgular için yönlendirmelere izin ver" + allowExternalApRedirect_description: "Etkinleştirildiğinde, diğer sunucular bu sunucu aracılığıyla üçüncü taraf içeriğini sorgulayabilir, ancak bu durum içerik sahteciliğine yol açabilir." + userGeneratedContentsVisibilityForVisitor: "Kullanıcılar tarafından oluşturulan içeriğin misafirlere görünürlüğü" + userGeneratedContentsVisibilityForVisitor_description: "Bu, uygunsuz ve iyi denetlenmemiş uzaktaki içeriğin kendi sunucunuz aracılığıyla istemeden internette yayınlanmasını önlemek için yararlıdır." + userGeneratedContentsVisibilityForVisitor_description2: "Sunucu tarafından alınan uzak içerik dahil olmak üzere sunucudaki tüm içeriği koşulsuz olarak İnternet'e yayınlamak risklidir. Bu, içeriğin dağıtılmış yapısından haberdar olmayan misafirler için özellikle önemlidir, çünkü onlar yanlışlıkla uzak içeriğin bile sunucudaki kullanıcılar tarafından oluşturulan içerik olduğunu düşünebilirler." + restartServerSetupWizardConfirm_title: "Sunucu kurulum sihirbazını yeniden başlatmak ister misiniz?" + restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır." + _userGeneratedContentsVisibilityForVisitor: + all: "Her şey halka açıktır." + localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur." + none: "Her şey gizlidir." +_accountMigration: + moveFrom: "Başka bir hesabı bu hesaba taşıyın" + moveFromSub: "Başka bir hesaba takma ad oluşturun" + moveFromLabel: "Orijinal Hesap #{n}" + moveFromDescription: "Bu hesaptan taşınacak hesap için bir takma ad oluşturmalısınız.\nTaşınacak hesabı aşağıdaki biçimde girin: @username@server.example.com\nTakma adı silmek için alanı boş bırakın (önerilmez)." + moveTo: "Bu hesabı başka bir hesaba taşıyın" + moveToLabel: "Taşınacak hesap:" + moveCannotBeUndone: "Hesap taşıma işlemi geri alınamaz." + moveAccountDescription: "Bu işlem, hesabınızı farklı bir hesaba taşıyacaktır.\n・Bu hesabın takipçileri otomatik olarak yeni hesaba taşınacaktır.\n・Bu hesap, şu anda takip ettiği tüm kullanıcıları takipten çıkaracaktır.\n・Bu hesapta yeni notlar vb. oluşturamayacaksınız.\n\nTakipçilerin taşınması otomatik olarak gerçekleşirken, takip ettiğiniz kullanıcıların listesini taşımak için bazı adımları manuel olarak hazırlamanız gerekir. Bunu yapmak için, ayarlar menüsünden takipçilerinizi dışa aktarın ve daha sonra yeni hesaba içe aktarın. Aynı prosedür, listelerinizin yanı sıra sessize aldığınız ve engellediğiniz kullanıcılar için de geçerlidir.\n\n(Bu açıklama Misskey v13.12.0 ve sonraki sürümler için geçerlidir. Mastodon gibi diğer ActivityPub yazılımları farklı şekilde çalışabilir.)" + moveAccountHowTo: "Geçiş yapmak için, önce taşınacak hesapta bu hesap için bir takma ad oluşturun.\nTakma adı oluşturduktan sonra, taşınacak hesabı aşağıdaki biçimde girin: @username@server.example.com" + startMigration: "Taşın" + migrationConfirm: "Bu hesabı {account} hesabına gerçekten taşımak istiyor musunuz? Bu işlem başlatıldıktan sonra durdurulamaz veya geri alınamaz ve bu hesabı artık orijinal haliyle kullanamazsınız." + movedAndCannotBeUndone: "\nBu hesap taşınmıştır.\nTaşıma işlemi geri alınamaz." + postMigrationNote: "Bu hesap, geçiş işlemi tamamlandıktan 24 saat sonra şu anda takip ettiği tüm hesapları takipten çıkaracaktır.\nHem takipçi sayısı hem de takip edilenler sayısı sıfır olacaktır. Takipçilerinizin bu hesabın yalnızca takipçilere açık gönderilerini görememesi durumunu önlemek için, takipçileriniz bu hesabı takip etmeye devam edecektir." + movedTo: "Yeni hesap:" +_achievements: + earnedAt: "Şurada açıldı" + _types: + _notes1: + title: "msky'ımı kuruyorum" + description: "İlk notunuzu yayınlayın" + flavor: "Misskey ile iyi vakit geçirin!" + _notes10: + title: "Bazı notlar" + description: "10 not gönder" + _notes100: + title: "Çok sayıda not" + description: "100 notu yayınla" + _notes500: + title: "Notlarla kaplı" + description: "500 notu yayınla" + _notes1000: + title: "Notlardan oluşan bir dağ" + description: "1.000 not yayınla" + _notes5000: + title: "Taşan notlar" + description: "5.000 not yayınla" + _notes10000: + title: "Süper not" + description: "10.000 not yayınla" + _notes20000: + title: "Daha... fazla... not... lazım..." + description: "20.000 not yayınla" + _notes30000: + title: "Notlar notlar notlar!" + description: "30.000 not yayınla" + _notes40000: + title: "Not fabrikası" + description: "40.000 not yayınla" + _notes50000: + title: "Notların gezegeni" + description: "50.000 not yayınla" + _notes60000: + title: "Not kuasar" + description: "60.000 not yayınla" + _notes70000: + title: "Not kara deliği" + description: "70.000 not yayınla" + _notes80000: + title: "Not galaksisi" + description: "80.000 not yayınla" + _notes90000: + title: "Not evreni" + description: "90.000 not yayınla" + _notes100000: + title: "TÜM NOTLARINIZ BİZE AİTTİR" + description: "100.000 yayınlanmış not" + flavor: "Gerçekten söyleyecek çok şeyin var." + _login3: + title: "Başlangıç I" + description: "Log in for a total of 3 days" + flavor: "Toplam 3 gün boyunca oturum açın" + _login7: + title: "Başlangıç II" + description: "Toplam 7 gün boyunca oturum açın" + flavor: "Henüz işlerin nasıl yürüdüğünü anladığını hissediyor musun?" + _login15: + title: "Başlangıç III" + description: "Toplam 15 gün boyunca oturum açın" + _login30: + title: "Misskist I" + description: "Toplam 30 gün boyunca oturum açın" + _login60: + title: "Misskist II" + description: "Toplam 60 gün boyunca oturum açın" + _login100: + title: "Misskist III" + description: "Toplam 100 gün boyunca oturum açın" + flavor: "Şiddetli Misskist" + _login200: + title: "Düzenli I" + description: "Toplam 200 gün boyunca oturum açın." + _login300: + title: "Düzenli II" + description: "Toplam 300 gün boyunca oturum açın" + _login400: + title: "Düzenli III" + description: "Toplam 400 gün boyunca oturum açın" + _login500: + title: "Uzman I" + description: "Toplam 500 gün boyunca oturum açın" + flavor: "Arkadaşlar, sık sık not almayı sevdiğim söylenir." + _login600: + title: "Uzman II" + description: "Toplam 600 gün boyunca oturum açın" + _login700: + title: "Uzman III" + description: "Toplam 700 gün boyunca oturum açın" + _login800: + title: "Notların Ustası I" + description: "Toplam 800 gün boyunca oturum açın" + _login900: + title: "Notların Ustası II" + description: "Toplam 900 gün boyunca oturum açın" + _login1000: + title: "Notların Ustası III" + description: "Toplam 1.000 gün boyunca oturum açın." + flavor: "Misskey'i kullandığınız için teşekkür ederiz!" + _noteClipped1: + title: "Kesinlikle... kesmeliyim..." + description: "İlk notunuzu ekleyin" + _noteFavorited1: + title: "Yıldız gözlemcisi" + description: "İlk notunu favorilerine ekle" + _myNoteFavorited1: + title: "Yıldızları Arayış" + description: "Başka birinin notlarınızdan birini favorilerine eklemesini sağlayın" + _profileFilled: + title: "İyi hazırlanmış" + description: "Profilinizi oluşturun" + _markedAsCat: + title: "Ben bir kediyim." + description: "Hesabınızı kedi olarak işaretleyin" + flavor: "Sana daha sonra bir isim vereceğim." + _following1: + title: "İlk kullanıcınızı takip edin" + description: "Bir kullanıcıyı takip et" + _following10: + title: "Devam et... devam et..." + description: "10 kullanıcıyı takip et" + _following50: + title: "Bir sürü arkadaş" + description: "50 hesabı takip et" + _following100: + title: "100 Arkadaş" + description: "100 hesabı takip et" + _following300: + title: "Arkadaş yüklemesi" + description: "300 hesabı takip et" + _followers1: + title: "İlk takipçi" + description: "1 takipçi kazanın" + _followers10: + title: "Beni takip edin!" + description: "10 takipçi kazanın" + _followers50: + title: "Kalabalıklar halinde gelmek" + description: "50 takipçi kazanın" + _followers100: + title: "Popüler" + description: "100 takipçi kazanın" + _followers300: + title: "Lütfen tek sıra halinde dizilin." + description: "300 takipçi kazanın" + _followers500: + title: "Radyo Kulesi" + description: "500 takipçi kazanın" + _followers1000: + title: "Etkileyici" + description: "1.000 takipçi kazanın" + _collectAchievements30: + title: "Başarı Koleksiyoncusu" + description: "30 başarı kazan" + _viewAchievements3min: + title: "Beğeniler Başarılar" + description: "Likes Achievements" + _iLoveMisskey: + title: "Misskey'i seviyorum" + description: "“I ❤ #Misskey” yazısını paylaş" + flavor: "Misskey geliştirme ekibi desteğiniz için çok teşekkür eder!" + _foundTreasure: + title: "Hazine Avı" + description: "Gizli hazineyi buldunuz." + _client30min: + title: "Kısa mola" + description: "Misskey'i en az 30 dakika açık tutun." + _client60min: + title: "Misskey'de “Miss” yok" + description: "Misskey'i en az 60 dakika açık tutun." + _noteDeletedWithin1min: + title: "Nevermind" + description: "Boş ver" + _postedAtLateNight: + title: "Gececi" + description: "Gece geç saatlerde bir not yayınlayın" + flavor: "Yatma vakti geldi." + _postedAt0min0sec: + title: "Konuşan Saat" + description: "00:00'da bir not yayınlayın." + flavor: "Tık tık tık, güm!" + _selfQuote: + title: "Öz Referans" + description: "Kendi notunuzu alıntı yapın" + _htl20npm: + title: "Akış Zaman Çizelgesi" + description: "Ev zaman çizelgenizin hızı 20 npm'yi (dakika başına not sayısı) aşıyor mu?" + _viewInstanceChart: + title: "Analist" + description: "Sunucunuzun grafiklerini görüntüleyin" + _outputHelloWorldOnScratchpad: + title: "Merhaba, dünya!" + description: "Scratchpad'de “hello world” yazdırın." + _open3windows: + title: "Çoklu Pencere" + description: "Aynı anda en az 3 pencere açık olsun." + _driveFolderCircularReference: + title: "Döngüsel Referans" + description: "Drive'da yinelemeli olarak iç içe geçmiş bir klasör oluşturmaya çalışın." + _reactWithoutRead: + title: "Gerçekten okudun mu?" + description: "100 karakterden uzun bir notun yayınlanmasından itibaren 3 saniye içinde yanıt verin." + _clickedClickHere: + title: "Buraya tıklayın" + description: "Buraya tıkladınız" + _justPlainLucky: + title: "Sadece Şanslı" + description: "Her 10 saniyede bir %0,005 olasılıkla elde edilme şansı vardır." + _setNameToSyuilo: + title: "Tanrı Kompleksi" + description: "Adınızı “syuilo” olarak ayarlayın." + _passedSinceAccountCreated1: + title: "Birinci Yıl Dönümü" + description: "Hesabınızın oluşturulmasından bu yana bir yıl geçti." + _passedSinceAccountCreated2: + title: "İki Yıllık Yıldönümü" + description: "Hesabınızın oluşturulmasından bu yana iki yıl geçti." + _passedSinceAccountCreated3: + title: "Üçüncü Yıl Dönümü" + description: "Hesabınızın oluşturulmasından bu yana üç yıl geçti." + _loggedInOnBirthday: + title: "Doğum günün kutlu olsun" + description: "Doğum gününüzde giriş yapın" + _loggedInOnNewYearsDay: + title: "Yeni yılınız kutlu olsun!" + description: "Yılın ilk gününde oturum açıldı" + flavor: "Bu sunucuda bir başka harika yıla" + _cookieClicked: + title: "Çerezleri tıklayarak oynanan bir oyun" + description: "Çerezi tıkladı" + flavor: "Wait, are you on the correct website?" + _brainDiver: + title: "Brain Diver" + description: "Brain Diver bağlantısını paylaşın" + flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "Test taşması" + description: "Bildirim testini çok kısa bir süre içinde tekrar tekrar tetikleyin." + _tutorialCompleted: + title: "Misskey Temel Kurs Diploması" + description: "Eğitim tamamlandı" + _bubbleGameExplodingHead: + title: "🤯" + description: "Kabarcık oyunundaki en büyük nesne" + _bubbleGameDoubleExplodingHead: + title: "Çift🤯" + description: "Aynı anda balon oyunundaki en büyük iki nesne" + flavor: "Öğle yemeği kutunuzu şöyle doldurabilirsiniz 🤯 🤯 biraz." +_role: + new: "Yeni rol" + edit: "Rolü düzenle" + name: "Rol adı" + description: "Rol tanımı" + permission: "Rol izinleri" + descriptionOfPermission: "Moderators temel moderasyon işlemlerini gerçekleştirebilir.\nAdministrators örneğin tüm ayarlarını değiştirebilir." + assignTarget: "Görev türü" + descriptionOfAssignTarget: "Bu rolün parçası olan ve olmayan kişileri manuel olarak değiştirmek için
manuel
.\nKullanıcıların bir koşula bağlı olarak bu role otomatik olarak atanmasını ve bu rolden çıkarılmasını sağlamak için koşullu." + manual: "Kılavuz" + manualRoles: "Manuel roller" + conditional: "Koşullu" + conditionalRoles: "Koşullu roller" + condition: "Durum" + isConditionalRole: "Bu, koşullu bir roldür." + isPublic: "Kamu rolü" + descriptionOfIsPublic: "Bu rol, atanan kullanıcıların profillerinde görüntülenecektir." + options: "Seçenekler" + policies: "Politikalar" + baseRole: "Rol şablonu" + useBaseValue: "Rol şablonu değerini kullan" + chooseRoleToAssign: "Atamak istediğiniz rolü seçin" + iconUrl: "Simge URL'si" + asBadge: "Rozet olarak göster" + descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." + isExplorable: "Rolü keşfedilebilir hale getirin" + descriptionOfIsExplorable: "Bu rolün zaman çizelgesi ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecektir." + displayOrder: "Pozisyon" + descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur." + preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun" + preserveAssignmentOnMoveAccount_description: "Etkinleştirildiğinde, bu rol, bu role sahip bir hesap taşındığında hedef hesaba aktarılacaktır." + canEditMembersByModerator: "Moderatörlerin bu rol için üye listesini düzenlemesine izin ver" + descriptionOfCanEditMembersByModerator: "Etkinleştirildiğinde, moderatörler ve yöneticiler bu role kullanıcıları atayabilir ve atamalarını kaldırabilir. Devre dışı bırakıldığında, yalnızca yöneticiler kullanıcıları atayabilir." + priority: "Öncelik" + _priority: + low: "Düşük" + middle: "Orta" + high: "Yüksek" + _options: + gtlAvailable: "Küresel zaman çizelgesini görüntüleyebilir" + ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir" + canPublicNote: "Halka açık notlar gönderebilir" + mentionMax: "Bir notta maksimum bahsetme sayısı" + canInvite: "Sunucu davet kodları oluşturabilir" + inviteLimit: "Davet sınırı" + inviteLimitCycle: "Davet sınırı bekleme süresi" + inviteExpirationTime: "Davet süresi dolma aralığı" + canManageCustomEmojis: "Özel emojileri yönetebilir" + canManageAvatarDecorations: "Avatar süslemelerini yönet" + driveCapacity: "Sürücü kapasitesi" + maxFileSize: "Yükleyebileceğiniz maksimum dosya boyutu" + alwaysMarkNsfw: "Dosyaları her zaman NSFW olarak işaretleyin" + canUpdateBioMedia: "Bir simge veya banner görüntüsünü düzenleyebilir" + pinMax: "Sabitlenmiş notların maksimum sayısı" + antennaMax: "Maksimum anten sayısı" + wordMuteMax: "Kelime sessizlerinde izin verilen maksimum karakter sayısı" + webhookMax: "Maksimum Webhook sayısı" + clipMax: "Maksimum klip sayısı" + noteEachClipsMax: "Bir klip içindeki maksimum nota sayısı" + userListMax: "Maksimum kullanıcı listesi sayısı" + userEachUserListsMax: "Kullanıcı listesindeki maksimum kullanıcı sayısı" + rateLimitFactor: "Hız Sınırı" + descriptionOfRateLimitFactor: "Daha düşük oran sınırları daha az kısıtlayıcıdır, daha yüksek olanlar ise daha kısıtlayıcıdır." + canHideAds: "Reklamları gizleyebilir" + canSearchNotes: "Not arama kullanımı" + canUseTranslator: "Çevirmen kullanımı" + avatarDecorationLimit: "Uygulanabilecek maksimum avatar süsleme sayısı" + canImportAntennas: "Antenlerin içe aktarılmasına izin ver" + canImportBlocking: "Engellemeyi içe aktarmaya izin ver" + canImportFollowing: "Aşağıdakilerin içe aktarılmasına izin ver" + canImportMuting: "Sessize alma özelliğini içe aktarmaya izin ver" + canImportUserLists: "Listelerin içe aktarılmasına izin ver" + chatAvailability: "Sohbeti İzin Ver" + uploadableFileTypes: "Yüklenebilir dosya türleri" + uploadableFileTypes_caption: "İzin verilen MIME/dosya türlerini belirtir. Birden fazla MIME türü, yeni bir satırla ayırarak belirtilebilir ve joker karakterler yıldız işareti (*) ile belirtilebilir. (örneğin, image/*)" + uploadableFileTypes_caption2: "Bazı dosya türleri algılanamayabilir. Bu tür dosyalara izin vermek için, spesifikasyona {x} ekleyin." + noteDraftLimit: "Sunucu notlarının olası taslak sayısı" + watermarkAvailable: "Filigran işlevinin kullanılabilirliği" + _condition: + roleAssignedTo: "Manuel rollere atanmış" + isLocal: "Yerel kullanıcı" + isRemote: "Uzak kullanıcı" + isCat: "Kedi Kullanıcıları" + isBot: "Bot Kullanıcıları" + isSuspended: "Askıya alınmış kullanıcı" + isLocked: "Özel hesaplar" + isExplorable: "“Hesabı bulunabilir hale getir” özelliğini etkili bir şekilde kullanan kullanıcı" + createdLessThan: "Hesap oluşturulduktan sonra X'ten az zaman geçti." + createdMoreThan: "Hesap oluşturulmasından bu yana X'ten fazla zaman geçti." + followersLessThanOrEq: "X veya daha az takipçisi var" + followersMoreThanOrEq: "X veya daha fazla takipçisi var" + followingLessThanOrEq: "X veya daha az sayıda hesabı takip ediyor" + followingMoreThanOrEq: "X veya daha fazla hesabı takip ediyor" + notesLessThanOrEq: "Gönderi sayısı şundan az/eşit" + notesMoreThanOrEq: "Gönderi sayısı şundan büyük/eşit" + and: "Koşul-AND" + or: "Koşul-QR" + not: "Koşul-NOT" +_sensitiveMediaDetection: + description: "Makine öğrenimi yoluyla hassas medyayı otomatik olarak tanıyarak sunucu moderasyonunun yükünü azaltır. Bu, sunucu üzerindeki yükü biraz artıracaktır." + sensitivity: "Algılama hassasiyeti" + sensitivityDescription: "Hassasiyeti azaltmak, yanlış algılamaların (yanlış pozitifler) azalmasına neden olurken, hassasiyeti artırmak ise algılamaların kaçırılmasının (yanlış negatifler) azalmasına neden olur." + setSensitiveFlagAutomatically: "Hassas olarak işaretle" + setSensitiveFlagAutomaticallyDescription: "Bu seçenek kapatılsa bile, dahili algılama sonuçları korunacaktır." + analyzeVideos: "Videoların analizini etkinleştir" + analyzeVideosDescription: "Görüntülerin yanı sıra videoları da analiz eder. Bu, sunucu üzerindeki yükü biraz artıracaktır." +_emailUnavailable: + used: "Bu E-Posta adresi zaten kullanılıyor." + format: "Bu E-Posta adresinin biçimi geçersizdir." + disposable: "Tek kullanımlık E-Posta adresleri kullanılamaz." + mx: "Bu E-Posta sunucusu geçersizdir." + smtp: "Bu E-Posta sunucusu yanıt vermiyor." + banned: "Bu E-Posta adresiyle kayıt olamazsınız." +_ffVisibility: + public: "Bu e-posta adresiyle kayıt olamazsınız." + followers: "Sadece takipçiler tarafından görülebilir" + private: "Özel" +_signup: + almostThere: "Neredeyse vardık" + emailAddressInfo: "Lütfen E-Posta adresinizi girin. Bu adres kamuya açık hale getirilmeyecektir." + emailSent: "Onay e-postası E-Posta adresinize ({email}) gönderilmiştir. Hesap oluşturma işlemini tamamlamak için e-postadaki bağlantıya tıklayın." _accountDelete: - started: "Silme işlemi başlatıldı" + accountDelete: "Hesabı sil" + mayTakeTime: "Hesap silme işlemi kaynak yoğun bir işlem olduğundan, oluşturduğunuz içerik miktarına ve yüklediğiniz dosya sayısına bağlı olarak tamamlanması biraz zaman alabilir." + sendEmail: "Hesap silme işlemi tamamlandıktan sonra, bu hesaba kayıtlı E-Posta adresine bir e-posta gönderilecektir." + requestAccountDelete: "Hesap silme talebi" + started: "Silme işlemi başlatıldı." + inProgress: "Silme işlemi şu anda devam ediyor." +_ad: + back: "Geri" + reduceFrequencyOfThisAd: "Bu reklamı daha az göster" + hide: "Gizle" + timezoneinfo: "Haftanın günü, sunucunun saat diliminden belirlenir." + adsSettings: "Reklam ayarları" + notesPerOneAd: "Gerçek zamanlı güncelleme reklam yerleşim aralığı (Reklam başına notlar)" + setZeroToDisable: "Bu değeri 0 olarak ayarlayarak gerçek zamanlı güncelleme reklamlarını devre dışı bırakın." + adsTooClose: "Mevcut reklam aralığı çok düşük olduğu için kullanıcı deneyimini önemli ölçüde kötüleştirebilir." +_forgotPassword: + enterEmail: "Kayıt olurken kullandığınız E-Posta adresini girin. Şifrenizi sıfırlayabileceğiniz bir bağlantı bu adrese gönderilecektir." + ifNoEmail: "Kayıt sırasında E-Posta kullanmadıysanız, lütfen bunun yerine sunucu yöneticisiyle iletişime geçin." + contactAdmin: "This instance does not support using email addresses, please contact the instance administrator to reset your password instead." +_gallery: + my: "Benim Galerim" + liked: "Beğenilen Gönderiler" + like: "Beğen" + unlike: "Benzerlerini kaldır" _email: _follow: - title: "seni takip etti" + title: "Yeni bir takipçin var." + _receiveFollowRequest: + title: "Bir takip isteği aldınız." +_plugin: + install: "Eklentileri yükle takip isteği aldınız" + installWarn: "Güvenilir olmayan eklentileri yüklemeyiniz." + manage: "Eklentileri yönet" + viewSource: "Kaynak görüntüle" + viewLog: "Günlüğü göster" +_preferencesBackups: + list: "Created backups" + saveNew: "Yeni yedeklemeyi kaydet" + loadFile: "Dosyadan yükle" + apply: "Bu cihaza başvur" + save: "Değişiklikleri kaydet" + inputName: "Lütfen bu yedekleme için bir ad girin." + cannotSave: "Kaydetme başarısız oldu" + nameAlreadyExists: "“{name}” adlı bir yedekleme zaten mevcut. Lütfen farklı bir ad girin." + applyConfirm: "Bu cihaza “{name}” yedeklemesini gerçekten uygulamak istiyor musunuz? Bu cihazın mevcut ayarları üzerine yazılacaktır." + saveConfirm: "Yedeklemeyi {name} olarak kaydedin?" + deleteConfirm: "{name} yedeklemesini silmek ister misiniz?" + renameConfirm: "Bu yedeğin adını “{old}” den “{new}” ye değiştirmek ister misiniz?" + noBackups: "Yedekleme mevcut değildir. “Yeni yedekleme oluştur” seçeneğini kullanarak bu sunucudaki istemci ayarlarınızı yedekleyebilirsiniz." + createdAt: "Oluşturulma tarihi: {date} {time}" + updatedAt: "Güncelleme tarihi: {date} {time}" + cannotLoad: "Yükleme başarısız" + invalidFile: "Geçersiz dosya biçimi" +_registry: + scope: "Kapsam" + key: "Anahtar" + keys: "Anahtarlar" + domain: "Alan adı" + createKey: "Anahtar oluştur" +_aboutMisskey: + about: "Misskey, 2014 yılından beri syuilo tarafından geliştirilen açık kaynaklı bir yazılımdır." + contributors: "Başlıca katkıda bulunanlar" + allContributors: "Tüm katkıda bulunanlar" + source: "Kaynak kodu" + original: "Orijinal" + thisIsModifiedVersion: "{name} orijinal Misskey'in değiştirilmiş bir sürümünü kullanır." + translation: "Misskey'i çevir" + donate: "Misskey'e bağış yapın" + morePatrons: "Burada adı geçmeyen diğer birçok yardımseverin desteğine de teşekkür ederiz. Teşekkürler! 🥰" + patrons: "Müşteriler" + projectMembers: "Proje üyeleri" +_displayOfSensitiveMedia: + respect: "Hassas olarak işaretlenmiş medyayı gizle" + ignore: "Hassas olarak işaretlenmiş medya görüntüleme" + force: "Hide all media" +_instanceTicker: + none: "Asla gösterme" + remote: "Uzak kullanıcılar için göster" + always: "Her zaman göster" +_serverDisconnectedBehavior: + reload: "Otomatik olarak yeniden yükle" + dialog: "Otomatik olarak yeniden yükle" + quiet: "Göze batmayan uyarı göster" +_channel: + create: "Kanal oluştur" + edit: "Kanalı düzenle" + setBanner: "Afiş ayarla" + removeBanner: "Afişi kaldır" + featured: "Trend olan" + owned: "Sahip olunan" + following: "Takip edildi" + usersCount: "{n} Katılımcılar" + notesCount: "{n} Notlar" + nameAndDescription: "Adı ve açıklaması" + nameOnly: "Sadece isim" + allowRenoteToExternal: "Kanal dışında yeniden not alma ve alıntı yapmaya izin ver" +_menuDisplay: + sideFull: "Yan" + sideIcon: "Yan (Simgeler)" + top: "En üst" + hide: "Gizle" +_wordMute: + muteWords: "Sessiz kelimeler" + muteWordsDescription: "AND koşulu için boşluklarla, OR koşulu için satır sonlarıyla ayırın." + muteWordsDescription2: "Surround keywords with slashes to use regular expressions." +_instanceMute: + instanceMuteDescription: "Bu, listelenen sunuculardan gelen tüm notları/yeniden notları sessize alır, sessize alınan bir sunucudan bir kullanıcıya yanıt veren kullanıcıların notları da dahil olmak üzere." + instanceMuteDescription2: "Yeni satırlarla ayırın" + title: "Listelenen sunuculardan notları gizler." + heading: "Sessize alınacak sunucuların listesi" _theme: + explore: "Temaları Keşfedin" + install: "Bir tema yükleyin" + manage: "Temaları yönet" + code: "Tema kodu" + copyThemeCode: "Tema kodunu kopyala" + description: "Açıklama" + installed: "{name} kuruldu" + installedThemes: "Yüklü temalar" + builtinThemes: "Yerleşik temalar" + instanceTheme: "Sunucu teması" + alreadyInstalled: "Bu tema zaten yüklenmiştir." + invalid: "Bu temanın biçimi geçersizdir." + make: "Bir tema oluşturun" + base: "Base" + addConstant: "Sabit ekle" + constant: "Sabit" + defaultValue: "Varsayılan değer" color: "Renk" + refProp: "Bir özelliği referans al" + refConst: "Sabiti referans al" + key: "Anahtar" + func: "İşlevler" + funcKind: "İşlev türü" + argument: "Tartışma" + basedProp: "Referans verilen mülk" + alpha: "Opaklık" + darken: "Koyulaştır" + lighten: "Hafiflet" + inputConstantName: "Bu sabit için bir ad girin" + importInfo: "Buraya tema kodunu girerseniz, onu tema düzenleyicisine aktarabilirsiniz." + deleteConstantConfirm: "{const} sabitini gerçekten silmek istiyor musunuz?" keys: - mention: "Bahset" - renote: "vazgeçme" + accent: "Aksan" + bg: "Arka plan" + fg: "Metin" + focus: "Odak" + indicator: "Gösterge" + panel: "Panel" + shadow: "Gölge" + header: "Başlık" + navBg: "Kenar çubuğu arka planı" + navFg: "Kenar çubuğu metni" + navActive: "Kenar çubuğu metni (Etkin)" + navIndicator: "Kenar çubuğu göstergesi" + link: "Link" + hashtag: "Hashtag" + mention: "Bahsetmeler" + mentionMe: "Bahsetmeler (Ben)" + renote: "Renote" + modalBg: "Modal arka plan" + divider: "Bölücü" + scrollbarHandle: "Kaydırma çubuğu" + scrollbarHandleHover: "Kaydırma çubuğu (Fareyi üzerine getir)" + dateLabelFg: "Tarih etiketi metni" + infoBg: "Bilgi arka planı" + infoFg: "Bilgi metni" + infoWarnBg: "Uyarı arka planı" + infoWarnFg: "Uyarı metni" + toastBg: "Bildirim arka planı" + toastFg: "Bildirim metni" + buttonBg: "Düğme arka planı" + buttonHoverBg: "Button background (Hover)" + inputBorder: "Giriş alanı kenarlığı" + badge: "Rozet" + messageBg: "Sohbet arka planı" + fgHighlighted: "Vurgulanan Metin" _sfx: - note: "notlar" - notification: "Bildirim" + note: "Yeni not" + noteMy: "Kendi notu" + notification: "Bildirimler" + reaction: "Reaksiyon seçimi hakkında" + chatMessage: "Sohbet Mesajları" +_soundSettings: + driveFile: "Drive'da bir ses dosyası kullanın." + driveFileWarn: "Drive'dan bir ses dosyası seçin." + driveFileTypeWarn: "Bu dosya desteklenmiyor" + driveFileTypeWarnDescription: "Bir ses dosyası seçin" + driveFileDurationWarn: "Ses kaydı çok uzun." + driveFileDurationWarnDescription: "Uzun sesli mesajlar Misskey'in kullanımını engelleyebilir. Devam etmek istiyor musunuz?" + driveFileError: "Ses yüklenemedi. Lütfen ayarları değiştirin." +_ago: + future: "Gelecekte" + justNow: "Şu anda" + secondsAgo: "{n} saniye önce" + minutesAgo: "{n} dakika önce" + hoursAgo: "{n} saat önce" + daysAgo: "{n} gün önce" + weeksAgo: "{n} hafta önce" + monthsAgo: "{n} ay önce" + yearsAgo: "{n} yıl önce" + invalid: "Yok" +_timeIn: + seconds: "{n} saniye içinde" + minutes: "{n} dakika içinde" + hours: "{n} saat içinde" + days: "{n} gün içinde" + weeks: "{n} hafta içinde" + months: "{n} ay içinde" + years: "{n} yıl içinde" +_time: + second: "Saniye(ler)" + minute: "Dakika(lar)" + hour: "Saat(ler)" + day: "Gün(ler)" _2fa: - renewTOTPCancel: "Hayır, teşekkürler" + alreadyRegistered: "2 faktörlü kimlik doğrulama cihazını zaten kaydettiniz." + registerTOTP: "Kimlik doğrulama uygulamasını kaydet" + step1: "Öncelikle, cihazınıza bir kimlik doğrulama uygulaması (örneğin {a} veya {b}) yükleyin." + step2: "Ardından, bu ekranda görüntülenen QR kodunu tarayın." + step2Uri: "Masaüstü programı kullanıyorsanız aşağıdaki URI'yi girin" + step3Title: "Doğrulama kodunu girin" + step3: "Uygulamanız tarafından sağlanan kimlik doğrulama kodunu (token) girerek kurulumu tamamlayın." + setupCompleted: "Kurulum tamamlandı" + step4: "Bundan sonra, gelecekteki tüm oturum açma girişimlerinde bu tür bir oturum açma jetonu istenecektir." + securityKeyNotSupported: "Tarayıcınız güvenlik anahtarlarını desteklemiyor." + registerTOTPBeforeKey: "Güvenlik veya geçiş anahtarını kaydetmek için bir kimlik doğrulama uygulaması kurun." + securityKeyInfo: "Parmak izi veya PIN kimlik doğrulamasının yanı sıra, hesabınızı daha da güvenli hale getirmek için FIDO2'yi destekleyen donanım güvenlik anahtarları aracılığıyla kimlik doğrulama da ayarlayabilirsiniz." + registerSecurityKey: "Güvenlik veya geçiş anahtarını kaydedin" + securityKeyName: "Bir anahtar adı girin" + tapSecurityKey: "Güvenlik veya geçiş anahtarını kaydetmek için lütfen tarayıcınızı takip edin." + removeKey: "Güvenlik anahtarını kaldır" + removeKeyConfirm: "{name} anahtarını gerçekten silmek istiyor musunuz?" + whyTOTPOnlyRenew: "Güvenlik anahtarı kayıtlı olduğu sürece kimlik doğrulama uygulaması kaldırılamaz." + renewTOTP: "Kimlik doğrulama uygulamasını yeniden yapılandırın" + renewTOTPConfirm: "Bu, önceki uygulamanızdaki doğrulama kodlarının çalışmamasına neden olacaktır." + renewTOTPOk: "Yeniden yapılandır" + renewTOTPCancel: "İptal" + checkBackupCodesBeforeCloseThisWizard: "Bu pencereyi kapatmadan önce, lütfen aşağıdaki yedek kodları not edin." + backupCodes: "Yedek kodlar" + backupCodesDescription: "İki faktörlü kimlik doğrulama uygulamasını kullanamaz hale gelmeniz durumunda, bu kodları kullanarak hesabınıza erişebilirsiniz. Her kod yalnızca bir kez kullanılabilir. Lütfen bu kodları güvenli bir yerde saklayın." + backupCodeUsedWarning: "Yedek kod kullanıldı. Artık kullanamıyorsanız, lütfen iki faktörlü kimlik doğrulamayı mümkün olan en kısa sürede yeniden yapılandırın." + backupCodesExhaustedWarning: "Tüm yedek kodlar kullanıldı. İki faktörlü kimlik doğrulama uygulamanıza erişiminizi kaybederseniz, bu hesaba erişemezsiniz. Lütfen iki faktörlü kimlik doğrulamayı yeniden yapılandırın." + moreDetailedGuideHere: "İşte ayrıntılı kılavuz" _permissions: - "read:blocks": "Engellenen hesapları gör" - "write:blocks": "Engellenen hesap listesini düzenle" + "read:account": "Hesap bilgilerinizi görüntüleyin" + "write:account": "Hesap bilgilerinizi düzenleyin" + "read:blocks": "Engellenen kullanıcıların listesini görüntüleyin" + "write:blocks": "Engellenen kullanıcılar listenizi düzenleyin" + "read:drive": "Drive dosyalarınıza ve klasörlerinize erişin" + "write:drive": "Drive dosyalarınızı ve klasörlerinizi düzenleyin veya silin" + "read:favorites": "Favoriler listenizi görüntüleyin" + "write:favorites": "Favoriler listenizi düzenleyin" + "read:following": "Takip ettiğiniz kişilerle ilgili bilgileri görüntüleyin" + "write:following": "Diğer hesapları takip et veya takipten çıkar" + "read:messaging": "Sohbetlerinizi görüntüleyin" + "write:messaging": "Sohbet mesajlarını oluşturun veya silin" + "read:mutes": "Sessize alınan kullanıcıların listesini görüntüleyin" + "write:mutes": "Sessize alınan kullanıcıların listesini düzenleyin" + "write:notes": "Notlar oluşturun veya silin" + "read:notifications": "Bildirimlerinizi görüntüleyin" + "write:notifications": "Bildirimlerinizi yönetin" + "read:reactions": "Tepkilerinizi görüntüleyin" + "write:reactions": "Tepkilerinizi düzenleyin" + "write:votes": "Ankete oy verin" + "read:pages": "Sayfalarınızı görüntüleyin" + "write:pages": "Sayfalarınızı düzenleyin veya silin" + "read:page-likes": "Beğenilen sayfaların listesini görüntüle" + "write:page-likes": "Beğenilen sayfaların listesini düzenle" + "read:user-groups": "Kullanıcı gruplarınızı görüntüleyin" + "write:user-groups": "Kullanıcı gruplarınızı düzenleyin veya silin" + "read:channels": "Kanallarınızı görüntüleyin" + "write:channels": "Kanallarınızı düzenleyin" + "read:gallery": "Galeriyi görüntüle" + "write:gallery": "Galeri düzenle" + "read:gallery-likes": "Beğendiğiniz galeri gönderilerinin listesini görüntüleyin" + "write:gallery-likes": "Beğendiğiniz galeri gönderilerinin listesini düzenleyin" + "read:flash": "Oynat" + "write:flash": "Oyunları Düzenle" + "read:flash-likes": "Beğenilen Oyunların listesini görüntüle" + "write:flash-likes": "Beğenilen Oyunlar listesini düzenle" + "read:admin:abuse-user-reports": "Kullanıcı raporlarını görüntüle" + "write:admin:delete-account": "Kullanıcı hesabını sil" + "write:admin:delete-all-files-of-a-user": "Bir kullanıcının tüm dosyalarını sil" + "read:admin:index-stats": "Veritabanı dizin istatistiklerini görüntüle" + "read:admin:table-stats": "Veritabanı tablosu istatistiklerini görüntüle" + "read:admin:user-ips": "Kullanıcı IP adreslerini görüntüleyin" + "read:admin:meta": "Sunucu meta verilerini görüntüle" + "write:admin:reset-password": "Kullanıcı şifresini sıfırla" + "write:admin:resolve-abuse-user-report": "Kullanıcı raporunu çözme" + "write:admin:send-email": "E-Posta gönder" + "read:admin:server-info": "Sunucu bilgilerini görüntüle" + "read:admin:show-moderation-log": "Moderasyon günlüğünü görüntüle" + "read:admin:show-user": "Özel kullanıcı bilgilerini görüntüle" + "write:admin:suspend-user": "Kullanıcıyı askıya al" + "write:admin:unset-user-avatar": "Kullanıcı avatarını kaldır" + "write:admin:unset-user-banner": "Kullanıcı afişini kaldır" + "write:admin:unsuspend-user": "Kullanıcı askıya alma işlemini kaldır" + "write:admin:meta": "Sunucu meta verilerini yönetme" + "write:admin:user-note": "Moderasyon notunu yönet" + "write:admin:roles": "Rolleri yönet" + "read:admin:roles": "Rolü görüntüle" + "write:admin:relays": "Röleleri yönetme" + "read:admin:relays": "Röleleri görüntüle" + "write:admin:invite-codes": "Davet kodlarını yönet" + "read:admin:invite-codes": "Davet kodlarını görüntüle" + "write:admin:announcements": "Duyuruları yönet" + "read:admin:announcements": "Duyuruları görüntüle" + "write:admin:avatar-decorations": "Avatar süslemelerini yönetebilir" + "read:admin:avatar-decorations": "Avatar süslemelerini görüntüle" + "write:admin:federation": "Federasyon verilerini yönetme" + "write:admin:account": "Kullanıcı hesabını yönet" + "read:admin:account": "Kullanıcı hesabını görüntüle" + "write:admin:emoji": "Emoji'leri yönet" + "read:admin:emoji": "Emojiyi görüntüle" + "write:admin:queue": "İş kuyruğunu yönet" + "read:admin:queue": "İş kuyruğu bilgilerini görüntüle" + "write:admin:promo": "Promosyon notlarını yönet" + "write:admin:drive": "Kullanıcı sürücüsünü yönet" + "read:admin:drive": "Kullanıcı sürücü bilgilerini görüntüle" + "read:admin:stream": "Yönetici için WebSocket API'sını kullanın" + "write:admin:ad": "Reklamları yönet" + "read:admin:ad": "Reklamları görüntüle" + "write:invite-codes": "Davet kodları oluşturun" + "read:invite-codes": "Davet kodlarını alın" + "write:clip-favorite": "Favorilere eklenen klipleri yönet" + "read:clip-favorite": "Favorilere eklenen klipleri görüntüle" + "read:federation": "Federasyon verilerini alın" + "write:report-abuse": "İhlali bildir" + "write:chat": "Sohbet mesajlarını oluşturun veya silin" + "read:chat": "Sohbeti Gözat" +_auth: + shareAccessTitle: "Uygulama izinlerinin verilmesi" + shareAccess: "“{name}”nin bu hesaba erişmesine izin vermek ister misiniz?" + shareAccessAsk: "Bu uygulamanın hesabınıza erişmesine izin vermek istediğinizden emin misiniz?" + permission: "{name} aşağıdaki izinleri talep etmektedir." + permissionAsk: "Bu uygulama aşağıdaki izinleri talep etmektedir" + pleaseGoBack: "Lütfen uygulamaya geri dönün." + callback: "Uygulamaya geri dönmek" + accepted: "Erişim izni verildi" + denied: "Erişim reddedildi" + scopeUser: "Aşağıdaki kullanıcı olarak çalıştırın" + pleaseLogin: "Uygulamaları yetkilendirmek için lütfen giriş yapın." + byClickingYouWillBeRedirectedToThisUrl: "Erişim izni verildiğinde, otomatik olarak aşağıdaki URL'ye yönlendirileceksiniz." +_antennaSources: + all: "Tüm notlar" + homeTimeline: "Takip edilen kullanıcıların notları" + users: "Belirli kullanıcılardan gelen notlar" + userList: "Belirtilen kullanıcı listesinden notlar" + userBlacklist: "Bir veya daha fazla belirli kullanıcıya ait olanlar hariç tüm notlar" +_weekday: + sunday: "Pazar" + monday: "Pazartesi" + tuesday: "Salı" + wednesday: "Çarşamba" + thursday: "Perşembe" + friday: "Cuma" + saturday: "Cumartesi" _widgets: profile: "Profil" instanceInfo: "Sunucu Bilgisi" - notifications: "Bildirim" - timeline: "Zaman çizelgesi" + memo: "Yapışkan notlar" + notifications: "Bildirimler" + timeline: "Timeline" calendar: "Takvim" + trends: "Trend olan" clock: "Saat" + rss: "RSS okuyucu" + rssTicker: "RSS-Ticker" activity: "Etkinlik" + photos: "Fotoğraflar" + digitalClock: "Dijital saat" + unixClock: "UNIX saati" federation: "Federasyon" - jobQueue: "İşlem sırası" + instanceCloud: "Bulut sunucu" + postForm: "Gönderim formu" + slideshow: "Slayt gösterisi" + button: "Düğme" + onlineUsers: "Çevrimiçi kullanıcılar" + jobQueue: "İş Kuyruğu" + serverMetric: "Sunucu ölçümleri" + aiscript: "AiScript konsolu" + aiscriptApp: "AiScript Uygulaması" + aichan: "Ai" + userList: "Kullanıcı listesi" _userList: - chooseList: "Bir liste seç" + chooseList: "Bir liste seçin" + clicker: "Tıklayıcı" + birthdayFollowings: "Bugünün Doğum Günleri" + chat: "Sohbet" _cw: - show: "Devamını yükle" + hide: "Gizle" + show: "İçeriği göster" + chars: "{count} karakter" + files: "{count} dosya(lar)" _poll: - vote: "Oy kullan" + noOnlyOneChoice: "En az iki seçenek gereklidir." + choiceN: "Seçim {n}" + noMore: "Daha fazla seçenek ekleyemezsiniz." + canMultipleVote: "Birden fazla seçenek seçilmesine izin ver" + expiration: "Anketi sonlandır" + infinite: "Asla" + at: "Şurada bitir..." + after: "Sonrasında bitir..." + deadlineDate: "Bitiş tarihi" + deadlineTime: "Zaman" + duration: "Süre" + votesCount: "{n} oy" + totalVotes: "Toplam {n} oy" + vote: "Oy ver" + showResult: "Sonuçları görüntüle" + voted: "Oylandı" + closed: "Sona erdi" + remainingDays: "{d} gün {h} saat kaldı" + remainingHours: "{h} saat {m} dakika kaldı" + remainingMinutes: "{m} dakika {s} saniye kaldı" + remainingSeconds: "{s} saniye kaldı" _visibility: - publicDescription: "Herkese açık" + public: "Halka açık" + publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır." home: "Ana sayfa" - followers: "takipçi" + homeDescription: "Yalnızca ana zaman çizelgesine gönder" + followers: "Takipçiler" + followersDescription: "Sadece takipçilerinize görünür hale getirin" + specified: "Doğrudan" + specifiedDescription: "Yalnızca belirli kullanıcılar için görünür hale getir" + disableFederation: "Federasyon olmadan" + disableFederationDescription: "Diğer sunuculara aktarma" +_postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "Yüklenmemiş dosyalar var, bunları silip formu kapatmak ister misiniz?" + uploaderTip: "Dosya henüz yüklenmemiştir. Dosya menüsünden dosyayı yeniden adlandırabilir, görüntüleri kırpabilir, filigran ekleyebilir ve dosyayı sıkıştırabilir veya sıkıştırmayı kaldırabilirsiniz. Notu yayınladığınızda dosyalar otomatik olarak yüklenir." + replyPlaceholder: "Bu notu yanıtla..." + quotePlaceholder: "Bu notu alıntı yap..." + channelPlaceholder: "Bir kanala gönder..." + _placeholders: + a: "Ne yapıyorsun?" + b: "Çevrenizde neler oluyor?" + c: "Aklında ne var?" + d: "Ne söylemek istiyorsun?" + e: "Yazmaya başlayın..." + f: "Yazmanızı bekliyoruz..." _profile: - username: "Kullanıcı Adı" + name: "Ad" + username: "Kullanıcı adı" + description: "Biyografi" + youCanIncludeHashtags: "Biyografinize hashtag'ler de ekleyebilirsiniz." + metadata: "Ek Bilgiler" + metadataEdit: "Ek bilgileri düzenle" + metadataDescription: "Bunları kullanarak profilinizde ek bilgi alanları görüntüleyebilirsiniz." + metadataLabel: "Etiket" + metadataContent: "İçerik" + changeAvatar: "Avatarı değiştir" + changeBanner: "Change banner" + verifiedLinkDescription: "Buraya profilinize bağlantı içeren bir URL girerek, alanın yanında bir sahiplik doğrulama simgesi görüntülenebilir." + avatarDecorationMax: "En fazla {max} dekorasyon ekleyebilirsiniz." + followedMessage: "Takip edildiğinizde gönderilen mesaj" + followedMessageDescription: "Aboneleriniz sizi takip ettiklerinde görüntülenmesini istediğiniz kısa bir mesaj ayarlayabilirsiniz." + followedMessageDescriptionForLockedAccount: "Takip isteklerinin onay gerektirdiğini ayarladıysanız, bir takip isteğini kabul ettiğinizde bu mesaj görüntülenir." _exportOrImport: - followingList: "takipçi" - muteList: "Gizle" - blockingList: "engelle" - userLists: "Listeler" + allNotes: "Tüm notlar" + favoritedNotes: "Favori notlar" + clips: "Klip" + followingList: "Takip edilen kullanıcılar" + muteList: "Sessize alınan kullanıcılar" + blockingList: "Engellenen kullanıcılar" + userLists: "Kullanıcı listeleri" + excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut" + excludeInactiveUsers: "Etkin olmayan kullanıcıları hariç tut" + withReplies: "İçe aktarılan kullanıcıların yanıtlarını zaman çizelgesine dahil edin" _charts: federation: "Federasyon" + apRequest: "Talepler" + usersIncDec: "Kullanıcı sayısındaki fark" + usersTotal: "Toplam kullanıcı sayısı" + activeUsers: "Aktif kullanıcılar" + notesIncDec: "Not sayısındaki fark" + localNotesIncDec: "Yerel notaların sayısındaki fark" + remoteNotesIncDec: "Uzak notların sayısındaki fark" + notesTotal: "Toplam not sayısı" + filesIncDec: "Dosya sayısındaki fark" + filesTotal: "Toplam dosya sayısı" + storageUsageIncDec: "Depolama kullanımı farkı" + storageUsageTotal: "Total storage usage" +_instanceCharts: + requests: "Talepler" + users: "Kullanıcı sayısındaki fark" + usersTotal: "Kümülatif kullanıcı sayısı" + notes: "Not sayısındaki fark" + notesTotal: "Kümülatif not sayısı" + ff: "Takip edilen kullanıcı sayısı / takipçi sayısı farkı" + ffTotal: "Takip edilen kullanıcıların / takipçilerin toplam sayısı" + cacheSize: "Önbellek boyutundaki fark" + cacheSizeTotal: "Kümülatif önbellek boyutu" + files: "Dosya sayısındaki fark" + filesTotal: "Toplam dosya sayısı" _timelines: - home: "Ana sayfa" + home: "Ana Sayfa" + local: "Yerel" + social: "Sosyal" global: "Küresel" +_play: + new: "Oyun Oluştur" + edit: "Düzenle Oynat" + created: "Oyun oluşturuldu" + updated: "Düzenlenmiş oynat" + deleted: "Oyun silindi" + pageSetting: "Oyun ayarları" + editThisPage: "Bu Oyunu Düzenle" + viewSource: "Kaynak görüntüle" + my: "Oyunlarım" + liked: "Beğenilen Oyunlar" + featured: "Popüler" + title: "Başlık" + script: "Senaryo" + summary: "Açıklama" + visibilityDescription: "Özel olarak ayarlamak, profilinizde görünmeyeceği anlamına gelir, ancak URL'ye sahip olan herkes yine de erişebilir." _pages: + newPage: "Yeni bir Sayfa oluşturun" + editPage: "Bu sayfayı düzenle" + readPage: "Bu Sayfanın Kaynağını Görüntüleme" + pageSetting: "Sayfa ayarları" + nameAlreadyExists: "Belirtilen Sayfa URL'si zaten mevcut." + invalidNameTitle: "Belirtilen Sayfa URL'si geçersiz" + invalidNameText: "Sayfa başlığının boş olmadığından emin olun." + editThisPage: "Bu sayfayı düzenle" + viewSource: "Kaynak görüntüle" + viewPage: "Sayfalarınızı görüntüleyin" + like: "Beğen" + unlike: "Benzerlerini kaldır" + my: "Benzerlerini kaldır" + liked: "Beğenilen Sayfalar" + featured: "Popüler" + inspector: "Müfettiş" + contents: "İçindekiler" + content: "Sayfa bloğu" + variables: "Değişkenler" + title: "Başlık" + url: "Sayfa URL'si" + summary: "Sayfa özeti" + alignCenter: "Merkez öğeleri" + hideTitleWhenPinned: "Profiline sabitlendiğinde sayfa başlığını gizle" + font: "Yazı tipi" + fontSerif: "Serif" + fontSansSerif: "Sans Serif" + eyeCatchingImageSet: "Küçük resmi ayarla" + eyeCatchingImageRemove: "Küçük resmi sil" + chooseBlock: "Blok ekle" + enterSectionTitle: "Bölüm başlığını girin" + selectType: "Bir tür seçin" + contentBlocks: "İçerik" + inputBlocks: "Giriş" + specialBlocks: "Özel" blocks: + text: "Metin" + textarea: "Metin alanı" + section: "Bölüm" image: "Görseller" + button: "Düğme" + dynamic: "Dinamik Bloklar" + dynamicDescription: "Bu blok kaldırılmıştır. Bundan sonra lütfen {play} kullanın." + note: "Gömülü not" + _note: + id: "Not Kimliği" + idDescription: "Alternatif olarak notun URL'sini buraya yapıştırabilirsiniz." + detailed: "Ayrıntılı görünüm" +_relayStatus: + requesting: "Beklemede" + accepted: "Accepted" + rejected: "Reddedildi" _notification: + fileUploaded: "Dosya başarıyla yüklendi" + youGotMention: "{name} sizden bahsetti." + youGotReply: "{name} size yanıt verdi" + youGotQuote: "{name} sizden alıntı yaptı" + youRenoted: "{name}'den Renote" youWereFollowed: "seni takip etti" + youReceivedFollowRequest: "Bir takip isteği aldınız." + yourFollowRequestAccepted: "Takip isteğiniz kabul edildi." + pollEnded: "Anket sonuçları açıklandı." + newNote: "Yeni not" unreadAntennaNote: "{name} anteni" + roleAssigned: "Verilen rol" + chatRoomInvitationReceived: "Sohbet odasına davet edildiniz." + emptyPushNotificationMessage: "Push bildirimleri güncellendi" + achievementEarned: "Achievement unlocked" + testNotification: "Test bildirimi" + checkNotificationBehavior: "Bildirim görünümünü kontrol edin" + sendTestNotification: "Test bildirimi gönder" + notificationWillBeDisplayedLikeThis: "Bildirimler şöyle görünür" + reactedBySomeUsers: "{n} kullanıcı tepki gösterdi" + likedBySomeUsers: "{n} kullanıcı notunuzu beğendi." + renotedBySomeUsers: "{n} kullanıcıdan gelen hatırlatma" + followedBySomeUsers: "{n} kullanıcı tarafından takip ediliyor" + flushNotification: "Bildirimleri temizle" + exportOfXCompleted: "{x} ihracatı tamamlandı." + login: "Birisi oturum açtı" + createToken: "Bir erişim jetonu oluşturuldu." + createTokenDescription: "Eğer bilmiyorsanız, “{text}” aracılığıyla erişim jetonunu silin." _types: - follow: "takipçi" - mention: "Bahset" - renote: "vazgeçme" - quote: "alıntı" - reaction: "Tepkiler" - receiveFollowRequest: "Takip isteği alındı" - followRequestAccepted: "Takip isteği kabul edildi" - login: "Giriş Yap " + all: "Tümü" + note: "Yeni notlar" + follow: "Yeni takipçiler" + mention: "Bahsetmeler" + reply: "Yanıtlar" + renote: "Renote" + quote: "Alıntılar" + reaction: "Tepki" + pollEnded: "Anketler sona eriyor" + receiveFollowRequest: "Takip istekleri alındı" + followRequestAccepted: "Kabul edilen takip istekleri" + roleAssigned: "Verilen rol" + chatRoomInvitationReceived: "Sohbet odasına davet edildi" + achievementEarned: "Başarı kilidi açıldı" + exportCompleted: "İhracat işlemi tamamlandı." + login: "Giriş Yap" + createToken: "Erişim jetonu oluştur" + test: "Bildirim testi" + app: "Bağlı uygulamalardan gelen bildirimler" _actions: - reply: "yanıt" - renote: "vazgeçme" + followBack: "seni takip ettim" + reply: "Yanıtla" + renote: "Renote" _deck: - configureColumn: "Sütun seçenekleri" + alwaysShowMainColumn: "Ana sütunu her zaman göster" + columnAlign: "Sütunları hizala" + columnGap: "Sütunlar arasındaki kenar boşluğu" + deckMenuPosition: "Sütunlar arasındaki kenar boşluğu" + navbarPosition: "Gezinti çubuğu konumu" + addColumn: "Sütun ekle" + newNoteNotificationSettings: "Notification setting for new notes" + configureColumn: "Sütun ayarları" + swapLeft: "Sol sütunla değiştir" + swapRight: "Sağ sütunla değiştir" + swapUp: "Yukarıdaki sütunla değiştirin" + swapDown: "Aşağıdaki sütunla değiştirin" + stackLeft: "Sol sütunda yığın" + popRight: "Sağdaki pop sütunu" + profile: "Profil" + newProfile: "Yeni profil" + deleteProfile: "Profili sil" + introduction: "Sütunları serbestçe düzenleyerek size en uygun arayüzü oluşturun!" + introduction2: "Ekranın sağındaki + işaretine tıklayarak istediğiniz zaman yeni sütunlar ekleyebilirsiniz." + widgetsIntroduction: "Lütfen sütun menüsünden “Widget'ları düzenle” seçeneğini seçin ve bir widget ekleyin." + useSimpleUiForNonRootPages: "Gezinilen sayfalar için basit kullanıcı arayüzü kullanın" + usedAsMinWidthWhenFlexible: "“Otomatik genişlik ayarı” seçeneği etkinleştirildiğinde, bunun için minimum genişlik kullanılacaktır." + flexible: "Otomatik genişlik ayarı" + enableSyncBetweenDevicesForProfiles: "Cihazlar arasında profil bilgilerinin senkronizasyonunu etkinleştirin" _columns: - notifications: "Bildirim" - tl: "Zaman çizelgesi" - list: "Listeler" + main: "Ana" + widgets: "Widget'lar" + notifications: "Bildirimler" + tl: "Ana Sayfa" + antenna: "Antenler" + list: "Liste" + channel: "Kanal" mentions: "Bahsetmeler" + direct: "Doğrudan notlar" + roleTimeline: "Rol Timeline" + chat: "Sohbet" +_dialog: + charactersExceeded: "Maksimum karakter sınırını aştınız! Şu anda {current} karakterde {max} karakterlik sınırın {current} karakterinde bulunuyorsunuz." + charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." +_disabledTimeline: + title: "Timeline devre dışı bırakıldı" + description: "Mevcut rolleriniz altında bu Timeline'ı kullanamazsınız." +_drivecleaner: + orderBySizeDesc: "Azalan Dosya Boyutları" + orderByCreatedAtAsc: "Yükselen Tarihler" +_webhookSettings: + createWebhook: "Webhook oluştur" + modifyWebhook: "Webhook'u değiştir" + name: "Webhook'u değiştir" + secret: "Gizli" + trigger: "Tetikleyici" + active: "Etkin" + _events: + follow: "Bir kullanıcıyı takip ederken" + followed: "Takip edildiğinde" + note: "Not gönderirken" + reply: "Yanıt alındığında" + renote: "Yeniden not edildiğinde" + reaction: "Tepki aldığınızda" + mention: "Bahsedildiğinde" + _systemEvents: + abuseReport: "Yeni bir rapor alındığında" + abuseReportResolved: "Çözüldüğünde rapor" + userCreated: "Kullanıcı oluşturulduğunda" + inactiveModeratorsWarning: "Moderatörler bir süredir aktif olmadıklarında" + inactiveModeratorsInvitationOnlyChanged: "Bir moderatör bir süre aktif olmadığında ve sunucu davetle erişilebilir hale getirildiğinde" + deleteConfirm: "Webhook'u silmek istediğinizden emin misiniz?" + testRemarks: "Anahtarın sağındaki düğmeyi tıklayarak sahte verilerle bir test Webhook gönderin." +_abuseReport: + _notificationRecipient: + createRecipient: "Raporlar için alıcı ekle" + modifyRecipient: "Raporlar için alıcıyı düzenle" + recipientType: "Bildirim türü" + _recipientType: + mail: "E-Posta" + webhook: "Webhook" + _captions: + mail: "Raporları aldığınızda, E-Postayı moderatörlerin e-posta adreslerine gönderin." + webhook: "Raporları aldığınızda veya çözdüğünüzde Sistem Webhook'una bir bildirim gönderin." + keywords: "Anahtar kelimeler" + notifiedUser: "Bildirilecek kullanıcılar" + notifiedWebhook: "Kullanılacak webhook" + deleteConfirm: "Bildirim alıcısını silmek istediğinizden emin misiniz?" _moderationLogTypes: - suspend: "askıya al" - resetPassword: "Şifre sıfırlama" + createRole: "Rol oluşturuldu" + deleteRole: "Rol silindi" + updateRole: "Rol güncellendi" + assignRole: "Rol atandı" + unassignRole: "Görevinden alınmış" + suspend: "Askıya alınmış" + unsuspend: "Askıya alınmamış" + addCustomEmoji: "Özel emoji eklendi" + updateCustomEmoji: "Özel emoji güncellendi" + deleteCustomEmoji: "Özel emoji silindi" + updateServerSettings: "Sunucu ayarları güncellendi" + updateUserNote: "Moderasyon notu güncellendi" + deleteDriveFile: "Dosya silindi" + deleteNote: "Not silindi" + createGlobalAnnouncement: "Küresel duyuru oluşturuldu" + createUserAnnouncement: "Kullanıcı duyurusu oluşturuldu" + updateGlobalAnnouncement: "Küresel duyuru güncellendi" + updateUserAnnouncement: "Kullanıcı duyurusu güncellendi" + deleteGlobalAnnouncement: "Küresel duyuru silindi" + deleteUserAnnouncement: "Kullanıcı duyurusu silindi" + resetPassword: "Şifreyi sıfırla" + suspendRemoteInstance: "Uzak sunucu askıya alındı" + unsuspendRemoteInstance: "Uzak sunucu askıya alınmadı" + updateRemoteInstanceNote: "Uzak sunucular için güncellenmiş moderasyon notu" + markSensitiveDriveFile: "Hassas olarak işaretlenmiş dosya" + unmarkSensitiveDriveFile: "Dosya hassas olarak işaretlenmemiş" + resolveAbuseReport: "Rapor çözüldü" + forwardAbuseReport: "Rapor iletildi" + updateAbuseReportNote: "Güncellenen raporun moderasyon notu" + createInvitation: "Davet oluşturuldu" + createAd: "Reklam oluşturuldu" + deleteAd: "Reklam silindi" + updateAd: "Reklam güncellendi" + createAvatarDecoration: "Avatar dekorasyonu oluşturuldu" + updateAvatarDecoration: "Avatar dekorasyonu güncellendi" + deleteAvatarDecoration: "Avatar süslemesi silindi" + unsetUserAvatar: "Kullanıcı avatarı ayarlanmamış" + unsetUserBanner: "Kullanıcı başlığı ayarlanmamış" + createSystemWebhook: "Sistem Webhook oluşturuldu" + updateSystemWebhook: "Sistem Webhook güncellendi" + deleteSystemWebhook: "Sistem Webhook silindi" + createAbuseReportNotificationRecipient: "Oluşturulan raporlar için alıcı" + updateAbuseReportNotificationRecipient: "Raporlar için alıcı güncellendi" + deleteAbuseReportNotificationRecipient: "Silinen raporlar için alıcı" + deleteAccount: "Hesap silindi" + deletePage: "Sayfa silindi" + deleteFlash: "Oyun silindi" + deleteGalleryPost: "Galeri gönderisi silindi" + deleteChatRoom: "Deleted Chat Room" + updateProxyAccountDescription: "Proxy hesabının açıklamasını güncelleyin" +_fileViewer: + title: "Dosya ayrıntıları" + type: "Dosya türü" + size: "Dosya boyutu" + url: "URL" + uploadedAt: "Yüklendiği tarih" + attachedNotes: "Ekli notlar" + usage: "Kullanılmış" + thisPageCanBeSeenFromTheAuthor: "Bu sayfa, bu dosyayı yükleyen kullanıcı tarafından görülebilir." +_externalResourceInstaller: + title: "Harici siteden yükle" + checkVendorBeforeInstall: "Yüklemeden önce bu kaynağın dağıtımcısının güvenilir olduğundan emin olun." + _plugin: + title: "Bu eklentiyi yüklemek ister misiniz?" + _theme: + title: "Bu temayı yüklemek ister misiniz?" + _meta: + base: "Temel renk şeması" + _vendorInfo: + title: "Dağıtıcı bilgileri" + endpoint: "Referans uç nokta" + hashVerify: "Hash doğrulama" + _errors: + _invalidParams: + title: "Geçersiz parametreler" + description: "Harici bir siteden veri yüklemek için yeterli bilgi yok. Lütfen girdiğiniz URL'yi kontrol edin." + _resourceTypeNotSupported: + title: "Bu harici kaynak desteklenmemektedir." + description: "Bu harici kaynağın türü desteklenmemektedir. Lütfen site yöneticisiyle iletişime geçin." + _failedToFetch: + title: "Veriler alınamadı" + fetchErrorDescription: "Harici siteyle iletişim sırasında bir hata oluştu. Tekrar denemeniz sorunu çözmezse, lütfen site yöneticisine başvurun." + parseErrorDescription: "Harici siteden yüklenen veriler işlenirken bir hata oluştu. Lütfen site yöneticisiyle iletişime geçin." + _hashUnmatched: + title: "Veri doğrulama başarısız oldu" + description: "Alınan verilerin bütünlüğünü doğrularken bir hata oluştu. Güvenlik önlemi olarak, kurulum devam edemez. Lütfen site yöneticisiyle iletişime geçin." + _pluginParseFailed: + title: "AiScript Hatası" + description: "İstenen veriler başarıyla alındı, ancak AiScript ayrıştırma sırasında bir hata oluştu. Lütfen eklenti yazarına başvurun. Hata ayrıntıları Javascript konsolunda görüntülenebilir." + _pluginInstallFailed: + title: "Eklenti kurulumu başarısız oldu" + description: "Eklenti yükleme sırasında bir sorun oluştu. Lütfen tekrar deneyin. Hata ayrıntıları Javascript konsolunda görüntülenebilir." + _themeParseFailed: + title: "Tema ayrıştırma başarısız oldu" + description: "İstenen veriler başarıyla alındı, ancak tema ayrıştırma sırasında bir hata oluştu. Lütfen tema yazarıyla iletişime geçin. Hata ayrıntıları Javascript konsolunda görüntülenebilir." + _themeInstallFailed: + title: "Tema yüklenemedi" + description: "Tema yükleme sırasında bir sorun oluştu. Lütfen tekrar deneyin. Hata ayrıntıları Javascript konsolunda görüntülenebilir." +_dataSaver: + _media: + title: "Medya yükleniyor" + description: "Görüntülerin/videoların otomatik olarak yüklenmesini engeller. Gizli görüntüler/videolar dokunulduğunda yüklenir." + _avatar: + title: "Avatar resmi" + description: "Avatar görüntüsünün animasyonunu durdurun. Animasyonlu görüntüler normal görüntülere göre dosya boyutu açısından daha büyük olabilir ve bu da veri trafiğinde daha fazla azalmaya yol açabilir." + _urlPreviewThumbnail: + title: "URL önizleme küçük resimlerini gizle" + description: "URL önizleme küçük resimleri artık yüklenmeyecektir." + _disableUrlPreview: + title: "URL önizlemesini devre dışı bırak" + description: "URL önizleme işlevini devre dışı bırakır. Küçük resimler aksine, bu işlev bağlantılı bilgilerin kendisinin yüklenmesini azaltır." + _code: + title: "Kod vurgulama" + description: "MFM vb. programlarda kod vurgulama notasyonları kullanılıyorsa, bunlar dokunulana kadar yüklenmez. Sözdizimi vurgulama, her programlama dili için vurgu tanım dosyalarının indirilmesini gerektirir. Bu nedenle, bu dosyaların otomatik olarak yüklenmesinin devre dışı bırakılması, iletişim verisi miktarını azaltması beklenir." +_hemisphere: + N: "Kuzey Yarımküre" + S: "Güney Yarımküre" + caption: "Bazı istemci ayarlarında mevsimi belirlemek için kullanılır." +_reversi: + reversi: "Tersine çevirme" + gameSettings: "Oyun ayarları" + chooseBoard: "Bir tahta seçin" + blackOrWhite: "Siyah/Beyaz" + blackIs: "{name} siyah oynuyor." + rules: "Kurallar" + thisGameIsStartedSoon: "Oyun kısa süre içinde başlayacak." + waitingForOther: "Rakibin sırasını beklemek" + waitingForMe: "Sıranızı bekliyorsunuz" + waitingBoth: "Hazır olun" + ready: "Hazır" + cancelReady: "Hazır değil" + opponentTurn: "Rakibin sırası" + myTurn: "Sıra sende" + turnOf: "Sıra {name}'de." + pastTurnOf: "{name}'nin sırası" + surrender: "Teslimiyet" + surrendered: "Teslim oldu" + timeout: "Zaman doldu" + drawn: "Çiz" + won: "{name} kazandı" + black: "Siyah" + white: "Beyaz" + total: "Toplam" + turnCount: "{count} döndür" + myGames: "Benim turlarım" + allGames: "Tüm turlar" + ended: "Sona erdi" + playing: "Şu anda oynatılıyor" + isLlotheo: "Taş sayısı daha az olan kazanır (Llotheo)" + loopedMap: "Döngüsel harita" + canPutEverywhere: "Fayanslar her yere yerleştirilebilir." + timeLimitForEachTurn: "Sıra için zaman sınırı" + freeMatch: "Ücretsiz Eşleştirme" + lookingForPlayer: "Rakip aranıyor..." + gameCanceled: "Oyun iptal edildi." + shareToTlTheGameWhenStart: "Oyun başlatıldığında zaman çizelgesinde paylaş" + iStartedAGame: "Oyun başladı! #MisskeyReversi" + opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiştir." + allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)" + disallowIrregularRules: "Düzensiz kurallar yok" + showBoardLabels: "Tahtada satır ve sütun numaralarını göster" + useAvatarAsStone: "Taşları kullanıcı avatarlarına dönüştürün" +_offlineScreen: + title: "Çevrimdışı - sunucuya bağlanılamıyor" + header: "Sunucuya bağlanılamıyor" +_urlPreviewSetting: + title: "URL önizleme ayarları" + enable: "URL önizlemesini etkinleştir" + allowRedirect: "URL önizleme yönlendirmesine izin ver" + allowRedirectDescription: "Bir URL'de yönlendirme ayarlanmışsa, bu özelliği etkinleştirerek yönlendirmeyi takip edebilir ve yönlendirilen içeriğin önizlemesini görüntüleyebilirsiniz. Bu özelliği devre dışı bırakmak sunucu kaynaklarından tasarruf sağlar, ancak yönlendirilen içerik görüntülenmez." + timeout: "Önizleme alırken zaman aşımı (ms)" + timeoutDescription: "Önizlemeyi almak bu değerden daha uzun sürerse, önizleme oluşturulmaz." + maximumContentLength: "Maksimum İçerik Uzunluğu (bayt)" + maximumContentLengthDescription: "Content-Length bu değerden yüksekse, önizleme oluşturulmaz." + requireContentLength: "Yalnızca Content-Length değerini alabiliyorsanız önizlemeyi oluşturun." + requireContentLengthDescription: "Diğer sunucu Content-Length değerini döndürmezse, önizleme oluşturulmaz." + userAgent: "Kullanıcı Aracısı" + userAgentDescription: "Önizlemeleri alırken kullanılacak Kullanıcı Aracısını ayarlar. Boş bırakılırsa, varsayılan Kullanıcı Aracısı kullanılır." + summaryProxy: "Önizlemeler oluşturan proxy uç noktaları" + summaryProxyDescription: "Misskey'in kendisi değil, Summaly Proxy kullanarak önizlemeler oluşturun." + summaryProxyDescription2: "Aşağıdaki parametreler, sorgu dizesi olarak proxy'ye bağlanır. Proxy bunları desteklemiyorsa, değerler yok sayılır." +_mediaControls: + pip: "Resim içinde resim" + playbackRate: "Oynatma Hızı" + loop: "Döngüsel oynatma" +_contextMenu: + title: "Bağlam menüsü" + app: "Uygulama" + appWithShift: "Shift tuşuyla uygulama" + native: "Doğal" +_gridComponent: + _error: + requiredValue: "Bu değer gereklidir." + columnTypeNotSupport: "Düzenli ifade ile doğrulama yalnızca type:text sütunları için desteklenir." + patternNotMatch: "Bu değer {pattern} içindeki desenle eşleşmiyor." + notUnique: "Bu değer benzersiz olmalıdır." +_roleSelectDialog: + notSelected: "Seçilmedi" +_customEmojisManager: + _gridCommon: + copySelectionRows: "Seçili satırları kopyala" + copySelectionRanges: "Seçimi kopyala" + deleteSelectionRows: "Seçili satırları sil" + deleteSelectionRanges: "Seçimdeki satırları sil" + searchSettings: "Arama ayarları" + searchSettingCaption: "Ayrıntılı arama kriterleri belirleyin." + searchLimit: "Sonuç sayısı" + sortOrder: "Sıralama düzeni" + registrationLogs: "Kayıt günlüğü" + registrationLogsCaption: "Emojileri güncellerken veya silerken günlükler görüntülenecektir. Güncelleme veya silme işleminden sonra, yeni bir sayfaya geçildiğinde veya yeniden yüklendiğinde günlükler kaybolacaktır." + alertEmojisRegisterFailedDescription: "Emojileri güncelleyemedi veya silemedi. Ayrıntılar için kayıt günlüğünü kontrol edin." + _logs: + showSuccessLogSwitch: "Başarı günlüğünü göster" + failureLogNothing: "Hata günlüğü yoktur." + logNothing: "Günlük kaydı yok." + _remote: + selectionRowDetail: "Seçilen satırın ayrıntıları" + importSelectionRows: "Seçilen satırları içe aktar" + importSelectionRangesRows: "Seçimdeki satırları içe aktar" + importEmojisButton: "Kontrol edilen Emojileri içe aktar" + confirmImportEmojisTitle: "Emoji'leri İçe Aktar" + confirmImportEmojisDescription: "Uzak sunucudan alınan {count} Emoji(ler)i içe aktarın. Emoji lisansına dikkat edin. Devam etmek istediğinizden emin misiniz?" + _local: + tabTitleList: "Kayıtlı emojiler" + tabTitleRegister: "Emoji kaydı" + _list: + emojisNothing: "Kayıtlı Emoji yok." + markAsDeleteTargetRows: "Silinecek hedef olarak seçilen satırları işaretleyin" + markAsDeleteTargetRanges: "Seçimdeki satırları silinecek hedef olarak işaretleyin" + alertUpdateEmojisNothingDescription: "Güncellenmiş Emoji yok." + alertDeleteEmojisNothingDescription: "Silinecek Emoji yok." + confirmMovePage: "Sayfaları taşımak ister misiniz?" + confirmChangeView: "Görüntüleme şeklini değiştirmek ister misiniz?" + confirmUpdateEmojisDescription: "{count} Emoji'yi güncelle. Devam etmek istediğinden emin misin?" + confirmDeleteEmojisDescription: "İşaretli {count} Emoji(leri) silin. Devam etmek istediğinizden emin misiniz?" + confirmResetDescription: "Şimdiye kadar yapılan tüm değişiklikler geri alınacaktır." + confirmMovePageDesciption: "Bu sayfadaki Emojilerde değişiklikler yapılmıştır.\nSayfayı kaydetmeden terk ederseniz, bu sayfada yapılan tüm değişiklikler silinecektir." + dialogSelectRoleTitle: "Emojilerde rol setine göre arama yapın" + _register: + uploadSettingTitle: "Yükleme ayarları" + uploadSettingDescription: "Bu ekranda, Emoji yüklerken davranışı yapılandırabilirsiniz." + directoryToCategoryLabel: "“Kategori” alanına dizin adını girin." + directoryToCategoryCaption: "Bir dizini sürükleyip bıraktığınızda, “kategori” alanına dizin adını girin." + confirmRegisterEmojisDescription: "Listeden Emojileri yeni özel Emojiler olarak kaydedin. Devam etmek istediğinizden emin misiniz? (Aşırı yüklemeyi önlemek için, tek bir işlemde yalnızca {count} Emoji kaydedilebilir)" + confirmClearEmojisDescription: "Düzenlemeleri silin ve listeden Emojileri temizleyin. Devam etmek istediğinizden emin misiniz?" + confirmUploadEmojisDescription: "Sürücüye sürüklenip bırakılan {count} dosyayı/dosyaları yükleyin. Devam etmek istediğinizden emin misiniz?" +_embedCodeGen: + title: "Gömme kodunu özelleştir" + header: "Başlığı göster" + autoload: "Otomatik olarak daha fazlasını yükle (kullanımdan kaldırıldı)" + maxHeight: "Maksimum yükseklik" + maxHeightDescription: "0 olarak ayarlandığında maksimum yükseklik ayarı devre dışı bırakılır. Widget'ın dikey olarak genişlemeye devam etmesini önlemek için bir değer belirtin." + maxHeightWarn: "Maksimum yükseklik sınırı devre dışıdır (0). Bu istenmeyen bir durumsa, maksimum yüksekliği bir değer olarak ayarlayın." + previewIsNotActual: "Ekran, önizleme ekranında görüntülenen aralığı aştığı için gerçek gömme işleminden farklıdır." + rounded: "Yuvarlak hale getirin" + border: "Dış çerçeveye kenarlık ekle" + applyToPreview: "Önizlemeye başvur" + generateCode: "Gömme kodu oluştur" + codeGenerated: "Kod oluşturuldu" + codeGeneratedDescription: "Oluşturulan kodu web sitenize yapıştırarak içeriği gömün." +_selfXssPrevention: + warning: "UYARI" + title: "“Bu ekrana bir şey yapıştırın” tamamen bir aldatmacadır." + description1: "Buraya bir şey yapıştırırsanız, kötü niyetli bir kullanıcı hesabınızı ele geçirebilir veya kişisel bilgilerinizi çalabilir." + description2: "Yapıştırmaya çalıştığınız şeyi tam olarak anlamıyorsanız, %c hemen çalışmayı bırakın ve bu pencereyi kapatın." + description3: "Daha fazla bilgi için lütfen buraya bakın. {link}" +_followRequest: + recieved: "Talep alındı" + sent: "İstek gönderildi" +_remoteLookupErrors: + _federationNotAllowed: + title: "Bu sunucuyla iletişim kurulamıyor" + description: "Bu sunucu ile iletişim devre dışı bırakılmış olabilir veya bu sunucu engellenmiş olabilir.\nLütfen sunucu yöneticisi ile iletişime geçin." + _uriInvalid: + title: "URI geçersiz" + description: "Girdiğiniz URI ile ilgili bir sorun var. Lütfen URI'da kullanılamayan karakterler girip girmediğinizi kontrol edin." + _requestFailed: + title: "İstek başarısız oldu" + description: "Bu sunucuyla iletişim kurulamadı. Sunucu kapalı olabilir. Ayrıca, geçersiz veya mevcut olmayan bir URI girmediğinizden emin olun." + _responseInvalid: + title: "Yanıt geçersiz" + description: "Bu sunucuyla iletişim kurabildi, ancak elde edilen veriler yanlıştı." + _noSuchObject: + title: "Bulunamadı" + description: "İstenen kaynak bulunamadı, lütfen URI'yi tekrar kontrol edin." +_captcha: + verify: "Lütfen CAPTCHA'yı doğrulayın" + testSiteKeyMessage: "Site ve gizli anahtarlar için test değerlerini girerek önizlemeyi kontrol edebilirsiniz.\nAyrıntılar için lütfen aşağıdaki sayfaya bakın." + _error: + _requestFailed: + title: "CAPTCHA isteği başarısız oldu" + text: "Lütfen bir süre sonra tekrar çalıştırın veya ayarları tekrar kontrol edin." + _verificationFailed: + title: "CAPTCHA doğrulaması başarısız oldu" + text: "Ayarların doğru olup olmadığını lütfen tekrar kontrol edin." + _unknown: + title: "CAPTCHA hatası" + text: "Beklenmedik bir hata oluştu." +_bootErrors: + title: "Yükleme başarısız" + serverError: "Bir süre bekledikten ve yeniden yükledikten sonra sorun devam ederse, lütfen aşağıdaki Hata ID ile sunucu yöneticisine başvurun." + solution: "Aşağıdakiler sorunu çözebilir." + solution1: "Tarayıcınızı ve işletim sisteminizi en son sürüme güncelleyin." + solution2: "Reklam engelleyiciyi devre dışı bırak" + solution3: "Tarayıcı önbelleğini temizle" + solution4: "Tor Tarayıcı için dom.webaudio.enabled değerini true olarak ayarlayın." + otherOption: "Diğer seçenekler" + otherOption1: "İstemci ayarlarını ve önbelleği sil" + otherOption2: "Basit istemciyi başlatın" + otherOption3: "Onarım aracını başlatın" + otherOption4: "Misskey'i güvenli modda başlatın" _search: searchScopeAll: "Tümü" + searchScopeLocal: "Yerel" + searchScopeServer: "Spesifik sunucu" + searchScopeUser: "Spesifik kullanıcı" + pleaseEnterServerHost: "Sunucu ana bilgisayarını girin" + pleaseSelectUser: "Kullanıcı seç" + serverHostPlaceholder: "Örnek: misskey.example.com" +_serverSetupWizard: + installCompleted: "Misskey kurulumu tamamlandı!" + firstCreateAccount: "Başlamak için bir yönetici hesabı oluşturun." + accountCreated: "Yönetici hesabı oluşturuldu!" + serverSetting: "Sunucu Ayarları" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Bu sihirbaz, sunucu ayarlarını yapılandırmayı kolaylaştırır." + settingsYouMakeHereCanBeChangedLater: "Bu sihirbaz aracılığıyla değiştirilen ayarlar daha sonra yeniden düzenlenebilir." + howWillYouUseMisskey: "Misskey'i nasıl kullanacaksınız?" + _use: + single: "Tek Kullanıcı Sunucusu" + single_description: "Kendi sunucunuz olarak tek başına kullanın." + single_youCanCreateMultipleAccounts: "Tek kullanıcı sunucusu olarak çalıştırıldığında bile, gerektiğinde birden fazla hesap oluşturulabilir." + group: "Grup sunucusu" + group_description: "Diğer güvenilir kullanıcıları birden fazla kullanıcıyla birlikte kullanmaya davet edin." + open: "Genel sunucu" + open_description: "Herkesin kayıt olmasına izin verin." + openServerAdvice: "Çok sayıda bilinmeyen kullanıcıyı kabul etmek risklidir. Herhangi bir sorunu çözmek için güvenilir bir moderasyon sistemi kullanmanızı öneririz." + openServerAntiSpamAdvice: "Sunucunuzun spam için bir basamak haline gelmesini önlemek için, reCAPTCHA gibi anti-bot işlevlerini etkinleştirerek güvenliğe de özen göstermelisiniz." + howManyUsersDoYouExpect: "Kaç kullanıcı bekliyorsunuz?" + _scale: + small: "100'den az (küçük ölçekli)" + medium: "100'den fazla ve 1000'den az kullanıcı (orta büyüklükte)" + large: "1000'den fazla (Büyük ölçekli)" + largeScaleServerAdvice: "Büyük sunucular, yük dengeleme ve veritabanı çoğaltma gibi gelişmiş altyapı bilgisi gerektirebilir." + doYouConnectToFediverse: "Fediverse'e bağlanmak ister misiniz?" + doYouConnectToFediverse_description1: "Dağıtılmış sunucular ağına (Fediverse) bağlandığında, içerik diğer sunucularla paylaşılabilir." + doYouConnectToFediverse_description2: "Fediverse ile bağlantı kurmak “federasyon” olarak da adlandırılır." + youCanConfigureMoreFederationSettingsLater: "Birleştirilmiş sunucuları belirtme gibi gelişmiş ayarlar daha sonra yapılandırılabilir." + remoteContentsCleaning: "Alınan içeriklerin otomatik olarak temizlenmesi" + remoteContentsCleaning_description: "Federasyon, sürekli içerik akışına neden olabilir. Otomatik temizleme özelliğini etkinleştirmek, depolama alanından tasarruf etmek için sunucudan eski ve referanslanmamış içeriği kaldıracaktır." + adminInfo: "Yönetici bilgileri" + adminInfo_description: "Sorguları almak için kullanılan yönetici bilgilerini ayarlar." + adminInfo_mustBeFilled: "Genel sunucu veya federasyon açıksa girilmelidir." + followingSettingsAreRecommended: "Aşağıdaki ayarlar önerilir" + applyTheseSettings: "Bu ayarları uygulayın" + skipSettings: "Ayarları atla" + settingsCompleted: "Kurulum tamamlandı!" + settingsCompleted_description: "Zaman ayırdığınız için teşekkür ederiz. Artık her şey hazır olduğuna göre, sunucuyu hemen kullanmaya başlayabilirsiniz." + settingsCompleted_description2: "Sunucu ayarları “Kontrol Paneli”nden değiştirilebilir." + donationRequest: "Bağış Talebi" + _donationRequest: + text1: "Misskey, gönüllüler tarafından geliştirilen ücretsiz bir yazılımdır." + text2: "Bu yazılımı gelecekte de geliştirmeye devam edebilmemiz için desteğinizi rica ederiz." + text3: "Destekçilere özel avantajlar da var!" +_uploader: + editImage: "Resmi Düzenle" + compressedToX: "{x} boyutuna sıkıştırıldı" + savedXPercent: "{x}% tasarruf" + abortConfirm: "Bazı dosyalar yüklenmedi, iptal etmek ister misiniz?" + doneConfirm: "Bazı dosyalar yüklenmedi, yine de devam etmek istiyor musunuz?" + maxFileSizeIsX: "Yükleyebileceğiniz maksimum dosya boyutu {x}" + allowedTypes: "Yüklenebilir dosya türleri" + tip: "Dosya henüz yüklenmediğinden, bu iletişim kutusu yüklemeden önce dosyayı onaylamanıza, yeniden adlandırmanıza, sıkıştırmanıza ve kırpmanıza olanak tanır. Hazır olduğunuzda, “Yükle” düğmesine basarak yüklemeyi başlatabilirsiniz." +_clientPerformanceIssueTip: + title: "Performans ipuçları" + makeSureDisabledAdBlocker: "Reklam engelleyicinizi devre dışı bırakın" + makeSureDisabledAdBlocker_description: "Reklam engelleyiciler performansı etkileyebilir, lütfen sisteminizde veya tarayıcınızın özelliklerinde/uzantılarında reklam engelleyicilerin etkinleştirilmediğinden emin olun." + makeSureDisabledCustomCss: "Özel CSS'yi devre dışı bırak" + makeSureDisabledCustomCss_description: "Stil geçersiz kılma, performansı etkileyebilir. Stil geçersiz kılan özel CSS veya uzantıların etkinleştirilmediğinden emin olun." + makeSureDisabledAddons: "Uzantıları devre dışı bırak" + makeSureDisabledAddons_description: "Bazı uzantılar istemci davranışını engelleyebilir ve performansı etkileyebilir. Lütfen tarayıcı uzantılarınızı devre dışı bırakın ve durumun düzelip düzelmediğini kontrol edin." +_clip: + tip: "Clip, notlarınızı düzenlemenizi sağlayan bir özelliktir." +_userLists: + tip: "Listeler, oluşturulurken belirttiğiniz herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir zaman çizelgesi olarak görüntülenebilir." +watermark: "Filigran" +defaultPreset: "Varsayılan Ön Ayar" +_watermarkEditor: + tip: "Kredi bilgileri gibi bir filigran görüntüye eklenebilir." + quitWithoutSaveConfirm: "Kaydedilmemiş değişiklikleri silmek ister misiniz?" + driveFileTypeWarn: "Bu dosya desteklenmiyor" + driveFileTypeWarnDescription: "Bir görüntü dosyası seçin" + title: "Filigranı Düzenle" + cover: "Her şeyi örtün" + repeat: "her yere yayılmış" + opacity: "Opaklık" + scale: "Boyut" + text: "Metin" + position: "Pozisyon" + type: "Tür" + image: "Görseller" + advanced: "Gelişmiş" + stripe: "Çizgiler" + stripeWidth: "Çizgi genişliği" + stripeFrequency: "Satır sayısı" + angle: "Açı" + polkadot: "Nokta deseni" + checker: "Kontrolcü" + polkadotMainDotOpacity: "Ana noktanın opaklığı" + polkadotMainDotRadius: "Ana noktanın boyutu" + polkadotSubDotOpacity: "İkincil noktanın opaklığı" + polkadotSubDotRadius: "İkincil noktanın boyutu" + polkadotSubDotDivisions: "Alt nokta sayısı." +_imageEffector: + title: "Effektler" + addEffect: "Efektler Ekle" + discardChangesConfirm: "Gerçekten çıkmak istiyor musunuz? Kaydedilmemiş değişiklikleriniz var." + _fxs: + chromaticAberration: "Renk Sapması" + glitch: "Bozulma" + mirror: "Ayna" + invert: "Renkleri Ters Çevir" + grayscale: "Gri tonlama" + colorAdjust: "Renk Düzeltme" + colorClamp: "Renk Sıkıştırma" + colorClampAdvanced: "Renk Sıkıştırma (Gelişmiş)" + distort: "Bozulma" + threshold: "İkilileştir" + zoomLines: "Doymuş hatlar" + stripe: "Çizgiler" + polkadot: "Nokta deseni" + checker: "Denetleyici" + blockNoise: "Gürültüyü Engelle" + tearing: "Yırtılma" +drafts: "Taslaklar" +_drafts: + select: "Taslak Seç" + cannotCreateDraftAnymore: "Oluşturulabilecek taslak sayısı aşılmıştır." + cannotCreateDraft: "Bu içerikle taslak oluşturamazsınız." + delete: "Taslak Sil" + deleteAreYouSure: "Taslağı silmek ister misiniz?" + noDrafts: "Taslak yok" + replyTo: "{user} notunu yanıtla" + quoteOf: "{user} notuna alıntı" + postTo: "{channel}'a gönder" + saveToDraft: "Taslak olarak kaydet" + restoreFromDraft: "Taslaktan geri yükle" + restore: "Geri yükle" + listDrafts: "Taslaklar Listesi" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 8ddfd8f5a1..26843c6917 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -8,6 +8,9 @@ search: "Пошук" notifications: "Сповіщення" username: "Ім'я користувача" password: "Пароль" +initialPasswordForSetup: "Початковий пароль для налаштування" +initialPasswordIsIncorrect: "Початковий пароль для налаштування неправильний" +initialPasswordForSetupDescription: "Використайте пароль, вказаний у конфігураційному файлі, якщо ви встановлювали Misskey власноруч.\nЯкщо використовуєте сервіси хостингу Misskey, використайте наданий пароль.\nЯкщо ви не маєте паролю, лишіть порожнім щоб продовжити. " forgotPassword: "Я забув пароль" fetchingAsApObject: "Отримуємо з федіверсу..." ok: "OK" @@ -45,6 +48,7 @@ pin: "Закріпити" unpin: "Відкріпити" copyContent: "Скопіювати контент" copyLink: "Скопіювати посилання" +copyRemoteLink: "Копіювати віддалене посилання" delete: "Видалити" deleteAndEdit: "Видалити й редагувати" deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї." @@ -57,6 +61,7 @@ copyUserId: "Копіювати ID користувача" copyNoteId: "блокнот ID користувача" copyFileId: "Скопіювати ідентифікатор файлу." searchUser: "Пошук користувачів" +searchThisUsersNotes: "Пошук нотаток користувача" reply: "Відповісти" loadMore: "Показати більше" showMore: "Показати більше" @@ -105,9 +110,11 @@ enterEmoji: "Введіть емодзі" renote: "Поширити" unrenote: "Відміна поширення" renoted: "Поширити запис." +renotedToX: "Поширено до {name}" cantRenote: "Неможливо поширити." cantReRenote: "Поширення не можливо поширити." quote: "Цитата" +inChannelRenote: "Поширено у канал" pinnedNote: "Закріплений запис" pinned: "Закріпити" you: "Ви" @@ -116,6 +123,7 @@ sensitive: "NSFW" add: "Додати" reaction: "Реакції" reactions: "Реакції" +emojiPicker: "Вибір реакції" reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати." rememberNoteVisibility: "Пам’ятати параметри видимісті" attachCancel: "Видалити вкладення" @@ -289,7 +297,9 @@ folderName: "Ім'я теки" createFolder: "Створити теку" renameFolder: "Перейменувати теку" deleteFolder: "Видалити теку" +folder: "Тека" addFile: "Додати файл" +showFile: "Показати файл" emptyDrive: "Диск порожній" emptyFolder: "Тека порожня" unableToDelete: "Видалення неможливе" @@ -302,6 +312,7 @@ copyUrl: "Копіювати URL" rename: "Перейменувати" avatar: "Аватар" banner: "Банер" +displayOfSensitiveMedia: "Показ чутливого медіа" whenServerDisconnected: "Коли зв’язок із сервером втрачено" disconnectedFromServer: "Зв’язок із сервером було перервано" reload: "Оновити" @@ -348,8 +359,11 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Увімкнути hCaptcha" hcaptchaSiteKey: "Ключ сайту" hcaptchaSecretKey: "Секретний ключ" +mcaptcha: "MCaptcha" +enableMcaptcha: "Увімкнути MCaptcha" mcaptchaSiteKey: "Ключ сайту" mcaptchaSecretKey: "Секретний ключ" +mcaptchaInstanceUrl: "Посилання на сервер MCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Увімкнути reCAPTCHA" recaptchaSiteKey: "Ключ сайту" @@ -905,6 +919,8 @@ flip: "Перевернути" lastNDays: "Останні {n} днів" postForm: "Створення нотатки" information: "Інформація" +inMinutes: "х" +inDays: "д" _chat: invitations: "Запросити" noHistory: "Історія порожня" @@ -1302,7 +1318,6 @@ _theme: buttonBg: "Фон кнопки" buttonHoverBg: "Фон кнопки (при наведенні)" inputBorder: "Край поля вводу" - driveFolderBg: "Фон папки на диску" badge: "Значок" messageBg: "Фон переписки" fgHighlighted: "Виділений текст" @@ -1626,3 +1641,10 @@ _remoteLookupErrors: _search: searchScopeAll: "Всі" searchScopeLocal: "Локальна" +_watermarkEditor: + opacity: "Непрозорість" + scale: "Розмір" + text: "Текст" + type: "Тип" + image: "Зображення" + advanced: "Розширені" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 612df9e43c..bfa91cb0db 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -903,7 +903,7 @@ _theme: header: "Sarlavha" navBg: "Yon panel foni" navFg: "Yon panel matni" - mention: "Murojat" + mention: "Eslatib o'tish" renote: "Qayta qayd etish" divider: "Ajratrmoq" fgHighlighted: "Belgilangan matn" @@ -1045,7 +1045,7 @@ _notification: _types: all: "Barchasi" follow: "Obuna bo‘lish" - mention: "Murojat" + mention: "Eslatib o'tish" renote: "Qayta qayd etish" quote: "Iqtibos keltirish" reaction: "Reaktsiyalar" @@ -1097,3 +1097,8 @@ _remoteLookupErrors: _search: searchScopeAll: "Barcha" searchScopeLocal: "Mahalliy" +_watermarkEditor: + text: "Matn" + type: "turi" + image: "Rasmlar" + advanced: "Murakkab" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 1fe9347711..049e96b044 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -102,7 +102,7 @@ pageLoadErrorDescription: "Có thể là do bộ nhớ đệm của trình duy serverIsDead: "Máy chủ không phản hồi. Vui lòng thử lại sau giây lát." youShouldUpgradeClient: "Để xem trang này, hãy làm tươi để cập nhật ứng dụng." enterListName: "Đặt tên cho danh sách" -privacy: "Bảo mật" +privacy: "Riêng tư" makeFollowManuallyApprove: "Yêu cầu theo dõi cần được duyệt" defaultNoteVisibility: "Kiểu tút mặc định" follow: "Theo dõi" @@ -220,6 +220,7 @@ silenceThisInstance: "Máy chủ im lặng" mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này" operations: "Vận hành" software: "Phần mềm" +softwareName: "Tên phần mềm" version: "Phiên bản" metadata: "Metadata" withNFiles: "{n} tập tin" @@ -297,6 +298,7 @@ uploadFromUrl: "Tải lên bằng một URL" uploadFromUrlDescription: "URL của tập tin bạn muốn tải lên" uploadFromUrlRequested: "Đã yêu cầu tải lên" uploadFromUrlMayTakeTime: "Sẽ mất một khoảng thời gian để tải lên xong." +uploadNFiles: "Tải lên {n} tập tin" explore: "Khám phá" messageRead: "Đã đọc" noMoreHistory: "Không còn gì để đọc" @@ -576,6 +578,7 @@ withRepliesByDefaultForNewlyFollowed: "Mặc định hiển thị trả lời t newNoteRecived: "Đã nhận tút mới" sounds: "Âm thanh" sound: "Âm thanh" +notificationSoundSettings: "Cài đặt âm thanh thông báo" listen: "Nghe" none: "Không" showInPage: "Hiện trong trang" @@ -1174,9 +1177,12 @@ mutualFollow: "Theo dõi lẫn nhau" followingOrFollower: "Đang theo dõi hoặc người theo dõi" externalServices: "Các dịch vụ bên ngoài" sourceCode: "Mã nguồn" +sourceCodeIsNotYetProvided: "Mã nguồn hiện chưa có sẵn, vui lòng liên hệ với quản trị viên để khắc phục sự cố này." repositoryUrlDescription: "Nếu bạn có kho lưu trữ mã nguồn có thể truy cập công khai, hãy nhập URL. Nếu bạn đang sử dụng Misskey theo mặc định (không thực hiện bất kỳ thay đổi nào đối với mã nguồn), hãy nhập https://github.com/misskey-dev/misskey." feedback: "Phản hồi" feedbackUrl: "URL phản hồi" +impressum: "Thông tin nhà điều hành" +impressumUrl: "URL thông tin nhà điều hành" privacyPolicy: "Chính sách bảo mật" privacyPolicyUrl: "URL Chính sách bảo mật" tosAndPrivacyPolicy: "Điều khoản sử dụng và Chính sách bảo mật" @@ -1190,6 +1196,7 @@ showAvatarDecorations: "Hiển thị trang trí ảnh đại diện" releaseToRefresh: "Thả để làm mới" refreshing: "Đang làm mới" pullDownToRefresh: "Kéo xuống để làm mới" +signupPendingError: "Đã xảy ra sự cố khi xác minh địa chỉ email của bạn. Liên kết có thể đã hết hạn." cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải có chú thích." decorate: "Trang trí" lastNDays: "{n} ngày trước" @@ -1200,17 +1207,30 @@ passkeyVerificationFailed: "Xác minh mật khẩu không thành công." messageToFollower: "Tin nhắn cho người theo dõi" yourNameContainsProhibitedWords: "Tên bạn đang cố gắng đổi có chứa chuỗi ký tự bị cấm." yourNameContainsProhibitedWordsDescription: "Tên có chứa chuỗi ký tự bị cấm. Nếu bạn muốn sử dụng tên này, hãy liên hệ với quản trị viên máy chủ của bạn." +pleaseSelectAccount: "Chọn tài khoản của bạn" federationDisabled: "Liên kết bị vô hiệu hóa trên máy chủ này. Bạn không thể tương tác với người dùng trên các máy chủ khác." reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?" +preferences: "Thiết lập môi trường" +accessibility: "Khả năng tiếp cận" +preferencesProfile: "Hồ sơ sở thích" +preferenceSyncConflictTitle: "Cài đặt tồn tại trên máy chủ" +preferenceSyncConflictText: "Các thiết lập đồng bộ hóa được bật sẽ lưu các giá trị của chúng vào máy chủ. Tuy nhiên, có những giá trị hiện có trên máy chủ. Bạn muốn ghi đè lên bộ giá trị nào?" paste: "dán" postForm: "Mẫu đăng" information: "Giới thiệu" +chat: "Trò chuyện" +migrateOldSettings: "Di chuyển cài đặt cũ" +migrateOldSettings_description: "Thông thường, quá trình này diễn ra tự động, nhưng nếu vì lý do nào đó mà quá trình di chuyển không thành công, bạn có thể kích hoạt thủ công quy trình di chuyển, quá trình này sẽ ghi đè lên thông tin cấu hình hiện tại của bạn." +inMinutes: "phút" +inDays: "ngày" _chat: invitations: "Mời" noHistory: "Không có dữ liệu" members: "Thành viên" home: "Trang chính" send: "Gửi" +_settings: + preferencesBanner: "Bạn có thể cấu hình hành vi chung của máy khách theo sở thích của mình." _accountSettings: requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung" requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin." @@ -1667,7 +1687,6 @@ _theme: buttonBg: "Nền nút" buttonHoverBg: "Nền nút (Chạm)" inputBorder: "Đường viền khung soạn thảo" - driveFolderBg: "Nền thư mục Ổ đĩa" badge: "Huy hiệu" messageBg: "Nền chat" fgHighlighted: "Chữ nổi bật" @@ -1798,6 +1817,7 @@ _widgets: _userList: chooseList: "Chọn danh sách" clicker: "clicker" + chat: "Trò chuyện" _cw: hide: "Ẩn" show: "Tải thêm" @@ -2023,6 +2043,7 @@ _deck: channel: "Kênh" mentions: "Lượt nhắc" direct: "Nhắn riêng" + chat: "Trò chuyện" _dialog: charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}" charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}" @@ -2061,3 +2082,12 @@ _search: searchScopeAll: "Tất cả" searchScopeLocal: "Máy chủ này" searchScopeUser: "Người dùng chỉ định" +_watermarkEditor: + opacity: "Độ trong suốt" + scale: "Kích thước" + text: "Văn bản" + position: "Vị trí" + type: "Loại" + image: "Hình ảnh" + advanced: "Nâng cao" + angle: "Góc" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index ed9a4a3e66..c6a7e15bf5 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -239,9 +239,9 @@ clearCachedFilesConfirm: "确定要清除所有缓存的远程文件吗?" blockedInstances: "被屏蔽的服务器" blockedInstancesDescription: "设定要屏蔽的服务器,以换行分隔。被屏蔽的服务器将无法与本服务器进行交换通讯。子域名也同样会被屏蔽。" silencedInstances: "被静音的服务器" -silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。" +silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户都被视为「静音」状态,且关注操作均需要被批准。被阻止的实例不受影响。" mediaSilencedInstances: "已隐藏媒体文件的服务器" -mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" +mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" federationAllowedHosts: "允许联合的服务器" federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" muteAndBlock: "隐藏和屏蔽" @@ -298,6 +298,7 @@ uploadFromUrl: "从网址上传" uploadFromUrlDescription: "输入文件的 URL" uploadFromUrlRequested: "请求上传" uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。" +uploadNFiles: "上传 {n} 个文件" explore: "发现" messageRead: "已读" noMoreHistory: "没有更多的历史记录" @@ -326,6 +327,7 @@ dark: "深色" lightThemes: "浅色主题" darkThemes: "深色主题" syncDeviceDarkMode: "将深色模式与设备设置同步" +switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」已开启。要关闭同步并手动切换模式吗?" drive: "网盘" fileName: "文件名称" selectFile: "选择文件" @@ -575,8 +577,10 @@ showFixedPostForm: "在时间线顶部显示发帖框" showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)" withRepliesByDefaultForNewlyFollowed: "在时间线中默认包含新关注用户的回复" newNoteRecived: "有新的帖子" +newNote: "新帖子" sounds: "提示音" sound: "提示音" +notificationSoundSettings: "设置通知声音" listen: "试听" none: "无" showInPage: "在页面中显示" @@ -791,6 +795,7 @@ wide: "宽" narrow: "窄" reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?" needReloadToApply: "重新载入后应用才会生效。" +needToRestartServerToApply: "需要重启服务才能应用更改。" showTitlebar: "显示标题栏" clearCache: "清除缓存" onlineUsersCount: "{n} 人在线" @@ -997,6 +1002,7 @@ failedToUpload: "上传失败" cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法上传。" cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。" cannotUploadBecauseExceedsFileSizeLimit: "无法上传文件,超过文件大小限制。" +cannotUploadBecauseUnallowedFileType: "因文件类型被禁止而无法上传。" beta: "测试" enableAutoSensitive: "自动 NSFW 识别" enableAutoSensitiveDescription: "使用机器学习在可用时自动使用 NSFW 标记来标记媒体。即使您关闭此功能,根据服务器的不同,它仍然可能会自动设置。" @@ -1137,7 +1143,7 @@ channelArchiveConfirmTitle: "要将 {name} 归档吗?" channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。" thisChannelArchived: "该频道已被归档。" displayOfNote: "显示帖子" -initialAccountSetting: "初始设置" +initialAccountSetting: "初始设定" youFollowing: "正在关注" preventAiLearning: "拒绝接受生成式 AI 的学习" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" @@ -1307,11 +1313,12 @@ availableRoles: "可用角色" acknowledgeNotesAndEnable: "理解注意事项后再开启。" federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。" federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。" +draft: "草稿" confirmOnReact: "发送回应前需要确认" reactAreYouSure: "要用「{emoji}」进行回应吗?" markAsSensitiveConfirm: "要将此媒体标记为敏感吗?" unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?" -preferences: "设置" +preferences: "偏好设置" accessibility: "辅助功能" preferencesProfile: "设置的配置" copyPreferenceId: "复制设置 ID" @@ -1324,6 +1331,7 @@ restore: "恢复" syncBetweenDevices: "设备间同步" preferenceSyncConflictTitle: "服务器上已存在设定值" preferenceSyncConflictText: "服务器上已有此设置的设定值。要覆盖哪个设定值?" +preferenceSyncConflictChoiceMerge: "合并" preferenceSyncConflictChoiceServer: "服务器上的设定值" preferenceSyncConflictChoiceDevice: "设备上的设定值" preferenceSyncConflictChoiceCancel: "取消同步" @@ -1346,6 +1354,29 @@ goToDeck: "返回至 Deck" federationJobs: "联合作业" driveAboutTip: "网盘可以显示以前上传的文件。
\n也可以在发布帖子时重复使用文件,或在发布帖子前预先上传文件。
\n删除文件时,其将从至今为止所有用到该文件的地方(如帖子、页面、头像、横幅)消失。
\n也可以新建文件夹来整理文件。" scrollToClose: "滑动并关闭" +advice: "建议" +realtimeMode: "实时模式" +turnItOn: "开启" +turnItOff: "关闭" +emojiMute: "隐藏表情符号" +emojiUnmute: "解除隐藏表情符号" +muteX: "隐藏{x}" +unmuteX: "解除隐藏{x}" +abort: "中止" +tip: "提示和技巧" +redisplayAllTips: "重新显示所有的提示和技巧" +hideAllTips: "隐藏所有的提示和技巧" +defaultImageCompressionLevel: "默认图像压缩等级" +defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。
较高的等级可以减少文件大小,但相对应的画质将会降低。" +inMinutes: "分" +inDays: "日" +safeModeEnabled: "已启用安全模式" +pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。" +customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。" +themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。" +_order: + newest: "从新到旧" + oldest: "从旧到新" _chat: noMessagesYet: "还没有消息" newMessage: "新消息" @@ -1379,6 +1410,8 @@ _chat: chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。" cannotChatWithTheUser: "无法与此用户聊天" cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。" + youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。" + doYouAcceptInvitation: "要接受邀请吗?" chatWithThisUser: "聊天" thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。" thisUserAllowsChatOnlyFromFollowing: "此用户仅接受关注的人发起的聊天。" @@ -1418,11 +1451,21 @@ _settings: makeEveryTextElementsSelectable: "使所有的文字均可选择" makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。" useStickyIcons: "使图标跟随滚动" + enableHighQualityImagePlaceholders: "显示高质量图像的占位符" + uiAnimations: "UI 动画" showNavbarSubButtons: "在导航栏中显示副按钮" ifOn: "启用时" ifOff: "关闭时" + enableSyncThemesBetweenDevices: "在设备间同步已安装的主题" enablePullToRefresh: "开启下拉刷新" enablePullToRefresh_description: "使用鼠标时按下滚轮来拖动" + realtimeMode_description: "与服务器建立连接并实时更新内容。将会增加流量和电池消耗。" + contentsUpdateFrequency: "内容获取频率" + contentsUpdateFrequency_description: "设置越高,内容更新越实时,但性能会降低,并且会消耗更多的流量和电池。" + contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。" + showUrlPreview: "显示 URL 预览" + showAvailableReactionsFirstInNote: "在顶部显示可用的回应" + showPageTabBarBottom: "在下方显示页面标签栏" _chat: showSenderName: "显示发送者的名字" sendOnEnter: "回车键发送" @@ -1469,7 +1512,7 @@ _delivery: manuallySuspended: "手动停止中" goneSuspended: "因服务器被删除而停止" autoSuspendedForNotResponding: "因服务器无应答而停止" - softwareSuspended: "因有不可用的软件而停止" + softwareSuspended: "因有停止投递的软件而停止" _bubbleGame: howToPlay: "游戏说明" hold: "抓住" @@ -1494,13 +1537,13 @@ _announcement: tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验下降。请考虑归档已完成的公告。" readConfirmTitle: "标记为已读?" readConfirmText: "阅读“{title}”的内容并将其标记为已读。" - shouldNotBeUsedToPresentPermanentInfo: "我们建议使用公告来发布临时性的流动信息而不是固定的常规信息,因为这可能损害用户体验,尤其是对于新用户而言。" + shouldNotBeUsedToPresentPermanentInfo: "因可能损坏新用户的 UX 体验,建议将通知用于发布具有时效性的信息,而不是用于长期展示的信息。" dialogAnnouncementUxWarn: "同时存在 2 个或以上的对话框公告极有可能对用户体验产生负面的影响,建议谨慎使用。" silence: "不发送通知" silenceDescription: "开启后,此条公告将不会发送通知,也不强制用户阅读。" _initialAccountSetting: accountCreated: "账户创建完成了!" - letsStartAccountSetup: "来进行帐户的初始设置吧。" + letsStartAccountSetup: "马上来进行账户的初始设定吧。" letsFillYourProfile: "首先,来设定你的个人档案吧!" profileSetting: "个人资料设置" privacySetting: "隐私设置" @@ -1512,7 +1555,7 @@ _initialAccountSetting: haveFun: "希望 {name} 在这里玩得开心!" youCanContinueTutorial: "您可以继续了解 {name}(Misskey) 的使用教程,也可以在此停止教程并立即开始使用它。\n" startTutorial: "开始教学" - skipAreYouSure: "要跳过初始设置吗?" + skipAreYouSure: "要跳过初始设定吗?" laterAreYouSure: "要稍后再进行初始设定吗?" _initialTutorial: launchTutorial: "观看教学" @@ -1596,12 +1639,34 @@ _serverSettings: fanoutTimelineDbFallback: "回退到数据库" fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。" reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。" + remoteNotesCleaning: "自动清理远程投稿" + remoteNotesCleaning_description: "启用后,将自动清理已无法找到的旧的远程投稿,可减缓数据库的增长。" + remoteNotesCleaningMaxProcessingDuration: "最长清理持续时间" + remoteNotesCleaningExpiryDaysForEachNotes: "最短帖子保留期限" inquiryUrl: "联络地址" inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。" openRegistration: "开放注册" - openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器并在出现问题时能够立即响应时才打开它。" + openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器,并在出现问题时能够立即响应时才打开它。" thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。" - deliverSuspendedSoftware: "不可用的软件" + deliverSuspendedSoftware: "停止投递的软件" + deliverSuspendedSoftwareDescription: "可因安全漏洞之类的原因,停止向指定的服务器及服务器版本送信。版本信息由服务器提供,不保证可靠性。可使用 semver 范围来指定版本,但指定 >= 2024.3.1 将不包括如 2024.3.1-custom.0 等自定义版本,因此建议像 >= 2024.3.1-0 这样指定 prerelease 版本。" + singleUserMode: "单用户模式" + singleUserMode_description: "若此服务器只有自己使用,开启此模式将最佳化性能。" + signToActivityPubGet: "对 GET 请求签名" + signToActivityPubGet_description: "通常情况下请保持启用。若遇到联合通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。" + proxyRemoteFiles: "代理远程文件" + proxyRemoteFiles_description: "如果启用,远程服务器的文件将由代理提供。可有效保护图像预览缩略图的生成与用户隐私。" + allowExternalApRedirect: "允许通过 ActivityPub 重定向查询" + allowExternalApRedirect_description: "启用时,将允许其它服务器通过此服务器查询第三方内容,但有可能导致内容欺骗。" + userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性" + userGeneratedContentsVisibilityForVisitor_description: "对于防止难以审核的不适当的远程内容等,通过自己的服务器无意中在互联网上公开等问题很有用。" + userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。" + restartServerSetupWizardConfirm_title: "要重新开始服务器初始设定向导吗?" + restartServerSetupWizardConfirm_text: "现有的部分设定将重置。" + _userGeneratedContentsVisibilityForVisitor: + all: "全部公开" + localOnly: "仅公开本地内容,隐藏远程内容" + none: "全部隐藏" _accountMigration: moveFrom: "从别的账号迁移到此账户" moveFromSub: "为另一个账户建立别名" @@ -1875,7 +1940,7 @@ _role: name: "角色名称" description: "角色描述" permission: "角色权限" - descriptionOfPermission: "监察员可以执行基本地审核操作。\n管理员可以更改服务器的所有设置。" + descriptionOfPermission: "监察员可以执行基本的审核操作。\n管理员可以更改实例的所有设置。" assignTarget: "授权对象" descriptionOfAssignTarget: "手动指手动选择谁被包括在这个角色中。\n符合条件指设置条件以自动包括符合条件的用户。" manual: "手动" @@ -1942,6 +2007,11 @@ _role: canImportMuting: "允许导入隐藏列表" canImportUserLists: "允许导入用户列表" chatAvailability: "允许聊天" + uploadableFileTypes: "可上传的文件类型" + uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*)" + uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。" + noteDraftLimit: "可在服务器上创建多少草稿" + watermarkAvailable: "能否使用水印功能" _condition: roleAssignedTo: "已分配给手动角色" isLocal: "是本地用户" @@ -2092,7 +2162,7 @@ _wordMute: muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" muteWordsDescription2: "正则表达式用斜线包裹" _instanceMute: - instanceMuteDescription: "隐藏服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" + instanceMuteDescription: "隐藏服务器中所有的帖子和转帖,包括这些服务器上用户的回复。" instanceMuteDescription2: "一行一个" title: "下面实例中的帖子将被隐藏。" heading: "已隐藏的服务器" @@ -2101,6 +2171,7 @@ _theme: install: "安装主题" manage: "主题管理" code: "主题代码" + copyThemeCode: "复制主题代码" description: "描述" installed: "{name} 已安装" installedThemes: "已安装的主题" @@ -2159,7 +2230,6 @@ _theme: buttonBg: "按钮背景" buttonHoverBg: "按钮背景(悬停)" inputBorder: "输入框边框" - driveFolderBg: "网盘的文件夹背景" badge: "徽章" messageBg: "聊天背景" fgHighlighted: "高亮显示文本" @@ -2415,6 +2485,8 @@ _visibility: disableFederation: "不参与联合" disableFederationDescription: "不发送到其他服务器" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "还有未上传的文件,要丢弃并关闭窗口吗?" + uploaderTip: "文件还未上传。可以在文件菜单中进行重命名、裁剪、添加水印、设置是否压缩等操作。文件将在发帖时自动上传。" replyPlaceholder: "回复这个帖子..." quotePlaceholder: "引用这个帖子..." channelPlaceholder: "发布到频道…" @@ -2441,7 +2513,7 @@ _profile: avatarDecorationMax: "最多可添加 {max} 个挂件" followedMessage: "被关注时显示的消息" followedMessageDescription: "可以设置被关注时向对方显示的短消息。" - followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。" + followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息会在请求被批准后显示。" _exportOrImport: allNotes: "所有帖子" favoritedNotes: "收藏的帖子" @@ -2748,6 +2820,7 @@ _fileViewer: url: "URL" uploadedAt: "添加日期" attachedNotes: "附加到的帖子" + usage: "使用" thisPageCanBeSeenFromTheAuthor: "此页只能被该文件的上传者查看。" _externalResourceInstaller: title: "从外部站点安装" @@ -2795,9 +2868,12 @@ _dataSaver: _avatar: title: "头像" description: "停止播放头像的动画。 由于动画图片的文件大小可能比普通图像大,这可以进一步减少数据流量。" - _urlPreview: - title: "URL预览缩略图\n" + _urlPreviewThumbnail: + title: "不显示 URL预览缩略图" description: "将不再加载 URL 预览缩略图。" + _disableUrlPreview: + title: "禁用 URL 预览" + description: "关闭 URL 预览功能。与预览缩略图不同,减少了链接信息的加载。" _code: title: "代码高亮" description: "如果使用了代码高亮标记,例如在 MFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。" @@ -2855,6 +2931,8 @@ _offlineScreen: _urlPreviewSetting: title: "设置 URL 预览" enable: "启用 URL 预览" + allowRedirect: "允许预览目标的重定向" + allowRedirectDescription: "如果输入的 URL 被重定向,可设置是否跟随重定向目标并显示预览。禁用此选项将节省服务器资源,但重定向目标的内容将不会显示。" timeout: "超时阈值(ms)" timeoutDescription: "如果获取预览所用时间超过这个值,则不生成预览。" maximumContentLength: "Content-Length 的最大值(byte)" @@ -2928,10 +3006,6 @@ _customEmojisManager: uploadSettingDescription: "可以在此页面设置上传表情符号时的行为。" directoryToCategoryLabel: "将目录名设为「category」" directoryToCategoryCaption: "拖放目录时,将目录名设置为「category」" - emojiInputAreaCaption: "请使用其中一种方法选择要注册的表情符号。" - emojiInputAreaList1: "在此区域内拖放图像文件或者目录" - emojiInputAreaList2: "单击此链接以从电脑中选择" - emojiInputAreaList3: "单击此链接以从网盘中选择" confirmRegisterEmojisDescription: "要将列表内显示的表情符号替换为新的自定义表情符号吗?(为降低服务器负载,一次操作最多只能注册 {count} 个表情符号)" confirmClearEmojisDescription: "要放弃编辑并将列表内表示的表情符号清空吗?" confirmUploadEmojisDescription: "要将拖放的 {count} 个文件上传到网盘上吗?" @@ -2999,6 +3073,7 @@ _bootErrors: otherOption1: "清除客户端设定与缓存" otherOption2: "使用简易客户端" otherOption3: "启动修复工具" + otherOption4: "以安全模式启动 Misskey" _search: searchScopeAll: "全部" searchScopeLocal: "本地" @@ -3007,3 +3082,132 @@ _search: pleaseEnterServerHost: "请填写服务器主机名" pleaseSelectUser: "请选择用户" serverHostPlaceholder: "如:misskey.example.com" +_serverSetupWizard: + installCompleted: "Misskey 安装完成!" + firstCreateAccount: "首先来创建管理员账号吧。" + accountCreated: "管理员账号已创建!" + serverSetting: "服务器设置" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "用此向导来轻松地以最佳方式配置服务器。" + settingsYouMakeHereCanBeChangedLater: "这里的设置在之后也能更改。" + howWillYouUseMisskey: "打算怎样使用 Misskey?" + _use: + single: "单用户服务器" + single_description: "仅供自己使用的单人服务器" + single_youCanCreateMultipleAccounts: "使用单用户服务器模式使用时,也可以根据需要创建多个账号。" + group: "小圈子服务器" + group_description: "邀请其他可信用户一起使用的多人服务器" + open: "开放服务器" + open_description: "以容纳不限定数量的用户的模式运行" + openServerAdvice: "容纳不限定数量的用户有风险。推荐建立能应对各种问题的强大的管理体制来运营。" + openServerAntiSpamAdvice: "为防止自己的服务器成为广告发信基地,请打开如 reCAPTCHA 等 Bot 防御功能,并谨慎关注安全性。" + howManyUsersDoYouExpect: "预计会有多少用户?" + _scale: + small: "100 人以下(小规模)" + medium: "100 人以上 1000 人以下(中规模)" + large: "1000 人以上(大规模)" + largeScaleServerAdvice: "运营大规模服务器可能需要高级基础设施知识,如负载均衡和数据库复制。" + doYouConnectToFediverse: "要加入 Fediverse 吗?" + doYouConnectToFediverse_description1: "若加入由分散性服务器所构成的网络(Fediverse),将能与其它服务器交换内容。" + doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联合」。" + youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。" + remoteContentsCleaning: "自动清理传入内容" + remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。" + adminInfo: "管理员信息" + adminInfo_description: "设置用于接受询问的管理员信息。" + adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。" + followingSettingsAreRecommended: "推荐以下设置" + applyTheseSettings: "使用此设置" + skipSettings: "跳过设置" + settingsCompleted: "设置完成!" + settingsCompleted_description: "辛苦了。设置已完成,可以立即开始使用服务器了。" + settingsCompleted_description2: "服务器的详细设置可在「控制面板」进行。" + donationRequest: "请求捐助" + _donationRequest: + text1: "Misskey 是由志愿者开发的免费软件。" + text2: "为了今后也能继续开发,如果可以的话,请考虑一下捐助。" + text3: "也有面向支援者的特典!" +_uploader: + editImage: "编辑图像" + compressedToX: "压缩 {x}" + savedXPercent: "节省了 {x}% 的空间" + abortConfirm: "还有未上传的文件,要中止吗?" + doneConfirm: "还有未上传的文件,要完成吗?" + maxFileSizeIsX: "可上传最大 {x} 的文件。" + allowedTypes: "可上传的文件类型" + tip: "文件还没有被上传。可在此对话框中进行上传前确认、重命名、压缩、裁剪等操作。准备完成后,点击「上传」即可开始上传。" +_clientPerformanceIssueTip: + title: "如果觉得电池耗电过高" + makeSureDisabledAdBlocker: "请关闭广告拦截器" + makeSureDisabledAdBlocker_description: "广告拦截器会影响性能。请检查操作系统功能、浏览器功能或附加组件是否启用了广告拦截器。" + makeSureDisabledCustomCss: "请关闭自定义 CSS" + makeSureDisabledCustomCss_description: "覆盖样式可能会影响性能。请确保没有启用任何自定义 CSS 或覆盖样式的扩展。" + makeSureDisabledAddons: "请关闭扩展" + makeSureDisabledAddons_description: "某些扩展可能会干扰客户端的运行并影响性能。尝试禁用浏览器扩展并查看是否有改善。" +_clip: + tip: "便签功能可以将帖子合并在一起。" +_userLists: + tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。" +watermark: "水印" +defaultPreset: "默认预设" +_watermarkEditor: + tip: "可在图像内增加包含作者等信息的水印。" + quitWithoutSaveConfirm: "不保存就退出吗?" + driveFileTypeWarn: "不支持此文件" + driveFileTypeWarnDescription: "请选择图像文件" + title: "编辑水印" + cover: "覆盖全体" + repeat: "平铺" + opacity: "不透明度" + scale: "大小" + text: "文本" + position: "位置" + type: "类型" + image: "图片" + advanced: "高级" + stripe: "条纹" + stripeWidth: "线条宽度" + stripeFrequency: "线条数量" + angle: "角度" + polkadot: "波点" + checker: "检查" + polkadotMainDotOpacity: "主波点的不透明度" + polkadotMainDotRadius: "主波点的大小" + polkadotSubDotOpacity: "副波点的不透明度" + polkadotSubDotRadius: "副波点的大小" + polkadotSubDotDivisions: "副波点的数量" +_imageEffector: + title: "效果" + addEffect: "添加效果" + discardChangesConfirm: "丢弃当前设置并退出?" + _fxs: + chromaticAberration: "色差" + glitch: "故障" + mirror: "镜像" + invert: "反转颜色" + grayscale: "黑白" + colorAdjust: "色彩校正" + colorClamp: "颜色限制" + colorClampAdvanced: "颜色限制(高级)" + distort: "失真" + threshold: "二值化" + zoomLines: "集中线" + stripe: "条纹" + polkadot: "波点" + checker: "检查" + blockNoise: "块状噪点" + tearing: "撕裂" +drafts: "草稿" +_drafts: + select: "选择草稿" + cannotCreateDraftAnymore: "已超过可创建的草稿数量。" + cannotCreateDraft: "此内容无法创建草稿。" + delete: "删除草稿" + deleteAreYouSure: "要删除草稿吗?" + noDrafts: "没有草稿" + replyTo: "回复给 {user}" + quoteOf: "对 {user} 帖子的引用" + postTo: "向 {channel} 的投稿" + saveToDraft: "保存到草稿" + restoreFromDraft: "从草稿恢复" + restore: "恢复" + listDrafts: "草稿一览" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 5900bccff7..1a2aaa6a12 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -298,6 +298,7 @@ uploadFromUrl: "從網址上傳" uploadFromUrlDescription: "您要上傳的檔案網址" uploadFromUrlRequested: "已請求上傳" uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。" +uploadNFiles: "上傳了 {n} 個檔案" explore: "探索" messageRead: "已讀" noMoreHistory: "沒有更多歷史紀錄" @@ -326,6 +327,7 @@ dark: "深色" lightThemes: "淺色佈景主題" darkThemes: "深色佈景主題" syncDeviceDarkMode: "與裝置的深色模式同步" +switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」已開啟。要關閉同步並手動切換模式嗎?\n" drive: "雲端硬碟" fileName: "檔案名稱" selectFile: "選擇檔案" @@ -575,8 +577,10 @@ showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)" withRepliesByDefaultForNewlyFollowed: "在追隨其他人後,預設在時間軸納入回覆的貼文" newNoteRecived: "發現新貼文" +newNote: "新的貼文" sounds: "音效" sound: "音效" +notificationSoundSettings: "設定通知音效" listen: "聆聽" none: "無" showInPage: "在頁面中顯示" @@ -634,7 +638,7 @@ inboxUrl: "收件夾 URL" addedRelays: "已加入的中繼器" serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。" deletedNote: "已刪除的貼文" -invisibleNote: "私人貼文" +invisibleNote: "私密的貼文" enableInfiniteScroll: "啟用自動滾動頁面模式" visibility: "可見性" poll: "票選活動" @@ -791,6 +795,7 @@ wide: "寬" narrow: "窄" reloadToApplySetting: "設定將會在頁面重新載入之後生效。要現在就重載頁面嗎?" needReloadToApply: "必須重新載入才會生效。" +needToRestartServerToApply: "必須重新啟動伺服器才會使變更生效。" showTitlebar: "顯示標題列" clearCache: "清除快取資料" onlineUsersCount: "{n} 人上線" @@ -997,6 +1002,7 @@ failedToUpload: "上傳失敗" cannotUploadBecauseInappropriate: "由於判定可能包含不適當的內容,因此無法上傳。" cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此無法上傳。" cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。" +cannotUploadBecauseUnallowedFileType: "由於檔案類型不被允許,無法上傳。\n" beta: "測試版" enableAutoSensitive: "自動 NSFW 判定" enableAutoSensitiveDescription: "如果可行,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依伺服器規則而自動啟用。" @@ -1307,6 +1313,7 @@ availableRoles: "可用角色" acknowledgeNotesAndEnable: "了解注意事項後再開啟。" federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。" federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。" +draft: "草稿\n" confirmOnReact: "在做出反應前先確認" reactAreYouSure: "用「 {emoji} 」反應嗎?" markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?" @@ -1324,6 +1331,7 @@ restore: "還原" syncBetweenDevices: "裝置之間的同步化" preferenceSyncConflictTitle: "伺服器上存在設定值" preferenceSyncConflictText: "已啟用同步的設定項目會將設定值儲存至伺服器,並已找到該設定項目在伺服器上儲存的設定值。請選擇要使用哪個設定值進行覆寫。" +preferenceSyncConflictChoiceMerge: "合併至" preferenceSyncConflictChoiceServer: "伺服器設定值" preferenceSyncConflictChoiceDevice: "裝置的設定值" preferenceSyncConflictChoiceCancel: "取消啟用同步" @@ -1346,6 +1354,29 @@ goToDeck: "回去甲板" federationJobs: "聯邦通訊作業" driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。
\n可以在附加到貼文時重新利用,或者事先上傳之後再用於發布。
\n請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。
\n也可以建立資料夾來整理檔案。" scrollToClose: "用滾輪關閉" +advice: "建議" +realtimeMode: "即時模式" +turnItOn: "開啟" +turnItOff: "關閉" +emojiMute: "表情符號靜音" +emojiUnmute: "表情符號解除靜音" +muteX: "將 {x} 靜音" +unmuteX: "將 {x} 解除靜音" +abort: "取消" +tip: "提示與技巧" +redisplayAllTips: "重新顯示所有「提示與技巧」" +hideAllTips: "隱藏所有「提示與技巧」" +defaultImageCompressionLevel: "預設的影像壓縮程度" +defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。
高的話可以減少檔案大小,但是會降低畫質。" +inMinutes: "分鐘" +inDays: "日" +safeModeEnabled: "啟用安全模式" +pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛都被停用。" +customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。" +themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。" +_order: + newest: "最新的在前" + oldest: "最舊的在前" _chat: noMessagesYet: "尚無訊息" newMessage: "新訊息" @@ -1379,6 +1410,8 @@ _chat: chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。" cannotChatWithTheUser: "無法與此使用者聊天" cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。" + youAreNotAMemberOfThisRoomButInvited: "您不是此聊天室的參與者,但已收到邀請。若要加入,請先接受邀請。\n" + doYouAcceptInvitation: "您要接受這個邀請嗎?\n" chatWithThisUser: "聊天" thisUserAllowsChatOnlyFromFollowers: "此使用者僅接受來自追隨者的聊天訊息。" thisUserAllowsChatOnlyFromFollowing: "此使用者僅接受自己追隨的使用者傳送聊天訊息。" @@ -1418,12 +1451,21 @@ _settings: makeEveryTextElementsSelectable: "允許選取所有文字" makeEveryTextElementsSelectable_description: "啟用此功能後,可能會在某些情境下降低可用性。" useStickyIcons: "使大頭貼跟隨捲動" + enableHighQualityImagePlaceholders: "顯示高品質的圖片預覽圖" + uiAnimations: "使用者介面的動畫效果\n" showNavbarSubButtons: "在導覽列顯示輔助按鈕" ifOn: "開啟時" ifOff: "關閉時" enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題" enablePullToRefresh: "下拉更新" enablePullToRefresh_description: "使用滑鼠,按下並拖曳滾輪。" + realtimeMode_description: "已與伺服器建立連線,將即時更新內容。這可能會增加資料傳輸量與電池消耗。\n" + contentsUpdateFrequency: "內容取得頻率" + contentsUpdateFrequency_description: "頻率越高,內容更新越即時,但可能會降低效能,並增加資料傳輸量與電池消耗。\n" + contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。" + showUrlPreview: "顯示網址預覽" + showAvailableReactionsFirstInNote: "將可用的反應顯示在頂部" + showPageTabBarBottom: "在底部顯示頁面的標籤列" _chat: showSenderName: "顯示發送者的名稱" sendOnEnter: "按下 Enter 發送訊息" @@ -1508,7 +1550,7 @@ _initialAccountSetting: theseSettingsCanEditLater: "這裡的設定可以在之後變更。" youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。" followUsers: "為了構築時間軸,試著追隨您感興趣的使用者吧。" - pushNotificationDescription: "啟用推送通知後,就可以在裝置上接收來自{name}的通知了。" + pushNotificationDescription: "啟用推送通知後,就可以在裝置上接收來自 {name} 的通知了。" initialAccountSettingCompleted: "初始設定完成了!" haveFun: "盡情享受{name}吧!" youCanContinueTutorial: "您可以繼續學習如何使用{name}(Misskey),也可以就此打住,立即開始使用。" @@ -1597,6 +1639,10 @@ _serverSettings: fanoutTimelineDbFallback: "資料庫的回退" fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。" reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是,Redis 記憶體使用量會增加。" + remoteNotesCleaning: "自動清除遠端發佈內容" + remoteNotesCleaning_description: "啟用後,系統會定期清理未被參照的舊遠端貼文,以抑制資料庫的膨脹。" + remoteNotesCleaningMaxProcessingDuration: "清理作業的最長持續時間" + remoteNotesCleaningExpiryDaysForEachNotes: "貼文最短保留天數" inquiryUrl: "聯絡表單網址" inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。" openRegistration: "允許建立帳戶" @@ -1604,6 +1650,23 @@ _serverSettings: thisSettingWillAutomaticallyOffWhenModeratorsInactive: "如果在一段期間內沒有偵測到任何審查員活動,此設定將自動關閉,以防止垃圾內容。" deliverSuspendedSoftware: "已停止發佈的軟體" deliverSuspendedSoftwareDescription: "由於脆弱性等原因,可以指定伺服器軟體的名稱與版本範圍來停止其發佈。這些版本資訊是由伺服器所提供,其可靠性無法保證。版本的指定可以使用 semver(語意化版本控制) 的範圍語法,但如果指定為 >= 2024.3.1,則像 2024.3.1-custom.0 這樣的自訂版本將不會被包含在內,因此建議使用 >= 2024.3.1-0 的方式來同時包含預發佈版本。" + singleUserMode: "單人模式" + singleUserMode_description: "如果只有自己使用此伺服器的話,啟用此模式將使效能最佳化。" + signToActivityPubGet: "簽署 GET 請求" + signToActivityPubGet_description: "通常應該啟用此功能。停用可能會改善聯邦通訊的問題,但反過來也可能會使某些伺服器無法通訊。" + proxyRemoteFiles: "代理提供遠端檔案" + proxyRemoteFiles_description: "啟用時,它會代理並提供遠端檔案。 這有助於產生影像縮圖和保護使用者隱私。" + allowExternalApRedirect: "允許透過 ActivityPub 查詢時進行重新導向" + allowExternalApRedirect_description: "啟用後,其他伺服器可以透過此伺服器查詢第三方的內容,但也可能導致內容遭到冒充的風險。" + userGeneratedContentsVisibilityForVisitor: "使用者建立的內容對訪客的公開範圍" + userGeneratedContentsVisibilityForVisitor_description: "這有助於防止一些問題的發生,例如未經適當審核的不適當遠端內容無意中透過您自己的伺服器發佈到網際網路上。" + userGeneratedContentsVisibilityForVisitor_description2: "包括伺服器接收到的遠端內容在內,無條件地將伺服器內所有內容公開到網際網路上是具有風險的。特別是對於不了解分散式架構特性的瀏覽者來說,他們可能會誤以為這些遠端內容是由該伺服器所創建的,因此需要特別留意。" + restartServerSetupWizardConfirm_title: "要重新執行伺服器的初始設定精靈嗎?" + restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。" + _userGeneratedContentsVisibilityForVisitor: + all: "全部公開\n" + localOnly: "僅公開本地內容,遠端內容則不公開\n" + none: "全部不公開" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" @@ -1944,6 +2007,11 @@ _role: canImportMuting: "允許匯入靜音名單" canImportUserLists: "允許匯入清單" chatAvailability: "允許聊天" + uploadableFileTypes: "可上傳的檔案類型" + uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*)作為萬用字元進行指定。(例如:image/*)\n" + uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。" + noteDraftLimit: "伺服器端可建立的貼文草稿數量上限\n" + watermarkAvailable: "浮水印功能是否可用" _condition: roleAssignedTo: "手動指派角色完成" isLocal: "本地使用者" @@ -2103,6 +2171,7 @@ _theme: install: "安裝佈景主題" manage: "管理佈景主題" code: "佈景主題代碼" + copyThemeCode: "複製主題代碼" description: "描述" installed: "{name}已安裝" installedThemes: "已經安裝的佈景主題" @@ -2161,7 +2230,6 @@ _theme: buttonBg: "按鈕背景" buttonHoverBg: "按鈕背景 (漂浮)" inputBorder: "輸入框邊框" - driveFolderBg: "雲端硬碟文件夾背景" badge: "徽章" messageBg: "私訊背景" fgHighlighted: "突顯文字" @@ -2417,6 +2485,8 @@ _visibility: disableFederation: "停用聯邦" disableFederationDescription: "不發送到其他伺服器" _postForm: + quitInspiteOfThereAreUnuploadedFilesConfirm: "尚有未上傳的檔案,確定要放棄並關閉表單嗎?" + uploaderTip: "檔案尚未上傳。您可以從檔案選單中設定重新命名、裁切圖片、加上浮水印、是否壓縮等選項。檔案會在發布貼文時自動上傳。\n" replyPlaceholder: "回覆此貼文..." quotePlaceholder: "引用此貼文..." channelPlaceholder: "發佈到頻道" @@ -2750,6 +2820,7 @@ _fileViewer: url: "URL" uploadedAt: "加入日期" attachedNotes: "含有附件的貼文" + usage: "使用情況" thisPageCanBeSeenFromTheAuthor: "本頁面僅限上傳了這個檔案的使用者可以檢視。" _externalResourceInstaller: title: "從外部網站安裝" @@ -2797,9 +2868,12 @@ _dataSaver: _avatar: title: "大頭貼" description: "停止顯示大頭貼的動畫。由於動畫圖片的檔案大小可能比普通圖片大,這可以進一步減少資料流量。" - _urlPreview: - title: "網址預覽縮圖" + _urlPreviewThumbnail: + title: "不顯示網址預覽縮圖" description: "將不再自動載入網址預覽縮圖。" + _disableUrlPreview: + title: "停用網址預覽" + description: "停用網址預覽功能。與單獨使用縮圖不同,這樣可以減少載入連結資訊本身。" _code: title: "程式碼突出顯示" description: "如果使用了程式碼突顯語法(如 MFM),則在點擊之前不會被載入。由於需要為對應的程式語言下載突顯定義檔案,因此關閉自動載入有助於減少資料流量。" @@ -2857,6 +2931,8 @@ _offlineScreen: _urlPreviewSetting: title: "URL 預覽設定" enable: "啟用 URL 預覽" + allowRedirect: "允許預覽目標的重新導向" + allowRedirectDescription: "設定當輸入的 URL 發生重新導向時,是否追蹤該重新導向並顯示預覽。若停用此功能,雖可節省伺服器資源,但將無法顯示重新導向後的內容。\n" timeout: "取得預覽的逾時時間 (ms)" timeoutDescription: "若取得預覽所需的時間超過這個值,則不會產生預覽。" maximumContentLength: "Content-Length 的最大値 (byte)" @@ -2930,10 +3006,6 @@ _customEmojisManager: uploadSettingDescription: "您可以在此畫面設定表情符號上傳時的操作。" directoryToCategoryLabel: "在「類別」欄位中輸入目錄名稱" directoryToCategoryCaption: "拖放目錄時,請在「類別」欄位中輸入目錄名稱。" - emojiInputAreaCaption: "以下列其中一種方式選擇您想要註冊的表情符號" - emojiInputAreaList1: "將圖片檔案或目錄拖放到此框中" - emojiInputAreaList2: "點擊此連結從電腦中選擇" - emojiInputAreaList3: "點擊此連結從雲端硬碟中選擇" confirmRegisterEmojisDescription: "將列表中顯示的表情符號登錄為新的自定表情符號。是否確定?(為避免過高負荷,每次操作最多可登錄{count}個表情符號)" confirmClearEmojisDescription: "放棄編輯內容並清除列表中顯示的表情符號。是否確定?" confirmUploadEmojisDescription: "將拖放的{count}個檔案上傳到雲端硬碟。是否執行此操作?" @@ -3001,6 +3073,7 @@ _bootErrors: otherOption1: "刪除用戶端設定和快取" otherOption2: "啟動簡易用戶端" otherOption3: "啟動修復工具" + otherOption4: "以安全模式啟動 Misskey" _search: searchScopeAll: "全部" searchScopeLocal: "本地" @@ -3009,3 +3082,132 @@ _search: pleaseEnterServerHost: "請輸入伺服器的主機名稱" pleaseSelectUser: "請選擇使用者" serverHostPlaceholder: "例:misskey.example.com" +_serverSetupWizard: + installCompleted: "Misskey 的安裝已經完成了!" + firstCreateAccount: "首先,請建立管理者帳戶。" + accountCreated: "已建立管理者帳戶!" + serverSetting: "伺服器設定" + youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "利用這個精靈,可以簡單地最佳化伺服器的設定。" + settingsYouMakeHereCanBeChangedLater: "這裡的設定之後也可以進行更改。\n" + howWillYouUseMisskey: "您打算如何使用 Misskey?\n" + _use: + single: "單人伺服器" + single_description: "作為自己專用的伺服器,單獨使用。\n" + single_youCanCreateMultipleAccounts: "即使作為單人伺服器運行,根據需要也可以創建多個帳戶。\n" + group: "群組伺服器\n" + group_description: "邀請可信賴的其他使用者,共同使用伺服器。\n" + open: "開放式伺服器" + open_description: "運營時接納不特定多數的使用者。" + openServerAdvice: "接納不特定多數使用者會帶來風險。為了能夠有效處理問題,建議建立完善的審查機制來進行運營。\n" + openServerAntiSpamAdvice: "為了防止自家伺服器成為垃圾郵件的跳板,必須啟用如 reCAPTCHA 等反機器人功能,並對安全性保持高度警覺。\n" + howManyUsersDoYouExpect: "您預計有多少人使用呢?\n" + _scale: + small: "100人以下(小規模)\n" + medium: "100人以上1000人以下(中規模)\n" + large: "1000人以上(大規模)\n" + largeScaleServerAdvice: "在大規模伺服器中,可能需要具備高階基礎設施知識,如負載平衡和資料庫複寫等。\n" + doYouConnectToFediverse: "您要連接到聯邦宇宙(Fediverse)嗎?\n" + doYouConnectToFediverse_description1: "連接到由分散型伺服器構成的網絡(聯邦宇宙)後,您可以與其他伺服器進行內容的互相交流。\n" + doYouConnectToFediverse_description2: "連接到聯邦宇宙被稱為「聯邦」。\n" + youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n" + remoteContentsCleaning: "自動清理接收的內容" + remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。" + adminInfo: "管理員資訊" + adminInfo_description: "設定用於接收查詢的管理者資訊。\n" + adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n" + followingSettingsAreRecommended: "建議使用下列設定" + applyTheseSettings: "套用此設定" + skipSettings: "跳過設定" + settingsCompleted: "設定完成!" + settingsCompleted_description: "辛苦了!準備已經完成,您可以立即開始使用伺服器了。\n" + settingsCompleted_description2: "詳細的伺服器設定可透過「控制臺」進行。" + donationRequest: "請求捐款" + _donationRequest: + text1: "Misskey 是由志願者開發的免費軟體。" + text2: "為了能夠繼續開發,若您願意的話,請考慮進行捐款。\n" + text3: "也有提供支援者專屬的特典!\n" +_uploader: + editImage: "編輯圖片" + compressedToX: "壓縮為 {x}" + savedXPercent: "節省了 {x}%" + abortConfirm: "有些檔案尚未上傳,您要中止嗎?" + doneConfirm: "有些檔案尚未上傳,是否要完成上傳?" + maxFileSizeIsX: "可上傳的最大檔案大小為 {x}。" + allowedTypes: "可上傳的檔案類型" + tip: "檔案尚未上傳。您可以在此對話框中進行上傳前的確認、重新命名、壓縮、裁切等操作。準備完成後,請點選「上傳」按鈕開始上傳。\n" +_clientPerformanceIssueTip: + title: "如果覺得電池消耗過快的話" + makeSureDisabledAdBlocker: "請將廣告阻擋器停用" + makeSureDisabledAdBlocker_description: "廣告阻擋器可能會影響效能。請確認作業系統功能、瀏覽器設定或擴充功能中是否啟用了廣告阻擋器。\n" + makeSureDisabledCustomCss: "請停用自訂 CSS" + makeSureDisabledCustomCss_description: "覆蓋樣式可能會影響效能。請確認是否啟用了自訂 CSS 或其他會覆蓋樣式的擴充功能。\n" + makeSureDisabledAddons: "請停用擴充功能" + makeSureDisabledAddons_description: "部分擴充功能可能會干擾用戶端的運作並影響效能。請嘗試停用瀏覽器的擴充功能,以確認是否能改善情況" +_clip: + tip: "摘錄是一項可以用來整理貼文的功能。" +_userLists: + tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n" +watermark: "浮水印" +defaultPreset: "預設值" +_watermarkEditor: + tip: "可以在圖片中以浮水印加上出處等資訊。" + quitWithoutSaveConfirm: "不儲存就退出嗎?" + driveFileTypeWarn: "不支援此檔案" + driveFileTypeWarnDescription: "請選擇圖片檔案" + title: "編輯浮水印" + cover: "覆蓋整體" + repeat: "佈局" + opacity: "透明度" + scale: "大小" + text: "文字" + position: "位置" + type: "類型" + image: "圖片" + advanced: "進階" + stripe: "條紋" + stripeWidth: "線條寬度" + stripeFrequency: "線條數量" + angle: "角度" + polkadot: "波卡圓點" + checker: "棋盤格" + polkadotMainDotOpacity: "主圓點的不透明度" + polkadotMainDotRadius: "主圓點的尺寸" + polkadotSubDotOpacity: "子圓點的不透明度" + polkadotSubDotRadius: "子圓點的尺寸" + polkadotSubDotDivisions: "子圓點的數量" +_imageEffector: + title: "特效" + addEffect: "新增特效" + discardChangesConfirm: "捨棄更改並退出嗎?" + _fxs: + chromaticAberration: "色差" + glitch: "異常雜訊效果" + mirror: "鏡像" + invert: "反轉色彩" + grayscale: "黑白" + colorAdjust: "色彩校正" + colorClamp: "壓縮色彩" + colorClampAdvanced: "壓縮色彩(進階)" + distort: "變形" + threshold: "閾值" + zoomLines: "速度線" + stripe: "條紋" + polkadot: "波卡圓點" + checker: "棋盤格" + blockNoise: "阻擋雜訊" + tearing: "撕裂" +drafts: "草稿\n" +_drafts: + select: "選擇草槁" + cannotCreateDraftAnymore: "已超出可建立的草稿數量上限。\n" + cannotCreateDraft: "無法以此內容建立草稿。\n" + delete: "刪除草稿" + deleteAreYouSure: "確定要刪除草稿嗎?\n" + noDrafts: "沒有草稿。\n" + replyTo: "回覆給 {user}\n" + quoteOf: "引用自 {user} 的貼文\n" + postTo: "發佈到 {channel}\n" + saveToDraft: "儲存為草稿" + restoreFromDraft: "從草稿復原\n" + restore: "還原" + listDrafts: "草稿清單" diff --git a/package.json b/package.json index f5210a545d..3b7918bbca 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { "name": "misskey", - "version": "2025.5.1-alpha.0", + "version": "2025.8.0-alpha.9", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@10.10.0", + "packageManager": "pnpm@10.14.0", "workspaces": [ "packages/frontend-shared", "packages/frontend", "packages/frontend-embed", + "packages/icons-subsetter", "packages/backend", "packages/sw", "packages/misskey-js", @@ -51,30 +52,30 @@ "lodash": "4.17.21" }, "dependencies": { - "cssnano": "7.0.6", - "esbuild": "0.25.3", - "execa": "9.5.2", + "cssnano": "7.1.0", + "esbuild": "0.25.8", + "execa": "9.6.0", "fast-glob": "3.3.3", - "glob": "11.0.2", + "glob": "11.0.3", "ignore-walk": "7.0.0", "js-yaml": "4.1.0", - "postcss": "8.5.3", + "postcss": "8.5.6", "tar": "7.4.3", - "terser": "5.39.0", - "typescript": "5.8.3" + "terser": "5.43.1", + "typescript": "5.9.2" }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.1.0", - "@types/node": "22.15.2", - "@typescript-eslint/eslint-plugin": "8.31.0", - "@typescript-eslint/parser": "8.31.0", + "@types/node": "22.17.1", + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", "cross-env": "7.0.3", - "cypress": "14.3.2", - "eslint": "9.25.1", - "globals": "16.0.0", + "cypress": "14.5.4", + "eslint": "9.33.0", + "globals": "16.3.0", "ncp": "2.0.0", - "pnpm": "10.10.0", - "start-server-and-test": "2.0.11" + "pnpm": "10.14.0", + "start-server-and-test": "2.0.13" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.22.0" @@ -82,6 +83,9 @@ "pnpm": { "overrides": { "@aiscript-dev/aiscript-languageserver": "-" + }, + "patchedDependencies": { + "typeorm": "patches/typeorm.patch" } } } diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js index d15a703ba2..ba7c705def 100644 --- a/packages/backend/eslint.config.js +++ b/packages/backend/eslint.config.js @@ -25,6 +25,7 @@ export default [ }, }, rules: { + '@typescript-eslint/no-unused-vars': 'off', 'import/order': ['warn', { groups: [ 'builtin', diff --git a/packages/backend/jest.js b/packages/backend/jest.js index 0cb2c2ab77..0e761d8c92 100644 --- a/packages/backend/jest.js +++ b/packages/backend/jest.js @@ -17,4 +17,15 @@ args.push(...[ ...process.argv.slice(2), ]); -child_process.spawn(process.execPath, args, { stdio: 'inherit' }); +const child = child_process.spawn(process.execPath, args, { stdio: 'inherit' }); +child.on('error', (err) => { + console.error('Failed to start Jest:', err); + process.exit(1); +}); +child.on('exit', (code, signal) => { + if (code === null) { + process.exit(128 + signal); + } else { + process.exit(code); + } +}); diff --git a/packages/backend/migration/1736686850345-createNoteDraft.js b/packages/backend/migration/1736686850345-createNoteDraft.js new file mode 100644 index 0000000000..3b525a7339 --- /dev/null +++ b/packages/backend/migration/1736686850345-createNoteDraft.js @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class CreateNoteDraft1736686850345 { + name = 'CreateNoteDraft1736686850345' + + async up(queryRunner) { + await queryRunner.query(` + CREATE TABLE "note_draft" ( + "id" varchar NOT NULL, + "replyId" varchar NULL, + "renoteId" varchar NULL, + "text" text NULL, + "cw" varchar(512) NULL, + "userId" varchar NOT NULL, + "localOnly" boolean DEFAULT false, + "reactionAcceptance" varchar(64) NULL, + "visibility" varchar NOT NULL, + "fileIds" varchar[] DEFAULT '{}', + "visibleUserIds" varchar[] DEFAULT '{}', + "hashtag" varchar(128) NULL, + "channelId" varchar NULL, + "hasPoll" boolean DEFAULT false, + "pollChoices" varchar(256)[] DEFAULT '{}', + "pollMultiple" boolean NULL, + "pollExpiresAt" TIMESTAMP WITH TIME ZONE NULL, + "pollExpiredAfter" bigint NULL, + PRIMARY KEY ("id") + )`); + + await queryRunner.query(` + CREATE INDEX "IDX_NOTE_DRAFT_REPLY_ID" ON "note_draft" ("replyId") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_NOTE_DRAFT_RENOTE_ID" ON "note_draft" ("renoteId") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_NOTE_DRAFT_USER_ID" ON "note_draft" ("userId") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_NOTE_DRAFT_FILE_IDS" ON "note_draft" USING GIN ("fileIds") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_NOTE_DRAFT_VISIBLE_USER_IDS" ON "note_draft" USING GIN ("visibleUserIds") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_NOTE_DRAFT_CHANNEL_ID" ON "note_draft" ("channelId") + `); + + await queryRunner.query(` + ALTER TABLE "note_draft" + ADD CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE + `); + + await queryRunner.query(` + ALTER TABLE "note_draft" + ADD CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE + `); + + await queryRunner.query(` + ALTER TABLE "note_draft" + ADD CONSTRAINT "FK_NOTE_DRAFT_USER_ID" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE + `); + + await queryRunner.query(` + ALTER TABLE "note_draft" + ADD CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE + `); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_USER_ID"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID"`); + await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_CHANNEL_ID"`); + await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_VISIBLE_USER_IDS"`); + await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_FILE_IDS"`); + await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_USER_ID"`); + await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_RENOTE_ID"`); + await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_REPLY_ID"`); + await queryRunner.query(`DROP TABLE "note_draft"`); + } +} diff --git a/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js b/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js new file mode 100644 index 0000000000..3243f43b91 --- /dev/null +++ b/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import {loadConfig} from "./js/migration-config.js"; + +export class MigrateSomeConfigFileSettingsToMeta1746949539915 { + name = 'MigrateSomeConfigFileSettingsToMeta1746949539915' + + async up(queryRunner) { + const config = loadConfig(); + // $1 cannot be used in ALTER TABLE queries + await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT ${config.proxyRemoteFiles}`); + await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT ${config.signToActivityPubGet}`); + await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT ${!config.disallowExternalApRedirect}`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowExternalApRedirect"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "signToActivityPubGet"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`); + } +} diff --git a/packages/backend/migration/1748310233000-addUrlPreviewAllowRedirect.js b/packages/backend/migration/1748310233000-addUrlPreviewAllowRedirect.js new file mode 100644 index 0000000000..a895d0a941 --- /dev/null +++ b/packages/backend/migration/1748310233000-addUrlPreviewAllowRedirect.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddUrlPreviewAllowRedirect1748310233000 { + name = 'AddUrlPreviewAllowRedirect1748310233000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewAllowRedirect" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewAllowRedirect"`); + } +} diff --git a/packages/backend/migration/1750729939704-FixAvatarUrl.js b/packages/backend/migration/1750729939704-FixAvatarUrl.js new file mode 100644 index 0000000000..81a598136b --- /dev/null +++ b/packages/backend/migration/1750729939704-FixAvatarUrl.js @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +export class FixAvatarUrl1750729939704 { + name = 'FixAvatarUrl1750729939704' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "avatarUrl" TYPE character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "avatarUrl" TYPE character varying(512)`); + } +} diff --git a/packages/backend/migration/1752502434151-no-action-on-draft-relation.js b/packages/backend/migration/1752502434151-no-action-on-draft-relation.js new file mode 100644 index 0000000000..e3c63b79c7 --- /dev/null +++ b/packages/backend/migration/1752502434151-no-action-on-draft-relation.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class NoActionOnDraftRelation1752502434151 { + name = 'NoActionOnDraftRelation1752502434151' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_USER_ID"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID"`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_e4983f28b4b18b03491536052f5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_e4983f28b4b18b03491536052f5"`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_NOTE_DRAFT_USER_ID" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1752509043847-migration-cleanup.js b/packages/backend/migration/1752509043847-migration-cleanup.js new file mode 100644 index 0000000000..450e22af0c --- /dev/null +++ b/packages/backend/migration/1752509043847-migration-cleanup.js @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MigrationCleanup1752509043847 { + name = 'MigrationCleanup1752509043847' + + async up(queryRunner) { + // 1745378064470-composite-note-index.js created a index ON "note" ("userId", "id" DESC) as IDX_724b311e6f883751f261ebe378 but should be named IDX_a6f649630f55af3888e5a42919 + await queryRunner.query(`ALTER INDEX "IDX_724b311e6f883751f261ebe378" RENAME TO "IDX_a6f649630f55af3888e5a42919"`); + + // 1713656541000-abuse-report-notification.js generated system_webhook with hand-written SQL with CURRENT_TIMESTAMP as the default value, but its representation in TypeORM is `now()` + // see https://github.com/typeorm/typeorm/blob/f351757a15b9d2bd9d4222c69dcfd2316f46b5d1/src/driver/postgres/PostgresDriver.ts#L1575 + await queryRunner.query(`ALTER TABLE "system_webhook" ALTER COLUMN "updatedAt" SET DEFAULT now()`); + + // 1702718871541-ffVisibility.js defined a enum type "user_profile_followersVisibility_enum" but it should be "user_profile_followersvisibility_enum" (lowercase 'v') in typeorm + await queryRunner.query(`ALTER TYPE "public"."user_profile_followersVisibility_enum" RENAME TO "user_profile_followersvisibility_enum"`); + + // 1713656541000-abuse-report-notification.js generated abuse_report_notification_recipient with hand-written SQL with CURRENT_TIMESTAMP as the default value, but its representation in TypeORM is `now()` + await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "updatedAt" SET DEFAULT now()`); + + // 1690796169261-play-visibility.js added visibility column to flash table but it forgot to set NOT NULL constraint + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "visibility" SET NOT NULL`); + + // 1736686850345-createNoteDraft.js created note_draft with hand-written SQL but several types and comments are not correctly defined + await queryRunner.query(`CREATE TYPE "public"."note_draft_visibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "id" SET DATA TYPE character varying(32)`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "replyId" SET DATA TYPE character varying(32)`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "renoteId" SET DATA TYPE character varying(32)`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "userId" SET DATA TYPE character varying(32)`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "channelId" SET DATA TYPE character varying(32)`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "fileIds" SET DATA TYPE character varying(32) array`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "visibleUserIds" SET DATA TYPE character varying(32) array`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "visibility" SET DATA TYPE "public"."note_draft_visibility_enum" USING visibility::note_draft_visibility_enum`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."replyId" IS 'The ID of reply target.'`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."renoteId" IS 'The ID of renote target.'`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."userId" IS 'The ID of author.'`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."channelId" IS 'The ID of source channel.'`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "fileIds" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "hasPoll" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "pollChoices" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "pollMultiple" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "localOnly" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "visibleUserIds" SET NOT NULL`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "visibleUserIds" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "localOnly" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "pollMultiple" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "pollChoices" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "hasPoll" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "fileIds" DROP NOT NULL`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."channelId" IS NULL`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."userId" IS 'The ID of author.'`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."renoteId" IS NULL`); + await queryRunner.query(`COMMENT ON COLUMN "note_draft"."replyId" IS NULL`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "visibility" SET DATA TYPE varchar`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "visibleUserIds" SET DATA TYPE varchar[]`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "fileIds" SET DATA TYPE varchar[]`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "channelId" SET DATA TYPE varchar`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "userId" SET DATA TYPE varchar`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "renoteId" SET DATA TYPE varchar`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "replyId" SET DATA TYPE varchar`); + await queryRunner.query(`ALTER TABLE "note_draft" ALTER COLUMN "id" SET DATA TYPE varchar`); + await queryRunner.query(`DROP TYPE "public"."note_draft_visibility_enum"`); + + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "visibility" DROP NOT NULL`); + + await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP`); + + await queryRunner.query(`ALTER TYPE "public"."user_profile_followersvisibility_enum" RENAME TO "user_profile_followersVisibility_enum"`); + + await queryRunner.query(`ALTER TABLE "system_webhook" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP`); + + await queryRunner.query(`ALTER INDEX "IDX_a6f649630f55af3888e5a42919" RENAME TO "IDX_724b311e6f883751f261ebe378"`); + } +} diff --git a/packages/backend/migration/1753863104203-remoteNotesCleaning.js b/packages/backend/migration/1753863104203-remoteNotesCleaning.js new file mode 100644 index 0000000000..8250d8d0df --- /dev/null +++ b/packages/backend/migration/1753863104203-remoteNotesCleaning.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoteNotesCleaning1753863104203 { + name = 'RemoteNotesCleaning1753863104203' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT false`); + await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningMaxProcessingDurationInMinutes" integer NOT NULL DEFAULT \'60\''); + await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningExpiryDaysForEachNotes" integer NOT NULL DEFAULT \'90\''); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningExpiryDaysForEachNotes"'); + await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningMaxProcessingDurationInMinutes"'); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableRemoteNotesCleaning"`); + } +} diff --git a/packages/backend/migration/1753868431598-remove_note_constraints.js b/packages/backend/migration/1753868431598-remove_note_constraints.js new file mode 100644 index 0000000000..29540cf9de --- /dev/null +++ b/packages/backend/migration/1753868431598-remove_note_constraints.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoveNoteConstraints1753868431598 { + name = 'RemoveNoteConstraints1753868431598' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`); + await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js b/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js new file mode 100644 index 0000000000..1051b18e1b --- /dev/null +++ b/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class TweakDefaultFederationSettings1754019326356 { + name = 'TweakDefaultFederationSettings1754019326356' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'none'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'all'`); + } +} diff --git a/packages/backend/migration/js/migration-config.js b/packages/backend/migration/js/migration-config.js index 8cfbb21470..853735661b 100644 --- a/packages/backend/migration/js/migration-config.js +++ b/packages/backend/migration/js/migration-config.js @@ -3,6 +3,29 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { path as configYamlPath } from '../../built/config.js'; +import * as yaml from 'js-yaml'; +import fs from "node:fs"; + export function isConcurrentIndexMigrationEnabled() { return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1'; } + +let loadedConfigCache = undefined; + +function loadConfigInternal() { + const config = yaml.load(fs.readFileSync(configYamlPath, 'utf-8')); + + return { + disallowExternalApRedirect: Boolean(config.disallowExternalApRedirect ?? false), + proxyRemoteFiles: Boolean(config.proxyRemoteFiles ?? false), + signToActivityPubGet: Boolean(config.signToActivityPubGet ?? true), + } +} + +export function loadConfig() { + if (loadedConfigCache === undefined) { + loadedConfigCache = loadConfigInternal(); + } + return loadedConfigCache; +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 36f7781908..13ec9a862d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": "^20.10.0 || ^22.0.0" + "node": "^22.15.0" }, "scripts": { "start": "node ./built/boot/entry.js", @@ -33,21 +33,22 @@ "test:fed": "pnpm jest:fed", "test-and-coverage": "pnpm jest-and-coverage", "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", + "check-migrations": "node scripts/check_migrations_clean.js", "generate-api-json": "node ./scripts/generate_api_json.js" }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.11.22", - "@swc/core-darwin-x64": "1.11.22", + "@swc/core-darwin-arm64": "1.13.3", + "@swc/core-darwin-x64": "1.13.3", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.11.22", - "@swc/core-linux-arm64-gnu": "1.11.22", - "@swc/core-linux-arm64-musl": "1.11.22", - "@swc/core-linux-x64-gnu": "1.11.22", - "@swc/core-linux-x64-musl": "1.11.22", - "@swc/core-win32-arm64-msvc": "1.11.22", - "@swc/core-win32-ia32-msvc": "1.11.22", - "@swc/core-win32-x64-msvc": "1.11.22", + "@swc/core-linux-arm-gnueabihf": "1.13.3", + "@swc/core-linux-arm64-gnu": "1.13.3", + "@swc/core-linux-arm64-musl": "1.13.3", + "@swc/core-linux-x64-gnu": "1.13.3", + "@swc/core-linux-x64-musl": "1.13.3", + "@swc/core-win32-arm64-msvc": "1.13.3", + "@swc/core-win32-ia32-msvc": "1.13.3", + "@swc/core-win32-x64-msvc": "1.13.3", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.0.9", @@ -67,32 +68,32 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.797.0", - "@aws-sdk/lib-storage": "3.797.0", - "@discordapp/twemoji": "15.1.0", + "@aws-sdk/client-s3": "3.864.0", + "@aws-sdk/lib-storage": "3.864.0", + "@discordapp/twemoji": "16.0.1", "@fastify/accepts": "5.0.2", "@fastify/cookie": "11.0.2", "@fastify/cors": "10.1.0", "@fastify/express": "4.0.2", "@fastify/http-proxy": "10.0.2", "@fastify/multipart": "9.0.3", - "@fastify/static": "8.1.1", + "@fastify/static": "8.2.0", "@fastify/view": "10.0.2", "@misskey-dev/sharp-read-bmp": "1.2.0", - "@misskey-dev/summaly": "5.2.1", - "@napi-rs/canvas": "0.1.69", - "@nestjs/common": "11.1.0", - "@nestjs/core": "11.1.0", - "@nestjs/testing": "11.1.0", + "@misskey-dev/summaly": "5.2.3", + "@napi-rs/canvas": "0.1.77", + "@nestjs/common": "11.1.6", + "@nestjs/core": "11.1.6", + "@nestjs/testing": "11.1.6", "@peertube/http-signature": "1.7.0", "@sentry/node": "8.55.0", "@sentry/profiling-node": "8.55.0", "@simplewebauthn/server": "12.0.0", "@sinonjs/fake-timers": "11.3.1", "@smithy/node-http-handler": "2.5.0", - "@swc/cli": "0.7.3", - "@swc/core": "1.11.22", - "@twemoji/parser": "15.1.1", + "@swc/cli": "0.7.8", + "@swc/core": "1.13.3", + "@twemoji/parser": "16.0.0", "@types/redis-info": "3.0.3", "accepts": "1.3.8", "ajv": "8.17.1", @@ -101,29 +102,29 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.51.1", + "bullmq": "5.56.9", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", - "chalk": "5.4.1", + "chalk": "5.5.0", "chalk-template": "1.1.0", - "chokidar": "3.6.0", + "chokidar": "4.0.3", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "5.3.2", + "fastify": "5.4.0", "fastify-raw-body": "5.0.0", "feed": "4.2.2", "file-type": "19.6.0", "fluent-ffmpeg": "2.1.3", - "form-data": "4.0.2", + "form-data": "4.0.4", "got": "14.4.7", "happy-dom": "16.8.1", "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", - "ioredis": "5.6.1", + "ioredis": "5.7.0", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", @@ -133,9 +134,9 @@ "jsonld": "8.3.3", "jsrsasign": "11.1.0", "juice": "11.0.1", - "meilisearch": "0.50.0", - "mfm-js": "0.24.0", - "microformats-parser": "2.0.2", + "meilisearch": "0.51.0", + "mfm-js": "0.25.0", + "microformats-parser": "2.0.4", "mime-types": "2.1.35", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", @@ -151,7 +152,7 @@ "os-utils": "0.0.14", "otpauth": "9.4.0", "parse5": "7.3.0", - "pg": "8.15.6", + "pg": "8.16.3", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -159,45 +160,45 @@ "qrcode": "1.5.4", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.21.4", + "re2": "1.22.1", "redis-info": "3.1.0", "redis-lock": "0.1.4", "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.2", - "sanitize-html": "2.16.0", + "sanitize-html": "2.17.0", "secure-json-parse": "3.0.2", "sharp": "0.33.5", - "semver": "7.7.1", + "semver": "7.7.2", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.25.11", + "systeminformation": "5.27.7", "tinycolor2": "1.6.0", "tmp": "0.2.3", - "tsc-alias": "1.8.15", + "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.22", - "typescript": "5.8.3", + "typeorm": "0.3.25", + "typescript": "5.9.2", "ulid": "2.4.0", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.18.1", + "ws": "8.18.3", "xev": "3.0.2" }, "devDependencies": { "@jest/globals": "29.7.0", - "@nestjs/platform-express": "10.4.17", - "@sentry/vue": "9.14.0", + "@nestjs/platform-express": "10.4.20", + "@sentry/vue": "9.45.0", "@simplewebauthn/types": "12.0.0", - "@swc/jest": "0.2.38", + "@swc/jest": "0.2.39", "@types/accepts": "1.3.7", "@types/archiver": "6.0.3", "@types/bcryptjs": "2.4.6", - "@types/body-parser": "1.19.5", + "@types/body-parser": "1.19.6", "@types/color-convert": "2.0.4", - "@types/content-disposition": "0.5.8", + "@types/content-disposition": "0.5.9", "@types/fluent-ffmpeg": "2.1.27", "@types/htmlescape": "1.1.3", "@types/http-link-header": "1.0.7", @@ -208,18 +209,18 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "22.15.2", + "@types/node": "22.17.1", "@types/nodemailer": "6.4.17", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.14", + "@types/pg": "8.15.5", "@types/pug": "2.0.10", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.15.0", + "@types/sanitize-html": "2.16.0", "@types/semver": "7.7.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", @@ -229,11 +230,11 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.31.0", - "@typescript-eslint/parser": "8.31.0", + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", "aws-sdk-client-mock": "4.1.0", "cross-env": "7.0.3", - "eslint-plugin-import": "2.31.0", + "eslint-plugin-import": "2.32.0", "execa": "8.0.1", "fkill": "9.0.0", "jest": "29.7.0", @@ -241,6 +242,6 @@ "nodemon": "3.1.10", "pid-port": "1.0.2", "simple-oauth2": "5.1.0", - "supertest": "7.1.0" + "supertest": "7.1.4" } } diff --git a/packages/backend/scripts/check_migrations_clean.js b/packages/backend/scripts/check_migrations_clean.js new file mode 100644 index 0000000000..ce67b1cd81 --- /dev/null +++ b/packages/backend/scripts/check_migrations_clean.js @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// This script checks if the database migrations has been generated correctly. + +import dataSource from '../ormconfig.js'; + +await dataSource.initialize(); + +const sqlInMemory = await dataSource.driver.createSchemaBuilder().log(); + +if (sqlInMemory.upQueries.length > 0 || sqlInMemory.downQueries.length > 0) { + console.error('There are several pending migrations. Please make sure you have generated the migrations correctly, or configured entities class correctly.'); + for (const query of sqlInMemory.upQueries) { + console.error(`- ${query.query}`); + } + for (const query of sqlInMemory.downQueries) { + console.error(`- ${query.query}`); + } + process.exit(1); +} else { + console.log('All migrations are clean.'); + process.exit(0); +} diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs index a3e0558abd..023eb7eae6 100644 --- a/packages/backend/scripts/dev.mjs +++ b/packages/backend/scripts/dev.mjs @@ -42,7 +42,7 @@ async function killProc() { './node_modules/nodemon/bin/nodemon.js', [ '-w', 'src', - '-e', 'ts,js,mjs,cjs,json', + '-e', 'ts,js,mjs,cjs,json,pug', '--exec', 'pnpm', 'run', 'build', ], { diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 646fa07911..f71f1d7e34 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -79,7 +79,6 @@ type Source = { proxyBypassHosts?: string[]; allowedPrivateNetworks?: string[]; - disallowExternalApRedirect?: boolean; maxFileSize?: number; @@ -100,11 +99,8 @@ type Source = { inboxJobMaxAttempts?: number; mediaProxy?: string; - proxyRemoteFiles?: boolean; videoThumbnailGenerator?: string; - signToActivityPubGet?: boolean; - perChannelMaxNoteCacheCount?: number; perUserNotificationsMaxCount?: number; deactivateAntennaThreshold?: number; @@ -156,7 +152,6 @@ export type Config = { proxySmtp: string | undefined; proxyBypassHosts: string[] | undefined; allowedPrivateNetworks: string[] | undefined; - disallowExternalApRedirect: boolean; maxFileSize: number; clusterLimit: number | undefined; id: string; @@ -170,8 +165,6 @@ export type Config = { relationshipJobPerSec: number | undefined; deliverJobMaxAttempts: number | undefined; inboxJobMaxAttempts: number | undefined; - proxyRemoteFiles: boolean | undefined; - signToActivityPubGet: boolean | undefined; logging?: { sql?: { disableQueryTruncation?: boolean, @@ -191,9 +184,9 @@ export type Config = { authUrl: string; driveUrl: string; userAgent: string; - frontendEntry: string; + frontendEntry: { file: string | null }; frontendManifestExists: boolean; - frontendEmbedEntry: string; + frontendEmbedEntry: { file: string | null }; frontendEmbedManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; @@ -229,7 +222,7 @@ const dir = `${_dirname}/../../../.config`; /** * Path of configuration file */ -const path = process.env.MISSKEY_CONFIG_YML +export const path = process.env.MISSKEY_CONFIG_YML ? resolve(dir, process.env.MISSKEY_CONFIG_YML) : process.env.NODE_ENV === 'test' ? resolve(dir, 'test.yml') @@ -242,10 +235,10 @@ export function loadConfig(): Config { const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json'); const frontendManifest = frontendManifestExists ? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8')) - : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; + : { 'src/_boot_.ts': { file: null } }; const frontendEmbedManifest = frontendEmbedManifestExists ? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8')) - : { 'src/boot.ts': { file: 'src/boot.ts' } }; + : { 'src/boot.ts': { file: null } }; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; @@ -300,7 +293,6 @@ export function loadConfig(): Config { proxySmtp: config.proxySmtp, proxyBypassHosts: config.proxyBypassHosts, allowedPrivateNetworks: config.allowedPrivateNetworks, - disallowExternalApRedirect: config.disallowExternalApRedirect ?? false, maxFileSize: config.maxFileSize ?? 262144000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, @@ -313,8 +305,6 @@ export function loadConfig(): Config { relationshipJobPerSec: config.relationshipJobPerSec, deliverJobMaxAttempts: config.deliverJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts, - proxyRemoteFiles: config.proxyRemoteFiles, - signToActivityPubGet: config.signToActivityPubGet ?? true, mediaProxy: externalMediaProxy ?? internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, videoThumbnailGenerator: config.videoThumbnailGenerator ? diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 2ceff341cc..5cd336a097 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -29,7 +29,7 @@ import { emojiRegex } from '@/misc/emoji-regex.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -const MAX_ROOM_MEMBERS = 30; +const MAX_ROOM_MEMBERS = 50; const MAX_REACTIONS_PER_MESSAGE = 100; const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; @@ -331,6 +331,16 @@ export class ChatService { await redisPipeline.exec(); } + @bindThis + public async readAllChatMessages( + readerId: MiUser['id'], + ): Promise { + const redisPipeline = this.redisClient.pipeline(); + // TODO: newUserChatMessageExists とか newRoomChatMessageExists も消したい(けどキーの列挙が必要になって面倒) + redisPipeline.del(`newChatMessagesExists:${readerId}`); + await redisPipeline.exec(); + } + @bindThis public findMessageById(messageId: MiChatMessage['id']) { return this.chatMessagesRepository.findOneBy({ id: messageId }); diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index d8617e343c..0c0c5d3a39 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -44,6 +44,7 @@ import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; +import { NoteDraftService } from './NoteDraftService.js'; import { NotificationService } from './NotificationService.js'; import { PollService } from './PollService.js'; import { PushNotificationService } from './PushNotificationService.js'; @@ -118,6 +119,7 @@ import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService. import { NoteEntityService } from './entities/NoteEntityService.js'; import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js'; import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js'; +import { NoteDraftEntityService } from './entities/NoteDraftEntityService.js'; import { NotificationEntityService } from './entities/NotificationEntityService.js'; import { PageEntityService } from './entities/PageEntityService.js'; import { PageLikeEntityService } from './entities/PageLikeEntityService.js'; @@ -185,6 +187,7 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; +const $NoteDraftService: Provider = { provide: 'NoteDraftService', useExisting: NoteDraftService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService }; @@ -266,6 +269,7 @@ const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityServi const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService }; const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService }; const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService }; +const $NoteDraftEntityService: Provider = { provide: 'NoteDraftEntityService', useExisting: NoteDraftEntityService }; const $NotificationEntityService: Provider = { provide: 'NotificationEntityService', useExisting: NotificationEntityService }; const $PageEntityService: Provider = { provide: 'PageEntityService', useExisting: PageEntityService }; const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', useExisting: PageLikeEntityService }; @@ -335,6 +339,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteDeleteService, NotePiningService, + NoteDraftService, NotificationService, PollService, SystemAccountService, @@ -416,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteEntityService, NoteFavoriteEntityService, NoteReactionEntityService, + NoteDraftEntityService, NotificationEntityService, PageEntityService, PageLikeEntityService, @@ -481,6 +487,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteDeleteService, $NotePiningService, + $NoteDraftService, $NotificationService, $PollService, $SystemAccountService, @@ -562,6 +569,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteEntityService, $NoteFavoriteEntityService, $NoteReactionEntityService, + $NoteDraftEntityService, $NotificationEntityService, $PageEntityService, $PageLikeEntityService, @@ -628,6 +636,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteDeleteService, NotePiningService, + NoteDraftService, NotificationService, PollService, SystemAccountService, @@ -708,6 +717,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteEntityService, NoteFavoriteEntityService, NoteReactionEntityService, + NoteDraftEntityService, NotificationEntityService, PageEntityService, PageLikeEntityService, @@ -773,6 +783,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteDeleteService, $NotePiningService, + $NoteDraftService, $NotificationService, $PollService, $SystemAccountService, @@ -852,6 +863,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteEntityService, $NoteFavoriteEntityService, $NoteReactionEntityService, + $NoteDraftEntityService, $NotificationEntityService, $PageEntityService, $PageLikeEntityService, diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 5f1e373429..567bad2a2d 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -8,7 +8,7 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import sharp from 'sharp'; import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; -import { IsNull } from 'typeorm'; +import { In, IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; @@ -469,13 +469,14 @@ export class DriveService { if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; const info = await this.fileInfoService.getFileInfo(path, { + fileName: name, skipSensitiveDetection: skipNsfwCheck, sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる - this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : - this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : - this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : - this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : - 0.5, + this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : + this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : + this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : + this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : + 0.5, sensitiveThresholdForPorn: 0.75, enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos, }); @@ -515,16 +516,23 @@ export class DriveService { this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`); - //#region Check drive usage + //#region Check drive usage and mime type if (user && !isLink) { - const usage = await this.driveFileEntityService.calcDriveUsageOf(user); const isLocalUser = this.userEntityService.isLocalUser(user); - const policies = await this.roleService.getUserPolicies(user.id); + + const allowedMimeTypes = policies.uploadableFileTypes; + const isAllowed = allowedMimeTypes.some((mimeType) => { + if (mimeType === '*' || mimeType === '*/*') return true; + if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1)); + return info.type.mime === mimeType; + }); + if (!isAllowed) { + throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', `Unallowed file type: ${info.type.mime}`); + } + const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb; - this.registerLogger.debug('drive capacity override applied'); - this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); if (maxFileSize < info.size) { if (isLocalUser) { @@ -532,6 +540,11 @@ export class DriveService { } } + const usage = await this.driveFileEntityService.calcDriveUsageOf(user); + + this.registerLogger.debug('drive capacity override applied'); + this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + // If usage limit exceeded if (driveCapacity < usage + info.size) { if (isLocalUser) { @@ -720,6 +733,21 @@ export class DriveService { return fileObj; } + @bindThis + public async moveFiles(fileIds: MiDriveFile['id'][], folderId: MiDriveFolder['id'] | null, userId: MiUser['id']) { + const folder = folderId ? await this.driveFoldersRepository.findOneByOrFail({ + id: folderId, + userId: userId, + }) : null; + + await this.driveFilesRepository.update({ + id: In(fileIds), + userId: userId, + }, { + folderId: folder ? folder.id : null, + }); + } + @bindThis public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) { if (file.storedInternal) { @@ -775,14 +803,14 @@ export class DriveService { await Promise.all(promises); } - this.deletePostProcess(file, isExpired, deleter); + await this.deletePostProcess(file, isExpired, deleter); } @bindThis private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) { // リモートファイル期限切れ削除後は直リンクにする if (isExpired && file.userHost !== null && file.uri != null) { - this.driveFilesRepository.update(file.id, { + await this.driveFilesRepository.update(file.id, { isLink: true, url: file.uri, thumbnailUrl: null, @@ -794,7 +822,7 @@ export class DriveService { webpublicAccessKey: 'webpublic-' + randomUUID(), }); } else { - this.driveFilesRepository.delete(file.id); + await this.driveFilesRepository.delete(file.id); } this.driveChart.update(file, false); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 45d7ea11e4..c7be0f7843 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -145,7 +145,10 @@ export class EmailService { try { // TODO: htmlサニタイズ const info = await transporter.sendMail({ - from: this.meta.email!, + from: this.meta.name ? { + name: this.meta.name, + address: this.meta.email!, + } : this.meta.email!, to: to, subject: subject, text: text, diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 6253f792ed..94c5691bf4 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -20,6 +20,8 @@ import { CacheService } from '@/core/CacheService.js'; import { isReply } from '@/misc/is-reply.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; +type NoteFilter = (note: MiNote) => boolean; + type TimelineOptions = { untilId: string | null, sinceId: string | null, @@ -28,7 +30,7 @@ type TimelineOptions = { me?: { id: MiUser['id'] } | undefined | null, useDbFallback: boolean, redisTimelines: FanoutTimelineName[], - noteFilter?: (note: MiNote) => boolean, + noteFilter?: NoteFilter, alwaysIncludeMyNotes?: boolean; ignoreAuthorFromBlock?: boolean; ignoreAuthorFromMute?: boolean; @@ -79,7 +81,7 @@ export class FanoutTimelineEndpointService { const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId; if (!shouldFallbackToDb) { - let filter = ps.noteFilter ?? (_note => true); + let filter = ps.noteFilter ?? (_note => true) as NoteFilter; if (ps.alwaysIncludeMyNotes && ps.me) { const me = ps.me; @@ -120,6 +122,8 @@ export class FanoutTimelineEndpointService { filter = (note) => { if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; + if (isUserRelated(note.renote, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; + if (isUserRelated(note.renote, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false; if (isInstanceMuted(note, userMutedInstances)) return false; @@ -143,15 +147,11 @@ export class FanoutTimelineEndpointService { { const parentFilter = filter; filter = (note) => { - const noteJoined = note as MiNote & { - renoteUser: MiUser | null; - replyUser: MiUser | null; - }; if (!ps.ignoreAuthorFromUserSuspension) { if (note.user!.isSuspended) return false; } - if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false; - if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false; + if (note.userId !== note.renoteUserId && note.renote?.user?.isSuspended) return false; + if (note.userId !== note.replyUserId && note.reply?.user?.isSuspended) return false; return parentFilter(note); }; @@ -198,7 +198,7 @@ export class FanoutTimelineEndpointService { return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); } - private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise { + private async getAndFilterFromDb(noteIds: string[], noteFilter: NoteFilter, idCompare: (a: string, b: string) => number): Promise { const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index a295e81920..6250d4d3a1 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -64,6 +64,7 @@ export class FileInfoService { */ @bindThis public async getFileInfo(path: string, opts: { + fileName?: string | null; skipSensitiveDetection: boolean; sensitiveThreshold?: number; sensitiveThresholdForPorn?: number; @@ -76,6 +77,26 @@ export class FileInfoService { let type = await this.detectType(path); + if (type.mime === TYPE_OCTET_STREAM.mime && opts.fileName != null) { + const ext = opts.fileName.split('.').pop(); + if (ext === 'txt') { + type = { + mime: 'text/plain', + ext: 'txt', + }; + } else if (ext === 'csv') { + type = { + mime: 'text/csv', + ext: 'csv', + }; + } else if (ext === 'json') { + type = { + mime: 'application/json', + ext: 'json', + }; + } + } + // image dimensions let width: number | undefined; let height: number | undefined; @@ -438,12 +459,12 @@ export class FileInfoService { */ @bindThis private async detectImageSize(path: string): Promise<{ - width: number; - height: number; - wUnits: string; - hUnits: string; - orientation?: number; -}> { + width: number; + height: number; + wUnits: string; + hUnits: string; + orientation?: number; + }> { const readable = fs.createReadStream(path); const imageSize = await probeImageSize(readable); readable.destroy(); diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts index 2a98225382..8caffe9e45 100644 --- a/packages/backend/src/core/FlashService.ts +++ b/packages/backend/src/core/FlashService.ts @@ -4,8 +4,11 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { type FlashsRepository } from '@/models/_.js'; +import { type FlashLikesRepository, MiUser, type FlashsRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; /** * MisskeyPlay関係のService @@ -15,6 +18,11 @@ export class FlashService { constructor( @Inject(DI.flashsRepository) private flashRepository: FlashsRepository, + + @Inject(DI.flashLikesRepository) + private flashLikesRepository: FlashLikesRepository, + + private queryService: QueryService, ) { } @@ -37,4 +45,43 @@ export class FlashService { return await builder.getMany(); } + + public async myLikes(meId: MiUser['id'], opts: { sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number, limit?: number, search?: string | null }) { + const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), opts.sinceId, opts.untilId, opts.sinceDate, opts.untilDate) + .andWhere('like.userId = :meId', { meId }) + .leftJoinAndSelect('like.flash', 'flash'); + + if (opts.search != null) { + for (const word of opts.search.trim().split(' ')) { + query.andWhere(new Brackets(qb => { + qb.orWhere('flash.title ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); + qb.orWhere('flash.summary ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); + })); + } + } + + const likes = await query + .limit(opts.limit) + .getMany(); + + return likes; + } + + public async search(searchQuery: string, opts: { sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number, limit?: number }) { + const query = this.queryService.makePaginationQuery(this.flashRepository.createQueryBuilder('flash'), opts.sinceId, opts.untilId, opts.sinceDate, opts.untilDate) + .andWhere('flash.visibility = \'public\''); + + for (const word of searchQuery.trim().split(' ')) { + query.andWhere(new Brackets(qb => { + qb.orWhere('flash.title ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); + qb.orWhere('flash.summary ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); + })); + } + + const result = await query + .limit(opts.limit) + .getMany(); + + return result; + } } diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 3ddfe52045..f7973cbb66 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -6,6 +6,7 @@ import * as http from 'node:http'; import * as https from 'node:https'; import * as net from 'node:net'; +import * as stream from 'node:stream'; import ipaddr from 'ipaddr.js'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; @@ -26,12 +27,6 @@ export type HttpRequestSendOptions = { validators?: ((res: Response) => void)[]; }; -declare module 'node:http' { - interface Agent { - createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket; - } -} - class HttpRequestServiceAgent extends http.Agent { constructor( private config: Config, @@ -41,11 +36,11 @@ class HttpRequestServiceAgent extends http.Agent { } @bindThis - public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { + public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { const socket = super.createConnection(options, callback) .on('connect', () => { - const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { + if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { + const address = socket.remoteAddress; if (address && ipaddr.isValid(address)) { if (this.isPrivateIp(address)) { socket.destroy(new Error(`Blocked address: ${address}`)); @@ -80,11 +75,11 @@ class HttpsRequestServiceAgent extends https.Agent { } @bindThis - public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { + public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { const socket = super.createConnection(options, callback) .on('connect', () => { - const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { + if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { + const address = socket.remoteAddress; if (address && ipaddr.isValid(address)) { if (this.isPrivateIp(address)) { socket.destroy(new Error(`Blocked address: ${address}`)); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 469426f87e..1eefcfa054 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -421,7 +421,7 @@ export class NoteCreateService implements OnApplicationShutdown { emojis, userId: user.id, localOnly: data.localOnly!, - reactionAcceptance: data.reactionAcceptance, + reactionAcceptance: data.reactionAcceptance ?? null, visibility: data.visibility as any, visibleUserIds: data.visibility === 'specified' ? data.visibleUsers @@ -483,7 +483,11 @@ export class NoteCreateService implements OnApplicationShutdown { await this.notesRepository.insert(insert); } - return insert; + return { + ...insert, + reply: data.reply ?? null, + renote: data.renote ?? null, + }; } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index e394506a44..af1f0eda9a 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -62,7 +62,6 @@ export class NoteDeleteService { */ async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { const deletedAt = new Date(); - const cascadingNotes = await this.findCascadingNotes(note); if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); @@ -90,15 +89,6 @@ export class NoteDeleteService { this.deliverToConcerned(user, note, content); } - - // also deliver delete activity to cascaded notes - const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes - for (const cascadingNote of federatedLocalCascadingNotes) { - if (!cascadingNote.user) continue; - if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; - const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); - this.deliverToConcerned(cascadingNote.user, cascadingNote, content); - } //#endregion this.notesChart.update(note, false); @@ -118,9 +108,6 @@ export class NoteDeleteService { } } - for (const cascadingNote of cascadingNotes) { - this.searchService.unindexNote(cascadingNote); - } this.searchService.unindexNote(note); await this.notesRepository.delete({ @@ -140,29 +127,6 @@ export class NoteDeleteService { } } - @bindThis - private async findCascadingNotes(note: MiNote): Promise { - const recursive = async (noteId: string): Promise => { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.replyId = :noteId', { noteId }) - .orWhere(new Brackets(q => { - q.where('note.renoteId = :noteId', { noteId }) - .andWhere('note.text IS NOT NULL'); - })) - .leftJoinAndSelect('note.user', 'user'); - const replies = await query.getMany(); - - return [ - replies, - ...await Promise.all(replies.map(reply => recursive(reply.id))), - ].flat(); - }; - - const cascadingNotes: MiNote[] = await recursive(note.id); - - return cascadingNotes; - } - @bindThis private async getMentionedRemoteUsers(note: MiNote) { const where = [] as any[]; diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts new file mode 100644 index 0000000000..c43be96efa --- /dev/null +++ b/packages/backend/src/core/NoteDraftService.ts @@ -0,0 +1,314 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import type { noteVisibilities, noteReactionAcceptances } from '@/types.js'; +import { DI } from '@/di-symbols.js'; +import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import { IPoll } from '@/models/Poll.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { isRenote, isQuote } from '@/misc/is-renote.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; + +export type NoteDraftOptions = { + replyId?: MiNote['id'] | null; + renoteId?: MiNote['id'] | null; + text?: string | null; + cw?: string | null; + localOnly?: boolean | null; + reactionAcceptance?: typeof noteReactionAcceptances[number]; + visibility?: typeof noteVisibilities[number]; + fileIds?: MiDriveFile['id'][]; + visibleUserIds?: MiUser['id'][]; + hashtag?: string; + channelId?: MiChannel['id'] | null; + poll?: (IPoll & { expiredAfter?: number | null }) | null; +}; + +@Injectable() +export class NoteDraftService { + constructor( + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.noteDraftsRepository) + private noteDraftsRepository: NoteDraftsRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private roleService: RoleService, + private idService: IdService, + private noteEntityService: NoteEntityService, + ) { + } + + @bindThis + public async get(me: MiLocalUser, draftId: MiNoteDraft['id']): Promise { + const draft = await this.noteDraftsRepository.findOneBy({ + id: draftId, + userId: me.id, + }); + + return draft; + } + + @bindThis + public async create(me: MiLocalUser, data: NoteDraftOptions): Promise { + //#region check draft limit + + const currentCount = await this.noteDraftsRepository.countBy({ + userId: me.id, + }); + if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) { + throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts'); + } + //#endregion + + if (data.poll) { + if (typeof data.poll.expiresAt === 'number') { + if (data.poll.expiresAt < Date.now()) { + throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); + } + } else if (typeof data.poll.expiredAfter === 'number') { + data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter); + } + } + + const appliedDraft = await this.checkAndSetDraftNoteOptions(me, this.noteDraftsRepository.create(), data); + + appliedDraft.id = this.idService.gen(); + appliedDraft.userId = me.id; + const draft = this.noteDraftsRepository.save(appliedDraft); + + return draft; + } + + @bindThis + public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise { + const draft = await this.noteDraftsRepository.findOneBy({ + id: draftId, + userId: me.id, + }); + + if (draft == null) { + throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft'); + } + + if (data.poll) { + if (typeof data.poll.expiresAt === 'number') { + if (data.poll.expiresAt < Date.now()) { + throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); + } + } else if (typeof data.poll.expiredAfter === 'number') { + data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter); + } + } + + const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data); + + return await this.noteDraftsRepository.save(appliedDraft); + } + + @bindThis + public async delete(me: MiLocalUser, draftId: MiNoteDraft['id']): Promise { + const draft = await this.noteDraftsRepository.findOneBy({ + id: draftId, + userId: me.id, + }); + + if (draft == null) { + throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft'); + } + + await this.noteDraftsRepository.delete(draft.id); + } + + @bindThis + public async getDraft(me: MiLocalUser, draftId: MiNoteDraft['id']): Promise { + const draft = await this.noteDraftsRepository.findOneBy({ + id: draftId, + userId: me.id, + }); + + if (draft == null) { + throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft'); + } + + return draft; + } + + // 関連エンティティを取得し紐づける部分を共通化する + @bindThis + public async checkAndSetDraftNoteOptions( + me: MiLocalUser, + draft: MiNoteDraft, + data: NoteDraftOptions, + ): Promise { + data.visibility ??= 'public'; + data.localOnly ??= false; + if (data.reactionAcceptance === undefined) data.reactionAcceptance = null; + if (data.channelId != null) { + data.visibility = 'public'; + data.visibleUserIds = []; + data.localOnly = true; + } + + let appliedDraft = draft; + + //#region visibleUsers + let visibleUsers: MiUser[] = []; + if (data.visibleUserIds != null) { + visibleUsers = await this.usersRepository.findBy({ + id: In(data.visibleUserIds), + }); + } + //#endregion + + //#region files + let files: MiDriveFile[] = []; + const fileIds = data.fileIds ?? null; + if (fileIds != null) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: me.id, + fileIds: fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + + if (files.length !== fileIds.length) { + throw new IdentifiableError('b6992544-63e7-67f0-fa7f-32444b1b5306', 'No such drive file'); + } + } + //#endregion + + //#region renote + let renote: MiNote | null = null; + if (data.renoteId != null) { + renote = await this.notesRepository.findOneBy({ id: data.renoteId }); + + if (renote == null) { + throw new IdentifiableError('64929870-2540-4d11-af41-3b484d78c956', 'No such renote'); + } else if (isRenote(renote) && !isQuote(renote)) { + throw new IdentifiableError('76cc5583-5a14-4ad3-8717-0298507e32db', 'Cannot renote'); + } + + // Check blocking + if (renote.userId !== me.id) { + const blockExist = await this.blockingsRepository.exists({ + where: { + blockerId: renote.userId, + blockeeId: me.id, + }, + }); + if (blockExist) { + throw new IdentifiableError('075ca298-e6e7-485a-b570-51a128bb5168', 'You have been blocked by the user'); + } + } + + if (renote.visibility === 'followers' && renote.userId !== me.id) { + // 他人のfollowers noteはreject + throw new IdentifiableError('81eb8188-aea1-4e35-9a8f-3334a3be9855', 'Cannot Renote Due to Visibility'); + } else if (renote.visibility === 'specified') { + // specified / direct noteはreject + throw new IdentifiableError('81eb8188-aea1-4e35-9a8f-3334a3be9855', 'Cannot Renote Due to Visibility'); + } + + if (renote.channelId && renote.channelId !== data.channelId) { + // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック + // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する + const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); + if (renoteChannel == null) { + // リノートしたいノートが書き込まれているチャンネルがない + throw new IdentifiableError('6815399a-6f13-4069-b60d-ed5156249d12', 'No such channel'); + } else if (!renoteChannel.allowRenoteToExternal) { + // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 + throw new IdentifiableError('ed1952ac-2d26-4957-8b30-2deda76bedf7', 'Cannot Renote to External'); + } + } + } + //#endregion + + //#region reply + let reply: MiNote | null = null; + if (data.replyId != null) { + // Fetch reply + reply = await this.notesRepository.findOneBy({ id: data.replyId }); + + if (reply == null) { + throw new IdentifiableError('c4721841-22fc-4bb7-ad3d-897ef1d375b5', 'No such reply'); + } else if (isRenote(reply) && !isQuote(reply)) { + throw new IdentifiableError('e6c10b57-2c09-4da3-bd4d-eda05d51d140', 'Cannot reply To Pure Renote'); + } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { + throw new IdentifiableError('593c323c-6b6a-4501-a25c-2f36bd2a93d6', 'Cannot reply To Invisible Note'); + } else if (reply.visibility === 'specified' && data.visibility !== 'specified') { + throw new IdentifiableError('215dbc76-336c-4d2a-9605-95766ba7dab0', 'Cannot reply To Specified Note With Extended Visibility'); + } + + // Check blocking + if (reply.userId !== me.id) { + const blockExist = await this.blockingsRepository.exists({ + where: { + blockerId: reply.userId, + blockeeId: me.id, + }, + }); + if (blockExist) { + throw new IdentifiableError('075ca298-e6e7-485a-b570-51a128bb5168', 'You have been blocked by the user'); + } + } + } + //#endregion + + //#region channel + let channel: MiChannel | null = null; + if (data.channelId != null) { + channel = await this.channelsRepository.findOneBy({ id: data.channelId, isArchived: false }); + + if (channel == null) { + throw new IdentifiableError('6815399a-6f13-4069-b60d-ed5156249d12', 'No such channel'); + } + } + //#endregion + + appliedDraft = { + ...appliedDraft, + visibility: data.visibility, + cw: data.cw ?? null, + fileIds: fileIds ?? [], + replyId: data.replyId ?? null, + renoteId: data.renoteId ?? null, + channelId: data.channelId ?? null, + text: data.text ?? null, + hashtag: data.hashtag ?? null, + hasPoll: data.poll != null, + pollChoices: data.poll ? data.poll.choices : [], + pollMultiple: data.poll ? data.poll.multiple : false, + pollExpiresAt: data.poll ? data.poll.expiresAt : null, + pollExpiredAfter: data.poll ? data.poll.expiredAfter ?? null : null, + visibleUserIds: data.visibleUserIds ?? [], + localOnly: data.localOnly, + reactionAcceptance: data.reactionAcceptance, + } satisfies MiNoteDraft; + + return appliedDraft; + } +} diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index b9cef5b0ec..49f93ad108 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -77,9 +77,51 @@ export class QueryService { return q; } + /** + * ミュートやブロックのようにすべてのタイムラインで共通に使用するフィルターを定義します。 + * + * 特別な事情がない限り、各タイムラインはこの関数を呼び出してフィルターを適用してください。 + * + * Notes for future maintainers: + * 1) この関数で生成するクエリと同等の処理が FanoutTimelineEndpointService にあります。 + * この関数を変更した場合、FanoutTimelineEndpointService の方も変更する必要があります。 + * 2) 以下のエンドポイントでは特別な事情があるため queryService のそれぞれの関数を呼び出しています。 + * この関数を変更した場合、以下のエンドポイントの方も変更する必要があることがあります。 + * - packages/backend/src/server/api/endpoints/clips/notes.ts + */ + @bindThis + public generateBaseNoteFilteringQuery( + query: SelectQueryBuilder, + me: { id: MiUser['id'] } | null, + { + excludeUserFromMute, + excludeAuthor, + }: { + excludeUserFromMute?: MiUser['id'], + excludeAuthor?: boolean, + } = {}, + ): void { + this.generateBlockedHostQueryForNote(query, excludeAuthor); + this.generateSuspendedUserQueryForNote(query, excludeAuthor); + if (me) { + this.generateMutedUserQueryForNotes(query, me, { excludeUserFromMute }); + this.generateBlockedUserQueryForNotes(query, me); + this.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote', excludeUserFromMute }); + this.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' }); + } + } + // ここでいうBlockedは被Blockedの意 @bindThis - public generateBlockedUserQueryForNotes(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { + public generateBlockedUserQueryForNotes( + q: SelectQueryBuilder, + me: { id: MiUser['id'] }, + { + noteColumn = 'note', + }: { + noteColumn?: string, + } = {}, + ): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockerId') .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); @@ -88,16 +130,20 @@ export class QueryService { // 投稿の返信先の作者にブロックされていない かつ // 投稿の引用元の作者にブロックされていない q - .andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) .andWhere(new Brackets(qb => { qb - .where('note.replyUserId IS NULL') - .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); + .where(`${noteColumn}.userId IS NULL`) + .orWhere(`${noteColumn}.userId NOT IN (${ blockingQuery.getQuery() })`); })) .andWhere(new Brackets(qb => { qb - .where('note.renoteUserId IS NULL') - .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); + .where(`${noteColumn}.replyUserId IS NULL`) + .orWhere(`${noteColumn}.replyUserId NOT IN (${ blockingQuery.getQuery() })`); + })) + .andWhere(new Brackets(qb => { + qb + .where(`${noteColumn}.renoteUserId IS NULL`) + .orWhere(`${noteColumn}.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); })); q.setParameters(blockingQuery.getParameters()); @@ -137,13 +183,23 @@ export class QueryService { } @bindThis - public generateMutedUserQueryForNotes(q: SelectQueryBuilder, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void { + public generateMutedUserQueryForNotes( + q: SelectQueryBuilder, + me: { id: MiUser['id'] }, + { + excludeUserFromMute, + noteColumn = 'note', + }: { + excludeUserFromMute?: MiUser['id'], + noteColumn?: string, + } = {}, + ): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); - if (exclude) { - mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); + if (excludeUserFromMute) { + mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: excludeUserFromMute }); } const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') @@ -154,32 +210,36 @@ export class QueryService { // 投稿の返信先の作者をミュートしていない かつ // 投稿の引用元の作者をミュートしていない q - .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) .andWhere(new Brackets(qb => { qb - .where('note.replyUserId IS NULL') - .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); + .where(`${noteColumn}.userId IS NULL`) + .orWhere(`${noteColumn}.userId NOT IN (${ mutingQuery.getQuery() })`); })) .andWhere(new Brackets(qb => { qb - .where('note.renoteUserId IS NULL') - .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); + .where(`${noteColumn}.replyUserId IS NULL`) + .orWhere(`${noteColumn}.replyUserId NOT IN (${ mutingQuery.getQuery() })`); + })) + .andWhere(new Brackets(qb => { + qb + .where(`${noteColumn}.renoteUserId IS NULL`) + .orWhere(`${noteColumn}.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); })) // mute instances .andWhere(new Brackets(qb => { qb - .andWhere('note.userHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); + .andWhere(`${noteColumn}.userHost IS NULL`) + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.userHost)`); })) .andWhere(new Brackets(qb => { qb - .where('note.replyUserHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); + .where(`${noteColumn}.replyUserHost IS NULL`) + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.replyUserHost)`); })) .andWhere(new Brackets(qb => { qb - .where('note.renoteUserHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); + .where(`${noteColumn}.renoteUserHost IS NULL`) + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.renoteUserHost)`); })); q.setParameters(mutingQuery.getParameters()); @@ -300,7 +360,7 @@ export class QueryService { public generateSuspendedUserQueryForNote(q: SelectQueryBuilder, excludeAuthor?: boolean): void { if (excludeAuthor) { const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) + .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮 .orWhere(`user.id = ${user}.id`) .orWhere(`${user}.isSuspended = FALSE`)); q @@ -308,7 +368,7 @@ export class QueryService { .andWhere(brakets('renoteUser')); } else { const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) + .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮 .orWhere(`${user}.isSuspended = FALSE`)); q .andWhere('user.isSuspended = FALSE') diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index a1e806816b..0f225a8242 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; import { type SystemWebhookPayload } from '@/core/SystemWebhookService.js'; +import type { Packed } from '@/misc/json-schema.js'; import { type UserWebhookPayload } from './UserWebhookService.js'; import type { DbJobData, @@ -52,6 +53,37 @@ export const QUEUE_TYPES = [ 'systemWebhookDeliver', ] as const; +const REPEATABLE_SYSTEM_JOB_DEF = [{ + name: 'tickCharts', + pattern: '55 * * * *', +}, { + name: 'resyncCharts', + pattern: '0 0 * * *', +}, { + name: 'cleanCharts', + pattern: '0 0 * * *', +}, { + name: 'aggregateRetention', + pattern: '0 0 * * *', +}, { + name: 'clean', + pattern: '0 0 * * *', +}, { + name: 'checkExpiredMutings', + pattern: '*/5 * * * *', +}, { + name: 'bakeBufferedReactions', + pattern: '0 0 * * *', +}, { + name: 'checkModeratorsActivity', + // 毎時30分に起動 + pattern: '30 * * * *', +}, { + name: 'cleanRemoteNotes', + // 毎日午前4時に起動(最も人の少ない時間帯) + pattern: '0 4 * * *', +}]; + @Injectable() export class QueueService { constructor( @@ -68,61 +100,31 @@ export class QueueService { @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, ) { - this.systemQueue.add('tickCharts', { - }, { - repeat: { pattern: '55 * * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); + for (const def of REPEATABLE_SYSTEM_JOB_DEF) { + this.systemQueue.upsertJobScheduler(def.name, { + pattern: def.pattern, + immediately: false, + }, { + name: def.name, + opts: { + // 期限ではなくcountで設定したいが、ジョブごとではなくキュー全体でカウントされるため、高頻度で実行されるジョブによって低頻度で実行されるジョブのログが消えることになる + removeOnComplete: { + age: 3600 * 24 * 7, // keep up to 7 days + }, + removeOnFail: { + age: 3600 * 24 * 7, // keep up to 7 days + }, + }, + }); + } - this.systemQueue.add('resyncCharts', { - }, { - repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('cleanCharts', { - }, { - repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('aggregateRetention', { - }, { - repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('clean', { - }, { - repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('checkExpiredMutings', { - }, { - repeat: { pattern: '*/5 * * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('bakeBufferedReactions', { - }, { - repeat: { pattern: '0 0 * * *' }, - removeOnComplete: 10, - removeOnFail: 30, - }); - - this.systemQueue.add('checkModeratorsActivity', { - }, { - // 毎時30分に起動 - repeat: { pattern: '30 * * * *' }, - removeOnComplete: 10, - removeOnFail: 30, + // 古いバージョンで作成され現在使われなくなったrepeatableジョブをクリーンアップ + this.systemQueue.getJobSchedulers().then(schedulers => { + for (const scheduler of schedulers) { + if (!REPEATABLE_SYSTEM_JOB_DEF.some(def => def.name === scheduler.key)) { + this.systemQueue.removeJobScheduler(scheduler.key); + } + } }); } @@ -774,13 +776,13 @@ export class QueueService { } @bindThis - private packJobData(job: Bull.Job) { + private packJobData(job: Bull.Job): Packed<'QueueJob'> { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : []; stacktrace.reverse(); return { - id: job.id, + id: job.id!, name: job.name, data: job.data, opts: job.opts, @@ -809,6 +811,13 @@ export class QueueService { } } + @bindThis + public async queueGetJobLogs(queueType: typeof QUEUE_TYPES[number], jobId: string) { + const queue = this.getQueue(queueType); + const result = await queue.getJobLogs(jobId); + return result.logs; + } + @bindThis public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) { const RETURN_LIMIT = 100; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index fc97780ba3..3df7ee69ee 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -43,6 +43,7 @@ export type RolePolicies = { canManageCustomEmojis: boolean; canManageAvatarDecorations: boolean; canSearchNotes: boolean; + canSearchUsers: boolean; canUseTranslator: boolean; canHideAds: boolean; driveCapacityMb: number; @@ -65,6 +66,9 @@ export type RolePolicies = { canImportMuting: boolean; canImportUserLists: boolean; chatAvailability: 'available' | 'readonly' | 'unavailable'; + uploadableFileTypes: string[]; + noteDraftLimit: number; + watermarkAvailable: boolean; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -79,10 +83,11 @@ export const DEFAULT_POLICIES: RolePolicies = { canManageCustomEmojis: false, canManageAvatarDecorations: false, canSearchNotes: false, + canSearchUsers: true, canUseTranslator: true, canHideAds: false, driveCapacityMb: 100, - maxFileSizeMb: 10, + maxFileSizeMb: 30, alwaysMarkNsfw: false, canUpdateBioMedia: true, pinLimit: 5, @@ -101,6 +106,15 @@ export const DEFAULT_POLICIES: RolePolicies = { canImportMuting: true, canImportUserLists: true, chatAvailability: 'available', + uploadableFileTypes: [ + 'text/plain', + 'application/json', + 'image/*', + 'video/*', + 'audio/*', + ], + noteDraftLimit: 10, + watermarkAvailable: true, }; @Injectable() @@ -390,6 +404,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), + canSearchUsers: calc('canSearchUsers', vs => vs.some(v => v === true)), canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), @@ -412,6 +427,18 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), chatAvailability: calc('chatAvailability', aggregateChatAvailability), + uploadableFileTypes: calc('uploadableFileTypes', vs => { + const set = new Set(); + for (const v of vs) { + for (const type of v) { + if (type.trim() === '') continue; + set.add(type.trim()); + } + } + return [...set]; + }), + noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)), + watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)), }; } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 20a776ded8..71dc718916 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -227,17 +227,14 @@ export class SearchService { if (opts.host) { if (opts.host === '.') { - query.andWhere('user.host IS NULL'); + query.andWhere('note.userHost IS NULL'); } else { - query.andWhere('user.host = :host', { host: opts.host }); + query.andWhere('note.userHost = :host', { host: opts.host }); } } this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - if (me) this.queryService.generateMutedUserQueryForNotes(query, me); - if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); return query.limit(pagination.limit).getMany(); } diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 5462cb0b13..a85da62b86 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -93,6 +93,11 @@ export class SignupService { if (isPreserved) { throw new Error('USED_USERNAME'); } + + const hasProhibitedWords = this.utilityService.isKeyWordIncluded(username.toLowerCase(), this.meta.prohibitedWordsForNameOfUser); + if (hasProhibitedWords) { + throw new Error('USED_USERNAME'); + } } const keyPair = await new Promise((res, rej) => diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index f0a8768c8f..61435500b7 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -91,7 +91,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async addMember(target: MiUser, list: MiUserList, me: MiUser) { + public async addMember(target: MiUser, list: MiUserList, me: MiUser, options: { withReplies?: boolean } = {}) { const currentCount = await this.userListMembershipsRepository.countBy({ userListId: list.id, }); @@ -104,6 +104,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit { userId: target.id, userListId: list.id, userListUserId: list.userId, + withReplies: options.withReplies ?? false, } as MiUserListMembership); this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id }); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 2534899ad1..646150455b 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -104,7 +104,7 @@ export class Resolver { throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', 'Instance is blocked'); } - if (this.config.signToActivityPubGet && !this.user) { + if (this.meta.signToActivityPubGet && !this.user) { this.user = await this.systemAccountService.fetch('actor'); } diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts index da112d5444..6bce2413fd 100644 --- a/packages/backend/src/core/entities/ChatEntityService.ts +++ b/packages/backend/src/core/entities/ChatEntityService.ts @@ -238,13 +238,15 @@ export class ChatEntityService { options?: { _hint_?: { packedOwners: Map>; - memberships?: Map; + myMemberships?: Map; + myInvitations?: Map; }; }, ): Promise> { const room = typeof src === 'object' ? src : await this.chatRoomsRepository.findOneByOrFail({ id: src }); - const membership = me && me.id !== room.ownerId ? (options?._hint_?.memberships?.get(room.id) ?? await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null; + const membership = me && me.id !== room.ownerId ? (options?._hint_?.myMemberships?.get(room.id) ?? await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null; + const invitation = me && me.id !== room.ownerId ? (options?._hint_?.myInvitations?.get(room.id) ?? await this.chatRoomInvitationsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null; return { id: room.id, @@ -254,6 +256,7 @@ export class ChatEntityService { ownerId: room.ownerId, owner: options?._hint_?.packedOwners.get(room.ownerId) ?? await this.userEntityService.pack(room.owner ?? room.ownerId, me), isMuted: membership != null ? membership.isMuted : false, + invitationExists: invitation != null, }; } @@ -278,7 +281,7 @@ export class ChatEntityService { const owners = _rooms.map(x => x.owner ?? x.ownerId); - const [packedOwners, memberships] = await Promise.all([ + const [packedOwners, myMemberships, myInvitations] = await Promise.all([ this.userEntityService.packMany(owners, me) .then(users => new Map(users.map(u => [u.id, u]))), this.chatRoomMembershipsRepository.find({ @@ -287,9 +290,15 @@ export class ChatEntityService { userId: me.id, }, }).then(memberships => new Map(_rooms.map(r => [r.id, memberships.find(m => m.roomId === r.id)]))), + this.chatRoomInvitationsRepository.find({ + where: { + roomId: In(_rooms.map(x => x.id)), + userId: me.id, + }, + }).then(invitations => new Map(_rooms.map(r => [r.id, invitations.find(i => i.roomId === r.id)]))), ]); - return Promise.all(_rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners, memberships } }))); + return Promise.all(_rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners, myMemberships, myInvitations } }))); } @bindThis diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index c485555f90..a6f7f369a6 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -6,7 +6,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; @@ -34,6 +34,9 @@ export class DriveFileEntityService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -95,7 +98,7 @@ export class DriveFileEntityService { return this.getProxiedUrl(file.uri, 'static'); } - if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) { // リモートかつ期限切れはローカルプロキシを試みる // 従来は/files/${thumbnailAccessKey}にアクセスしていたが、 // /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する @@ -115,7 +118,7 @@ export class DriveFileEntityService { } // リモートかつ期限切れはローカルプロキシを試みる - if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) { const key = file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts new file mode 100644 index 0000000000..3ef8cdaa12 --- /dev/null +++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { EntityNotFoundError } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { MiUser, MiNote, MiNoteDraft } from '@/models/_.js'; +import type { NoteDraftsRepository, ChannelsRepository } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { DebounceLoader } from '@/misc/loader.js'; +import { IdService } from '@/core/IdService.js'; +import type { OnModuleInit } from '@nestjs/common'; +import type { UserEntityService } from './UserEntityService.js'; +import type { DriveFileEntityService } from './DriveFileEntityService.js'; +import type { NoteEntityService } from './NoteEntityService.js'; + +@Injectable() +export class NoteDraftEntityService implements OnModuleInit { + private userEntityService: UserEntityService; + private driveFileEntityService: DriveFileEntityService; + private idService: IdService; + private noteEntityService: NoteEntityService; + private noteDraftLoader = new DebounceLoader(this.findNoteDraftOrFail); + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.noteDraftsRepository) + private noteDraftsRepository: NoteDraftsRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + ) { + } + + onModuleInit() { + this.userEntityService = this.moduleRef.get('UserEntityService'); + this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); + this.idService = this.moduleRef.get('IdService'); + this.noteEntityService = this.moduleRef.get('NoteEntityService'); + } + + @bindThis + public async packAttachedFiles(fileIds: MiNote['fileIds'], packedFiles: Map | null>): Promise[]> { + const missingIds = []; + for (const id of fileIds) { + if (!packedFiles.has(id)) missingIds.push(id); + } + if (missingIds.length) { + const additionalMap = await this.driveFileEntityService.packManyByIdsMap(missingIds); + for (const [k, v] of additionalMap) { + packedFiles.set(k, v); + } + } + return fileIds.map(id => packedFiles.get(id)).filter(x => x != null); + } + + @bindThis + public async pack( + src: MiNoteDraft['id'] | MiNoteDraft, + me?: { id: MiUser['id'] } | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + withReactionAndUserPairCache?: boolean; + _hint_?: { + packedFiles: Map | null>; + packedUsers: Map> + }; + }, + ): Promise> { + const opts = Object.assign({ + detail: true, + }, options); + + const noteDraft = typeof src === 'object' ? src : await this.noteDraftLoader.load(src); + + const text = noteDraft.text; + + const channel = noteDraft.channelId + ? noteDraft.channel + ? noteDraft.channel + : await this.channelsRepository.findOneBy({ id: noteDraft.channelId }) + : null; + + const packedFiles = options?._hint_?.packedFiles; + const packedUsers = options?._hint_?.packedUsers; + + async function nullIfEntityNotFound(promise: Promise): Promise { + try { + return await promise; + } catch (err) { + if (err instanceof EntityNotFoundError) { + return null; + } + throw err; + } + } + + const packed: Packed<'NoteDraft'> = await awaitAll({ + id: noteDraft.id, + createdAt: this.idService.parse(noteDraft.id).date.toISOString(), + userId: noteDraft.userId, + user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), + text: text, + cw: noteDraft.cw, + visibility: noteDraft.visibility, + localOnly: noteDraft.localOnly, + reactionAcceptance: noteDraft.reactionAcceptance, + visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined, + hashtag: noteDraft.hashtag ?? undefined, + fileIds: noteDraft.fileIds, + files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds), + replyId: noteDraft.replyId, + renoteId: noteDraft.renoteId, + channelId: noteDraft.channelId ?? undefined, + channel: channel ? { + id: channel.id, + name: channel.name, + color: channel.color, + isSensitive: channel.isSensitive, + allowRenoteToExternal: channel.allowRenoteToExternal, + userId: channel.userId, + } : undefined, + + ...(opts.detail ? { + reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, { + detail: false, + skipHide: opts.skipHide, + })) : undefined, + + renote: noteDraft.renoteId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.renoteId, me, { + detail: true, + skipHide: opts.skipHide, + })) : undefined, + + poll: noteDraft.hasPoll ? { + choices: noteDraft.pollChoices, + multiple: noteDraft.pollMultiple, + expiresAt: noteDraft.pollExpiresAt?.toISOString(), + expiredAfter: noteDraft.pollExpiredAfter, + } : undefined, + } : {} ), + }); + + return packed; + } + + @bindThis + public async packMany( + noteDrafts: MiNoteDraft[], + me?: { id: MiUser['id'] } | null | undefined, + options?: { + detail?: boolean; + }, + ) { + if (noteDrafts.length === 0) return []; + + // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく + const fileIds = noteDrafts.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null); + const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); + const users = [ + ...noteDrafts.map(({ user, userId }) => user ?? userId), + ]; + const packedUsers = await this.userEntityService.packMany(users, me) + .then(users => new Map(users.map(u => [u.id, u]))); + + return await Promise.all(noteDrafts.map(n => this.pack(n, me, { + ...options, + _hint_: { + packedFiles, + packedUsers, + }, + }))); + } + + @bindThis + private findNoteDraftOrFail(id: string): Promise { + return this.noteDraftsRepository.findOneOrFail({ + where: { id }, + relations: ['user'], + }); + } +} diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 92caad908c..6871ba2c72 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import { EntityNotFoundError, In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -46,6 +46,17 @@ function getAppearNoteIds(notes: MiNote[]): Set { return appearNoteIds; } +async function nullIfEntityNotFound(promise: Promise): Promise { + try { + return await promise; + } catch (err) { + if (err instanceof EntityNotFoundError) { + return null; + } + throw err; + } +} + @Injectable() export class NoteEntityService implements OnModuleInit { private userEntityService: UserEntityService; @@ -436,19 +447,21 @@ export class NoteEntityService implements OnModuleInit { ...(opts.detail ? { clippedCount: note.clippedCount, - reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { + // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される + reply: (note.replyId && note.reply === null) ? null : note.replyId ? nullIfEntityNotFound(this.pack(note.reply ?? note.replyId, me, { detail: false, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, - }) : undefined, + })) : undefined, - renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { + // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される + renote: (note.renoteId && note.renote === null) ? null : note.renoteId ? nullIfEntityNotFound(this.pack(note.renote ?? note.renoteId, me, { detail: true, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, - }) : undefined, + })) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, @@ -591,7 +604,7 @@ export class NoteEntityService implements OnModuleInit { private findNoteOrFail(id: string): Promise { return this.notesRepository.findOneOrFail({ where: { id }, - relations: ['user'], + relations: ['user', 'renote', 'reply'], }); } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 77d2838e09..c915133453 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -89,5 +89,6 @@ export const DI = { chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'), + noteDraftsRepository: Symbol('noteDraftsRepository'), //#endregion }; diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts index 6d03b433ba..fabbdc335f 100644 --- a/packages/backend/src/misc/emoji-regex.ts +++ b/packages/backend/src/misc/emoji-regex.ts @@ -4,6 +4,6 @@ */ // taken from @twemoji/parser/dist/lib/regex.js -const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b|\ud83d\udc26\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|\ud83e\udef0|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef1-\udef8]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedc-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec2\udece-\udedb\udee0-\udee8]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; +const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])(?:\u200d\u27a1\ufe0f)?|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f(?:\u200d\u27a1\ufe0f)?)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f(?:\u200d\u27a1\ufe0f)?|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83e\uddd1\u200d\ud83e\uddd1\u200d\ud83e\uddd2\u200d\ud83e\uddd2|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83e\uddd1\u200d\ud83e\uddd1\u200d\ud83e\uddd2|\ud83e\uddd1\u200d\ud83e\uddd2\u200d\ud83e\uddd2|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u26d3\ufe0f\u200d\ud83d\udca5|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udf44\u200d\ud83d\udfeb|\ud83c\udf4b\u200d\ud83d\udfe9|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc26\u200d\ud83d\udd25|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83d\ude42\u200d\u2194\ufe0f|\ud83d\ude42\u200d\u2195\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddd1\u200d\ud83e\uddd2|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b|\ud83d\udc26\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|\ud83e\udef0|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c\udfc3|\ud83d\udeb6|\ud83e\uddce)(?:\ud83c[\udffb-\udfff])?(?:\u200d\u27a1\ufe0f)?|(?:\ud83c[\udf85\udfc2\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4\udeb5\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd\uddcf\uddd1-\udddd\udec3-\udec5\udef1-\udef8]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedc-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude7c\ude80-\ude89\ude8f-\udec2\udec6\udece-\udedc\udedf-\udee9]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts index 862d6e6a38..6f72082733 100644 --- a/packages/backend/src/misc/is-user-related.ts +++ b/packages/backend/src/misc/is-user-related.ts @@ -3,7 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export function isUserRelated(note: any, userIds: Set, ignoreAuthor = false): boolean { +import type { MiUser } from '@/models/_.js'; + +interface NoteLike { + userId: MiUser['id']; + reply?: NoteLike | null; + renote?: NoteLike | null; + replyUserId?: MiUser['id'] | null; + renoteUserId?: MiUser['id'] | null; +} + +export function isUserRelated(note: NoteLike | null | undefined, userIds: Set, ignoreAuthor = false): boolean { if (!note) { return false; } @@ -12,13 +22,16 @@ export function isUserRelated(note: any, userIds: Set, ignoreAuthor = fa return true; } - if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) { + const replyUserId = note.replyUserId ?? note.reply?.userId; + if (replyUserId != null && replyUserId !== note.userId && userIds.has(replyUserId)) { return true; } - if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) { + const renoteUserId = note.renoteUserId ?? note.renote?.userId; + if (renoteUserId != null && renoteUserId !== note.userId && userIds.has(renoteUserId)) { return true; } return false; } + diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index e4eb10efca..ed47edff9b 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -31,7 +31,11 @@ import { packedChannelSchema } from '@/models/json-schema/channel.js'; import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; import { packedClipSchema } from '@/models/json-schema/clip.js'; import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; -import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; +import { + packedQueueCountSchema, + packedQueueMetricsSchema, + packedQueueJobSchema, +} from '@/models/json-schema/queue.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; import { packedEmojiDetailedAdminSchema, @@ -68,6 +72,7 @@ import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js'; import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js'; import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js'; import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js'; +import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -85,6 +90,7 @@ export const refs = { Announcement: packedAnnouncementSchema, App: packedAppSchema, Note: packedNoteSchema, + NoteDraft: packedNoteDraftSchema, NoteReaction: packedNoteReactionSchema, NoteFavorite: packedNoteFavoriteSchema, Notification: packedNotificationSchema, @@ -100,6 +106,8 @@ export const refs = { PageBlock: packedPageBlockSchema, Channel: packedChannelSchema, QueueCount: packedQueueCountSchema, + QueueMetrics: packedQueueMetricsSchema, + QueueJob: packedQueueJobSchema, Antenna: packedAntennaSchema, Clip: packedClipSchema, FederationInstance: packedFederationInstanceSchema, @@ -212,7 +220,17 @@ type NullOrUndefined

= // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // Get intersection from union type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -type PartialIntersection = Partial>; + +type ArrayToIntersection> = + T extends readonly [infer Head, ...infer Tail] + ? Head extends Schema + ? Tail extends ReadonlyArray + ? Tail extends [] + ? SchemaType + : SchemaType & ArrayToIntersection + : never + : never + : never; // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 // To get union, we use `Foo extends any ? Hoge : never` @@ -230,8 +248,8 @@ type ObjectSchemaTypeDef

= : never : ObjType> : - p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md - p['allOf'] extends ReadonlyArray ? UnionToIntersection> : + p['anyOf'] extends ReadonlyArray ? UnionSchemaType : + p['allOf'] extends ReadonlyArray ? ArrayToIntersection : p['additionalProperties'] extends true ? Record : p['additionalProperties'] extends Schema ? p['additionalProperties'] extends infer AdditionalProperties ? @@ -271,7 +289,8 @@ export type SchemaTypeDef

= p['items'] extends NonNullable ? SchemaType[] : any[] ) : - p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> : + p['anyOf'] extends ReadonlyArray ? UnionSchemaType : + p['allOf'] extends ReadonlyArray ? ArrayToIntersection : p['oneOf'] extends ReadonlyArray ? UnionSchemaType : any; diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts index fbff880afc..17ec6abed5 100644 --- a/packages/backend/src/models/AbuseReportNotificationRecipient.ts +++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts @@ -22,7 +22,7 @@ export class MiAbuseReportNotificationRecipient { /** * 有効かどうか. */ - @Index() + @Index('IDX_abuse_report_notification_recipient_isActive') @Column('boolean', { default: true, }) @@ -47,7 +47,7 @@ export class MiAbuseReportNotificationRecipient { /** * 通知方法. */ - @Index() + @Index('IDX_abuse_report_notification_recipient_method') @Column('varchar', { length: 64, }) @@ -56,10 +56,11 @@ export class MiAbuseReportNotificationRecipient { /** * 通知先のユーザID. */ - @Index() + @Index('IDX_abuse_report_notification_recipient_userId') @Column({ ...id(), nullable: true, + default: null, }) public userId: MiUser['id'] | null; @@ -75,17 +76,20 @@ export class MiAbuseReportNotificationRecipient { /** * 通知先のユーザプロフィール. */ - @ManyToOne(type => MiUserProfile, {}) + @ManyToOne(type => MiUserProfile, { + onDelete: 'CASCADE', + }) @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' }) public userProfile: MiUserProfile | null; /** * 通知先のシステムWebhookId. */ - @Index() + @Index('IDX_abuse_report_notification_recipient_systemWebhookId') @Column({ ...id(), nullable: true, + default: null, }) public systemWebhookId: string | null; @@ -95,6 +99,6 @@ export class MiAbuseReportNotificationRecipient { @ManyToOne(type => MiSystemWebhook, { onDelete: 'CASCADE', }) - @JoinColumn() + @JoinColumn({ name: 'systemWebhookId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_systemWebhookId' }) public systemWebhook: MiSystemWebhook | null; } diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index d62b6e9f6f..8dff8fd153 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -8,6 +8,7 @@ import { id } from './util/id.js'; @Entity('emoji') @Index(['name', 'host'], { unique: true }) +@Index('IDX_EMOJI_ROLE_IDS', { synchronize: false }) // GIN for roleIdsThatCanBeUsedThisEmojiAsReaction in production export class MiEmoji { @PrimaryColumn(id()) public id: string; @@ -32,6 +33,7 @@ export class MiEmoji { @Column('varchar', { length: 128, nullable: true, }) + @Index('IDX_EMOJI_CATEGORY') public category: string | null; @Column('varchar', { diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index e3625b247c..1fc50cbd07 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -59,7 +59,7 @@ export class MiMeta { public maintainerEmail: string | null; @Column('boolean', { - default: false, + default: true, }) public disableRegistration: boolean; @@ -570,7 +570,7 @@ export class MiMeta { public bannedEmailDomains: string[]; @Column('varchar', { - length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', + length: 1024, array: true, default: ['admin', 'administrator', 'root', 'system', 'maintainer', 'host', 'mod', 'moderator', 'owner', 'superuser', 'staff', 'auth', 'i', 'me', 'everyone', 'all', 'mention', 'mentions', 'example', 'user', 'users', 'account', 'accounts', 'official', 'help', 'helps', 'support', 'supports', 'info', 'information', 'informations', 'announce', 'announces', 'announcement', 'announcements', 'notice', 'notification', 'notifications', 'dev', 'developer', 'developers', 'tech', 'misskey'], }) public preservedUsernames: string[]; @@ -619,6 +619,11 @@ export class MiMeta { }) public urlPreviewEnabled: boolean; + @Column('boolean', { + default: true, + }) + public urlPreviewAllowRedirect: boolean; + @Column('integer', { default: 10000, }) @@ -630,7 +635,7 @@ export class MiMeta { public urlPreviewMaximumContentLength: number; @Column('boolean', { - default: true, + default: false, }) public urlPreviewRequireContentLength: boolean; @@ -643,12 +648,13 @@ export class MiMeta { @Column('varchar', { length: 1024, nullable: true, + default: null, }) public urlPreviewUserAgent: string | null; @Column('varchar', { length: 128, - default: 'all', + default: 'none', }) public federation: 'all' | 'specified' | 'none'; @@ -680,6 +686,36 @@ export class MiMeta { default: false, }) public singleUserMode: boolean; + + @Column('boolean', { + default: true, + }) + public proxyRemoteFiles: boolean; + + @Column('boolean', { + default: true, + }) + public signToActivityPubGet: boolean; + + @Column('boolean', { + default: true, + }) + public allowExternalApRedirect: boolean; + + @Column('boolean', { + default: false, + }) + public enableRemoteNotesCleaning: boolean; + + @Column('integer', { + default: 60, // minutes + }) + public remoteNotesCleaningMaxProcessingDurationInMinutes: number; + + @Column('integer', { + default: 90, // days + }) + public remoteNotesCleaningExpiryDaysForEachNotes: number; } export type SoftwareSuspension = { diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 3dcbdb735b..ff46615729 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -4,7 +4,7 @@ */ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { noteVisibilities } from '@/types.js'; +import { noteVisibilities, noteReactionAcceptances } from '@/types.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiChannel } from './Channel.js'; @@ -20,7 +20,8 @@ import type { MiDriveFile } from './DriveFile.js'; // You should not use `@Index({ concurrent: true })` decorator because database initialization for test will fail // because it will always run CREATE INDEX in transaction based on decorators. // Not appending `{ concurrent: true }` to `@Index` will not cause any problem in production, -@Index(['userId', 'id']) + +@Index(['userId', 'id']) // Note: this index is ("userId", "id" DESC) in production, but not in test. @Entity('note') export class MiNote { @PrimaryColumn(id()) @@ -35,7 +36,7 @@ export class MiNote { public replyId: MiNote['id'] | null; @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', + createForeignKeyConstraints: false, }) @JoinColumn() public reply: MiNote | null; @@ -49,7 +50,7 @@ export class MiNote { public renoteId: MiNote['id'] | null; @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', + createForeignKeyConstraints: false, }) @JoinColumn() public renote: MiNote | null; @@ -96,7 +97,7 @@ export class MiNote { @Column('varchar', { length: 64, nullable: true, }) - public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; + public reactionAcceptance: typeof noteReactionAcceptances[number]; @Column('smallint', { default: 0, diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts new file mode 100644 index 0000000000..6483748bc2 --- /dev/null +++ b/packages/backend/src/models/NoteDraft.ts @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { noteVisibilities, noteReactionAcceptances } from '@/types.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChannel } from './Channel.js'; +import { MiNote } from './Note.js'; +import type { MiDriveFile } from './DriveFile.js'; + +@Entity('note_draft') +@Index('IDX_NOTE_DRAFT_FILE_IDS', { synchronize: false }) // GIN for fileIds in production +@Index('IDX_NOTE_DRAFT_VISIBLE_USER_IDS', { synchronize: false }) // GIN for visibleUserIds in production +export class MiNoteDraft { + @PrimaryColumn(id()) + public id: string; + + @Index('IDX_NOTE_DRAFT_REPLY_ID') + @Column({ + ...id(), + nullable: true, + comment: 'The ID of reply target.', + }) + public replyId: MiNote['id'] | null; + + // There is a possibility that replyId is not null but reply is null when the reply note is deleted. + @ManyToOne(type => MiNote, { + createForeignKeyConstraints: false, + }) + @JoinColumn() + public reply: MiNote | null; + + @Index('IDX_NOTE_DRAFT_RENOTE_ID') + @Column({ + ...id(), + nullable: true, + comment: 'The ID of renote target.', + }) + public renoteId: MiNote['id'] | null; + + // There is a possibility that renoteId is not null but renote is null when the renote note is deleted. + @ManyToOne(type => MiNote, { + createForeignKeyConstraints: false, + }) + @JoinColumn() + public renote: MiNote | null; + + // TODO: varcharにしたい(Note.tsと同じ) + @Column('text', { + nullable: true, + }) + public text: string | null; + + @Column('varchar', { + length: 512, nullable: true, + }) + public cw: string | null; + + @Index('IDX_NOTE_DRAFT_USER_ID') + @Column({ + ...id(), + comment: 'The ID of author.', + }) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column('boolean', { + default: false, + }) + public localOnly: boolean; + + @Column('varchar', { + length: 64, nullable: true, + }) + public reactionAcceptance: typeof noteReactionAcceptances[number]; + + /** + * public ... 公開 + * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す + * followers ... フォロワーのみ + * specified ... visibleUserIds で指定したユーザーのみ + */ + @Column('enum', { enum: noteVisibilities }) + public visibility: typeof noteVisibilities[number]; + + @Index('IDX_NOTE_DRAFT_FILE_IDS', { synchronize: false }) + @Column({ + ...id(), + array: true, default: '{}', + }) + public fileIds: MiDriveFile['id'][]; + + @Index('IDX_NOTE_DRAFT_VISIBLE_USER_IDS', { synchronize: false }) + @Column({ + ...id(), + array: true, default: '{}', + }) + public visibleUserIds: MiUser['id'][]; + + @Column('varchar', { + length: 128, nullable: true, + }) + public hashtag: string | null; + + @Index('IDX_NOTE_DRAFT_CHANNEL_ID') + @Column({ + ...id(), + nullable: true, + comment: 'The ID of source channel.', + }) + public channelId: MiChannel['id'] | null; + + // There is a possibility that channelId is not null but channel is null when the channel is deleted. + // (deleting channel is not implemented so it's not happening now but may happen in the future) + @ManyToOne(type => MiChannel, { + createForeignKeyConstraints: false, + }) + @JoinColumn() + public channel: MiChannel | null; + + // 以下、Pollについて追加 + + @Column('boolean', { + default: false, + }) + public hasPoll: boolean; + + @Column('varchar', { + length: 256, array: true, default: '{}', + }) + public pollChoices: string[]; + + @Column('boolean') + public pollMultiple: boolean; + + @Column('timestamp with time zone', { + nullable: true, + }) + public pollExpiresAt: Date | null; + + @Column('bigint', { + nullable: true, + }) + public pollExpiredAfter: number | null; + + // ここまで追加 + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index b7142d91bf..146dbbc3b8 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -42,6 +42,7 @@ import { MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, + MiNoteDraft, MiPage, MiPageLike, MiPasswordResetRequest, @@ -140,6 +141,12 @@ const $noteReactionsRepository: Provider = { inject: [DI.db], }; +const $noteDraftsRepository: Provider = { + provide: DI.noteDraftsRepository, + useFactory: (db: DataSource) => db.getRepository(MiNoteDraft).extend(miRepository as MiRepository), + inject: [DI.db], +}; + const $pollsRepository: Provider = { provide: DI.pollsRepository, useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository), @@ -542,6 +549,7 @@ const $reversiGamesRepository: Provider = { $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, + $noteDraftsRepository, $pollsRepository, $pollVotesRepository, $userProfilesRepository, @@ -618,6 +626,7 @@ const $reversiGamesRepository: Provider = { $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, + $noteDraftsRepository, $pollsRepository, $pollVotesRepository, $userProfilesRepository, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index baf4eefdf1..a6e9edcf5f 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -120,7 +120,7 @@ export class MiUser { // avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること @Column('varchar', { - length: 512, nullable: true, + length: 1024, nullable: true, }) public avatarUrl: string | null; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index c4c1fa5ec9..501b539210 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -29,7 +29,7 @@ export class MiUserProfile { }) public location: string | null; - @Index() + // Note: There's index named IDX_de22cd2b445eee31ae51cdbe99 for SUBSTR("birthday", 6, 5) @Column('char', { length: 10, nullable: true, comment: 'The birthday (YYYY-MM-DD) of the User.', diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index e1ea2a2604..84b5cbed0a 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -55,6 +55,7 @@ import { MiMeta } from '@/models/Meta.js'; import { MiModerationLog } from '@/models/ModerationLog.js'; import { MiMuting } from '@/models/Muting.js'; import { MiNote } from '@/models/Note.js'; +import { MiNoteDraft } from '@/models/NoteDraft.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; @@ -188,6 +189,7 @@ export { MiMuting, MiRenoteMuting, MiNote, + MiNoteDraft, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, @@ -266,6 +268,7 @@ export type ModerationLogsRepository = Repository & MiRepositor export type MutingsRepository = Repository & MiRepository; export type RenoteMutingsRepository = Repository & MiRepository; export type NotesRepository = Repository & MiRepository; +export type NoteDraftsRepository = Repository & MiRepository; export type NoteFavoritesRepository = Repository & MiRepository; export type NoteReactionsRepository = Repository & MiRepository; export type NoteThreadMutingsRepository = Repository & MiRepository; diff --git a/packages/backend/src/models/json-schema/chat-room.ts b/packages/backend/src/models/json-schema/chat-room.ts index e97556e378..e628a9baa3 100644 --- a/packages/backend/src/models/json-schema/chat-room.ts +++ b/packages/backend/src/models/json-schema/chat-room.ts @@ -36,5 +36,9 @@ export const packedChatRoomSchema = { type: 'boolean', optional: true, nullable: false, }, + invitationExists: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 42b2172409..d50200e6e9 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -51,7 +51,7 @@ export const packedFlashSchema = { }, likedCount: { type: 'number', - optional: false, nullable: true, + optional: false, nullable: false, }, isLiked: { type: 'boolean', diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts new file mode 100644 index 0000000000..504b263a6d --- /dev/null +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedNoteDraftSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + text: { + type: 'string', + optional: false, nullable: true, + }, + cw: { + type: 'string', + optional: true, nullable: true, + }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + replyId: { + type: 'string', + optional: true, nullable: true, + format: 'id', + example: 'xxxxxxxxxx', + }, + renoteId: { + type: 'string', + optional: true, nullable: true, + format: 'id', + example: 'xxxxxxxxxx', + }, + reply: { + type: 'object', + optional: true, nullable: true, + ref: 'Note', + description: 'The reply target note contents if exists. If the reply target has been deleted since the draft was created, this will be null while replyId is not null.', + }, + renote: { + type: 'object', + optional: true, nullable: true, + ref: 'Note', + description: 'The renote target note contents if exists. If the renote target has been deleted since the draft was created, this will be null while renoteId is not null.', + }, + visibility: { + type: 'string', + optional: false, nullable: false, + enum: ['public', 'home', 'followers', 'specified'], + }, + visibleUserIds: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + fileIds: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + files: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'DriveFile', + }, + }, + hashtag: { + type: 'string', + optional: true, nullable: false, + }, + poll: { + type: 'object', + optional: true, nullable: true, + properties: { + expiresAt: { + type: 'string', + optional: true, nullable: true, + format: 'date-time', + }, + expiredAfter: { + type: 'number', + optional: true, nullable: true, + }, + multiple: { + type: 'boolean', + optional: false, nullable: false, + }, + choices: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + }, + channelId: { + type: 'string', + optional: true, nullable: true, + format: 'id', + example: 'xxxxxxxxxx', + }, + channel: { + type: 'object', + optional: true, nullable: true, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + color: { + type: 'string', + optional: false, nullable: false, + }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + allowRenoteToExternal: { + type: 'boolean', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: true, + }, + }, + }, + localOnly: { + type: 'boolean', + optional: true, nullable: false, + }, + reactionAcceptance: { + type: 'string', + optional: false, nullable: true, + enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/queue.ts b/packages/backend/src/models/json-schema/queue.ts index 2ecf5c831f..dad0cf57f6 100644 --- a/packages/backend/src/models/json-schema/queue.ts +++ b/packages/backend/src/models/json-schema/queue.ts @@ -28,3 +28,110 @@ export const packedQueueCountSchema = { }, }, } as const; + +// Bull.Metrics +export const packedQueueMetricsSchema = { + type: 'object', + properties: { + meta: { + type: 'object', + optional: false, nullable: false, + properties: { + count: { + type: 'number', + optional: false, nullable: false, + }, + prevTS: { + type: 'number', + optional: false, nullable: false, + }, + prevCount: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + data: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'number', + optional: false, nullable: false, + }, + }, + count: { + type: 'number', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedQueueJobSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + data: { + type: 'object', + optional: false, nullable: false, + }, + opts: { + type: 'object', + optional: false, nullable: false, + }, + timestamp: { + type: 'number', + optional: false, nullable: false, + }, + processedOn: { + type: 'number', + optional: true, nullable: false, + }, + processedBy: { + type: 'string', + optional: true, nullable: false, + }, + finishedOn: { + type: 'number', + optional: true, nullable: false, + }, + progress: { + type: 'object', + optional: false, nullable: false, + }, + attempts: { + type: 'number', + optional: false, nullable: false, + }, + delay: { + type: 'number', + optional: false, nullable: false, + }, + failedReason: { + type: 'string', + optional: false, nullable: false, + }, + stacktrace: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + returnValue: { + type: 'object', + optional: false, nullable: false, + }, + isFailed: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index e67704e8d3..0b9234cb81 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -212,6 +212,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canSearchUsers: { + type: 'boolean', + optional: false, nullable: false, + }, canUseTranslator: { type: 'boolean', optional: false, nullable: false, @@ -228,6 +232,14 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, + uploadableFileTypes: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, alwaysMarkNsfw: { type: 'boolean', optional: false, nullable: false, @@ -301,6 +313,14 @@ export const packedRolePoliciesSchema = { optional: false, nullable: false, enum: ['available', 'readonly', 'unavailable'], }, + noteDraftLimit: { + type: 'integer', + optional: false, nullable: false, + }, + watermarkAvailable: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index b06895fcc9..f6cbbbe64c 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -45,6 +45,7 @@ import { MiNote } from '@/models/Note.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; +import { MiNoteDraft } from '@/models/NoteDraft.js'; import { MiPage } from '@/models/Page.js'; import { MiPageLike } from '@/models/PageLike.js'; import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; @@ -210,6 +211,7 @@ export const entities = [ MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, + MiNoteDraft, MiPage, MiPageLike, MiGalleryPost, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 9044285bf6..e01414cd53 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -6,7 +6,6 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; -import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; @@ -18,6 +17,8 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; +import { CheckModeratorsActivityProcessorService } from './processors/CheckModeratorsActivityProcessorService.js'; +import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -83,6 +84,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor AggregateRetentionProcessorService, CheckExpiredMutingsProcessorService, CheckModeratorsActivityProcessorService, + CleanRemoteNotesProcessorService, QueueProcessorService, ], exports: [ diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index c98ebcdcd9..7b64182754 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -43,6 +43,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; +import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseWorkerOptions } from './const.js'; @@ -123,6 +124,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService, private cleanProcessorService: CleanProcessorService, + private cleanRemoteNotesProcessorService: CleanRemoteNotesProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); + case 'cleanRemoteNotes': return this.cleanRemoteNotesProcessorService.process(job); default: throw new Error(`unrecognized job type ${job.name} for system`); } }; diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index 728fc9e72b..782b74f0cd 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -34,6 +34,11 @@ export class CleanRemoteFilesProcessorService { let deletedCount = 0; let cursor: MiDriveFile['id'] | null = null; + const total = await this.driveFilesRepository.countBy({ + userHost: Not(IsNull()), + isLink: false, + }); + while (true) { const files = await this.driveFilesRepository.find({ where: { @@ -58,12 +63,7 @@ export class CleanRemoteFilesProcessorService { deletedCount += 8; - const total = await this.driveFilesRepository.countBy({ - userHost: Not(IsNull()), - isLink: false, - }); - - job.updateProgress(100 / total * deletedCount); + job.updateProgress(deletedCount * total / 100); } this.logger.succ('All cached remote files has been deleted.'); diff --git a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts new file mode 100644 index 0000000000..da3bb804c2 --- /dev/null +++ b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts @@ -0,0 +1,203 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { MiMeta, MiNote, NotesRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; + +@Injectable() +export class CleanRemoteNotesProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.meta) + private meta: MiMeta, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private idService: IdService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-notes'); + } + + @bindThis + public async process(job: Bull.Job>): Promise<{ + deletedCount: number; + oldest: number | null; + newest: number | null; + skipped?: boolean; + }> { + if (!this.meta.enableRemoteNotesCleaning) { + this.logger.info('Remote notes cleaning is disabled, skipping...'); + return { + deletedCount: 0, + oldest: null, + newest: null, + skipped: true, + }; + } + + this.logger.info('cleaning remote notes...'); + + const maxDuration = this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000; // Convert minutes to milliseconds + const startAt = Date.now(); + + const MAX_NOTE_COUNT_PER_QUERY = 50; + + //#retion queries + // We use string literals instead of query builder for several reasons: + // - for removeCondition, we need to use it in having clause, which is not supported by Brackets. + // - for recursive part, we need to preserve the order of columns, but typeorm query builder does not guarantee the order of columns in the result query + + // The condition for removing the notes. + // The note must be: + // - old enough (older than the newestLimit) + // - a remote note (userHost is not null). + // - not have clipped + // - not have pinned on the user profile + // - not has been favorite by any user + const removeCondition = 'note.id < :newestLimit' + + ' AND note."clippedCount" = 0' + + ' AND note."userHost" IS NOT NULL' + // using both userId and noteId instead of just noteId to use index on user_note_pining table. + // This is safe because notes are only pinned by the user who created them. + + ' AND NOT EXISTS(SELECT 1 FROM "user_note_pining" WHERE "noteId" = note."id" AND "userId" = note."userId")' + // We cannot use userId trick because users can favorite notes from other users. + + ' AND NOT EXISTS(SELECT 1 FROM "note_favorite" WHERE "noteId" = note."id")' + ; + + // The initiator query contains the oldest ${MAX_NOTE_COUNT_PER_QUERY} remote non-clipped notes + const initiatorQuery = this.notesRepository.createQueryBuilder('note') + .select('note.id', 'id') + .where(removeCondition) + .andWhere('note.id > :cursor') + .orderBy('note.id', 'ASC') + .limit(MAX_NOTE_COUNT_PER_QUERY); + + // The union query queries the related notes and replies related to the initiator query + const unionQuery = ` + SELECT "note"."id", "note"."replyId", "note"."renoteId", rn."initiatorId" + FROM "note" "note" + INNER JOIN "related_notes" "rn" + ON "note"."replyId" = rn.id + OR "note"."renoteId" = rn.id + OR "note"."id" = rn."replyId" + OR "note"."id" = rn."renoteId" + `; + + const selectRelatedNotesFromInitiatorIdsQuery = ` + SELECT "note"."id" AS "id", "note"."replyId" AS "replyId", "note"."renoteId" AS "renoteId", "note"."id" AS "initiatorId" + FROM "note" "note" WHERE "note"."id" IN (:...initiatorIds) + `; + + const recursiveQuery = `(${selectRelatedNotesFromInitiatorIdsQuery}) UNION (${unionQuery})`; + + const removableInitiatorNotesQuery = this.notesRepository.createQueryBuilder('note') + .select('rn."initiatorId"') + .innerJoin('related_notes', 'rn', 'note.id = rn.id') + .groupBy('rn."initiatorId"') + .having(`bool_and(${removeCondition})`); + + const notesQuery = this.notesRepository.createQueryBuilder('note') + .addCommonTableExpression(recursiveQuery, 'related_notes', { recursive: true }) + .select('note.id', 'id') + .addSelect('rn."initiatorId"') + .innerJoin('related_notes', 'rn', 'note.id = rn.id') + .where(`rn."initiatorId" IN (${removableInitiatorNotesQuery.getQuery()})`) + .distinctOn(['note.id']); + //#endregion + + const stats = { + deletedCount: 0, + oldest: null as number | null, + newest: null as number | null, + }; + + // The date limit for the newest note to be considered for deletion. + // All notes newer than this limit will always be retained. + const newestLimit = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes)); + + let cursor = '0'; // oldest note ID to start from + + while (true) { + //#region check time + const batchBeginAt = Date.now(); + + const elapsed = batchBeginAt - startAt; + + if (elapsed >= maxDuration) { + this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`); + job.log('Reached maximum duration, stopping cleaning.'); + job.updateProgress(100); + break; + } + + job.updateProgress((elapsed / maxDuration) * 100); + //#endregion + + // First, we fetch the initiator notes that are older than the newestLimit. + const initiatorNotes: { id: MiNote['id'] }[] = await initiatorQuery.setParameters({ cursor, newestLimit }).getRawMany(); + + // update the cursor to the newest initiatorId found in the fetched notes. + const newCursor = initiatorNotes.reduce((max, note) => note.id > max ? note.id : max, cursor); + + if (initiatorNotes.length === 0 || cursor === newCursor || newCursor >= newestLimit) { + // If no notes were found or the cursor did not change, we can stop. + job.log('No more notes to clean. (no initiator notes found or cursor did not change.)'); + break; + } + + const notes: { id: MiNote['id'], initiatorId: MiNote['id'] }[] = await notesQuery.setParameters({ + initiatorIds: initiatorNotes.map(note => note.id), + newestLimit, + }).getRawMany(); + + cursor = newCursor; + + if (notes.length > 0) { + await this.notesRepository.delete(notes.map(note => note.id)); + + for (const { id } of notes) { + const t = this.idService.parse(id).date.getTime(); + if (stats.oldest === null || t < stats.oldest) { + stats.oldest = t; + } + if (stats.newest === null || t > stats.newest) { + stats.newest = t; + } + } + + stats.deletedCount += notes.length; + } + + job.log(`Deleted ${notes.length} from ${initiatorNotes.length} initiators; ${Date.now() - batchBeginAt}ms`); + + if (initiatorNotes.length < MAX_NOTE_COUNT_PER_QUERY) { + // If we fetched less than the maximum, it means there are no more notes to process. + job.log(`No more notes to clean. (fewer than MAX_NOTE_COUNT_PER_QUERY =${MAX_NOTE_COUNT_PER_QUERY}.)`); + break; + } + + await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db + } + + this.logger.succ('cleaning of remote notes completed.'); + + return { + deletedCount: stats.deletedCount, + oldest: stats.oldest, + newest: stats.newest, + skipped: false, + }; + } +} diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 291fa4a6d8..5a8a8ca940 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -43,6 +43,10 @@ export class DeleteDriveFilesProcessorService { let deletedCount = 0; let cursor: MiDriveFile['id'] | null = null; + const total = await this.driveFilesRepository.countBy({ + userId: user.id, + }); + while (true) { const files = await this.driveFilesRepository.find({ where: { @@ -67,11 +71,7 @@ export class DeleteDriveFilesProcessorService { deletedCount++; } - const total = await this.driveFilesRepository.countBy({ - userId: user.id, - }); - - job.updateProgress(deletedCount / total); + job.updateProgress(deletedCount / total * 100); } this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index ecc439db69..cca7cdf9da 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -58,6 +58,10 @@ export class ExportBlockingProcessorService { let exportedCount = 0; let cursor: MiBlocking['id'] | null = null; + const total = await this.blockingsRepository.countBy({ + blockerId: user.id, + }); + while (true) { const blockings = await this.blockingsRepository.find({ where: { @@ -97,11 +101,7 @@ export class ExportBlockingProcessorService { exportedCount++; } - const total = await this.blockingsRepository.countBy({ - blockerId: user.id, - }); - - job.updateProgress(exportedCount / total); + job.updateProgress(exportedCount / total * 100); } stream.end(); diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts index 583ddbb745..486dc4c01f 100644 --- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -95,6 +95,10 @@ export class ExportClipsProcessorService { let exportedClipsCount = 0; let cursor: MiClip['id'] | null = null; + const total = await this.clipsRepository.countBy({ + userId: user.id, + }); + while (true) { const clips = await this.clipsRepository.find({ where: { @@ -126,11 +130,7 @@ export class ExportClipsProcessorService { exportedClipsCount++; } - const total = await this.clipsRepository.countBy({ - userId: user.id, - }); - - job.updateProgress(exportedClipsCount / total); + job.updateProgress(exportedClipsCount / total * 100); } } diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index b81feece01..7918c8ccb5 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -78,6 +78,10 @@ export class ExportFavoritesProcessorService { let exportedFavoritesCount = 0; let cursor: MiNoteFavorite['id'] | null = null; + const total = await this.noteFavoritesRepository.countBy({ + userId: user.id, + }); + while (true) { const favorites = await this.noteFavoritesRepository.find({ where: { @@ -109,11 +113,7 @@ export class ExportFavoritesProcessorService { exportedFavoritesCount++; } - const total = await this.noteFavoritesRepository.countBy({ - userId: user.id, - }); - - job.updateProgress(exportedFavoritesCount / total); + job.updateProgress(exportedFavoritesCount / total * 100); } await write(']'); diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 903f962515..91c39cb758 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -94,7 +94,8 @@ export class ExportFollowingProcessorService { continue; } - const content = this.utilityService.getFullApAccount(u.username, u.host); + const userAcct = this.utilityService.getFullApAccount(u.username, u.host); + const content = `${userAcct},withReplies=${following.withReplies}`; await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index f9867ade29..59448ccd34 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -58,6 +58,10 @@ export class ExportMutingProcessorService { let exportedCount = 0; let cursor: MiMuting['id'] | null = null; + const total = await this.mutingsRepository.countBy({ + muterId: user.id, + }); + while (true) { const mutes = await this.mutingsRepository.find({ where: { @@ -98,11 +102,7 @@ export class ExportMutingProcessorService { exportedCount++; } - const total = await this.mutingsRepository.countBy({ - muterId: user.id, - }); - - job.updateProgress(exportedCount / total); + job.updateProgress(exportedCount / total * 100); } stream.end(); diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 9e2b678219..3b68a4277a 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -37,6 +37,8 @@ class NoteStream extends ReadableStream> { let exportedNotesCount = 0; let cursor: MiNote['id'] | null = null; + const totalPromise = notesRepository.countBy({ userId }); + const serialize = ( note: MiNote, poll: MiPoll | null, @@ -88,8 +90,8 @@ class NoteStream extends ReadableStream> { exportedNotesCount++; } - const total = await notesRepository.countBy({ userId }); - job.updateProgress(exportedNotesCount / total); + const total = await totalPromise; + job.updateProgress(exportedNotesCount / total * 100); }, }); } diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index c483d79854..733e75f65f 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -67,10 +67,12 @@ export class ExportUserListsProcessorService { const users = await this.usersRepository.findBy({ id: In(memberships.map(j => j.userId)), }); + const usersWithReplies = new Set(memberships.filter(m => m.withReplies).map(m => m.userId)); for (const u of users) { const acct = this.utilityService.getFullApAccount(u.username, u.host); - const content = `${list.name},${acct}`; + // 3rd column and later will be key=value pairs + const content = `${list.name},${acct},withReplies=${usersWithReplies.has(u.id)}`; await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 70c9f3a096..03663d3b06 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -67,8 +67,19 @@ export class ImportFollowingProcessorService { const user = job.data.user; try { - const acct = line.split(',')[0].trim(); + const parts = line.split(','); + const acct = parts[0].trim(); const { username, host } = Acct.parse(acct); + let withReplies: boolean | null = null; + + for (const keyValue of parts.slice(2)) { + const [key, value] = keyValue.split('='); + switch (key) { + case 'withReplies': + withReplies = value === 'true'; + break; + } + } if (!host) return; @@ -95,7 +106,7 @@ export class ImportFollowingProcessorService { this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`); - this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]); + await this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: withReplies ?? job.data.withReplies }]); } catch (e) { this.logger.warn(`Error: ${e}`); } diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index db9255b35d..bf061a1f78 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -70,8 +70,19 @@ export class ImportUserListsProcessorService { linenum++; try { - const listName = line.split(',')[0].trim(); - const { username, host } = Acct.parse(line.split(',')[1].trim()); + const parts = line.split(','); + const listName = parts[0].trim(); + const { username, host } = Acct.parse(parts[1].trim()); + let withReplies = false; + + for (const keyValue of parts.slice(2)) { + const [key, value] = keyValue.split('='); + switch (key) { + case 'withReplies': + withReplies = value === 'true'; + break; + } + } let list = await this.userListsRepository.findOneBy({ userId: user.id, @@ -100,7 +111,9 @@ export class ImportUserListsProcessorService { if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; - this.userListService.addMember(target, list!, user); + await this.userListService.addMember(target, list, user, { + withReplies: withReplies, + }); } catch (e) { this.logger.warn(`Error in line:${linenum} ${e}`); } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index f7b22c44c4..a5fb5b82e3 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -482,9 +482,19 @@ export class ActivityPubServerService { return true; }, dbFallback: async (untilId, sinceId, limit) => { - return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id); + return await this.getUserNotesFromDb({ + untilId, + sinceId, + limit, + userId: user.id, + }); }, - }) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id); + }) : await this.getUserNotesFromDb({ + untilId: untilId ?? null, + sinceId: sinceId ?? null, + limit, + userId: user.id, + }); if (sinceId) notes.reverse(); @@ -523,16 +533,21 @@ export class ActivityPubServerService { } @bindThis - private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) { - return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) - .andWhere('note.userId = :userId', { userId }) + private async getUserNotesFromDb(ps: { + untilId: string | null, + sinceId: string | null, + limit: number, + userId: MiUser['id'], + }) { + return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: ps.userId }) .andWhere(new Brackets(qb => { qb .where('note.visibility = \'public\'') .orWhere('note.visibility = \'home\''); })) .andWhere('note.localOnly = FALSE') - .limit(limit) + .limit(ps.limit) .getMany(); } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index c859f1d82c..23c085ee27 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -108,7 +108,7 @@ export class ServerService implements OnApplicationShutdown { // this will break lookup that involve copying a URL from a third-party server, like trying to lookup http://charlie.example.com/@alice@alice.com // // this is not required by standard but protect us from peers that did not validate final URL. - if (this.config.disallowExternalApRedirect) { + if (!this.meta.allowExternalApRedirect) { const maybeApLookupRegex = /application\/activity\+json|application\/ld\+json.+activitystreams/i; fastify.addHook('onSend', (request, reply, _, done) => { const location = reply.getHeader('location'); @@ -133,8 +133,8 @@ export class ServerService implements OnApplicationShutdown { reply.header('content-type', 'text/plain; charset=utf-8'); reply.header('link', `<${encodeURI(location)}>; rel="canonical"`); done(null, [ - "Refusing to relay remote ActivityPub object lookup.", - "", + 'Refusing to relay remote ActivityPub object lookup.', + '', `Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`, ].join('\n')); }); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index a42fdaf730..7a4af407a3 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown { if (factor > 0) { // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { - if ('info' in err) { - // errはLimiter.LimiterInfoであることが期待される - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }, err.info); - } else { - throw new TypeError('information must be a rate-limiter information.'); - } - }); + const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor); + if (rateLimit != null) { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }, rateLimit.info); + } } } diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 444e6db744..8f4213dfb6 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -40,8 +40,8 @@ export class GetterService { } @bindThis - public async getNoteWithUser(noteId: MiNote['id']) { - const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); + public async getNoteWithRelations(noteId: MiNote['id']) { + const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] }); if (note == null) { throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index 52d73baa0a..a730d8c60e 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type { IEndpointMeta } from './endpoints.js'; +type RateLimitInfo = { + code: 'BRIEF_REQUEST_INTERVAL', + info: Limiter.LimiterInfo, +} | { + code: 'RATE_LIMIT_EXCEEDED', + info: Limiter.LimiterInfo, +}; + @Injectable() export class RateLimiterService { private logger: Logger; @@ -31,77 +39,55 @@ export class RateLimiterService { } @bindThis - public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { - { - if (this.disabled) { - return Promise.resolve(); - } + private checkLimiter(options: Limiter.LimiterOption): Promise { + return new Promise((resolve, reject) => { + new Limiter(options).get((err, info) => { + if (err) { + return reject(err); + } + resolve(info); + }); + }); + } - // Short-term limit - const min = new Promise((ok, reject) => { - const minIntervalLimiter = new Limiter({ - id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval! * factor, - max: 1, - db: this.redisClient, - }); + @bindThis + public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1): Promise { + if (this.disabled) { + return null; + } - minIntervalLimiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'BRIEF_REQUEST_INTERVAL', info }); - } else { - if (hasLongTermLimit) { - return max.then(ok, reject); - } else { - return ok(); - } - } - }); + // Short-term limit + if (limitation.minInterval != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}:min`, + duration: limitation.minInterval * factor, + max: 1, + db: this.redisClient, }); - // Long term limit - const max = new Promise((ok, reject) => { - const limiter = new Limiter({ - id: `${actor}:${limitation.key}`, - duration: limitation.duration! * factor, - max: limitation.max! / factor, - db: this.redisClient, - }); + this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - limiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'RATE_LIMIT_EXCEEDED', info }); - } else { - return ok(); - } - }); - }); - - const hasShortTermLimit = typeof limitation.minInterval === 'number'; - - const hasLongTermLimit = - typeof limitation.duration === 'number' && - typeof limitation.max === 'number'; - - if (hasShortTermLimit) { - return min; - } else if (hasLongTermLimit) { - return max; - } else { - return Promise.resolve(); + if (info.remaining === 0) { + return { code: 'BRIEF_REQUEST_INTERVAL', info }; } } + + // Long term limit + if (limitation.duration != null && limitation.max != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}`, + duration: limitation.duration, + max: limitation.max / factor, + db: this.redisClient, + }); + + this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); + + if (info.remaining === 0) { + return { code: 'RATE_LIMIT_EXCEEDED', info }; + } + } + + return null; } } diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 1d983ca4bc..3e889372d8 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -89,10 +89,9 @@ export class SigninApiService { return { error }; } - try { // not more than 1 attempt per second and not more than 10 attempts per hour - await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); - } catch (err) { + const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); + if (rateLimit != null) { reply.code(429); return { error: { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 3ec5e5d3e6..53336a087d 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -129,7 +129,8 @@ export class SignupApiService { let ticket: MiRegistrationTicket | null = null; - if (this.meta.disableRegistration) { + // テスト時はこの機構は障害となるため無効にする + if (process.env.NODE_ENV !== 'test' && this.meta.disableRegistration) { if (invitationCode == null || typeof invitationCode !== 'string') { reply.code(400); return; diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index bd466b3cad..c0c43dd5c9 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -70,6 +70,7 @@ export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-dela export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js'; export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js'; export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js'; +export * as 'admin/queue/show-job-logs' from './endpoints/admin/queue/show-job-logs.js'; export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js'; export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js'; export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js'; @@ -168,6 +169,7 @@ export * as 'clips/update' from './endpoints/clips/update.js'; export * as 'drive' from './endpoints/drive.js'; export * as 'drive/files' from './endpoints/drive/files.js'; export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js'; +export * as 'drive/files/attached-chat-messages' from './endpoints/drive/files/attached-chat-messages.js'; export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js'; export * as 'drive/files/create' from './endpoints/drive/files/create.js'; export * as 'drive/files/delete' from './endpoints/drive/files/delete.js'; @@ -175,6 +177,7 @@ export * as 'drive/files/find' from './endpoints/drive/files/find.js'; export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js'; export * as 'drive/files/show' from './endpoints/drive/files/show.js'; export * as 'drive/files/update' from './endpoints/drive/files/update.js'; +export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js'; export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js'; export * as 'drive/folders' from './endpoints/drive/folders.js'; export * as 'drive/folders/create' from './endpoints/drive/folders/create.js'; @@ -207,6 +210,7 @@ export * as 'flash/my-likes' from './endpoints/flash/my-likes.js'; export * as 'flash/show' from './endpoints/flash/show.js'; export * as 'flash/unlike' from './endpoints/flash/unlike.js'; export * as 'flash/update' from './endpoints/flash/update.js'; +export * as 'flash/search' from './endpoints/flash/search.js'; export * as 'following/create' from './endpoints/following/create.js'; export * as 'following/delete' from './endpoints/following/delete.js'; export * as 'following/invalidate' from './endpoints/following/invalidate.js'; @@ -306,6 +310,11 @@ export * as 'notes/clips' from './endpoints/notes/clips.js'; export * as 'notes/conversation' from './endpoints/notes/conversation.js'; export * as 'notes/create' from './endpoints/notes/create.js'; export * as 'notes/delete' from './endpoints/notes/delete.js'; +export * as 'notes/drafts/list' from './endpoints/notes/drafts/list.js'; +export * as 'notes/drafts/create' from './endpoints/notes/drafts/create.js'; +export * as 'notes/drafts/delete' from './endpoints/notes/drafts/delete.js'; +export * as 'notes/drafts/update' from './endpoints/notes/drafts/update.js'; +export * as 'notes/drafts/count' from './endpoints/notes/drafts/count.js'; export * as 'notes/favorites/create' from './endpoints/notes/favorites/create.js'; export * as 'notes/favorites/delete' from './endpoints/notes/favorites/delete.js'; export * as 'notes/featured' from './endpoints/notes/featured.js'; @@ -427,4 +436,5 @@ export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitat export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js'; export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js'; export * as 'chat/history' from './endpoints/chat/history.js'; +export * as 'chat/read-all' from './endpoints/chat/read-all.js'; export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 0dbfaae054..ff7133e73a 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -98,6 +98,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, state: { type: 'string', nullable: true, default: null }, reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, @@ -115,7 +117,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.abuseUserReportsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.abuseUserReportsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); switch (ps.state) { case 'resolved': query.andWhere('report.resolved = TRUE'); break; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 6406709cda..4f897d98e4 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, publishing: { type: 'boolean', default: null, nullable: true }, }, required: [], @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); if (ps.publishing === true) { query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() }); } else if (ps.publishing === false) { diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 7596bf44e3..81a788de2b 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -68,6 +68,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, userId: { type: 'string', format: 'misskey:id', nullable: true }, status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' }, }, @@ -87,7 +89,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); if (ps.status === 'archived') { query.andWhere('announcement.isActive = false'); diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index d4d9a7235b..765bfd6766 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -71,6 +71,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, userId: { type: 'string', format: 'misskey:id', nullable: true }, }, required: [], diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index 915d777e77..59b02482a2 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, userId: { type: 'string', format: 'misskey:id', nullable: true }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, @@ -57,7 +59,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); if (ps.userId) { query.andWhere('file.userId = :userId', { userId: ps.userId }); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index a7136d8c8c..b84a5c73f9 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -162,14 +162,21 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - url: { type: 'string' }, - }, anyOf: [ - { required: ['fileId'] }, - { required: ['url'] }, + { + type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], + }, + { + type: 'object', + properties: { + url: { type: 'string' }, + }, + required: ['url'], + }, ], } as const; @@ -186,15 +193,11 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({ - where: [{ - url: ps.url, - }, { - thumbnailUrl: ps.url, - }, { - webpublicUrl: ps.url, - }], - }); + const file = await this.driveFilesRepository.findOneBy( + 'fileId' in ps + ? { id: ps.fileId } + : [{ url: ps.url }, { thumbnailUrl: ps.url }, { webpublicUrl: ps.url }], + ); if (file == null) { throw new ApiError(meta.errors.noSuchFile); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index b44007962d..660aa55bf8 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -74,6 +74,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -89,7 +91,7 @@ export default class extends Endpoint { // eslint- private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { - const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); + const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); if (ps.host == null) { q.andWhere('emoji.host IS NOT NULL'); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 4342e178cc..34d200455e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -68,6 +68,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -82,7 +84,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) + const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('emoji.host IS NULL'); let emojis: MiEmoji[]; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 6834a6d213..7bde10af46 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -37,29 +37,45 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, - fileId: { type: 'string', format: 'misskey:id' }, - category: { - type: 'string', - nullable: true, - description: 'Use `null` to reset the category.', + allOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + }, + required: ['id'], + }, + { + type: 'object', + properties: { + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + }, + required: ['name'], + }, + ], + }, + { + type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, + }, }, - aliases: { type: 'array', items: { - type: 'string', - } }, - license: { type: 'string', nullable: true }, - isSensitive: { type: 'boolean' }, - localOnly: { type: 'boolean' }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { - type: 'string', - } }, - }, - anyOf: [ - { required: ['id'] }, - { required: ['name'] }, ], } as const; @@ -78,10 +94,9 @@ export default class extends Endpoint { // eslint- if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } - // JSON schemeのanyOfの型変換がうまくいっていないらしい - const required = { id: ps.id, name: ps.name } as - | { id: MiEmoji['id']; name?: string } - | { id?: MiEmoji['id']; name: string }; + const required = 'id' in ps + ? { id: ps.id, name: 'name' in ps ? ps.name as string : undefined } + : { name: ps.name }; const error = await this.customEmojiService.update({ ...required, diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts index 5ecae3161a..e52b177e2b 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -68,6 +68,8 @@ export default class extends Endpoint { // eslint- for (let i = 0; i < ps.count; i++) { ticketsPromises.push(this.registrationTicketsRepository.insertOne({ id: this.idService.gen(), + createdBy: me, + createdById: me.id, expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, code: generateInviteCode(), })); diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index cd36985485..4d3f6d6cd8 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -495,6 +495,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + urlPreviewAllowRedirect: { + type: 'boolean', + optional: false, nullable: false, + }, urlPreviewTimeout: { type: 'number', optional: false, nullable: false, @@ -555,6 +559,30 @@ export const meta = { enum: ['all', 'local', 'none'], optional: false, nullable: false, }, + proxyRemoteFiles: { + type: 'boolean', + optional: false, nullable: false, + }, + signToActivityPubGet: { + type: 'boolean', + optional: false, nullable: false, + }, + allowExternalApRedirect: { + type: 'boolean', + optional: false, nullable: false, + }, + enableRemoteNotesCleaning: { + type: 'boolean', + optional: false, nullable: false, + }, + remoteNotesCleaningExpiryDaysForEachNotes: { + type: 'number', + optional: false, nullable: false, + }, + remoteNotesCleaningMaxProcessingDurationInMinutes: { + type: 'number', + optional: false, nullable: false, + }, }, }, } as const; @@ -692,6 +720,7 @@ export default class extends Endpoint { // eslint- notesPerOneAd: instance.notesPerOneAd, summalyProxy: instance.urlPreviewSummaryProxyUrl, urlPreviewEnabled: instance.urlPreviewEnabled, + urlPreviewAllowRedirect: instance.urlPreviewAllowRedirect, urlPreviewTimeout: instance.urlPreviewTimeout, urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength, urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, @@ -702,6 +731,12 @@ export default class extends Endpoint { // eslint- deliverSuspendedSoftware: instance.deliverSuspendedSoftware, singleUserMode: instance.singleUserMode, ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor, + proxyRemoteFiles: instance.proxyRemoteFiles, + signToActivityPubGet: instance.signToActivityPubGet, + allowExternalApRedirect: instance.allowExternalApRedirect, + enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning, + remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes, + remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts index 79731c9786..a68e95bf3f 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,13 +13,22 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + optional: false, nullable: false, + ref: 'QueueJob', + }, + }, } as const; export const paramDef = { type: 'object', properties: { queue: { type: 'string', enum: QUEUE_TYPES }, - state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } }, + state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed', 'paused'] } }, search: { type: 'string' }, }, required: ['queue', 'state'], diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts index 10ce48332a..0098160165 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,6 +13,118 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + name: { + type: 'string', + optional: false, nullable: false, + enum: QUEUE_TYPES, + }, + qualifiedName: { + type: 'string', + optional: false, nullable: false, + }, + counts: { + type: 'object', + optional: false, nullable: false, + additionalProperties: { + type: 'number', + }, + }, + isPaused: { + type: 'boolean', + optional: false, nullable: false, + }, + metrics: { + type: 'object', + optional: false, nullable: false, + properties: { + completed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + failed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + }, + }, + db: { + type: 'object', + optional: false, nullable: false, + properties: { + version: { + type: 'string', + optional: false, nullable: false, + }, + mode: { + type: 'string', + optional: false, nullable: false, + enum: ['cluster', 'standalone', 'sentinel'], + }, + runId: { + type: 'string', + optional: false, nullable: false, + }, + processId: { + type: 'string', + optional: false, nullable: false, + }, + port: { + type: 'number', + optional: false, nullable: false, + }, + os: { + type: 'string', + optional: false, nullable: false, + }, + uptime: { + type: 'number', + optional: false, nullable: false, + }, + memory: { + type: 'object', + optional: false, nullable: false, + properties: { + total: { + type: 'number', + optional: false, nullable: false, + }, + used: { + type: 'number', + optional: false, nullable: false, + }, + fragmentationRatio: { + type: 'number', + optional: false, nullable: false, + }, + peak: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + clients: { + type: 'object', + optional: false, nullable: false, + properties: { + blocked: { + type: 'number', + optional: false, nullable: false, + }, + connected: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + }, + } + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts index 3a38275f60..8d27e38c84 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,6 +13,47 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + name: { + type: 'string', + optional: false, nullable: false, + enum: QUEUE_TYPES, + }, + counts: { + type: 'object', + optional: false, nullable: false, + additionalProperties: { + type: 'number', + }, + }, + isPaused: { + type: 'boolean', + optional: false, nullable: false, + }, + metrics: { + type: 'object', + optional: false, nullable: false, + properties: { + completed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + failed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/show-job-logs.ts b/packages/backend/src/server/api/endpoints/admin/queue/show-job-logs.ts new file mode 100644 index 0000000000..b9292ed12a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/queue/show-job-logs.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'read:admin:queue', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + optional: false, nullable: false, + type: 'string', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + queue: { type: 'string', enum: QUEUE_TYPES }, + jobId: { type: 'string' }, + }, + required: ['queue', 'jobId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + return this.queueService.queueGetJobLogs(ps.queue, ps.jobId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts index 63747b5540..1735c22674 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,6 +13,11 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + optional: false, nullable: false, + ref: 'QueueJob', + }, } as const; export const paramDef = { @@ -28,7 +32,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private moderationLogService: ModerationLogService, private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 198166bec2..63a8d513fd 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -49,6 +49,8 @@ export const paramDef = { roleId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: ['roleId'], @@ -76,7 +78,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRole); } - const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('assign.roleId = :roleId', { roleId: role.id }) .andWhere(new Brackets(qb => { qb diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index 58c5f1f60a..697fdf4210 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogEntityService } from '@/core/entities/ModerationLogEntityService.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], @@ -63,8 +64,11 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, type: { type: 'string', nullable: true }, userId: { type: 'string', format: 'misskey:id', nullable: true }, + search: { type: 'string', nullable: true }, }, required: [], } as const; @@ -79,19 +83,24 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('log'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); if (ps.type != null) { - query.andWhere('report.type = :type', { type: ps.type }); + query.andWhere('log.type = :type', { type: ps.type }); } if (ps.userId != null) { - query.andWhere('report.userId = :userId', { userId: ps.userId }); + query.andWhere('log.userId = :userId', { userId: ps.userId }); } - const reports = await query.limit(ps.limit).getMany(); + if (ps.search != null) { + const escapedSearch = sqlLikeEscape(ps.search); + query.andWhere('log.info::text ILIKE :search', { search: `%${escapedSearch}%` }); + } - return await this.moderationLogEntityService.packMany(reports); + const logs = await query.limit(ps.limit).getMany(); + + return await this.moderationLogEntityService.packMany(logs); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts index 28071e7a33..93d293ed41 100644 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts @@ -48,8 +48,8 @@ export const paramDef = { }, secret: { type: 'string', - minLength: 1, maxLength: 1024, + default: '', }, }, required: [ @@ -57,7 +57,6 @@ export const paramDef = { 'name', 'on', 'url', - 'secret', ], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts index 8d68bb8f87..e021806398 100644 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts @@ -52,8 +52,8 @@ export const paramDef = { }, secret: { type: 'string', - minLength: 1, maxLength: 1024, + default: '', }, }, required: [ @@ -62,7 +62,6 @@ export const paramDef = { 'name', 'on', 'url', - 'secret', ], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index a96fbd759c..08cea23119 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -170,6 +170,7 @@ export const paramDef = { description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', }, urlPreviewEnabled: { type: 'boolean' }, + urlPreviewAllowRedirect: { type: 'boolean' }, urlPreviewTimeout: { type: 'integer' }, urlPreviewMaximumContentLength: { type: 'integer' }, urlPreviewRequireContentLength: { type: 'boolean' }, @@ -201,6 +202,12 @@ export const paramDef = { type: 'string', enum: ['all', 'local', 'none'], }, + proxyRemoteFiles: { type: 'boolean' }, + signToActivityPubGet: { type: 'boolean' }, + allowExternalApRedirect: { type: 'boolean' }, + enableRemoteNotesCleaning: { type: 'boolean' }, + remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' }, + remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' }, }, required: [], } as const; @@ -661,6 +668,10 @@ export default class extends Endpoint { // eslint- set.urlPreviewEnabled = ps.urlPreviewEnabled; } + if (ps.urlPreviewAllowRedirect !== undefined) { + set.urlPreviewAllowRedirect = ps.urlPreviewAllowRedirect; + } + if (ps.urlPreviewTimeout !== undefined) { set.urlPreviewTimeout = ps.urlPreviewTimeout; } @@ -703,6 +714,30 @@ export default class extends Endpoint { // eslint- set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor; } + if (ps.proxyRemoteFiles !== undefined) { + set.proxyRemoteFiles = ps.proxyRemoteFiles; + } + + if (ps.signToActivityPubGet !== undefined) { + set.signToActivityPubGet = ps.signToActivityPubGet; + } + + if (ps.allowExternalApRedirect !== undefined) { + set.allowExternalApRedirect = ps.allowExternalApRedirect; + } + + if (ps.enableRemoteNotesCleaning !== undefined) { + set.enableRemoteNotesCleaning = ps.enableRemoteNotesCleaning; + } + + if (ps.remoteNotesCleaningExpiryDaysForEachNotes !== undefined) { + set.remoteNotesCleaningExpiryDaysForEachNotes = ps.remoteNotesCleaningExpiryDaysForEachNotes; + } + + if (ps.remoteNotesCleaningMaxProcessingDurationInMinutes !== undefined) { + set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index ff8dd73605..2ad1702f72 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -33,6 +33,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, isActive: { type: 'boolean', default: true }, }, required: [], @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private announcementEntityService: AnnouncementEntityService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('announcement.isActive = :isActive', { isActive: ps.isActive }) .andWhere(new Brackets(qb => { if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id }); diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index f37cdc6658..b2d9cea03c 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -111,11 +111,8 @@ export default class extends Endpoint { // eslint- // NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。 // https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255 - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); const notes = await query.getMany(); if (sinceId != null && untilId == null) { diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 8431fa6b34..9885d32eda 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('blocking.blockerId = :meId', { meId: me.id }); const blockings = await query diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index 294b5e4bc4..faceb03631 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -33,6 +33,8 @@ export const paramDef = { properties: { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 }, }, required: [], @@ -53,8 +55,8 @@ export default class extends Endpoint { // eslint- this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId, - null, - null, + ps.sinceDate, + ps.untilDate, 'followeeId', ) .andWhere({ followerId: me.id }); diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index daab685f1b..d22ac18b4b 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -33,6 +33,8 @@ export const paramDef = { properties: { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 }, }, required: [], @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('channel.isArchived = FALSE') .andWhere({ userId: me.id }); diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index ae32203603..7b6c4db91c 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -35,6 +35,8 @@ export const paramDef = { type: { type: 'string', enum: ['nameAndDescription', 'nameOnly'], default: 'nameAndDescription' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 }, }, required: ['query'], @@ -50,7 +52,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('channel.isArchived = FALSE'); if (ps.query !== '') { diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 2401ab8208..46b050d4b4 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -121,12 +121,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - if (me) { - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); - } + this.queryService.generateBaseNoteFilteringQuery(query, me); //#endregion return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts index c0e344b889..dd598b5628 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts @@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js'; import { ChatService } from '@/core/ChatService.js'; import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['chat'], @@ -42,6 +43,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, roomId: { type: 'string', format: 'misskey:id' }, }, required: ['roomId'], @@ -52,8 +55,12 @@ export default class extends Endpoint { // eslint- constructor( private chatEntityService: ChatEntityService, private chatService: ChatService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + await this.chatService.checkChatAvailability(me.id, 'read'); const room = await this.chatService.findRoomById(ps.roomId); @@ -65,7 +72,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRoom); } - const messages = await this.chatService.roomTimeline(room.id, ps.limit, ps.sinceId, ps.untilId); + const messages = await this.chatService.roomTimeline(room.id, ps.limit, sinceId, untilId); this.chatService.readRoomChatMessage(me.id, room.id); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts index a057e2e088..52a13b2178 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts @@ -10,6 +10,7 @@ import { GetterService } from '@/server/api/GetterService.js'; import { ChatService } from '@/core/ChatService.js'; import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['chat'], @@ -43,6 +44,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, userId: { type: 'string', format: 'misskey:id' }, }, required: ['userId'], @@ -54,8 +57,12 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, private chatService: ChatService, private getterService: GetterService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + await this.chatService.checkChatAvailability(me.id, 'read'); const other = await this.getterService.getUser(ps.userId).catch(err => { @@ -63,7 +70,7 @@ export default class extends Endpoint { // eslint- throw err; }); - const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, ps.sinceId, ps.untilId); + const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, sinceId, untilId); this.chatService.readUserChatMessage(me.id, other.id); diff --git a/packages/backend/src/server/api/endpoints/chat/read-all.ts b/packages/backend/src/server/api/endpoints/chat/read-all.ts new file mode 100644 index 0000000000..e2d9601aa6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/read-all.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + + await this.chatService.readAllChatMessages(me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts index 8a02d1c704..f4f91a7d8f 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts @@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js'; import { ChatService } from '@/core/ChatService.js'; import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['chat'], @@ -37,6 +38,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, } as const; @@ -45,11 +48,15 @@ export default class extends Endpoint { // eslint- constructor( private chatEntityService: ChatEntityService, private chatService: ChatService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + await this.chatService.checkChatAvailability(me.id, 'read'); - const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); + const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, sinceId, untilId); return this.chatEntityService.packRoomInvitations(invitations, me); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts index 0702ba086c..827bef731c 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { ChatService } from '@/core/ChatService.js'; import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['chat'], @@ -44,6 +45,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['roomId'], } as const; @@ -53,8 +56,12 @@ export default class extends Endpoint { // eslint- constructor( private chatService: ChatService, private chatEntityService: ChatEntityService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + await this.chatService.checkChatAvailability(me.id, 'read'); const room = await this.chatService.findMyRoomById(me.id, ps.roomId); @@ -62,7 +69,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRoom); } - const invitations = await this.chatService.getSentRoomInvitationsWithPagination(ps.roomId, ps.limit, ps.sinceId, ps.untilId); + const invitations = await this.chatService.getSentRoomInvitationsWithPagination(ps.roomId, ps.limit, sinceId, untilId); return this.chatEntityService.packRoomInvitations(invitations, me); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts index ba9242c762..b061da6d9c 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts @@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js'; import { ChatService } from '@/core/ChatService.js'; import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['chat'], @@ -37,6 +38,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, } as const; @@ -45,11 +48,15 @@ export default class extends Endpoint { // eslint- constructor( private chatService: ChatService, private chatEntityService: ChatEntityService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + await this.chatService.checkChatAvailability(me.id, 'read'); - const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId); + const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, sinceId, untilId); return this.chatEntityService.packRoomMemberships(memberships, me, { populateUser: false, diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts index f5ffa21d32..aa3fb5168d 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts @@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js'; import { ChatService } from '@/core/ChatService.js'; import { ApiError } from '@/server/api/error.js'; import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['chat'], @@ -43,6 +44,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['roomId'], } as const; @@ -52,8 +55,12 @@ export default class extends Endpoint { // eslint- constructor( private chatService: ChatService, private chatEntityService: ChatEntityService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + await this.chatService.checkChatAvailability(me.id, 'read'); const room = await this.chatService.findRoomById(ps.roomId); @@ -65,7 +72,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRoom); } - const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, ps.sinceId, ps.untilId); + const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, sinceId, untilId); return this.chatEntityService.packRoomMemberships(memberships, me, { populateUser: true, diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts index accf7e1bee..40c954fcca 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts @@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js'; import { ChatService } from '@/core/ChatService.js'; import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['chat'], @@ -37,6 +38,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, } as const; @@ -45,11 +48,15 @@ export default class extends Endpoint { // eslint- constructor( private chatEntityService: ChatEntityService, private chatService: ChatService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + await this.chatService.checkChatAvailability(me.id, 'read'); - const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); + const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, sinceId, untilId); return this.chatEntityService.packRooms(rooms, me); }); } diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 33f32d1d8a..c4260fd87c 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -4,11 +4,13 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -44,6 +46,9 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + search: { type: 'string', minLength: 1, maxLength: 100, nullable: true }, }, required: ['clipId'], } as const; @@ -76,7 +81,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchClip); } - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .innerJoin(this.clipNotesRepository.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') @@ -91,6 +96,17 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote' }); + this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' }); + } + + if (ps.search != null) { + for (const word of ps.search.trim().split(' ')) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.text ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); + qb.orWhere('note.cw ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); + })); + } } const notes = await query diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index 10c521332d..303a4d4925 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] }, @@ -51,7 +53,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('file.userId = :userId', { userId: me.id }); if (ps.folderId) { diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts new file mode 100644 index 0000000000..b34ac4abd1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { DriveFilesRepository, ChatMessagesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['drive', 'chat'], + + requireCredential: true, + + kind: 'read:drive', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessage', + }, + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '485ce26d-f5d2-4313-9783-e689d131eafb', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.chatMessagesRepository) + private chatMessagesRepository: ChatMessagesRepository, + + private chatService: ChatService, + private chatEntityService: ChatEntityService, + private queryService: QueryService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const isModerator = await this.roleService.isModerator(me); + + if (!isModerator) { + await this.chatService.checkChatAvailability(me.id, 'read'); + } + + const file = await this.driveFilesRepository.findOneBy({ + id: ps.fileId, + userId: isModerator ? undefined : me.id, + }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + + const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); + query.andWhere('message.fileId = :fileId', { fileId: file.id }); + + const messages = await query.limit(ps.limit).getMany(); + + return await this.chatEntityService.packMessagesDetailed(messages, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index b86059b5e7..6bc8730a1e 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -9,8 +9,8 @@ import type { NotesRepository, DriveFilesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['drive', 'notes'], @@ -45,6 +45,8 @@ export const paramDef = { properties: { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, fileId: { type: 'string', format: 'misskey:id' }, }, @@ -75,7 +77,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchFile); } - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); query.andWhere(':file <@ note.fileIds', { file: [file.id] }); const notes = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 11c255a361..7d5c0ccd4d 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -63,6 +63,12 @@ export const meta = { id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a', httpStatusCode: 413, }, + + unallowedFileType: { + message: 'Cannot upload the file because it is an unallowed file type.', + code: 'UNALLOWED_FILE_TYPE', + id: '4becd248-7f2c-48c4-a9f0-75edc4f9a1ea', + }, }, } as const; @@ -123,6 +129,7 @@ export default class extends Endpoint { // eslint- if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded); + if (err.id === 'bd71c601-f9b0-4808-9137-a330647ced9b') throw new ApiError(meta.errors.unallowedFileType); } throw new ApiError(); } finally { diff --git a/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts new file mode 100644 index 0000000000..c8500895eb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { DriveService } from '@/core/DriveService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['drive'], + + requireCredential: true, + + kind: 'write:drive', + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } }, + folderId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: ['fileIds'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index e8f4539d61..9a2e2c73e8 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -43,14 +43,21 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - url: { type: 'string' }, - }, anyOf: [ - { required: ['fileId'] }, - { required: ['url'] }, + { + type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], + }, + { + type: 'object', + properties: { + url: { type: 'string' }, + }, + required: ['url'], + }, ], } as const; @@ -64,21 +71,11 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - let file: MiDriveFile | null = null; - - if (ps.fileId) { - file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - } else if (ps.url) { - file = await this.driveFilesRepository.findOne({ - where: [{ - url: ps.url, - }, { - webpublicUrl: ps.url, - }, { - thumbnailUrl: ps.url, - }], - }); - } + const file = await this.driveFilesRepository.findOneBy( + 'fileId' in ps + ? { id: ps.fileId } + : [{ url: ps.url }, { webpublicUrl: ps.url }, { thumbnailUrl: ps.url }], + ); if (file == null) { throw new ApiError(meta.errors.noSuchFile); diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index 8c4848f8e1..23b3db60a8 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, }, required: [], @@ -49,7 +51,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.driveFoldersRepository.createQueryBuilder('folder'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.driveFoldersRepository.createQueryBuilder('folder'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('folder.userId = :userId', { userId: me.id }); if (ps.folderId) { diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index f7c1ed39b5..8bf83a9653 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, type: { type: 'string', pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, }, required: [], @@ -49,7 +51,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('file.userId = :userId', { userId: me.id }); if (ps.type) { diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index ce4dd13067..296bc7c5a8 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -32,6 +32,8 @@ export const paramDef = { host: { type: 'string' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: ['host'], @@ -47,7 +49,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('following.followeeHost = :host', { host: ps.host }); const followings = await query diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 1a793889c7..091bf442af 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -32,6 +32,8 @@ export const paramDef = { host: { type: 'string' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: ['host'], @@ -47,7 +49,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('following.followerHost = :host', { host: ps.host }); const followings = await query diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 71b1aeb07b..c2d660f163 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -32,6 +32,8 @@ export const paramDef = { host: { type: 'string' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: ['host'], @@ -47,7 +49,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.usersRepository.createQueryBuilder('user'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.usersRepository.createQueryBuilder('user'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('user.host = :host', { host: ps.host }); const users = await query diff --git a/packages/backend/src/server/api/endpoints/flash/my-likes.ts b/packages/backend/src/server/api/endpoints/flash/my-likes.ts index 755cc5acfc..ff9d6c3264 100644 --- a/packages/backend/src/server/api/endpoints/flash/my-likes.ts +++ b/packages/backend/src/server/api/endpoints/flash/my-likes.ts @@ -5,10 +5,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FlashLikesRepository } from '@/models/_.js'; -import { QueryService } from '@/core/QueryService.js'; import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FlashService } from '@/core/FlashService.js'; export const meta = { tags: ['account', 'flash'], @@ -44,6 +43,9 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + search: { type: 'string', minLength: 1, maxLength: 100, nullable: true }, }, required: [], } as const; @@ -51,20 +53,18 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.flashLikesRepository) - private flashLikesRepository: FlashLikesRepository, - private flashLikeEntityService: FlashLikeEntityService, - private queryService: QueryService, + private flashService: FlashService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId) - .andWhere('like.userId = :meId', { meId: me.id }) - .leftJoinAndSelect('like.flash', 'flash'); - - const likes = await query - .limit(ps.limit) - .getMany(); + const likes = await this.flashService.myLikes(me.id, { + sinceId: ps.sinceId, + untilId: ps.untilId, + sinceDate: ps.sinceDate, + untilDate: ps.untilDate, + limit: ps.limit, + search: ps.search, + }); return this.flashLikeEntityService.packMany(likes, me); }); diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts index 5746096232..6ec623b719 100644 --- a/packages/backend/src/server/api/endpoints/flash/my.ts +++ b/packages/backend/src/server/api/endpoints/flash/my.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('flash.userId = :meId', { meId: me.id }); const flashs = await query diff --git a/packages/backend/src/server/api/endpoints/flash/search.ts b/packages/backend/src/server/api/endpoints/flash/search.ts new file mode 100644 index 0000000000..36948bb7b4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/flash/search.ts @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { FlashService } from '@/core/FlashService.js'; + +export const meta = { + tags: ['flash'], + + requireCredential: false, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Flash', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + query: { type: 'string', minLength: 1, maxLength: 100 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 }, + }, + required: ['query'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private flashService: FlashService, + private flashEntityService: FlashEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const result = await this.flashService.search(ps.query, { + sinceId: ps.sinceId, + untilId: ps.untilId, + sinceDate: ps.sinceDate, + untilDate: ps.untilDate, + limit: ps.limit, + }); + + return await this.flashEntityService.packMany(result, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index fa59e38976..cf614e433d 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -49,6 +49,8 @@ export const paramDef = { properties: { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: [], @@ -64,7 +66,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('request.followeeId = :meId', { meId: me.id }); const requests = await query diff --git a/packages/backend/src/server/api/endpoints/following/requests/sent.ts b/packages/backend/src/server/api/endpoints/following/requests/sent.ts index 6325f01bb8..2b3cc35f03 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/sent.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/sent.ts @@ -49,6 +49,8 @@ export const paramDef = { properties: { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: [], @@ -64,7 +66,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('request.followerId = :meId', { meId: me.id }); const requests = await query diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index d398418ab4..846b903bdb 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -30,6 +30,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -44,7 +46,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .innerJoinAndSelect('post.user', 'user'); const posts = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 3558035eca..00277282ba 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.noteFavoritesRepository.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.noteFavoritesRepository.createQueryBuilder('favorite'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('favorite.userId = :meId', { meId: me.id }) .leftJoinAndSelect('favorite.note', 'note'); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index d492585ffa..9913ed7489 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -45,6 +45,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -59,7 +61,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.galleryLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.galleryLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('like.userId = :meId', { meId: me.id }) .leftJoinAndSelect('like.post', 'post'); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index 73a6fcc98b..c9d17fc35c 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('post.userId = :meId', { meId: me.id }); const posts = await query diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts index b9c41b057d..f933eaab00 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -49,6 +49,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, markAsRead: { type: 'boolean', default: true }, // 後方互換のため、廃止された通知タイプも受け付ける includeTypes: { type: 'array', items: { @@ -64,15 +66,14 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - private idService: IdService, private notificationEntityService: NotificationEntityService, private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { const EXTRA_LIMIT = 100; + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : undefined); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : undefined); // includeTypes が空の場合はクエリしない if (ps.includeTypes && ps.includeTypes.length === 0) { @@ -87,8 +88,8 @@ export default class extends Endpoint { // eslint- const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][]; const notifications = await this.notificationService.getNotifications(me.id, { - sinceId: ps.sinceId, - untilId: ps.untilId, + sinceId: sinceId, + untilId: untilId, limit: ps.limit, includeTypes, excludeTypes, diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index f5a48b2f69..158cc9fd73 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -44,6 +44,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, markAsRead: { type: 'boolean', default: true }, // 後方互換のため、廃止された通知タイプも受け付ける includeTypes: { type: 'array', items: { @@ -59,17 +61,14 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private idService: IdService, private notificationEntityService: NotificationEntityService, private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : undefined); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : undefined); + // includeTypes が空の場合はクエリしない if (ps.includeTypes && ps.includeTypes.length === 0) { return []; @@ -83,8 +82,8 @@ export default class extends Endpoint { // eslint- const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const notifications = await this.notificationService.getNotifications(me.id, { - sinceId: ps.sinceId, - untilId: ps.untilId, + sinceId: sinceId, + untilId: untilId, limit: ps.limit, includeTypes, excludeTypes, diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index d4c09426a7..d62dd819f8 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -44,6 +44,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -58,7 +60,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.pageLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.pageLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('like.userId = :meId', { meId: me.id }) .leftJoinAndSelect('like.page', 'page'); diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 1b6359a633..385842539e 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.pagesRepository.createQueryBuilder('page'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.pagesRepository.createQueryBuilder('page'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('page.userId = :meId', { meId: me.id }); const pages = await query diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index c05ee93c6f..08f5e3a7a1 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -15,14 +15,21 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - tokenId: { type: 'string', format: 'misskey:id' }, - token: { type: 'string', nullable: true }, - }, anyOf: [ - { required: ['tokenId'] }, - { required: ['token'] }, + { + type: 'object', + properties: { + tokenId: { type: 'string', format: 'misskey:id' }, + }, + required: ['tokenId'], + }, + { + type: 'object', + properties: { + token: { type: 'string', nullable: true }, + }, + required: ['token'], + }, ], } as const; @@ -33,7 +40,7 @@ export default class extends Endpoint { // eslint- private accessTokensRepository: AccessTokensRepository, ) { super(meta, paramDef, async (ps, me) => { - if (ps.tokenId) { + if ('tokenId' in ps) { const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } }); if (tokenExist) { diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 76ad0bbe21..c104dfdcdd 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -31,6 +31,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -45,7 +47,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('signin.userId = :meId', { meId: me.id }); const history = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index a99974a91e..71f8f579a5 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.registrationTicketsRepository.createQueryBuilder('ticket'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.registrationTicketsRepository.createQueryBuilder('ticket'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('ticket.createdById = :meId', { meId: me.id }) .leftJoinAndSelect('ticket.createdBy', 'createdBy') .leftJoinAndSelect('ticket.usedBy', 'usedBy'); diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 23204f2829..7762556bee 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.mutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.mutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('muting.muterId = :meId', { meId: me.id }); const mutings = await query diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 9938322a2a..0800828a87 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -35,6 +35,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -49,7 +51,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.visibility = \'public\'') .andWhere('note.localOnly = FALSE') .innerJoinAndSelect('note.user', 'user') diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 712a86eb13..e9d56e2892 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['noteId'], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere(new Brackets(qb => { qb .where('note.replyId = :noteId', { noteId: ps.noteId }) @@ -70,12 +72,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - if (me) { - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); - } + this.queryService.generateBaseNoteFilteringQuery(query, me); const notes = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 253a360815..7caea8eedc 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -269,7 +269,10 @@ export default class extends Endpoint { // eslint- let renote: MiNote | null = null; if (ps.renoteId != null) { // Fetch renote to note - renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); + renote = await this.notesRepository.findOne({ + where: { id: ps.renoteId }, + relations: ['user', 'renote', 'reply'], + }); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); @@ -315,7 +318,10 @@ export default class extends Endpoint { // eslint- let reply: MiNote | null = null; if (ps.replyId != null) { // Fetch reply - reply = await this.notesRepository.findOneBy({ id: ps.replyId }); + reply = await this.notesRepository.findOne({ + where: { id: ps.replyId }, + relations: ['user'], + }); if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/count.ts b/packages/backend/src/server/api/endpoints/notes/drafts/count.ts new file mode 100644 index 0000000000..002a545d32 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/drafts/count.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { NoteDraftsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['notes', 'drafts'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'read:account', + + res: { + type: 'number', + optional: false, nullable: false, + description: 'The number of drafts', + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteDraftsRepository) + private noteDraftsRepository: NoteDraftsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const count = await this.noteDraftsRepository.createQueryBuilder('drafts') + .where('drafts.userId = :meId', { meId: me.id }) + .getCount(); + + return count; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts new file mode 100644 index 0000000000..1c28ec22d0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -0,0 +1,258 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteDraftService } from '@/core/NoteDraftService.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { ApiError } from '@/server/api/error.js'; +import { NoteDraftEntityService } from '@/core/entities/NoteDraftEntityService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; + +export const meta = { + tags: ['notes', 'drafts'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'write:account', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + createdDraft: { + type: 'object', + optional: false, nullable: false, + ref: 'NoteDraft', + }, + }, + }, + + errors: { + noSuchRenoteTarget: { + message: 'No such renote target.', + code: 'NO_SUCH_RENOTE_TARGET', + id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4', + }, + + cannotReRenote: { + message: 'You can not Renote a pure Renote.', + code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', + id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', + }, + + cannotRenoteDueToVisibility: { + message: 'You can not Renote due to target visibility.', + code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', + id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', + }, + + noSuchReplyTarget: { + message: 'No such reply target.', + code: 'NO_SUCH_REPLY_TARGET', + id: '749ee0f6-d3da-459a-bf02-282e2da4292c', + }, + + cannotReplyToInvisibleNote: { + message: 'You cannot reply to an invisible Note.', + code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE', + id: 'b98980fa-3780-406c-a935-b6d0eeee10d1', + }, + + cannotReplyToPureRenote: { + message: 'You can not reply to a pure Renote.', + code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', + }, + + cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { + message: 'You cannot reply to a specified visibility note with extended visibility.', + code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', + id: 'ed940410-535c-4d5e-bfa3-af798671e93c', + }, + + cannotCreateAlreadyExpiredPoll: { + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', + }, + + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb', + }, + + youHaveBeenBlocked: { + message: 'You have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', + }, + + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, + + cannotRenoteOutsideOfChannel: { + message: 'Cannot renote outside of channel.', + code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', + id: '33510210-8452-094c-6227-4a6c05d99f00', + }, + + containsProhibitedWords: { + message: 'Cannot post because it contains prohibited words.', + code: 'CONTAINS_PROHIBITED_WORDS', + id: 'aa6e01d3-a85c-669d-758a-76aab43af334', + }, + + containsTooManyMentions: { + message: 'Cannot post because it exceeds the allowed number of mentions.', + code: 'CONTAINS_TOO_MANY_MENTIONS', + id: '4de0363a-3046-481b-9b0f-feff3e211025', + }, + + tooManyDrafts: { + message: 'You cannot create drafts any more.', + code: 'TOO_MANY_DRAFTS', + id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', + }, + + cannotRenoteToExternal: { + message: 'Cannot Renote to External.', + code: 'CANNOT_RENOTE_TO_EXTERNAL', + id: 'ed1952ac-2d26-4957-8b30-2deda76bedf7', + }, + }, + + limit: { + duration: ms('1hour'), + max: 300, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibleUserIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, + hashtag: { type: 'string', nullable: true, maxLength: 200 }, + localOnly: { type: 'boolean', default: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, + channelId: { type: 'string', format: 'misskey:id', nullable: true }, + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + text: { + type: 'string', + minLength: 0, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: true, + }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 0, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private noteDraftService: NoteDraftService, + private noteDraftEntityService: NoteDraftEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const draft = await this.noteDraftService.create(me, { + fileIds: ps.fileIds, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + expiredAfter: ps.poll.expiredAfter ?? null, + } : undefined, + text: ps.text ?? null, + replyId: ps.replyId ?? undefined, + renoteId: ps.renoteId ?? undefined, + cw: ps.cw ?? null, + ...(ps.hashtag ? { hashtag: ps.hashtag } : {}), + localOnly: ps.localOnly, + reactionAcceptance: ps.reactionAcceptance, + visibility: ps.visibility, + visibleUserIds: ps.visibleUserIds ?? [], + channelId: ps.channelId ?? undefined, + }).catch((err) => { + if (err instanceof IdentifiableError) { + switch (err.id) { + case '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8': + throw new ApiError(meta.errors.tooManyDrafts); + case '04da457d-b083-4055-9082-955525eda5a5': + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + case 'b6992544-63e7-67f0-fa7f-32444b1b5306': + throw new ApiError(meta.errors.noSuchFile); + case '64929870-2540-4d11-af41-3b484d78c956': + throw new ApiError(meta.errors.noSuchRenoteTarget); + case '76cc5583-5a14-4ad3-8717-0298507e32db': + throw new ApiError(meta.errors.cannotReRenote); + case '075ca298-e6e7-485a-b570-51a128bb5168': + throw new ApiError(meta.errors.youHaveBeenBlocked); + case '81eb8188-aea1-4e35-9a8f-3334a3be9855': + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + case '6815399a-6f13-4069-b60d-ed5156249d12': + throw new ApiError(meta.errors.noSuchChannel); + case 'ed1952ac-2d26-4957-8b30-2deda76bedf7': + throw new ApiError(meta.errors.cannotRenoteToExternal); + case 'c4721841-22fc-4bb7-ad3d-897ef1d375b5': + throw new ApiError(meta.errors.noSuchReplyTarget); + case 'e6c10b57-2c09-4da3-bd4d-eda05d51d140': + throw new ApiError(meta.errors.cannotReplyToPureRenote); + case '593c323c-6b6a-4501-a25c-2f36bd2a93d6': + throw new ApiError(meta.errors.cannotReplyToInvisibleNote); + case '215dbc76-336c-4d2a-9605-95766ba7dab0': + throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); + default: + throw err; + } + } + throw err; + }); + + const createdDraft = await this.noteDraftEntityService.pack(draft, me); + + return { + createdDraft, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/delete.ts b/packages/backend/src/server/api/endpoints/notes/drafts/delete.ts new file mode 100644 index 0000000000..6c41145c18 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/drafts/delete.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteDraftService } from '@/core/NoteDraftService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['notes', 'drafts'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'write:account', + + errors: { + noSuchNoteDraft: { + message: 'No such note draft.', + code: 'NO_SUCH_NOTE_DRAFT', + id: '49cd6b9d-848e-41ee-b0b9-adaca711a6b1', + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + draftId: { type: 'string', nullable: false, format: 'misskey:id' }, + }, + required: ['draftId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private noteDraftService: NoteDraftService, + ) { + super(meta, paramDef, async (ps, me) => { + const draft = await this.noteDraftService.get(me, ps.draftId); + if (draft == null) { + throw new ApiError(meta.errors.noSuchNoteDraft); + } + + if (draft.userId !== me.id) { + throw new ApiError(meta.errors.accessDenied); + } + + await this.noteDraftService.delete(me, draft.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/list.ts b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts new file mode 100644 index 0000000000..f24f9b8fb2 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { MiNoteDraft, NoteDraftsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteDraftEntityService } from '@/core/entities/NoteDraftEntityService.js'; + +export const meta = { + tags: ['notes', 'drafts'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'read:account', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'NoteDraft', + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteDraftsRepository) + private noteDraftsRepository: NoteDraftsRepository, + + private queryService: QueryService, + private noteDraftEntityService: NoteDraftEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('drafts.userId = :meId', { meId: me.id }); + + const drafts = await query + .limit(ps.limit) + .getMany(); + + return await this.noteDraftEntityService.packMany(drafts, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts new file mode 100644 index 0000000000..ee221fb765 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteDraftService } from '@/core/NoteDraftService.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { NoteDraftEntityService } from '@/core/entities/NoteDraftEntityService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['notes', 'drafts'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'write:account', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + updatedDraft: { + type: 'object', + optional: false, nullable: false, + ref: 'NoteDraft', + }, + }, + }, + + errors: { + noSuchRenoteTarget: { + message: 'No such renote target.', + code: 'NO_SUCH_RENOTE_TARGET', + id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4', + }, + + cannotReRenote: { + message: 'You can not Renote a pure Renote.', + code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', + id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', + }, + + cannotRenoteDueToVisibility: { + message: 'You can not Renote due to target visibility.', + code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', + id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', + }, + + noSuchReplyTarget: { + message: 'No such reply target.', + code: 'NO_SUCH_REPLY_TARGET', + id: '749ee0f6-d3da-459a-bf02-282e2da4292c', + }, + + cannotReplyToInvisibleNote: { + message: 'You cannot reply to an invisible Note.', + code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE', + id: 'b98980fa-3780-406c-a935-b6d0eeee10d1', + }, + + cannotReplyToPureRenote: { + message: 'You can not reply to a pure Renote.', + code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', + }, + + cannotReplyToSpecifiedNoteWithExtendedVisibility: { + message: 'You cannot reply to a specified visibility note with extended visibility.', + code: 'CANNOT_REPLY_TO_SPECIFIED_NOTE_WITH_EXTENDED_VISIBILITY', + id: 'ed940410-535c-4d5e-bfa3-af798671e93c', + }, + + cannotCreateAlreadyExpiredPoll: { + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', + }, + + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb', + }, + + youHaveBeenBlocked: { + message: 'You have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', + }, + + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, + + cannotRenoteOutsideOfChannel: { + message: 'Cannot renote outside of channel.', + code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', + id: '33510210-8452-094c-6227-4a6c05d99f00', + }, + + containsProhibitedWords: { + message: 'Cannot post because it contains prohibited words.', + code: 'CONTAINS_PROHIBITED_WORDS', + id: 'aa6e01d3-a85c-669d-758a-76aab43af334', + }, + + containsTooManyMentions: { + message: 'Cannot post because it exceeds the allowed number of mentions.', + code: 'CONTAINS_TOO_MANY_MENTIONS', + id: '4de0363a-3046-481b-9b0f-feff3e211025', + }, + + noSuchNoteDraft: { + message: 'No such note draft.', + code: 'NO_SUCH_NOTE_DRAFT', + id: '49cd6b9d-848e-41ee-b0b9-adaca711a6b1', + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', + }, + + noSuchRenote: { + message: 'No such renote.', + code: 'NO_SUCH_RENOTE', + id: '64929870-2540-4d11-af41-3b484d78c956', + }, + + cannotRenote: { + message: 'Cannot renote.', + code: 'CANNOT_RENOTE', + id: '76cc5583-5a14-4ad3-8717-0298507e32db', + }, + + cannotRenoteToExternal: { + message: 'Cannot Renote to External.', + code: 'CANNOT_RENOTE_TO_EXTERNAL', + id: 'ed1952ac-2d26-4957-8b30-2deda76bedf7', + }, + + noSuchReply: { + message: 'No such reply.', + code: 'NO_SUCH_REPLY', + id: 'c4721841-22fc-4bb7-ad3d-897ef1d375b5', + }, + + cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { + message: 'You cannot reply to a specified visibility note with extended visibility.', + code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', + id: '215dbc76-336c-4d2a-9605-95766ba7dab0', + }, + }, + + limit: { + duration: ms('1hour'), + max: 300, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + draftId: { type: 'string', nullable: false, format: 'misskey:id' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibleUserIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, + hashtag: { type: 'string', nullable: true, maxLength: 200 }, + localOnly: { type: 'boolean', default: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, + channelId: { type: 'string', format: 'misskey:id', nullable: true }, + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + // See https://github.com/misskey-dev/misskey/pull/10082 + text: { + type: 'string', + minLength: 0, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: true, + }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 0, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, + }, + required: ['draftId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private noteDraftService: NoteDraftService, + private noteDraftEntityService: NoteDraftEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const draft = await this.noteDraftService.update(me, ps.draftId, { + fileIds: ps.fileIds, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + expiredAfter: ps.poll.expiredAfter ?? null, + } : undefined, + text: ps.text ?? null, + replyId: ps.replyId ?? undefined, + renoteId: ps.renoteId ?? undefined, + cw: ps.cw ?? null, + ...(ps.hashtag ? { hashtag: ps.hashtag } : {}), + localOnly: ps.localOnly, + reactionAcceptance: ps.reactionAcceptance, + visibility: ps.visibility, + visibleUserIds: ps.visibleUserIds ?? [], + channelId: ps.channelId ?? undefined, + }).catch((err) => { + if (err instanceof IdentifiableError) { + switch (err.id) { + case '49cd6b9d-848e-41ee-b0b9-adaca711a6b1': + throw new ApiError(meta.errors.noSuchNoteDraft); + case '04da457d-b083-4055-9082-955525eda5a5': + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + case 'b6992544-63e7-67f0-fa7f-32444b1b5306': + throw new ApiError(meta.errors.noSuchFile); + case '64929870-2540-4d11-af41-3b484d78c956': + throw new ApiError(meta.errors.noSuchRenote); + case '76cc5583-5a14-4ad3-8717-0298507e32db': + throw new ApiError(meta.errors.cannotRenote); + case '075ca298-e6e7-485a-b570-51a128bb5168': + throw new ApiError(meta.errors.youHaveBeenBlocked); + case '81eb8188-aea1-4e35-9a8f-3334a3be9855': + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + case '6815399a-6f13-4069-b60d-ed5156249d12': + throw new ApiError(meta.errors.noSuchChannel); + case 'ed1952ac-2d26-4957-8b30-2deda76bedf7': + throw new ApiError(meta.errors.cannotRenoteToExternal); + case 'c4721841-22fc-4bb7-ad3d-897ef1d375b5': + throw new ApiError(meta.errors.noSuchReply); + case 'e6c10b57-2c09-4da3-bd4d-eda05d51d140': + throw new ApiError(meta.errors.cannotReplyToPureRenote); + case '593c323c-6b6a-4501-a25c-2f36bd2a93d6': + throw new ApiError(meta.errors.cannotReplyToInvisibleNote); + case '215dbc76-336c-4d2a-9605-95766ba7dab0': + throw new ApiError(meta.errors.cannotReplyToSpecifiedNoteWithExtendedVisibility); + case 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4': + throw new ApiError(meta.errors.noSuchRenoteTarget); + case 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a': + throw new ApiError(meta.errors.cannotReRenote); + case '749ee0f6-d3da-459a-bf02-282e2da4292c': + throw new ApiError(meta.errors.noSuchReplyTarget); + case '33510210-8452-094c-6227-4a6c05d99f00': + throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); + case 'aa6e01d3-a85c-669d-758a-76aab43af334': + throw new ApiError(meta.errors.containsProhibitedWords); + case '4de0363a-3046-481b-9b0f-feff3e211025': + throw new ApiError(meta.errors.containsTooManyMentions); + default: + throw err; + } + } + throw err; + }); + + const updatedDraft = await this.noteDraftEntityService.pack(draft, me); + + return { + updatedDraft, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 8d38bb1c65..1c73edf08e 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -78,11 +78,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - if (me) { - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - } + this.queryService.generateBaseNoteFilteringQuery(query, me); + if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 6a3ee817e4..2c8459525a 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -243,10 +243,7 @@ export default class extends Endpoint { // eslint- } this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index d1dc22f233..ee61ab43da 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -156,10 +156,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - if (me) this.queryService.generateMutedUserQueryForNotes(query, me); - if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.withFiles) { diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index c3722b1b5a..05ffdc1f97 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -35,6 +35,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, visibility: { type: 'string' }, }, required: [], @@ -57,7 +59,7 @@ export default class extends Endpoint { // eslint- .select('following.followeeId') .where('following.followerId = :followerId', { followerId: me.id }); - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere(new Brackets(qb => { qb // このmeIdAsListパラメータはqueryServiceのgenerateVisibilityQueryでセットされる .where(':meIdAsList <@ note.mentions') @@ -72,11 +74,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); this.queryService.generateMutedNoteThreadQuery(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); if (ps.visibility) { query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 97b12ab7f7..e5e15fd4b3 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -47,6 +47,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['noteId'], } as const; @@ -61,7 +63,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('reaction.noteId = :noteId', { noteId: ps.noteId }) .leftJoinAndSelect('reaction.user', 'user') .leftJoinAndSelect('reaction.note', 'note'); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index ce2435b8eb..b294794de6 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -43,6 +43,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['noteId'], } as const; @@ -63,7 +65,7 @@ export default class extends Endpoint { // eslint- throw err; }); - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.renoteId = :renoteId', { renoteId: note.id }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') @@ -72,10 +74,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - if (me) this.queryService.generateMutedUserQueryForNotes(query, me); - if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); const renotes = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index f491cc38ab..d567d7c64a 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -32,6 +32,8 @@ export const paramDef = { noteId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: ['noteId'], @@ -47,7 +49,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.replyId = :replyId', { replyId: ps.noteId }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') @@ -56,10 +58,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - if (me) this.queryService.generateMutedUserQueryForNotes(query, me); - if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index d0781bd8dd..2cd6610cf6 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -28,38 +28,55 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - reply: { type: 'boolean', nullable: true, default: null }, - renote: { type: 'boolean', nullable: true, default: null }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, - poll: { type: 'boolean', nullable: true, default: null }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - - tag: { type: 'string', minLength: 1 }, - query: { - type: 'array', - description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', - items: { - type: 'array', - items: { - type: 'string', - minLength: 1, + allOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + tag: { type: 'string', minLength: 1 }, + }, + required: ['tag'], }, - minItems: 1, - }, - minItems: 1, + { + type: 'object', + properties: { + query: { + type: 'array', + description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', + items: { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + minItems: 1, + }, + minItems: 1, + }, + }, + required: ['query'], + }, + ], + }, + { + type: 'object', + properties: { + reply: { type: 'boolean', nullable: true, default: null }, + renote: { type: 'boolean', nullable: true, default: null }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, + poll: { type: 'boolean', nullable: true, default: null }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, }, - }, - anyOf: [ - { required: ['tag'] }, - { required: ['query'] }, ], } as const; @@ -73,7 +90,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') @@ -81,18 +98,15 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - if (me) this.queryService.generateMutedUserQueryForNotes(query, me); - if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); try { - if (ps.tag) { + if ('tag' in ps) { if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); query.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(ps.tag)] }); } else { query.andWhere(new Brackets(qb => { - for (const tags of ps.query!) { + for (const tags of ps.query) { qb.orWhere(new Brackets(qb => { for (const tag of tags) { if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection'); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 3fe19806e3..ab1bd934fb 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { SearchService } from '@/core/SearchService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -40,6 +41,8 @@ export const paramDef = { query: { type: 'string' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, host: { @@ -60,8 +63,12 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private searchService: SearchService, private roleService: RoleService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : undefined); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : undefined); + const policies = await this.roleService.getUserPolicies(me ? me.id : null); if (!policies.canSearchNotes) { throw new ApiError(meta.errors.unavailable); @@ -72,8 +79,8 @@ export default class extends Endpoint { // eslint- channelId: ps.channelId, host: ps.host, }, { - untilId: ps.untilId, - sinceId: ps.sinceId, + untilId: untilId, + sinceId: sinceId, limit: ps.limit, }); diff --git a/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts index 87b368e17e..e102bc1d4a 100644 --- a/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts +++ b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts @@ -19,7 +19,26 @@ export const meta = { optional: false, nullable: false, items: { type: 'object', - optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + reactions: { + type: 'object', + optional: false, nullable: false, + additionalProperties: { + type: 'number', + }, + }, + reactionEmojis: { + type: 'object', + optional: false, nullable: false, + additionalProperties: { + type: 'string', + }, + }, + }, }, }, diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index b93c73b0c5..cae0e752da 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -55,7 +55,7 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => { + const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index e6d6a1b629..1f3631ae3d 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -199,10 +199,7 @@ export default class extends Endpoint { // eslint- })); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { @@ -240,7 +237,13 @@ export default class extends Endpoint { // eslint- } if (ps.withRenotes === false) { - query.andWhere('note.renoteId IS NULL'); + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); } //#endregion diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index ec7c4b0f97..614cd9204d 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -184,10 +184,7 @@ export default class extends Endpoint { // eslint- })); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index e08b832a3f..8427bab2d5 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -33,15 +33,22 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - name: { type: 'string' }, - username: { type: 'string' }, - }, anyOf: [ - { required: ['pageId'] }, - { required: ['name', 'username'] }, + { + type: 'object', + properties: { + pageId: { type: 'string', format: 'misskey:id' }, + }, + required: ['pageId'], + }, + { + type: 'object', + properties: { + name: { type: 'string' }, + username: { type: 'string' }, + }, + required: ['name', 'username'], + }, ], } as const; @@ -59,9 +66,9 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { let page: MiPage | null = null; - if (ps.pageId) { + if ('pageId' in ps) { page = await this.pagesRepository.findOneBy({ id: ps.pageId }); - } else if (ps.name && ps.username) { + } else { const author = await this.usersRepository.findOneBy({ host: IsNull(), usernameLower: ps.username.toLowerCase(), diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts index 3be01f989a..adf5aa76bf 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts @@ -34,6 +34,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: [], } as const; @@ -48,7 +50,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.renoteMutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.renoteMutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('muting.muterId = :meId', { meId: me.id }); const mutings = await query diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts index 6b06068727..22f3a0617b 100644 --- a/packages/backend/src/server/api/endpoints/reversi/games.ts +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -27,6 +27,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, my: { type: 'boolean', default: false }, }, required: [], @@ -42,7 +44,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .innerJoinAndSelect('game.user1', 'user1') .innerJoinAndSelect('game.user2', 'user2'); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 16b0783a01..e8a760e9f8 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -102,10 +102,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query); - this.queryService.generateSuspendedUserQueryForNote(query); - this.queryService.generateMutedUserQueryForNotes(query, me); - this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBaseNoteFilteringQuery(query, me); const notes = await query.getMany(); notes.sort((a, b) => a.id > b.id ? -1 : 1); diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 48d350af59..c7ec7910d6 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -51,6 +51,8 @@ export const paramDef = { roleId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, required: ['roleId'], @@ -79,7 +81,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRole); } - const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('assign.roleId = :roleId', { roleId: role.id }) .andWhere(new Brackets(qb => { qb diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 7f7d2ea8cc..fb5a2391f2 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -33,6 +33,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['userId'], } as const; @@ -47,7 +49,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('clip.userId = :userId', { userId: ps.userId }) .andWhere('clip.isPublic = true'); diff --git a/packages/backend/src/server/api/endpoints/users/flashs.ts b/packages/backend/src/server/api/endpoints/users/flashs.ts index e5ea450215..2ef514bebf 100644 --- a/packages/backend/src/server/api/endpoints/users/flashs.ts +++ b/packages/backend/src/server/api/endpoints/users/flashs.ts @@ -33,11 +33,12 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['userId'], } as const; - -// eslint-disable-next-line import/no-default-export + @Injectable() export default class extends Endpoint { constructor( @@ -48,7 +49,7 @@ export default class extends Endpoint { private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('flash.userId = :userId', { userId: ps.userId }) .andWhere('flash.visibility = \'public\''); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index a8b4319a61..84c4c80d01 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -47,23 +47,40 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - - userId: { type: 'string', format: 'misskey:id' }, - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', + allOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + type: 'object', + properties: { + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, + required: ['username', 'host'], + }, + ], + }, + { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, }, - }, - anyOf: [ - { required: ['userId'] }, - { required: ['username', 'host'] }, ], } as const; @@ -85,9 +102,9 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy(ps.userId != null + const user = await this.usersRepository.findOneBy('userId' in ps ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); + : { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); if (user == null) { throw new ApiError(meta.errors.noSuchUser); @@ -117,7 +134,7 @@ export default class extends Endpoint { // eslint- } } - const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('following.followeeId = :userId', { userId: user.id }) .innerJoinAndSelect('following.follower', 'follower'); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index feda5bb353..047f9a053b 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -54,25 +54,41 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - - userId: { type: 'string', format: 'misskey:id' }, - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', + allOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + type: 'object', + properties: { + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, + required: ['username', 'host'], + }, + ], + }, + { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + birthday: { ...birthdaySchema, nullable: true }, + }, }, - - birthday: { ...birthdaySchema, nullable: true }, - }, - anyOf: [ - { required: ['userId'] }, - { required: ['username', 'host'] }, ], } as const; @@ -94,9 +110,9 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy(ps.userId != null + const user = await this.usersRepository.findOneBy('userId' in ps ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); + : { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() }); if (user == null) { throw new ApiError(meta.errors.noSuchUser); @@ -126,7 +142,7 @@ export default class extends Endpoint { // eslint- } } - const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 553886374c..55e885e3c3 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -33,6 +33,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['userId'], } as const; @@ -47,7 +49,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('post.userId = :userId', { userId: ps.userId }); const posts = await query diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts index 6d6e8d34ea..a841c4b94d 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -64,6 +64,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['listId'], } as const; @@ -94,7 +96,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchList); } - const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('membership.userListId = :userListId', { userListId: userList.id }) .innerJoinAndSelect('membership.user', 'user'); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 0c64df569d..5832790a61 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -186,12 +186,10 @@ export default class extends Endpoint { // eslint- } this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateBlockedHostQueryForNote(query, true); - this.queryService.generateSuspendedUserQueryForNote(query, true); - if (me) { - this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId }); - this.queryService.generateBlockedUserQueryForNotes(query, me); - } + this.queryService.generateBaseNoteFilteringQuery(query, me, { + excludeAuthor: true, + excludeUserFromMute: ps.userId, + }); if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index bb7de0e0b5..1d7499d4be 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -33,6 +33,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, }, required: ['userId'], } as const; @@ -47,7 +49,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.pagesRepository.createQueryBuilder('page'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.pagesRepository.createQueryBuilder('page'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('page.userId = :userId', { userId: ps.userId }) .andWhere('page.visibility = \'public\''); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 5b1c6b514b..769a72d7a1 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -64,6 +64,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateMutedUserQueryForUsers(query, me); this.queryService.generateBlockQueryForUsers(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' }); const followingQuery = this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 1d75437b81..f146095cf1 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -114,7 +114,7 @@ export const paramDef = { type: 'object', properties: { userId: { - anyOf: [ + oneOf: [ { type: 'string', format: 'misskey:id' }, { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 134f1a8e87..d1d6354d53 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -26,17 +26,32 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - detail: { type: 'boolean', default: true }, - - username: { type: 'string', nullable: true }, - host: { type: 'string', nullable: true }, - }, - anyOf: [ - { required: ['username'] }, - { required: ['host'] }, + allOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + username: { type: 'string', nullable: true }, + }, + required: ['username'], + }, + { + type: 'object', + properties: { + host: { type: 'string', nullable: true }, + }, + required: ['host'], + }, + ], + }, + { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + detail: { type: 'boolean', default: true }, + }, + }, ], } as const; @@ -47,8 +62,8 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, (ps, me) => { return this.userSearchService.searchByUsernameAndHost({ - username: ps.username, - host: ps.host, + username: 'username' in ps ? ps.username : undefined, + host: 'host' in ps ? ps.host : undefined, }, { limit: ps.limit, detail: ps.detail, diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 5d36847e03..c422286152 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -13,6 +13,7 @@ export const meta = { tags: ['users'], requireCredential: false, + requiredRolePolicy: 'canSearchUsers', description: 'Search for users.', diff --git a/packages/backend/src/server/api/endpoints/users/show.test.ts b/packages/backend/src/server/api/endpoints/users/show.test.ts new file mode 100644 index 0000000000..068ffd8bc9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/show.test.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; +import { paramDef } from './show.js'; + +const VALID = true; +const INVALID = false; + +describe('api:users/show', () => { + describe('validation', () => { + const v = getValidator(paramDef); + + test('Reject empty', () => expect(v({})).toBe(INVALID)); + test('Reject host only', () => expect(v({ host: 'misskey.test' })).toBe(INVALID)); + test('Accept userId only', () => expect(v({ userId: '1' })).toBe(VALID)); + test('Accept username and host', () => expect(v({ username: 'alice', host: 'misskey.test' })).toBe(VALID)); + }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 431869d47f..5ff3a63d6a 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -59,23 +59,44 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - userIds: { type: 'array', uniqueItems: true, items: { - type: 'string', format: 'misskey:id', - } }, - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', + allOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + type: 'object', + properties: { + userIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + }, + required: ['userIds'], + }, + { + type: 'object', + properties: { + username: { type: 'string' }, + }, + required: ['username'], + }, + ], + }, + { + type: 'object', + properties: { + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, }, - }, - anyOf: [ - { required: ['userId'] }, - { required: ['userIds'] }, - { required: ['username'] }, ], } as const; @@ -95,16 +116,19 @@ export default class extends Endpoint { // eslint- private apiLoggerService: ApiLoggerService, ) { super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => { - if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { - throw new ApiError(meta.errors.noSuchUser); - } + // ログイン時にusers/showできなくなってしまう + //if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { + // throw new ApiError(meta.errors.noSuchUser); + //} let user; const isModerator = await this.roleService.isModerator(me); - ps.username = ps.username?.trim(); + if ('username' in ps) { + ps.username = ps.username.trim(); + } - if (ps.userIds) { + if ('userIds' in ps) { if (ps.userIds.length === 0) { return []; } @@ -129,7 +153,7 @@ export default class extends Endpoint { // eslint- return _users.map(u => _userMap.get(u.id)!); } else { // Lookup user - if (typeof ps.host === 'string' && typeof ps.username === 'string') { + if (typeof ps.host === 'string' && 'username' in ps) { if (this.serverSettings.ugcVisibilityForVisitor === 'local' && me == null) { throw new ApiError(meta.errors.noSuchUser); } @@ -139,7 +163,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.failedToResolveRemoteUser); }); } else { - const q: FindOptionsWhere = ps.userId != null + const q: FindOptionsWhere = 'userId' in ps ? { id: ps.userId } : { usernameLower: ps.username!.toLowerCase(), host: IsNull() }; diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts index 7139715293..c07d100a99 100644 --- a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts @@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['admin'], @@ -65,6 +66,8 @@ export const paramDef = { }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, page: { type: 'integer' }, sortKeys: { @@ -84,8 +87,12 @@ export default class extends Endpoint { // eslint- constructor( private customEmojiService: CustomEmojiService, private emojiEntityService: EmojiEntityService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : undefined); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : undefined); + const q = ps.query; const result = await this.customEmojiService.fetchEmojis( { @@ -105,8 +112,8 @@ export default class extends Endpoint { // eslint- hostType: q?.hostType, roleIds: q?.roleIds, }, - sinceId: ps.sinceId, - untilId: ps.untilId, + sinceId: sinceId, + untilId: untilId, }, { limit: ps.limit, diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index ea64e32ee6..e1dead07cf 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -89,7 +89,8 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { schema.required = undefined; } - const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1); + const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1) + || ['allOf', 'oneOf', 'anyOf'].some(o => (Array.isArray(schema[o]) && schema[o].length >= 0)); const info = { operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index c80dda8d96..1cdcbebd1a 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -38,14 +38,13 @@ export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 're if (type === 'res' && schema.ref && (!schema.selfRef || includeSelfRef)) { const $ref = `#/components/schemas/${schema.ref}`; - if (schema.nullable || schema.optional) { - res.allOf = [{ $ref }]; + if (schema.nullable) { + res.oneOf = [{ $ref }, { type: 'null' }]; } else { res.$ref = $ref; } - } - - if (schema.nullable) { + delete res.type; + } else if (schema.nullable) { if (Array.isArray(schema.type) && !schema.type.includes('null')) { res.type.push('null'); } else if (typeof schema.type === 'string') { diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index c9801d8314..8e28ab263b 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -32,7 +32,6 @@ export default class Connection { public subscriber: StreamEventEmitter; private channels: Channel[] = []; private subscribingNotes: Partial> = {}; - private cachedNotes: Packed<'Note'>[] = []; public userProfile: MiUserProfile | null = null; public following: Record | undefined> = {}; public followingChannels: Set = new Set(); @@ -132,26 +131,6 @@ export default class Connection { this.sendMessageToWs(data.type, data.body); } - @bindThis - public cacheNote(note: Packed<'Note'>) { - const add = (note: Packed<'Note'>) => { - const existIndex = this.cachedNotes.findIndex(n => n.id === note.id); - if (existIndex > -1) { - this.cachedNotes[existIndex] = note; - return; - } - - this.cachedNotes.unshift(note); - if (this.cachedNotes.length > 32) { - this.cachedNotes.splice(32); - } - }; - - add(note); - if (note.reply) add(note.reply); - if (note.renote) add(note.renote); - } - @bindThis private onReadNotification(payload: JsonValue | undefined) { this.notificationService.readAllNotification(this.user!.id); diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 53dc7f18b6..e08562fdf9 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -43,8 +43,6 @@ class AntennaChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - this.connection.cacheNote(note); - this.send('note', note); } else { this.send(data.type, data.body); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 7108e0cd6e..ac79c31854 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -49,8 +49,6 @@ class ChannelChannel extends Channel { } } - this.connection.cacheNote(note); - this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 795980821b..d7c781ad12 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -65,8 +65,6 @@ class GlobalTimelineChannel extends Channel { } } - this.connection.cacheNote(note); - this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 8105f15cb1..c911d63642 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -53,8 +53,6 @@ class HashtagChannel extends Channel { } } - this.connection.cacheNote(note); - this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 66644ed58c..157d9fc279 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -86,8 +86,6 @@ class HomeTimelineChannel extends Channel { } } - this.connection.cacheNote(note); - this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 5681311493..db5b4576be 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -100,8 +100,6 @@ class HybridTimelineChannel extends Channel { } } - this.connection.cacheNote(note); - this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 2984e18774..3d7ed6acdb 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -75,8 +75,6 @@ class LocalTimelineChannel extends Channel { } } - this.connection.cacheNote(note); - this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 863d7f4c4e..525f24c105 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -39,7 +39,6 @@ class MainChannel extends Channel { const note = await this.noteEntityService.pack(data.body.note.id, this.user, { detail: true, }); - this.connection.cacheNote(note); data.body.note = note; } break; @@ -52,7 +51,6 @@ class MainChannel extends Channel { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true, }); - this.connection.cacheNote(note); data.body = note; } break; diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 4f38351e94..5bfd8fa68c 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -118,8 +118,6 @@ class UserListChannel extends Channel { } } - this.connection.cacheNote(note); - this.send('note', note); } diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 9a33d27d86..b515a0c0c8 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -20,17 +20,6 @@ import type { Config } from '@/config.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { DI } from '@/di-symbols.js'; import * as Acct from '@/misc/acct.js'; -import type { - DbQueue, - DeliverQueue, - EndedPollNotificationQueue, - InboxQueue, - ObjectStorageQueue, - RelationshipQueue, - SystemQueue, - UserWebhookDeliverQueue, - SystemWebhookDeliverQueue, -} from '@/core/QueueModule.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; @@ -129,16 +118,6 @@ export class ClientServerService { private feedService: FeedService, private roleService: RoleService, private clientLoggerService: ClientLoggerService, - - @Inject('queue:system') public systemQueue: SystemQueue, - @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, - @Inject('queue:deliver') public deliverQueue: DeliverQueue, - @Inject('queue:inbox') public inboxQueue: InboxQueue, - @Inject('queue:db') public dbQueue: DbQueue, - @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, - @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, ) { //this.createServer = this.createServer.bind(this); } @@ -188,6 +167,10 @@ export class ClientServerService { 'url': 'url', }, }, + 'shortcuts': [{ + 'name': 'Safemode', + 'url': '/?safemode=true', + }], }; manifest = { @@ -212,6 +195,7 @@ export class ClientServerService { instanceUrl: this.config.url, metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)), now: Date.now(), + federationEnabled: this.meta.federation !== 'none', }; } @@ -579,7 +563,7 @@ export class ClientServerService { id: request.params.note, visibility: In(['public', 'home']), }, - relations: ['user'], + relations: ['user', 'reply', 'renote'], }); if ( @@ -820,8 +804,11 @@ export class ClientServerService { fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { reply.removeHeader('X-Frame-Options'); - const note = await this.notesRepository.findOneBy({ - id: request.params.note, + const note = await this.notesRepository.findOne({ + where: { + id: request.params.note, + }, + relations: ['user', 'reply', 'renote'], }); if (note == null) return; diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 531d085315..b9a4015031 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -94,8 +94,8 @@ export class UrlPreviewService { summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); - // Cache 7days - reply.header('Cache-Control', 'max-age=604800, immutable'); + // Cache 1day + reply.header('Cache-Control', 'max-age=86400, immutable'); return summary; } catch (err) { @@ -122,7 +122,7 @@ export class UrlPreviewService { : undefined; return summaly(url, { - followRedirects: false, + followRedirects: this.meta.urlPreviewAllowRedirect, lang: lang ?? 'ja-JP', agent: agent, userAgent: meta.urlPreviewUserAgent ?? undefined, @@ -137,6 +137,7 @@ export class UrlPreviewService { const queryStr = query({ url: url, lang: lang ?? 'ja-JP', + followRedirects: this.meta.urlPreviewAllowRedirect, userAgent: meta.urlPreviewUserAgent ?? undefined, operationTimeout: meta.urlPreviewTimeout, contentLengthLimit: meta.urlPreviewMaximumContentLength, diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js index 9de1275380..022ff064ad 100644 --- a/packages/backend/src/server/web/boot.embed.js +++ b/packages/backend/src/server/web/boot.embed.js @@ -32,61 +32,30 @@ } //#region Detect language & fetch translations - if (!localStorage.hasOwnProperty('locale')) { - const supportedLangs = LANGS; - let lang = localStorage.getItem('lang'); - if (lang == null || !supportedLangs.includes(lang)) { - if (supportedLangs.includes(navigator.language)) { - lang = navigator.language; - } else { - lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); - - // Fallback - if (lang == null) lang = 'en-US'; - } - } - - const metaRes = await window.fetch('/api/meta', { - method: 'POST', - body: JSON.stringify({}), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - }); - if (metaRes.status !== 200) { - renderError('META_FETCH'); - return; - } - const meta = await metaRes.json(); - const v = meta.version; - if (v == null) { - renderError('META_FETCH_V'); - return; - } - - // for https://github.com/misskey-dev/misskey/issues/10202 - if (lang == null || lang.toString == null || lang.toString() === 'null') { - console.error('invalid lang value detected!!!', typeof lang, lang); - lang = 'en-US'; - } - - const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); - if (localRes.status === 200) { - localStorage.setItem('lang', lang); - localStorage.setItem('locale', await localRes.text()); - localStorage.setItem('localeVersion', v); + const supportedLangs = LANGS; + /** @type { string } */ + let lang = localStorage.getItem('lang'); + if (lang == null || !supportedLangs.includes(lang)) { + if (supportedLangs.includes(navigator.language)) { + lang = navigator.language; } else { - renderError('LOCALE_FETCH'); - return; + lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); + + // Fallback + if (lang == null) lang = 'en-US'; } } + + // for https://github.com/misskey-dev/misskey/issues/10202 + if (lang == null || lang.toString == null || lang.toString() === 'null') { + console.error('invalid lang value detected!!!', typeof lang, lang); + lang = 'en-US'; + } //#endregion //#region Script async function importAppScript() { - await import(`/embed_vite/${CLIENT_ENTRY}`) + await import(CLIENT_ENTRY ? `/embed_vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/embed_vite/src/_boot_.ts') .catch(async e => { console.error(e); renderError('APP_IMPORT'); @@ -115,10 +84,26 @@ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } - const locale = JSON.parse(localStorage.getItem('locale') || '{}'); + let messages = null; + const bootloaderLocales = localStorage.getItem('bootloaderLocales'); + if (bootloaderLocales) { + messages = JSON.parse(bootloaderLocales); + } + if (!messages) { + // older version of misskey does not store bootloaderLocales, stores locale as a whole + const legacyLocale = localStorage.getItem('locale'); + if (legacyLocale) { + const parsed = JSON.parse(legacyLocale); + messages = { + ...(parsed._bootErrors ?? {}), + reload: parsed.reload, + }; + } + } + if (!messages) messages = {}; - const title = locale?._bootErrors?.title || 'Failed to initialize Misskey'; - const reload = locale?.reload || 'Reload'; + const title = messages?.title || 'Failed to initialize Misskey'; + const reload = messages?.reload || 'Reload'; document.body.innerHTML = `

${title}
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 24794cbf2a..0c0b46f82b 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -22,62 +22,31 @@ return; } - //#region Detect language & fetch translations - if (!localStorage.hasOwnProperty('locale')) { - const supportedLangs = LANGS; - let lang = localStorage.getItem('lang'); - if (lang == null || !supportedLangs.includes(lang)) { - if (supportedLangs.includes(navigator.language)) { - lang = navigator.language; - } else { - lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); - - // Fallback - if (lang == null) lang = 'en-US'; - } - } - - const metaRes = await window.fetch('/api/meta', { - method: 'POST', - body: JSON.stringify({}), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - }); - if (metaRes.status !== 200) { - renderError('META_FETCH'); - return; - } - const meta = await metaRes.json(); - const v = meta.version; - if (v == null) { - renderError('META_FETCH_V'); - return; - } - - // for https://github.com/misskey-dev/misskey/issues/10202 - if (lang == null || lang.toString == null || lang.toString() === 'null') { - console.error('invalid lang value detected!!!', typeof lang, lang); - lang = 'en-US'; - } - - const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); - if (localRes.status === 200) { - localStorage.setItem('lang', lang); - localStorage.setItem('locale', await localRes.text()); - localStorage.setItem('localeVersion', v); + //#region Detect language + const supportedLangs = LANGS; + /** @type { string } */ + let lang = localStorage.getItem('lang'); + if (lang == null || !supportedLangs.includes(lang)) { + if (supportedLangs.includes(navigator.language)) { + lang = navigator.language; } else { - renderError('LOCALE_FETCH'); - return; + lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); + + // Fallback + if (lang == null) lang = 'en-US'; } } + + // for https://github.com/misskey-dev/misskey/issues/10202 + if (lang == null || lang.toString == null || lang.toString() === 'null') { + console.error('invalid lang value detected!!!', typeof lang, lang); + lang = 'en-US'; + } //#endregion //#region Script async function importAppScript() { - await import(`/vite/${CLIENT_ENTRY}`) + await import(CLIENT_ENTRY ? `/vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/vite/src/_boot_.ts') .catch(async e => { console.error(e); renderError('APP_IMPORT', e); @@ -94,23 +63,37 @@ } //#endregion - //#region Theme - const theme = localStorage.getItem('theme'); - if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + let isSafeMode = (localStorage.getItem('isSafeMode') === 'true'); - // HTMLの theme-color 適用 - if (k === 'htmlThemeColor') { - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', v); - break; + if (!isSafeMode) { + const urlParams = new URLSearchParams(window.location.search); + + if (urlParams.has('safemode') && urlParams.get('safemode') === 'true') { + localStorage.setItem('isSafeMode', 'true'); + isSafeMode = true; + } + } + + //#region Theme + if (!isSafeMode) { + const theme = localStorage.getItem('theme'); + if (theme) { + for (const [k, v] of Object.entries(JSON.parse(theme))) { + document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + + // HTMLの theme-color 適用 + if (k === 'htmlThemeColor') { + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', v); + break; + } } } } } } + const colorScheme = localStorage.getItem('colorScheme'); if (colorScheme) { document.documentElement.style.setProperty('color-scheme', colorScheme); @@ -127,11 +110,13 @@ document.documentElement.classList.add('useSystemFont'); } - const customCss = localStorage.getItem('customCss'); - if (customCss && customCss.length > 0) { - const style = document.createElement('style'); - style.innerHTML = customCss; - document.head.appendChild(style); + if (!isSafeMode) { + const customCss = localStorage.getItem('customCss'); + if (customCss && customCss.length > 0) { + const style = document.createElement('style'); + style.innerHTML = customCss; + document.head.appendChild(style); + } } async function addStyle(styleText) { @@ -146,9 +131,25 @@ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } - const locale = JSON.parse(localStorage.getItem('locale') || '{}'); + let messages = null; + const bootloaderLocales = localStorage.getItem('bootloaderLocales'); + if (bootloaderLocales) { + messages = JSON.parse(bootloaderLocales); + } + if (!messages) { + // older version of misskey does not store bootloaderLocales, stores locale as a whole + const legacyLocale = localStorage.getItem('locale'); + if (legacyLocale) { + const parsed = JSON.parse(legacyLocale); + messages = { + ...(parsed._bootErrors ?? {}), + reload: parsed.reload, + }; + } + } + if (!messages) messages = {}; - const messages = Object.assign({ + messages = Object.assign({ title: 'Failed to initialize Misskey', solution: 'The following actions may solve the problem.', solution1: 'Update your os and browser', @@ -159,8 +160,12 @@ otherOption1: 'Clear preferences and cache', otherOption2: 'Start the simple client', otherOption3: 'Start the repair tool', - }, locale?._bootErrors || {}); - const reload = locale?.reload || 'Reload'; + otherOption4: 'Start Misskey in safe mode', + reload: 'Reload', + }, messages); + + const safeModeUrl = new URL(window.location.href); + safeModeUrl.searchParams.set('safemode', 'true'); let errorsElement = document.getElementById('errors'); @@ -173,7 +178,7 @@

${messages.title}

${messages.solution}

${messages.solution1}

@@ -182,6 +187,12 @@

${messages.solution4}

${messages.otherOption} + + + +
-
+
+ + + + + + + +
+ +
+
+ +
+ + +
+
+
+ +
(), { defaultOpen: false, maxHeight: null, withSpacer: true, spacerMin: 14, spacerMax: 22, + canPage: true, }); const rootEl = useTemplateRef('rootEl'); +const asPage = props.canPage && deviceKind === 'smartphone' && prefer.s['experimental.enableFolderPageView']; const bgSame = ref(false); -const opened = ref(props.defaultOpen); -const openedAtLeastOnce = ref(props.defaultOpen); +const opened = ref(asPage ? false : props.defaultOpen); +const openedAtLeastOnce = ref(opened.value); //#region interpolate-sizeに対応していないブラウザ向け(TODO: 主要ブラウザが対応したら消す) function enter(el: Element) { @@ -126,7 +161,22 @@ function afterLeave(el: Element) { } //#endregion -function toggle() { +let pageId = pageFolderTeleportCount.value; +pageFolderTeleportCount.value += 1000; + +async function toggle() { + if (asPage && !opened.value) { + pageId++; + const { dispose } = await popup(MkFolderPage, { + pageId, + }, { + closed: () => { + opened.value = false; + dispose(); + }, + }); + } + if (!opened.value) { openedAtLeastOnce.value = true; } diff --git a/packages/frontend/src/components/MkFolderPage.vue b/packages/frontend/src/components/MkFolderPage.vue new file mode 100644 index 0000000000..21700c9dd8 --- /dev/null +++ b/packages/frontend/src/components/MkFolderPage.vue @@ -0,0 +1,159 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue index 0a902f3400..182ff3ccf5 100644 --- a/packages/frontend/src/components/MkFormDialog.file.vue +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -15,7 +15,7 @@ import * as Misskey from 'misskey-js'; import { computed, ref } from 'vue'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; -import { selectFile } from '@/utility/select-file.js'; +import { selectFile } from '@/utility/drive.js'; import { misskeyApi } from '@/utility/misskey-api.js'; const props = defineProps<{ @@ -51,7 +51,10 @@ if (props.fileId) { } function selectButton(ev: MouseEvent) { - selectFile(ev.currentTarget ?? ev.target).then(async (file) => { + selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }).then(async (file) => { if (!file) return; if (props.validate && !await props.validate(file)) return; diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue index fba5dc854c..fc3de2845e 100644 --- a/packages/frontend/src/components/MkFukidashi.vue +++ b/packages/frontend/src/components/MkFukidashi.vue @@ -10,7 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only tail === 'left' ? $style.left : $style.right, negativeMargin === true && $style.negativeMargin, shadow === true && $style.shadow, - accented === true && $style.accented + accented === true && $style.accented, + fullWidth === true && $style.fullWidth, ]" >
@@ -32,11 +33,13 @@ withDefaults(defineProps<{ negativeMargin?: boolean; shadow?: boolean; accented?: boolean; + fullWidth?: boolean; }>(), { tail: 'right', negativeMargin: false, shadow: false, accented: false, + fullWidth: false, }); @@ -73,6 +76,14 @@ withDefaults(defineProps<{ margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1); } } + + &.fullWidth { + width: 100%; + + &.content { + width: 100%; + } + } } .bg { @@ -85,6 +96,7 @@ withDefaults(defineProps<{ .content { position: relative; padding: 10px 14px; + box-sizing: border-box; } @container (max-width: 450px) { diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 49a6c65170..e6fae285b3 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- import * as Misskey from 'misskey-js'; import { computed, ref } from 'vue'; -import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; +import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { prefer } from '@/preferences.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 28bb936755..abbf86004b 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -18,7 +18,7 @@ import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { store } from '@/store.js'; -import { useChartTooltip } from '@/use/use-chart-tooltip.js'; +import { useChartTooltip } from '@/composables/use-chart-tooltip.js'; import { alpha } from '@/utility/color.js'; import { initChart } from '@/utility/init-chart.js'; diff --git a/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue b/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue new file mode 100644 index 0000000000..f734325039 --- /dev/null +++ b/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue new file mode 100644 index 0000000000..2c6185fd33 --- /dev/null +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -0,0 +1,303 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkImageEffectorFxForm.vue b/packages/frontend/src/components/MkImageEffectorFxForm.vue new file mode 100644 index 0000000000..d7ab620132 --- /dev/null +++ b/packages/frontend/src/components/MkImageEffectorFxForm.vue @@ -0,0 +1,95 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index e3a0a371b4..983a0932c3 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -52,15 +52,20 @@ import TestWebGL2 from '@/workers/test-webgl2?worker'; import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js'; import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; +// テスト環境で Web Worker インスタンスは作成できない +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +const isTest = (import.meta.env.MODE === 'test' || window.Cypress != null); + const canvasPromise = new Promise(resolve => { - // テスト環境で Web Worker インスタンスは作成できない - if (import.meta.env.MODE === 'test') { + if (isTest) { const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); return; } + const testWorker = new TestWebGL2(); testWorker.addEventListener('message', event => { if (event.data.result) { @@ -82,7 +87,7 @@ const canvasPromise = new Promise(resol @@ -84,6 +60,7 @@ $height: 2ex; height: $height; border-radius: 4px 0 0 4px; overflow: clip; + color: #fff; // text-shadowは重いから使うな @@ -106,5 +83,10 @@ $height: 2ex; font-weight: bold; white-space: nowrap; overflow: visible; + + // text-shadowは重いから使うな + color: var(--MI_THEME-fg); + -webkit-text-stroke: var(--MI_THEME-panel) .225em; + paint-order: stroke fill; } diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 3e5a88a170..584afff55c 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->