diff --git a/.config/example.yml b/.config/example.yml index c127eaae22..489cceec34 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -105,6 +105,16 @@ port: 3000 # socket: /path/to/misskey.sock # chmodSocket: '777' +# Proxy trust settings +# +# Changes how the server interpret the origin IP of the request. +# +# Any format supported by Fastify is accepted. +# Default: trust all proxies (i.e. trustProxy: true) +# See: https://fastify.dev/docs/latest/reference/server/#trustproxy +# +# trustProxy: 1 + # ┌──────────────────────────┐ #───┘ PostgreSQL configuration └──────────────────────────────── diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index 6117e69c03..76a0bae8cc 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Setup pnpm uses: pnpm/action-setup@v4.1.0 diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index 5ca27749bb..f4abedd960 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout head - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Setup Node.js uses: actions/setup-node@v4.4.0 with: diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml index 22d500c306..05034ea0f4 100644 --- a/.github/workflows/check-misskey-js-autogen.yml +++ b/.github/workflows/check-misskey-js-autogen.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} steps: - name: checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 with: submodules: true persist-credentials: false @@ -66,7 +66,7 @@ jobs: if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} steps: - name: checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 with: submodules: true persist-credentials: false diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml index 2b15cbee53..0e336a1551 100644 --- a/.github/workflows/check-misskey-js-version.yml +++ b/.github/workflows/check-misskey-js-version.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Check version run: | if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml index cf1fd6007d..d0b1be4991 100644 --- a/.github/workflows/check-spdx-license-id.yml +++ b/.github/workflows/check-spdx-license-id.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Check run: | counter=0 diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml index eaf922d4bc..d891a538c6 100644 --- a/.github/workflows/check_copyright_year.yml +++ b/.github/workflows/check_copyright_year.yml @@ -10,7 +10,7 @@ jobs: check_copyright_year: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 - run: | if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then echo "Please change copyright year!" diff --git a/.github/workflows/deploy-test-environment.yml b/.github/workflows/deploy-test-environment.yml index 46baf7421b..d838bc35e5 100644 --- a/.github/workflows/deploy-test-environment.yml +++ b/.github/workflows/deploy-test-environment.yml @@ -28,7 +28,7 @@ jobs: wait_time: ${{ steps.get-wait-time.outputs.wait_time }} steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Check allowed users id: check-allowed-users diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index 56dedf273d..e24ef00d78 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -27,7 +27,7 @@ jobs: platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index eb98273ba0..991fb85d85 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,7 +32,7 @@ jobs: platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker meta diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index 3054607913..1c0863e274 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -15,7 +15,7 @@ jobs: DOCKER_CONTENT_TRUST: 1 DOCKLE_VERSION: 0.4.14 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 - name: Download and install dockle v${{ env.DOCKLE_VERSION }} run: | curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb" @@ -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/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 933404dfa5..1cd1ef0ebd 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -25,7 +25,7 @@ jobs: ref: refs/pull/${{ github.event.number }}/merge steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: ref: ${{ matrix.ref }} submodules: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 550438e308..fba3cc4920 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,7 +36,7 @@ jobs: pnpm_install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: fetch-depth: 0 submodules: true @@ -69,7 +69,7 @@ jobs: eslint-cache-version: v1 eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: fetch-depth: 0 submodules: true @@ -81,7 +81,7 @@ jobs: cache: 'pnpm' - run: pnpm i --frozen-lockfile - name: Restore eslint cache - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.3.0 with: path: ${{ env.eslint-cache-path }} key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} @@ -99,7 +99,7 @@ jobs: - sw - misskey-js steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: fetch-depth: 0 submodules: true diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml index 68e45fdf61..63702c8bc7 100644 --- a/.github/workflows/locale.yml +++ b/.github/workflows/locale.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: fetch-depth: 0 submodules: true diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index 7787d6055b..f36eb0037d 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -16,7 +16,7 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true - name: Setup pnpm diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml index 1170f898ce..f24cd7d30e 100644 --- a/.github/workflows/report-api-diff.yml +++ b/.github/workflows/report-api-diff.yml @@ -16,7 +16,7 @@ jobs: # api-artifact steps: - name: Download artifact - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v7.1.0 with: script: | const fs = require('fs'); diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index b1d95c1b33..1611706ac5 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -22,12 +22,12 @@ jobs: NODE_OPTIONS: "--max_old_space_size=7168" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 if: github.event_name != 'pull_request_target' with: fetch-depth: 0 submodules: true - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 if: github.event_name == 'pull_request_target' with: fetch-depth: 0 @@ -90,7 +90,7 @@ jobs: env: CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - name: Notify that Chromatic detects changes - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v7.1.0 if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false' with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 5358df3dc4..e9791ac95e 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -50,7 +50,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true - name: Setup pnpm @@ -129,7 +129,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true - name: Setup pnpm @@ -173,7 +173,7 @@ jobs: POSTGRES_HOST_AUTH_METHOD: trust steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true - name: Setup pnpm diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 94e43cf91e..da3c9255f2 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true - name: Setup pnpm @@ -76,7 +76,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true # https://github.com/cypress-io/cypress-docker-images/issues/150 diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index f6d16bbd76..0fad57f8da 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4.3.0 - name: Setup pnpm uses: pnpm/action-setup@v4.1.0 diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 751c374608..08289dfd3b 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true - name: Setup pnpm diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index edff7dbecb..25353e4d29 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.0 with: submodules: true - name: Setup pnpm diff --git a/CHANGELOG.md b/CHANGELOG.md index 69851cee73..52f4fe0694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +## 2025.10.0 + +### NOTE +- pnpm 10.16.0 が必要です + +### General +- Feat: 予約投稿ができるようになりました + - デフォルトで作成可能数は1になっています。適宜ロールのポリシーで設定を行ってください。 +- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました +- Enhance: 依存関係の更新 +- Enhance: 翻訳の更新 + +### Client +- Feat: アカウントのQRコードを表示・読み取りできるようになりました +- Feat: 動画を圧縮してアップロードできるようになりました +- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました +- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし、モザイク)を追加 +- Enhance: 画像編集の集中線エフェクトを強化 +- Enhance: ウォーターマークにアカウントのQRコードを追加できるように +- Enhance: テーマをドラッグ&ドロップできるように +- Enhance: 絵文字ピッカーのサイズをより大きくできるように +- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上 +- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正 +- Fix: アクティビティウィジェットのグラフモードが動作しない問題を修正 +- Fix: デッキでリンクをダブルクリックすると、ウィンドウが2枚開いてしまう問題を修正 + +### Server +- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました + +## 2025.9.0 + +### Client +- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように +- Enhance: /flushページでサイトキャッシュをクリアできるようになりました +- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張 +- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように +- Enhance: Ctrlキー(Commandキー)を押下しながらリンクをクリックすると新しいタブで開くように +- Fix: プッシュ通知を有効にできない問題を修正 +- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正 +- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正 +- Fix: エラー画像が横に引き伸ばされてしまう問題に対応 + +### Server +- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正 + ## 2025.8.0 ### Note @@ -6,44 +51,78 @@ ### General - ノートを削除した際、関連するノートが同時に削除されないようになりました - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります -- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning) - - 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります +- 定期的に古いリモートの投稿を削除する機能が実装されました + - コントロールパネル→パフォーマンス→Remote Notes Cleaning で有効化できます - データベースの肥大化を防止することが可能です - 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。 - 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。 + - データベースサイズへの効果が見られない場合はautovacuumが有効になっているか確認してください - サーバーの初期設定が完了するまでは連合がオンにならないようになりました - 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました + - 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました + - 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが(過去のバージョンのMisskeyでも、当該機能は「チャット」ではなく「ダイレクトメッセージ」でした)、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました + - 今後、「チャット」の名称を「ダイレクトメッセージ」に戻す可能性があります - 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になります +- Feat: AiScriptが1.1.0に更新されました + - プラグインは1.xに対応したものが必要です + - Playはそのまま動作しますが、新規に作られるプリセットは1.xになります - 以前のバージョンから無効化されていた note_view_interruptor が有効になりました + - ハンドラは同期的である必要があります - Feat: セーフモード - プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます - 以下の方法でセーフモードを起動できます - `g` キーを連打する - URLに`?safemode=true`を付ける - PWAのショートカットで Safemode を選択して起動する +- Feat: 非ログイン時に表示されるトップページのスタイルを選択できるように + - コントロールパネル→ブランディング→エントランスページのスタイル - Feat: ページのタブバーを下部に表示できるように -- Enhance: コントロールパネルを検索できるように +- Feat: (実験的)iOSでの触覚フィードバックを有効にできるように +- Feat: コントロールパネルを検索できるように +- Enhance: 「自動でもっと見る」オプションが有効になり、安定性が向上しました - Enhance: トルコ語 (tr-TR) に対応 -- Enhance: 言語別のスクリプトバンドルを生成するように +- Enhance: 不必要な翻訳データを読み込まなくなり、パフォーマンスが向上しました +- Enhance: 画像エフェクトのパラメータ名の多言語対応 +- Enhance: ノートを非表示にする相対期間を1ヶ月単位で自由に指定できるように +- Enhance: メールアドレス確認画面のUIを改善 +- Enhance: アイコンのスクロール追従を無効化する際の適用範囲を強化 +- Enhance: レンダリングパフォーマンスの向上 +- Enhance: 依存ソフトウェアの更新 - Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正 - Fix: 一部の設定検索結果が存在しないパスになる問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) - Fix: テーマエディタが動作しない問題を修正 +- Fix: チャンネルのハイライトページにノートが表示されない問題を修正 - Fix: カラムの名前が正しくリスト/チャンネルの名前にならない問題を修正 -- Fix: デッキでリンクをダブルクリックすると、ウィンドウが2枚開いてしまう問題を修正 +- Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正 +- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正 +- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正 +- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正 +- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正 +- Fix: タッチ操作時にマウスホバー時のユーザープレビューが開くことがある問題を修正 +- Fix: 管理中アカウント一覧で正しい表示が行われない問題を修正 +- Fix: lookupページでリモートURLを指定した際に正しく動作しない問題を修正 ### Server +- Feat: サーバー管理コマンド + - `pnpm cli foo` の形式で実行可能です + - 現在以下のコマンドが利用可能です + - `reset-captcha` - CAPTCHA設定をリセットします - Enhance: ノートの削除処理の効率化 - Enhance: 全体的なパフォーマンスの向上 +- Enhance: 依存ソフトウェアの更新 +- Enhance: `clips/list` APIがページネーションに対応しました +- Fix: `notes/mentions` で場合によっては並び順が正しく返されない問題を修正 - Fix: SystemWebhook設定でsecretを空に出来ない問題を修正 +- Fix: 削除されたユーザーがチャットメッセージにリアクションしている場合`chat/history`などでエラーになる問題を修正 +- Fix: Pageのアイキャッチ画像をドライブから消してもPageごと消えないように +- Fix: タイムラインAPIの withRenotes: false 時のレスポンスを修正 ## 2025.7.0 diff --git a/idea/MkAbuseReport.stories.impl.ts b/idea/MkAbuseReport.stories.impl.ts index 717bceb23d..138e5cd0cd 100644 --- a/idea/MkAbuseReport.stories.impl.ts +++ b/idea/MkAbuseReport.stories.impl.ts @@ -4,8 +4,8 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; +import { action } from 'storybook/actions'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js'; import { commonHandlers } from '../packages/frontend/.storybook/mocks.js'; diff --git a/idea/MkAnimatedBg.dotted-ripples.vue b/idea/MkAnimatedBg.dotted-ripples.vue new file mode 100644 index 0000000000..f8f809c8ce --- /dev/null +++ b/idea/MkAnimatedBg.dotted-ripples.vue @@ -0,0 +1,232 @@ + + + + + + + diff --git a/idea/MkAnimatedBg.dotted.vue b/idea/MkAnimatedBg.dotted.vue new file mode 100644 index 0000000000..7f68b8972a --- /dev/null +++ b/idea/MkAnimatedBg.dotted.vue @@ -0,0 +1,190 @@ + + + + + + + diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index b5b832080f..117c38b677 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1599,3 +1599,13 @@ _watermarkEditor: type: "نوع" image: "صور" advanced: "متقدم" +_imageEffector: + _fxProps: + scale: "الحجم" + size: "الحجم" + offset: "الموضع" + color: "اللون" + opacity: "الشفافية" +_qr: + showTabTitle: "المظهر" + raw: "نص" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 93ee2b1cb5..395cee114a 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1357,3 +1357,13 @@ _watermarkEditor: text: "লেখা" image: "ছবি" advanced: "উন্নত" +_imageEffector: + _fxProps: + scale: "আকার" + size: "আকার" + color: "রং" + opacity: "অস্বচ্ছতা" + lightness: "উজ্জ্বল করুন" +_qr: + showTabTitle: "প্রদর্শন" + raw: "লেখা" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 7f26a96d37..bb1a42232d 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "Segur que voleu eliminar aquesta publicació?" pinLimitExceeded: "No podeu fixar més publicacions" done: "Fet" processing: "S'està processant..." +preprocessing: "Preparant" preview: "Vista prèvia" default: "Per defecte" defaultValueIs: "Per defecte: {value}" @@ -1054,6 +1055,7 @@ permissionDeniedError: "Operació no permesa " permissionDeniedErrorDescription: "Aquest compte no té suficients permisos per dur a terme aquesta acció " preset: "Predefinit" selectFromPresets: "Escull des dels predefinits" +custom: "Personalitzat" achievements: "Assoliments" gotInvalidResponseError: "Resposta del servidor invàlida " gotInvalidResponseErrorDescription: "No es pot contactar amb el servidor o potser es troba fora de línia per manteniment. Provar-ho de nou més tard." @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "Fent servir espais crearà expressions AND si l'ex hiddenTags: "Etiquetes ocultes" hiddenTagsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." notesSearchNotAvailable: "La cerca de notes no es troba disponible." +usersSearchNotAvailable: "La cerca d'usuaris no està disponible." license: "Llicència" unfavoriteConfirm: "Esborrar dels favorits?" myClips: "Els meus retalls" @@ -1243,7 +1246,7 @@ releaseToRefresh: "Deixar anar per actualitzar" refreshing: "Recarregant..." pullDownToRefresh: "Llisca cap a baix per recarregar" useGroupedNotifications: "Mostrar les notificacions agrupades " -signupPendingError: "Hi ha hagut un problema verificant l'adreça de correu electrònic. L'enllaç pot haver caducat." +emailVerificationFailedError: "Hem tingut un problema en verificar la teva adreça de correu electrònic. És probable que l'enllaç estigui caducat." cwNotationRequired: "Si està activat \"Amagar contingut\" s'ha d'escriure una descripció " doReaction: "Afegeix una reacció " code: "Codi" @@ -1314,6 +1317,7 @@ acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perill 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 " +draftsAndScheduledNotes: "Esborranys i publicacions programades" confirmOnReact: "Confirmar en reaccionar" reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?" markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?" @@ -1341,6 +1345,8 @@ postForm: "Formulari de publicació" textCount: "Nombre de caràcters " information: "Informació" chat: "Xat" +directMessage: "Xateja amb aquest usuari" +directMessage_short: "Missatge" migrateOldSettings: "Migrar la configuració anterior" migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual." compress: "Comprimir " @@ -1368,16 +1374,35 @@ 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." +defaultCompressionLevel: "Nivell de compressió predeterminat" +defaultCompressionLevel_description: "Si el redueixes augmentaràs la qualitat de la imatge, però la mida de l'arxiu serà més gran.
Si augmentes l'opció redueixes la mida de l'arxiu i la qualitat de la imatge és pitjor." 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." +thankYouForTestingBeta: "Gràcies per ajudar-nos a provar la versió beta!" +createUserSpecifiedNote: "Crear notes especificades per l'usuari " +schedulePost: "Programar una nota" +scheduleToPostOnX: "Programar una nota per {x}" +scheduledToPostOnX: "S'ha programat la nota per {x}" +schedule: "Programa" +scheduled: "Programat" +_compression: + _quality: + high: "Qualitat alta" + medium: "Qualitat mitjana" + low: "Qualitat baixa" + _size: + large: "Mida gran" + medium: "Mida mitjana" + small: "Mida petita" _order: newest: "Més recent" oldest: "Antigues primer" _chat: + messages: "Missatge" noMessagesYet: "Encara no tens missatges " newMessage: "Missatge nou" individualChat: "Xat individual " @@ -1534,7 +1559,7 @@ _announcement: needConfirmationToRead: "Es necessita confirmació de lectura de la notificació " needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació. A més aquesta notificació serà exclosa de qualsevol funcionalitat com \"Marcar tot com a llegit\"." end: "Final de la notificació " - tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els avisos que siguin antics." + tooManyActiveAnnouncementDescription: "Tenir masses notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els avisos que siguin antics." readConfirmTitle: "Marcar com llegida?" readConfirmText: "Això marcarà el contingut de \"{title}\" com llegit." shouldNotBeUsedToPresentPermanentInfo: "Ja que l'ús de notificacions pot impactar l'experiència dels nous usuaris, és recomanable fer servir les notificacions amb el flux d'informació en comptes de fer-les servir en un únic bloc." @@ -1641,7 +1666,7 @@ _serverSettings: 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" + remoteNotesCleaningMaxProcessingDuration: "Duració 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ó." @@ -1663,6 +1688,9 @@ _serverSettings: 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." + entrancePageStyle: "Estil de la pàgina d'inici" + showTimelineForVisitor: "Mostrar la línia de temps" + showActivitiesForVisitor: "Mostrar activitat" _userGeneratedContentsVisibilityForVisitor: all: "Tot obert al públic " localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat" @@ -1999,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "Límits baixos són menys restrictius, límits alts són més restrictius." canHideAds: "Pot amagar la publicitat" canSearchNotes: "Pot cercar notes" + canSearchUsers: "Pot cercar usuaris" canUseTranslator: "Pot fer servir el traductor" avatarDecorationLimit: "Nombre màxim de decoracions que es poden aplicar els avatars" canImportAntennas: "Autoritza la importació d'antenes " @@ -2011,6 +2040,7 @@ _role: 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" + scheduledNoteLimit: "Màxim nombre de notes programades que es poden crear simultàniament" watermarkAvailable: "Pots fer servir la marca d'aigua" _condition: roleAssignedTo: "Assignat a rols manuals" @@ -2069,7 +2099,7 @@ _ad: timezoneinfo: "El dia de la setmana ve determinat del fus horari del servidor." adsSettings: "Configurar la publicitat" notesPerOneAd: "Interval d'emplaçament publicitari en temps real (Notes per anuncis)" - setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització d'anuncis en temps real" + setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització de publicitat en temps real" adsTooClose: "L'interval actual pot fer que l'experiència de l'usuari sigui dolenta perquè l'interval és molt baix." _forgotPassword: enterEmail: "Escriu l'adreça de correu electrònic amb la que et vas registrar. S'enviarà un correu electrònic amb un enllaç perquè puguis canviar-la." @@ -2271,6 +2301,7 @@ _time: minute: "Minut(s)" hour: "Hor(a)(es)" day: "Di(a)(es)" + month: "Mes(os)" _2fa: alreadyRegistered: "J has registrat un dispositiu d'autenticació de doble factor." registerTOTP: "Registrar una aplicació autenticadora" @@ -2445,7 +2476,7 @@ _widgets: chooseList: "Tria una llista" clicker: "Clicker" birthdayFollowings: "Usuaris que fan l'aniversari avui" - chat: "Xat" + chat: "Xateja amb aquest usuari" _cw: hide: "Amagar" show: "Carregar més" @@ -2635,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Has rebut una petició de seguiment" yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada" pollEnded: "Ja pots veure els resultats de l'enquesta " + scheduledNotePosted: "Una nota programada ha sigut publicada" + scheduledNotePostFailed: "Ha fallat la publicació d'una nota programada" newNote: "Nota nova" unreadAntennaNote: "Antena {name}" roleAssigned: "Rol assignat " @@ -2714,7 +2747,7 @@ _deck: mentions: "Mencions" direct: "Publicacions directes" roleTimeline: "Línia de temps dels rols" - chat: "Xat" + chat: "Xateja amb aquest usuari" _dialog: charactersExceeded: "Has arribat al màxim de caràcters! Actualment és {current} de {max}" charactersBelow: "Ets per sota del mínim de caràcters! Actualment és {current} de {min}" @@ -3160,14 +3193,16 @@ _watermarkEditor: opacity: "Opacitat" scale: "Mida" text: "Text" + qr: "Codi QR" position: "Posició " + margin: "Marge" type: "Tipus" image: "Imatges" advanced: "Avançat" + angle: "Angle" stripe: "Bandes" stripeWidth: "Amplada de la banda" stripeFrequency: "Freqüència de la banda" - angle: "Angle" polkadot: "Lunars" checker: "Escacs" polkadotMainDotOpacity: "Opacitat del lunar principal" @@ -3175,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacitat del lunar secundari" polkadotSubDotRadius: "Mida del lunar secundari" polkadotSubDotDivisions: "Nombre de punts secundaris" + leaveBlankToAccountUrl: "Si deixes aquest camp buit, es farà servir l'URL del teu compte" _imageEffector: title: "Efecte" addEffect: "Afegeix un efecte" discardChangesConfirm: "Vols descartar els canvis i sortir?" + nothingToConfigure: "No hi ha opcions de configuració disponibles" _fxs: chromaticAberration: "Aberració cromàtica" glitch: "Glitch" mirror: "Mirall" invert: "Inversió cromàtica " grayscale: "Monocrom " + blur: "Desenfocament" + pixelate: "Mosaic" colorAdjust: "Correcció de color" colorClamp: "Compressió cromàtica " colorClampAdvanced: "Compressió de cromàtica avançada " @@ -3196,6 +3235,43 @@ _imageEffector: checker: "Escacs" blockNoise: "Bloqueig de soroll" tearing: "Trencament d'imatge " + fill: "Omplir" + _fxProps: + angle: "Angle" + scale: "Mida" + size: "Mida" + radius: "Radi" + samples: "Mida de la mostra" + offset: "Posició " + color: "Color" + opacity: "Opacitat" + normalize: "Normalitzar" + amount: "Quantitat" + lightness: "Brillantor" + contrast: "Contrast" + hue: "Tonalitat" + brightness: "Brillantor" + saturation: "Saturació" + max: "Màxim" + min: "Mínim" + direction: "Direcció " + phase: "Fase" + frequency: "Freqüència " + strength: "Intensitat" + glitchChannelShift: "Canvi de canal " + seed: "Llindar" + redComponent: "Component vermell" + greenComponent: "Component verd" + blueComponent: "Component blau" + threshold: "Llindar" + centerX: "Centre de X" + centerY: "Centre de Y" + zoomLinesSmoothing: "Suavitzat" + zoomLinesSmoothingDescription: "Els paràmetres de suavitzat i amplada de línia en augmentar no es poden fer servir junts." + zoomLinesThreshold: "Amplada de línia a l'augmentar " + zoomLinesMaskSize: "Diàmetre del centre" + zoomLinesBlack: "Obscurir" + circle: "Cercle" drafts: "Esborrany " _drafts: select: "Seleccionar esborrany" @@ -3211,3 +3287,22 @@ _drafts: restoreFromDraft: "Restaurar des dels esborranys" restore: "Restaurar esborrany" listDrafts: "Llistat d'esborranys" + schedule: "Programació esborranys" + listScheduledNotes: "Llista de notes programades" + cancelSchedule: "Cancel·lar la programació" +qr: "Codi QR" +_qr: + showTabTitle: "Veure" + readTabTitle: "Escanejar " + shareTitle: "{name} {acct}" + shareText: "Segueix-me al Fediverse" + chooseCamera: "Seleccionar càmera " + cannotToggleFlash: "No es pot activar el flaix" + turnOnFlash: "Activar el flaix" + turnOffFlash: "Apagar el flaix" + startQr: "Reiniciar el lector de codis QR" + stopQr: "Parar el lector de codis QR" + noQrCodeFound: "No s'ha trobat cap codi QR" + scanFile: "Escanejar la imatge des del dispositiu" + raw: "Text" + mfm: "MFM" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 5945ceaf96..0d2ddccc60 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -2053,3 +2053,14 @@ _watermarkEditor: type: "Typ" image: "Obrázky" advanced: "Pokročilé" +_imageEffector: + _fxProps: + scale: "Velikost" + size: "Velikost" + offset: "Pozice" + color: "Barva" + opacity: "Průhlednost" + lightness: "Zesvětlit" +_qr: + showTabTitle: "Zobrazit" + raw: "Text" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index e3897613a0..ba74564fc3 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1243,7 +1243,6 @@ releaseToRefresh: "Zum Aktualisieren loslassen" refreshing: "Wird aktualisiert..." pullDownToRefresh: "Zum Aktualisieren ziehen" useGroupedNotifications: "Benachrichtigungen gruppieren" -signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen. Der Link könnte abgelaufen sein." cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." doReaction: "Reagieren" code: "Code" @@ -1341,6 +1340,7 @@ postForm: "Notizfenster" textCount: "Zeichenanzahl" information: "Über" chat: "Chat" +directMessage: "Mit dem Benutzer chatten" migrateOldSettings: "Alte Client-Einstellungen migrieren" migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben." compress: "Komprimieren" @@ -2434,7 +2434,7 @@ _widgets: chooseList: "Liste auswählen" clicker: "Klickzähler" birthdayFollowings: "Nutzer, die heute Geburtstag haben" - chat: "Chat" + chat: "Mit dem Benutzer chatten" _cw: hide: "Inhalt verbergen" show: "Inhalt anzeigen" @@ -2703,7 +2703,7 @@ _deck: mentions: "Erwähnungen" direct: "Direktnachrichten" roleTimeline: "Rollenchronik" - chat: "Chat" + chat: "Mit dem Benutzer chatten" _dialog: charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}" charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}" @@ -3147,10 +3147,10 @@ _watermarkEditor: type: "Art" image: "Bilder" advanced: "Fortgeschritten" + angle: "Winkel" stripe: "Streifen" stripeWidth: "Linienbreite" stripeFrequency: "Linienanzahl" - angle: "Winkel" polkadot: "Punktmuster" polkadotMainDotOpacity: "Deckkraft des Hauptpunktes" polkadotMainDotRadius: "Größe des Hauptpunktes" @@ -3173,6 +3173,14 @@ _imageEffector: distort: "Verzerrung" stripe: "Streifen" polkadot: "Punktmuster" + _fxProps: + angle: "Winkel" + scale: "Größe" + size: "Größe" + offset: "Position" + color: "Farbe" + opacity: "Transparenz" + lightness: "Erhellen" drafts: "Entwurf" _drafts: select: "Entwurf auswählen" @@ -3187,3 +3195,6 @@ _drafts: restoreFromDraft: "Aus Entwurf wiederherstellen" restore: "Wiederherstellen" listDrafts: "Liste der Entwürfe" +_qr: + showTabTitle: "Anzeigeart" + raw: "Text" diff --git a/locales/en-US.yml b/locales/en-US.yml index f5c0f25ad0..049ad54d82 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "Are you sure you want to delete this note?" pinLimitExceeded: "You cannot pin any more notes" done: "Done" processing: "Processing..." +preprocessing: "Preparing..." preview: "Preview" default: "Default" defaultValueIs: "Default: {value}" @@ -1054,6 +1055,7 @@ permissionDeniedError: "Operation denied" permissionDeniedErrorDescription: "This account does not have the permission to perform this action." preset: "Preset" selectFromPresets: "Choose from presets" +custom: "Custom" achievements: "Achievements" gotInvalidResponseError: "Invalid server response" gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later." @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "Using spaces will create AND expressions and surro hiddenTags: "Hidden hashtags" hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines." notesSearchNotAvailable: "Note search is unavailable." +usersSearchNotAvailable: "User search is not available." license: "License" unfavoriteConfirm: "Really remove from favorites?" myClips: "My clips" @@ -1216,8 +1219,8 @@ showRepliesToOthersInTimeline: "Show replies to others in timeline" hideRepliesToOthersInTimeline: "Hide replies to others from timeline" showRepliesToOthersInTimelineAll: "Show replies to others from everyone you follow in timeline" hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you follow in timeline" -confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?" -confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" +confirmShowRepliesAll: "Are you sure you want to show replies from everyone you follow in your timeline? This action is irreversible." +confirmHideRepliesAll: "Are you sure you want to hide replies from everyone you follow in your timeline? This action is irreversible." externalServices: "External Services" sourceCode: "Source code" sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem." @@ -1243,7 +1246,7 @@ releaseToRefresh: "Release to refresh" refreshing: "Refreshing..." pullDownToRefresh: "Pull down to refresh" useGroupedNotifications: "Display grouped notifications" -signupPendingError: "There was a problem verifying the email address. The link may have expired." +emailVerificationFailedError: "A problem occurred while verifying your email address. The link may have expired." cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided." doReaction: "Add reaction" code: "Code" @@ -1314,6 +1317,7 @@ 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" +draftsAndScheduledNotes: "Drafts and scheduled notes" confirmOnReact: "Confirm when reacting" reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?" markAsSensitiveConfirm: "Do you want to set this media as sensitive?" @@ -1341,6 +1345,8 @@ postForm: "Posting form" textCount: "Character count" information: "About" chat: "Chat" +directMessage: "Chat with user" +directMessage_short: "Message" migrateOldSettings: "Migrate old client settings" migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten." compress: "Compress" @@ -1368,16 +1374,35 @@ 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." +defaultCompressionLevel: "Default compression level" +defaultCompressionLevel_description: "Lower compression preserves quality but increases file size.
Higher compression reduces file size but lowers 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." +thankYouForTestingBeta: "Thank you for helping us test the beta version!" +createUserSpecifiedNote: "Create a direct note" +schedulePost: "Schedule note" +scheduleToPostOnX: "Scheduled to note on {x}" +scheduledToPostOnX: "Note is scheduled for {x}" +schedule: "Schedule" +scheduled: "Scheduled" +_compression: + _quality: + high: "High quality" + medium: "Medium quality" + low: "Low quality" + _size: + large: "Large size" + medium: "Medium size" + small: "Small size" _order: newest: "Newest First" oldest: "Oldest First" _chat: + messages: "Messages" noMessagesYet: "No messages yet" newMessage: "New message" individualChat: "Private Chat" @@ -1465,6 +1490,7 @@ _settings: 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." + showPageTabBarBottom: "Show page tab bar at the bottom" _chat: showSenderName: "Show sender's name" sendOnEnter: "Press Enter to send" @@ -1662,6 +1688,9 @@ _serverSettings: 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." + entrancePageStyle: "Entrance page style" + showTimelineForVisitor: "Show timeline" + showActivitiesForVisitor: "Show activities" _userGeneratedContentsVisibilityForVisitor: all: "Everything is public" localOnly: "Only local content is published, remote content is kept private" @@ -1998,19 +2027,21 @@ _role: descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" + canSearchUsers: "User search" canUseTranslator: "Translator usage" - avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" - canImportAntennas: "Allow importing antennas" - canImportBlocking: "Allow importing blocking" - canImportFollowing: "Allow importing following" - canImportMuting: "Allow importing muting" - canImportUserLists: "Allow importing lists" - chatAvailability: "Allow Chat" + avatarDecorationLimit: "Maximum number of avatar decorations" + canImportAntennas: "Can import antennas" + canImportBlocking: "Can import blocking" + canImportFollowing: "Can import following" + canImportMuting: "Can import muting" + canImportUserLists: "Can import lists" + chatAvailability: "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" + scheduledNoteLimit: "Maximum number of simultaneous scheduled notes" + watermarkAvailable: "Watermark function" _condition: roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" @@ -2270,6 +2301,7 @@ _time: minute: "Minute(s)" hour: "Hour(s)" day: "Day(s)" + month: "Month(s)" _2fa: alreadyRegistered: "You have already registered a 2-factor authentication device." registerTOTP: "Register authenticator app" @@ -2444,7 +2476,7 @@ _widgets: chooseList: "Select a list" clicker: "Clicker" birthdayFollowings: "Today's Birthdays" - chat: "Chat" + chat: "Chat with user" _cw: hide: "Hide" show: "Show content" @@ -2634,6 +2666,8 @@ _notification: youReceivedFollowRequest: "You've received a follow request" yourFollowRequestAccepted: "Your follow request was accepted" pollEnded: "Poll results have become available" + scheduledNotePosted: "Scheduled note has been posted" + scheduledNotePostFailed: "Failed to post scheduled note" newNote: "New note" unreadAntennaNote: "Antenna {name}" roleAssigned: "Role given" @@ -2713,7 +2747,7 @@ _deck: mentions: "Mentions" direct: "Direct notes" roleTimeline: "Role Timeline" - chat: "Chat" + chat: "Chat with user" _dialog: charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}." charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." @@ -3159,14 +3193,16 @@ _watermarkEditor: opacity: "Opacity" scale: "Size" text: "Text" + qr: "QR Code" position: "Position" + margin: "Margin" type: "Type" image: "Images" advanced: "Advanced" + angle: "Angle" stripe: "Stripes" stripeWidth: "Line width" stripeFrequency: "Lines count" - angle: "Angle" polkadot: "Polkadot" checker: "Checker" polkadotMainDotOpacity: "Opacity of the main dot" @@ -3174,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacity of the secondary dot" polkadotSubDotRadius: "Size of the secondary dot" polkadotSubDotDivisions: "Number of sub-dots." + leaveBlankToAccountUrl: "Leave blank to use account URL" _imageEffector: title: "Effects" addEffect: "Add Effects" discardChangesConfirm: "Are you sure you want to leave? You have unsaved changes." + nothingToConfigure: "No configurable options available" _fxs: chromaticAberration: "Chromatic Aberration" glitch: "Glitch" mirror: "Mirror" invert: "Invert Colors" grayscale: "Grayscale" + blur: "Blur" + pixelate: "Pixelate" colorAdjust: "Color Correction" colorClamp: "Color Compression" colorClampAdvanced: "Color Compression (Advanced)" @@ -3195,6 +3235,43 @@ _imageEffector: checker: "Checker" blockNoise: "Block Noise" tearing: "Tearing" + fill: "Fill" + _fxProps: + angle: "Angle" + scale: "Size" + size: "Size" + radius: "Radius" + samples: "Sample count" + offset: "Position" + color: "Color" + opacity: "Opacity" + normalize: "Normalize" + amount: "Amount" + lightness: "Lighten" + contrast: "Contrast" + hue: "Hue" + brightness: "Brightness" + saturation: "Saturation" + max: "Maximum" + min: "Minimum" + direction: "Direction" + phase: "Phase" + frequency: "Frequency" + strength: "Strength" + glitchChannelShift: "Channel shift" + seed: "Seed value" + redComponent: "Red component" + greenComponent: "Green component" + blueComponent: "Blue component" + threshold: "Threshold" + centerX: "Center X" + centerY: "Center Y" + zoomLinesSmoothing: "Smoothing" + zoomLinesSmoothingDescription: "Smoothing and zoom line width cannot be used together." + zoomLinesThreshold: "Zoom line width" + zoomLinesMaskSize: "Center diameter" + zoomLinesBlack: "Make black" + circle: "Circular" drafts: "Drafts" _drafts: select: "Select Draft" @@ -3210,3 +3287,22 @@ _drafts: restoreFromDraft: "Restore from Draft" restore: "Restore" listDrafts: "List of Drafts" + schedule: "Schedule note" + listScheduledNotes: "Scheduled notes list" + cancelSchedule: "Cancel schedule" +qr: "QR Code" +_qr: + showTabTitle: "Display" + readTabTitle: "Scan" + shareTitle: "{name} {acct}" + shareText: "Follow me on the Fediverse!" + chooseCamera: "Choose camera" + cannotToggleFlash: "Unable to toggle flashlight" + turnOnFlash: "Turn on flashlight" + turnOffFlash: "Turn off flashlight" + startQr: "Resume QR code reader" + stopQr: "Stop QR code reader" + noQrCodeFound: "No QR code found" + scanFile: "Scan image from device" + raw: "Text" + mfm: "MFM" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 20289f605c..5ba924c78a 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "¿Quieres borrar esta nota?" pinLimitExceeded: "Ya no se pueden fijar más notas" done: "Terminado" processing: "Procesando..." +preprocessing: "Preparando" preview: "Vista previa" default: "Predeterminado" defaultValueIs: "Por defecto: {value}" @@ -910,7 +911,7 @@ pubSub: "Cuentas Pub/Sub" lastCommunication: "Última comunicación" resolved: "Resuelto" unresolved: "Sin resolver" -breakFollow: "Dejar de seguir" +breakFollow: "Eliminar seguidor" breakFollowConfirm: "¿Quieres dejar de seguir?" itsOn: "¡Está encendido!" itsOff: "¡Está apagado!" @@ -1054,6 +1055,7 @@ permissionDeniedError: "Operación denegada" permissionDeniedErrorDescription: "Esta cuenta no tiene permisos para hacer esa acción." preset: "Predefinido" selectFromPresets: "Escoger desde predefinidos" +custom: "Personalizado" achievements: "Logros" gotInvalidResponseError: "Respuesta del servidor inválida" gotInvalidResponseErrorDescription: "Puede que el servidor esté caído o en mantenimiento. Favor de intentar más tarde" @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "Si se usan espacios se crearán expresiones AND y hiddenTags: "Hashtags ocultos" hiddenTagsDescription: "Selecciona las etiquetas que no se mostrarán en tendencias. Una etiqueta por línea." notesSearchNotAvailable: "No se puede buscar una nota" +usersSearchNotAvailable: "La búsqueda de usuarios no está disponible." license: "Licencia" unfavoriteConfirm: "¿Desea quitar de favoritos?" myClips: "Mis clips" @@ -1243,7 +1246,7 @@ releaseToRefresh: "Soltar para recargar" refreshing: "Recargando..." pullDownToRefresh: "Tira hacia abajo para recargar" useGroupedNotifications: "Mostrar notificaciones agrupadas" -signupPendingError: "Ha habido un problema al verificar tu dirección de correo electrónico. Es posible que el enlace haya caducado." +emailVerificationFailedError: "Se ha producido un error al confirmar tu dirección de correo electrónico. Es posible que el enlace haya caducado." cwNotationRequired: "Si se ha activado \"ocultar contenido\", es necesario proporcionar una descripción." doReaction: "Añadir reacción" code: "Código" @@ -1314,6 +1317,7 @@ 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" +draftsAndScheduledNotes: "Borradores y notas programadas" confirmOnReact: "Confirmar la reacción" reactAreYouSure: "¿Quieres añadir una reacción «{emoji}»?" markAsSensitiveConfirm: "¿Desea establecer este medio multimedia(Imagen,vídeo...) como sensible?" @@ -1341,6 +1345,8 @@ postForm: "Formulario" textCount: "caracteres" information: "Información" chat: "Chat" +directMessage: "Chatear" +directMessage_short: "Mensaje" 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" @@ -1368,16 +1374,35 @@ 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." +defaultCompressionLevel: "Nivel de compresión predeterminado" +defaultCompressionLevel_description: "Al reducir el ajuste se conserva la calidad, pero aumenta el tamaño del archivo.
Al aumentar el ajuste se reduce el tamaño del archivo, pero disminuye la calidad." 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." +thankYouForTestingBeta: "¡Gracias por tu colaboración en la prueba de la versión beta!" +createUserSpecifiedNote: "Crear notas especificadas por el usuario" +schedulePost: "Programar una nota" +scheduleToPostOnX: "Programar una nota para {x}" +scheduledToPostOnX: "La nota está programada para {x}." +schedule: "Programado" +scheduled: "Programado" +_compression: + _quality: + high: "Calidad alta" + medium: "Calidad media" + low: "Calidad baja" + _size: + large: "Tamaño grande" + medium: "Tamaño mediano" + small: "Tamaño pequeño" _order: newest: "Los más recientes primero" oldest: "Los más antiguos primero" _chat: + messages: "Mensaje" noMessagesYet: "Aún no hay mensajes" newMessage: "Mensajes nuevos" individualChat: "Chat individual" @@ -1663,6 +1688,9 @@ _serverSettings: 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" + entrancePageStyle: "Estilo de la página de inicio" + showTimelineForVisitor: "Mostrar la línea de tiempo" + showActivitiesForVisitor: "Mostrar actividades" _userGeneratedContentsVisibilityForVisitor: all: "Todo es público." localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado" @@ -1999,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos" canHideAds: "Puede ocultar anuncios" canSearchNotes: "Uso de la búsqueda de notas" + canSearchUsers: "Uso de la búsqueda de usuarios" canUseTranslator: "Uso de traductor" avatarDecorationLimit: "Número máximo de decoraciones de avatar" canImportAntennas: "Permitir la importación de antenas" @@ -2011,6 +2040,7 @@ _role: 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" + scheduledNoteLimit: "Máximo número de notas programadas que se pueden crear simultáneamente." watermarkAvailable: "Disponibilidad de la función de marca de agua" _condition: roleAssignedTo: "Asignado a roles manuales" @@ -2130,7 +2160,7 @@ _aboutMisskey: _displayOfSensitiveMedia: respect: "Esconder medios marcados como sensibles" ignore: "Mostrar medios marcados como sensibles" - force: "Esconder todala multimedia" + force: "Esconder toda la multimedia" _instanceTicker: none: "No mostrar" remote: "Mostrar a usuarios remotos" @@ -2271,6 +2301,7 @@ _time: minute: "Minutos" hour: "Horas" day: "Días" + month: "Mes(es)" _2fa: alreadyRegistered: "Ya has completado la configuración." registerTOTP: "Registrar aplicación autenticadora" @@ -2445,7 +2476,7 @@ _widgets: chooseList: "Seleccione una lista" clicker: "Cliqueador" birthdayFollowings: "Hoy cumplen años" - chat: "Chat" + chat: "Chatear" _cw: hide: "Ocultar" show: "Ver más" @@ -2635,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Has mandado una solicitud de seguimiento" yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada" pollEnded: "Estan disponibles los resultados de la encuesta" + scheduledNotePosted: "Una nota programada ha sido publicada" + scheduledNotePostFailed: "Ha fallado la publicación de una nota programada" newNote: "Nueva nota" unreadAntennaNote: "Antena {name}" roleAssigned: "Rol asignado" @@ -2714,7 +2747,7 @@ _deck: mentions: "Menciones" direct: "Notas directas" roleTimeline: "Linea de tiempo del rol" - chat: "Chat" + chat: "Chatear" _dialog: charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}." @@ -3160,14 +3193,16 @@ _watermarkEditor: opacity: "Opacidad" scale: "Tamaño" text: "Texto" + qr: "Código QR" position: "Posición" + margin: "Margen" type: "Tipo" image: "Imágenes" advanced: "Avanzado" + angle: "Ángulo" 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" @@ -3175,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacidad del círculo secundario" polkadotSubDotRadius: "Tamaño del círculo secundario." polkadotSubDotDivisions: "Número de subpuntos." + leaveBlankToAccountUrl: "Si dejas este campo en blanco, se utilizará la URL de tu cuenta." _imageEffector: title: "Efecto" addEffect: "Añadir Efecto" discardChangesConfirm: "¿Ignorar cambios y salir?" + nothingToConfigure: "No hay opciones configurables disponibles." _fxs: chromaticAberration: "Aberración Cromática" glitch: "Glitch" mirror: "Espejo" invert: "Invertir colores" grayscale: "Blanco y negro" + blur: "Difuminar" + pixelate: "Pixelar" colorAdjust: "Corrección de Color" colorClamp: "Compresión cromática" colorClampAdvanced: "Compresión cromática avanzada" @@ -3196,6 +3235,43 @@ _imageEffector: checker: "Corrector" blockNoise: "Bloquear Ruido" tearing: "Rasgado de Imagen (Tearing)" + fill: "Relleno de color" + _fxProps: + angle: "Ángulo" + scale: "Tamaño" + size: "Tamaño" + radius: "Radio" + samples: "Tamaño de muestra" + offset: "Posición" + color: "Color" + opacity: "Opacidad" + normalize: "Normalización" + amount: "Cantidad" + lightness: "Brillo" + contrast: "Contraste" + hue: "Tonalidad" + brightness: "Brillo" + saturation: "Saturación" + max: "Valor máximo" + min: "Valor mínimo" + direction: "Dirección" + phase: "Fase" + frequency: "Frecuencia" + strength: "Intensidad" + glitchChannelShift: "cambio de canal de imagen" + seed: "Valor de la semilla" + redComponent: "Componente rojo" + greenComponent: "Componente Verde" + blueComponent: "Componente Azul" + threshold: "Umbral" + centerX: "Centrar X" + centerY: "Centrar Y" + zoomLinesSmoothing: "Suavizado" + zoomLinesSmoothingDescription: "El suavizado y el ancho de línea de zoom no se pueden utilizar juntos." + zoomLinesThreshold: "Ancho de línea del zoom" + zoomLinesMaskSize: "Diámetro del centro" + zoomLinesBlack: "Hacer oscuro" + circle: "Círculo" drafts: "Borrador" _drafts: select: "Seleccionar borradores" @@ -3211,3 +3287,22 @@ _drafts: restoreFromDraft: "Restaurar desde los borradores" restore: "Restaurar" listDrafts: "Listar los borradores" + schedule: "Programar Nota" + listScheduledNotes: "Lista de notas programadas" + cancelSchedule: "Cancelar programación" +qr: "Código QR" +_qr: + showTabTitle: "Apariencia" + readTabTitle: "Escanear" + shareTitle: "{name} {acct}" + shareText: "¡Sígueme en el Fediverso!" + chooseCamera: "Seleccione cámara" + cannotToggleFlash: "No se puede activar el flash" + turnOnFlash: "Encender el flash" + turnOffFlash: "Apagar el flash" + startQr: "Reiniciar el lector de códigos QR" + stopQr: "Detener el lector de códigos QR" + noQrCodeFound: "No se encontró el código QR" + scanFile: "Escanear imagen desde un dispositivo" + raw: "Texto" + mfm: "MFM" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index d68e7dfde4..23c7ba97bb 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1208,7 +1208,6 @@ releaseToRefresh: "Relâcher pour rafraîchir" refreshing: "Rafraîchissement..." pullDownToRefresh: "Tirer vers le bas pour rafraîchir" useGroupedNotifications: "Grouper les notifications" -signupPendingError: "Un problème est survenu lors de la vérification de votre adresse e-mail. Le lien a peut-être expiré." cwNotationRequired: "Si « Masquer le contenu » est activé, une description doit être fournie." doReaction: "Réagir" code: "Code" @@ -2372,3 +2371,15 @@ _watermarkEditor: image: "Images" advanced: "Avancé" angle: "Angle" +_imageEffector: + _fxProps: + angle: "Angle" + scale: "Taille" + size: "Taille" + offset: "Position" + color: "Couleur" + opacity: "Transparence" + lightness: "Clair" +_qr: + showTabTitle: "Affichage" + raw: "Texte" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 009142a4ad..abb0720c6b 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1212,7 +1212,6 @@ releaseToRefresh: "Lepaskan untuk memuat ulang" refreshing: "Sedang memuat ulang..." pullDownToRefresh: "Tarik ke bawah untuk memuat ulang" useGroupedNotifications: "Tampilkan notifikasi secara dikelompokkan" -signupPendingError: "Terdapat masalah ketika memverifikasi alamat surel. Tautan kemungkinan telah kedaluwarsa." cwNotationRequired: "Jika \"Sembunyikan konten\" diaktifkan, deskripsi harus disediakan." doReaction: "Tambahkan reaksi" code: "Kode" @@ -2627,3 +2626,15 @@ _watermarkEditor: image: "Gambar" advanced: "Tingkat lanjut" angle: "Sudut" +_imageEffector: + _fxProps: + angle: "Sudut" + scale: "Ukuran" + size: "Ukuran" + offset: "Posisi" + color: "Warna" + opacity: "Opasitas" + lightness: "Menerangkan" +_qr: + showTabTitle: "Tampilkan" + raw: "Teks" diff --git a/locales/index.d.ts b/locales/index.d.ts index b0a15e0ad1..fbe8c30c94 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1030,6 +1030,10 @@ export interface Locale extends ILocale { * 処理中 */ "processing": string; + /** + * 準備中 + */ + "preprocessing": string; /** * プレビュー */ @@ -1227,7 +1231,7 @@ export interface Locale extends ILocale { */ "noMoreHistory": string; /** - * チャットを始める + * メッセージを送る */ "startChat": string; /** @@ -1927,7 +1931,7 @@ export interface Locale extends ILocale { */ "markAsReadAllUnreadNotes": string; /** - * すべてのチャットを既読にする + * すべてのダイレクトメッセージを既読にする */ "markAsReadAllTalkMessages": string; /** @@ -4234,6 +4238,10 @@ export interface Locale extends ILocale { * プリセットから選択 */ "selectFromPresets": string; + /** + * カスタム + */ + "custom": string; /** * 実績 */ @@ -4386,6 +4394,10 @@ export interface Locale extends ILocale { * ノート検索は利用できません。 */ "notesSearchNotAvailable": string; + /** + * ユーザー検索は利用できません。 + */ + "usersSearchNotAvailable": string; /** * ライセンス */ @@ -4993,7 +5005,7 @@ export interface Locale extends ILocale { /** * メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。 */ - "signupPendingError": string; + "emailVerificationFailedError": string; /** * 「内容を隠す」がオンの場合は注釈の記述が必要です。 */ @@ -5274,6 +5286,10 @@ export interface Locale extends ILocale { * 下書き */ "draft": string; + /** + * 下書きと予約投稿 + */ + "draftsAndScheduledNotes": string; /** * リアクションする際に確認する */ @@ -5382,6 +5398,14 @@ export interface Locale extends ILocale { * チャット */ "chat": string; + /** + * ダイレクトメッセージ + */ + "directMessage": string; + /** + * メッセージ + */ + "directMessage_short": string; /** * 旧設定情報を移行 */ @@ -5493,6 +5517,14 @@ export interface Locale extends ILocale { * 低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。 */ "defaultImageCompressionLevel_description": string; + /** + * デフォルトの圧縮度 + */ + "defaultCompressionLevel": string; + /** + * 低くすると品質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、品質は低下します。 + */ + "defaultCompressionLevel_description": string; /** * 分 */ @@ -5517,6 +5549,64 @@ export interface Locale extends ILocale { * セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。 */ "themeIsDefaultBecauseSafeMode": string; + /** + * ベータ版の検証にご協力いただきありがとうございます! + */ + "thankYouForTestingBeta": string; + /** + * ユーザー指定ノートを作成 + */ + "createUserSpecifiedNote": string; + /** + * 投稿を予約 + */ + "schedulePost": string; + /** + * {x}に投稿を予約します + */ + "scheduleToPostOnX": ParameterizedString<"x">; + /** + * {x}に投稿が予約されています + */ + "scheduledToPostOnX": ParameterizedString<"x">; + /** + * 予約 + */ + "schedule": string; + /** + * 予約 + */ + "scheduled": string; + "_compression": { + "_quality": { + /** + * 高品質 + */ + "high": string; + /** + * 中品質 + */ + "medium": string; + /** + * 低品質 + */ + "low": string; + }; + "_size": { + /** + * サイズ大 + */ + "large": string; + /** + * サイズ中 + */ + "medium": string; + /** + * サイズ小 + */ + "small": string; + }; + }; "_order": { /** * 新しい順 @@ -5528,6 +5618,10 @@ export interface Locale extends ILocale { "oldest": string; }; "_chat": { + /** + * メッセージ + */ + "messages": string; /** * まだメッセージはありません */ @@ -5537,36 +5631,36 @@ export interface Locale extends ILocale { */ "newMessage": string; /** - * 個人チャット + * 個別 */ "individualChat": string; /** - * 特定ユーザーとの一対一のチャットができます。 + * 特定ユーザーと個別にメッセージのやりとりができます。 */ "individualChat_description": string; /** - * ルームチャット + * グループ */ "roomChat": string; /** - * 複数人でのチャットができます。 - * また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。 + * 複数人でメッセージのやりとりができます。 + * また、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。 */ "roomChat_description": string; /** - * ルームを作成 + * グループを作成 */ "createRoom": string; /** - * ユーザーを招待してチャットを始めましょう + * ユーザーを招待してメッセージを送信しましょう */ "inviteUserToChat": string; /** - * 作成したルーム + * 作成したグループ */ "yourRooms": string; /** - * 参加中のルーム + * 参加中のグループ */ "joiningRooms": string; /** @@ -5586,7 +5680,7 @@ export interface Locale extends ILocale { */ "noHistory": string; /** - * ルームはありません + * グループはありません */ "noRooms": string; /** @@ -5606,7 +5700,7 @@ export interface Locale extends ILocale { */ "ignore": string; /** - * ルームから退出 + * グループから退出 */ "leave": string; /** @@ -5630,35 +5724,35 @@ export interface Locale extends ILocale { */ "newline": string; /** - * このルームをミュート + * このグループをミュート */ "muteThisRoom": string; /** - * ルームを削除 + * グループを削除 */ "deleteRoom": string; /** - * このサーバー、またはこのアカウントでチャットは有効化されていません。 + * このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。 */ "chatNotAvailableForThisAccountOrServer": string; /** - * このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。 + * このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。 */ "chatIsReadOnlyForThisAccountOrServer": string; /** - * 相手のアカウントでチャット機能が使えない状態になっています。 + * 相手のアカウントでダイレクトメッセージが使えない状態になっています。 */ "chatNotAvailableInOtherAccount": string; /** - * このユーザーとのチャットを開始できません + * このユーザーとのダイレクトメッセージを開始できません */ "cannotChatWithTheUser": string; /** - * チャットが使えない状態になっているか、相手がチャットを開放していません。 + * ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。 */ "cannotChatWithTheUser_description": string; /** - * あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。 + * あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。 */ "youAreNotAMemberOfThisRoomButInvited": string; /** @@ -5666,31 +5760,31 @@ export interface Locale extends ILocale { */ "doYouAcceptInvitation": string; /** - * チャットする + * ダイレクトメッセージ */ "chatWithThisUser": string; /** - * このユーザーはフォロワーからのみチャットを受け付けています。 + * このユーザーはフォロワーからのみメッセージを受け付けています。 */ "thisUserAllowsChatOnlyFromFollowers": string; /** - * このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。 + * このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。 */ "thisUserAllowsChatOnlyFromFollowing": string; /** - * このユーザーは相互フォローのユーザーからのみチャットを受け付けています。 + * このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。 */ "thisUserAllowsChatOnlyFromMutualFollowing": string; /** - * このユーザーは誰からもチャットを受け付けていません。 + * このユーザーは誰からもメッセージを受け付けていません。 */ "thisUserNotAllowedChatAnyone": string; /** - * チャットを許可する相手 + * メッセージを許可する相手 */ "chatAllowedUsers": string; /** - * 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。 + * 自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。 */ "chatAllowedUsers_note": string; "_chatAllowedUsers": { @@ -6519,7 +6613,7 @@ export interface Locale extends ILocale { */ "remoteNotesCleaning": string; /** - * 有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。 + * 有効にすると、一定期間経過したリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。 */ "remoteNotesCleaning_description": string; /** @@ -6610,6 +6704,18 @@ export interface Locale extends ILocale { * 現在の一部の設定はリセットされます。 */ "restartServerSetupWizardConfirm_text": string; + /** + * エントランスページのスタイル + */ + "entrancePageStyle": string; + /** + * タイムラインを表示する + */ + "showTimelineForVisitor": string; + /** + * アクティビティを表示する + */ + "showActivitiesForVisitor": string; "_userGeneratedContentsVisibilityForVisitor": { /** * 全て公開 @@ -7799,6 +7905,10 @@ export interface Locale extends ILocale { * ノート検索の利用 */ "canSearchNotes": string; + /** + * ユーザー検索の利用 + */ + "canSearchUsers": string; /** * 翻訳機能の利用 */ @@ -7828,7 +7938,7 @@ export interface Locale extends ILocale { */ "canImportUserLists": string; /** - * チャットを許可 + * ダイレクトメッセージを許可 */ "chatAvailability": string; /** @@ -7847,6 +7957,10 @@ export interface Locale extends ILocale { * サーバーサイドのノートの下書きの作成可能数 */ "noteDraftLimit": string; + /** + * 予約投稿の同時作成可能数 + */ + "scheduledNoteLimit": string; /** * ウォーターマーク機能の使用可否 */ @@ -8678,7 +8792,7 @@ export interface Locale extends ILocale { */ "badge": string; /** - * チャットの背景 + * メッセージの背景 */ "messageBg": string; /** @@ -8705,7 +8819,7 @@ export interface Locale extends ILocale { */ "reaction": string; /** - * チャットのメッセージ + * ダイレクトメッセージ */ "chatMessage": string; }; @@ -8828,6 +8942,10 @@ export interface Locale extends ILocale { * 日 */ "day": string; + /** + * ヶ月 + */ + "month": string; }; "_2fa": { /** @@ -8985,11 +9103,11 @@ export interface Locale extends ILocale { */ "write:following": string; /** - * チャットを見る + * ダイレクトメッセージを見る */ "read:messaging": string; /** - * チャットを操作する + * ダイレクトメッセージを操作する */ "write:messaging": string; /** @@ -9281,11 +9399,11 @@ export interface Locale extends ILocale { */ "write:report-abuse": string; /** - * チャットを操作する + * ダイレクトメッセージを操作する */ "write:chat": string; /** - * チャットを閲覧する + * ダイレクトメッセージを閲覧する */ "read:chat": string; }; @@ -9511,7 +9629,7 @@ export interface Locale extends ILocale { */ "birthdayFollowings": string; /** - * チャット + * ダイレクトメッセージ */ "chat": string; }; @@ -10238,6 +10356,14 @@ export interface Locale extends ILocale { * アンケートの結果が出ました */ "pollEnded": string; + /** + * 予約ノートが投稿されました + */ + "scheduledNotePosted": string; + /** + * 予約ノートの投稿に失敗しました + */ + "scheduledNotePostFailed": string; /** * 新しい投稿 */ @@ -10251,7 +10377,7 @@ export interface Locale extends ILocale { */ "roleAssigned": string; /** - * チャットルームへ招待されました + * ダイレクトメッセージのグループへ招待されました */ "chatRoomInvitationReceived": string; /** @@ -10364,7 +10490,7 @@ export interface Locale extends ILocale { */ "roleAssigned": string; /** - * チャットルームへ招待された + * ダイレクトメッセージのグループへ招待された */ "chatRoomInvitationReceived": string; /** @@ -10546,7 +10672,7 @@ export interface Locale extends ILocale { */ "roleTimeline": string; /** - * チャット + * ダイレクトメッセージ */ "chat": string; }; @@ -10913,7 +11039,7 @@ export interface Locale extends ILocale { */ "deleteGalleryPost": string; /** - * チャットルームを削除 + * ダイレクトメッセージのグループを削除 */ "deleteChatRoom": string; /** @@ -12000,11 +12126,11 @@ export interface Locale extends ILocale { */ "youCanConfigureMoreFederationSettingsLater": string; /** - * 受信コンテンツの自動クリーニング + * リモートコンテンツの自動クリーニング */ "remoteContentsCleaning": string; /** - * 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。 + * 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、一定期間経過したリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。 */ "remoteContentsCleaning_description": string; /** @@ -12187,10 +12313,18 @@ export interface Locale extends ILocale { * テキスト */ "text": string; + /** + * 二次元コード + */ + "qr": string; /** * 位置 */ "position": string; + /** + * マージン + */ + "margin": string; /** * タイプ */ @@ -12203,6 +12337,10 @@ export interface Locale extends ILocale { * 高度 */ "advanced": string; + /** + * 角度 + */ + "angle": string; /** * ストライプ */ @@ -12215,10 +12353,6 @@ export interface Locale extends ILocale { * ラインの数 */ "stripeFrequency": string; - /** - * 角度 - */ - "angle": string; /** * ポルカドット */ @@ -12247,6 +12381,10 @@ export interface Locale extends ILocale { * サブドットの数 */ "polkadotSubDotDivisions": string; + /** + * 空欄にするとアカウントのURLになります + */ + "leaveBlankToAccountUrl": string; }; "_imageEffector": { /** @@ -12261,6 +12399,10 @@ export interface Locale extends ILocale { * 変更を破棄して終了しますか? */ "discardChangesConfirm": string; + /** + * 設定項目はありません + */ + "nothingToConfigure": string; "_fxs": { /** * 色収差 @@ -12282,6 +12424,14 @@ export interface Locale extends ILocale { * 白黒 */ "grayscale": string; + /** + * ぼかし + */ + "blur": string; + /** + * モザイク + */ + "pixelate": string; /** * 色調補正 */ @@ -12326,6 +12476,152 @@ export interface Locale extends ILocale { * ティアリング */ "tearing": string; + /** + * 塗りつぶし + */ + "fill": string; + }; + "_fxProps": { + /** + * 角度 + */ + "angle": string; + /** + * サイズ + */ + "scale": string; + /** + * サイズ + */ + "size": string; + /** + * 半径 + */ + "radius": string; + /** + * サンプル数 + */ + "samples": string; + /** + * 位置 + */ + "offset": 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; + /** + * 円形 + */ + "circle": string; }; }; /** @@ -12385,6 +12681,80 @@ export interface Locale extends ILocale { * 下書き一覧 */ "listDrafts": string; + /** + * 投稿予約 + */ + "schedule": string; + /** + * 予約投稿一覧 + */ + "listScheduledNotes": string; + /** + * 予約解除 + */ + "cancelSchedule": string; + }; + /** + * 二次元コード + */ + "qr": string; + "_qr": { + /** + * 表示 + */ + "showTabTitle": string; + /** + * 読み取る + */ + "readTabTitle": string; + /** + * {name} {acct} + */ + "shareTitle": ParameterizedString<"name" | "acct">; + /** + * Fediverseで私をフォローしてください! + */ + "shareText": string; + /** + * カメラを選択 + */ + "chooseCamera": string; + /** + * ライト選択不可 + */ + "cannotToggleFlash": string; + /** + * ライトをオンにする + */ + "turnOnFlash": string; + /** + * ライトをオフにする + */ + "turnOffFlash": string; + /** + * コードリーダーを再開 + */ + "startQr": string; + /** + * コードリーダーを停止 + */ + "stopQr": string; + /** + * QRコードが見つかりません + */ + "noQrCodeFound": string; + /** + * 端末の画像をスキャン + */ + "scanFile": string; + /** + * テキスト + */ + "raw": string; + /** + * MFM + */ + "mfm": string; }; } declare const locales: { diff --git a/locales/it-IT.yml b/locales/it-IT.yml index fb32deec50..8fa481afe8 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -139,7 +139,7 @@ overwriteFromPinnedEmojis: "Sovrascrivi con le impostazioni globali" reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" attachCancel: "Rimuovi allegato" -deleteFile: "File da Drive eliminato" +deleteFile: "Elimina un file dal Drive" markAsSensitive: "Segna come esplicito" unmarkAsSensitive: "Non segnare come esplicito " enterFileName: "Nome del file" @@ -253,6 +253,7 @@ noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?" pinLimitExceeded: "Non puoi fissare altre note " done: "Fine" processing: "In elaborazione" +preprocessing: "In preparazione" preview: "Anteprima" default: "Predefinito" defaultValueIs: "Predefinito: {value}" @@ -577,7 +578,7 @@ 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" +newNote: "Nuove Note" sounds: "Impostazioni suoni" sound: "Suono" notificationSoundSettings: "Preferenze di notifica" @@ -1054,6 +1055,7 @@ permissionDeniedError: "Errore, attività non autorizzata" permissionDeniedErrorDescription: "Non si dispone dell'autorizzazione per eseguire questa operazione." preset: "Preimpostato" selectFromPresets: "Seleziona preimpostato" +custom: "Personalizzato" achievements: "Conquiste" gotInvalidResponseError: "Risposta del server non valida" gotInvalidResponseErrorDescription: "Il server potrebbe essere irraggiungibile o in manutenzione. Riprova più tardi." @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (qu hiddenTags: "Hashtag nascosti" hiddenTagsDescription: "Impedire la visualizzazione del tag impostato nei trend. Puoi impostare più valori, uno per riga." notesSearchNotAvailable: "Non è possibile cercare tra le Note." +usersSearchNotAvailable: "La ricerca profili non è disponibile." license: "Licenza" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" myClips: "Le mie Clip" @@ -1243,7 +1246,7 @@ releaseToRefresh: "Rilascia per aggiornare" refreshing: "Aggiornamento..." pullDownToRefresh: "Trascinare per aggiornare" useGroupedNotifications: "Mostra le notifiche raggruppate" -signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo." +emailVerificationFailedError: "La verifica dell'indirizzo e-mail non è andata a buon fine. Il link potrebbe essere scaduto." cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito." doReaction: "Reagisci" code: "Codice" @@ -1314,13 +1317,14 @@ 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" +draftsAndScheduledNotes: "Bozze e Note pianificate" confirmOnReact: "Confermare le reazioni" reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?" unmarkAsSensitiveConfirm: "Vuoi davvero indicare come non esplicito il contenuto multimediale?" preferences: "Preferenze" accessibility: "Accessibilità" -preferencesProfile: "Profilo preferenze" +preferencesProfile: "Preferenze del profilo" copyPreferenceId: "Copia ID preferenze" resetToDefaultValue: "Ripristina a predefinito" overrideByAccount: "Sovrascrivere col profilo" @@ -1341,6 +1345,8 @@ postForm: "Finestra di pubblicazione" textCount: "Il numero di caratteri" information: "Informazioni" chat: "Chat" +directMessage: "Chatta con questa persona" +directMessage_short: "Messaggio" 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: "Compressione" @@ -1368,16 +1374,35 @@ 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." +defaultCompressionLevel: "Compressione predefinita" +defaultCompressionLevel_description: "Diminuisci per mantenere la qualità aumentando le dimensioni del file.
Aumenta per ridurre le dimensioni del file e anche la qualità." 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." +thankYouForTestingBeta: "Grazie per la tua collaborazione nella verifica delle versioni beta!" +createUserSpecifiedNote: "Creare Nota personalizzata" +schedulePost: "Pianificare la pubblicazione" +scheduleToPostOnX: "Pianificare la pubblicazione {x}" +scheduledToPostOnX: "Pubblicazione pianificata {x}" +schedule: "Pianificare" +scheduled: "Pianificata" +_compression: + _quality: + high: "Alta qualità" + medium: "Media qualità" + low: "Bassa qualità" + _size: + large: "Taglia grande" + medium: "Taglia media" + small: "Taglia piccola" _order: newest: "Prima i più recenti" oldest: "Meno recenti prima" _chat: + messages: "Messaggi" noMessagesYet: "Ancora nessun messaggio" newMessage: "Nuovo messaggio" individualChat: "Chat individuale" @@ -1663,6 +1688,9 @@ _serverSettings: 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." + entrancePageStyle: "Stile della pagina di ingresso" + showTimelineForVisitor: "Mostra la Timeline a visitatori non autenticati" + showActivitiesForVisitor: "Mostrare la propria attività" _userGeneratedContentsVisibilityForVisitor: all: "Tutto pubblico" localOnly: "Pubblica solo contenuti locali, mantieni privati ​​i contenuti remoti" @@ -1999,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." canHideAds: "Nascondere i banner" canSearchNotes: "Ricercare nelle Note" + canSearchUsers: "Può cercare profili" canUseTranslator: "Tradurre le Note" avatarDecorationLimit: "Numero massimo di decorazioni foto profilo installabili" canImportAntennas: "Può importare Antenne" @@ -2011,6 +2040,7 @@ _role: 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" + scheduledNoteLimit: "Quantità di Note pianificabili contemporaneamente" watermarkAvailable: "Disponibilità della funzione filigrana" _condition: roleAssignedTo: "Assegnato a ruoli manualmente" @@ -2215,7 +2245,7 @@ _theme: hashtag: "Hashtag" mention: "Menzioni" mentionMe: "Menzioni (di me)" - renote: "Renota" + renote: "Rinota" modalBg: "Sfondo modale." divider: "Interruzione di linea" scrollbarHandle: "Maniglie della barra di scorrimento" @@ -2271,6 +2301,7 @@ _time: minute: "min" hour: "ore" day: "giorni" + month: "Mese" _2fa: alreadyRegistered: "La configurazione è stata già completata." registerTOTP: "Registra una App di autenticazione a due fattori (2FA/MFA)" @@ -2445,7 +2476,7 @@ _widgets: chooseList: "Seleziona una lista" clicker: "Cliccheria" birthdayFollowings: "Compleanni del giorno" - chat: "Chat" + chat: "Chatta con questa persona" _cw: hide: "Nascondere" show: "Continua la lettura..." @@ -2635,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" pollEnded: "Risultati del sondaggio." + scheduledNotePosted: "Pubblicazione Nota pianificata" + scheduledNotePostFailed: "Impossibile pubblicare la Nota pianificata" newNote: "Nuove Note" unreadAntennaNote: "Antenna {name}" roleAssigned: "Ruolo assegnato" @@ -2655,7 +2688,7 @@ _notification: createToken: "È stato creato un token di accesso" createTokenDescription: "In caso contrario, eliminare il token di accesso tramite ({text})." _types: - all: "Tutto" + all: "Tutte" note: "Nuove Note" follow: "Follower" mention: "Menzioni" @@ -2663,7 +2696,7 @@ _notification: renote: "Rinota" quote: "Cita" reaction: "Reazioni" - pollEnded: "Sondaggio chiuso." + pollEnded: "Sondaggio terminato" receiveFollowRequest: "Richieste di follow in arrivo" followRequestAccepted: "Richieste di follow accettate" roleAssigned: "Ruolo concesso" @@ -2671,7 +2704,7 @@ _notification: achievementEarned: "Risultato raggiunto" exportCompleted: "Esportazione completata" login: "Accessi" - createToken: "Creare un token di accesso" + createToken: "Aggiunto un token di accesso" test: "Notifiche di test" app: "Notifiche da applicazioni" _actions: @@ -2679,7 +2712,7 @@ _notification: reply: "Rispondi" renote: "Rinota" _deck: - alwaysShowMainColumn: "Mostra sempre la colonna principale" + alwaysShowMainColumn: "Mostrare sempre la colonna Principale" columnAlign: "Allineamento delle colonne" columnGap: "Spessore del margine tra colonne" deckMenuPosition: "Posizione del menu Deck" @@ -2696,8 +2729,8 @@ _deck: profile: "Profilo" newProfile: "Nuovo profilo" deleteProfile: "Cancellare il profilo." - introduction: "Combinate le colonne per creare la vostra interfaccia!" - introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo." + introduction: "Crea la tua interfaccia combinando le colonne!" + introduction2: "Per aggiungere una colonna, cliccare il bottone + (più) visibile al margine dello schermo." widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità" useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice" usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima" @@ -2714,7 +2747,7 @@ _deck: mentions: "Menzioni" direct: "Note Dirette" roleTimeline: "Timeline Ruolo" - chat: "Chat" + chat: "Chatta con questa persona" _dialog: charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})" charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})" @@ -2763,56 +2796,56 @@ _abuseReport: notifiedWebhook: "Webhook da usare" deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?" _moderationLogTypes: - createRole: "Ruolo creato" - deleteRole: "Ruolo eliminato" - updateRole: "Ruolo aggiornato" - assignRole: "Ruolo assegnato" - unassignRole: "Ruolo disassegnato" - suspend: "Sospensione" - unsuspend: "Sospensione rimossa" - addCustomEmoji: "Emoji personalizzata aggiunta" - updateCustomEmoji: "Emoji personalizzata aggiornata" - deleteCustomEmoji: "Emoji personalizzata eliminata" - updateServerSettings: "Impostazioni del server aggiornate" - updateUserNote: "Promemoria di moderazione aggiornato" - deleteDriveFile: "File da Drive eliminato" - deleteNote: "Nota eliminata" - createGlobalAnnouncement: "Annuncio globale creato" - createUserAnnouncement: "Annuncio ai profili iscritti creato" - updateGlobalAnnouncement: "Annuncio globale aggiornato" - updateUserAnnouncement: "Annuncio ai profili iscritti aggiornato" - deleteGlobalAnnouncement: "Annuncio globale eliminato" - deleteUserAnnouncement: "Annuncio ai profili iscritti eliminato" - resetPassword: "Password azzerata" - suspendRemoteInstance: "Istanza remota sospesa" - unsuspendRemoteInstance: "Istanza remota riattivata" - updateRemoteInstanceNote: "Aggiornamento del promemoria di moderazione per il server remoto" - markSensitiveDriveFile: "File nel Drive segnato come esplicito" - unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" - resolveAbuseReport: "Segnalazione risolta" - forwardAbuseReport: "Segnalazione inoltrata" - updateAbuseReportNote: "Ha aggiornato la segnalazione" - createInvitation: "Genera codice di invito" - createAd: "Banner creato" - deleteAd: "Banner eliminato" - updateAd: "Banner aggiornato" - createAvatarDecoration: "Creazione decorazione della foto profilo" - updateAvatarDecoration: "Aggiornamento decorazione foto profilo" - deleteAvatarDecoration: "Eliminazione decorazione della foto profilo" - unsetUserAvatar: "Rimossa foto profilo" - unsetUserBanner: "Rimossa intestazione profilo" - createSystemWebhook: "Crea un SystemWebhook" - updateSystemWebhook: "Modifica SystemWebhook" - deleteSystemWebhook: "Elimina SystemWebhook" + createRole: "Crea un Ruolo" + deleteRole: "Elimina un Ruolo" + updateRole: "Modifica un ruolo" + assignRole: "Assegna un Ruolo" + unassignRole: "Toglie un Ruolo al Profilo" + suspend: "Sospende" + unsuspend: "Solleva la sospensione" + addCustomEmoji: "Aggiunge Emoji personalizzata" + updateCustomEmoji: "Modifica Emoji personalizzata" + deleteCustomEmoji: "Elimina Emoji personalizzata" + updateServerSettings: "Modifica le impostazioni del server" + updateUserNote: "Modifica un promemoria di moderazione" + deleteDriveFile: "Elimina un file dal Drive" + deleteNote: "Elimina una Nota" + createGlobalAnnouncement: "Crea un annuncio globale" + createUserAnnouncement: "Crea un annuncio ai profili già iscritti" + updateGlobalAnnouncement: "Modifica un annuncio globale" + updateUserAnnouncement: "Modifica un annuncio ai profili già iscritti" + deleteGlobalAnnouncement: "Elimina un annuncio globale" + deleteUserAnnouncement: "Elimina un annuncio ai profili già iscritti" + resetPassword: "Azzera la password" + suspendRemoteInstance: "Sospende una istanza remota" + unsuspendRemoteInstance: "Riattiva una istanza remota" + updateRemoteInstanceNote: "Modifica il promemoria di moderazione per il server remoto" + markSensitiveDriveFile: "Aggiunge NSFW a un file nel Drive" + unmarkSensitiveDriveFile: "Toglie NSFW da un file nel Drive" + resolveAbuseReport: "Risolve una segnalazione" + forwardAbuseReport: "Inoltra una segnalazione" + updateAbuseReportNote: "Modifica una segnalazione" + createInvitation: "Genera un codice di invito" + createAd: "Aggiunge un Banner" + deleteAd: "Elimina un Banner" + updateAd: "Modifica un Banner" + createAvatarDecoration: "Crea una decorazione della foto profilo" + updateAvatarDecoration: "Modifica una decorazione della foto profilo" + deleteAvatarDecoration: "Elimina una decorazione della foto profilo" + unsetUserAvatar: "Toglie una foto profilo" + unsetUserBanner: "Toglie una immagine di intestazione profilo" + createSystemWebhook: "Aggiunge un System Webhook" + updateSystemWebhook: "Modifica un System Webhook" + deleteSystemWebhook: "Elimina un System Webhook" createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni" - updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni" - deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni" - deleteAccount: "Quando viene eliminato un profilo" - deletePage: "Pagina eliminata" - deleteFlash: "Play eliminato" - deleteGalleryPost: "Eliminazione pubblicazione nella Galleria" - deleteChatRoom: "Elimina chat" - updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy" + updateAbuseReportNotificationRecipient: "Modifica un destinatario per le notifiche di segnalazioni" + deleteAbuseReportNotificationRecipient: "Elimina un destinatario per le notifiche di segnalazioni" + deleteAccount: "Elimina un profilo" + deletePage: "Elimina una Pagina" + deleteFlash: "Elimina un Play" + deleteGalleryPost: "Elimina pubblicazione nella Galleria" + deleteChatRoom: "Elimina una Chat" + updateProxyAccountDescription: "Aggiorna la descrizione del profilo proxy" _fileViewer: title: "Dettagli del file" type: "Tipo di file" @@ -3160,14 +3193,16 @@ _watermarkEditor: opacity: "Opacità" scale: "Dimensioni" text: "Testo" + qr: "QR Code" position: "Posizione" + margin: "Margine" type: "Tipo" image: "Immagini" advanced: "Avanzato" + angle: "Angolo" stripe: "Strisce" stripeWidth: "Larghezza della linea" stripeFrequency: "Il numero di linee" - angle: "Angolo" polkadot: "A pallini" checker: "revisore" polkadotMainDotOpacity: "Opacità del punto principale" @@ -3175,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacità del punto secondario" polkadotSubDotRadius: "Dimensione del punto secondario" polkadotSubDotDivisions: "Quantità di punti secondari" + leaveBlankToAccountUrl: "Il valore vuoto indica la URL dell'account" _imageEffector: title: "Effetto" addEffect: "Aggiungi effetto" discardChangesConfirm: "Scarta le modifiche ed esci?" + nothingToConfigure: "Nessuna impostazione configurabile." _fxs: chromaticAberration: "Aberrazione cromatica" glitch: "Glitch" mirror: "Specchio" invert: "Inversione colore" grayscale: "Bianco e nero" + blur: "Sfocatura" + pixelate: "Mosaico" colorAdjust: "Correzione Colore" colorClamp: "Compressione del colore" colorClampAdvanced: "Compressione del colore (avanzata)" @@ -3196,7 +3235,44 @@ _imageEffector: checker: "revisore" blockNoise: "Attenua rumore" tearing: "Strappa immagine" -drafts: "Bozza" + fill: "Riempimento" + _fxProps: + angle: "Angolo" + scale: "Dimensioni" + size: "Dimensioni" + radius: "Raggio" + samples: "Quantità di campioni" + offset: "Posizione" + color: "Colore" + opacity: "Opacità" + normalize: "Normalizza" + amount: "Quantità" + lightness: "Chiaro" + contrast: "Contrasto" + hue: "Tinta" + brightness: "Luminosità" + saturation: "Saturazione" + max: "Valore massimo" + min: "Valore minimo" + direction: "Orientamento" + phase: "Fasare" + frequency: "Frequenza" + strength: "Forza" + glitchChannelShift: "Glitch cambio canale" + seed: "Seme" + redComponent: "Rosso composito" + greenComponent: "Verde composito" + blueComponent: "Blu composito" + threshold: "Soglia" + centerX: "Centro orizzontale" + centerY: "Centro verticale" + zoomLinesSmoothing: "Levigatura" + zoomLinesSmoothingDescription: "Non si possono usare insieme la levigatura e la larghezza della linea centrale." + zoomLinesThreshold: "Limite delle linee zoom" + zoomLinesMaskSize: "Ampiezza del diametro" + zoomLinesBlack: "Bande nere" + circle: "Circolare" +drafts: "Bozze" _drafts: select: "Selezionare bozza" cannotCreateDraftAnymore: "Hai superato il numero massimo di bozze ammissibili." @@ -3211,3 +3287,22 @@ _drafts: restoreFromDraft: "Recuperare dalle bozze" restore: "Ripristina" listDrafts: "Elenco bozze" + schedule: "Pianifica pubblicazione" + listScheduledNotes: "Elenca Note pianificate" + cancelSchedule: "Annulla pianificazione" +qr: "QR Code" +_qr: + showTabTitle: "Visualizza" + readTabTitle: "Leggere" + shareTitle: "{name} {acct}" + shareText: "Seguimi nel Fediverso!" + chooseCamera: "Seleziona fotocamera" + cannotToggleFlash: "Flash non controllabile" + turnOnFlash: "Accendi il flash" + turnOffFlash: "Spegni il flash" + startQr: "Inizia lettura QR Code" + stopQr: "Interrompi lettura QR Code" + noQrCodeFound: "Non trovo alcun QR Code" + scanFile: "Scansiona immagine nel dispositivo" + raw: "Testo" + mfm: "MFM" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4562cfe370..d21df5d5e1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?" pinLimitExceeded: "これ以上ピン留めできません" done: "完了" processing: "処理中" +preprocessing: "準備中" preview: "プレビュー" default: "デフォルト" defaultValueIs: "デフォルト: {value}" @@ -302,7 +303,7 @@ uploadNFiles: "{n}個のファイルをアップロード" explore: "みつける" messageRead: "既読" noMoreHistory: "これより過去の履歴はありません" -startChat: "チャットを始める" +startChat: "メッセージを送る" nUsersRead: "{n}人が読みました" agreeTo: "{0}に同意" agree: "同意する" @@ -477,7 +478,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ uploadFolder: "既定アップロード先" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" -markAsReadAllTalkMessages: "すべてのチャットを既読にする" +markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする" help: "ヘルプ" inputMessageHere: "ここにメッセージを入力" close: "閉じる" @@ -1054,6 +1055,7 @@ permissionDeniedError: "操作が拒否されました" permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。" preset: "プリセット" selectFromPresets: "プリセットから選択" +custom: "カスタム" achievements: "実績" gotInvalidResponseError: "サーバーの応答が無効です" gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "スペースで区切るとAND指定になり、 hiddenTags: "非表示ハッシュタグ" hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" +usersSearchNotAvailable: "ユーザー検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" @@ -1243,7 +1246,7 @@ releaseToRefresh: "離してリロード" refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" useGroupedNotifications: "通知をグルーピング" -signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" +emailVerificationFailedError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" doReaction: "リアクションする" code: "コード" @@ -1314,6 +1317,7 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" draft: "下書き" +draftsAndScheduledNotes: "下書きと予約投稿" confirmOnReact: "リアクションする際に確認する" reactAreYouSure: "\" {emoji} \" をリアクションしますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" @@ -1341,6 +1345,8 @@ postForm: "投稿フォーム" textCount: "文字数" information: "情報" chat: "チャット" +directMessage: "ダイレクトメッセージ" +directMessage_short: "メッセージ" migrateOldSettings: "旧設定情報を移行" migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。" compress: "圧縮" @@ -1368,59 +1374,79 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示" hideAllTips: "全ての「ヒントとコツ」を非表示" defaultImageCompressionLevel: "デフォルトの画像圧縮度" defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。" +defaultCompressionLevel: "デフォルトの圧縮度" +defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、品質は低下します。" inMinutes: "分" inDays: "日" safeModeEnabled: "セーフモードが有効です" pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プラグインはすべて無効化されています。" customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。" themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。" +thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!" +createUserSpecifiedNote: "ユーザー指定ノートを作成" +schedulePost: "投稿を予約" +scheduleToPostOnX: "{x}に投稿を予約します" +scheduledToPostOnX: "{x}に投稿が予約されています" +schedule: "予約" +scheduled: "予約" + +_compression: + _quality: + high: "高品質" + medium: "中品質" + low: "低品質" + _size: + large: "サイズ大" + medium: "サイズ中" + small: "サイズ小" _order: newest: "新しい順" oldest: "古い順" _chat: + messages: "メッセージ" noMessagesYet: "まだメッセージはありません" newMessage: "新しいメッセージ" - individualChat: "個人チャット" - individualChat_description: "特定ユーザーとの一対一のチャットができます。" - roomChat: "ルームチャット" - roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。" - createRoom: "ルームを作成" - inviteUserToChat: "ユーザーを招待してチャットを始めましょう" - yourRooms: "作成したルーム" - joiningRooms: "参加中のルーム" + individualChat: "個別" + individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。" + roomChat: "グループ" + roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。" + createRoom: "グループを作成" + inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう" + yourRooms: "作成したグループ" + joiningRooms: "参加中のグループ" invitations: "招待" noInvitations: "招待はありません" history: "履歴" noHistory: "履歴はありません" - noRooms: "ルームはありません" + noRooms: "グループはありません" inviteUser: "ユーザーを招待" sentInvitations: "送信した招待" join: "参加" ignore: "無視" - leave: "ルームから退出" + leave: "グループから退出" members: "メンバー" searchMessages: "メッセージを検索" home: "ホーム" send: "送信" newline: "改行" - muteThisRoom: "このルームをミュート" - deleteRoom: "ルームを削除" - chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。" - chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。" - chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。" - cannotChatWithTheUser: "このユーザーとのチャットを開始できません" - cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" - youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。" + muteThisRoom: "このグループをミュート" + deleteRoom: "グループを削除" + chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。" + chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。" + chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。" + cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません" + cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。" + youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。" doYouAcceptInvitation: "招待を承認しますか?" - chatWithThisUser: "チャットする" - thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。" - thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。" - thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。" - thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。" - chatAllowedUsers: "チャットを許可する相手" - chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。" + chatWithThisUser: "ダイレクトメッセージ" + thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。" + thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。" + thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。" + thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。" + chatAllowedUsers: "メッセージを許可する相手" + chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。" _chatAllowedUsers: everyone: "誰でも" followers: "自分のフォロワーのみ" @@ -1657,7 +1683,7 @@ _serverSettings: fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" remoteNotesCleaning: "リモート投稿の自動クリーニング" - remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。" + remoteNotesCleaning_description: "有効にすると、一定期間経過したリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。" remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間" remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数" inquiryUrl: "問い合わせ先URL" @@ -1680,6 +1706,9 @@ _serverSettings: userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。" restartServerSetupWizardConfirm_title: "サーバーの初期設定ウィザードをやり直しますか?" restartServerSetupWizardConfirm_text: "現在の一部の設定はリセットされます。" + entrancePageStyle: "エントランスページのスタイル" + showTimelineForVisitor: "タイムラインを表示する" + showActivitiesForVisitor: "アクティビティを表示する" _userGeneratedContentsVisibilityForVisitor: all: "全て公開" @@ -2020,6 +2049,7 @@ _role: descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" canHideAds: "広告の非表示" canSearchNotes: "ノート検索の利用" + canSearchUsers: "ユーザー検索の利用" canUseTranslator: "翻訳機能の利用" avatarDecorationLimit: "アイコンデコレーションの最大取付個数" canImportAntennas: "アンテナのインポートを許可" @@ -2027,11 +2057,12 @@ _role: canImportFollowing: "フォローのインポートを許可" canImportMuting: "ミュートのインポートを許可" canImportUserLists: "リストのインポートを許可" - chatAvailability: "チャットを許可" + chatAvailability: "ダイレクトメッセージを許可" uploadableFileTypes: "アップロード可能なファイル種別" uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)" uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。" noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数" + scheduledNoteLimit: "予約投稿の同時作成可能数" watermarkAvailable: "ウォーターマーク機能の使用可否" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" @@ -2274,7 +2305,7 @@ _theme: buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" badge: "バッジ" - messageBg: "チャットの背景" + messageBg: "メッセージの背景" fgHighlighted: "強調された文字" _sfx: @@ -2282,7 +2313,7 @@ _sfx: noteMy: "ノート(自分)" notification: "通知" reaction: "リアクション選択時" - chatMessage: "チャットのメッセージ" + chatMessage: "ダイレクトメッセージ" _soundSettings: driveFile: "ドライブの音声を使用" @@ -2319,6 +2350,7 @@ _time: minute: "分" hour: "時間" day: "日" + month: "ヶ月" _2fa: alreadyRegistered: "既に設定は完了しています。" @@ -2361,8 +2393,8 @@ _permissions: "write:favorites": "お気に入りを操作する" "read:following": "フォローの情報を見る" "write:following": "フォロー・フォロー解除する" - "read:messaging": "チャットを見る" - "write:messaging": "チャットを操作する" + "read:messaging": "ダイレクトメッセージを見る" + "write:messaging": "ダイレクトメッセージを操作する" "read:mutes": "ミュートを見る" "write:mutes": "ミュートを操作する" "write:notes": "ノートを作成・削除する" @@ -2435,8 +2467,8 @@ _permissions: "read:clip-favorite": "クリップのいいねを見る" "read:federation": "連合に関する情報を取得する" "write:report-abuse": "違反を報告する" - "write:chat": "チャットを操作する" - "read:chat": "チャットを閲覧する" + "write:chat": "ダイレクトメッセージを操作する" + "read:chat": "ダイレクトメッセージを閲覧する" _auth: shareAccessTitle: "アプリへのアクセス許可" @@ -2499,7 +2531,7 @@ _widgets: chooseList: "リストを選択" clicker: "クリッカー" birthdayFollowings: "今日誕生日のユーザー" - chat: "チャット" + chat: "ダイレクトメッセージ" _cw: hide: "隠す" @@ -2703,10 +2735,12 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" pollEnded: "アンケートの結果が出ました" + scheduledNotePosted: "予約ノートが投稿されました" + scheduledNotePostFailed: "予約ノートの投稿に失敗しました" newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" roleAssigned: "ロールが付与されました" - chatRoomInvitationReceived: "チャットルームへ招待されました" + chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" testNotification: "通知テスト" @@ -2736,7 +2770,7 @@ _notification: receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" roleAssigned: "ロールが付与された" - chatRoomInvitationReceived: "チャットルームへ招待された" + chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された" achievementEarned: "実績の獲得" exportCompleted: "エクスポートが完了した" login: "ログイン" @@ -2786,7 +2820,7 @@ _deck: mentions: "メンション" direct: "指名" roleTimeline: "ロールタイムライン" - chat: "チャット" + chat: "ダイレクトメッセージ" _dialog: charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" @@ -2889,7 +2923,7 @@ _moderationLogTypes: deletePage: "ページを削除" deleteFlash: "Playを削除" deleteGalleryPost: "ギャラリーの投稿を削除" - deleteChatRoom: "チャットルームを削除" + deleteChatRoom: "ダイレクトメッセージのグループを削除" updateProxyAccountDescription: "プロキシアカウントの説明を更新" _fileViewer: @@ -3208,8 +3242,8 @@ _serverSetupWizard: doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" - remoteContentsCleaning: "受信コンテンツの自動クリーニング" - remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。" + remoteContentsCleaning: "リモートコンテンツの自動クリーニング" + remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、一定期間経過したリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。" adminInfo: "管理者情報" adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" @@ -3263,14 +3297,16 @@ _watermarkEditor: opacity: "不透明度" scale: "サイズ" text: "テキスト" + qr: "二次元コード" position: "位置" + margin: "マージン" type: "タイプ" image: "画像" advanced: "高度" + angle: "角度" stripe: "ストライプ" stripeWidth: "ラインの幅" stripeFrequency: "ラインの数" - angle: "角度" polkadot: "ポルカドット" checker: "チェッカー" polkadotMainDotOpacity: "メインドットの不透明度" @@ -3278,11 +3314,13 @@ _watermarkEditor: polkadotSubDotOpacity: "サブドットの不透明度" polkadotSubDotRadius: "サブドットの大きさ" polkadotSubDotDivisions: "サブドットの数" + leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります" _imageEffector: title: "エフェクト" addEffect: "エフェクトを追加" discardChangesConfirm: "変更を破棄して終了しますか?" + nothingToConfigure: "設定項目はありません" _fxs: chromaticAberration: "色収差" @@ -3290,6 +3328,8 @@ _imageEffector: mirror: "ミラー" invert: "色の反転" grayscale: "白黒" + blur: "ぼかし" + pixelate: "モザイク" colorAdjust: "色調補正" colorClamp: "色の圧縮" colorClampAdvanced: "色の圧縮(高度)" @@ -3301,6 +3341,44 @@ _imageEffector: checker: "チェッカー" blockNoise: "ブロックノイズ" tearing: "ティアリング" + fill: "塗りつぶし" + + _fxProps: + angle: "角度" + scale: "サイズ" + size: "サイズ" + radius: "半径" + samples: "サンプル数" + offset: "位置" + 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: "黒色にする" + circle: "円形" drafts: "下書き" _drafts: @@ -3317,3 +3395,23 @@ _drafts: restoreFromDraft: "下書きから復元" restore: "復元" listDrafts: "下書き一覧" + schedule: "投稿予約" + listScheduledNotes: "予約投稿一覧" + cancelSchedule: "予約解除" + +qr: "二次元コード" +_qr: + showTabTitle: "表示" + readTabTitle: "読み取る" + shareTitle: "{name} {acct}" + shareText: "Fediverseで私をフォローしてください!" + chooseCamera: "カメラを選択" + cannotToggleFlash: "ライト選択不可" + turnOnFlash: "ライトをオンにする" + turnOffFlash: "ライトをオフにする" + startQr: "コードリーダーを再開" + stopQr: "コードリーダーを停止" + noQrCodeFound: "QRコードが見つかりません" + scanFile: "端末の画像をスキャン" + raw: "テキスト" + mfm: "MFM" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 235f11e197..d781a14b85 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1239,7 +1239,6 @@ releaseToRefresh: "離したらリロード" refreshing: "リロードしとる" pullDownToRefresh: "引っ張ってリロードするで" useGroupedNotifications: "通知をグループ分けして出すで" -signupPendingError: "メアド確認してたらなんか変なことなったわ。リンクの期限切れてるかもしれん。" cwNotationRequired: "「内容を隠す」んやったら注釈書かなアカンで。" doReaction: "ツッコむで" code: "コード" @@ -1320,6 +1319,7 @@ preferenceSyncConflictChoiceMerge: "ガッチャンコしよか" preferenceSyncConflictChoiceCancel: "同期の有効化はやめとくわ" postForm: "投稿フォーム" information: "情報" +directMessage: "チャットしよか" migrateOldSettings: "旧設定情報をお引っ越し" migrateOldSettings_description: "通常これは自動で行われるはずなんやけど、なんかの理由で上手く移行できへんかったときは手動で移行処理をポチっとできるで。今の設定情報は上書きされるで。" settingsMigrating: "設定を移行しとるで。ちょっと待っとってな... (後で、設定→その他→旧設定情報を移行 で手動で移行することもできるで)" @@ -1410,7 +1410,7 @@ _accountSettings: makeNotesFollowersOnlyBefore: "昔のノートをフォロワーだけに見てもらう" makeNotesFollowersOnlyBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" makeNotesHiddenBefore: "昔のノートを見れんようにする" - makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" + makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがあんただけ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。" mayNotEffectSomeSituations: "これらの制限は簡易的なものやで。リモートサーバーでの閲覧とかモデレーション時とか、一部のシチュエーションでは適用されへんかもしれん。" notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート" @@ -2136,6 +2136,7 @@ _sfx: noteMy: "ノート(自分)" notification: "通知" reaction: "ツッコミ選んどるとき" + chatMessage: "チャットしよか" _soundSettings: driveFile: "ドライブん中の音使う" driveFileWarn: "ドライブん中のファイル選びや" @@ -2341,6 +2342,7 @@ _widgets: chooseList: "リストを選ぶ" clicker: "クリッカー" birthdayFollowings: "今日誕生日のツレ" + chat: "チャットしよか" _cw: hide: "隠す" show: "続き見して!" @@ -2603,6 +2605,7 @@ _deck: mentions: "あんた宛て" direct: "ダイレクト" roleTimeline: "ロールタイムライン" + chat: "チャットしよか" _dialog: charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}" charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}" @@ -3020,9 +3023,20 @@ _watermarkEditor: angle: "角度" _imageEffector: discardChangesConfirm: "変更をせんで終わるか?" + _fxProps: + angle: "角度" + scale: "大きさ" + size: "大きさ" + offset: "位置" + color: "色" + opacity: "不透明度" + lightness: "明るさ" _drafts: cannotCreateDraftAnymore: "下書きはこれ以上は作れへんな。" cannotCreateDraft: "この内容で下書きは作れへんな。" delete: "下書きをほかす" deleteAreYouSure: "下書きをほかしてもええか?" noDrafts: "下書きはあらへん" +_qr: + showTabTitle: "表示" + raw: "テキスト" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 59fe63be6c..2712586e65 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -852,3 +852,5 @@ _search: searchScopeUser: "사용자 지정" _watermarkEditor: image: "이미지" +_qr: + showTabTitle: "보기" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 4950f78b4d..a29744bad8 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "이 노트를 삭제하시겠습니까?" pinLimitExceeded: "더 이상 고정할 수 없습니다." done: "완료" processing: "처리중" +preprocessing: "준비중" preview: "미리보기" default: "기본값" defaultValueIs: "기본값: {value}" @@ -1054,6 +1055,7 @@ permissionDeniedError: "작업이 거부되었습니다" permissionDeniedErrorDescription: "이 작업을 수행할 권한이 없습니다." preset: "프리셋" selectFromPresets: "프리셋에서 선택" +custom: "커스텀" achievements: "도전 과제" gotInvalidResponseError: "서버의 응답이 올바르지 않습니다" gotInvalidResponseErrorDescription: " 서버가 다운되었거나 점검중일 가능성이 있습니다. 잠시후에 다시 시도해 주십시오." @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, hiddenTags: "숨긴 해시태그" hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다." notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다." +usersSearchNotAvailable: "유저 검색을 이용하실 수 없습니다." license: "라이선스" unfavoriteConfirm: "즐겨찾기를 해제하시겠습니까?" myClips: "내 클립" @@ -1243,7 +1246,7 @@ releaseToRefresh: "놓아서 새로고침" refreshing: "새로고침 중" pullDownToRefresh: "아래로 내려서 새로고침" useGroupedNotifications: "알림을 그룹화하고 표시" -signupPendingError: "메일 주소 확인중에 문제가 발생했습니다. 링크의 유효기간이 지났을 가능성이 있습니다." +emailVerificationFailedError: "메일 주소 확인에 실패했습니다. 확인에 필요한 URL의 유효기간이 지났을 가능성이 있습니다." cwNotationRequired: "'내용을 숨기기'를 체크한 경우 주석을 써야 합니다." doReaction: "리액션 추가" code: "문자열" @@ -1314,6 +1317,7 @@ acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했 federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다." federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다." draft: "초안" +draftsAndScheduledNotes: "초안과 예약 게시물" confirmOnReact: "리액션할 때 확인" reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?" markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?" @@ -1341,6 +1345,8 @@ postForm: "글 입력란" textCount: "문자 수" information: "정보" chat: "채팅" +directMessage: "채팅하기" +directMessage_short: "메시지" migrateOldSettings: "기존 설정 정보를 이전" migrateOldSettings_description: "보통은 자동으로 이루어지지만, 어떤 이유로 인해 성공적으로 이전이 이루어지지 않는 경우 수동으로 이전을 실행할 수 있습니다. 현재 설정 정보는 덮어쓰게 됩니다." compress: "압축" @@ -1368,16 +1374,35 @@ redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시" hideAllTips: "모든 '팁과 유용한 정보'를 비표시" defaultImageCompressionLevel: "기본 이미지 압축 정도" defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다.
높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다." +defaultCompressionLevel: "기본 압축 정도 " +defaultCompressionLevel_description: "낮추면 품질을 유지합니다만 파일 크기는 증가합니다.
높이면 파일 크기를 줄일 수 있습니다만 품질은 저하됩니다." inMinutes: "분" inDays: "일" safeModeEnabled: "세이프 모드가 활성화돼있습니다" pluginsAreDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 플러그인은 전부 비활성화됩니다." customCssIsDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 커스텀 CSS는 적용되지 않습니다." themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다." +thankYouForTestingBeta: "베타 버전의 검증에 협력해 주셔서 감사합니다!" +createUserSpecifiedNote: "사용자 지정 노트를 작성" +schedulePost: "게시 예약" +scheduleToPostOnX: "{x}에 게시를 예약합니다." +scheduledToPostOnX: "{x}에 게시가 예약돼있습니다." +schedule: "예약" +scheduled: "예약" +_compression: + _quality: + high: "고품질" + medium: "중간 품질" + low: "저품질" + _size: + large: "대형" + medium: "중형" + small: "소형" _order: newest: "최신 순" oldest: "오래된 순" _chat: + messages: "메시지" noMessagesYet: "아직 메시지가 없습니다" newMessage: "새로운 메시지" individualChat: "개인 대화" @@ -1663,6 +1688,9 @@ _serverSettings: userGeneratedContentsVisibilityForVisitor_description2: "서버에서 받은 리모트 콘텐츠를 포함해 서버 내의 모든 콘텐츠를 무조건 인터넷에 공개하는 것에는 위험이 따릅니다. 특히, 분산형 특성에 대해 모르는 열람자에게는 리모트 콘텐츠여도 서버 내에서 작성된 콘텐츠라고 잘못 인식할 수 있기에 주의가 필요합니다." restartServerSetupWizardConfirm_title: "서버의 초기 설정 위자드를 재시도하시겠습니까?" restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다." + entrancePageStyle: "입구 페이지의 스타일" + showTimelineForVisitor: "타임라인 표시" + showActivitiesForVisitor: "액티비티 표시하기" _userGeneratedContentsVisibilityForVisitor: all: "모두 공개" localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개" @@ -1999,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." canHideAds: "광고 숨기기" canSearchNotes: "노트 검색 이용 가능 여부" + canSearchUsers: "유저 검색 이용" canUseTranslator: "번역 기능의 사용" avatarDecorationLimit: "아바타 장식의 최대 붙임 개수" canImportAntennas: "안테나 가져오기 허용" @@ -2011,6 +2040,7 @@ _role: uploadableFileTypes_caption: "MIME 유형을 " uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오." noteDraftLimit: "서버측 노트 초안 작성 가능 수" + scheduledNoteLimit: "예약 게시물의 동시 작성 가능 수" watermarkAvailable: "워터마크 기능의 사용 여부" _condition: roleAssignedTo: "수동 역할에 이미 할당됨" @@ -2271,6 +2301,7 @@ _time: minute: "분" hour: "시간" day: "일" + month: "개월" _2fa: alreadyRegistered: "이미 설정이 완료되었습니다." registerTOTP: "인증 앱 설정 시작" @@ -2445,7 +2476,7 @@ _widgets: chooseList: "리스트 선택" clicker: "클리커" birthdayFollowings: "오늘이 생일인 유저" - chat: "채팅" + chat: "채팅하기" _cw: hide: "숨기기" show: "더 보기" @@ -2635,6 +2666,8 @@ _notification: youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" pollEnded: "투표 결과가 발표되었습니다" + scheduledNotePosted: "예약 노트가 게시됐습니다." + scheduledNotePostFailed: "예약 노트의 게시에 실패했습니다." newNote: "새 게시물" unreadAntennaNote: "안테나 {name}" roleAssigned: "역할이 부여 되었습니다." @@ -2714,7 +2747,7 @@ _deck: mentions: "받은 멘션" direct: "다이렉트" roleTimeline: "역할 타임라인" - chat: "채팅" + chat: "채팅하기" _dialog: charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {max}" charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}" @@ -3160,14 +3193,16 @@ _watermarkEditor: opacity: "불투명도" scale: "크기" text: "텍스트" + qr: "QR 코드" position: "위치" + margin: "여백" type: "종류" image: "이미지" advanced: "고급" + angle: "각도" stripe: "줄무늬" stripeWidth: "라인의 폭" stripeFrequency: "라인의 수" - angle: "각도" polkadot: "물방울 무늬" checker: "체크 무늬" polkadotMainDotOpacity: "주요 물방울의 불투명도" @@ -3175,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "서브 물방울의 불투명도" polkadotSubDotRadius: "서브 물방울의 크기" polkadotSubDotDivisions: "서브 물방울의 수" + leaveBlankToAccountUrl: "빈칸일 경우 계정의 URL로 됩니다." _imageEffector: title: "이펙트" addEffect: "이펙트를 추가" discardChangesConfirm: "변경을 취소하고 종료하시겠습니까?" + nothingToConfigure: "설정 항목이 없습니다." _fxs: chromaticAberration: "색수차" glitch: "글리치" mirror: "미러" invert: "색 반전" grayscale: "흑백" + blur: "흐림 효과" + pixelate: "모자이크" colorAdjust: "색조 보정" colorClamp: "색 압축" colorClampAdvanced: "색 압축(고급)" @@ -3196,6 +3235,43 @@ _imageEffector: checker: "체크 무늬" blockNoise: "노이즈 방지" tearing: "티어링" + fill: "채우기" + _fxProps: + angle: "각도" + scale: "크기" + size: "크기" + radius: "반지름" + samples: "샘플 수" + offset: "위치" + 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: "검은색으로 하기" + circle: "원형" drafts: "초안" _drafts: select: "초안 선택" @@ -3211,3 +3287,22 @@ _drafts: restoreFromDraft: "초안에서 복원\n" restore: "복원" listDrafts: "초안 목록" + schedule: "게시 예약" + listScheduledNotes: "예약 게시물 목록" + cancelSchedule: "예약 해제" +qr: "QR 코드" +_qr: + showTabTitle: "보기" + readTabTitle: "읽어들이기" + shareTitle: "{name} {acct}" + shareText: "Fediverse로 저를 팔로우해 주세요!" + chooseCamera: "카메라 선택" + cannotToggleFlash: "플래시 선택 불가" + turnOnFlash: "플래시 켜기" + turnOffFlash: "플래시 끄기" + startQr: "코드 리더 재개" + stopQr: "코드 리더 정지" + noQrCodeFound: "QR 코드를 찾을 수 없습니다." + scanFile: "단말기의 이미지 스캔" + raw: "텍스트" + mfm: "MFM" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index e4efbc7e39..87a805429e 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -1083,3 +1083,5 @@ _search: _watermarkEditor: image: "Afbeeldingen" advanced: "Geavanceerd" +_qr: + showTabTitle: "Weergave" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index a38237208a..2df475bffd 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -742,3 +742,10 @@ _watermarkEditor: text: "Tekst" type: "Type" image: "Bilder" +_imageEffector: + _fxProps: + scale: "Størrelse" + size: "Størrelse" + color: "Farge" +_qr: + raw: "Tekst" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 1edd803972..40f7aad9fa 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1593,3 +1593,13 @@ _watermarkEditor: type: "Typ" image: "Zdjęcia" advanced: "Zaawansowane" +_imageEffector: + _fxProps: + scale: "Rozmiar" + size: "Rozmiar" + color: "Kolor" + opacity: "Przezroczystość" + lightness: "Rozjaśnij" +_qr: + showTabTitle: "Wyświetlanie" + raw: "Tekst" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 5fdf4f8258..0c365068e6 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "Deseja excluir esta nota?" pinLimitExceeded: "Não é possível fixar novas notas" done: "Concluído" processing: "Em Progresso" +preprocessing: "Preparando..." preview: "Pré-visualizar" default: "Predefinição" defaultValueIs: "Predefinição: {value}" @@ -1054,6 +1055,7 @@ permissionDeniedError: "Operação recusada" permissionDeniedErrorDescription: "Esta conta não tem permissão para executar esta ação." preset: "Predefinições" selectFromPresets: "Escolher de predefinições" +custom: "Personalizado" achievements: "Conquistas" gotInvalidResponseError: "Resposta do servidor inválida" gotInvalidResponseErrorDescription: "Servidor fora do ar ou em manutenção. Favor tentar mais tarde." @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas hiddenTags: "Hashtags escondidas" hiddenTagsDescription: "Selecione tags que não serão exibidas na lista de destaques. Várias tags podem ser escolhidas, separadas por linha." notesSearchNotAvailable: "A pesquisa de notas está indisponível." +usersSearchNotAvailable: "Pesquisa de usuário está indisponível." license: "Licença" unfavoriteConfirm: "Deseja realmente remover dos favoritos?" myClips: "Meus clipes" @@ -1243,7 +1246,7 @@ releaseToRefresh: "Solte para atualizar" refreshing: "Atualizando..." pullDownToRefresh: "Puxe para baixo para atualizar" useGroupedNotifications: "Agrupar notificações" -signupPendingError: "Houve um problema ao verificar o endereço de email. O link pode ter expirado." +emailVerificationFailedError: "Houve um problema ao verificar seu endereço de email. O link pode ter expirado." cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada." doReaction: "Adicionar reação" code: "Código" @@ -1314,6 +1317,7 @@ 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" +draftsAndScheduledNotes: "Rascunhos e notas agendadas." confirmOnReact: "Confirmar ao reagir" reactAreYouSure: "Você deseja adicionar uma reação \"{emoji}\"?" markAsSensitiveConfirm: "Você deseja definir essa mídia como sensível?" @@ -1341,6 +1345,8 @@ postForm: "Campo de postagem" textCount: "Contagem de caracteres" information: "Sobre" chat: "Conversas" +directMessage: "Conversar com usuário" +directMessage_short: "Mensagem" 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." compress: "Comprimir" @@ -1368,12 +1374,35 @@ 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." +defaultCompressionLevel: "Nível padrão de compressão" +defaultCompressionLevel_description: "Menor compressão preserva a qualidade mas aumenta o tamanho do arquivo.
Maior compressão reduz o tamanho do arquivo mas diminui a qualidade." inMinutes: "Minuto(s)" inDays: "Dia(s)" +safeModeEnabled: "Modo seguro está habilitado" +pluginsAreDisabledBecauseSafeMode: "Todos os plugins estão desabilitados porque o modo seguro está habilitado." +customCssIsDisabledBecauseSafeMode: "CSS personalizado não está aplicado porque o modo seguro está habilitado." +themeIsDefaultBecauseSafeMode: "Enquanto o modo seguro estiver ativo, o tema padrão é utilizado. Desabilitar o modo seguro reverterá essas mudanças." +thankYouForTestingBeta: "Obrigado por nos ajudar a testar a versão beta!" +createUserSpecifiedNote: "Criar uma nota direta" +schedulePost: "Agendar publicação" +scheduleToPostOnX: "Agendar nota para {x}" +scheduledToPostOnX: "A nota está agendada para {x}" +schedule: "Agendar" +scheduled: "Agendado" +_compression: + _quality: + high: "Qualidade alta" + medium: "Qualidade média" + low: "Qualidade baixa" + _size: + large: "Tamanho grande" + medium: "Tamanho médio" + small: "Tamanho pequeno" _order: newest: "Priorizar Mais Novos" oldest: "Priorizar Mais Antigos" _chat: + messages: "Mensagem" noMessagesYet: "Ainda não há mensagens" newMessage: "Nova mensagem" individualChat: "Conversa Particular" @@ -1461,6 +1490,7 @@ _settings: 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." + showPageTabBarBottom: "Mostrar barra de aba da página inferiormente" _chat: showSenderName: "Exibir nome de usuário do remetente" sendOnEnter: "Pressionar Enter para enviar" @@ -1634,6 +1664,10 @@ _serverSettings: fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados" fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas." reactionsBufferingDescription: "Quando ativado, o desempenho durante a criação de uma reação será melhorado substancialmente, reduzindo a carga do banco de dados. Porém, a o uso de memória do Redis irá aumentar." + remoteNotesCleaning: "Limpeza automática de notas remotas" + remoteNotesCleaning_description: "Quando habilitado, notas remotas obsoletas e não utilizadas serão periodicamente limpadas para previnir sobrecarga no banco de dados." + remoteNotesCleaningMaxProcessingDuration: "Maximizar tempo de processamento da limpeza" + remoteNotesCleaningExpiryDaysForEachNotes: "Mínimo de dias para retenção de notas" inquiryUrl: "URL de inquérito" inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato." openRegistration: "Abrir a criação de contas" @@ -1652,6 +1686,11 @@ _serverSettings: 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." + restartServerSetupWizardConfirm_title: "Reiniciar o assistente de configuração?" + restartServerSetupWizardConfirm_text: "Algumas configurações atuais serão reiniciadas." + entrancePageStyle: "Estilo da página de entrada" + showTimelineForVisitor: "Mostrar linha do tempo" + showActivitiesForVisitor: "Mostrar atividades" _userGeneratedContentsVisibilityForVisitor: all: "Tudo é público" localOnly: "Conteúdo local é publicado, conteúdo remoto é privado" @@ -1988,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos." canHideAds: "Permitir ocultar anúncios" canSearchNotes: "Permitir a busca de notas" + canSearchUsers: "Busca de usuário" canUseTranslator: "Uso do tradutor" avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas" canImportAntennas: "Permitir importação de antenas" @@ -2000,6 +2040,7 @@ _role: 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" + scheduledNoteLimit: "Número máximo de notas agendadas simultâneas" watermarkAvailable: "Disponibilidade da função de marca d'água" _condition: roleAssignedTo: "Atribuído a cargos manuais" @@ -2260,6 +2301,7 @@ _time: minute: "Minuto(s)" hour: "Hora(s)" day: "Dia(s)" + month: "Mês(es)" _2fa: alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores." registerTOTP: "Cadastrar aplicativo autenticador" @@ -2434,7 +2476,7 @@ _widgets: chooseList: "Selecione uma lista" clicker: "Clicker" birthdayFollowings: "Usuários de aniversário hoje" - chat: "Conversas" + chat: "Conversar com usuário" _cw: hide: "Esconder" show: "Carregar mais" @@ -2624,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Você recebeu um pedido de seguidor" yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito" pollEnded: "Os resultados da enquete agora estão disponíveis" + scheduledNotePosted: "Nota agendada foi publicada" + scheduledNotePostFailed: "Não foi possível publicar nota agendada" newNote: "Nova nota" unreadAntennaNote: "Antena {name}" roleAssigned: "Cargo dado" @@ -2703,7 +2747,7 @@ _deck: mentions: "Menções" direct: "Notas diretas" roleTimeline: "Linha do tempo do cargo" - chat: "Conversas" + chat: "Conversar com usuário" _dialog: charactersExceeded: "Você excedeu o limite de caracteres! Atualmente em {current} de {max}." charactersBelow: "Você está abaixo do limite mínimo de caracteres! Atualmente em {current} of {min}." @@ -3062,6 +3106,7 @@ _bootErrors: otherOption1: "Excluir ajustes de cliente e cache" otherOption2: "Iniciar o cliente simples" otherOption3: "Iniciar ferramenta de reparo" + otherOption4: "Abrir Misskey no modo seguro" _search: searchScopeAll: "Todos" searchScopeLocal: "Local" @@ -3098,6 +3143,8 @@ _serverSetupWizard: 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" + remoteContentsCleaning: "Limpeza automática de conteúdos recebidos" + remoteContentsCleaning_description: "A federação pode resultar em uma entrada contínua de conteúdo. Habilitar a limpeza automática removerá conteúdo obsoleto e não referenciado do servidor para economizar armazenamento." 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." @@ -3146,14 +3193,16 @@ _watermarkEditor: opacity: "Opacidade" scale: "Tamanho" text: "Texto" + qr: "Código QR" position: "Posição" + margin: "Margem" type: "Tipo" image: "imagem" advanced: "Avançado" + angle: "Ângulo" stripe: "Listras" stripeWidth: "Largura da linha" stripeFrequency: "Número de linhas" - angle: "Ângulo" polkadot: "Bolinhas" checker: "Xadrez" polkadotMainDotOpacity: "Opacidade da bolinha principal" @@ -3161,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacidade da bolinha secundária" polkadotSubDotRadius: "Raio das bolinhas adicionais" polkadotSubDotDivisions: "Número de bolinhas adicionais" + leaveBlankToAccountUrl: "Deixe em branco para utilizar URL da conta" _imageEffector: title: "Efeitos" addEffect: "Adicionar efeitos" discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas." + nothingToConfigure: "Não há nada para configurar" _fxs: chromaticAberration: "Aberração cromática" glitch: "Glitch" mirror: "Espelho" invert: "Inverter Cores" grayscale: "Tons de Cinza" + blur: "Desfoque" + pixelate: "Pixelizar" colorAdjust: "Correção de Cores" colorClamp: "Compressão de Cores" colorClampAdvanced: "Compressão Avançada de Cores" @@ -3182,6 +3235,43 @@ _imageEffector: checker: "Xadrez" blockNoise: "Bloquear Ruído" tearing: "Descontinuidade" + fill: "Preencher" + _fxProps: + angle: "Ângulo" + scale: "Tamanho" + size: "Tamanho" + radius: "Raio" + samples: "Número de amostras" + offset: "Posição" + color: "Cor" + opacity: "Opacidade" + normalize: "Normalizar" + amount: "Quantidade" + lightness: "Esclarecer" + contrast: "Contraste" + hue: "Matiz" + brightness: "Brilho" + saturation: "Saturação" + max: "Máximo" + min: "Mínimo" + direction: "Direção" + phase: "Fase" + frequency: "Frequência" + strength: "Força" + glitchChannelShift: "Mudança de canal" + seed: "Valor da semente" + redComponent: "Componente vermelho" + greenComponent: "Componente verde" + blueComponent: "Componente azul" + threshold: "Limiar" + centerX: "Centralizar X" + centerY: "Centralizar Y" + zoomLinesSmoothing: "Suavização" + zoomLinesSmoothingDescription: "Suavização e largura das linhas de zoom não podem ser utilizados simultaneamente." + zoomLinesThreshold: "Largura das linhas de zoom" + zoomLinesMaskSize: "Diâmetro do centro" + zoomLinesBlack: "Linhas pretas" + circle: "Circular" drafts: "Rascunhos" _drafts: select: "Selecionar Rascunho" @@ -3197,3 +3287,22 @@ _drafts: restoreFromDraft: "Restaurar de Rascunho" restore: "Redefinir" listDrafts: "Lista de Rascunhos" + schedule: "Agendar nota" + listScheduledNotes: "Lista de notas agendadas" + cancelSchedule: "Cancelar agendamento" +qr: "Código QR" +_qr: + showTabTitle: "Visualizar" + readTabTitle: "Escanear" + shareTitle: "{name} {acct}" + shareText: "Siga-me no Fediverso!" + chooseCamera: "Escolher câmera" + cannotToggleFlash: "Não foi possível ligar a lanterna" + turnOnFlash: "Ligar a lanterna" + turnOffFlash: "Desligar a lanterna" + startQr: "Retornar ao leitor de códigos QR" + stopQr: "Deixar o leitor de códigos QR" + noQrCodeFound: "Nenhum código QR encontrado" + scanFile: "Escanear imagem de dispositivo" + raw: "Texto" + mfm: "MFM" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index f07e4d8d2f..cc37b531f5 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -1400,3 +1400,11 @@ _watermarkEditor: type: "Tip" image: "Imagini" advanced: "Avansat" +_imageEffector: + _fxProps: + scale: "Dimensiune" + size: "Dimensiune" + offset: "Poziție" +_qr: + showTabTitle: "Arată" + raw: "Text" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 375b46c3e9..27fc425e3d 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1215,12 +1215,12 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности" avatarDecorations: "Украшения для аватара" attach: "Прикрепить" +detachAll: "Убрать всё" angle: "Угол" flip: "Переворот" showAvatarDecorations: "Показать украшения для аватара" pullDownToRefresh: "Опустите что бы обновить" useGroupedNotifications: "Отображать уведомления сгруппировано" -signupPendingError: "Возникла проблема с подтверждением вашего адреса электронной почты. Возможно, срок действия ссылки истёк." cwNotationRequired: "Если включена опция «Скрыть содержимое», необходимо написать аннотацию." doReaction: "Добавить реакцию" code: "Код" @@ -1254,7 +1254,7 @@ clipNoteLimitExceeded: "К этому клипу больше нельзя до performance: "Производительность" modified: "Изменено" signinWithPasskey: "Войдите в систему, используя свой пароль" -unknownWebAuthnKey: "Не известный ключ " +unknownWebAuthnKey: "Неизвестный ключ" passkeyVerificationFailed: "Ошибка проверка ключа доступа " messageToFollower: "Сообщение подписчикам" testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. Не использовать это в рабочей среде" @@ -1269,8 +1269,11 @@ availableRoles: "Доступные роли" federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах." draft: "Черновик" markAsSensitiveConfirm: "Отметить контент как чувствительный?" +preferences: "Основное" resetToDefaultValue: "Сбросить настройки до стандартных" +syncBetweenDevices: "Синхронизировать между устройствами" postForm: "Форма отправки" +textCount: "Количество символов" information: "Описание" inMinutes: "мин" inDays: "сут" @@ -1282,6 +1285,11 @@ _chat: send: "Отправить" _settings: webhook: "Вебхук" + preferencesBanner: "Вы можете настроить общее поведение клиента по вашим предпочтениям" + timelineAndNote: "Лента и заметки" + _chat: + showSenderName: "Показывать имя отправителя" + sendOnEnter: "Использовать Enter для отправки" _delivery: stop: "Заморожено" _type: @@ -1530,7 +1538,7 @@ _achievements: description: "Нажато здесь" _justPlainLucky: title: "Чистая удача" - description: "Может достаться с вероятностью 0,01% каждые 10 секунд." + description: "Может достаться с вероятностью 0,005% каждые 10 секунд." _setNameToSyuilo: title: "Комплекс бога" description: "Установлено «syuilo» в качестве имени" @@ -1558,6 +1566,12 @@ _achievements: title: "Brain Diver" description: "Опубликована ссылка на песню «Brain Diver»" flavor: "Мисски-Мисски Ла-Ту-Ма" + _bubbleGameExplodingHead: + title: "🤯" + description: "Самый большой объект в Bubble game" + _bubbleGameDoubleExplodingHead: + title: "Двойной🤯" + description: "Два самых больших объекта в Bubble game одновременно!" _role: new: "Новая роль" edit: "Изменить роль" @@ -2257,4 +2271,16 @@ _watermarkEditor: image: "Изображения" advanced: "Для продвинутых" angle: "Угол" +_imageEffector: + _fxProps: + angle: "Угол" + scale: "Размер" + size: "Размер" + offset: "Позиция" + color: "Цвет" + opacity: "Непрозрачность" + lightness: "Осветление" drafts: "Черновик" +_qr: + showTabTitle: "Отображение" + raw: "Текст" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 80a1f2f0a9..2b86826703 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1459,3 +1459,13 @@ _watermarkEditor: type: "Typ" image: "Obrázky" advanced: "Rozšírené" +_imageEffector: + _fxProps: + scale: "Veľkosť" + size: "Veľkosť" + color: "Farba" + opacity: "Priehľadnosť" + lightness: "Zosvetliť" +_qr: + showTabTitle: "Zobraziť" + raw: "Text" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 95947607cb..96944706e9 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -716,3 +716,8 @@ _search: _watermarkEditor: scale: "Storlek" image: "Bilder" +_imageEffector: + _fxProps: + scale: "Storlek" + size: "Storlek" + color: "Färg" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 519d10daa6..b945decbab 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "ต้องการลบโน้ตนี้ใช่ไ pinLimitExceeded: "คุณไม่สามารถปักหมุดโน้ตเพิ่มเติมใดๆได้อีก" done: "เสร็จสิ้น" processing: "กำลังประมวลผล..." +preprocessing: "กำลังจัดเตรียม..." preview: "แสดงตัวอย่าง" default: "ค่าเริ่มต้น" defaultValueIs: "ค่าเริ่มต้น: {value}" @@ -1054,6 +1055,7 @@ permissionDeniedError: "การดำเนินถูกปฏิเสธ" permissionDeniedErrorDescription: "บัญชีนี้ไม่มีสิทธิ์อนุญาตในการดำเนินการนี้" preset: "พรีเซ็ต" selectFromPresets: "เลือกจากการพรีเซ็ต" +custom: "แบบกำหนดเอง" achievements: "ความสำเร็จ" gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง" gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ" @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "ถ้าแยกด้วยเว้นวร hiddenTags: "แฮชแท็กที่ซ่อนอยู่" hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่" notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน" +usersSearchNotAvailable: "การค้นหาผู้ใช้ไม่พร้อมใช้งาน" license: "ใบอนุญาต" unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?" myClips: "คลิปของฉัน" @@ -1243,7 +1246,7 @@ releaseToRefresh: "ปล่อยเพื่อรีเฟรช" refreshing: "กำลังรีเฟรช..." pullDownToRefresh: "ดึงลงเพื่อรีเฟรช" useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว" -signupPendingError: "มีปัญหาในการตรวจสอบที่อยู่อีเมลลิงก์อาจหมดอายุแล้ว" +emailVerificationFailedError: "เกิดปัญหาในขณะตรวจสอบอีเมล อาจเป็นไปได้ว่าลิงก์หมดอายุแล้ว" cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย" doReaction: "เพิ่มรีแอคชั่น" code: "โค้ด" @@ -1314,6 +1317,7 @@ acknowledgeNotesAndEnable: "เปิดใช้งานหลังจาก federationSpecified: "เซิร์ฟเวอร์นี้ดำเนินงานในระบบกลุ่มไวท์ลิสต์ ไม่สามารถติดต่อกับเซิร์ฟเวอร์อื่นที่ไม่ได้รับอนุญาตจากผู้ดูแลระบบได้" federationDisabled: "เซิร์ฟเวอร์นี้ปิดใช้งานสหพันธ์ ไม่สามารถติดต่อหรือแลกเปลี่ยนข้อมูลกับผู้ใช้จากเซิร์ฟเวอร์อื่นได้" draft: "ร่าง" +draftsAndScheduledNotes: "ร่างและกำหนดเวลาโพสต์" confirmOnReact: "ยืนยันเมื่อทำการรีแอคชั่น" reactAreYouSure: "ต้องการใส่รีแอคชั่นด้วย \"{emoji}\" หรือไม่?" markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อนี้ว่าเป็นเนื้อหาละเอียดอ่อนหรือไม่?" @@ -1341,6 +1345,8 @@ postForm: "แบบฟอร์มการโพสต์" textCount: "จำนวนอักขระ" information: "เกี่ยวกับ" chat: "แชต" +directMessage: "แชตเลย" +directMessage_short: "ข้อความ" migrateOldSettings: "ย้ายข้อมูลการตั้งค่าเก่า" migrateOldSettings_description: "โดยปกติจะทำโดยอัตโนมัติ แต่หากด้วยเหตุผลบางประการที่ไม่สามารถย้ายได้สำเร็จ สามารถสั่งย้ายด้วยตนเองได้ การตั้งค่าปัจจุบันจะถูกเขียนทับ" compress: "บีบอัด" @@ -1366,14 +1372,37 @@ abort: "หยุดและยกเลิก" tip: "คำแนะนำและเคล็ดลับ" redisplayAllTips: "แสดงคำแนะนำและเคล็ดลับทั้งหมดอีกครั้ง" hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด" -defaultImageCompressionLevel: "ความละเอียดเริ่มต้นสำหรับการบีบอัดภาพ" +defaultImageCompressionLevel: "ค่าการบีบอัดภาพเริ่มต้น" defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น
หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง" +defaultCompressionLevel: "ค่าการบีบอัดเริ่มต้น" +defaultCompressionLevel_description: "ถ้าต่ำ จะรักษาคุณภาพได้ แต่ขนาดไฟล์จะเพิ่มขึ้น
ถ้าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพจะลดลง" inMinutes: "นาที" inDays: "วัน" +safeModeEnabled: "โหมดปลอดภัยถูกเปิดใช้งาน" +pluginsAreDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน ปลั๊กอินทั้งหมดจึงถูกปิดใช้งาน" +customCssIsDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน CSS แบบกำหนดเองจึงไม่ได้ถูกนำมาใช้" +themeIsDefaultBecauseSafeMode: "ในระหว่างที่โหมดปลอดภัยถูกเปิดใช้งาน จะใช้ธีมเริ่มต้น เมื่อปิดโหมดปลอดภัยจะกลับคืนดังเดิม" +thankYouForTestingBeta: "ขอบคุณที่ให้ความร่วมมือในการทดสอบเวอร์ชันเบต้า!" +createUserSpecifiedNote: "สร้างโน้ตแบบไดเร็กต์" +schedulePost: "กำหนดเวลาให้โพสต์" +scheduleToPostOnX: "กำหนดเวลาให้โพสต์ไว้ที่ {x}" +scheduledToPostOnX: "มีการกำหนดเวลาให้โพสต์ไว้ที่ {x}" +schedule: "กำหนดเวลา" +scheduled: "กำหนดเวลา" +_compression: + _quality: + high: "คุณภาพสูง" + medium: "คุณภาพปานกลาง" + low: "คุณภาพต่ำ" + _size: + large: "ขนาดใหญ่" + medium: "ขนาดปานกลาง" + small: "ขนาดเล็ก" _order: newest: "เรียงจากใหม่ไปเก่า" oldest: "เรียงจากเก่าไปใหม่" _chat: + messages: "ข้อความ" noMessagesYet: "ยังไม่มีข้อความ" newMessage: "ข้อความใหม่" individualChat: "แชตส่วนตัว" @@ -1659,6 +1688,9 @@ _serverSettings: userGeneratedContentsVisibilityForVisitor_description2: "การเปิดเผยเนื้อหาทั้งหมดในเซิร์ฟเวอร์รวมทั้งเนื้อหาที่รับมาจากระยะไกลสู่สาธารณะบนอินเทอร์เน็ตโดยไม่มีข้อจำกัดใดๆ มีความเสี่ยงโดยเฉพาะอย่างยิ่งสำหรับผู้ชมที่ไม่เข้าใจลักษณะของระบบแบบกระจาย อาจทำให้เกิดความเข้าใจผิดคิดว่าเนื้อหาที่มาจากระยะไกลนั้นเป็นเนื้อหาที่สร้างขึ้นภายในเซิร์ฟเวอร์นี้ จึงควรใช้ความระมัดระวังอย่างมาก" restartServerSetupWizardConfirm_title: "ต้องการเริ่มวิซาร์ดการตั้งค่าเซิร์ฟเวอร์ใหม่หรือไม่?" restartServerSetupWizardConfirm_text: "การตั้งค่าบางส่วนในปัจจุบันจะถูกรีเซ็ต" + entrancePageStyle: "สไตล์ของหน้าเพจทางเข้า" + showTimelineForVisitor: "แสดงไทม์ไลน์" + showActivitiesForVisitor: "แสดงกิจกรรม" _userGeneratedContentsVisibilityForVisitor: all: "ทั้งหมดสาธารณะ" localOnly: "เผยแพร่เป็นสาธารณะเฉพาะเนื้อหาท้องถิ่น เนื้อหาระยะไกลให้เป็นส่วนตัว" @@ -1995,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "ยิ่งตัวเลขน้อยก็ยิ่งจำกัดน้อย ยิ่งมากก็ยิ่งเข้มงวดมากขึ้น" canHideAds: "ซ่อนโฆษณา" canSearchNotes: "การใช้การค้นหาโน้ต" + canSearchUsers: "ค้นหาผู้ใช้" canUseTranslator: "การใช้งานแปล" avatarDecorationLimit: "จำนวนของตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้" canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ" @@ -2007,6 +2040,7 @@ _role: uploadableFileTypes_caption: "สามารถระบุ MIME type ได้ โดยใช้การขึ้นบรรทัดใหม่เพื่อแยกหลายรายการ และสามารถใช้ดอกจัน (*) เพื่อระบุแบบไวลด์การ์ดได้ (เช่น: image/*)" uploadableFileTypes_caption2: "ไฟล์บางประเภทอาจไม่สามารถระบุชนิดได้ หากต้องการอนุญาตไฟล์ลักษณะนั้น กรุณาเพิ่ม {x} ลงในรายการที่อนุญาต" noteDraftLimit: "จำนวนโน้ตฉบับร่างที่สามารถสร้างได้บนฝั่งเซิร์ฟเวอร์" + scheduledNoteLimit: "จำนวนโพสต์กำหนดเวลาที่สร้างพร้อมกันได้" watermarkAvailable: "มีฟังก์ชั่นลายน้ำให้เลือกใช้" _condition: roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ" @@ -2267,6 +2301,7 @@ _time: minute: "นาที" hour: "ชั่วโมง" day: "วัน" + month: "เดือน" _2fa: alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" @@ -2441,7 +2476,7 @@ _widgets: chooseList: "เลือกรายชื่อ" clicker: "คลิกเกอร์" birthdayFollowings: "วันเกิดผู้ใช้ในวันนี้" - chat: "แชต" + chat: "แชตเลย" _cw: hide: "ซ่อน" show: "โหลดเพิ่มเติม" @@ -2631,6 +2666,8 @@ _notification: youReceivedFollowRequest: "ได้รับคำขอติดตาม" yourFollowRequestAccepted: "คำขอติดตามได้รับการอนุมัติแล้ว" pollEnded: "ผลโพลออกมาแล้ว" + scheduledNotePosted: "โน้ตที่กำหนดเวลาไว้ได้ถูกโพสต์แล้ว" + scheduledNotePostFailed: "ล้มเหลวในการโพสต์โน้ตที่กำหนดเวลาไว้" newNote: "โพสต์ใหม่" unreadAntennaNote: "เสาอากาศ {name}" roleAssigned: "ได้รับบทบาท" @@ -2710,7 +2747,7 @@ _deck: mentions: "กล่าวถึงคุณ" direct: "ไดเร็กต์" roleTimeline: "บทบาทไทม์ไลน์" - chat: "แชต" + chat: "แชตเลย" _dialog: charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}" @@ -3069,6 +3106,7 @@ _bootErrors: otherOption1: "ลบการตั้งค่าและแคชของไคลเอนต์" otherOption2: "เริ่มใช้งานไคลเอนต์แบบง่าย" otherOption3: "เปิดเครื่องมือซ่อมแซม" + otherOption4: "เริ่มทำงาน Misskey ในโหมดปลอดภัย" _search: searchScopeAll: "ทั้งหมด" searchScopeLocal: "ท้องถิ่น" @@ -3155,14 +3193,16 @@ _watermarkEditor: opacity: "ความทึบแสง" scale: "ขนาด" text: "ข้อความ" + qr: "QR โค้ด" position: "ตำแหน่ง" + margin: "ระยะขอบ" type: "รูปแบบ" image: "รูปภาพ" advanced: "ขั้นสูง" + angle: "แองเกิล" stripe: "ริ้ว" stripeWidth: "ความกว้างเส้น" stripeFrequency: "จำนวนเส้น" - angle: "แองเกิล" polkadot: "ลายจุด" checker: "ช่องตาราง" polkadotMainDotOpacity: "ความทึบของจุดหลัก" @@ -3170,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "ความทึบของจุดรอง" polkadotSubDotRadius: "ขนาดของจุดรอง" polkadotSubDotDivisions: "จำนวนจุดรอง" + leaveBlankToAccountUrl: "เว้นว่างไว้หากต้องการใช้ URL ของบัญชีแทน" _imageEffector: title: "เอฟเฟกต์" addEffect: "เพิ่มเอฟเฟกต์" discardChangesConfirm: "ต้องการทิ้งการเปลี่ยนแปลงแล้วออกหรือไม่?" + nothingToConfigure: "ไม่มีอะไรให้ตั้งค่า" _fxs: chromaticAberration: "ความคลาดสี" glitch: "กลิตช์" mirror: "กระจก" invert: "กลับสี" grayscale: "ขาวดำเทา" + blur: "มัว" + pixelate: "โมเสก" colorAdjust: "ปรับแก้สี" colorClamp: "บีบอัดสี" colorClampAdvanced: "บีบอัดสี (ขั้นสูง)" @@ -3191,6 +3235,43 @@ _imageEffector: checker: "ช่องตาราง" blockNoise: "บล็อกที่มีการรบกวน" tearing: "ฉีกขาด" + fill: "เติมเต็ม" + _fxProps: + angle: "แองเกิล" + scale: "ขนาด" + size: "ขนาด" + radius: "รัศสี" + samples: "จำนวนตัวอย่าง" + offset: "ตำแหน่ง" + color: "สี" + opacity: "ความทึบแสง" + normalize: "นอร์มัลไลซ์" + amount: "จำนวน" + lightness: "สว่าง" + contrast: "คอนทราสต์" + hue: "HUE" + brightness: "ความสว่าง" + saturation: "ความอิ่มตัว" + max: "สูงสุด" + min: "ต่ำสุด" + direction: "ทิศทาง" + phase: "ระยะ" + frequency: "ความถี่" + strength: "ความแรง" + glitchChannelShift: "ความเคลื่อน" + seed: "ซีด" + redComponent: "ส่วนสีแดง" + greenComponent: "ส่วนสีเขียว" + blueComponent: "ส่วนสีน้ำเงิน" + threshold: "เทรชโฮลด์" + centerX: "กลาง X" + centerY: "กลาง Y" + zoomLinesSmoothing: "ทำให้สมูธ" + zoomLinesSmoothingDescription: "ตั้งให้สมูธไม่สามารถใช้ร่วมกับตั้งความกว้างเส้นรวมศูนย์ได้" + zoomLinesThreshold: "ความกว้างเส้นรวมศูนย์" + zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง" + zoomLinesBlack: "ทำให้ดำ" + circle: "ทรงกลม" drafts: "ร่าง" _drafts: select: "เลือกฉบับร่าง" @@ -3206,3 +3287,22 @@ _drafts: restoreFromDraft: "คืนค่าจากฉบับร่าง" restore: "กู้คืน" listDrafts: "รายการฉบับร่าง" + schedule: "โพสต์กำหนดเวลา" + listScheduledNotes: "รายการโน้ตที่กำหนดเวลาไว้" + cancelSchedule: "ยกเลิกกำหนดเวลา" +qr: "QR โค้ด" +_qr: + showTabTitle: "แสดงผล" + readTabTitle: "แสกน" + shareTitle: "{name}{acct}" + shareText: "โปรดติดตามฉันบน Fediverse ด้วย!" + chooseCamera: "เลือกกล้อง" + cannotToggleFlash: "ไม่สามารถเลือกแสงแฟลชได้" + turnOnFlash: "ปิดแสงแฟลช" + turnOffFlash: "เปิดแสงแฟลช" + startQr: "เริ่มตัวอ่าน QR โค้ด" + stopQr: "หยุดตัวอ่าน QR โค้ด" + noQrCodeFound: "ไม่พบ QR โค้ด" + scanFile: "สแกนภาพจากอุปกรณ์" + raw: "ข้อความ" + mfm: "MFM" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 8604cf1af6..f73ebafa89 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1,25 +1,25 @@ --- _lang_: "Türkçe" 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! 🚀" +introMisskey: "Hoş geldiniz! Misskey, merkezi olmayan bir açık kaynaklı mikroblog platformudur.\n“Notlar” yazarak şu anda neler olduğunu anlatabilir veya olayları başkalarıyla paylaşabilirsiniz. 📡\n“Tepkiler” ile diğer kullanıcıların notları hakkındaki duygularınızı hızlı bir şekilde ifade edebilirsiniz. 👍\nYeni bir dünya sizi bekliyor! 🚀" 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" +monthAndDay: "{day}.{month}." +search: "Ara" reset: "Sıfırla" notifications: "Bildirimler" username: "Kullanıcı Adı" password: "Şifre" 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." +initialPasswordIsIncorrect: "Kurulum için ilk şifre yanlış" +initialPasswordForSetupDescription: "Misskey'i kendiniz kurduysan, yapılandırma dosyasında belirtilen şifreyi kullan.\nMisskey barındırma hizmeti veya benzeri bir hizmet kullanıyorsan, orada belirtilen şifreyi kullan.\nŞifre belirlemediysen, devam etmek için boş bırak." forgotPassword: "Şifremi unuttum" -fetchingAsApObject: "Fediverse'den getiriliyor..." +fetchingAsApObject: "Fediverse'den talep ediliyor..." ok: "Tamam" gotIt: "Anladım!" -cancel: "İptal" -noThankYou: "Şimdi değil" -enterUsername: "Kullanıcı adını girin" -renotedBy: "{user} tarafından renot edildi" +cancel: "Vazgeç" +noThankYou: "Hayır, teşekkürler." +enterUsername: "Kullanıcı adı gir" +renotedBy: "{user} renote etti" noNotes: "Not yok" noNotifications: "Bildirim yok" instance: "Sunucu" @@ -29,31 +29,31 @@ basicSettings: "Temel Ayarlar" otherSettings: "Diğer Ayarlar" openInWindow: "Pencerede aç" profile: "Profil" -timeline: "Timeline" +timeline: "Pano" noAccountDescription: "Bu kullanıcı henüz biyografisini yazmamış." -login: "Giriş Yap" -loggingIn: "Giriş yapılıyor" +login: "Oturum Aç" +loggingIn: "Giriş Yapılıyor..." logout: "Çıkış Yap" signup: "Kaydol" uploading: "Yükleniyor..." save: "Kaydet" users: "Kullanıcılar" addUser: "Kullanıcı ekle" -favorite: "Favorilere ekle" +favorite: "Favori" favorites: "Favoriler" -unfavorite: "Favorilerden kaldır" -favorited: "Favorilere eklendi." -alreadyFavorited: "Zaten favorilere eklendi" -cantFavorite: "Favorilere ekleyemedim." +unfavorite: "Favoriden kaldır" +favorited: "Favoriye eklendi." +alreadyFavorited: "Zaten favoride" +cantFavorite: "Favoriye eklenemedi" pin: "Profiline sabitle" unpin: "Profilden sabitlemeyi kaldır" copyContent: "İçeriği kopyala" -copyLink: "Linki kopyala" +copyLink: "Link kopyala" copyRemoteLink: "Uzak linki kopyala" copyLinkRenote: "Renote linkini kopyala" delete: "Sil" deleteAndEdit: "Sil ve yeniden düzenle" -deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek ister misiniz? Bu notla ilgili tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." +deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek ister misin? Bu notla ilgili tüm Tepki, Renote ve Yanıtlar da silinecek." addToList: "Listeye ekle" addToAntenna: "Anten'e ekle" sendMessage: "Mesaj gönder" @@ -65,7 +65,7 @@ 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" +searchThisUsersNotes: "Kullanıcının notlarını ara" reply: "Yanıtla" loadMore: "Daha fazla yükle" showMore: "Daha fazlasını göster" @@ -81,16 +81,16 @@ import: "İçeri aktar" export: "Dışa aktar" files: "Dosyalar" download: "İndir" -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." +driveFileDeleteConfirm: "“{name}” dosyasını silmek istediğinden emin misin? Bu dosyaya ekli tüm notlar da silinecek." +unfollowConfirm: "{name} kullanıcısını cidden takipden çıkmak istiyor musun?" +exportRequested: "Dışa aktarma işlemi talep ettin. Bu işlem biraz zaman alabilir. İşlem tamamlandığında Drive'ına eklenecek." +importRequested: "İçe aktarma talebinde bulundun. Bu işlem biraz zaman alabilir." lists: "Listeler" -noLists: "Hiçbir listeniz yok." +noLists: "Hiç liste yok" note: "Not" notes: "Notlar" -following: "Takip eden" -followers: "Takipçiler" +following: "Takip" +followers: "Takipçi" followsYou: "Sizi takip ediyor" createList: "Liste oluştur" manageLists: "Listeleri yönet" @@ -98,9 +98,9 @@ error: "Hata" somethingHappened: "Bir hata oluştu" retry: "Tekrar dene" 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." +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 dene." +serverIsDead: "Bu sunucu yanıt vermiyor. Lütfen bir süre bekleyin ve tekrar dene." +youShouldUpgradeClient: "Bu sayfayı görüntülemek için lütfen yenileyerek istemcini güncelle." enterListName: "Listeye bir ad girin" privacy: "Gizlilik" makeFollowManuallyApprove: "Takip istekleri onay gerektirir" @@ -110,18 +110,18 @@ followRequest: "Takip isteği gönder" followRequests: "Takip istekleri" unfollow: "Takibi bırak" followRequestPending: "Takip isteği beklemede" -enterEmoji: "Bir emoji girin" +enterEmoji: "Bir emoji gir" 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." +unrenote: "Renote geri al" +renoted: "Renote yapıldı." +renotedToX: "{name} adresine Renote" +cantRenote: "Bu not renote edilemez." +cantReRenote: "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" +renoteToChannel: "Kanala Renote" +renoteToOtherChannel: "Diğer kanala Renote\n" pinnedNote: "Sabit not" pinned: "Profiline sabitle" you: "Sen" @@ -129,14 +129,14 @@ clickToShow: "Göstermek için tıklayın" sensitive: "Hassas" add: "Ekle" reaction: "Tepki" -reactions: "Tepki" +reactions: "Tepkiler" 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." +reactionSettingDescription2: "Sıralamayı değiştirmek için sürükle, silmek için tıkla, eklemek için “+” tuşuna bas." rememberNoteVisibility: "Not görünürlük ayarlarını hatırla" attachCancel: "Eki kaldır" deleteFile: "Dosyayı sil" @@ -146,15 +146,15 @@ enterFileName: "Dosya ismini gir" mute: "Gizle" unmute: "sesi aç" renoteMute: "sesi kapat" -renoteUnmute: "sesi açmayı iptal et" +renoteUnmute: "Renote sessiz modunu kaldır" block: "engelle" unblock: "engellemeyi kaldır" suspend: "askıya al" -unsuspend: "askıya alma" -blockConfirm: "Onayı engelle" -unblockConfirm: "engellemeyi kaldır onayla" +unsuspend: "askıya almayı kaldır" +blockConfirm: "Engeli onayla" +unblockConfirm: "Engel kaldırmayı onayla" suspendConfirm: "Hesap askıya alınsın mı?" -unsuspendConfirm: "Hesap askıdan kaldırılsın mı" +unsuspendConfirm: "Hesap askıdan kaldırılsın mı?" selectList: "Bir liste seç" editList: "Listeyi düzenle" selectChannel: "Kanal seç" @@ -172,16 +172,16 @@ emojiUrl: "Emoji URL'si" 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: "Dosya yönetimi görünümünde 🗑️ düğmesine tıklayarak önbelleği temizleyebilirsiniz." +cacheRemoteFilesDescription: "Bu ayar açık olduğunda diğer sitelerin dosyaları doğrudan uzak sunucudan yüklenece. Bu ayarı kapatmak depolama kullanımını azaltacak ama küçük resimler oluşturulmadığından trafiği arttıracak." +youCanCleanRemoteFilesCache: "Dosya yönetimi görünümünde 🗑️ düğmesine tıklayarak önbelleği temizleyebilirsin." 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." +cacheRemoteSensitiveFilesDescription: "Bu ayar kapalı olduğunda hassas uzak dosyalar ön belleğe alınmadan doğrudan uzak sunucudan yüklenecek." flagAsBot: "Bot olarak işaretle" -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." +flagAsBotDescription: "Bu hesap bir program tarafından kontrol ediliyorsa bu seçeneği etkinleştir. 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 ayarlayacak." flagAsCat: "Kedi hesabı" flagAsCatDescription: "Kedi hesabı" -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." +flagShowTimelineReplies: "Pano'da notlara gelen cevapları göster" +flagShowTimelineRepliesDescription: "Açık olduğu durumda, Pano'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" @@ -195,14 +195,14 @@ general: "Genel" wallpaper: "Duvar kağıdı" setWallpaper: "Duvar kağıdını ayarla" removeWallpaper: "Duvar kağıdını kaldır" -searchWith: "Arama: {q}" -youHaveNoLists: "Hiçbir listeniz yok." -followConfirm: "{name}'i takip etmek istediğinizden emin misiniz?" +searchWith: "Ara: {q}" +youHaveNoLists: "Hiç listeniz yok." +followConfirm: "{name} kullanıcısını takip etmek istediğinden emin misin?" 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." +proxyAccountDescription: "Proxy hesabı, belirli koşullar altında kullanıcılar için uzaktan takipçi görevi gören bir hesap. Ö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" +selectUser: "Kullanıcı seç" recipient: "Alıcı" annotation: "Yorumlar" federation: "Federasyon" @@ -223,7 +223,7 @@ software: "Yazılım" softwareName: "Yazılım" version: "Sürüm" metadata: "Meta veri" -withNFiles: "{n} dosya(lar)" +withNFiles: "{n} dosya" monitor: "Monitör" jobQueue: "İşlem sırası" cpuAndMemory: "CPU ve Bellek" @@ -232,16 +232,16 @@ disk: "Disk" instanceInfo: "Sunucu Bilgisi" statistics: "İstatistikler" 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?" +clearQueueConfirmTitle: "Kuyruğu silmek istediğinden emin misin?" +clearQueueConfirmText: "Kuyrukta kalan teslim edilmemiş notlar birleştirilmeyecek. Genellikle bu işlem gerekli değildir." +clearCachedFiles: "Önbelleği temizle" +clearCachedFilesConfirm: "Tüm önbelleğe alınmış uzak dosyaları silmek istediğinden emin misin?" 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." +blockedInstancesDescription: "Engellemek istediğin sunucuların ana bilgisayar adlarını satır sonlarıyla ayırarak liste. 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." +silencedInstancesDescription: "Sessize almak istediğin sunucuların ana bilgisayar adlarını yeni bir satırla ayırarak listele. 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ı etkilemeyecek." 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." +mediaSilencedInstancesDescription: "Medya sessize almak istediğin sunucuların ana bilgisayar adlarını yeni bir satırla ayırarak liste. Listelenen sunuculara ait tüm hesaplar hassas hesap olarak değerlendirilecek ve özel emojiler kullanılamayacaktır. Bu durum, engellenen sunucuları etkilemeyecek." 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" @@ -249,15 +249,15 @@ 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: "Artık daha fazla not sabitleyemezsiniz" +noteDeleteConfirm: "Bu notu silmek istediğinden emin misin?" +pinLimitExceeded: "Artık daha fazla not sabitleyemezsin" done: "Tamam" -processing: "İşleme..." +processing: "İşleniyor..." preview: "Önizleme" default: "Varsayılan" defaultValueIs: "Varsayılan: {value}" noCustomEmojis: "Emoji yok" -noJobs: "Hiç iş yok" +noJobs: "Hiç ş yok" federating: "Birleştirme" blocked: "Engellenmiş" suspended: "Askıya alınmış" @@ -277,31 +277,31 @@ newPasswordRetype: "Yeni şifreyi tekrar girin" attachFile: "Dosyaları ekle" more: "Daha fazlası!" featured: "Öne çıkan" -usernameOrUserId: "Kullanıcı adı veya ID'si" +usernameOrUserId: "Kullanıcı adı veya ID" noSuchUser: "Kullanıcı bulunamadı" lookup: "Sorgu" announcements: "Duyurular" imageUrl: "Görsel URL" remove: "Sil" removed: "Silindi" -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?" +removeAreYouSure: "“{x}” öğesini kaldırmak istediğinizden emin misin?" +deleteAreYouSure: "“{x}” öğesini silmek istediğinizden emin misin?" +resetAreYouSure: "Cidden sıfırlansın mı?" +areYouSure: "Emin misin?" saved: "Kaydedildi" upload: "Yükle" 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" +fromDrive: "Drive'den" fromUrl: "URL'den" -uploadFromUrl: "Bir URL'den yükle" +uploadFromUrl: "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: "Oku" -noMoreHistory: "Daha fazla geçmiş bilgisi yoktur." +noMoreHistory: "Daha fazla geçmiş bilgisi yok." startChat: "Sohbete başla" nUsersRead: "{n} tarafından okundu" agreeTo: "{0}'ı kabul ediyorum." @@ -310,48 +310,48 @@ agreeBelow: "Aşağıdakileri kabul ediyorum" basicNotesBeforeCreateAccount: "Önemli notlar" termsOfService: "Hizmet Şartları" start: "Başla" -home: "Ana sayfa" +home: "Pano" remoteUserCaution: "Bu kullanıcı uzak bir sunucudan geldiği için, gösterilen bilgiler eksik olabilir." activity: "Etkinlik" images: "Görseller" image: "Görsel" birthday: "Doğum günü" yearsOld: "{age} yaşında" -registeredDate: "Katılım tarihi" +registeredDate: "Katılma tarihi" location: "Konum" -theme: "Temalar" +theme: "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: "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ü" +syncDeviceDarkMode: "Karanlık Modu cihaz ayarlarınızla senkronize et" +switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" açık. Senkronizasyonu kapatıp modları manuel olarak değiştirmek ister misin?" +drive: "Drive" fileName: "Dosya adı" -selectFile: "Bir dosya seçin" +selectFile: "Dosya seçin" selectFiles: "Dosyaları seçin" -selectFolder: "Bir klasör seçin" +selectFolder: "Klasör seçin" selectFolders: "Klasörleri seçin" -fileNotSelected: "Hiçbir dosya seçilmedi" +fileNotSelected: "Hiç dosya seçilmedi" renameFile: "Dosyayı yeniden adlandır" folderName: "Klasör adı" createFolder: "Bir klasör oluşturun" -renameFolder: "Bu klasörü yeniden adlandırın" +renameFolder: "Bu klasörü yeniden adlandır" deleteFolder: "Bu klasörü sil" folder: "Dosya" addFile: "Bir dosya ekle" showFile: "Dosyaları göster" -emptyDrive: "Sürücünüz boş" +emptyDrive: "Drive boş" emptyFolder: "Bu klasör boş" 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." +circularReferenceFolder: "Hedef klasör, taşımak istediğiniz klasörün bir alt klasörü." hasChildFilesOrFolders: "Bu klasör boş olmadığı için silinemez." -copyUrl: "URL'yi kopyala" +copyUrl: "URL kopyala" rename: "Yeniden adlandır" avatar: "Avatar" banner: "Banner" @@ -360,7 +360,7 @@ whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde" disconnectedFromServer: "Sunucu bağlantısı kesildi" reload: "Yenile" doNothing: "Yoksay" -reloadConfirm: "Zaman çizelgesini yenilemek ister misiniz?" +reloadConfirm: "Panoyu yenilemek ister misin?" watch: "İzle" unwatch: "İzlemeyi bırak" accept: "Kabul et" @@ -381,41 +381,41 @@ 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." +enableLocalTimeline: "Yerel Pano'yu etkinleştir" +enableGlobalTimeline: "Global Pano'yu etkinleştir" +disablingTimelinesInfo: "Yöneticiler ve Moderatörler, etkinleştirilmemiş olsalar bile her zaman tüm Pano'ya 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" +driveCapacityPerLocalAccount: "Yerel kullanıcı başına Drive kapasitesi" +driveCapacityPerRemoteAccount: "Uzak kullanıcı başına Drive kapasitesi" inMb: "Megabayt cinsinden" -bannerUrl: "Banner görseli URL'si" -backgroundImageUrl: "Arka plan görseli URL'si" +bannerUrl: "Banner görseli URL" +backgroundImageUrl: "Arka plan görseli URL" basicInfo: "Temel bilgiler" pinnedUsers: "Sabitlenmiş kullanıcılar" -pinnedUsersDescription: "“Keşfet” sekmesinde sabitlenecek kullanıcı adlarını satır sonlarıyla ayırarak listeleyin." +pinnedUsersDescription: "“Keşfet” sekmesinde sabitlenecek kullanıcı adlarını satır sonlarıyla ayırarak liste." pinnedPages: "Sabitlenmiş Sayfalar" -pinnedPagesDescription: "Bu örneğin üst sayfasına sabitlemek istediğiniz Sayfaların yollarını satır sonlarıyla ayırarak girin." +pinnedPagesDescription: "Bu örneğin üst sayfasına sabitlemek istediğin Sayfaların yollarını satır sonlarıyla ayırarak gir." pinnedClipId: "Sabitlenecek klibin ID" pinnedNotes: "Sabitlenmiş notlar" hcaptcha: "hCaptcha" -enableHcaptcha: "hCaptcha'yı etkinleştir" +enableHcaptcha: "hCaptcha etkinleştir" hcaptchaSiteKey: "Site anahtar" hcaptchaSecretKey: "Gizli anahtar" mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptcha'yı etkinleştir" +enableMcaptcha: "mCaptcha etkinleştir" mcaptchaSiteKey: "Site anahtarı" mcaptchaSecretKey: "Gizli anahtar" mcaptchaInstanceUrl: "mCaptcha sunucu URL'si" recaptcha: "reCAPTCHA" -enableRecaptcha: "reCAPTCHA'yı etkinleştir" +enableRecaptcha: "reCAPTCHA etkinleştir" recaptchaSiteKey: "Site anahtar" recaptchaSecretKey: "Gizli anahtar" turnstile: "Turnstile" -enableTurnstile: "Turnstile'yi etkinleştir" +enableTurnstile: "Turnstile 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." +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, iptal düğmesine bas." antennas: "Antenler" manageAntennas: "Antenleri Yönet" name: "İsim" @@ -427,17 +427,17 @@ antennaKeywordsDescription: "VE koşulu için boşluklarla, VEYA koşulu için s 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" +enableServiceworker: "Tarayıcınız için Push Bildirimlerini Etkinleştir" +antennaUsersDescription: "Satır başına bir kullanıcı adı listele" 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?" +silenceConfirm: "Bu kullanıcıyı susturmak istediğinden emin misin?" unsilence: "Sessize almayı geri al" -unsilenceConfirm: "Bu kullanıcının sessize alınmasını geri almak istediğinizden emin misiniz?" +unsilenceConfirm: "Bu kullanıcının sessize alınmasını geri almak istediğinden emin misin?" popularUsers: "Popüler kullanıcılar" recentlyUpdatedUsers: "Son zamanlarda aktif olan kullanıcılar" recentlyRegisteredUsers: "Yeni katılan kullanıcılar" @@ -457,10 +457,10 @@ totpDescription: "Tek seferlik şifreleri girmek için bir kimlik doğrulama uyg moderator: "Moderatör" moderation: "Moderasyon" moderationNote: "Moderasyon notu" -moderationNoteDescription: "Moderatörler arasında paylaşılacak notları girebilirsiniz." +moderationNoteDescription: "Moderatörler arasında paylaşılacak notları girebilirsin." addModerationNote: "Moderasyon notu ekle" moderationLogs: "Moderasyon günlükleri" -nUsersMentioned: "{n} kullanıcı tarafından bahsedildi" +nUsersMentioned: "{n} kullanıcı bahsetti" securityKeyAndPasskey: "Güvenlik ve geçiş anahtarları" securityKey: "Güvenlik anahtarı" lastUsed: "Son kullanılan" @@ -489,11 +489,11 @@ text: "Metin" enable: "Etkin" next: "Sonraki" retype: "Tekrar girin" -noteOf: "{user} tarafından not" +noteOf: "{user} not'u" 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." +attachAsFileQuestion: "Panodaki metin uzun. Metin dosyası olarak eklemek ister misin?" +onlyOneFileCanBeAttached: "Bir mesaja yalnızca bir dosya ekleyebilirsin." 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" @@ -501,7 +501,7 @@ 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)" +usernameInvalidFormat: "Büyük ve küçük harfler, rakamlar ve alt çizgi kullanabilirsin. (a~z、A~Z、0~9)" tooShort: "Çok kısa" tooLong: "Çok uzun" weakPassword: "Zayıf şifre" @@ -537,13 +537,13 @@ 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." +noFollowRequests: "Bekleyen takip istekleri yok." openImageInNewTab: "Görüntüleri yeni sekmede aç" dashboard: "Gösterge paneli" local: "Yerel" -remote: "Uzaktan" +remote: "Uzak" total: "Toplam" -weekOverWeekChanges: "Geçen haftadan bu yana yapılan değişiklikler" +weekOverWeekChanges: "Geçen haftadan beri yapılan değişiklikler" dayOverDayChanges: "Dünkü değişiklikler" appearance: "Görünüm" clientSettings: "İstemci Ayarları" @@ -552,31 +552,31 @@ 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" +showFeaturedNotesInTimeline: "Pano'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" +objectStoragePrefix: "Ön ek" 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." +objectStorageRegionDesc: "'xx-east-1' gibi bir bölge belirt. Hizmetin 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 boş bırak." 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." +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ştirmen 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" +showFixedPostForm: "Gönderi formunu pano üstünde görüntüle" +showFixedPostFormInChannel: "Gönderi formunu pano üstünde görüntüle (Kanallar)" +withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak panoya dahil et" +newNoteRecived: "Yeni Not'lar var" newNote: "Yeni Not" sounds: "Sesler" sound: "Ses" @@ -602,41 +602,41 @@ installedDate: "Yetkili" lastUsedDate: "En son kullanıldığı tarih" state: "Durum" sort: "Sıralama düzeni" -ascendingOrder: "Yükselme" -descendingOrder: "Alçalma" +ascendingOrder: "Artan" +descendingOrder: "Azalan" 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." +scratchpadDescription: "Scratchpad, AiScript deneyleri için bir ortam sağlar. Misskey ile etkileşim halindeyken yazabilir, çalıştırabilir ve sonuçlarını kontrol edebilirsin." 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." +uiInspectorDescription: "Bellekteki UI bileşeni sunucu listesini görebilirsin. UI bileşeni, Ui:C: işlevi tarafından oluşturulacak." 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?" +unsetUserAvatarConfirm: "Avatarı silmek istediğinden emin misin?" unsetUserBanner: "Banner'ı kaldır" -unsetUserBannerConfirm: "Banner'ı kaldırmak istediğinizden emin misiniz?" +unsetUserBannerConfirm: "Banner'ı kaldırmak istediğinden emin misin?" 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" +deleteAllFilesConfirm: "Tüm dosyaları silmek istediğinden emin misin?" +removeAllFollowing: "Takip ettiğin tüm kullanıcıları takipten çıkar" 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." +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 istersen yöneticiyle iletişime geç. Lütfen yeni bir hesap oluşturma." tokenRevoked: "Geçersiz jeton" tokenRevokedDescription: "Bu jetonun süresi doldu. Lütfen tekrar giriş yapın." accountDeleted: "Hesap silindi" -accountDeletedDescription: "Bu hesap silinmiştir." +accountDeletedDescription: "Bu hesap silinmiş." 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" +inboxUrl: "Gelen Kutusu URL" addedRelays: "Eklenen Röleler" -serviceworkerInfo: "Push bildirimleri için etkinleştirilmelidir." +serviceworkerInfo: "Push bildirimleri için etkinleştirilmeli." deletedNote: "Silinen not" invisibleNote: "Görünmez not" enableInfiniteScroll: "Otomatik olarak daha fazlasını yükle" @@ -670,12 +670,12 @@ 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." +pluginTokenRequestedDescription: "Bu eklenti, burada ayarlanan izinleri kullanabilecek." 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." +emailConfigInfo: "Kayıt sırasında veya şifreni unuttuğunda E-postanı doğrulamak için kullanılır." email: "E-Posta" emailAddress: "E-Posta adresi" smtpConfig: "SMTP Sunucu yapılandırması" @@ -691,7 +691,7 @@ 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." +hardWordMuteDescription: "Belirtilen kelime veya kelime öbeğini içeren notları gizle. 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" @@ -724,7 +724,7 @@ 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." +fillAbuseReportDescription: "Bu raporla ilgili ayrıntıları lütfen doldur. Belirli bir notla ilgiliyse, lütfen URL'sini de ekle." abuseReported: "Raporunuz gönderildi. Çok teşekkür ederiz." reporter: "Raporlayan" reporteeOrigin: "Bildirim Kaynağı" @@ -745,27 +745,27 @@ 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" +confirmToUnclipAlreadyClippedNote: "Bu not zaten “{name}” klibinin bir parçası. Bu klipten silmek ister misin?" +public: "Herkese 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" +i18nInfo: "Misskey, gönüllüler tarafından çeşitli dillere çevrilmektedir. {link} adresinden yardımcı olabilirsin." +manageAccessTokens: "Acces Tokens yönet" +accountInfo: "Hesap bilgileri" notesCount: "Not sayısı" -repliesCount: "Gönderilen yanıt sayısı" -renotesCount: "Gönderilen renote sayısı" +repliesCount: "Yanıt sayısı" +renotesCount: "Renote sayısı" repliedCount: "Alınan yanıt sayısı" -renotedCount: "Alınan renot sayısı" -followingCount: "Takip edilen hesap sayısı" +renotedCount: "Alınan Renote sayısı" +followingCount: "Takip sayısı" followersCount: "Takipçi sayısı" -sentReactionsCount: "Gönderilen tepki sayısı" +sentReactionsCount: "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ı" +pollVotesCount: "Anket oy sayısı" +pollVotedCount: "Alınan anket oy sayısı" yes: "Evet" no: "Hayır" -driveFilesCount: "Sürücü dosya sayısı" -driveUsage: "Sürücü alanı kullanımı" +driveFilesCount: "Drive dosya sayısı" +driveUsage: "Drive 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." 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." @@ -775,84 +775,84 @@ 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ı" +emailVerified: "E-posta adresi doğrulandı." +noteFavoritesCount: "Favori not sayısı" +pageLikesCount: "Beğenilen sayfa sayısı" +pageLikedCount: "Alınan sayfa beğen sayısı" +contact: "İletişim" 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." +thisIsExperimentalFeature: "Bu deneysel bir özellik. İş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" +makeExplorable: "Hesabı “Keşfet” bölümünde görünür hale getir" 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?" +reloadToApplySetting: "Bu ayar, sayfa yeniden yüklendikten sonra geçerli olacaktır. Şimdi yeniden yüklemek ister misin?" 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" +clearCache: "Önbellek temizle" 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." +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ı olacak.\nBu bilgiler arasında işletim sisteminizin sürümü, kullandığınız tarayıcı, Misskey'deki faaliyetlerin vb. yer alacaktır." myTheme: "Benim temam" backgroundColor: "Arka plan rengi" accentColor: "Vurgu rengi" textColor: "Metin rengi" -saveAs: "Farklı kaydet..." +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?" +saveConfirm: "Değişiklikleri kaydetmek ister misin?" +deleteConfirm: "Cidden 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." +youAreRunningUpToDateClient: "İstemci 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ış" +inUse: "Kullanılıyor" editCode: "Kodu düzenle" apply: "Uygula" receiveAnnouncementFromInstance: "Bu sunucudan bildirimler alın" -emailNotification: "E-posta bildirimleri" +emailNotification: "E-posta bildirimi" 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" +showingPastTimeline: "Şu anda eski bir Pano görüntüleniyor." +clear: "Temizle" markAllAsRead: "Tümünü okundu olarak işaretle" goBack: "Geri" -unlikeConfirm: "Gerçekten beğenini kaldırmak mı istiyorsun?" +unlikeConfirm: "Cidden 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." +userPagePinTip: "Bireysel notların menüsünden “Profiline sabitle” seçeneğini seçerek notları burada görüntüleyebilirsin." 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" +userInfo: "Kullanıcı hakkında" 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" +online: "Online" active: "Aktif" -offline: "Çevrimdışı" +offline: "Offline" notRecommended: "Tavsiye edilmez" botProtection: "Bot Koruması" instanceBlocking: "Blocked/Silenced Instances" @@ -888,7 +888,7 @@ 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" +global: "Global" squareAvatars: "Kare avatarlar" sent: "Gönderilen" received: "Alınan" @@ -902,7 +902,7 @@ 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." +usernameInfo: "Bu sunucudaki diğer hesaplardan hesabını ayıran bir isim. Alfabe (a~z, A~Z), rakamlar (0~9) veya alt çizgi (_) kullanabilirsin. Kullanıcı adları daha sonra değiştirilemez." aiChanMode: "Ai Modu" devMode: "Geliştirici modu" keepCw: "İçerik uyarılarını sakla" @@ -911,7 +911,7 @@ lastCommunication: "Son iletişim" resolved: "Çözülmüş" unresolved: "Çözülmemiş" breakFollow: "Takipçiyi kaldır" -breakFollowConfirm: "Bu takipçiyi gerçekten silmek istiyor musun?" +breakFollowConfirm: "Bu takipçiyi ciddden silmek istiyor musun?" itsOn: "Etkin" itsOff: "Devre Dışı" on: "Açık" @@ -922,14 +922,14 @@ 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." +makeReactionsPublicDescription: "Bu, geçmişteki tüm tepkilerinin listesini herkese açık hale getirecek." 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?" +deleteAccountConfirm: "Bu, hesabını geri dönüşü olmayan bir şekilde silecek. Devam etmek istiyor musun?" incorrectPassword: "Yanlış şifre." incorrectTotp: "Tek kullanımlık şifre yanlış veya süresi dolmuş." voteConfirm: "\"{choice}\" için oyunuzu onaylıyor musunuz?" @@ -963,7 +963,7 @@ 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?" +cropImageAsk: "Bu görüntüyü kırpmak ister misin?" cropYes: "Kırp" cropNo: "Olduğu gibi kullanın" file: "Dosyalar" @@ -973,7 +973,7 @@ 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" +driveCapOverrideLabel: "Bu kullanıcının Drive kapasitesini değiştir" 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." @@ -982,8 +982,8 @@ 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." +logoutConfirm: "Çıkmak istediğinden emin misin?" +logoutWillClearClientData: "Oturumu kapatmak, tarayıcıdan istemcinin ayarlarını siler. Tekrar oturum açtığında ayarları geri yükleyebilmek için, ayarlarının otomatik yedeklenmesini etkinleştirmen gerekir." lastActiveDate: "Son kullanımı" statusbar: "Durum çubuğu" pleaseSelect: "Bir seçenek seçin" @@ -1000,7 +1000,7 @@ 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." +cannotUploadBecauseNoFreeSpace: "Drive 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" @@ -1032,7 +1032,7 @@ numberOfLikes: "Beğeniler" show: "Göster" neverShow: "Bir daha gösterme" remindMeLater: "Belki daha sonra" -didYouLikeMisskey: "Misskey'i sevdiniz mi?" +didYouLikeMisskey: "Misskey'i sevdin 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" @@ -1044,33 +1044,34 @@ assign: "Atama" unassign: "Atamayı kaldır" color: "Renk" manageCustomEmojis: "Özel Emojileri Yönet" -manageAvatarDecorations: "Avatar süslemelerini yönet" +manageAvatarDecorations: "Avatar süslerini 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." +cannotPerformTemporaryDescription: "Bu işlem, yürütme sınırını aştığı için geçici olarak gerçekleştirilememekte. Lütfen bir süre bekle ve tekrar dene." 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" +custom: "Özel" 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." +gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar dene." thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir." -thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder" +thisPostMayBeAnnoyingHome: "Ana panoya 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." +collapseRenotes: "Daha önce görüntülenen Renote'lari kısaltılmış olarak göster" +collapseRenotesDescription: "Zaten yanıtladığın veya renote aldığın notları kapat." 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." +joinThisServer: "Kaydol" +exploreOtherServers: "Diğer sunucuları keşfet" +letsLookAtTimeline: "Pano'ya bir göz atın" +disableFederationConfirm: "Federasyonu cidden devre dışı bırakmak istiyor musun?" +disableFederationConfirmWarn: "Federasyondan ayrılsa bile, aksi belirtilmedikçe gönderiler herkese açık olmaya devam edecek. 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." @@ -1082,9 +1083,9 @@ 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?" +resetPasswordConfirm: "Şifreni gerçekten sıfırlamak istiyor musun?" 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." +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 listeleyebilirsin." 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." @@ -1092,19 +1093,20 @@ prohibitedWordsDescription2: "Boşluk kullanmak AND ifadeleri oluşturur ve anah 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." +usersSearchNotAvailable: "Kullanıcı araması mevcut değildir." license: "Lisans" -unfavoriteConfirm: "Gerçekten favorilerden kaldırmak istiyor musunuz?" +unfavoriteConfirm: "Cidden favorilerden kaldırmak istiyor musunuz?" myClips: "Kliplerim" -drivecleaner: "Sürücü Temizleyici" +drivecleaner: "Drive Temizleyici" retryAllQueuesNow: "Tüm kuyrukları yeniden çalıştırmayı deneyin" -retryAllQueuesConfirmTitle: "Gerçekten hepsini tekrar denemek istiyor musunuz?" +retryAllQueuesConfirmTitle: "Cidden 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." +limitWidthOfReaction: "Tepkilerin maksimum genişliğini sınırla ve bunları küçültülmüş boyutta görüntüle." noteIdOrUrl: "Not ID veya URL" video: "Video" videos: "Videolar" @@ -1130,18 +1132,18 @@ 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." +pleaseConfirmBelowBeforeSignup: "Bu sunucuya kaydolmak için aşağıdakileri gözden geçirip kabul etmelisin:" +pleaseAgreeAllToContinue: "Devam etmek için yukarıdaki tüm alanları kabul etmelisin." 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." +preservedUsernamesDescription: "Rezervasyon yapmak için kullanıcı adlarını satır sonlarıyla ayırarak listele. 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." +channelArchiveConfirmTitle: "Cidden {name} arşivlemek mi istiyorsun?" +channelArchiveConfirmDescription: "Arşivlenmiş bir kanal artık kanal listesinde veya arama sonuçlarında görünmeyecektir. Ayrıca, bu kanala yeni gönderiler eklenemeyecek." +thisChannelArchived: "Bu kanal arşivlenmiş." displayOfNote: "Not ekranı" initialAccountSetting: "Profil ayarları" youFollowing: "Takip edildi" @@ -1149,16 +1151,16 @@ 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?" +lookupConfirm: "Yukarı bakmak ister misin?" +openTagPageConfirm: "Bir hashtag sayfası açmak ister misin?" specifyHost: "Belirli ana bilgisayar" failedToPreviewUrl: "Önizleme yapılamadı" update: "Güncelle" -rolesThatCanBeUsedThisEmojiAsReaction: "Bu emojiyi tepki olarak kullanabileceğiniz roller" +rolesThatCanBeUsedThisEmojiAsReaction: "Bu emojiyi tepki olarak kullanabileceğin 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?" +cancelReactionConfirm: "Tepkini cidden silmek istiyor musun?" +changeReactionConfirm: "Tepkini cidden değiştirmek istiyor musun?" later: "Daha sonra" goToMisskey: "Misskey'e" additionalEmojiDictionary: "Ek emoji sözlükleri" @@ -1171,7 +1173,7 @@ 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." +inviteLimitExceeded: "Oluşturulabilecek davetiyelerin maksimum sayısına ulaştın." createLimitRemaining: "{limit} Davet limiti kaldı" inviteLimitResetCycle: "Bu limit {time} tarihinde {limit} değerine sıfırlanacaktır." expirationDate: "Son kullanma tarihi" @@ -1193,7 +1195,7 @@ 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." +useSecurityKey: "Güvenlik anahtarını veya şifreni kullanmak için lütfen tarayıcının veya cihazının talimatlarını izle." replies: "Yanıtla" renotes: "Renote'lar" loadReplies: "Yanıtları göster" @@ -1206,18 +1208,18 @@ 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" +showRenotes: "Renote'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?" +showRepliesToOthersInTimeline: "Pano'da diğer kişilere verilen yanıtları göster" +hideRepliesToOthersInTimeline: "Pano'dan diğer kişilerin yanıtlarını gizle" +showRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesin diğerlerine verdiği yanıtları göster" +hideRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesten diğer kişilere verilen yanıtları gizle" +confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını panoda diğer kullanıcılara göstermek istiyor musun?" +confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını panoda cidden göstermeyecek misin?" 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." @@ -1232,24 +1234,24 @@ impressumDescription: "Almanya gibi bazı ülkelerde, ticari web sitelerinde iş privacyPolicy: "Gizlilik Politikası" privacyPolicyUrl: "Gizlilik Politikası URL'si" tosAndPrivacyPolicy: "Hizmet Şartları ve Gizlilik Politikası" -avatarDecorations: "Avatar süslemeleri" +avatarDecorations: "Avatar süsleri" attach: "Ek" detach: "Kaldır" detachAll: "Tümünü Kaldır" angle: "Açı" flip: "Çevir" -showAvatarDecorations: "Avatar süslemelerini göster" +showAvatarDecorations: "Avatar süslerini 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." +emailVerificationFailedError: "E-posta adresi doğrulanırken bir sorun oluştu. Bağlantının geçerlilik süresi dolmuş olabilir." +cwNotationRequired: "“İçeriği gizle” seçeneği etkinleştirilirse, bir açıklama sağlanmalı." 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?" +overwriteContentConfirm: "Mevcut içeriği üzerine yazmak istediğinden emin misin?" seasonalScreenEffect: "Mevsimsel Ekran Efekti" decorate: "Süsle" addMfmFunction: "MFM ekle" @@ -1266,7 +1268,7 @@ 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" +withSensitive: "Hassas dosyalara notlar ekle" 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" @@ -1278,13 +1280,13 @@ 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." +keepOriginalFilenameDescription: "Bu ayarı kapatırsan, dosya yüklediğinde dosya adları otomatik olarak rastgele bir dizeyle değiştirilecek." noDescription: "Açıklama yok" alwaysConfirmFollow: "Takip ederken her zaman onaylayın" inquiry: "İletişim" -tryAgain: "Lütfen daha sonra tekrar deneyin." +tryAgain: "Lütfen daha sonra tekrar dene." confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media" -sensitiveMediaRevealConfirm: "Bu hassas bir medya olabilir. Açıklamakta emin misiniz?" +sensitiveMediaRevealConfirm: "Bu hassas bir medya olabilir. Açıklamakta emin misin?" createdLists: "Oluşturulan listeler" createdAntennas: "Oluşturulan antenler" fromX: "{x}'den" @@ -1305,7 +1307,7 @@ testCaptchaWarning: "Bu işlev CAPTCHA testi amacıyla tasarlanmıştır.\n\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." +driveAboutTip: "Drive'da, geçmişte yüklediğin dosyaların bir listesi görüntülenir.
\nBu dosyaları notlara eklerken yeniden kullanabilir veya daha sonra paylaşmak üzere önceden yükleyebilirsin.
\nBir dosyayı silerken dikkatli ol, çünkü kullanıldığı her yerde (notlar, sayfalar, avatarlar, afişler vb.) mevcut olmayacakt.
\nAyrıca dosyalarını düzenlemek için klasörler oluşturabilirsin." scrollToClose: "Kaydırarak kapatın" advice: "Tavsiye" realtimeMode: "Gerçek zamanlı mod" @@ -1363,9 +1366,9 @@ 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" +tip: "İpucu & Püf Nokta" +redisplayAllTips: "Tüm “İpucu & Püf Nokta” tekrar göster" +hideAllTips: "Tüm “İpucu & Püf Nokta” 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)" @@ -1374,6 +1377,7 @@ 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." +thankYouForTestingBeta: "Beta sürümünü test ettiğin için teşekkür ederiz!" _order: newest: "Önce yeni" oldest: "Önce eski" @@ -1410,7 +1414,7 @@ _chat: 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." + youAreNotAMemberOfThisRoomButInvited: "Bu odanın katılımcısı değilsin, ancak bir davet aldın. Lütfen daveti kabul ederek katıl." doYouAcceptInvitation: "Daveti kabul ediyor musunuz?" chatWithThisUser: "Kullanıcıyla sohbet et" thisUserAllowsChatOnlyFromFollowers: "Bu kullanıcı yalnızca takipçilerinden gelen sohbetleri kabul eder." @@ -1418,12 +1422,12 @@ _chat: 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_note: "Bu ayardan bağımsız olarak, sohbet mesajı gönderdiğin herkesle sohbet edebilirsin." _chatAllowedUsers: everyone: "Herkes" - followers: "Sadece takipçileriniz" + followers: "Sadece takipçilerin" following: "Only users you are following" - mutual: "Sadece takip ettiğiniz kullanıcılar" + mutual: "Sadece takiplerin" none: "Kimse" _emojiPalette: palettes: "Palet" @@ -1431,24 +1435,24 @@ _emojiPalette: 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." + driveBanner: "Drive'ı yönetebilir ve yapılandırabilir, kullanımı kontrol edebilir ve dosya yükleme ayarlarını yapılandırabilirsin." + pluginBanner: "Eklentilerle istemci özelliklerini genişletebilirsin. Eklentileri yükleyebilir, ayrı ayrı yapılandırabilir ve yönetebilirsin." + notificationsBanner: "Sunucudan gelen bildirimlerin türlerini ve kapsamını ve push bildirimlerini yapılandırabilirsin." 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" + muteAndBlockBanner: "İçeriği gizlemek ve belirli kullanıcıların eylemlerini kısıtlamak için ayarları yapılandırabilir ve yönetebilirsin." + accessibilityBanner: "İstemci, görünüm ve davranışları açısından en iyi şekilde kullanılmak üzere kişiselleştirilebilir ve ayarlanabilir." + privacyBanner: "Hesap gizliliği ile ilgili ayarları, örneğin içerik görünürlüğü, bulunabilirlik ve takip onayı gibi ayarları yapılandırabilirsin." + securityBanner: "Şifre, oturum açma yöntemleri, kimlik doğrulama uygulamaları ve Passkeys gibi hesap güvenliği ile ilgili ayarları yapılandırabilirsin." + preferencesBanner: "İstediğin şekilde istemcinin genel davranışını yapılandırabilirsin." + appearanceBanner: "İstemcinin görünüm ve ekran ayarlarını tercihlerini göre yapılandırabilirsin." + soundsBanner: "İstemcide oynatma için ses ayarlarını yapılandırabilirsin." + timelineAndNote: "Pano ve not" + makeEveryTextElementsSelectable: "Tüm metin öğelerini seçilebilir hale getir" 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" @@ -1458,12 +1462,12 @@ _settings: 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." + enablePullToRefresh_description: "Fareyi kullanırken, kaydırma tekerleğini basılı tutarken sürükle." 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" + showUrlPreview: "URL önizlemesi" showAvailableReactionsFirstInNote: "Mevcut tepkileri en üstte göster." showPageTabBarBottom: "Sayfa sekme çubuğunu aşağıda göster" _chat: @@ -1471,27 +1475,27 @@ _settings: sendOnEnter: "Enter tuşuna basarak gönderin" _preferencesProfile: profileName: "Profil adı" - profileNameDescription: "Bu cihazı tanımlayan bir ad belirleyin." + profileNameDescription: "Bu cihazı tanımlayan bir ad belirle." 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." + noBackupsFoundDescription: "Otomatik olarak oluşturulan yedekleme bulunamadı, ancak manuel olarak bir yedekleme dosyası kaydettiysen, bunu içe aktarabilir ve geri yükleyebilirsin." 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." + autoPreferencesBackupIsNotEnabledForThisDevice: "Bu cihazda ayarların otomatik yedeklemesi etkinleştirilmemiş." 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." + requireSigninToViewContentsDescription1: "Oluşturduğun tüm notları ve diğer içeriği görüntülemek için oturum açman gerekir. Bu, tarayıcıların bilgilerini 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ülenmeyecek." 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." + 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üklenecek." 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." @@ -1502,7 +1506,7 @@ _abuseUserReport: 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." + resolveTutorial: "Raporun içeriği meşruysa, “Kabul Et” seçeneğini seçerek sorunu çözülmüş olarak işaretle.\nRaporun içeriği meşru değilse, “Reddet” seçeneğini seçerek raporu yok say." _delivery: status: "Teslimat durumu" stop: "Askıya al" @@ -1527,74 +1531,74 @@ _bubbleGame: _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!" + section3: "Kutu dolduğunda oyun biter. Kutuyu doldurmadan nesneleri birleştirerek yüksek puan almaya çalış!" _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." + forExistingUsersDescription: "Bu duyuru, etkinleştirildiğinde yalnızca yayınlandığı anda mevcut olan kullanıcılara gösterilecek. Devre dışı bırakıldığında, yayınlandıktan sonra yeni kaydolan kullanıcılar da bu duyuruyu görecek." 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." + tooManyActiveAnnouncementDescription: "Çok fazla aktif duyuru olması kullanıcı deneyimini kötüleştirebilir. Artık geçerliliğini yitirmiş duyuruları arşivlemeyi düşün." readConfirmTitle: "Okundu olarak işaretle?" - readConfirmText: "Bu, “{title}” içeriğini okundu olarak işaretleyecektir." + readConfirmText: "Bu, “{title}” içeriğini okundu olarak işaretleyecek." 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." + silenceDescription: "Bu seçeneği etkinleştirdiğinde, bu duyurunun bildirimi atlanacak ve kullanıcı bunu okumak zorunda kalmayacak." _initialAccountSetting: accountCreated: "Hesabınız başarıyla oluşturuldu!" - letsStartAccountSetup: "Öncelikle, profilinizi oluşturalım." - letsFillYourProfile: "Öncelikle profilinizi oluşturalım." + letsStartAccountSetup: "Şimdi hesabını oluşturalım." + letsFillYourProfile: "Önce profilini 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." + theseSettingsCanEditLater: "Bu ayarları daha sonra istediğin zaman değiştirebilirsin." + youCanEditMoreSettingsInSettingsPageLater: "“Ayarlar” sayfasından yapılandırabileceğin daha birçok ayar bulunmaktadır. Daha sonra mutlaka ziyaret et." + followUsers: "İlgilendiğiniz bazı kullanıcıları takip ederek zaman akışını oluşturmaya çalış." + pushNotificationDescription: "Push bildirimlerini etkinleştirdiğinde, {name} adresinden gelen bildirimleri doğrudan cihazınıza alabilirsin." 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." + haveFun: "{name} ile iyi eğlenceler!" + youCanContinueTutorial: "{name} (Misskey) öğreticisine geçebilir veya buradan kurulumu sonlandırıp hemen kullanabilirsin." startTutorial: "Öğreticiye başla" - skipAreYouSure: "Profil kurulumunu gerçekten atlamak mı istiyorsunuz?" - laterAreYouSure: "Profil ayarlarını gerçekten daha sonra mı yapacaksınız?" + skipAreYouSure: "Profil kurulumunu cidden atlamak mı istiyorsun?" + laterAreYouSure: "Profil ayarlarını cidden daha sonra mı yapacaksın?" _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." + title: "Öğreticiye hoş geldin" + description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsin." _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." + description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar panoda 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." + renote: "Bu notu kendi panonda paylaşabilirsin. Ayrıca yorumlarınla birlikte alıntı da yapabilirsin." + reaction: "Not'a tepkiler ekleyebilirsin. Daha fazla ayrıntı bir sonraki sayfada açıklanacak." + menu: "Not ayrıntılarını görüntüleyebilir, bağlantıları kopyalayabilir ve çeşitli diğer işlemleri gerçekleştirebilirsin." _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." + description: "Notlara çeşitli emojilerle tepki verilebilir. Tepkiler, sadece bir ‘beğeni’ ile ifade edilemeyen nüansları ifade etmeni 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." + reactToContinue: "Devam etmek için bir tepki ekle." + reactNotification: "Biri notunuza tepki verdiğinde gerçek zamanlı bildirimler alacaksınız." + reactDone: "“-” düğmesine basarak bir tepkiyi geri alabilirsin." _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." + title: "Pano Kavramı" + description1: "Misskey, kullanıma göre birden fazla Pano sunar (Bazı Pano'lar sunucunun politikalarına bağlı olarak kullanılamayabilir)." + home: "Takip ettiğin hesapların notlarını görüntüleyebilirsin." + local: "Bu sunucudaki tüm kullanıcıların notlarını görüntüleyebilirsin." + social: "Ev ve Yerel Pano'dan notlar görüntülenecek." + global: "Bağlı tüm sunuculardan gelen notları görüntüleyebilirsin." + description2: "Ekranın üst kısmındaki Pano'lar arasında istediğin zaman geçiş yapabilirsin." + description3: "Ayrıca, Liste Pano ve Kanal Pano 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." + description: "Notunu kimlerin görüntüleyebileceğini sınırlayabilirsin." 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." + home: "Yalnızca Ana zaman akışında herkese açık. Profilinizi ziyaret edenler, takipçilerin 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!" @@ -1602,30 +1606,30 @@ _initialTutorial: 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." + description: "Gövde yerine, “Yorumlar” alanına yazılan içerik görüntülenecek. “Devamını oku” düğmesine basıldığında gövde görüntülenecek." _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!" + description: "Sunucu kuralları gereği gerekli olan veya bozulmaması gereken ekler için “hassas” bayrağı ekle." + tryThisFile: "Bu forma ekli resmi hassas olarak işaretlemeyi dene!" _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." + doItToContinue: "Devam etmek için ek dosyayı hassas olarak işaretle." _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." + home: "Ana Pano'da, takip ettiğin hesapların notlarını görebilirsin." + local: "Yerel Pano'da, bu sunucudaki tüm kullanıcıların notlarını görebilirsin." + social: "Pano, Sosyal Pano ve Yerel Pano'dan gelen notları görüntüler." + global: "Global Pano'da, bağlı tüm sunuculardan gelen notları görebilirsin." _serverRules: - description: "Kayıt öncesinde gösterilecek bir dizi kural. Hizmet Şartlarının özetini belirlemeniz önerilir." + description: "Kayıt öncesinde gösterilecek bir dizi kural. Hizmet Şartlarının özetini belirlemen önerilir." _serverSettings: iconUrl: "Simge URL'si" appIconDescription: " {host} bir uygulama olarak görüntülendiğinde kullanılacak simgeyi belirtir." @@ -1635,23 +1639,23 @@ _serverSettings: 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." + fanoutTimelineDescription: "Etkinleştirildiğinde Pano 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." 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." + fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano ö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 panoların 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." 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." + remoteNotesCleaning_description: "Etkinleştirildiğinde, kullanılmayan ve güncelliğini yitirmiş uzak notlar, veritabanının şişmesini önlemek için periyodik olarak temizlenecek." 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." + openRegistration: "Hesap oluşturmayı açık hale getir" + 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 sistemin varsa, bu işlemi etkinleştirmen ö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." + singleUserMode_description: "Bu sunucunun tek kullanıcısıysanız, bu modu etkinleştirerek performansını optimize edebilirsin." 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" @@ -1661,8 +1665,11 @@ _serverSettings: 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_title: "Sunucu kurulum sihirbazını yeniden başlatmak ister misin?" restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır." + entrancePageStyle: "Giriş sayfası stili" + showTimelineForVisitor: "Panoyu göster" + showActivitiesForVisitor: "Aktiviteleri göster" _userGeneratedContentsVisibilityForVisitor: all: "Her şey halka açıktır." localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur." @@ -1675,12 +1682,12 @@ _accountMigration: 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.)" + moveAccountDescription: "Bu işlem, hesabını farklı bir hesaba taşıyacaktır.\n・Bu hesabın takipçileri otomatik olarak yeni hesaba taşınacak.\n・Bu hesap, şu anda takip ettiği tüm kullanıcıları takipten çıkaracak.\n・Bu hesapta yeni notlar vb. oluşturamayacaksın.\n\nTakipçilerin taşınması otomatik olarak gerçekleşirken, takip ettiğin kullanıcıların listesini taşımak için bazı adımları manuel olarak hazırlaman gerekir. Bunu yapmak için, ayarlar menüsünden takipçilerini dışa aktar ve daha sonra yeni hesaba içe aktar. Aynı prosedür, listelerinin yanı sıra sessize aldığın ve engellediğin kullanıcılar için de geçerli.\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." + migrationConfirm: "Bu hesabı {account} hesabına gerçekten taşımak istiyor musun? Bu işlem başlatıldıktan sonra durdurulamaz veya geri alınamaz ve bu hesabı artık orijinal haliyle kullanamazsın." 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." + postMigrationNote: "Bu hesap, geçiş işlemi tamamlandıktan 24 saat sonra şu anda takip ettiği tüm hesapları takipten çıkaracak.\nHem takipçi sayısı hem de takip edilenler sayısı sıfır olacak. Takipçilerinin bu hesabın yalnızca takipçilere açık gönderilerini görememesi durumunu önlemek için, takipçilerin bu hesabı takip etmeye devam edecek." movedTo: "Yeni hesap:" _achievements: earnedAt: "Şurada açıldı" @@ -1787,7 +1794,7 @@ _achievements: flavor: "Misskey'i kullandığınız için teşekkür ederiz!" _noteClipped1: title: "Kesinlikle... kesmeliyim..." - description: "İlk notunuzu ekleyin" + description: "İlk notunu ekle" _noteFavorited1: title: "Yıldız gözlemcisi" description: "İlk notunu favorilerine ekle" @@ -1796,10 +1803,10 @@ _achievements: description: "Başka birinin notlarınızdan birini favorilerine eklemesini sağlayın" _profileFilled: title: "İyi hazırlanmış" - description: "Profilinizi oluşturun" + description: "Profilini oluştur" _markedAsCat: title: "Ben bir kediyim." - description: "Hesabınızı kedi olarak işaretleyin" + description: "Hesabını kedi olarak işaretle" flavor: "Sana daha sonra bir isim vereceğim." _following1: title: "İlk kullanıcınızı takip edin" @@ -1846,7 +1853,7 @@ _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!" + flavor: "Misskey geliştirme ekibi desteğin için çok teşekkür eder!" _foundTreasure: title: "Hazine Avı" description: "Gizli hazineyi buldunuz." @@ -1871,11 +1878,11 @@ _achievements: title: "Öz Referans" description: "Kendi notunuzu alıntı yapın" _htl20npm: - title: "Akış Zaman Çizelgesi" + title: "Akış Panosu" 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" + description: "Sunucunun grafiklerini görüntüle" _outputHelloWorldOnScratchpad: title: "Merhaba, dünya!" description: "Scratchpad'de “hello world” yazdırın." @@ -1884,9 +1891,9 @@ _achievements: 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." + description: "Drive'da yinelemeli olarak iç içe geçmiş bir klasör oluşturmaya çalış." _reactWithoutRead: - title: "Gerçekten okudun mu?" + title: "Cidden 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" @@ -1923,7 +1930,7 @@ _achievements: 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." + description: "Bildirim testini çok kısa bir süre içinde tekrar tekrar tetikle." _tutorialCompleted: title: "Misskey Temel Kurs Diploması" description: "Eğitim tamamlandı" @@ -1933,7 +1940,7 @@ _achievements: _bubbleGameDoubleExplodingHead: title: "Çift🤯" description: "Aynı anda balon oyunundaki en büyük iki nesne" - flavor: "Öğle yemeği kutunuzu şöyle doldurabilirsiniz 🤯 🤯 biraz." + flavor: "Öğle yemeği kutunu şöyle doldurabilirsin 🤯 🤯 biraz." _role: new: "Yeni rol" edit: "Rolü düzenle" @@ -1950,21 +1957,21 @@ _role: 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." + descriptionOfIsPublic: "Bu rol, atanan kullanıcıların profillerinde görüntülenecek." options: "Seçenekler" policies: "Politikalar" baseRole: "Rol şablonu" useBaseValue: "Rol şablonu değerini kullan" - chooseRoleToAssign: "Atamak istediğiniz rolü seçin" + chooseRoleToAssign: "Atamak istediğin rolü seç" 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." + isExplorable: "Rolü keşfedilebilir hale getir" + descriptionOfIsExplorable: "Bu rolün panosu ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek." 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." + preserveAssignmentOnMoveAccount_description: "Etkinleştirildiğinde, bu rol, bu role sahip bir hesap taşındığında hedef hesaba aktarılacak." 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" @@ -1973,8 +1980,8 @@ _role: middle: "Orta" high: "Yüksek" _options: - gtlAvailable: "Küresel zaman çizelgesini görüntüleyebilir" - ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir" + gtlAvailable: "Global Pano'yu görüntüleyebilir" + ltlAvailable: "Yerel panoyu görüntüleyebilir" canPublicNote: "Halka açık notlar gönderebilir" mentionMax: "Bir notta maksimum bahsetme sayısı" canInvite: "Sunucu davet kodları oluşturabilir" @@ -1982,25 +1989,26 @@ _role: 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" + canManageAvatarDecorations: "Avatar süslerini yönet" + driveCapacity: "Drive kapasitesi" + maxFileSize: "Yükleyebileceğin maksimum dosya boyutu" + alwaysMarkNsfw: "Dosyaları her zaman NSFW olarak işaretle" 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ı" + noteEachClipsMax: "Bir klip içindeki maksimum not 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ı" + canSearchUsers: "Kullanıcı arama" canUseTranslator: "Çevirmen kullanımı" - avatarDecorationLimit: "Uygulanabilecek maksimum avatar süsleme sayısı" + avatarDecorationLimit: "Maksimum avatar süsü 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" @@ -2009,7 +2017,7 @@ _role: 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." + uploadableFileTypes_caption2: "Bazı dosya türleri algılanamayabilir. Bu tür dosyalara izin vermek için, spesifikasyona {x} ekle." noteDraftLimit: "Sunucu notlarının olası taslak sayısı" watermarkAvailable: "Filigran işlevinin kullanılabilirliği" _condition: @@ -2053,12 +2061,12 @@ _ffVisibility: 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." + emailAddressInfo: "Lütfen E-Posta adresini gir. Bu adres kamuya açık hale getirilmeyecek." + emailSent: "Onay e-postası E-Posta adresine ({email}) gönderilmiştir. Hesap oluşturma işlemini tamamlamak için e-postadaki bağlantıya tıkla." _accountDelete: 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." + mayTakeTime: "Hesap silme işlemi kaynak yoğun bir işlem olduğundan, oluşturduğun içerik miktarına ve yüklediğin 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önderilecek." requestAccountDelete: "Hesap silme talebi" started: "Silme işlemi başlatıldı." inProgress: "Silme işlemi şu anda devam ediyor." @@ -2072,7 +2080,7 @@ _ad: 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." + enterEmail: "Kayıt olurken kullandığın E-Posta adresini gir. Şifreni sıfırlayabileceğin bir bağlantı bu adrese gönderilecek." 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: @@ -2087,7 +2095,7 @@ _email: 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." + installWarn: "Güvenilir olmayan eklentileri yükleme." manage: "Eklentileri yönet" viewSource: "Kaynak görüntüle" viewLog: "Günlüğü göster" @@ -2100,11 +2108,11 @@ _preferencesBackups: 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." + applyConfirm: "Bu cihaza “{name}” yedeklemesini cidden uygulamak istiyor musun? 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." + deleteConfirm: "{name} yedeklemesini silmek ister misin?" + renameConfirm: "Bu yedeğin adını “{old}” den “{new}” ye değiştirmek ister misin?" + noBackups: "Yedekleme mevcut değil. “Yeni yedekleme oluştur” seçeneğini kullanarak bu sunucudaki istemci ayarlarınızı yedekleyebilirsin." createdAt: "Oluşturulma tarihi: {date} {time}" updatedAt: "Güncelleme tarihi: {date} {time}" cannotLoad: "Yükleme başarısız" @@ -2168,7 +2176,7 @@ _instanceMute: heading: "Sessize alınacak sunucuların listesi" _theme: explore: "Temaları Keşfedin" - install: "Bir tema yükleyin" + install: "Bir tema yükle" manage: "Temaları yönet" code: "Tema kodu" copyThemeCode: "Tema kodunu kopyala" @@ -2177,7 +2185,7 @@ _theme: installedThemes: "Yüklü temalar" builtinThemes: "Yerleşik temalar" instanceTheme: "Sunucu teması" - alreadyInstalled: "Bu tema zaten yüklenmiştir." + alreadyInstalled: "Bu tema zaten yüklenmiş." invalid: "Bu temanın biçimi geçersizdir." make: "Bir tema oluşturun" base: "Base" @@ -2196,8 +2204,8 @@ _theme: 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?" + importInfo: "Buraya tema kodunu girersen, onu tema düzenleyicisine aktarabilirsin." + deleteConstantConfirm: "{const} sabitini cidden silmek istiyor musun?" keys: accent: "Aksan" bg: "Arka plan" @@ -2246,18 +2254,18 @@ _soundSettings: 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." + driveFileError: "Ses yüklenemedi. Lütfen ayarları değiştir." _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" + future: "Gelecek" + justNow: "Şimdi" + secondsAgo: "{n} sn" + minutesAgo: "{n} dk" + hoursAgo: "{n} sa" + daysAgo: "{n} gün" + weeksAgo: "{n} hafta" + monthsAgo: "{n} ay" + yearsAgo: "{n} yıl" + invalid: "Geçersiz" _timeIn: seconds: "{n} saniye içinde" minutes: "{n} dakika içinde" @@ -2271,8 +2279,9 @@ _time: minute: "Dakika(lar)" hour: "Saat(ler)" day: "Gün(ler)" + month: "Ay" _2fa: - alreadyRegistered: "2 faktörlü kimlik doğrulama cihazını zaten kaydettiniz." + alreadyRegistered: "2fa kimlik doğrulama cihazını zaten kaydettin." 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." @@ -2280,15 +2289,15 @@ _2fa: 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." + step4: "Bundan sonra, gelecekteki tüm oturum açma girişimlerinde bu tür bir oturum açma jetonu istenecek." 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." + securityKeyInfo: "Parmak izi veya PIN kimlik doğrulamasının yanı sıra, hesabını daha da güvenli hale getirmek için FIDO2'yi destekleyen donanım güvenlik anahtarları aracılığıyla kimlik doğrulama da ayarlayabilirsin." 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?" + removeKeyConfirm: "{name} anahtarını cidden silmek istiyor musun?" 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." @@ -2296,43 +2305,43 @@ _2fa: 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." + backupCodesDescription: "İki faktörlü kimlik doğrulama uygulamasını kullanamaz hale gelmen durumunda, bu kodları kullanarak hesabınıza erişebilirsin. Her kod yalnızca bir kez kullanılabilir. Lütfen bu kodları güvenli bir yerde sakla." 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." + backupCodesExhaustedWarning: "Tüm yedek kodlar kullanıldı. İki faktörlü kimlik doğrulama uygulamana erişimini kaybedersen, bu hesaba erişemezsin. Lütfen iki faktörlü kimlik doğrulamayı yeniden yapılandır." moreDetailedGuideHere: "İşte ayrıntılı kılavuz" _permissions: - "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" + "read:account": "Hesap bilgilerini gör" + "write:account": "Hesap bilgilerini düzenle" + "read:blocks": "Engellenen kullanıcıların listesini görüntüle" + "write:blocks": "Engellenen kullanıcılar listeni düzenle" + "read:drive": "Drive dosyalarına ve klasörlerine eriş" + "write:drive": "Drive dosyalarını ve klasörlerini düzenle veya sil" + "read:favorites": "Favoriler listeni görüntüle" + "write:favorites": "Favoriler listeni düzenle" + "read:following": "Takip ettiğin kişilerle ilgili bilgileri görüntüle" "write:following": "Diğer hesapları takip et veya takipten çıkar" - "read:messaging": "Sohbetlerinizi görüntüleyin" + "read:messaging": "Sohbetlerini görüntüle" "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" + "read:mutes": "Sessize alınan kullanıcıların listesini görüntüle" + "write:mutes": "Sessize alınan kullanıcıların listesini düzenle" "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" + "read:notifications": "Bildirimlerini görüntüle" + "write:notifications": "Bildirimlerini yönet" + "read:reactions": "Tepkilerini görüntüle" + "write:reactions": "Tepkilerini düzenle" "write:votes": "Ankete oy verin" - "read:pages": "Sayfalarınızı görüntüleyin" - "write:pages": "Sayfalarınızı düzenleyin veya silin" + "read:pages": "Sayfalarını görüntüle" + "write:pages": "Sayfalarını düzenle veya sil" "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:user-groups": "Kullanıcı gruplarını görüntüle" + "write:user-groups": "Kullanıcı gruplarını düzenle veya sil" + "read:channels": "Kanallarını görüntüle" + "write:channels": "Kanallarını düzenle" "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:gallery-likes": "Beğendiğin galeri gönderilerinin listesini görüntüle" + "write:gallery-likes": "Beğendiğin galeri gönderilerinin listesini düzenle" "read:flash": "Oynat" "write:flash": "Oyunları Düzenle" "read:flash-likes": "Beğenilen Oyunların listesini görüntüle" @@ -2342,7 +2351,7 @@ _permissions: "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:user-ips": "Kullanıcı IP adreslerini görüntüle" "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" @@ -2364,8 +2373,8 @@ _permissions: "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:avatar-decorations": "Avatar süslerini yönetebilir" + "read:admin:avatar-decorations": "Avatar süslerini 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" @@ -2374,8 +2383,8 @@ _permissions: "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" + "write:admin:drive": "Kullanıcı Drive'ını yönet" + "read:admin:drive": "Kullanıcı Drive 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" @@ -2390,7 +2399,7 @@ _permissions: _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?" + shareAccessAsk: "Bu uygulamanın hesabınıza erişmesine izin vermek istediğinden emin misin?" 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." @@ -2399,7 +2408,7 @@ _auth: 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." + byClickingYouWillBeRedirectedToThisUrl: "Erişim izni verildiğinde, otomatik olarak aşağıdaki URL'ye yönlendirileceksin." _antennaSources: all: "Tüm notlar" homeTimeline: "Takip edilen kullanıcıların notları" @@ -2419,7 +2428,7 @@ _widgets: instanceInfo: "Sunucu Bilgisi" memo: "Yapışkan notlar" notifications: "Bildirimler" - timeline: "Timeline" + timeline: "Pano" calendar: "Takvim" trends: "Trend olan" clock: "Saat" @@ -2454,7 +2463,7 @@ _cw: _poll: noOnlyOneChoice: "En az iki seçenek gereklidir." choiceN: "Seçim {n}" - noMore: "Daha fazla seçenek ekleyemezsiniz." + noMore: "Daha fazla seçenek ekleyemezsin." canMultipleVote: "Birden fazla seçenek seçilmesine izin ver" expiration: "Anketi sonlandır" infinite: "Asla" @@ -2476,23 +2485,23 @@ _poll: _visibility: public: "Halka açık" publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır." - home: "Ana sayfa" - homeDescription: "Yalnızca ana zaman çizelgesine gönder" + home: "Pano" + homeDescription: "Yalnızca ana panoya gönder" followers: "Takipçiler" - followersDescription: "Sadece takipçilerinize görünür hale getirin" + followersDescription: "Sadece takipçilerine görünür hale getir" 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." + quitInspiteOfThereAreUnuploadedFilesConfirm: "Yüklenmemiş dosyalar var, bunları silip formu kapatmak ister misin?" + uploaderTip: "Dosya henüz yüklenmemiş. 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ırabilirsin. Notu yayınladığında 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?" + b: "Çevrende neler oluyor?" c: "Aklında ne var?" d: "Ne söylemek istiyorsun?" e: "Yazmaya başlayın..." @@ -2504,16 +2513,16 @@ _profile: 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." + metadataDescription: "Bunları kullanarak profilinde ek bilgi alanları görüntüleyebilirsin." 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." + changeAvatar: "Avatar değiştir" + changeBanner: "Banner değiştir" + verifiedLinkDescription: "Buraya profiline bağlantı içeren bir URL girerek, alanın yanında bir sahiplik doğrulama simgesi görüntülenebilir." + avatarDecorationMax: "En fazla {max} süs ekleyebilirsin." 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." + followedMessageDescription: "Abonelerin seni takip ettiklerinde görüntülenmesini istediğin kısa bir mesaj ayarlayabilirsin." + followedMessageDescriptionForLockedAccount: "Takip isteklerinin onay gerektirmesini ayarladıysan, bir takip isteğini kabul ettiğinde bu mesaj görüntülenir." _exportOrImport: allNotes: "Tüm notlar" favoritedNotes: "Favori notlar" @@ -2524,7 +2533,7 @@ _exportOrImport: 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" + withReplies: "İçe aktarılan kullanıcıların yanıtlarını panoya dahil edin" _charts: federation: "Federasyon" apRequest: "Talepler" @@ -2542,20 +2551,20 @@ _charts: _instanceCharts: requests: "Talepler" users: "Kullanıcı sayısındaki fark" - usersTotal: "Kümülatif kullanıcı sayısı" + usersTotal: "Toplam 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ı" + notesTotal: "Toplam not sayısı" + ff: "Takip / Takipçi sayısı farkı" + ffTotal: "Takip / Takipçi toplam sayısı" cacheSize: "Önbellek boyutundaki fark" - cacheSizeTotal: "Kümülatif önbellek boyutu" + cacheSizeTotal: "Önbelleğin toplam boyutu" files: "Dosya sayısındaki fark" filesTotal: "Toplam dosya sayısı" _timelines: - home: "Ana Sayfa" + home: "Pano" local: "Yerel" social: "Sosyal" - global: "Küresel" + global: "Global" _play: new: "Oyun Oluştur" edit: "Düzenle Oynat" @@ -2571,18 +2580,18 @@ _play: 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." + visibilityDescription: "Özel olarak ayarlamak, profilinde 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" + invalidNameTitle: "Belirtilen Sayfa URL 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" + viewPage: "Sayfalarını görüntüle" like: "Beğen" unlike: "Benzerlerini kaldır" my: "Benzerlerini kaldır" @@ -2619,7 +2628,7 @@ _pages: note: "Gömülü not" _note: id: "Not Kimliği" - idDescription: "Alternatif olarak notun URL'sini buraya yapıştırabilirsiniz." + idDescription: "Alternatif olarak notun URL buraya yapıştırabilirsin." detailed: "Ayrıntılı görünüm" _relayStatus: requesting: "Beklemede" @@ -2633,12 +2642,12 @@ _notification: youRenoted: "{name}'den Renote" youWereFollowed: "seni takip etti" youReceivedFollowRequest: "Bir takip isteği aldınız." - yourFollowRequestAccepted: "Takip isteğiniz kabul edildi." + yourFollowRequestAccepted: "Takip isteğin kabul edildi." pollEnded: "Anket sonuçları açıklandı." newNote: "Yeni not" unreadAntennaNote: "{name} anteni" roleAssigned: "Verilen rol" - chatRoomInvitationReceived: "Sohbet odasına davet edildiniz." + chatRoomInvitationReceived: "Sohbet odasına davet edildin." emptyPushNotificationMessage: "Push bildirimleri güncellendi" achievementEarned: "Achievement unlocked" testNotification: "Test bildirimi" @@ -2651,7 +2660,7 @@ _notification: followedBySomeUsers: "{n} kullanıcı tarafından takip ediliyor" flushNotification: "Bildirimleri temizle" exportOfXCompleted: "{x} ihracatı tamamlandı." - login: "Birisi oturum açtı" + login: "Biri oturum açtı" createToken: "Bir erişim jetonu oluşturuldu." createTokenDescription: "Eğer bilmiyorsanız, “{text}” aracılığıyla erişim jetonunu silin." _types: @@ -2670,7 +2679,7 @@ _notification: chatRoomInvitationReceived: "Sohbet odasına davet edildi" achievementEarned: "Başarı kilidi açıldı" exportCompleted: "İhracat işlemi tamamlandı." - login: "Giriş Yap" + login: "Oturum Aç" createToken: "Erişim jetonu oluştur" test: "Bildirim testi" app: "Bağlı uygulamalardan gelen bildirimler" @@ -2689,38 +2698,38 @@ _deck: 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" + swapUp: "Yukarıdaki sütunla değiştir" + swapDown: "Aşağıdaki sütunla değiştir" 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." + introduction2: "Ekranın sağındaki + işaretine tıklayarak istediğin zaman yeni sütunlar ekleyebilirsin." + widgetsIntroduction: "Lütfen sütun menüsünden “Widget'ları düzenle” seçeneğini seç ve bir widget ekle." 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." + usedAsMinWidthWhenFlexible: "“Otomatik genişlik ayarı” seçeneği etkinleştirildiğinde, bunun için minimum genişlik kullanılacak." flexible: "Otomatik genişlik ayarı" - enableSyncBetweenDevicesForProfiles: "Cihazlar arasında profil bilgilerinin senkronizasyonunu etkinleştirin" + enableSyncBetweenDevicesForProfiles: "Cihazlar arasında profil bilgilerinin senkronizasyonunu etkinleştir" _columns: main: "Ana" widgets: "Widget'lar" notifications: "Bildirimler" - tl: "Ana Sayfa" + tl: "Pano" antenna: "Antenler" list: "Liste" channel: "Kanal" mentions: "Bahsetmeler" direct: "Doğrudan notlar" - roleTimeline: "Rol Timeline" + roleTimeline: "Rol Pano" 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." + title: "Pano devre dışı bırakıldı" + description: "Mevcut rollerinle bu Pano kullanılamaz." _drivecleaner: orderBySizeDesc: "Azalan Dosya Boyutları" orderByCreatedAtAsc: "Yükselen Tarihler" @@ -2745,7 +2754,7 @@ _webhookSettings: 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?" + deleteConfirm: "Webhook'u silmek istediğinden emin misin?" testRemarks: "Anahtarın sağındaki düğmeyi tıklayarak sahte verilerle bir test Webhook gönderin." _abuseReport: _notificationRecipient: @@ -2761,7 +2770,7 @@ _abuseReport: keywords: "Anahtar kelimeler" notifiedUser: "Bildirilecek kullanıcılar" notifiedWebhook: "Kullanılacak webhook" - deleteConfirm: "Bildirim alıcısını silmek istediğinizden emin misiniz?" + deleteConfirm: "Bildirim alıcısını silmek istediğinden emin misin?" _moderationLogTypes: createRole: "Rol oluşturuldu" deleteRole: "Rol silindi" @@ -2777,11 +2786,11 @@ _moderationLogTypes: updateUserNote: "Moderasyon notu güncellendi" deleteDriveFile: "Dosya silindi" deleteNote: "Not silindi" - createGlobalAnnouncement: "Küresel duyuru oluşturuldu" + createGlobalAnnouncement: "Global duyuru oluşturuldu" createUserAnnouncement: "Kullanıcı duyurusu oluşturuldu" - updateGlobalAnnouncement: "Küresel duyuru güncellendi" + updateGlobalAnnouncement: "Global duyuru güncellendi" updateUserAnnouncement: "Kullanıcı duyurusu güncellendi" - deleteGlobalAnnouncement: "Küresel duyuru silindi" + deleteGlobalAnnouncement: "Global duyuru silindi" deleteUserAnnouncement: "Kullanıcı duyurusu silindi" resetPassword: "Şifreyi sıfırla" suspendRemoteInstance: "Uzak sunucu askıya alındı" @@ -2798,7 +2807,7 @@ _moderationLogTypes: updateAd: "Reklam güncellendi" createAvatarDecoration: "Avatar dekorasyonu oluşturuldu" updateAvatarDecoration: "Avatar dekorasyonu güncellendi" - deleteAvatarDecoration: "Avatar süslemesi silindi" + deleteAvatarDecoration: "Avatar süsü silindi" unsetUserAvatar: "Kullanıcı avatarı ayarlanmamış" unsetUserBanner: "Kullanıcı başlığı ayarlanmamış" createSystemWebhook: "Sistem Webhook oluşturuldu" @@ -2812,7 +2821,7 @@ _moderationLogTypes: deleteFlash: "Oyun silindi" deleteGalleryPost: "Galeri gönderisi silindi" deleteChatRoom: "Deleted Chat Room" - updateProxyAccountDescription: "Proxy hesabının açıklamasını güncelleyin" + updateProxyAccountDescription: "Proxy hesabının açıklamasını güncelle" _fileViewer: title: "Dosya ayrıntıları" type: "Dosya türü" @@ -2826,9 +2835,9 @@ _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?" + title: "Bu eklentiyi yüklemek ister misin?" _theme: - title: "Bu temayı yüklemek ister misiniz?" + title: "Bu temayı yüklemek ister misin?" _meta: base: "Temel renk şeması" _vendorInfo: @@ -2838,13 +2847,13 @@ _externalResourceInstaller: _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." + description: "Harici bir siteden veri yüklemek için yeterli bilgi yok. Lütfen girdiğin URL'yi kontrol et." _resourceTypeNotSupported: title: "Bu harici kaynak desteklenmemektedir." - description: "Bu harici kaynağın türü desteklenmemektedir. Lütfen site yöneticisiyle iletişime geçin." + description: "Bu harici kaynağın türü desteklenmemektedir. Lütfen site yöneticisiyle iletişime geç." _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." + fetchErrorDescription: "Harici siteyle iletişim sırasında bir hata oluştu. Tekrar denemen sorunu çözmezse, lütfen site yöneticisine başvur." 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" @@ -2854,13 +2863,13 @@ _externalResourceInstaller: 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." + description: "Eklenti yükleme sırasında bir sorun oluştu. Lütfen tekrar dene. 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." + description: "Tema yükleme sırasında bir sorun oluştu. Lütfen tekrar dene. Hata ayrıntıları Javascript konsolunda görüntülenebilir." _dataSaver: _media: title: "Medya yükleniyor" @@ -2870,7 +2879,7 @@ _dataSaver: 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." + description: "URL önizleme küçük resimleri artık yüklenmeyecek." _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." @@ -2889,8 +2898,8 @@ _reversi: 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" + waitingForOther: "Rakibin sırasını bekle" + waitingForMe: "Sıranı bekliyorsun" waitingBoth: "Hazır olun" ready: "Hazır" cancelReady: "Hazır değil" @@ -2918,9 +2927,9 @@ _reversi: freeMatch: "Ücretsiz Eşleştirme" lookingForPlayer: "Rakip aranıyor..." gameCanceled: "Oyun iptal edildi." - shareToTlTheGameWhenStart: "Oyun başlatıldığında zaman çizelgesinde paylaş" + shareToTlTheGameWhenStart: "Oyun başlatıldığında panoda paylaş" iStartedAGame: "Oyun başladı! #MisskeyReversi" - opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiştir." + opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş." allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)" disallowIrregularRules: "Düzensiz kurallar yok" showBoardLabels: "Tahtada satır ve sütun numaralarını göster" @@ -2932,7 +2941,7 @@ _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." + 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üleyebilirsin. 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)" @@ -2942,7 +2951,7 @@ _urlPreviewSetting: 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." + summaryProxyDescription: "Misskey'in kendisi değil, Summaly Proxy kullanarak önizlemeler oluştur." 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" @@ -2968,11 +2977,11 @@ _customEmojisManager: deleteSelectionRows: "Seçili satırları sil" deleteSelectionRanges: "Seçimdeki satırları sil" searchSettings: "Arama ayarları" - searchSettingCaption: "Ayrıntılı arama kriterleri belirleyin." + searchSettingCaption: "Ayrıntılı arama kriterleri belirle." 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." + registrationLogsCaption: "Emojileri güncellerken veya silerken günlükler görüntülenecek. Güncelleme veya silme işleminden sonra, yeni bir sayfaya geçildiğinde veya yeniden yüklendiğinde günlükler kaybolacak." 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" @@ -2990,43 +2999,43 @@ _customEmojisManager: 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" + markAsDeleteTargetRows: "Silinecek hedef olarak seçilen satırları işaretle" + markAsDeleteTargetRanges: "Seçimdeki satırları silinecek hedef olarak işaretle" 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?" + confirmMovePage: "Sayfaları taşımak ister misin?" + confirmChangeView: "Görüntüleme şeklini değiştirmek ister misn?" confirmUpdateEmojisDescription: "{count} Emoji'yi güncelle. Devam etmek istediğinden emin misin?" - confirmDeleteEmojisDescription: "İşaretli {count} Emoji(leri) silin. Devam etmek istediğinizden emin misiniz?" + confirmDeleteEmojisDescription: "İşaretli {count} Emoji(leri) silin. Devam etmek istediğinden emin misin?" 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." + confirmMovePageDesciption: "Bu sayfadaki Emojilerde değişiklikler yapılmış.\nSayfayı kaydetmeden terk ederseniz, bu sayfada yapılan tüm değişiklikler silinecek." dialogSelectRoleTitle: "Emojilerde rol setine göre arama yapın" _register: uploadSettingTitle: "Yükleme ayarları" - uploadSettingDescription: "Bu ekranda, Emoji yüklerken davranışı yapılandırabilirsiniz." + uploadSettingDescription: "Bu ekranda, Emoji yüklerken davranışı yapılandırabilirsin." 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?" + confirmRegisterEmojisDescription: "Listeden Emojileri yeni özel Emojiler olarak kaydet. Devam etmek istediğinden emin misin? (Aşırı yüklemeyi önlemek için, tek bir işlemde yalnızca {count} Emoji kaydedilebilir)" + confirmClearEmojisDescription: "Düzenlemeleri sil ve listeden Emojileri temizle. Devam etmek istediğinden emin misiniz?" + confirmUploadEmojisDescription: "Drive'a sürüklenip bırakılan {count} dosyayı yükle. Devam etmek istediğinden emin misin?" _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." + 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 belirt." + maxHeightWarn: "Maksimum yükseklik sınırı devre dışıdır (0). Bu istenmeyen bir durumsa, maksimum yüksekliği bir değer olarak ayarla." 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" + rounded: "Yuvarlak hale getir" 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." + codeGeneratedDescription: "Oluşturulan kodu web sitene yapıştırarak içeriği göm." _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." + title: "“Bu ekrana bir şey yapıştırın” tamamen bir aldatmaca." + description1: "Buraya bir şey yapıştırırsan, kötü niyetli bir kullanıcı hesabını ele geçirebilir veya kişisel bilgilerini ç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: @@ -3038,10 +3047,10 @@ _remoteLookupErrors: 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." + description: "Girdiğin URI ile ilgili bir sorun var. Lütfen URI'da kullanılamayan karakterler girip girmediğini kontrol et." _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." + description: "Bu sunucuyla iletişim kurulamadı. Sunucu kapalı olabilir. Ayrıca, geçersiz veya mevcut olmayan bir URI girmediğinizden emin ol." _responseInvalid: title: "Yanıt geçersiz" description: "Bu sunucuyla iletişim kurabildi, ancak elde edilen veriler yanlıştı." @@ -3050,7 +3059,7 @@ _remoteLookupErrors: 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." + testSiteKeyMessage: "Site ve gizli anahtarlar için test değerlerini girerek önizlemeyi kontrol edebilirsin.\nAyrıntılar için lütfen aşağıdaki sayfaya bak." _error: _requestFailed: title: "CAPTCHA isteği başarısız oldu" @@ -3065,7 +3074,7 @@ _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." + solution1: "Tarayıcını ve işletim sistemini en son sürüme güncelle." 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." @@ -3099,19 +3108,19 @@ _serverSetupWizard: 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." + 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östermelisin." 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: "Fediverse'e bağlanmak ister misin?" 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." + 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ıracak." 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." @@ -3119,39 +3128,39 @@ _serverSetupWizard: 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_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şlayabilirsin." 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." + text2: "Bu yazılımı gelecekte de geliştirmeye devam edebilmemiz için desteğini 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}" + abortConfirm: "Bazı dosyalar yüklenmedi, iptal etmek ister misin?" + doneConfirm: "Bazı dosyalar yüklenmedi, yine de devam etmek istiyor musun?" + maxFileSizeIsX: "Yükleyebileceğin 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." + tip: "Dosya henüz yüklenmediğinden, bu iletişim kutusu yüklemeden önce dosyayı onaylamanıza, yeniden adlandırmana, sıkıştırmana ve kırpmana olanak tanır. Hazır olduğunda, “Yükle” düğmesine basarak yüklemeyi başlatabilirsin." _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." + makeSureDisabledAdBlocker: "Reklam engelleyicini devre dışı bırak" + makeSureDisabledAdBlocker_description: "Reklam engelleyiciler performansı etkileyebilir, lütfen sisteminde veya tarayıcının özelliklerinde/uzantılarında reklam engelleyicilerin etkinleştirilmediğinden emin ol." 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." + makeSureDisabledCustomCss_description: "Stil geçersiz kılma, performansı etkileyebilir. Stil geçersiz kılan özel CSS veya uzantıların etkinleştirilmediğinden emin ol." 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." + tip: "Klip, notları gruplandırmanıza olanak tanıyan 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." + tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir pano 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?" + quitWithoutSaveConfirm: "Kaydedilmemiş değişiklikleri silmek ister misin?" driveFileTypeWarn: "Bu dosya desteklenmiyor" driveFileTypeWarnDescription: "Bir görüntü dosyası seçin" title: "Filigranı Düzenle" @@ -3164,10 +3173,10 @@ _watermarkEditor: type: "Tür" image: "Görseller" advanced: "Gelişmiş" + angle: "Açı" stripe: "Çizgiler" stripeWidth: "Çizgi genişliği" stripeFrequency: "Satır sayısı" - angle: "Açı" polkadot: "Nokta deseni" checker: "Kontrolcü" polkadotMainDotOpacity: "Ana noktanın opaklığı" @@ -3178,7 +3187,8 @@ _watermarkEditor: _imageEffector: title: "Effektler" addEffect: "Efektler Ekle" - discardChangesConfirm: "Gerçekten çıkmak istiyor musunuz? Kaydedilmemiş değişiklikleriniz var." + discardChangesConfirm: "Cidden çıkmak istiyor musun? Kaydedilmemiş değişikliklerin var." + nothingToConfigure: "Yapılandırılabilir seçenekler mevcut değildir." _fxs: chromaticAberration: "Renk Sapması" glitch: "Bozulma" @@ -3189,20 +3199,53 @@ _imageEffector: colorClamp: "Renk Sıkıştırma" colorClampAdvanced: "Renk Sıkıştırma (Gelişmiş)" distort: "Bozulma" - threshold: "İkilileştir" + threshold: "Binarize" zoomLines: "Doymuş hatlar" stripe: "Çizgiler" polkadot: "Nokta deseni" checker: "Denetleyici" blockNoise: "Gürültüyü Engelle" tearing: "Yırtılma" + _fxProps: + angle: "Açı" + scale: "Boyut" + size: "Boyut" + offset: "Pozisyon" + color: "Renk" + opacity: "Opaklık" + normalize: "Normalize" + amount: "Miktar" + lightness: "Hafiflet" + contrast: "Kontrast" + hue: "Hue" + brightness: "Parlaklık" + saturation: "Doygunluk" + max: "Maksimum" + min: "Minimum" + direction: "Yön" + phase: "Aşama" + frequency: "Sıklık" + strength: "Güç" + glitchChannelShift: "Kanal değişimi" + seed: "Tohum değeri" + redComponent: "Kırmızı bileşen" + greenComponent: "Yeşil bileşen" + blueComponent: "Mavi bileşen" + threshold: "Eşik" + centerX: "Merkez X" + centerY: "Merkez Y" + zoomLinesSmoothing: "Düzeltme" + zoomLinesSmoothingDescription: "Düzeltme ve yakınlaştırma çizgi genişliği birlikte kullanılamaz." + zoomLinesThreshold: "Zoom çizgi genişliği" + zoomLinesMaskSize: "Merkez çapı" + zoomLinesBlack: "Siyah yap" 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?" + deleteAreYouSure: "Taslağı silmek ister misin?" noDrafts: "Taslak yok" replyTo: "{user} notunu yanıtla" quoteOf: "{user} notuna alıntı" @@ -3211,3 +3254,6 @@ _drafts: restoreFromDraft: "Taslaktan geri yükle" restore: "Geri yükle" listDrafts: "Taslaklar Listesi" +_qr: + showTabTitle: "Ekran" + raw: "Metin" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 26843c6917..e33eff637c 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1648,3 +1648,13 @@ _watermarkEditor: type: "Тип" image: "Зображення" advanced: "Розширені" +_imageEffector: + _fxProps: + scale: "Розмір" + size: "Розмір" + color: "Колір" + opacity: "Непрозорість" + lightness: "Яскравість" +_qr: + showTabTitle: "Відображення" + raw: "Текст" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index bfa91cb0db..a17df99ad5 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -1102,3 +1102,10 @@ _watermarkEditor: type: "turi" image: "Rasmlar" advanced: "Murakkab" +_imageEffector: + _fxProps: + color: "Rang" + lightness: "Yoritish" +_qr: + showTabTitle: "Displey" + raw: "Matn" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 049e96b044..639cf92954 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1196,7 +1196,6 @@ 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" @@ -1817,7 +1816,6 @@ _widgets: _userList: chooseList: "Chọn danh sách" clicker: "clicker" - chat: "Trò chuyện" _cw: hide: "Ẩn" show: "Tải thêm" @@ -2043,7 +2041,6 @@ _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}" @@ -2091,3 +2088,15 @@ _watermarkEditor: image: "Hình ảnh" advanced: "Nâng cao" angle: "Góc" +_imageEffector: + _fxProps: + angle: "Góc" + scale: "Kích thước" + size: "Kích thước" + offset: "Vị trí" + color: "Màu sắc" + opacity: "Độ trong suốt" + lightness: "Độ sáng" +_qr: + showTabTitle: "Hiển thị" + raw: "Văn bản" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c6a7e15bf5..bf59023952 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -87,7 +87,7 @@ exportRequested: "导出请求已提交,这可能需要花一些时间,导 importRequested: "导入请求已提交,这可能需要花一点时间。" lists: "列表" noLists: "列表为空" -note: "帖子" +note: "发帖" notes: "帖子" following: "关注中" followers: "关注者" @@ -144,15 +144,15 @@ markAsSensitive: "标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容" enterFileName: "输入文件名" mute: "屏蔽" -unmute: "取消隐藏" -renoteMute: "隐藏转帖" -renoteUnmute: "解除隐藏转帖" -block: "屏蔽" -unblock: "取消屏蔽" +unmute: "取消屏蔽" +renoteMute: "屏蔽转帖" +renoteUnmute: "取消屏蔽转帖" +block: "拉黑" +unblock: "取消拉黑" suspend: "冻结" unsuspend: "解除冻结" -blockConfirm: "确定要屏蔽吗?" -unblockConfirm: "确定要取消屏蔽吗?" +blockConfirm: "确定要拉黑吗?" +unblockConfirm: "确定要取消拉黑吗?" suspendConfirm: "要冻结吗?" unsuspendConfirm: "要解除冻结吗?" selectList: "选择列表" @@ -244,22 +244,23 @@ mediaSilencedInstances: "已隐藏媒体文件的服务器" mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" federationAllowedHosts: "允许联合的服务器" federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" -muteAndBlock: "隐藏和屏蔽" -mutedUsers: "已隐藏用户" -blockedUsers: "已屏蔽的用户" +muteAndBlock: "屏蔽/拉黑" +mutedUsers: "已屏蔽用户" +blockedUsers: "已拉黑的用户" noUsers: "无用户" editProfile: "编辑资料" noteDeleteConfirm: "确定要删除该帖子吗?" pinLimitExceeded: "无法置顶更多了" done: "完成" processing: "正在处理" +preprocessing: "准备中" preview: "预览" default: "默认" defaultValueIs: "默认值: {value}" noCustomEmojis: "没有自定义表情符号" noJobs: "没有任务" federating: "联合中" -blocked: "已屏蔽" +blocked: "已拉黑" suspended: "停止投递" all: "全部" subscribing: "已订阅" @@ -303,7 +304,7 @@ explore: "发现" messageRead: "已读" noMoreHistory: "没有更多的历史记录" startChat: "开始聊天" -nUsersRead: "{n} 人已读" +nUsersRead: "{n}人已读" agreeTo: "勾选则表示已阅读并同意 {0}" agree: "同意" agreeBelow: "同意以下内容" @@ -395,7 +396,7 @@ basicInfo: "基本信息" pinnedUsers: "置顶用户" pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。" pinnedPages: "固定页面" -pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,一行一个。" +pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。" pinnedClipId: "置顶的便签 ID" pinnedNotes: "已置顶的帖子" hcaptcha: "hCaptcha" @@ -428,7 +429,7 @@ notifyAntenna: "开启通知" withFileAntenna: "仅带有附件的帖子" excludeNotesInSensitiveChannel: "排除敏感频道内的帖子" enableServiceworker: "启用 ServiceWorker" -antennaUsersDescription: "指定用户名,一行一个" +antennaUsersDescription: "指定用户名,用换行符进行分隔" caseSensitive: "区分大小写" withReplies: "包括回复" connectedTo: "您的账号已连到接以下第三方账号" @@ -460,7 +461,7 @@ moderationNote: "管理笔记" moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。" addModerationNote: "添加管理笔记" moderationLogs: "管理日志" -nUsersMentioned: "{n} 被提到" +nUsersMentioned: "{n}人投稿" securityKeyAndPasskey: "安全密钥或 Passkey" securityKey: "安全密钥" lastUsed: "最后使用:" @@ -477,7 +478,7 @@ notFoundDescription: "没有与指定 URL 对应的页面。" uploadFolder: "默认上传文件夹" markAsReadAllNotifications: "将所有通知标为已读" markAsReadAllUnreadNotes: "将所有帖子标记为已读" -markAsReadAllTalkMessages: "将所有聊天标记为已读" +markAsReadAllTalkMessages: "将所有私信标记为已读" help: "帮助" inputMessageHere: "在此键入信息" close: "关闭" @@ -597,7 +598,7 @@ recentUsed: "最近使用" install: "安装" uninstall: "卸载" installedApps: "已授权的应用" -nothing: "没有" +nothing: "无" installedDate: "授权日期" lastUsedDate: "最近使用" state: "状态" @@ -687,10 +688,10 @@ emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecureInfo: "使用 STARTTLS 时关闭。" testEmail: "邮件发送测试" -wordMute: "隐藏关键词" +wordMute: "屏蔽关键词" wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。" -hardWordMute: "隐藏硬关键词" -showMutedWord: "显示已隐藏的关键词" +hardWordMute: "强屏蔽关键词" +showMutedWord: "显示屏蔽关键词" hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。" regexpError: "正则表达式错误" regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:" @@ -779,7 +780,7 @@ emailVerified: "电子邮件地址已验证" noteFavoritesCount: "收藏的帖子数" pageLikesCount: "页面点赞次数" pageLikedCount: "页面被点赞次数" -contact: "联系人" +contact: "联系方式" useSystemFont: "使用系统默认字体" clips: "便签" experimentalFeatures: "实验性功能" @@ -800,7 +801,7 @@ showTitlebar: "显示标题栏" clearCache: "清除缓存" onlineUsersCount: "{n} 人在线" nUsers: "{n} 用户" -nNotes: "{n} 帖子" +nNotes: "{n}帖子" sendErrorReports: "发送错误报告" sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。错误信息包括操作系统版本、浏览器类型、行为历史记录等。" myTheme: "我的主题" @@ -824,7 +825,7 @@ youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。" newVersionOfClientAvailable: "新版本的客户端可用。" usageAmount: "使用量" capacity: "容量" -inUse: "已使用" +inUse: "使用中" editCode: "编辑代码" apply: "应用" receiveAnnouncementFromInstance: "从服务器接收通知" @@ -869,12 +870,12 @@ noMaintainerInformationWarning: "尚未设置管理员信息。" noInquiryUrlWarning: "尚未设置联络地址。" noBotProtectionWarning: "尚未设置 Bot 防御。" configure: "设置" -postToGallery: "发送到图库" +postToGallery: "创建新相册" postToHashtag: "投稿到这个标签" -gallery: "图库" +gallery: "相册" recentPosts: "最新发布" popularPosts: "热门投稿" -shareWithNote: "在帖子中分享" +shareWithNote: "分享到贴文" ads: "广告" expiration: "截止时间" startingperiod: "开始时间" @@ -885,7 +886,7 @@ middle: "中" low: "低" emailNotConfiguredWarning: "尚未设置电子邮件地址。" ratio: "比率" -previewNoteText: "预览文本" +previewNoteText: "预览正文" customCss: "自定义 CSS" customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用。" global: "全局" @@ -924,8 +925,8 @@ manageAccounts: "管理账户" makeReactionsPublic: "将回应设置为公开" makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。" classic: "经典" -muteThread: "隐藏帖子列表" -unmuteThread: "取消隐藏帖子列表" +muteThread: "屏蔽帖文串" +unmuteThread: "取消屏蔽帖文串" followingVisibility: "关注的人的公开范围" followersVisibility: "关注者的公开范围" continueThread: "查看更多帖子" @@ -948,17 +949,17 @@ searchByGoogle: "Google" instanceDefaultLightTheme: "服务器默认浅色主题" instanceDefaultDarkTheme: "服务器默认深色主题" instanceDefaultThemeDescription: "以对象格式输入主题代码" -mutePeriod: "隐藏期限" +mutePeriod: "屏蔽期限" period: "截止时间" indefinitely: "永久" -tenMinutes: "10 分钟" +tenMinutes: "10分钟" oneHour: "1 小时" -oneDay: "1 天" +oneDay: "1天" oneWeek: "1 周" -oneMonth: "1 个月" -threeMonths: "3 个月" +oneMonth: "1个月" +threeMonths: "3个月" oneYear: "1 年" -threeDays: "3 天" +threeDays: "3天" reflectMayTakeTime: "可能需要一些时间才能体现出效果。" failedToFetchAccountInformation: "获取账户信息失败" rateLimitExceeded: "已超过速率限制" @@ -967,8 +968,8 @@ cropImageAsk: "是否要裁剪图像?" cropYes: "去裁剪" cropNo: "就这样吧!" file: "文件" -recentNHours: "最近 {n} 小时" -recentNDays: "最近 {n} 天" +recentNHours: "最近{n}小时" +recentNDays: "最近{n}天" noEmailServerWarning: "电子邮件服务器未设置。" thereIsUnresolvedAbuseReportWarning: "有未解决的报告" recommended: "推荐" @@ -1054,6 +1055,7 @@ permissionDeniedError: "操作被拒绝" permissionDeniedErrorDescription: "本账户没有执行该操作的权限。" preset: "预设值" selectFromPresets: "从预设值中选择" +custom: "自定义" achievements: "成就" gotInvalidResponseError: "服务器无应答" gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。" @@ -1078,7 +1080,7 @@ postToTheChannel: "发布到频道" cannotBeChangedLater: "之后不能再更改。" reactionAcceptance: "接受表情回应" likeOnly: "仅点赞" -likeOnlyForRemote: "远程仅点赞" +likeOnlyForRemote: "全部(远程仅点赞)" nonSensitiveOnly: "仅限非敏感内容" nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)" rolesAssignedToMe: "指派给自己的角色" @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜 hiddenTags: "隐藏标签" hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。" notesSearchNotAvailable: "帖子检索不可用" +usersSearchNotAvailable: "用户检索不可用" license: "许可信息" unfavoriteConfirm: "确定要取消收藏吗?" myClips: "我的便签" @@ -1148,7 +1151,7 @@ youFollowing: "正在关注" preventAiLearning: "拒绝接受生成式 AI 的学习" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" options: "选项" -specifyUser: "用户指定" +specifyUser: "指定用户" lookupConfirm: "确定查询?" openTagPageConfirm: "确定打开话题标签页面?" specifyHost: "指定主机名" @@ -1243,7 +1246,7 @@ releaseToRefresh: "松开以刷新" refreshing: "刷新中" pullDownToRefresh: "下拉以刷新" useGroupedNotifications: "分组显示通知" -signupPendingError: "确认电子邮件时出现错误。链接可能已过期。" +emailVerificationFailedError: "确认电子邮件时出现错误。链接可能已过期。" cwNotationRequired: "在启用「隐藏内容」时必须输入注释" doReaction: "回应" code: "代码" @@ -1263,7 +1266,7 @@ replaying: "重播中" endReplay: "结束回放" copyReplayData: "复制回放数据" ranking: "排行榜" -lastNDays: "最近 {n} 天" +lastNDays: "最近{n}天" backToTitle: "返回标题" hemisphere: "居住地区" withSensitive: "显示包含敏感媒体的帖子" @@ -1314,6 +1317,7 @@ acknowledgeNotesAndEnable: "理解注意事项后再开启。" federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。" federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。" draft: "草稿" +draftsAndScheduledNotes: "草稿和定时发送" confirmOnReact: "发送回应前需要确认" reactAreYouSure: "要用「{emoji}」进行回应吗?" markAsSensitiveConfirm: "要将此媒体标记为敏感吗?" @@ -1341,6 +1345,8 @@ postForm: "投稿窗口" textCount: "字数" information: "关于" chat: "聊天" +directMessage: "私信" +directMessage_short: "消息" migrateOldSettings: "迁移旧设置信息" migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。" compress: "压缩" @@ -1358,41 +1364,60 @@ advice: "建议" realtimeMode: "实时模式" turnItOn: "开启" turnItOff: "关闭" -emojiMute: "隐藏表情符号" -emojiUnmute: "解除隐藏表情符号" -muteX: "隐藏{x}" -unmuteX: "解除隐藏{x}" +emojiMute: "屏蔽表情符号" +emojiUnmute: "取消屏蔽表情符号" +muteX: "屏蔽{x}" +unmuteX: "取消屏蔽{x}" abort: "中止" tip: "提示和技巧" redisplayAllTips: "重新显示所有的提示和技巧" hideAllTips: "隐藏所有的提示和技巧" defaultImageCompressionLevel: "默认图像压缩等级" defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。
较高的等级可以减少文件大小,但相对应的画质将会降低。" -inMinutes: "分" -inDays: "日" +defaultCompressionLevel: "默认压缩等级" +defaultCompressionLevel_description: "较低的等级可以保持质量,但会增加文件大小。
较高的等级可以减少文件大小,但相对应的质量将会降低。" +inMinutes: "分钟" +inDays: "天" safeModeEnabled: "已启用安全模式" pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。" customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。" themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。" +thankYouForTestingBeta: "感谢您协助测试 beta 版!" +createUserSpecifiedNote: "创建指定用户的帖子" +schedulePost: "定时发布" +scheduleToPostOnX: "预定在 {x} 发出" +scheduledToPostOnX: "已预定在 {x} 发出" +schedule: "定时" +scheduled: "定时" +_compression: + _quality: + high: "高质量" + medium: "中质量" + low: "低质量" + _size: + large: "大" + medium: "中" + small: "小" _order: newest: "从新到旧" oldest: "从旧到新" _chat: + messages: "消息" noMessagesYet: "还没有消息" newMessage: "新消息" individualChat: "私聊" individualChat_description: "可以与特定用户进行一对一聊天。" roomChat: "群聊" - roomChat_description: "可以进行多人聊天。\n就算用户未允许私聊,只要接受了邀请,仍可以聊天。" - createRoom: "创建房间" + roomChat_description: "支持多人同时进行消息交流。\n即使部分用户未开放私信权限,只要接受了邀请,仍可进行聊天。" + createRoom: "创建群组" inviteUserToChat: "邀请用户来开始聊天" - yourRooms: "已创建的房间" - joiningRooms: "已加入的房间" + yourRooms: "创建的群组" + joiningRooms: "已加入的群组" invitations: "邀请" noInvitations: "没有邀请" history: "历史" noHistory: "没有历史记录" - noRooms: "没有房间" + noRooms: "没有群组" inviteUser: "邀请用户" sentInvitations: "已发送的邀请" join: "加入" @@ -1403,16 +1428,16 @@ _chat: home: "首页" send: "发送" newline: "换行" - muteThisRoom: "静音此房间" - deleteRoom: "删除房间" + muteThisRoom: "屏蔽该群组" + deleteRoom: "删除群组" chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。" chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。" - chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。" - cannotChatWithTheUser: "无法与此用户聊天" + chatNotAvailableInOtherAccount: "对方的账户当前无法使用私信。" + cannotChatWithTheUser: "无法私信该用户" cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。" youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。" doYouAcceptInvitation: "要接受邀请吗?" - chatWithThisUser: "聊天" + chatWithThisUser: "私信" thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。" thisUserAllowsChatOnlyFromFollowing: "此用户仅接受关注的人发起的聊天。" thisUserAllowsChatOnlyFromMutualFollowing: "此用户仅接受互相关注的人发起的聊天。" @@ -1659,10 +1684,13 @@ _serverSettings: allowExternalApRedirect: "允许通过 ActivityPub 重定向查询" allowExternalApRedirect_description: "启用时,将允许其它服务器通过此服务器查询第三方内容,但有可能导致内容欺骗。" userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性" - userGeneratedContentsVisibilityForVisitor_description: "对于防止难以审核的不适当的远程内容等,通过自己的服务器无意中在互联网上公开等问题很有用。" + userGeneratedContentsVisibilityForVisitor_description: "对于防止诸如难以管理的不适当的远程内容通过自己的服务器意外地在互联网上公开等问题很有用。" userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。" restartServerSetupWizardConfirm_title: "要重新开始服务器初始设定向导吗?" restartServerSetupWizardConfirm_text: "现有的部分设定将重置。" + entrancePageStyle: "入口页面样式" + showTimelineForVisitor: "显示时间线" + showActivitiesForVisitor: "显示活动" _userGeneratedContentsVisibilityForVisitor: all: "全部公开" localOnly: "仅公开本地内容,隐藏远程内容" @@ -1887,7 +1915,7 @@ _achievements: description: "试图对网盘中的文件夹进行循环嵌套" _reactWithoutRead: title: "有好好读过吗?" - description: "在含有 100 字以上的帖子被发出三秒内做出回应" + description: "在含有100字以上的帖子被发出三秒内做出回应" _clickedClickHere: title: "点这里" description: "点了这里" @@ -1989,7 +2017,7 @@ _role: canUpdateBioMedia: "可以更新头像和横幅" pinMax: "帖子置顶数量限制" antennaMax: "可创建的最大天线数量" - wordMuteMax: "隐藏词的字数限制" + wordMuteMax: "屏蔽词的字数限制" webhookMax: "Webhook 创建数量限制" clipMax: "便签创建数量限制" noteEachClipsMax: "单个便签内的贴文数量限制" @@ -1999,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" canHideAds: "可以隐藏广告" canSearchNotes: "是否可以搜索帖子" + canSearchUsers: "使用用户检索" canUseTranslator: "使用翻译功能" avatarDecorationLimit: "可添加头像挂件的最大个数" canImportAntennas: "允许导入天线" @@ -2006,11 +2035,12 @@ _role: canImportFollowing: "允许导入关注列表" canImportMuting: "允许导入隐藏列表" canImportUserLists: "允许导入用户列表" - chatAvailability: "允许聊天" + chatAvailability: "允许私信" uploadableFileTypes: "可上传的文件类型" uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*)" uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。" noteDraftLimit: "可在服务器上创建多少草稿" + scheduledNoteLimit: "可同时创建的定时帖子数量" watermarkAvailable: "能否使用水印功能" _condition: roleAssignedTo: "已分配给手动角色" @@ -2076,9 +2106,9 @@ _forgotPassword: ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。" contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。" _gallery: - my: "我的图库" - liked: "喜欢的图片" - like: "喜欢" + my: "我的相册" + liked: "喜欢的相册" + like: "喜欢!" unlike: "取消喜欢" _email: _follow: @@ -2144,14 +2174,14 @@ _channel: edit: "编辑频道" setBanner: "设置横幅" removeBanner: "删除横幅" - featured: "热点" - owned: "管理中" + featured: "热门" + owned: "正在管理" following: "正在关注" - usersCount: "有 {n} 人参与" - notesCount: "有 {n} 个帖子" + usersCount: "有{n}人参与" + notesCount: "有{n}个帖子" nameAndDescription: "名称与描述" nameOnly: "仅名称" - allowRenoteToExternal: "允许在频道外转帖及引用" + allowRenoteToExternal: "允许转发到频道外和引用" _menuDisplay: sideFull: "横向" sideIcon: "横向(图标)" @@ -2162,10 +2192,10 @@ _wordMute: muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" muteWordsDescription2: "正则表达式用斜线包裹" _instanceMute: - instanceMuteDescription: "隐藏服务器中所有的帖子和转帖,包括这些服务器上用户的回复。" - instanceMuteDescription2: "一行一个" + instanceMuteDescription: "屏蔽服务器中所有的帖子和转帖,包括该服务器内用户的回复。" + instanceMuteDescription2: "通过换行符分隔进行设置" title: "下面实例中的帖子将被隐藏。" - heading: "已隐藏的服务器" + heading: "已屏蔽的服务器" _theme: explore: "寻找主题" install: "安装主题" @@ -2238,7 +2268,7 @@ _sfx: noteMy: "我的帖子" notification: "通知" reaction: "选择回应时" - chatMessage: "聊天信息" + chatMessage: "私信" _soundSettings: driveFile: "使用网盘内的音频" driveFileWarn: "选择网盘上的文件" @@ -2249,28 +2279,29 @@ _soundSettings: driveFileError: "无法读取声音。请更改设置。" _ago: future: "未来" - justNow: "最近" - secondsAgo: "{n} 秒前" - minutesAgo: "{n} 分前" - hoursAgo: "{n} 小时前" - daysAgo: "{n} 日前" - weeksAgo: "{n} 周前" - monthsAgo: "{n} 月前" - yearsAgo: "{n} 年前" + justNow: "刚刚" + secondsAgo: "{n}秒前" + minutesAgo: "{n}分钟前" + hoursAgo: "{n}小时前" + daysAgo: "{n}天前" + weeksAgo: "{n}周前" + monthsAgo: "{n}个月前" + yearsAgo: "{n}年前" invalid: "没有" _timeIn: seconds: "{n}秒后" - minutes: "{n} 分后" - hours: "{n} 小时后" + minutes: "{n}分钟后" + hours: "{n}小时后" days: "{n}天后" - weeks: "{n} 周后" - months: "{n} 月后" - years: "{n} 年后" + weeks: "{n}周后" + months: "{n}个月后" + years: "{n}年后" _time: second: "秒" - minute: "分" + minute: "分钟" hour: "小时" - day: "日" + day: "天" + month: "个月" _2fa: alreadyRegistered: "此设备已被注册" registerTOTP: "开始设置验证器" @@ -2303,36 +2334,36 @@ _2fa: _permissions: "read:account": "查看账户信息" "write:account": "更改帐户信息" - "read:blocks": "查看屏蔽列表" - "write:blocks": "编辑屏蔽列表" + "read:blocks": "查看黑名单" + "write:blocks": "编辑黑名单" "read:drive": "查看网盘" "write:drive": "管理网盘文件" "read:favorites": "查看收藏夹" "write:favorites": "编辑收藏夹" "read:following": "查看关注信息" "write:following": "关注/取消关注" - "read:messaging": "查看消息" + "read:messaging": "查看私信" "write:messaging": "撰写或删除消息" - "read:mutes": "查看隐藏列表" - "write:mutes": "编辑隐藏列表" + "read:mutes": "查看屏蔽列表" + "write:mutes": "编辑屏蔽列表" "write:notes": "撰写或删除帖子" "read:notifications": "查看通知" "write:notifications": "管理通知" "read:reactions": "查看回应" - "write:reactions": "回应操作" + "write:reactions": "编辑回应" "write:votes": "投票" "read:pages": "查看页面" - "write:pages": "操作页面" + "write:pages": "编辑页面" "read:page-likes": "查看喜欢的页面" - "write:page-likes": "操作喜欢的页面" + "write:page-likes": "管理喜欢的页面" "read:user-groups": "查看用户组" - "write:user-groups": "操作用户组" + "write:user-groups": "编辑用户组" "read:channels": "查看频道" "write:channels": "管理频道" - "read:gallery": "浏览图库" - "write:gallery": "操作图库" - "read:gallery-likes": "读取喜欢的图片" - "write:gallery-likes": "操作喜欢的图片" + "read:gallery": "浏览相册" + "write:gallery": "编辑相册" + "read:gallery-likes": "浏览喜欢的相册" + "write:gallery-likes": "管理喜欢的相册" "read:flash": "查看 Play" "write:flash": "编辑 Play" "read:flash-likes": "查看 Play 的点赞" @@ -2360,33 +2391,33 @@ _permissions: "read:admin:roles": "查看角色" "write:admin:relays": "编辑中继" "read:admin:relays": "查看中继" - "write:admin:invite-codes": "编辑邀请码" + "write:admin:invite-codes": "管理邀请码" "read:admin:invite-codes": "查看邀请码" - "write:admin:announcements": "编辑公告" + "write:admin:announcements": "管理公告" "read:admin:announcements": "查看公告" "write:admin:avatar-decorations": "编辑头像挂件" "read:admin:avatar-decorations": "查看头像挂件" "write:admin:federation": "编辑联合相关信息" "write:admin:account": "编辑用户账户" "read:admin:account": "查看用户相关情报" - "write:admin:emoji": "编辑表情文字" - "read:admin:emoji": "查看表情文字" + "write:admin:emoji": "编辑表情符号" + "read:admin:emoji": "查看表情符号" "write:admin:queue": "编辑作业队列" "read:admin:queue": "查看作业队列相关情报" "write:admin:promo": "运营推广说明" - "write:admin:drive": "编辑用户网盘" + "write:admin:drive": "管理用户网盘" "read:admin:drive": "查看用户网盘相关情报" "read:admin:stream": "使用管理员用的 Websocket API" - "write:admin:ad": "编辑广告" + "write:admin:ad": "管理广告" "read:admin:ad": "查看广告" "write:invite-codes": "生成邀请码" "read:invite-codes": "获取已发行的邀请码" - "write:clip-favorite": "编辑便签的点赞" + "write:clip-favorite": "管理喜欢的便签" "read:clip-favorite": "查看便签的点赞" "read:federation": "查看联合相关信息" "write:report-abuse": "举报用户" "write:chat": "撰写或删除消息" - "read:chat": "查看聊天" + "read:chat": "查看私信" _auth: shareAccessTitle: "应用程序授权许可" shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?" @@ -2405,7 +2436,7 @@ _antennaSources: homeTimeline: "已关注用户的帖子" users: "来自指定用户的帖子" userList: "来自指定列表中的帖子" - userBlacklist: "除掉已选择用户后所有的帖子" + userBlacklist: "过滤指定用户后的所有帖子" _weekday: sunday: "星期日" monday: "星期一" @@ -2445,7 +2476,7 @@ _widgets: chooseList: "选择列表" clicker: "点击器" birthdayFollowings: "今天是他们的生日" - chat: "聊天" + chat: "私信" _cw: hide: "隐藏" show: "查看更多" @@ -2453,26 +2484,26 @@ _cw: files: "{count} 个文件" _poll: noOnlyOneChoice: "需要至少两个选项" - choiceN: "选择 {n}" + choiceN: "选项{n}" noMore: "无法再添加更多了" - canMultipleVote: "允许多个投票" + canMultipleVote: "允许选择多个选项" expiration: "截止时间" infinite: "永久" at: "指定日期" after: "指定时间" deadlineDate: "截止日期" - deadlineTime: "小时" - duration: "时长" - votesCount: "{n} 票" + deadlineTime: "时间" + duration: "期限" + votesCount: "{n}票" totalVotes: "总票数 {n}" vote: "投票" showResult: "显示结果" voted: "已投票" closed: "已截止" - remainingDays: "{d} 天 {h} 小时后截止" + remainingDays: "{d}天{h}小时后截止" remainingHours: "{h} 小时 {m} 分后截止" - remainingMinutes: "{m} 分 {s} 秒后截止" - remainingSeconds: "{s} 秒后截止" + remainingMinutes: "{m}分{s}秒后截止" + remainingSeconds: "{s}秒后截止" _visibility: public: "公开" publicDescription: "您的帖子将出现在全局时间线上" @@ -2491,9 +2522,9 @@ _postForm: quotePlaceholder: "引用这个帖子..." channelPlaceholder: "发布到频道…" _placeholders: - a: "现在如何?" - b: "发生了什么?" - c: "你有什么想法?" + a: "现在怎么样?" + b: "想好发些什么了吗?" + c: "在想些什么呢?" d: "你想要发布些什么吗?" e: "请写下来吧" f: "等待您的发布..." @@ -2519,8 +2550,8 @@ _exportOrImport: favoritedNotes: "收藏的帖子" clips: "便签" followingList: "关注中" - muteList: "隐藏" - blockingList: "屏蔽" + muteList: "屏蔽" + blockingList: "拉黑" userLists: "列表" excludeMutingUsers: "排除屏蔽用户" excludeInactiveUsers: "排除不活跃用户" @@ -2566,7 +2597,7 @@ _play: editThisPage: "编辑此 Play" viewSource: "查看源代码" my: "我的 Play" - liked: "点赞的 Play" + liked: "喜欢的 Play" featured: "热门" title: "标题" script: "脚本" @@ -2583,7 +2614,7 @@ _pages: editThisPage: "编辑此页面" viewSource: "查看源代码" viewPage: "查看页面" - like: "赞" + like: "喜欢" unlike: "取消喜欢" my: "我的页面" liked: "喜欢的页面" @@ -2631,10 +2662,12 @@ _notification: youGotReply: "来自{name}的回复" youGotQuote: "来自{name}的引用" youRenoted: "来自{name}的转发" - youWereFollowed: "关注了你。" + youWereFollowed: "关注了你" youReceivedFollowRequest: "您有新的关注请求" yourFollowRequestAccepted: "您的关注请求已通过" pollEnded: "问卷调查结果已生成。" + scheduledNotePosted: "定时帖子已发布" + scheduledNotePostFailed: "定时帖子发布失败" newNote: "新的帖子" unreadAntennaNote: "天线 {name}" roleAssigned: "授予的角色" @@ -2714,7 +2747,7 @@ _deck: mentions: "提及" direct: "指定用户" roleTimeline: "角色时间线" - chat: "聊天" + chat: "私信" _dialog: charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}" charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}" @@ -2810,7 +2843,7 @@ _moderationLogTypes: deleteAccount: "删除了账户" deletePage: "删除了页面" deleteFlash: "删除了 Play" - deleteGalleryPost: "删除了图库稿件" + deleteGalleryPost: "删除相册内容" deleteChatRoom: "删除聊天室" updateProxyAccountDescription: "更新代理账户的简介" _fileViewer: @@ -3066,7 +3099,7 @@ _bootErrors: serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。" solution: "以下方法或许可以解决问题:" solution1: "将浏览器及操作系统更新到最新版本" - solution2: "禁用广告屏蔽插件" + solution2: "禁用广告拦截插件" solution3: "清除浏览器缓存" solution4: "(Tor Browser)将 dom.webaudio.enabled 设定为 true" otherOption: "其它选项" @@ -3160,14 +3193,16 @@ _watermarkEditor: opacity: "不透明度" scale: "大小" text: "文本" + qr: "二维码" position: "位置" + margin: "边距" type: "类型" image: "图片" advanced: "高级" + angle: "角度" stripe: "条纹" stripeWidth: "线条宽度" stripeFrequency: "线条数量" - angle: "角度" polkadot: "波点" checker: "检查" polkadotMainDotOpacity: "主波点的不透明度" @@ -3175,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "副波点的不透明度" polkadotSubDotRadius: "副波点的大小" polkadotSubDotDivisions: "副波点的数量" + leaveBlankToAccountUrl: "留空则为账户 URL" _imageEffector: title: "效果" addEffect: "添加效果" discardChangesConfirm: "丢弃当前设置并退出?" + nothingToConfigure: "还没有设置" _fxs: chromaticAberration: "色差" glitch: "故障" mirror: "镜像" invert: "反转颜色" grayscale: "黑白" + blur: "模糊" + pixelate: "马赛克" colorAdjust: "色彩校正" colorClamp: "颜色限制" colorClampAdvanced: "颜色限制(高级)" @@ -3196,6 +3235,43 @@ _imageEffector: checker: "检查" blockNoise: "块状噪点" tearing: "撕裂" + fill: "填充" + _fxProps: + angle: "角度" + scale: "大小" + size: "大小" + radius: "半径" + samples: "采样数" + offset: "位置" + 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: "变成黑色" + circle: "圆形" drafts: "草稿" _drafts: select: "选择草稿" @@ -3211,3 +3287,22 @@ _drafts: restoreFromDraft: "从草稿恢复" restore: "恢复" listDrafts: "草稿一览" + schedule: "定时发布" + listScheduledNotes: "定时发布列表" + cancelSchedule: "取消定时" +qr: "二维码" +_qr: + showTabTitle: "显示" + readTabTitle: "读取" + shareTitle: "{name} {acct}" + shareText: "请在 Fediverse 上关注我!" + chooseCamera: "选择相机" + cannotToggleFlash: "无法开关闪光灯" + turnOnFlash: "打开闪光灯" + turnOffFlash: "关闭闪光灯" + startQr: "重新打开二维码扫描器" + stopQr: "关闭二维码扫描器" + noQrCodeFound: "未找到二维码" + scanFile: "扫描设备上的图像" + raw: "文本" + mfm: "MFM" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 1a2aaa6a12..6f67be9741 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -75,7 +75,7 @@ receiveFollowRequest: "您有新的追隨請求" followRequestAccepted: "追隨請求已被接受" mention: "提及" mentions: "提及" -directNotes: "私訊" +directNotes: "指定使用者" importAndExport: "匯入與匯出" import: "匯入" export: "匯出" @@ -253,6 +253,7 @@ noteDeleteConfirm: "確定刪除此貼文嗎?" pinLimitExceeded: "不能置頂更多貼文了" done: "完成" processing: "處理中" +preprocessing: "準備中" preview: "預覽" default: "預設" defaultValueIs: "預設值:{value}" @@ -1054,6 +1055,7 @@ permissionDeniedError: "操作被拒絕" permissionDeniedErrorDescription: "此帳戶沒有執行這個操作的權限。" preset: "預設值" selectFromPresets: "從預設值中選擇" +custom: "自訂" achievements: "成就" gotInvalidResponseError: "伺服器的回應無效" gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。" @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "空格代表「以及」(AND),斜線包圍 hiddenTags: "隱藏標籤" hiddenTagsDescription: "設定的標籤不會在趨勢中顯示,換行可以設定多個標籤。" notesSearchNotAvailable: "無法使用搜尋貼文功能。" +usersSearchNotAvailable: "無法使用使用者搜尋功能。" license: "授權" unfavoriteConfirm: "要取消收錄我的最愛嗎?" myClips: "我的摘錄" @@ -1243,7 +1246,7 @@ releaseToRefresh: "放開以更新內容" refreshing: "載入更新中" pullDownToRefresh: "往下拉來更新內容" useGroupedNotifications: "分組顯示通知訊息" -signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。" +emailVerificationFailedError: "驗證您的電子郵件地址時出現問題。連結可能已過期。" cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。" doReaction: "做出反應" code: "程式碼" @@ -1314,6 +1317,7 @@ acknowledgeNotesAndEnable: "了解注意事項後再開啟。" federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。" federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。" draft: "草稿\n" +draftsAndScheduledNotes: "草稿與排定發布" confirmOnReact: "在做出反應前先確認" reactAreYouSure: "用「 {emoji} 」反應嗎?" markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?" @@ -1341,6 +1345,8 @@ postForm: "發文視窗" textCount: "字數" information: "關於" chat: "聊天" +directMessage: "直接訊息" +directMessage_short: "訊息" migrateOldSettings: "遷移舊設定資訊" migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。" compress: "壓縮" @@ -1368,16 +1374,35 @@ redisplayAllTips: "重新顯示所有「提示與技巧」" hideAllTips: "隱藏所有「提示與技巧」" defaultImageCompressionLevel: "預設的影像壓縮程度" defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。
高的話可以減少檔案大小,但是會降低畫質。" +defaultCompressionLevel: "預設的壓縮程度" +defaultCompressionLevel_description: "低的話可以保留品質,但是會增加檔案的大小。
高的話可以減少檔案大小,但是會降低品質。" inMinutes: "分鐘" inDays: "日" safeModeEnabled: "啟用安全模式" pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛都被停用。" customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。" themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。" +thankYouForTestingBeta: "感謝您協助驗證 beta 版!" +createUserSpecifiedNote: "建立使用者指定的筆記" +schedulePost: "排定發布" +scheduleToPostOnX: "排定在 {x} 發布" +scheduledToPostOnX: "已排定在 {x} 發布貼文" +schedule: "排定" +scheduled: "排定" +_compression: + _quality: + high: "高品質" + medium: "中品質" + low: "低品質" + _size: + large: "大" + medium: "中" + small: "小" _order: newest: "最新的在前" oldest: "最舊的在前" _chat: + messages: "訊息" noMessagesYet: "尚無訊息" newMessage: "新訊息" individualChat: "ㄧ對一聊天室" @@ -1663,6 +1688,9 @@ _serverSettings: userGeneratedContentsVisibilityForVisitor_description2: "包括伺服器接收到的遠端內容在內,無條件地將伺服器內所有內容公開到網際網路上是具有風險的。特別是對於不了解分散式架構特性的瀏覽者來說,他們可能會誤以為這些遠端內容是由該伺服器所創建的,因此需要特別留意。" restartServerSetupWizardConfirm_title: "要重新執行伺服器的初始設定精靈嗎?" restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。" + entrancePageStyle: "入口頁面的樣式" + showTimelineForVisitor: "顯示時間軸" + showActivitiesForVisitor: "顯示活動" _userGeneratedContentsVisibilityForVisitor: all: "全部公開\n" localOnly: "僅公開本地內容,遠端內容則不公開\n" @@ -1999,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" canHideAds: "不顯示廣告" canSearchNotes: "可否搜尋貼文" + canSearchUsers: "可使用使用者搜尋功能" canUseTranslator: "使用翻譯功能" avatarDecorationLimit: "頭像可掛上的最大裝飾數量" canImportAntennas: "允許匯入天線" @@ -2011,6 +2040,7 @@ _role: uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*)作為萬用字元進行指定。(例如:image/*)\n" uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。" noteDraftLimit: "伺服器端可建立的貼文草稿數量上限\n" + scheduledNoteLimit: "同時建立的排定發布數量" watermarkAvailable: "浮水印功能是否可用" _condition: roleAssignedTo: "手動指派角色完成" @@ -2231,7 +2261,7 @@ _theme: buttonHoverBg: "按鈕背景 (漂浮)" inputBorder: "輸入框邊框" badge: "徽章" - messageBg: "私訊背景" + messageBg: "聊天的背景" fgHighlighted: "突顯文字" _sfx: note: "貼文" @@ -2271,6 +2301,7 @@ _time: minute: "分鐘" hour: "小時" day: "日" + month: "個月" _2fa: alreadyRegistered: "此裝置已被註冊過了" registerTOTP: "開始設定驗證應用程式" @@ -2635,6 +2666,8 @@ _notification: youReceivedFollowRequest: "您有新的追隨請求" yourFollowRequestAccepted: "您的追隨請求已被核准" pollEnded: "問卷調查已產生結果" + scheduledNotePosted: "已排定發布貼文" + scheduledNotePostFailed: "排定發布貼文失敗了" newNote: "新的貼文" unreadAntennaNote: "天線 {name}" roleAssigned: "已授予角色" @@ -3160,14 +3193,16 @@ _watermarkEditor: opacity: "透明度" scale: "大小" text: "文字" + qr: "二維條碼" position: "位置" + margin: "邊界" type: "類型" image: "圖片" advanced: "進階" + angle: "角度" stripe: "條紋" stripeWidth: "線條寬度" stripeFrequency: "線條數量" - angle: "角度" polkadot: "波卡圓點" checker: "棋盤格" polkadotMainDotOpacity: "主圓點的不透明度" @@ -3175,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "子圓點的不透明度" polkadotSubDotRadius: "子圓點的尺寸" polkadotSubDotDivisions: "子圓點的數量" + leaveBlankToAccountUrl: "若留空則使用帳戶的 URL" _imageEffector: title: "特效" addEffect: "新增特效" discardChangesConfirm: "捨棄更改並退出嗎?" + nothingToConfigure: "無可設定的項目" _fxs: chromaticAberration: "色差" glitch: "異常雜訊效果" mirror: "鏡像" invert: "反轉色彩" grayscale: "黑白" + blur: "模糊" + pixelate: "馬賽克" colorAdjust: "色彩校正" colorClamp: "壓縮色彩" colorClampAdvanced: "壓縮色彩(進階)" @@ -3196,6 +3235,43 @@ _imageEffector: checker: "棋盤格" blockNoise: "阻擋雜訊" tearing: "撕裂" + fill: "填充" + _fxProps: + angle: "角度" + scale: "大小" + size: "大小" + radius: "半徑" + samples: "取樣數" + offset: "位置" + 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: "變成黑色" + circle: "圓形" drafts: "草稿\n" _drafts: select: "選擇草槁" @@ -3211,3 +3287,22 @@ _drafts: restoreFromDraft: "從草稿復原\n" restore: "還原" listDrafts: "草稿清單" + schedule: "排定發布" + listScheduledNotes: "排定發布列表" + cancelSchedule: "解除排定" +qr: "二維條碼" +_qr: + showTabTitle: "檢視" + readTabTitle: "讀取" + shareTitle: "{name} {acct}" + shareText: "請在聯邦宇宙追隨我吧!" + chooseCamera: "選擇相機" + cannotToggleFlash: "無法切換閃光燈" + turnOnFlash: "開啟閃光燈" + turnOffFlash: "關閉閃光燈" + startQr: "啟動條碼掃描器" + stopQr: "停止條碼掃描器" + noQrCodeFound: "找不到 QR code" + scanFile: "掃描在裝置上的影像" + raw: "文字" + mfm: "MFM" diff --git a/package.json b/package.json index 3195a538bf..772b822dd3 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "2025.8.0-alpha.6", + "version": "2025.10.0-beta.0", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@10.14.0", + "packageManager": "pnpm@10.17.1", "workspaces": [ "packages/frontend-shared", "packages/frontend", @@ -27,6 +27,7 @@ "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", + "cli": "cd packages/backend && pnpm cli", "init": "pnpm migrate", "migrate": "cd packages/backend && pnpm migrate", "revert": "cd packages/backend && pnpm revert", @@ -52,30 +53,31 @@ "lodash": "4.17.21" }, "dependencies": { - "cssnano": "7.1.0", - "esbuild": "0.25.8", + "cssnano": "7.1.1", + "esbuild": "0.25.10", "execa": "9.6.0", "fast-glob": "3.3.3", "glob": "11.0.3", "ignore-walk": "7.0.0", "js-yaml": "4.1.0", "postcss": "8.5.6", - "tar": "7.4.3", - "terser": "5.43.1", + "tar": "7.5.1", + "terser": "5.44.0", "typescript": "5.9.2" }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.1.0", - "@types/node": "22.17.1", - "@typescript-eslint/eslint-plugin": "8.39.0", - "@typescript-eslint/parser": "8.39.0", + "@types/js-yaml": "4.0.9", + "@types/node": "22.18.6", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "cross-env": "7.0.3", "cypress": "14.5.4", - "eslint": "9.33.0", - "globals": "16.3.0", + "eslint": "9.36.0", + "globals": "16.4.0", "ncp": "2.0.0", - "pnpm": "10.14.0", - "start-server-and-test": "2.0.13" + "pnpm": "10.17.1", + "start-server-and-test": "2.1.2" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.22.0" diff --git a/packages/backend/migration/1755168347001-PageCountInNote.js b/packages/backend/migration/1755168347001-PageCountInNote.js new file mode 100644 index 0000000000..9f1894ab2f --- /dev/null +++ b/packages/backend/migration/1755168347001-PageCountInNote.js @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class PageCountInNote1755168347001 { + name = 'PageCountInNote1755168347001' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "pageCount" smallint NOT NULL DEFAULT '0'`); + + // Update existing notes + // block_list CTE collects all page blocks on the pages including child blocks in the section blocks. + // The clipped_notes CTE counts how many distinct pages each note block is referenced in. + // Finally, we update the note table with the count of pages for each referenced note. + await queryRunner.query(` + WITH RECURSIVE block_list AS ( + ( + SELECT + page.id as page_id, + block as block + FROM page + CROSS JOIN LATERAL jsonb_array_elements(page.content) block + WHERE block->>'type' = 'note' OR block->>'type' = 'section' + ) + UNION ALL + ( + SELECT + block_list.page_id, + child_block AS block + FROM LATERAL ( + SELECT page_id, block + FROM block_list + WHERE block_list.block->>'type' = 'section' + ) block_list + CROSS JOIN LATERAL jsonb_array_elements(block_list.block->'children') child_block + WHERE child_block->>'type' = 'note' OR child_block->>'type' = 'section' + ) + ), + clipped_notes AS ( + SELECT + (block->>'note') AS note_id, + COUNT(distinct block_list.page_id) AS count + FROM block_list + WHERE block_list.block->>'type' = 'note' + GROUP BY block->>'note' + ) + UPDATE note + SET "pageCount" = clipped_notes.count + FROM clipped_notes + WHERE note.id = clipped_notes.note_id; + `); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "pageCount"`); + } +} diff --git a/packages/backend/migration/1755574887486-entrancePageStyle.js b/packages/backend/migration/1755574887486-entrancePageStyle.js new file mode 100644 index 0000000000..ba40764b94 --- /dev/null +++ b/packages/backend/migration/1755574887486-entrancePageStyle.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class EntrancePageStyle1755574887486 { + name = 'EntrancePageStyle1755574887486' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "clientOptions" jsonb NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "clientOptions"`); + } +} diff --git a/packages/backend/migration/1756062689648-NonCascadingPageEyeCatching.js b/packages/backend/migration/1756062689648-NonCascadingPageEyeCatching.js new file mode 100644 index 0000000000..8554cc4304 --- /dev/null +++ b/packages/backend/migration/1756062689648-NonCascadingPageEyeCatching.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class NonCascadingPageEyeCatching1756062689648 { + name = 'NonCascadingPageEyeCatching1756062689648' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa"`); + await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa"`); + await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1757823175259-sensitive-ad.js b/packages/backend/migration/1757823175259-sensitive-ad.js new file mode 100644 index 0000000000..46f0f270ab --- /dev/null +++ b/packages/backend/migration/1757823175259-sensitive-ad.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SensitiveAd1757823175259 { + name = 'SensitiveAd1757823175259' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" ADD "isSensitive" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "isSensitive"`); + } +} diff --git a/packages/backend/migration/1758677617888-scheduled-post.js b/packages/backend/migration/1758677617888-scheduled-post.js new file mode 100644 index 0000000000..b31313d9db --- /dev/null +++ b/packages/backend/migration/1758677617888-scheduled-post.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ScheduledPost1758677617888 { + name = 'ScheduledPost1758677617888' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index b62dd46790..07a80abc0f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -11,6 +11,7 @@ "start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", "revert": "pnpm typeorm migration:revert -d ormconfig.js", + "cli": "node ./built/boot/cli.js", "check:connect": "node ./scripts/check_connect.js", "build": "swc src -d built -D --strip-leading-paths", "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths", @@ -38,17 +39,17 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.12.0", - "@swc/core-darwin-x64": "1.12.0", + "@swc/core-darwin-arm64": "1.13.19", + "@swc/core-darwin-x64": "1.13.19", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.12.0", - "@swc/core-linux-arm64-gnu": "1.12.0", - "@swc/core-linux-arm64-musl": "1.12.0", - "@swc/core-linux-x64-gnu": "1.12.0", - "@swc/core-linux-x64-musl": "1.12.0", - "@swc/core-win32-arm64-msvc": "1.12.0", - "@swc/core-win32-ia32-msvc": "1.12.0", - "@swc/core-win32-x64-msvc": "1.12.0", + "@swc/core-linux-arm-gnueabihf": "1.13.19", + "@swc/core-linux-arm64-gnu": "1.13.19", + "@swc/core-linux-arm64-musl": "1.13.19", + "@swc/core-linux-x64-gnu": "1.13.19", + "@swc/core-linux-x64-musl": "1.13.19", + "@swc/core-win32-arm64-msvc": "1.13.19", + "@swc/core-win32-ia32-msvc": "1.13.19", + "@swc/core-win32-x64-msvc": "1.13.19", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.0.9", @@ -68,31 +69,31 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.826.0", - "@aws-sdk/lib-storage": "3.826.0", + "@aws-sdk/client-s3": "3.896.0", + "@aws-sdk/lib-storage": "3.895.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/multipart": "9.2.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.71", - "@nestjs/common": "11.1.3", - "@nestjs/core": "11.1.3", - "@nestjs/testing": "11.1.3", + "@misskey-dev/summaly": "5.2.3", + "@napi-rs/canvas": "0.1.80", + "@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.7", - "@swc/core": "1.12.0", + "@swc/cli": "0.7.8", + "@swc/core": "1.13.5", "@twemoji/parser": "16.0.0", "@types/redis-info": "3.0.3", "accepts": "1.3.8", @@ -102,29 +103,29 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.53.2", + "bullmq": "5.58.8", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", - "chalk": "5.4.1", - "chalk-template": "1.1.0", + "chalk": "5.6.2", + "chalk-template": "1.1.2", "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.3", + "fastify": "5.6.1", "fastify-raw-body": "5.0.0", "feed": "4.2.2", "file-type": "19.6.0", "fluent-ffmpeg": "2.1.3", - "form-data": "4.0.3", - "got": "14.4.7", + "form-data": "4.0.4", + "got": "14.4.9", "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.8.0", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", @@ -134,14 +135,14 @@ "jsonld": "8.3.3", "jsrsasign": "11.1.0", "juice": "11.0.1", - "meilisearch": "0.51.0", + "meilisearch": "0.53.0", "mfm-js": "0.25.0", - "microformats-parser": "2.0.3", + "microformats-parser": "2.0.4", "mime-types": "2.1.35", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", - "ms": "3.0.0-canary.1", - "nanoid": "5.1.5", + "ms": "3.0.0-canary.202508261828", + "nanoid": "5.1.6", "nested-property": "4.0.0", "node-fetch": "3.3.2", "nodemailer": "6.10.1", @@ -150,9 +151,9 @@ "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.4.0", + "otpauth": "9.4.1", "parse5": "7.3.0", - "pg": "8.16.0", + "pg": "8.16.3", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -174,25 +175,25 @@ "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.27.1", + "systeminformation": "5.27.10", "tinycolor2": "1.6.0", - "tmp": "0.2.3", + "tmp": "0.2.5", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.24", - "typescript": "5.8.3", + "typeorm": "0.3.27", + "typescript": "5.9.2", "ulid": "2.4.0", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.18.2", + "ws": "8.18.3", "xev": "3.0.2" }, "devDependencies": { "@jest/globals": "29.7.0", - "@nestjs/platform-express": "10.4.19", - "@sentry/vue": "9.28.0", + "@nestjs/platform-express": "10.4.20", + "@sentry/vue": "9.46.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", @@ -209,19 +210,19 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "22.15.31", - "@types/nodemailer": "6.4.17", + "@types/node": "22.18.6", + "@types/nodemailer": "6.4.19", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.15.4", + "@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.16.0", - "@types/semver": "7.7.0", + "@types/semver": "7.7.1", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", "@types/supertest": "6.0.3", @@ -230,11 +231,11 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.34.0", - "@typescript-eslint/parser": "8.34.0", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "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", @@ -242,6 +243,6 @@ "nodemon": "3.1.10", "pid-port": "1.0.2", "simple-oauth2": "5.1.0", - "supertest": "7.1.1" + "supertest": "7.1.4" } } diff --git a/packages/backend/src/boot/cli.ts b/packages/backend/src/boot/cli.ts new file mode 100644 index 0000000000..a5618f8152 --- /dev/null +++ b/packages/backend/src/boot/cli.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import 'reflect-metadata'; +import { EventEmitter } from 'node:events'; +import { NestFactory } from '@nestjs/core'; +import { CommandModule } from '@/cli/CommandModule.js'; +import { NestLogger } from '@/NestLogger.js'; +import { CommandService } from '@/cli/CommandService.js'; + +process.title = 'Misskey Cli'; + +Error.stackTraceLimit = Infinity; +EventEmitter.defaultMaxListeners = 128; + +const app = await NestFactory.createApplicationContext(CommandModule, { + logger: new NestLogger(), +}); + +const commandService = app.get(CommandService); + +const command = process.argv[2] ?? 'help'; + +switch (command) { + case 'help': { + console.log('Available commands:'); + console.log(' help - Displays this help message'); + console.log(' reset-captcha - Resets the captcha'); + break; + } + case 'ping': { + await commandService.ping(); + break; + } + case 'reset-captcha': { + await commandService.resetCaptcha(); + console.log('Captcha has been reset.'); + break; + } + default: { + console.error(`Unrecognized command: ${command}`); + console.error('Use "help" to see available commands.'); + process.exit(1); + } +} + +process.exit(0); diff --git a/packages/backend/src/cli/CommandModule.ts b/packages/backend/src/cli/CommandModule.ts new file mode 100644 index 0000000000..f4b1d25c18 --- /dev/null +++ b/packages/backend/src/cli/CommandModule.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Module } from '@nestjs/common'; +import { CoreModule } from '@/core/CoreModule.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { CommandService } from './CommandService.js'; + +@Module({ + imports: [ + GlobalModule, + CoreModule, + ], + providers: [ + CommandService, + ], + exports: [ + CommandService, + ], +}) +export class CommandModule {} diff --git a/packages/backend/src/cli/CommandService.ts b/packages/backend/src/cli/CommandService.ts new file mode 100644 index 0000000000..cdb2a9f382 --- /dev/null +++ b/packages/backend/src/cli/CommandService.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; + +@Injectable() +export class CommandService { + private logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + private metaService: MetaService, + ) { + } + + @bindThis + public async ping() { + console.log('pong'); + } + + @bindThis + public async resetCaptcha() { + await this.metaService.update({ + enableHcaptcha: false, + hcaptchaSiteKey: null, + hcaptchaSecretKey: null, + enableMcaptcha: false, + mcaptchaSitekey: null, + mcaptchaSecretKey: null, + mcaptchaInstanceUrl: null, + enableRecaptcha: false, + recaptchaSiteKey: null, + recaptchaSecretKey: null, + enableTurnstile: false, + turnstileSiteKey: null, + turnstileSecretKey: null, + enableTestcaptcha: false, + }); + } +} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index f71f1d7e34..fdf6fe18e2 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -7,6 +7,7 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; +import { type FastifyServerOptions } from 'fastify'; import type * as Sentry from '@sentry/node'; import type * as SentryVue from '@sentry/vue'; import type { RedisOptions } from 'ioredis'; @@ -27,6 +28,7 @@ type Source = { url?: string; port?: number; socket?: string; + trustProxy?: FastifyServerOptions['trustProxy']; chmodSocket?: string; disableHsts?: boolean; db: { @@ -118,6 +120,7 @@ export type Config = { url: string; port: number; socket: string | undefined; + trustProxy: FastifyServerOptions['trustProxy']; chmodSocket: string | undefined; disableHsts: boolean | undefined; db: { @@ -266,6 +269,7 @@ export function loadConfig(): Config { url: url.origin, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, + trustProxy: config.trustProxy, chmodSocket: config.chmodSocket, disableHsts: config.disableHsts, host, diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 248a9b8979..23ab8082ed 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -29,7 +29,7 @@ export class AiService { } @bindThis - public async detectSensitive(path: string): Promise { + public async detectSensitive(source: string | Buffer): Promise { try { if (isSupportedCpu === undefined) { isSupportedCpu = await this.computeIsSupportedCpu(); @@ -51,7 +51,7 @@ export class AiService { }); } - const buffer = await fs.promises.readFile(path); + const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source); const image = await tf.node.decodeImage(buffer, 3) as any; try { const predictions = await this.model.classify(image); diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 0c0c5d3a39..a30bff0fe4 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -78,6 +78,7 @@ import { ChannelFollowingService } from './ChannelFollowingService.js'; import { ChatService } from './ChatService.js'; import { RegistryApiService } from './RegistryApiService.js'; import { ReversiService } from './ReversiService.js'; +import { PageService } from './PageService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; @@ -227,6 +228,7 @@ const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService }; +const $PageService: Provider = { provide: 'PageService', useExisting: PageService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -379,6 +381,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ChatService, RegistryApiService, ReversiService, + PageService, ChartLoggerService, FederationChart, @@ -527,6 +530,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ChatService, $RegistryApiService, $ReversiService, + $PageService, $ChartLoggerService, $FederationChart, @@ -676,6 +680,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ChatService, RegistryApiService, ReversiService, + PageService, FederationChart, NotesChart, @@ -822,6 +827,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ChatService, $RegistryApiService, $ReversiService, + $PageService, $FederationChart, $NotesChart, diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index 6250d4d3a1..62a7d24afb 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -21,6 +21,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import type { PredictionType } from 'nsfwjs'; +import { isMimeImage } from '@/misc/is-mime-image.js'; export type FileInfo = { size: number; @@ -204,16 +205,7 @@ export class FileInfoService { return [sensitive, porn]; } - if ([ - 'image/jpeg', - 'image/png', - 'image/webp', - ].includes(mime)) { - const result = await this.aiService.detectSensitive(source); - if (result) { - [sensitive, porn] = judgePrediction(result); - } - } else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { + if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { const [outDir, disposeOutDir] = await createTempDir(); try { const command = FFmpeg() @@ -281,6 +273,23 @@ export class FileInfoService { } finally { disposeOutDir(); } + } else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) { + /* + * tfjs-node は限られた画像形式しか受け付けないため、sharp で PNG に変換する + * せっかくなので内部処理で使われる最大サイズの299x299に事前にリサイズする + */ + const png = await (await sharpBmp(source, mime)) + .resize(299, 299, { + withoutEnlargement: false, + }) + .rotate() + .flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす + .png() + .toBuffer(); + const result = await this.aiService.detectSensitive(png); + if (result) { + [sensitive, porn] = judgePrediction(result); + } } return [sensitive, porn]; diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 3ddfe52045..5714bde8bf 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,18 +36,24 @@ class HttpRequestServiceAgent extends http.Agent { } @bindThis - public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { - const socket = super.createConnection(options, callback) - .on('connect', () => { + public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { + const socket = super.createConnection(options, callback); + + if (socket == null) { + throw new Error('Failed to create socket'); + } + + socket.on('connect', () => { + if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); } } - }); + } + }); + return socket; } @@ -80,18 +81,24 @@ class HttpsRequestServiceAgent extends https.Agent { } @bindThis - public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { - const socket = super.createConnection(options, callback) - .on('connect', () => { + public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { + const socket = super.createConnection(options, callback); + + if (socket == null) { + throw new Error('Failed to create socket'); + } + + socket.on('connect', () => { + if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); } } - }); + } + }); + return socket; } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 1eefcfa054..b6acf4c5fb 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -56,6 +56,7 @@ import { trackPromise } from '@/misc/promise-tracker.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { CacheService } from '@/core/CacheService.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -192,6 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -221,6 +228,167 @@ export class NoteCreateService implements OnApplicationShutdown { this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount); } + @bindThis + public async fetchAndCreate(user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + isBot: MiUser['isBot']; + isCat: MiUser['isCat']; + }, data: { + createdAt: Date; + replyId: MiNote['id'] | null; + renoteId: MiNote['id'] | null; + fileIds: MiDriveFile['id'][]; + text: string | null; + cw: string | null; + visibility: string; + visibleUserIds: MiUser['id'][]; + channelId: MiChannel['id'] | null; + localOnly: boolean; + reactionAcceptance: MiNote['reactionAcceptance']; + poll: IPoll | null; + apMentions?: MinimumUser[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + }): Promise { + const visibleUsers = data.visibleUserIds.length > 0 ? await this.usersRepository.findBy({ + id: In(data.visibleUserIds), + }) : []; + + let files: MiDriveFile[] = []; + if (data.fileIds.length > 0) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: user.id, + fileIds: data.fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds: data.fileIds }) + .getMany(); + + if (files.length !== data.fileIds.length) { + throw new IdentifiableError('801c046c-5bf5-4234-ad2b-e78fc20a2ac7', 'No such file'); + } + } + + let renote: MiNote | null = null; + if (data.renoteId != null) { + // Fetch renote to note + renote = await this.notesRepository.findOne({ + where: { id: data.renoteId }, + relations: ['user', 'renote', 'reply'], + }); + + if (renote == null) { + throw new IdentifiableError('53983c56-e163-45a6-942f-4ddc485d4290', 'No such renote target'); + } else if (isRenote(renote) && !isQuote(renote)) { + throw new IdentifiableError('bde24c37-121f-4e7d-980d-cec52f599f02', 'Cannot renote pure renote'); + } + + // Check blocking + if (renote.userId !== user.id) { + const blockExist = await this.blockingsRepository.exists({ + where: { + blockerId: renote.userId, + blockeeId: user.id, + }, + }); + if (blockExist) { + throw new IdentifiableError('2b4fe776-4414-4a2d-ae39-f3418b8fd4d3', 'You have been blocked by the user'); + } + } + + if (renote.visibility === 'followers' && renote.userId !== user.id) { + // 他人のfollowers noteはreject + throw new IdentifiableError('90b9d6f0-893a-4fef-b0f1-e9a33989f71a', 'Renote target visibility'); + } else if (renote.visibility === 'specified') { + // specified / direct noteはreject + throw new IdentifiableError('48d7a997-da5c-4716-b3c3-92db3f37bf7d', 'Renote target visibility'); + } + + if (renote.channelId && renote.channelId !== data.channelId) { + // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック + // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する + const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); + if (renoteChannel == null) { + // リノートしたいノートが書き込まれているチャンネルが無い + throw new IdentifiableError('b060f9a6-8909-4080-9e0b-94d9fa6f6a77', 'No such channel'); + } else if (!renoteChannel.allowRenoteToExternal) { + // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 + throw new IdentifiableError('7e435f4a-780d-4cfc-a15a-42519bd6fb67', 'Channel does not allow renote to external'); + } + } + } + + let reply: MiNote | null = null; + if (data.replyId != null) { + // Fetch reply + reply = await this.notesRepository.findOne({ + where: { id: data.replyId }, + relations: ['user'], + }); + + if (reply == null) { + throw new IdentifiableError('60142edb-1519-408e-926d-4f108d27bee0', 'No such reply target'); + } else if (isRenote(reply) && !isQuote(reply)) { + throw new IdentifiableError('f089e4e2-c0e7-4f60-8a23-e5a6bf786b36', 'Cannot reply to pure renote'); + } else if (!await this.noteEntityService.isVisibleForMe(reply, user.id)) { + throw new IdentifiableError('11cd37b3-a411-4f77-8633-c580ce6a8dce', 'No such reply target'); + } else if (reply.visibility === 'specified' && data.visibility !== 'specified') { + throw new IdentifiableError('ced780a1-2012-4caf-bc7e-a95a291294cb', 'Cannot reply to specified note with different visibility'); + } + + // Check blocking + if (reply.userId !== user.id) { + const blockExist = await this.blockingsRepository.exists({ + where: { + blockerId: reply.userId, + blockeeId: user.id, + }, + }); + if (blockExist) { + throw new IdentifiableError('b0df6025-f2e8-44b4-a26a-17ad99104612', 'You have been blocked by the user'); + } + } + } + + if (data.poll) { + if (data.poll.expiresAt != null) { + if (data.poll.expiresAt.getTime() < Date.now()) { + throw new IdentifiableError('0c11c11e-0c8d-48e7-822c-76ccef660068', 'Poll expiration must be future time'); + } + } + } + + 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('bfa3905b-25f5-4894-b430-da331a490e4b', 'No such channel'); + } + } + + return this.create(user, { + createdAt: data.createdAt, + files: files, + poll: data.poll, + text: data.text, + reply, + renote, + cw: data.cw, + localOnly: data.localOnly, + reactionAcceptance: data.reactionAcceptance, + visibility: data.visibility, + visibleUsers, + channel, + apMentions: data.apMentions, + apHashtags: data.apHashtags, + apEmojis: data.apEmojis, + }); + } + @bindThis public async create(user: { id: MiUser['id']; diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index c43be96efa..7666407c1e 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -5,32 +5,18 @@ 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'; +import { QueueService } from '@/core/QueueService.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; -}; +export type NoteDraftOptions = Omit; @Injectable() export class NoteDraftService { @@ -56,6 +42,7 @@ export class NoteDraftService { private roleService: RoleService, private idService: IdService, private noteEntityService: NoteEntityService, + private queueService: QueueService, ) { } @@ -72,36 +59,43 @@ export class NoteDraftService { @bindThis public async create(me: MiLocalUser, data: NoteDraftOptions): Promise { //#region check draft limit + const policies = await this.roleService.getUserPolicies(me.id); const currentCount = await this.noteDraftsRepository.countBy({ userId: me.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) { + if (currentCount >= policies.noteDraftLimit) { throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts'); } + + if (data.isActuallyScheduled) { + const currentScheduledCount = await this.noteDraftsRepository.countBy({ + userId: me.id, + isActuallyScheduled: true, + }); + if (currentScheduledCount >= policies.scheduledNoteLimit) { + throw new IdentifiableError('c3275f19-4558-4c59-83e1-4f684b5fab66', 'Too many scheduled notes'); + } + } //#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); - } + await this.validate(me, data); + + const draft = await this.noteDraftsRepository.insertOne({ + ...data, + id: this.idService.gen(), + userId: me.id, + }); + + if (draft.scheduledAt && draft.isActuallyScheduled) { + this.schedule(draft); } - 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 { + public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial): Promise { const draft = await this.noteDraftsRepository.findOneBy({ id: draftId, userId: me.id, @@ -111,19 +105,36 @@ export class NoteDraftService { 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); + //#region check draft limit + const policies = await this.roleService.getUserPolicies(me.id); + + if (!draft.isActuallyScheduled && data.isActuallyScheduled) { + const currentScheduledCount = await this.noteDraftsRepository.countBy({ + userId: me.id, + isActuallyScheduled: true, + }); + if (currentScheduledCount >= policies.scheduledNoteLimit) { + throw new IdentifiableError('bacdf856-5c51-4159-b88a-804fa5103be5', 'Too many scheduled notes'); } } + //#endregion - const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data); + await this.validate(me, data); - return await this.noteDraftsRepository.save(appliedDraft); + const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update() + .set(data) + .where('id = :id', { id: draftId }) + .returning('*') + .execute() + .then((response) => response.raw[0]); + + this.clearSchedule(draftId).then(() => { + if (updatedDraft.scheduledAt != null && updatedDraft.isActuallyScheduled) { + this.schedule(updatedDraft); + } + }); + + return updatedDraft; } @bindThis @@ -138,6 +149,8 @@ export class NoteDraftService { } await this.noteDraftsRepository.delete(draft.id); + + this.clearSchedule(draftId); } @bindThis @@ -154,27 +167,20 @@ export class NoteDraftService { return draft; } - // 関連エンティティを取得し紐づける部分を共通化する @bindThis - public async checkAndSetDraftNoteOptions( + public async validate( 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; + data: Partial, + ): Promise { + if (data.pollExpiresAt != null) { + if (data.pollExpiresAt.getTime() < Date.now()) { + throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); + } } - let appliedDraft = draft; - //#region visibleUsers let visibleUsers: MiUser[] = []; - if (data.visibleUserIds != null) { + if (data.visibleUserIds != null && data.visibleUserIds.length > 0) { visibleUsers = await this.usersRepository.findBy({ id: In(data.visibleUserIds), }); @@ -184,7 +190,7 @@ export class NoteDraftService { //#region files let files: MiDriveFile[] = []; const fileIds = data.fileIds ?? null; - if (fileIds != null) { + if (fileIds != null && fileIds.length > 0) { files = await this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId AND file.id IN (:...fileIds)', { userId: me.id, @@ -288,27 +294,37 @@ export class NoteDraftService { } } //#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; + @bindThis + public async schedule(draft: MiNoteDraft): Promise { + if (!draft.isActuallyScheduled) return; + if (draft.scheduledAt == null) return; + if (draft.scheduledAt.getTime() <= Date.now()) return; - return appliedDraft; + const delay = draft.scheduledAt.getTime() - Date.now(); + this.queueService.postScheduledNoteQueue.add(draft.id, { + noteDraftId: draft.id, + }, { + delay, + removeOnComplete: { + age: 3600 * 24 * 7, // keep up to 7 days + count: 30, + }, + removeOnFail: { + age: 3600 * 24 * 7, // keep up to 7 days + count: 100, + }, + }); + } + + @bindThis + public async clearSchedule(draftId: MiNoteDraft['id']): Promise { + const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']); + for (const job of jobs) { + if (job.data.noteDraftId === draftId) { + await job.remove(); + } + } } } diff --git a/packages/backend/src/core/PageService.ts b/packages/backend/src/core/PageService.ts new file mode 100644 index 0000000000..7f0e5c7ccc --- /dev/null +++ b/packages/backend/src/core/PageService.ts @@ -0,0 +1,223 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { + type NotesRepository, + MiPage, + type PagesRepository, + MiDriveFile, + type UsersRepository, + MiNote, +} from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; +import type { MiUser } from '@/models/User.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +export interface PageBody { + title: string; + name: string; + summary: string | null; + content: Array>; + variables: Array>; + script: string; + eyeCatchingImage?: MiDriveFile | null; + font: string; + alignCenter: boolean; + hideTitleWhenPinned: boolean; +} + +@Injectable() +export class PageService { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private roleService: RoleService, + private moderationLogService: ModerationLogService, + private idService: IdService, + ) { + } + + @bindThis + public async create( + me: MiUser, + body: PageBody, + ): Promise { + await this.pagesRepository.findBy({ + userId: me.id, + name: body.name, + }).then(result => { + if (result.length > 0) { + throw new IdentifiableError('1a79e38e-3d83-4423-845b-a9d83ff93b61'); + } + }); + + const page = await this.pagesRepository.insertOne(new MiPage({ + id: this.idService.gen(), + updatedAt: new Date(), + title: body.title, + name: body.name, + summary: body.summary, + content: body.content, + variables: body.variables, + script: body.script, + eyeCatchingImageId: body.eyeCatchingImage ? body.eyeCatchingImage.id : null, + userId: me.id, + visibility: 'public', + alignCenter: body.alignCenter, + hideTitleWhenPinned: body.hideTitleWhenPinned, + font: body.font, + })); + + const referencedNotes = this.collectReferencedNotes(page.content); + if (referencedNotes.length > 0) { + await this.notesRepository.increment({ id: In(referencedNotes) }, 'pageCount', 1); + } + + return page; + } + + @bindThis + public async update( + me: MiUser, + pageId: MiPage['id'], + body: Partial, + ): Promise { + await this.db.transaction(async (transaction) => { + const page = await transaction.findOne(MiPage, { + where: { + id: pageId, + }, + lock: { mode: 'for_no_key_update' }, + }); + + if (page == null) { + throw new IdentifiableError('66aefd3c-fdb2-4a71-85ae-cc18bea85d3f'); + } + if (page.userId !== me.id) { + throw new IdentifiableError('d0017699-8256-46f1-aed4-bc03bed73616'); + } + + if (body.name != null) { + await transaction.findBy(MiPage, { + id: Not(pageId), + userId: me.id, + name: body.name, + }).then(result => { + if (result.length > 0) { + throw new IdentifiableError('d05bfe24-24b6-4ea2-a3ec-87cc9bf4daa4'); + } + }); + } + + await transaction.update(MiPage, page.id, { + updatedAt: new Date(), + title: body.title, + name: body.name, + summary: body.summary === undefined ? page.summary : body.summary, + content: body.content, + variables: body.variables, + script: body.script, + alignCenter: body.alignCenter, + hideTitleWhenPinned: body.hideTitleWhenPinned, + font: body.font, + eyeCatchingImageId: body.eyeCatchingImage === undefined ? undefined : (body.eyeCatchingImage?.id ?? null), + }); + + console.log("page.content", page.content); + + if (body.content != null) { + const beforeReferencedNotes = this.collectReferencedNotes(page.content); + const afterReferencedNotes = this.collectReferencedNotes(body.content); + + const removedNotes = beforeReferencedNotes.filter(noteId => !afterReferencedNotes.includes(noteId)); + const addedNotes = afterReferencedNotes.filter(noteId => !beforeReferencedNotes.includes(noteId)); + + if (removedNotes.length > 0) { + await transaction.decrement(MiNote, { id: In(removedNotes) }, 'pageCount', 1); + } + if (addedNotes.length > 0) { + await transaction.increment(MiNote, { id: In(addedNotes) }, 'pageCount', 1); + } + } + }); + } + + @bindThis + public async delete(me: MiUser, pageId: MiPage['id']): Promise { + await this.db.transaction(async (transaction) => { + const page = await transaction.findOne(MiPage, { + where: { + id: pageId, + }, + lock: { mode: 'pessimistic_write' }, // same lock level as DELETE + }); + + if (page == null) { + throw new IdentifiableError('66aefd3c-fdb2-4a71-85ae-cc18bea85d3f'); + } + + if (!await this.roleService.isModerator(me) && page.userId !== me.id) { + throw new IdentifiableError('d0017699-8256-46f1-aed4-bc03bed73616'); + } + + await transaction.delete(MiPage, page.id); + + if (page.userId !== me.id) { + const user = await this.usersRepository.findOneByOrFail({ id: page.userId }); + this.moderationLogService.log(me, 'deletePage', { + pageId: page.id, + pageUserId: page.userId, + pageUserUsername: user.username, + page, + }); + } + + const referencedNotes = this.collectReferencedNotes(page.content); + if (referencedNotes.length > 0) { + await transaction.decrement(MiNote, { id: In(referencedNotes) }, 'pageCount', 1); + } + }); + } + + collectReferencedNotes(content: MiPage['content']): string[] { + const referencingNotes = new Set(); + const recursiveCollect = (content: unknown[]) => { + for (const contentElement of content) { + if (typeof contentElement === 'object' + && contentElement !== null + && 'type' in contentElement) { + if (contentElement.type === 'note' + && 'note' in contentElement + && typeof contentElement.note === 'string') { + referencingNotes.add(contentElement.note); + } + if (contentElement.type === 'section' + && 'children' in contentElement + && Array.isArray(contentElement.children)) { + recursiveCollect(contentElement.children); + } + } + } + }; + recursiveCollect(content); + return [...referencingNotes]; + } +} diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index b10b8e5899..ecd96261e0 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -16,11 +16,13 @@ import { RelationshipJobData, UserWebhookDeliverJobData, SystemWebhookDeliverJobData, + PostScheduledNoteJobData, } from '../queue/types.js'; import type { Provider } from '@nestjs/common'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; +export type PostScheduledNoteQueue = Bull.Queue; export type DeliverQueue = Bull.Queue; export type InboxQueue = Bull.Queue; export type DbQueue = Bull.Queue; @@ -41,6 +43,12 @@ const $endedPollNotification: Provider = { inject: [DI.config], }; +const $postScheduledNote: Provider = { + provide: 'queue:postScheduledNote', + useFactory: (config: Config) => new Bull.Queue(QUEUE.POST_SCHEDULED_NOTE, baseQueueOptions(config, QUEUE.POST_SCHEDULED_NOTE)), + inject: [DI.config], +}; + const $deliver: Provider = { provide: 'queue:deliver', useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), @@ -89,6 +97,7 @@ const $systemWebhookDeliver: Provider = { providers: [ $system, $endedPollNotification, + $postScheduledNote, $deliver, $inbox, $db, @@ -100,6 +109,7 @@ const $systemWebhookDeliver: Provider = { exports: [ $system, $endedPollNotification, + $postScheduledNote, $deliver, $inbox, $db, @@ -113,6 +123,7 @@ export class QueueModule implements OnApplicationShutdown { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -129,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown { await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), + this.postScheduledNoteQueue.close(), this.deliverQueue.close(), this.inboxQueue.close(), this.dbQueue.close(), diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 4be568b334..42782167bb 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -31,6 +31,7 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, + PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, @@ -44,6 +45,7 @@ import type * as Bull from 'bullmq'; export const QUEUE_TYPES = [ 'system', 'endedPollNotification', + 'postScheduledNote', 'deliver', 'inbox', 'db', @@ -92,6 +94,7 @@ export class QueueService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -103,6 +106,7 @@ export class QueueService { for (const def of REPEATABLE_SYSTEM_JOB_DEF) { this.systemQueue.upsertJobScheduler(def.name, { pattern: def.pattern, + immediately: false, }, { name: def.name, opts: { @@ -716,6 +720,7 @@ export class QueueService { switch (type) { case 'system': return this.systemQueue; case 'endedPollNotification': return this.endedPollNotificationQueue; + case 'postScheduledNote': return this.postScheduledNoteQueue; case 'deliver': return this.deliverQueue; case 'inbox': return this.inboxQueue; case 'db': return this.dbQueue; @@ -755,8 +760,8 @@ export class QueueService { @bindThis public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { const queue = this.getQueue(queueType); - const job: Bull.Job | null = await queue.getJob(jobId); - if (job) { + const job = await queue.getJob(jobId); + if (job != null) { if (job.finishedOn != null) { await job.retry(); } else { @@ -768,8 +773,8 @@ export class QueueService { @bindThis public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { const queue = this.getQueue(queueType); - const job: Bull.Job | null = await queue.getJob(jobId); - if (job) { + const job = await queue.getJob(jobId); + if (job != null) { await job.remove(); } } @@ -802,8 +807,8 @@ export class QueueService { @bindThis public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { const queue = this.getQueue(queueType); - const job: Bull.Job | null = await queue.getJob(jobId); - if (job) { + const job = await queue.getJob(jobId); + if (job != null) { return this.packJobData(job); } else { throw new Error(`Job not found: ${jobId}`); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index cddfc0094e..6e4ac66e81 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -31,6 +31,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { NotificationService } from '@/core/NotificationService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; +// misskey-js の rolePolicies と同期すべし export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; @@ -43,6 +44,7 @@ export type RolePolicies = { canManageCustomEmojis: boolean; canManageAvatarDecorations: boolean; canSearchNotes: boolean; + canSearchUsers: boolean; canUseTranslator: boolean; canHideAds: boolean; driveCapacityMb: number; @@ -67,6 +69,7 @@ export type RolePolicies = { chatAvailability: 'available' | 'readonly' | 'unavailable'; uploadableFileTypes: string[]; noteDraftLimit: number; + scheduledNoteLimit: number; watermarkAvailable: boolean; }; @@ -82,6 +85,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canManageCustomEmojis: false, canManageAvatarDecorations: false, canSearchNotes: false, + canSearchUsers: true, canUseTranslator: true, canHideAds: false, driveCapacityMb: 100, @@ -98,20 +102,22 @@ export const DEFAULT_POLICIES: RolePolicies = { userEachUserListsLimit: 50, rateLimitFactor: 1, avatarDecorationLimit: 1, - canImportAntennas: true, - canImportBlocking: true, - canImportFollowing: true, - canImportMuting: true, - canImportUserLists: true, + canImportAntennas: false, + canImportBlocking: false, + canImportFollowing: false, + canImportMuting: false, + canImportUserLists: false, chatAvailability: 'available', uploadableFileTypes: [ 'text/plain', + 'text/csv', 'application/json', 'image/*', 'video/*', 'audio/*', ], noteDraftLimit: 10, + scheduledNoteLimit: 1, watermarkAvailable: true, }; @@ -402,6 +408,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)), @@ -435,6 +442,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { return [...set]; }), noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)), + scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)), watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)), }; } diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 9cf985b688..6714bda9a9 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -85,6 +85,7 @@ function generateDummyNote(override?: Partial): MiNote { renoteCount: 10, repliesCount: 5, clippedCount: 0, + pageCount: 0, reactions: {}, visibility: 'public', uri: null, @@ -243,7 +244,6 @@ export class WebhookTestService { case 'reaction': return; default: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const _exhaustiveAssertion: never = params.type; return; } @@ -326,7 +326,6 @@ export class WebhookTestService { break; } default: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const _exhaustiveAssertion: never = params.type; return; } @@ -411,7 +410,7 @@ export class WebhookTestService { name: user.name, username: user.username, host: user.host, - avatarUrl: user.avatarId == null ? null : user.avatarUrl, + avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? '', avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash, avatarDecorations: user.avatarDecorations.map(it => ({ id: it.id, diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts index 6bce2413fd..cfa983e766 100644 --- a/packages/backend/src/core/entities/ChatEntityService.ts +++ b/packages/backend/src/core/entities/ChatEntityService.ts @@ -54,12 +54,13 @@ export class ChatEntityService { const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); - const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = []; + // userは削除されている可能性があるのでnull許容 + const reactions: { user: Packed<'UserLite'> | null; reaction: string; }[] = []; for (const record of message.reactions) { const [userId, reaction] = record.split('/'); reactions.push({ - user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId), + user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId).catch(() => null), reaction, }); } @@ -76,7 +77,7 @@ export class ChatEntityService { toRoom: message.toRoomId ? (packedRooms?.get(message.toRoomId) ?? await this.packRoom(message.toRoom ?? message.toRoomId, me)) : undefined, fileId: message.fileId, file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, - reactions, + reactions: reactions.filter((r): r is { user: Packed<'UserLite'>; reaction: string; } => r.user != null), }; } @@ -108,6 +109,7 @@ export class ChatEntityService { } } + // TODO: packedUsersに削除されたユーザーもnullとして含める const [packedUsers, packedFiles, packedRooms] = await Promise.all([ this.userEntityService.packMany(users, me) .then(users => new Map(users.map(u => [u.id, u]))), @@ -183,12 +185,13 @@ export class ChatEntityService { const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); - const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = []; + // userは削除されている可能性があるのでnull許容 + const reactions: { user: Packed<'UserLite'> | null; reaction: string; }[] = []; for (const record of message.reactions) { const [userId, reaction] = record.split('/'); reactions.push({ - user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId), + user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId).catch(() => null), reaction, }); } @@ -202,7 +205,7 @@ export class ChatEntityService { toRoomId: message.toRoomId!, fileId: message.fileId, file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, - reactions, + reactions: reactions.filter((r): r is { user: Packed<'UserLite'>; reaction: string; } => r.user != null), }; } diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 02783dc450..2da614a120 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -109,6 +109,7 @@ export class MetaEntityService { maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, defaultLightTheme, defaultDarkTheme, + clientOptions: instance.clientOptions, ads: ads.map(ad => ({ id: ad.id, url: ad.url, @@ -116,6 +117,7 @@ export class MetaEntityService { ratio: ad.ratio, imageUrl: ad.imageUrl, dayOfWeek: ad.dayOfWeek, + isSensitive: ad.isSensitive ? true : undefined, })), notesPerOneAd: instance.notesPerOneAd, enableEmail: instance.enableEmail, diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts index 3ef8cdaa12..71e41a588d 100644 --- a/packages/backend/src/core/entities/NoteDraftEntityService.ts +++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts @@ -105,6 +105,8 @@ export class NoteDraftEntityService implements OnModuleInit { const packed: Packed<'NoteDraft'> = await awaitAll({ id: noteDraft.id, createdAt: this.idService.parse(noteDraft.id).date.toISOString(), + scheduledAt: noteDraft.scheduledAt?.getTime() ?? null, + isActuallyScheduled: noteDraft.isActuallyScheduled, userId: noteDraft.userId, user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), text: text, @@ -112,13 +114,13 @@ export class NoteDraftEntityService implements OnModuleInit { visibility: noteDraft.visibility, localOnly: noteDraft.localOnly, reactionAcceptance: noteDraft.reactionAcceptance, - visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined, - hashtag: noteDraft.hashtag ?? undefined, + visibleUserIds: noteDraft.visibleUserIds, + hashtag: noteDraft.hashtag, 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, + channelId: noteDraft.channelId, channel: channel ? { id: channel.id, name: channel.name, @@ -127,6 +129,12 @@ export class NoteDraftEntityService implements OnModuleInit { allowRenoteToExternal: channel.allowRenoteToExternal, userId: channel.userId, } : undefined, + poll: noteDraft.hasPoll ? { + choices: noteDraft.pollChoices, + multiple: noteDraft.pollMultiple, + expiresAt: noteDraft.pollExpiresAt?.toISOString(), + expiredAfter: noteDraft.pollExpiredAfter, + } : null, ...(opts.detail ? { reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, { @@ -138,13 +146,6 @@ export class NoteDraftEntityService implements OnModuleInit { detail: true, skipHide: opts.skipHide, })) : undefined, - - poll: noteDraft.hasPoll ? { - choices: noteDraft.pollChoices, - multiple: noteDraft.pollMultiple, - expiresAt: noteDraft.pollExpiresAt?.toISOString(), - expiredAfter: noteDraft.pollExpiredAfter, - } : undefined, } : {} ), }); diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index 46ec13704c..54ce4d472a 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -49,15 +49,12 @@ export class NoteReactionEntityService implements OnModuleInit { public async pack( src: MiNoteReaction['id'] | MiNoteReaction, me?: { id: MiUser['id'] } | null | undefined, - options?: { - withNote: boolean; - }, + options?: object, hints?: { packedUser?: Packed<'UserLite'> }, ): Promise> { const opts = Object.assign({ - withNote: false, }, options); const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src }); @@ -67,9 +64,6 @@ export class NoteReactionEntityService implements OnModuleInit { createdAt: this.idService.parse(reaction.id).date.toISOString(), user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me), type: this.reactionService.convertLegacyReaction(reaction.reaction), - ...(opts.withNote ? { - note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me), - } : {}), }; } @@ -77,16 +71,50 @@ export class NoteReactionEntityService implements OnModuleInit { public async packMany( reactions: MiNoteReaction[], me?: { id: MiUser['id'] } | null | undefined, - options?: { - withNote: boolean; - }, + options?: object, ): Promise[]> { const opts = Object.assign({ - withNote: false, }, options); const _users = reactions.map(({ user, userId }) => user ?? userId); const _userMap = await this.userEntityService.packMany(_users, me) .then(users => new Map(users.map(u => [u.id, u]))); return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) }))); } + + @bindThis + public async packWithNote( + src: MiNoteReaction['id'] | MiNoteReaction, + me?: { id: MiUser['id'] } | null | undefined, + options?: object, + hints?: { + packedUser?: Packed<'UserLite'> + }, + ): Promise> { + const opts = Object.assign({ + }, options); + + const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src }); + + return { + id: reaction.id, + createdAt: this.idService.parse(reaction.id).date.toISOString(), + user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me), + type: this.reactionService.convertLegacyReaction(reaction.reaction), + note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me), + }; + } + + @bindThis + public async packManyWithNote( + reactions: MiNoteReaction[], + me?: { id: MiUser['id'] } | null | undefined, + options?: object, + ): Promise[]> { + const opts = Object.assign({ + }, options); + const _users = reactions.map(({ user, userId }) => user ?? userId); + const _userMap = await this.userEntityService.packMany(_users, me) + .then(users => new Map(users.map(u => [u.id, u]))); + return Promise.all(reactions.map(reaction => this.packWithNote(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) }))); + } } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index e91fb9eb51..0e96237d32 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -21,7 +21,18 @@ import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([ + 'note', + 'mention', + 'reply', + 'renote', + 'renote:grouped', + 'quote', + 'reaction', + 'reaction:grouped', + 'pollEnded', + 'scheduledNotePosted', +] as (typeof groupedNotificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index d4769d24d4..47021359e1 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -471,8 +471,8 @@ export class UserEntityService implements OnModuleInit { (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; - const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null; - const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null; + const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : undefined; + const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : undefined; const unreadAnnouncements = isMe && isDetailed ? (await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({ createdAt: this.idService.parse(announcement.id).date.toISOString(), @@ -481,6 +481,7 @@ export class UserEntityService implements OnModuleInit { const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null; + // TODO: 例えば avatarUrl: true など間違った型を設定しても型エラーにならないのをどうにかする(ジェネリクス使わない方法で実装するしかなさそう?) const packed = { id: user.id, name: user.name, diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index ed47edff9b..ed7d5bfc3a 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -22,7 +22,7 @@ import { packedFollowingSchema } from '@/models/json-schema/following.js'; import { packedMutingSchema } from '@/models/json-schema/muting.js'; import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'; import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; -import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; +import { packedNoteReactionSchema, packedNoteReactionWithNoteSchema } from '@/models/json-schema/note-reaction.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js'; @@ -65,6 +65,7 @@ import { packedMetaDetailedSchema, packedMetaLiteSchema, } from '@/models/json-schema/meta.js'; +import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js'; @@ -92,6 +93,7 @@ export const refs = { Note: packedNoteSchema, NoteDraft: packedNoteDraftSchema, NoteReaction: packedNoteReactionSchema, + NoteReactionWithNote: packedNoteReactionWithNoteSchema, NoteFavorite: packedNoteFavoriteSchema, Notification: packedNotificationSchema, DriveFile: packedDriveFileSchema, @@ -133,6 +135,7 @@ export const refs = { MetaLite: packedMetaLiteSchema, MetaDetailedOnly: packedMetaDetailedOnlySchema, MetaDetailed: packedMetaDetailedSchema, + UserWebhook: packedUserWebhookSchema, SystemWebhook: packedSystemWebhookSchema, AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, ChatMessage: packedChatMessageSchema, diff --git a/packages/backend/src/models/Ad.ts b/packages/backend/src/models/Ad.ts index 108e991c70..0d402fcbe8 100644 --- a/packages/backend/src/models/Ad.ts +++ b/packages/backend/src/models/Ad.ts @@ -54,10 +54,17 @@ export class MiAd { length: 8192, nullable: false, }) public memo: string; + @Column('integer', { default: 0, nullable: false, }) public dayOfWeek: number; + + @Column('boolean', { + default: false, + }) + public isSensitive: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 1fc50cbd07..f8021a7a84 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -716,6 +716,11 @@ export class MiMeta { default: 90, // days }) public remoteNotesCleaningExpiryDaysForEachNotes: number; + + @Column('jsonb', { + default: { }, + }) + public clientOptions: Record; } export type SoftwareSuspension = { diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index ff46615729..26d5c1d535 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -114,6 +114,13 @@ export class MiNote { }) public clippedCount: number; + // The number of note page blocks referencing this note. + // This column is used by Remote Note Cleaning and manually updated rather than automatically with triggers. + @Column('smallint', { + default: 0, + }) + public pageCount: number; + @Column('jsonb', { default: {}, }) diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 6483748bc2..0ece02c943 100644 --- a/packages/backend/src/models/NoteDraft.ts +++ b/packages/backend/src/models/NoteDraft.ts @@ -126,7 +126,7 @@ export class MiNoteDraft { @JoinColumn() public channel: MiChannel | null; - // 以下、Pollについて追加 + //#region 以下、Pollについて追加 @Column('boolean', { default: false, @@ -151,13 +151,15 @@ export class MiNoteDraft { }) public pollExpiredAfter: number | null; - // ここまで追加 + //#endregion - constructor(data: Partial) { - if (data == null) return; + @Column('timestamp with time zone', { + nullable: true, + }) + public scheduledAt: Date | null; - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } + @Column('boolean', { + default: false, + }) + public isActuallyScheduled: boolean; } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 5764a307b0..7fa17e20fa 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -9,7 +9,9 @@ import { MiNote } from './Note.js'; import { MiAccessToken } from './AccessToken.js'; import { MiRole } from './Role.js'; import { MiDriveFile } from './DriveFile.js'; +import { MiNoteDraft } from './NoteDraft.js'; +// misskey-js の notificationTypes と同期すべし export type MiNotification = { type: 'note'; id: string; @@ -59,6 +61,16 @@ export type MiNotification = { createdAt: string; notifierId: MiUser['id']; noteId: MiNote['id']; +} | { + type: 'scheduledNotePosted'; + id: string; + createdAt: string; + noteId: MiNote['id']; +} | { + type: 'scheduledNotePostFailed'; + id: string; + createdAt: string; + noteDraftId: MiNoteDraft['id']; } | { type: 'receiveFollowRequest'; id: string; diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/Page.ts index 0b59e7a92c..d112a66c04 100644 --- a/packages/backend/src/models/Page.ts +++ b/packages/backend/src/models/Page.ts @@ -69,7 +69,7 @@ export class MiPage { public eyeCatchingImageId: MiDriveFile['id'] | null; @ManyToOne(type => MiDriveFile, { - onDelete: 'CASCADE', + onDelete: 'SET NULL', }) @JoinColumn() public eyeCatchingImage: MiDriveFile | null; diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts index b01b39a38b..d88ac23894 100644 --- a/packages/backend/src/models/json-schema/ad.ts +++ b/packages/backend/src/models/json-schema/ad.ts @@ -60,5 +60,10 @@ export const packedAdSchema = { optional: false, nullable: false, }, + isSensitive: { + type: 'boolean', + optional: false, + nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 2cd7620af0..a0e7d490b3 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -71,6 +71,10 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + clientOptions: { + type: 'object', + optional: false, nullable: false, + }, disableRegistration: { type: 'boolean', optional: false, nullable: false, @@ -191,6 +195,10 @@ export const packedMetaLiteSchema = { type: 'integer', optional: false, nullable: false, }, + isSensitive: { + type: 'boolean', + optional: true, nullable: false, + }, }, }, }, diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts index 504b263a6d..8144ac7b3b 100644 --- a/packages/backend/src/models/json-schema/note-draft.ts +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -23,7 +23,7 @@ export const packedNoteDraftSchema = { }, cw: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, userId: { type: 'string', @@ -37,27 +37,23 @@ export const packedNoteDraftSchema = { }, replyId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', - example: 'xxxxxxxxxx', }, renoteId: { type: 'string', - optional: true, nullable: true, + optional: false, 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', @@ -66,7 +62,7 @@ export const packedNoteDraftSchema = { }, visibleUserIds: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', optional: false, nullable: false, @@ -75,7 +71,7 @@ export const packedNoteDraftSchema = { }, fileIds: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', optional: false, nullable: false, @@ -93,11 +89,11 @@ export const packedNoteDraftSchema = { }, hashtag: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: true, }, poll: { type: 'object', - optional: true, nullable: true, + optional: false, nullable: true, properties: { expiresAt: { type: 'string', @@ -124,9 +120,8 @@ export const packedNoteDraftSchema = { }, channelId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', - example: 'xxxxxxxxxx', }, channel: { type: 'object', @@ -160,12 +155,20 @@ export const packedNoteDraftSchema = { }, localOnly: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, reactionAcceptance: { type: 'string', optional: false, nullable: true, enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], }, + scheduledAt: { + type: 'number', + optional: false, nullable: true, + }, + isActuallyScheduled: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/note-reaction.ts b/packages/backend/src/models/json-schema/note-reaction.ts index 95658ace1f..04c9f34232 100644 --- a/packages/backend/src/models/json-schema/note-reaction.ts +++ b/packages/backend/src/models/json-schema/note-reaction.ts @@ -10,7 +10,6 @@ export const packedNoteReactionSchema = { type: 'string', optional: false, nullable: false, format: 'id', - example: 'xxxxxxxxxx', }, createdAt: { type: 'string', @@ -28,3 +27,33 @@ export const packedNoteReactionSchema = { }, }, } as const; + +export const packedNoteReactionWithNoteSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + user: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + type: { + type: 'string', + optional: false, nullable: false, + }, + note: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 6de120c8d7..30e9c9327a 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -207,6 +207,36 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['scheduledNotePosted'], + }, + note: { + type: 'object', + ref: 'Note', + optional: false, nullable: false, + }, + }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['scheduledNotePostFailed'], + }, + noteDraft: { + type: 'object', + ref: 'NoteDraft', + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index c9cdbd5d89..b9000152d4 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, @@ -313,6 +317,10 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, + scheduledNoteLimit: { + type: 'integer', + optional: false, nullable: false, + }, watermarkAvailable: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/models/json-schema/user-webhook.ts b/packages/backend/src/models/json-schema/user-webhook.ts new file mode 100644 index 0000000000..8ea0991716 --- /dev/null +++ b/packages/backend/src/models/json-schema/user-webhook.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { webhookEventTypes } from '@/models/Webhook.js'; + +export const packedUserWebhookSchema = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'id', + optional: false, nullable: false, + }, + userId: { + type: 'string', + format: 'id', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + on: { + type: 'array', + items: { + type: 'string', + optional: false, nullable: false, + enum: webhookEventTypes, + }, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + secret: { + type: 'string', + optional: false, nullable: false, + }, + active: { + type: 'boolean', + optional: false, nullable: false, + }, + latestSentAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: true, + }, + latestStatus: { + type: 'integer', + optional: false, nullable: true, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 2b5f706ff9..b5fd38a7d7 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -65,7 +65,7 @@ export const packedUserLiteSchema = { avatarUrl: { type: 'string', format: 'url', - nullable: true, optional: false, + nullable: false, optional: false, }, avatarBlurhash: { type: 'string', @@ -465,11 +465,11 @@ export const packedMeDetailedOnlySchema = { }, isModerator: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, isAdmin: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, injectFeaturedNote: { type: 'boolean', @@ -591,7 +591,7 @@ export const packedMeDetailedOnlySchema = { }, mutedInstances: { type: 'array', - nullable: true, optional: false, + nullable: false, optional: false, items: { type: 'string', nullable: false, optional: false, @@ -609,6 +609,8 @@ export const packedMeDetailedOnlySchema = { quote: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig }, + scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, + scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index e01414cd53..e64882c4df 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; @@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor UserWebhookDeliverProcessorService, SystemWebhookDeliverProcessorService, EndedPollNotificationProcessorService, + PostScheduledNoteProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 7b64182754..642d3fc8ad 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -14,6 +14,7 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -85,6 +86,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; + private postScheduledNoteQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -94,6 +96,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, + private postScheduledNoteProcessorService: PostScheduledNoteProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, @@ -520,6 +523,21 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } //#endregion + + //#region post scheduled note + { + this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job)); + } else { + return this.postScheduledNoteProcessorService.process(job); + } + }, { + ...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE), + autorun: false, + }); + } + //#endregion } @bindThis @@ -534,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), + this.postScheduledNoteQueueWorker.run(), ]); } @@ -549,6 +568,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), + this.postScheduledNoteQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 7e146a7e03..625204b7ad 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -12,6 +12,7 @@ export const QUEUE = { INBOX: 'inbox', SYSTEM: 'system', ENDED_POLL_NOTIFICATION: 'endedPollNotification', + POST_SCHEDULED_NOTE: 'postScheduledNote', DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', diff --git a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts index da3bb804c2..36c34c753c 100644 --- a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts @@ -5,6 +5,7 @@ import { setTimeout } from 'node:timers/promises'; import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, IsNull, LessThan, QueryFailedError, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiMeta, MiNote, NotesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; @@ -24,18 +25,31 @@ export class CleanRemoteNotesProcessorService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.db) + private db: DataSource, + private idService: IdService, private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-notes'); } + @bindThis + private computeProgress(minId: string, maxId: string, cursorLeft: string) { + const minTs = this.idService.parse(minId).date.getTime(); + const maxTs = this.idService.parse(maxId).date.getTime(); + const cursorTs = this.idService.parse(cursorLeft).date.getTime(); + + return ((cursorTs - minTs) / (maxTs - minTs)) * 100; + } + @bindThis public async process(job: Bull.Job>): Promise<{ deletedCount: number; oldest: number | null; newest: number | null; - skipped?: boolean; + skipped: boolean; + transientErrors: number; }> { if (!this.meta.enableRemoteNotesCleaning) { this.logger.info('Remote notes cleaning is disabled, skipping...'); @@ -44,6 +58,7 @@ export class CleanRemoteNotesProcessorService { oldest: null, newest: null, skipped: true, + transientErrors: 0, }; } @@ -52,12 +67,10 @@ export class CleanRemoteNotesProcessorService { 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 + //#region queries + // 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)); // The condition for removing the notes. // The note must be: @@ -66,56 +79,95 @@ export class CleanRemoteNotesProcessorService { // - 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")' - ; + const removalCriteria = [ + 'note."id" < :newestLimit', + 'note."clippedCount" = 0', + 'note."pageCount" = 0', + 'note."userHost" IS NOT NULL', + 'NOT EXISTS (SELECT 1 FROM user_note_pining WHERE "noteId" = note."id")', + 'NOT EXISTS (SELECT 1 FROM note_favorite WHERE "noteId" = note."id")', + 'NOT EXISTS (SELECT 1 FROM note_reaction INNER JOIN "user" ON note_reaction."userId" = "user".id WHERE note_reaction."noteId" = note."id" AND "user"."host" IS NULL)', + ].join(' AND '); - // The initiator query contains the oldest ${MAX_NOTE_COUNT_PER_QUERY} remote non-clipped notes - const initiatorQuery = this.notesRepository.createQueryBuilder('note') + const minId = (await this.notesRepository.createQueryBuilder('note') + .select('MIN(note.id)', 'minId') + .where({ + id: LessThan(newestLimit), + userHost: Not(IsNull()), + replyId: IsNull(), + renoteId: IsNull(), + }) + .getRawOne<{ minId?: MiNote['id'] }>())?.minId; + + if (!minId) { + this.logger.info('No notes can possibly be deleted, skipping...'); + return { + deletedCount: 0, + oldest: null, + newest: null, + skipped: false, + transientErrors: 0, + }; + } + + // start with a conservative limit and adjust it based on the query duration + const minimumLimit = 10; + let currentLimit = 100; + let cursorLeft = '0'; + + const candidateNotesCteName = 'candidate_notes'; + + // tree walk down all root notes, short-circuit when the first unremovable note is found + const candidateNotesQueryBase = this.notesRepository.createQueryBuilder('note') + .select('note."id"', 'id') + .addSelect('note."replyId"', 'replyId') + .addSelect('note."renoteId"', 'renoteId') + .addSelect('note."id"', 'rootId') + .addSelect('TRUE', 'isRemovable') + .addSelect('TRUE', 'isBase') + .where('note."id" > :cursorLeft') + .andWhere(removalCriteria) + .andWhere({ replyId: IsNull(), renoteId: IsNull() }); + + const candidateNotesQueryInductive = this.notesRepository.createQueryBuilder('note') .select('note.id', 'id') - .where(removeCondition) - .andWhere('note.id > :cursor') - .orderBy('note.id', 'ASC') - .limit(MAX_NOTE_COUNT_PER_QUERY); + .addSelect('note."replyId"', 'replyId') + .addSelect('note."renoteId"', 'renoteId') + .addSelect('parent."rootId"', 'rootId') + .addSelect(removalCriteria, 'isRemovable') + .addSelect('FALSE', 'isBase') + .innerJoin(candidateNotesCteName, 'parent', 'parent."id" = note."replyId" OR parent."id" = note."renoteId"') + .where('parent."isRemovable" = TRUE'); - // 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 + // A note tree can be deleted if there are no unremovable rows with the same rootId. + // + // `candidate_notes` will have the following structure after recursive query (some columns omitted): + // After performing a LEFT JOIN with `candidate_notes` as `unremovable`, + // the note tree containing unremovable notes will be anti-joined. + // For removable rows, the `unremovable` columns will have `NULL` values. + // | id | rootId | isRemovable | + // |-----|--------|-------------| + // | aaa | aaa | TRUE | + // | bbb | aaa | FALSE | + // | ccc | aaa | FALSE | + // | ddd | ddd | TRUE | + // | eee | ddd | TRUE | + // | fff | fff | TRUE | + // | ggg | ggg | FALSE | + // + const candidateNotesQuery = this.db.createQueryBuilder() + .select(`"${candidateNotesCteName}"."id"`, 'id') + .addSelect('unremovable."id" IS NULL', 'isRemovable') + .addSelect(`BOOL_OR("${candidateNotesCteName}"."isBase")`, 'isBase') + .addCommonTableExpression( + `((SELECT "base".* FROM (${candidateNotesQueryBase.orderBy('note.id', 'ASC').limit(currentLimit).getQuery()}) AS "base") UNION ${candidateNotesQueryInductive.getQuery()})`, + candidateNotesCteName, + { recursive: true }, + ) + .from(candidateNotesCteName, candidateNotesCteName) + .leftJoin(candidateNotesCteName, 'unremovable', `unremovable."rootId" = "${candidateNotesCteName}"."rootId" AND unremovable."isRemovable" = FALSE`) + .groupBy(`"${candidateNotesCteName}"."id"`) + .addGroupBy('unremovable."id" IS NULL'); const stats = { deletedCount: 0, @@ -123,74 +175,107 @@ export class CleanRemoteNotesProcessorService { 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) { + let lowThroughputWarned = false; + let transientErrors = 0; + for (;;) { //#region check time const batchBeginAt = Date.now(); const elapsed = batchBeginAt - startAt; + const progress = this.computeProgress(minId, newestLimit, cursorLeft > minId ? cursorLeft : minId); + if (elapsed >= maxDuration) { - this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`); - job.log('Reached maximum duration, stopping cleaning.'); + job.log(`Reached maximum duration of ${maxDuration}ms, stopping... (last cursor: ${cursorLeft}, final progress ${progress}%)`); job.updateProgress(100); break; } - job.updateProgress((elapsed / maxDuration) * 100); + const wallClockUsage = elapsed / maxDuration; + if (wallClockUsage > 0.5 && progress < 50 && !lowThroughputWarned) { + const msg = `Not projected to finish in time! (wall clock usage ${wallClockUsage * 100}% at ${progress}%, current limit ${currentLimit})`; + this.logger.warn(msg); + job.log(msg); + lowThroughputWarned = true; + } + job.updateProgress(progress); //#endregion - // First, we fetch the initiator notes that are older than the newestLimit. - const initiatorNotes: { id: MiNote['id'] }[] = await initiatorQuery.setParameters({ cursor, newestLimit }).getRawMany(); + const queryBegin = performance.now(); + let noteIds = null; - // 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); + try { + noteIds = await candidateNotesQuery.setParameters( + { newestLimit, cursorLeft }, + ).getRawMany<{ id: MiNote['id'], isRemovable: boolean, isBase: boolean }>(); + } catch (e) { + if (currentLimit > minimumLimit && e instanceof QueryFailedError && e.driverError?.code === '57014') { + // Statement timeout (maybe suddenly hit a large note tree), reduce the limit and try again + // continuous failures will eventually converge to currentLimit == minimumLimit and then throw + currentLimit = Math.max(minimumLimit, Math.floor(currentLimit * 0.25)); + continue; + } + throw e; + } - 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.)'); + if (noteIds.length === 0) { + job.log('No more notes to clean.'); break; } - const notes: { id: MiNote['id'], initiatorId: MiNote['id'] }[] = await notesQuery.setParameters({ - initiatorIds: initiatorNotes.map(note => note.id), - newestLimit, - }).getRawMany(); + const queryDuration = performance.now() - queryBegin; + // try to adjust such that each query takes about 1~5 seconds and reasonable NodeJS heap so the task stays responsive + // this should not oscillate.. + if (queryDuration > 5000 || noteIds.length > 5000) { + currentLimit = Math.floor(currentLimit * 0.5); + } else if (queryDuration < 1000 && noteIds.length < 1000) { + currentLimit = Math.floor(currentLimit * 1.5); + } + // clamp to a sane range + currentLimit = Math.min(Math.max(currentLimit, minimumLimit), 5000); - cursor = newCursor; + const deletableNoteIds = noteIds.filter(result => result.isRemovable).map(result => result.id); + if (deletableNoteIds.length > 0) { + try { + await this.notesRepository.delete(deletableNoteIds); - 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; + for (const id of deletableNoteIds) { + 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; + } } - if (stats.newest === null || t > stats.newest) { - stats.newest = t; + + stats.deletedCount += deletableNoteIds.length; + } catch (e) { + // check for integrity violation errors (class 23) that might have occurred between the check and the delete + // we can safely continue to the next batch + if (e instanceof QueryFailedError && e.driverError?.code?.startsWith('23')) { + transientErrors++; + job.log(`Error deleting notes: ${e} (transient race condition?)`); + } else { + throw e; } } - - stats.deletedCount += notes.length; } - job.log(`Deleted ${notes.length} from ${initiatorNotes.length} initiators; ${Date.now() - batchBeginAt}ms`); + cursorLeft = noteIds.filter(result => result.isBase).reduce((max, { id }) => id > max ? id : max, cursorLeft); - 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; + job.log(`Deleted ${noteIds.length} notes; ${Date.now() - batchBeginAt}ms`); + + if (process.env.NODE_ENV !== 'test') { + await setTimeout(Math.min(1000 * 5, queryDuration)); // Wait a moment to avoid overwhelming the db } + }; - await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db + if (transientErrors > 0) { + const msg = `${transientErrors} transient errors occurred while cleaning remote notes. You may need a second pass to complete the cleaning.`; + this.logger.warn(msg); + job.log(msg); } - this.logger.succ('cleaning of remote notes completed.'); return { @@ -198,6 +283,7 @@ export class CleanRemoteNotesProcessorService { oldest: stats.oldest, newest: stats.newest, skipped: false, + transientErrors, }; } } diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 14a53e0c42..b643c2a6d0 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { DriveFilesRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; @@ -14,6 +14,7 @@ import type { MiNote } from '@/models/Note.js'; import { EmailService } from '@/core/EmailService.js'; import { bindThis } from '@/decorators.js'; import { SearchService } from '@/core/SearchService.js'; +import { PageService } from '@/core/PageService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserDeleteJobData } from '../types.js'; @@ -35,7 +36,11 @@ export class DeleteAccountProcessorService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + private driveService: DriveService, + private pageService: PageService, private emailService: EmailService, private queueLoggerService: QueueLoggerService, private searchService: SearchService, @@ -112,6 +117,28 @@ export class DeleteAccountProcessorService { this.logger.succ('All of files deleted'); } + { + // delete pages. Necessary for decrementing pageCount of notes. + while (true) { + const pages = await this.pagesRepository.find({ + where: { + userId: user.id, + }, + take: 100, + order: { + id: 1, + }, + }); + + if (pages.length === 0) { + break; + } + for (const page of pages) { + await this.pageService.delete(user, page.id); + } + } + } + { // Send email notification const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); if (profile.email && profile.emailVerified) { diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts new file mode 100644 index 0000000000..d0eaeee090 --- /dev/null +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { NoteDraftsRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { bindThis } from '@/decorators.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { PostScheduledNoteJobData } from '../types.js'; + +@Injectable() +export class PostScheduledNoteProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.noteDraftsRepository) + private noteDraftsRepository: NoteDraftsRepository, + + private noteCreateService: NoteCreateService, + private notificationService: NotificationService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('post-scheduled-note'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] }); + if (draft == null || draft.user == null || draft.scheduledAt == null || !draft.isActuallyScheduled) { + return; + } + + try { + const note = await this.noteCreateService.fetchAndCreate(draft.user, { + createdAt: new Date(), + fileIds: draft.fileIds, + poll: draft.hasPoll ? { + choices: draft.pollChoices, + multiple: draft.pollMultiple, + expiresAt: draft.pollExpiredAfter ? new Date(Date.now() + draft.pollExpiredAfter) : draft.pollExpiresAt ? new Date(draft.pollExpiresAt) : null, + } : null, + text: draft.text ?? null, + replyId: draft.replyId, + renoteId: draft.renoteId, + cw: draft.cw, + localOnly: draft.localOnly, + reactionAcceptance: draft.reactionAcceptance, + visibility: draft.visibility, + visibleUserIds: draft.visibleUserIds, + channelId: draft.channelId, + }); + + // await不要 + this.noteDraftsRepository.remove(draft); + + // await不要 + this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', { + noteId: note.id, + }); + } catch (err) { + this.notificationService.createNotification(draft.userId, 'scheduledNotePostFailed', { + noteDraftId: draft.id, + }); + } + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 757daea88b..1cb2b93918 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -109,6 +109,10 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; +export type PostScheduledNoteJobData = { + noteDraftId: string; +}; + export type SystemWebhookDeliverJobData = { type: T; content: SystemWebhookPayload; diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 23c085ee27..1286b4dad6 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown { @bindThis public async launch(): Promise { const fastify = Fastify({ - trustProxy: true, + trustProxy: this.config.trustProxy ?? true, logger: false, }); this.#fastify = fastify; @@ -238,30 +238,6 @@ export class ServerService implements OnApplicationShutdown { } }); - fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => { - const profile = await this.userProfilesRepository.findOneBy({ - emailVerifyCode: request.params.code, - }); - - if (profile != null) { - await this.userProfilesRepository.update({ userId: profile.userId }, { - emailVerified: true, - emailVerifyCode: null, - }); - - this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { - schema: 'MeDetailed', - includeSecrets: true, - })); - - reply.code(200).send('Verification succeeded! メールアドレスの認証に成功しました。'); - return; - } else { - reply.code(404).send('Verification failed. Please try again. メールアドレスの認証に失敗しました。もう一度お試しください'); - return; - } - }); - fastify.register(this.clientServerService.createServer); this.streamingApiServerService.attach(fastify.server); diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 32818003ad..57d74ef2b1 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -176,6 +176,17 @@ export class ApiServerService { } }); + fastify.all('/clear-browser-cache', (request, reply) => { + if (['GET', 'POST'].includes(request.method)) { + reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"'); + reply.code(204); + reply.send(); + } else { + reply.code(405); + reply.send(); + } + }); + // Make sure any unknown path under /api returns HTTP 404 Not Found, // because otherwise ClientServerService will return the base client HTML // page with HTTP 200. diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index c0c43dd5c9..f4aa07875d 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -412,6 +412,7 @@ export * as 'users/search' from './endpoints/users/search.js'; export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js'; export * as 'users/show' from './endpoints/users/show.js'; export * as 'users/update-memo' from './endpoints/users/update-memo.js'; +export * as 'verify-email' from './endpoints/verify-email.js'; export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js'; export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js'; export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 06047b58a6..6606202118 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -34,13 +34,22 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'MeDetailed', - properties: { - token: { - type: 'string', - optional: false, nullable: false, + allOf: [ + { + type: 'object', + ref: 'MeDetailed', }, - }, + { + type: 'object', + optional: false, nullable: false, + properties: { + token: { + type: 'string', + optional: false, nullable: false, + }, + }, + } + ], }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 955154f4fb..01697ae185 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -36,6 +36,7 @@ export const paramDef = { startsAt: { type: 'integer' }, imageUrl: { type: 'string', minLength: 1 }, dayOfWeek: { type: 'integer' }, + isSensitive: { type: 'boolean' }, }, required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'], } as const; @@ -55,6 +56,7 @@ export default class extends Endpoint { // eslint- expiresAt: new Date(ps.expiresAt), startsAt: new Date(ps.startsAt), dayOfWeek: ps.dayOfWeek, + isSensitive: ps.isSensitive, url: ps.url, imageUrl: ps.imageUrl, priority: ps.priority, @@ -73,6 +75,7 @@ export default class extends Endpoint { // eslint- expiresAt: ad.expiresAt.toISOString(), startsAt: ad.startsAt.toISOString(), dayOfWeek: ad.dayOfWeek, + isSensitive: ad.isSensitive, url: ad.url, imageUrl: ad.imageUrl, priority: ad.priority, 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 4f897d98e4..f67cad5bd2 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -63,6 +63,7 @@ export default class extends Endpoint { // eslint- expiresAt: ad.expiresAt.toISOString(), startsAt: ad.startsAt.toISOString(), dayOfWeek: ad.dayOfWeek, + isSensitive: ad.isSensitive, url: ad.url, imageUrl: ad.imageUrl, memo: ad.memo, diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 4e3d731aca..a3d9aaddc6 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -39,6 +39,7 @@ export const paramDef = { expiresAt: { type: 'integer' }, startsAt: { type: 'integer' }, dayOfWeek: { type: 'integer' }, + isSensitive: { type: 'boolean' }, }, required: ['id'], } as const; @@ -66,6 +67,7 @@ export default class extends Endpoint { // eslint- expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined, startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined, dayOfWeek: ps.dayOfWeek, + isSensitive: ps.isSensitive, }); const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id }); 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 81a788de2b..804bd5d9b9 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -49,6 +49,34 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + icon: { + type: 'string', + optional: false, nullable: true, + }, + display: { + type: 'string', + optional: false, nullable: false, + }, + isActive: { + type: 'boolean', + optional: false, nullable: false, + }, + forExistingUsers: { + type: 'boolean', + optional: false, nullable: false, + }, + silence: { + type: 'boolean', + optional: false, nullable: false, + }, + needConfirmationToRead: { + type: 'boolean', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: true, + }, imageUrl: { type: 'string', optional: false, nullable: true, 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 b84a5c73f9..e7a70d0762 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 @@ -157,6 +157,22 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + maybeSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + maybePorn: { + type: 'boolean', + optional: false, nullable: false, + }, + requestIp: { + type: 'string', + optional: false, nullable: true, + }, + requestHeaders: { + type: 'object', + optional: false, nullable: true, + }, }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 4d3f6d6cd8..21099c0a8c 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -223,10 +223,12 @@ export const meta = { sensitiveMediaDetection: { type: 'string', optional: false, nullable: false, + enum: ['none', 'all', 'local', 'remote'], }, sensitiveMediaDetectionSensitivity: { type: 'string', optional: false, nullable: false, + enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'], }, setSensitiveFlagAutomatically: { type: 'boolean', @@ -425,6 +427,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + clientOptions: { + type: 'object', + optional: false, nullable: false, + }, description: { type: 'string', optional: false, nullable: true, @@ -469,6 +475,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + feedbackUrl: { + type: 'string', + optional: false, nullable: true, + }, summalyProxy: { type: 'string', optional: false, nullable: true, @@ -650,6 +660,7 @@ export default class extends Endpoint { // eslint- logoImageUrl: instance.logoImageUrl, defaultLightTheme: instance.defaultLightTheme, defaultDarkTheme: instance.defaultDarkTheme, + clientOptions: instance.clientOptions, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index d7f9e4eaa3..b69699c338 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], @@ -49,6 +49,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 1ba6853dbe..2fd7ab8ca2 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -103,6 +103,8 @@ export const meta = { quote: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig }, + scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, + scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, 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 08cea23119..a1a2a99d6e 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -67,6 +67,7 @@ export const paramDef = { description: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true }, + clientOptions: { type: 'object', nullable: false }, cacheRemoteFiles: { type: 'boolean' }, cacheRemoteSensitiveFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, @@ -326,6 +327,10 @@ export default class extends Endpoint { // eslint- set.defaultDarkTheme = ps.defaultDarkTheme; } + if (ps.clientOptions !== undefined) { + set.clientOptions = ps.clientOptions; + } + if (ps.cacheRemoteFiles !== undefined) { set.cacheRemoteFiles = ps.cacheRemoteFiles; } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 4afed7dc5c..fe48e7497a 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; -import { ApiError } from '../../error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['federation'], diff --git a/packages/backend/src/server/api/endpoints/chat/read-all.ts b/packages/backend/src/server/api/endpoints/chat/read-all.ts index 2ed9497eef..e2d9601aa6 100644 --- a/packages/backend/src/server/api/endpoints/chat/read-all.ts +++ b/packages/backend/src/server/api/endpoints/chat/read-all.ts @@ -32,6 +32,8 @@ export default class extends Endpoint { // eslint- 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/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 2e4a3ff820..af20ea9f8d 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import type { ClipsRepository } from '@/models/_.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -29,7 +30,13 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + 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; @@ -39,12 +46,14 @@ export default class extends Endpoint { // eslint- @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, + private queryService: QueryService, private clipEntityService: ClipEntityService, ) { super(meta, paramDef, async (ps, me) => { - const clips = await this.clipsRepository.findBy({ - userId: me.id, - }); + const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('clip.userId = :userId', { userId: me.id }); + + const clips = await query.limit(ps.limit).getMany(); return await this.clipEntityService.packMany(clips, me); }); 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 index 5be477f468..b34ac4abd1 100644 --- 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 @@ -10,6 +10,7 @@ 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 = { @@ -60,14 +61,21 @@ export default class extends Endpoint { // eslint- @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: await this.roleService.isModerator(me) ? undefined : me.id, + userId: isModerator ? undefined : me.id, }); if (file == null) { diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts index e378669f0a..8696c6f6e8 100644 --- a/packages/backend/src/server/api/endpoints/flash/update.ts +++ b/packages/backend/src/server/api/endpoints/flash/update.ts @@ -73,8 +73,8 @@ export default class extends Endpoint { // eslint- updatedAt: new Date(), ...Object.fromEntries( Object.entries(ps).filter( - ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key) - ) + ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key), + ), ), }); }); diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 055b5cc061..523d81ac73 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -46,6 +46,14 @@ export const meta = { type: 'string', }, }, + iconUrl: { + type: 'string', + optional: true, nullable: true, + }, + description: { + type: 'string', + optional: true, nullable: true, + }, }, }, }, @@ -88,6 +96,8 @@ export default class extends Endpoint { // eslint- createdAt: this.idService.parse(token.id).date.toISOString(), lastUsedAt: token.lastUsedAt?.toISOString(), permission: token.app ? token.app.permission : token.permission, + iconUrl: token.iconUrl, + description: token.description ?? token.app?.description ?? null, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 082d97f5d4..5c7958fc1c 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -209,6 +209,8 @@ export const paramDef = { quote: notificationRecieveConfig, reaction: notificationRecieveConfig, pollEnded: notificationRecieveConfig, + scheduledNotePosted: notificationRecieveConfig, + scheduledNotePostFailed: notificationRecieveConfig, receiveFollowRequest: notificationRecieveConfig, followRequestAccepted: notificationRecieveConfig, roleAssigned: notificationRecieveConfig, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 394c178f2a..8a3ba9e026 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -21,29 +21,7 @@ export const meta = { type: 'array', items: { type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - name: { type: 'string' }, - on: { - type: 'array', - items: { - type: 'string', - enum: webhookEventTypes, - }, - }, - url: { type: 'string' }, - secret: { type: 'string' }, - active: { type: 'boolean' }, - latestSentAt: { type: 'string', format: 'date-time', nullable: true }, - latestStatus: { type: 'integer', nullable: true }, - }, + ref: 'UserWebhook', }, }, } as const; @@ -65,19 +43,17 @@ export default class extends Endpoint { // eslint- userId: me.id, }); - return webhooks.map(webhook => ( - { - id: webhook.id, - userId: webhook.userId, - name: webhook.name, - on: webhook.on, - url: webhook.url, - secret: webhook.secret, - active: webhook.active, - latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, - latestStatus: webhook.latestStatus, - } - )); + return webhooks.map(webhook => ({ + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, + latestStatus: webhook.latestStatus, + })); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 4a0c09ff0c..1c19081c98 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -28,29 +28,7 @@ export const meta = { res: { type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - name: { type: 'string' }, - on: { - type: 'array', - items: { - type: 'string', - enum: webhookEventTypes, - }, - }, - url: { type: 'string' }, - secret: { type: 'string' }, - active: { type: 'boolean' }, - latestSentAt: { type: 'string', format: 'date-time', nullable: true }, - latestStatus: { type: 'integer', nullable: true }, - }, + ref: 'UserWebhook', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 7caea8eedc..e48aa69d0f 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -6,17 +6,10 @@ import ms from 'ms'; import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiUser } from '@/models/User.js'; -import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiChannel } from '@/models/Channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; -import { DI } from '@/di-symbols.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; @@ -223,168 +216,28 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - @Inject(DI.channelsRepository) - private channelsRepository: ChannelsRepository, - private noteEntityService: NoteEntityService, private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { - let visibleUsers: MiUser[] = []; - if (ps.visibleUserIds) { - visibleUsers = await this.usersRepository.findBy({ - id: In(ps.visibleUserIds), - }); - } - - let files: MiDriveFile[] = []; - const fileIds = ps.fileIds ?? ps.mediaIds ?? null; - if (fileIds != null) { - files = await this.driveFilesRepository.createQueryBuilder('file') - .where('file.userId = :userId AND file.id IN (:...fileIds)', { - userId: me.id, - fileIds, - }) - .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') - .setParameters({ fileIds }) - .getMany(); - - if (files.length !== fileIds.length) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - let renote: MiNote | null = null; - if (ps.renoteId != null) { - // Fetch renote to note - renote = await this.notesRepository.findOne({ - where: { id: ps.renoteId }, - relations: ['user', 'renote', 'reply'], - }); - - if (renote == null) { - throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isRenote(renote) && !isQuote(renote)) { - throw new ApiError(meta.errors.cannotReRenote); - } - - // Check blocking - if (renote.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: renote.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - if (renote.visibility === 'followers' && renote.userId !== me.id) { - // 他人のfollowers noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } else if (renote.visibility === 'specified') { - // specified / direct noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } - - if (renote.channelId && renote.channelId !== ps.channelId) { - // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック - // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する - const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); - if (renoteChannel == null) { - // リノートしたいノートが書き込まれているチャンネルが無い - throw new ApiError(meta.errors.noSuchChannel); - } else if (!renoteChannel.allowRenoteToExternal) { - // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 - throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); - } - } - } - - let reply: MiNote | null = null; - if (ps.replyId != null) { - // Fetch reply - reply = await this.notesRepository.findOne({ - where: { id: ps.replyId }, - relations: ['user'], - }); - - if (reply == null) { - throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isRenote(reply) && !isQuote(reply)) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { - throw new ApiError(meta.errors.cannotReplyToInvisibleNote); - } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { - throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); - } - - // Check blocking - if (reply.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: reply.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - } - - if (ps.poll) { - if (typeof ps.poll.expiresAt === 'number') { - if (ps.poll.expiresAt < Date.now()) { - throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); - } - } else if (typeof ps.poll.expiredAfter === 'number') { - ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; - } - } - - let channel: MiChannel | null = null; - if (ps.channelId != null) { - channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - } - - // 投稿を作成 try { - const note = await this.noteCreateService.create(me, { + const note = await this.noteCreateService.fetchAndCreate(me, { createdAt: new Date(), - files: files, + fileIds: ps.fileIds ?? ps.mediaIds ?? [], poll: ps.poll ? { choices: ps.poll.choices, multiple: ps.poll.multiple ?? false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } : undefined, - text: ps.text ?? undefined, - reply, - renote, - cw: ps.cw, + expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : null, + text: ps.text ?? null, + replyId: ps.replyId ?? null, + renoteId: ps.renoteId ?? null, + cw: ps.cw ?? null, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUsers, - channel, + visibleUserIds: ps.visibleUserIds ?? [], + channelId: ps.channelId ?? null, apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, @@ -393,16 +246,46 @@ export default class extends Endpoint { // eslint- return { createdNote: await this.noteEntityService.pack(note, me), }; - } catch (e) { + } catch (err) { // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい - if (e instanceof IdentifiableError) { - if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { + if (err instanceof IdentifiableError) { + if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { throw new ApiError(meta.errors.containsProhibitedWords); - } else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') { + } else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') { throw new ApiError(meta.errors.containsTooManyMentions); + } else if (err.id === '801c046c-5bf5-4234-ad2b-e78fc20a2ac7') { + throw new ApiError(meta.errors.noSuchFile); + } else if (err.id === '53983c56-e163-45a6-942f-4ddc485d4290') { + throw new ApiError(meta.errors.noSuchRenoteTarget); + } else if (err.id === 'bde24c37-121f-4e7d-980d-cec52f599f02') { + throw new ApiError(meta.errors.cannotReRenote); + } else if (err.id === '2b4fe776-4414-4a2d-ae39-f3418b8fd4d3') { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } else if (err.id === '90b9d6f0-893a-4fef-b0f1-e9a33989f71a') { + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (err.id === '48d7a997-da5c-4716-b3c3-92db3f37bf7d') { + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (err.id === 'b060f9a6-8909-4080-9e0b-94d9fa6f6a77') { + throw new ApiError(meta.errors.noSuchChannel); + } else if (err.id === '7e435f4a-780d-4cfc-a15a-42519bd6fb67') { + throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); + } else if (err.id === '60142edb-1519-408e-926d-4f108d27bee0') { + throw new ApiError(meta.errors.noSuchReplyTarget); + } else if (err.id === 'f089e4e2-c0e7-4f60-8a23-e5a6bf786b36') { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } else if (err.id === '11cd37b3-a411-4f77-8633-c580ce6a8dce') { + throw new ApiError(meta.errors.cannotReplyToInvisibleNote); + } else if (err.id === 'ced780a1-2012-4caf-bc7e-a95a291294cb') { + throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); + } else if (err.id === 'b0df6025-f2e8-44b4-a26a-17ad99104612') { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } else if (err.id === '0c11c11e-0c8d-48e7-822c-76ccef660068') { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } else if (err.id === 'bfa3905b-25f5-4894-b430-da331a490e4b') { + throw new ApiError(meta.errors.noSuchChannel); } } - throw e; + throw err; } }); } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts index 1c28ec22d0..8f2fbf9197 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -124,6 +124,12 @@ export const meta = { id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', }, + tooManyScheduledNotes: { + message: 'You cannot create scheduled notes any more.', + code: 'TOO_MANY_SCHEDULED_NOTES', + id: '22ae69eb-09e3-4541-a850-773cfa45e693', + }, + cannotRenoteToExternal: { message: 'Cannot Renote to External.', code: 'CANNOT_RENOTE_TO_EXTERNAL', @@ -162,7 +168,7 @@ export const paramDef = { fileIds: { type: 'array', uniqueItems: true, - minItems: 1, + minItems: 0, maxItems: 16, items: { type: 'string', format: 'misskey:id' }, }, @@ -183,8 +189,10 @@ export const paramDef = { }, required: ['choices'], }, + scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean', default: false }, }, - required: [], + required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'], } as const; @Injectable() @@ -196,22 +204,23 @@ export default class extends Endpoint { // eslint- 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 } : {}), + pollChoices: ps.poll?.choices ?? [], + pollMultiple: ps.poll?.multiple ?? false, + pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null, + pollExpiredAfter: ps.poll?.expiredAfter ?? null, + hasPoll: ps.poll != null, + text: ps.text, + replyId: ps.replyId, + renoteId: ps.renoteId, + cw: ps.cw, + hashtag: ps.hashtag, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUserIds: ps.visibleUserIds ?? [], - channelId: ps.channelId ?? undefined, + visibleUserIds: ps.visibleUserIds, + channelId: ps.channelId, + scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { @@ -241,6 +250,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.cannotReplyToInvisibleNote); case '215dbc76-336c-4d2a-9605-95766ba7dab0': throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); + case 'c3275f19-4558-4c59-83e1-4f684b5fab66': + throw new ApiError(meta.errors.tooManyScheduledNotes); default: throw err; } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/list.ts b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts index f24f9b8fb2..0774f09228 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/list.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts @@ -41,6 +41,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + scheduled: { type: 'boolean', nullable: true }, }, required: [], } as const; @@ -58,6 +59,12 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('drafts.userId = :meId', { meId: me.id }); + if (ps.scheduled === true) { + query.andWhere('drafts.isActuallyScheduled = true'); + } else if (ps.scheduled === false) { + query.andWhere('drafts.isActuallyScheduled = false'); + } + const drafts = await query .limit(ps.limit) .getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts index ee221fb765..9a2e2ca415 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -159,6 +159,12 @@ export const meta = { code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', id: '215dbc76-336c-4d2a-9605-95766ba7dab0', }, + + tooManyScheduledNotes: { + message: 'You cannot create scheduled notes any more.', + code: 'TOO_MANY_SCHEDULED_NOTES', + id: '02f5df79-08ae-4a33-8524-f1503c8f6212', + }, }, limit: { @@ -171,14 +177,14 @@ export const paramDef = { type: 'object', properties: { draftId: { type: 'string', nullable: false, format: 'misskey:id' }, - visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] }, 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 }, + localOnly: { type: 'boolean' }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] }, replyId: { type: 'string', format: 'misskey:id', nullable: true }, renoteId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true }, @@ -194,7 +200,7 @@ export const paramDef = { fileIds: { type: 'array', uniqueItems: true, - minItems: 1, + minItems: 0, maxItems: 16, items: { type: 'string', format: 'misskey:id' }, }, @@ -215,6 +221,8 @@ export const paramDef = { }, required: ['choices'], }, + scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean' }, }, required: ['draftId'], } as const; @@ -228,22 +236,22 @@ export default class extends Endpoint { // eslint- 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 } : {}), + pollChoices: ps.poll?.choices, + pollMultiple: ps.poll?.multiple, + pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null, + pollExpiredAfter: ps.poll?.expiredAfter, + text: ps.text, + replyId: ps.replyId, + renoteId: ps.renoteId, + cw: ps.cw, + hashtag: ps.hashtag, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUserIds: ps.visibleUserIds ?? [], - channelId: ps.channelId ?? undefined, + visibleUserIds: ps.visibleUserIds, + channelId: ps.channelId, + scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { @@ -285,6 +293,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.containsProhibitedWords); case '4de0363a-3046-481b-9b0f-feff3e211025': throw new ApiError(meta.errors.containsTooManyMentions); + case 'bacdf856-5c51-4159-b88a-804fa5103be5': + throw new ApiError(meta.errors.tooManyScheduledNotes); default: throw err; } 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 1c73edf08e..7fa8004209 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -91,6 +91,7 @@ export default class extends Endpoint { // eslint- qb.orWhere(new Brackets(qb => { qb.where('note.text IS NOT NULL'); qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); })); })); } diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 05ffdc1f97..e775bdb7fd 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -66,7 +66,7 @@ export default class extends Endpoint { // eslint- .orWhere(':meIdAsList <@ note.visibleUserIds'); })) // Avoid scanning primary key index - .orderBy('CONCAT(note.id)', 'DESC') + .orderBy('CONCAT(note.id)', (ps.sinceDate || ps.sinceId) ? 'ASC' : 'DESC') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index cae0e752da..a41de25ddf 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -29,10 +29,16 @@ export const meta = { id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, - signinRequired: { - message: 'Signin required.', - code: 'SIGNIN_REQUIRED', - id: '8e75455b-738c-471d-9f80-62693f33372e', + contentRestrictedByUser: { + message: 'Content restricted by user. Please sign in to view.', + code: 'CONTENT_RESTRICTED_BY_USER', + id: 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab', + }, + + contentRestrictedByServer: { + message: 'Content restricted by server settings. Please sign in to view.', + code: 'CONTENT_RESTRICTED_BY_SERVER', + id: '145f88d2-b03d-4087-8143-a78928883c4b', }, }, } as const; @@ -61,15 +67,15 @@ export default class extends Endpoint { // eslint- }); if (note.user!.requireSigninToViewContents && me == null) { - throw new ApiError(meta.errors.signinRequired); + throw new ApiError(meta.errors.contentRestrictedByUser); } if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { - throw new ApiError(meta.errors.signinRequired); + throw new ApiError(meta.errors.contentRestrictedByServer); } if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) { - throw new ApiError(meta.errors.signinRequired); + throw new ApiError(meta.errors.contentRestrictedByServer); } return await this.noteEntityService.pack(note, me, { diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 1f3631ae3d..eeeb797efc 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -242,6 +242,7 @@ export default class extends Endpoint { // eslint- qb.orWhere(new Brackets(qb => { qb.orWhere('note.text IS NOT NULL'); qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); })); })); } 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 614cd9204d..42e80c6ae1 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 @@ -223,6 +223,7 @@ export default class extends Endpoint { // eslint- qb.orWhere(new Brackets(qb => { qb.orWhere('note.text IS NOT NULL'); qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); })); })); } diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index 6de5fe3d44..96bc2a953a 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -5,12 +5,13 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository, PagesRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { MiPage, pageNameSchema } from '@/models/Page.js'; +import type { DriveFilesRepository, MiDriveFile, PagesRepository } from '@/models/_.js'; +import { pageNameSchema } from '@/models/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; +import { PageService } from '@/core/PageService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -77,11 +78,11 @@ export default class extends Endpoint { // eslint- @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + private pageService: PageService, private pageEntityService: PageEntityService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - let eyeCatchingImage = null; + let eyeCatchingImage: MiDriveFile | null = null; if (ps.eyeCatchingImageId != null) { eyeCatchingImage = await this.driveFilesRepository.findOneBy({ id: ps.eyeCatchingImageId, @@ -102,24 +103,20 @@ export default class extends Endpoint { // eslint- } }); - const page = await this.pagesRepository.insertOne(new MiPage({ - id: this.idService.gen(), - updatedAt: new Date(), - title: ps.title, - name: ps.name, - summary: ps.summary, - content: ps.content, - variables: ps.variables, - script: ps.script, - eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, - userId: me.id, - visibility: 'public', - alignCenter: ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned, - font: ps.font, - })); + try { + const page = await this.pageService.create(me, { + ...ps, + eyeCatchingImage, + summary: ps.summary ?? null, + }); - return await this.pageEntityService.pack(page); + return await this.pageEntityService.pack(page); + } catch (err) { + if (err instanceof IdentifiableError && err.id === '1a79e38e-3d83-4423-845b-a9d83ff93b61') { + throw new ApiError(meta.errors.nameAlreadyExists); + } + throw err; + } }); } } diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index f2bc946788..a33868552d 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -4,12 +4,14 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, UsersRepository } from '@/models/_.js'; +import type { MiDriveFile, PagesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { PageService } from '@/core/PageService.js'; export const meta = { tags: ['pages'], @@ -44,36 +46,17 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.pagesRepository) - private pagesRepository: PagesRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private moderationLogService: ModerationLogService, - private roleService: RoleService, + private pageService: PageService, ) { super(meta, paramDef, async (ps, me) => { - const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); - - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - if (!await this.roleService.isModerator(me) && page.userId !== me.id) { - throw new ApiError(meta.errors.accessDenied); - } - - await this.pagesRepository.delete(page.id); - - if (page.userId !== me.id) { - const user = await this.usersRepository.findOneByOrFail({ id: page.userId }); - this.moderationLogService.log(me, 'deletePage', { - pageId: page.id, - pageUserId: page.userId, - pageUserUsername: user.username, - page, - }); + try { + await this.pageService.delete(me, ps.pageId); + } catch (err) { + if (err instanceof IdentifiableError) { + if (err.id === '66aefd3c-fdb2-4a71-85ae-cc18bea85d3f') throw new ApiError(meta.errors.noSuchPage); + if (err.id === 'd0017699-8256-46f1-aed4-bc03bed73616') throw new ApiError(meta.errors.accessDenied); + } + throw err; } }); } diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index a6aeb6002e..6fa5c1d75c 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -4,13 +4,14 @@ */ import ms from 'ms'; -import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiDriveFile } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; import { pageNameSchema } from '@/models/Page.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { PageService } from '@/core/PageService.js'; export const meta = { tags: ['pages'], @@ -75,57 +76,37 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.pagesRepository) - private pagesRepository: PagesRepository, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + + private pageService: PageService, ) { super(meta, paramDef, async (ps, me) => { - const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - if (page.userId !== me.id) { - throw new ApiError(meta.errors.accessDenied); - } + try { + let eyeCatchingImage: MiDriveFile | null | undefined | string = ps.eyeCatchingImageId; + if (eyeCatchingImage != null) { + eyeCatchingImage = await this.driveFilesRepository.findOneBy({ + id: eyeCatchingImage, + userId: me.id, + }); - if (ps.eyeCatchingImageId != null) { - const eyeCatchingImage = await this.driveFilesRepository.findOneBy({ - id: ps.eyeCatchingImageId, - userId: me.id, - }); - - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - if (ps.name != null) { - await this.pagesRepository.findBy({ - id: Not(ps.pageId), - userId: me.id, - name: ps.name, - }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); + if (eyeCatchingImage == null) { + throw new ApiError(meta.errors.noSuchFile); } - }); - } + } - await this.pagesRepository.update(page.id, { - updatedAt: new Date(), - title: ps.title, - name: ps.name, - summary: ps.summary === undefined ? page.summary : ps.summary, - content: ps.content, - variables: ps.variables, - script: ps.script, - alignCenter: ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned, - font: ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId, - }); + await this.pageService.update(me, ps.pageId, { + ...ps, + eyeCatchingImage, + }); + } catch (err) { + if (err instanceof IdentifiableError) { + if (err.id === '66aefd3c-fdb2-4a71-85ae-cc18bea85d3f') throw new ApiError(meta.errors.noSuchPage); + if (err.id === 'd0017699-8256-46f1-aed4-bc03bed73616') throw new ApiError(meta.errors.accessDenied); + if (err.id === 'd05bfe24-24b6-4ea2-a3ec-87cc9bf4daa4') throw new ApiError(meta.errors.nameAlreadyExists); + } + throw err; + } }); } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 8756801fe4..c6d477a92f 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -22,7 +22,26 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'UserList', + allOf: [ + { + type: 'object', + ref: 'UserList', + }, + { + type: 'object', + optional: false, nullable: false, + properties: { + likedCount: { + type: 'number', + optional: true, nullable: false, + }, + isLiked: { + type: 'boolean', + optional: true, nullable: false, + }, + }, + }, + ], }, errors: { diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index d6f1ecd8ed..d84a191f7a 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -28,7 +28,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - ref: 'NoteReaction', + ref: 'NoteReactionWithNote', }, }, @@ -120,7 +120,7 @@ export default class extends Endpoint { // eslint- return true; }); - return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); + return await this.noteReactionEntityService.packManyWithNote(reactions, me); }); } } 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/verify-email.ts b/packages/backend/src/server/api/endpoints/verify-email.ts new file mode 100644 index 0000000000..e069ed59f2 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/verify-email.ts @@ -0,0 +1,66 @@ +/* + * 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 { UserProfilesRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ApiError } from '../error.js'; + +export const meta = { + requireCredential: false, + + tags: ['account'], + + errors: { + noSuchCode: { + message: 'No such code.', + code: 'NO_SUCH_CODE', + id: '97c1f576-e4b8-4b8a-a6dc-9cb65e7f6f85', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + code: { type: 'string' }, + }, + required: ['code'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps) => { + const profile = await this.userProfilesRepository.findOneBy({ + emailVerifyCode: ps.code, + }); + + if (profile == null) { + throw new ApiError(meta.errors.noSuchCode); + } + + await this.userProfilesRepository.update({ userId: profile.userId }, { + emailVerified: true, + emailVerifyCode: null, + }); + + this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { + schema: 'MeDetailed', + includeSecrets: true, + })); + }); + } +} + 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 b515a0c0c8..3cd83efa1a 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -201,6 +201,8 @@ export class ClientServerService { @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { + const configUrl = new URL(this.config.url); + fastify.register(fastifyView, { root: _dirname + '/views', engine: { @@ -239,7 +241,6 @@ export class ClientServerService { done(); }); } else { - const configUrl = new URL(this.config.url); const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, ''); const port = (process.env.VITE_PORT ?? '5173'); @@ -887,6 +888,22 @@ export class ClientServerService { [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/'); fastify.get('/flush', async (request, reply) => { + let sendHeader = true; + + if (request.headers['origin']) { + const originURL = new URL(request.headers['origin']); + if (originURL.protocol !== 'https:') { // Clear-Site-Data only supports https + sendHeader = false; + } + if (originURL.host !== configUrl.host) { + sendHeader = false; + } + } + + if (sendHeader) { + reply.header('Clear-Site-Data', '"*"'); + } + reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60'); return await reply.view('flush'); }); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 0c0b46f82b..ab4b158287 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -13,7 +13,7 @@ }; window.onunhandledrejection = (e) => { console.error(e); - renderError('SOMETHING_HAPPENED_IN_PROMISE', e); + renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e); }; let forceError = localStorage.getItem('forceError'); diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug index a73a45212f..7884495d08 100644 --- a/packages/backend/src/server/web/views/flush.pug +++ b/packages/backend/src/server/web/views/flush.pug @@ -6,41 +6,45 @@ html const msg = document.getElementById('msg'); const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`; - message('Start flushing.'); + if (!document.cookie) { + message('Your site data is fully cleared by your browser.'); + message(successText); + } else { + message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.'); + (async function() { + try { + localStorage.clear(); + message('localStorage cleared.'); - (async function() { - try { - localStorage.clear(); - message('localStorage cleared.'); + const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { + const delidb = indexedDB.deleteDatabase(name); + delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); + delidb.onerror = e => rej(e) + })); - const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { - const delidb = indexedDB.deleteDatabase(name); - delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); - delidb.onerror = e => rej(e) - })); + await Promise.all(idbPromises); - await Promise.all(idbPromises); + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage('clear'); + await navigator.serviceWorker.getRegistrations() + .then(registrations => { + return Promise.all(registrations.map(registration => registration.unregister())); + }) + .catch(e => { throw new Error(e) }); + } - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - await navigator.serviceWorker.getRegistrations() - .then(registrations => { - return Promise.all(registrations.map(registration => registration.unregister())); - }) - .catch(e => { throw new Error(e) }); + message(successText); + } catch (e) { + message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); + message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) + + console.error(e); + setTimeout(() => { + location = '/'; + }, 10000) } - - message(successText); - } catch (e) { - message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); - message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) - - console.error(e); - setTimeout(() => { - location = '/'; - }, 10000) - } - })(); + })(); + } function message(text) { msg.insertAdjacentHTML('beforeend', `

[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}

`) diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index b20f2a2179..24654b0017 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -12,6 +12,8 @@ * quote - 投稿が引用Renoteされた * reaction - 投稿にリアクションされた * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * scheduledNotePosted - 予約したノートが投稿された + * scheduledNotePostFailed - 予約したノートの投稿に失敗した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された * roleAssigned - ロールが付与された @@ -32,6 +34,8 @@ export const notificationTypes = [ 'quote', 'reaction', 'pollEnded', + 'scheduledNotePosted', + 'scheduledNotePostFailed', 'receiveFollowRequest', 'followRequestAccepted', 'roleAssigned', diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts index b5103e00be..056a16ba15 100644 --- a/packages/backend/test-federation/test/utils.ts +++ b/packages/backend/test-federation/test/utils.ts @@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise { ADMIN_CACHE.set(host, { id: res.id, - // @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this i: res.token, }); return res as Misskey.entities.SignupResponse; @@ -190,7 +189,8 @@ export async function uploadFile( path = '../../test/resources/192.jpg', ): Promise { const filename = path.split('/').pop() ?? 'untitled'; - const blob = new Blob([await readFile(join(__dirname, path))]); + const buffer = await readFile(join(__dirname, path)); + const blob = new Blob([new Uint8Array(buffer)]); const body = new FormData(); body.append('i', user.i); diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index 570cc61c4b..fe9a217ee8 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -363,14 +363,11 @@ describe('クリップ', () => { const clipLimit = DEFAULT_POLICIES.clipLimit; const clips = await createMany({}, clipLimit); const res = await list({ - parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる + parameters: { limit: clips.length }, }); - // 返ってくる配列には順序保障がないのでidでソートして厳密比較 - assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), - clips.sort(compareBy(s => s.id)), - ); + // 作成responseの配列には順序保障がないのでidでソートして厳密比較 + assert.deepStrictEqual(res.toReversed(), clips.sort(compareBy(s => s.id))); }); test('の一覧が取得できる(空)', async () => { diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts index f2d4c8ffbb..23f409420e 100644 --- a/packages/backend/test/unit/NoteCreateService.ts +++ b/packages/backend/test/unit/NoteCreateService.ts @@ -40,6 +40,7 @@ describe('NoteCreateService', () => { renoteCount: 0, repliesCount: 0, clippedCount: 0, + pageCount: 0, reactions: {}, visibility: 'public', uri: null, diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts index 0b713e8bf6..74d17abcb6 100644 --- a/packages/backend/test/unit/misc/is-renote.ts +++ b/packages/backend/test/unit/misc/is-renote.ts @@ -23,6 +23,7 @@ const base: MiNote = { renoteCount: 0, repliesCount: 0, clippedCount: 0, + pageCount: 0, reactions: {}, visibility: 'public', uri: null, diff --git a/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts index 15f8eda865..631e160afc 100644 --- a/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts +++ b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts @@ -158,6 +158,7 @@ describe('CleanRemoteNotesProcessorService', () => { oldest: null, newest: null, skipped: true, + transientErrors: 0, }); }); @@ -172,6 +173,7 @@ describe('CleanRemoteNotesProcessorService', () => { oldest: null, newest: null, skipped: false, + transientErrors: 0, }); }, 3000); @@ -199,6 +201,7 @@ describe('CleanRemoteNotesProcessorService', () => { oldest: expect.any(Number), newest: expect.any(Number), skipped: false, + transientErrors: 0, }); // Check side-by-side from all notes @@ -278,6 +281,24 @@ describe('CleanRemoteNotesProcessorService', () => { expect(remainingNote).not.toBeNull(); }); + // ページ + test('should not delete note that is embedded in a page', async () => { + const job = createMockJob(); + + // Create old remote note that is embedded in a page + const clippedNote = await createNote({ + pageCount: 1, // Embedded in a page + }, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); + expect(result.skipped).toBe(false); + + const remainingNote = await notesRepository.findOneBy({ id: clippedNote.id }); + expect(remainingNote).not.toBeNull(); + }); + // 古いreply, renoteが含まれている時の挙動 test('should handle reply/renote relationships correctly', async () => { const job = createMockJob(); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 7eecf8bb0d..ace614115c 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -317,7 +317,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO const formData = new FormData(); formData.append('file', blob ?? - new File([await readFile(absPath)], basename(absPath.toString()))); + new File([new Uint8Array(await readFile(absPath))], basename(absPath.toString()))); formData.append('force', 'true'); if (name) { formData.append('name', name); @@ -608,8 +608,8 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { username: config.db.user, password: config.db.pass, database: config.db.db, - synchronize: true && !justBorrow, - dropSchema: true && !justBorrow, + synchronize: !justBorrow, + dropSchema: !justBorrow, entities: initEntities ?? entities, }); @@ -661,7 +661,9 @@ export async function captureWebhook(postAction: () => let timeoutHandle: NodeJS.Timeout | null = null; const result = await new Promise(async (resolve, reject) => { fastify.all('/', async (req, res) => { - timeoutHandle && clearTimeout(timeoutHandle); + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } const body = JSON.stringify(req.body); res.status(200).send('ok'); diff --git a/packages/frontend-builder/locale-inliner.ts b/packages/frontend-builder/locale-inliner.ts index 75bcdc5b3f..9bef465eeb 100644 --- a/packages/frontend-builder/locale-inliner.ts +++ b/packages/frontend-builder/locale-inliner.ts @@ -69,8 +69,10 @@ export class LocaleInliner { async saveAllLocales(locales: Record) { const localeNames = Object.keys(locales); for (const localeName of localeNames) { + this.logger.info(`Creating bundle for ${localeName}`); await this.saveLocale(localeName, locales[localeName]); } + this.logger.info('Done'); } async saveLocale(localeName: string, localeJson: Locale) { diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index 5fdd25b32d..bdaf0d4027 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -11,15 +11,15 @@ }, "devDependencies": { "@types/estree": "1.0.8", - "@types/node": "22.17.0", - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "rollup": "4.46.2", + "@types/node": "22.18.6", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", + "rollup": "4.52.2", "typescript": "5.9.2" }, "dependencies": { "estree-walker": "3.0.3", - "magic-string": "0.30.17", - "vite": "7.0.6" + "magic-string": "0.30.19", + "vite": "7.1.7" } } diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js index 179d811e77..46247e40d5 100644 --- a/packages/frontend-embed/eslint.config.js +++ b/packages/frontend-embed/eslint.config.js @@ -46,9 +46,71 @@ export default [ allowSingleExtends: true, }], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], + // window ... グローバルスコープと衝突し、予期せぬ結果を招くため + // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため + // close ... window.closeと衝突 or 紛らわしい + // open ... window.openと衝突 or 紛らわしい + // fetch ... window.fetchと衝突 or 紛らわしい + // location ... window.locationと衝突 or 紛らわしい + // document ... window.documentと衝突 or 紛らわしい + // history ... window.historyと衝突 or 紛らわしい + // scroll ... window.scrollと衝突 or 紛らわしい + // setTimeout ... window.setTimeoutと衝突 or 紛らわしい + // setInterval ... window.setIntervalと衝突 or 紛らわしい + // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい + // clearInterval ... window.clearIntervalと衝突 or 紛らわしい + 'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'], + 'no-restricted-globals': [ + 'error', + { + 'name': 'open', + 'message': 'Use `window.open`.', + }, + { + 'name': 'close', + 'message': 'Use `window.close`.', + }, + { + 'name': 'fetch', + 'message': 'Use `window.fetch`.', + }, + { + 'name': 'location', + 'message': 'Use `window.location`.', + }, + { + 'name': 'document', + 'message': 'Use `window.document`.', + }, + { + 'name': 'history', + 'message': 'Use `window.history`.', + }, + { + 'name': 'scroll', + 'message': 'Use `window.scroll`.', + }, + { + 'name': 'setTimeout', + 'message': 'Use `window.setTimeout`.', + }, + { + 'name': 'setInterval', + 'message': 'Use `window.setInterval`.', + }, + { + 'name': 'clearTimeout', + 'message': 'Use `window.clearTimeout`.', + }, + { + 'name': 'clearInterval', + 'message': 'Use `window.clearInterval`.', + }, + { + 'name': 'name', + 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている', + }, + ], 'no-shadow': ['warn'], 'vue/attributes-order': ['error', { alphabetical: false, diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index f9d1330ae5..cd5e5071a6 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -13,10 +13,10 @@ "@discordapp/twemoji": "16.0.1", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", - "@rollup/pluginutils": "5.2.0", + "@rollup/pluginutils": "5.3.0", "@twemoji/parser": "16.0.0", "@vitejs/plugin-vue": "6.0.1", - "@vue/compiler-sfc": "3.5.18", + "@vue/compiler-sfc": "3.5.22", "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", @@ -26,47 +26,47 @@ "mfm-js": "0.25.0", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.46.2", - "sass": "1.89.2", - "shiki": "3.9.1", + "rollup": "4.52.2", + "sass": "1.93.2", + "shiki": "3.13.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", "typescript": "5.9.2", "uuid": "11.1.0", - "vite": "7.0.6", - "vue": "3.5.18" + "vite": "7.1.7", + "vue": "3.5.22" }, "devDependencies": { "@misskey-dev/summaly": "5.2.3", - "@tabler/icons-webfont": "3.34.1", + "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/estree": "1.0.8", "@types/micromatch": "4.0.9", - "@types/node": "22.17.0", + "@types/node": "22.18.6", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "@vitest/coverage-v8": "3.2.4", - "@vue/runtime-core": "3.5.18", + "@vue/runtime-core": "3.5.22", "acorn": "8.15.0", "cross-env": "10.0.0", "eslint-plugin-import": "2.32.0", - "eslint-plugin-vue": "10.4.0", + "eslint-plugin-vue": "10.5.0", "fast-glob": "3.3.3", "happy-dom": "18.0.1", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.10.4", + "msw": "2.11.3", "nodemon": "3.1.10", "prettier": "3.6.2", - "start-server-and-test": "2.0.12", - "tsx": "4.20.3", + "start-server-and-test": "2.1.2", + "tsx": "4.20.6", "vite-plugin-turbosnap": "1.0.3", - "vue-component-type-helpers": "3.0.5", + "vue-component-type-helpers": "3.0.8", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.0.5" + "vue-tsc": "3.0.8" } } diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index 9d69437c30..961cbcef66 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js'; console.log('Misskey Embed'); //#region Embedパラメータの取得・パース -const params = new URLSearchParams(location.search); +const params = new URLSearchParams(window.location.search); const embedParams = parseEmbedParams(params); if (_DEV_) console.log(embedParams); //#endregion @@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload }); //#endregion // サイズの制限 -document.documentElement.style.maxWidth = '500px'; +window.document.documentElement.style.maxWidth = '500px'; // iframeIdの設定 function setIframeIdHandler(event: MessageEvent) { @@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams); const rootEl = ((): HTMLElement => { const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; - const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); + const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID); if (currentRoot) { console.warn('multiple import detected'); return currentRoot; } - const root = document.createElement('div'); + const root = window.document.createElement('div'); root.id = MISSKEY_MOUNT_DIV_ID; - document.body.appendChild(root); + window.document.body.appendChild(root); return root; })(); @@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu //#endregion function removeSplash() { - const splash = document.getElementById('splash'); + const splash = window.document.getElementById('splash'); if (splash) { splash.style.opacity = '0'; splash.style.pointerEvents = 'none'; diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue index 0bff048ce4..71f0ee9294 100644 --- a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue +++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue @@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha const canvasPromise = new Promise(resolve => { // テスト環境で Web Worker インスタンスは作成できない if (import.meta.env.MODE === 'test') { - const canvas = document.createElement('canvas'); + const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); @@ -34,7 +34,7 @@ const canvasPromise = new Promise(resol ); resolve(workers); } else { - const canvas = document.createElement('canvas'); + const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue index 4a116e317a..7add3bb53f 100644 --- a/packages/frontend-embed/src/components/EmInstanceTicker.vue +++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue @@ -29,7 +29,7 @@ const props = defineProps<{ // if no instance data is given, this is for the local instance const instance = props.instance ?? { name: serverMetadata.name, - themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content, + themeColor: (window.document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content, }; const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico'); diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue index b5aaa95894..0a8ac9c05a 100644 --- a/packages/frontend-embed/src/components/EmMention.vue +++ b/packages/frontend-embed/src/components/EmMention.vue @@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us const url = `/${canonical}`; -const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention')); +const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention')); bg.setAlpha(0.1); const bgCss = bg.toRgbString(); diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue index 94a91305f4..bd49d127a9 100644 --- a/packages/frontend-embed/src/components/EmPagination.vue +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -134,7 +134,7 @@ const isBackTop = ref(false); const empty = computed(() => items.value.size === 0); const error = ref(false); -const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body); +const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body); const visibility = useDocumentVisibility(); @@ -353,7 +353,7 @@ watch(visibility, () => { BACKGROUND_PAUSE_WAIT_SEC * 1000); } else { // 'visible' if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } else { isPausingUpdate = false; @@ -447,11 +447,11 @@ onBeforeMount(() => { init().then(() => { if (props.pagination.reversed) { nextTick(() => { - setTimeout(toBottom, 800); + window.setTimeout(toBottom, 800); // scrollToBottomでmoreFetchingボタンが画面外まで出るまで // more = trueを遅らせる - setTimeout(() => { + window.setTimeout(() => { moreFetching.value = false; }, 2000); }); @@ -461,11 +461,11 @@ onBeforeMount(() => { onBeforeUnmount(() => { if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } if (preventAppearFetchMoreTimer.value) { - clearTimeout(preventAppearFetchMoreTimer.value); + window.clearTimeout(preventAppearFetchMoreTimer.value); preventAppearFetchMoreTimer.value = null; } scrollObserver.value?.disconnect(); diff --git a/packages/frontend-embed/src/server-context.ts b/packages/frontend-embed/src/server-context.ts index a84a1a726a..c061d5a6f1 100644 --- a/packages/frontend-embed/src/server-context.ts +++ b/packages/frontend-embed/src/server-context.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -const providedContextEl = document.getElementById('misskey_embedCtx'); +const providedContextEl = window.document.getElementById('misskey_embedCtx'); export type ServerContext = { clip?: Misskey.entities.Clip; diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts index 6c94aacd48..ad9b5a1a91 100644 --- a/packages/frontend-embed/src/server-metadata.ts +++ b/packages/frontend-embed/src/server-metadata.ts @@ -6,7 +6,7 @@ import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/misskey-api.js'; -const providedMetaEl = document.getElementById('misskey_meta'); +const providedMetaEl = window.document.getElementById('misskey_meta'); const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null; diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts index 680ab80167..c7bc5df85d 100644 --- a/packages/frontend-embed/src/theme.ts +++ b/packages/frontend-embed/src/theme.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +// TODO: (可能な部分を)sharedに抽出して frontend と共通化 + import tinycolor from 'tinycolor2'; import lightTheme from '@@/themes/_light.json5'; import darkTheme from '@@/themes/_dark.json5'; @@ -33,15 +35,15 @@ export function assertIsTheme(theme: Record): theme is Theme { export function applyTheme(theme: Theme, persist = true) { if (timeout) window.clearTimeout(timeout); - document.documentElement.classList.add('_themeChanging_'); + window.document.documentElement.classList.add('_themeChanging_'); timeout = window.setTimeout(() => { - document.documentElement.classList.remove('_themeChanging_'); + window.document.documentElement.classList.remove('_themeChanging_'); }, 1000); const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; - document.documentElement.dataset.colorScheme = colorScheme; + window.document.documentElement.dataset.colorScheme = colorScheme; // Deep copy const _theme = JSON.parse(JSON.stringify(theme)); @@ -53,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) { const props = compile(_theme); - for (const tag of document.head.children) { + for (const tag of window.document.head.children) { if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { tag.setAttribute('content', props['htmlThemeColor']); break; @@ -61,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) { } for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); } // iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照 diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue index 4ba5968a91..711d0eae6d 100644 --- a/packages/frontend-embed/src/ui.vue +++ b/packages/frontend-embed/src/ui.vue @@ -52,8 +52,8 @@ function safeURIDecode(str: string): string { } } -const page = location.pathname.split('/')[2]; -const contentId = safeURIDecode(location.pathname.split('/')[3]); +const page = window.location.pathname.split('/')[2]; +const contentId = safeURIDecode(window.location.pathname.split('/')[3]); if (_DEV_) console.log(page, contentId); const embedParams = inject(DI.embedParams, defaultEmbedParams); diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index 3ddee9b8a9..db4afb43a7 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -64,6 +64,8 @@ function toBase62(n: number): string { } export function getConfig(): UserConfig { + const localesHash = toBase62(hash(JSON.stringify(locales))); + return { base: '/embed_vite/', @@ -148,9 +150,9 @@ export function getConfig(): UserConfig { // dependencies of i18n.ts 'config': ['@@/js/config.js'], }, - entryFileNames: 'scripts/[hash:8].js', - chunkFileNames: 'scripts/[hash:8].js', - assetFileNames: 'assets/[hash:8][extname]', + entryFileNames: `scripts/${localesHash}-[hash:8].js`, + chunkFileNames: `scripts/${localesHash}-[hash:8].js`, + assetFileNames: `assets/${localesHash}-[hash:8][extname]`, paths(id) { for (const p of externalPackages) { if (p.match.test(id)) { diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js index 6453be0042..b972cfdb27 100644 --- a/packages/frontend-shared/eslint.config.js +++ b/packages/frontend-shared/eslint.config.js @@ -51,9 +51,71 @@ export default [ allowSingleExtends: true, }], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], + // window ... グローバルスコープと衝突し、予期せぬ結果を招くため + // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため + // close ... window.closeと衝突 or 紛らわしい + // open ... window.openと衝突 or 紛らわしい + // fetch ... window.fetchと衝突 or 紛らわしい + // location ... window.locationと衝突 or 紛らわしい + // document ... window.documentと衝突 or 紛らわしい + // history ... window.historyと衝突 or 紛らわしい + // scroll ... window.scrollと衝突 or 紛らわしい + // setTimeout ... window.setTimeoutと衝突 or 紛らわしい + // setInterval ... window.setIntervalと衝突 or 紛らわしい + // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい + // clearInterval ... window.clearIntervalと衝突 or 紛らわしい + 'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'], + 'no-restricted-globals': [ + 'error', + { + 'name': 'open', + 'message': 'Use `window.open`.', + }, + { + 'name': 'close', + 'message': 'Use `window.close`.', + }, + { + 'name': 'fetch', + 'message': 'Use `window.fetch`.', + }, + { + 'name': 'location', + 'message': 'Use `window.location`.', + }, + { + 'name': 'document', + 'message': 'Use `window.document`.', + }, + { + 'name': 'history', + 'message': 'Use `window.history`.', + }, + { + 'name': 'scroll', + 'message': 'Use `window.scroll`.', + }, + { + 'name': 'setTimeout', + 'message': 'Use `window.setTimeout`.', + }, + { + 'name': 'setInterval', + 'message': 'Use `window.setInterval`.', + }, + { + 'name': 'clearTimeout', + 'message': 'Use `window.clearTimeout`.', + }, + { + 'name': 'clearInterval', + 'message': 'Use `window.clearInterval`.', + }, + { + 'name': 'name', + 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている', + }, + ], 'no-shadow': ['warn'], 'vue/attributes-order': ['error', { alphabetical: false, diff --git a/packages/frontend-shared/js/config.ts b/packages/frontend-shared/js/config.ts index ac5c5629f3..6272d3f6b9 100644 --- a/packages/frontend-shared/js/config.ts +++ b/packages/frontend-shared/js/config.ts @@ -4,15 +4,15 @@ */ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -const address = new URL(document.querySelector('meta[property="instance_url"]')?.content || location.href); -const siteName = document.querySelector('meta[property="og:site_name"]')?.content; +const address = new URL(window.document.querySelector('meta[property="instance_url"]')?.content || window.location.href); +const siteName = window.document.querySelector('meta[property="og:site_name"]')?.content; export const host = address.host; export const hostname = address.hostname; export const url = address.origin; export const port = address.port; -export const apiUrl = location.origin + '/api'; -export const wsOrigin = location.origin; +export const apiUrl = window.location.origin + '/api'; +export const wsOrigin = window.location.origin; export const lang = localStorage.getItem('lang') ?? 'en-US'; export const langs = _LANGS_; export const version = _VERSION_; diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 5c33c38f44..c8c437afe9 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -54,67 +54,6 @@ https://github.com/sindresorhus/file-type/blob/main/core.js https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers */ -export const notificationTypes = [ - 'note', - 'follow', - 'mention', - 'reply', - 'renote', - 'quote', - 'reaction', - 'pollEnded', - 'receiveFollowRequest', - 'followRequestAccepted', - 'roleAssigned', - 'chatRoomInvitationReceived', - 'achievementEarned', - 'exportCompleted', - 'login', - 'createToken', - 'test', - 'app', -] as const; -export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; - -export const ROLE_POLICIES = [ - 'gtlAvailable', - 'ltlAvailable', - 'canPublicNote', - 'mentionLimit', - 'canInvite', - 'inviteLimit', - 'inviteLimitCycle', - 'inviteExpirationTime', - 'canManageCustomEmojis', - 'canManageAvatarDecorations', - 'canSearchNotes', - 'canUseTranslator', - 'canHideAds', - 'driveCapacityMb', - 'maxFileSizeMb', - 'alwaysMarkNsfw', - 'canUpdateBioMedia', - 'pinLimit', - 'antennaLimit', - 'wordMuteLimit', - 'webhookLimit', - 'clipLimit', - 'noteEachClipsLimit', - 'userListLimit', - 'userEachUserListsLimit', - 'rateLimitFactor', - 'avatarDecorationLimit', - 'canImportAntennas', - 'canImportBlocking', - 'canImportFollowing', - 'canImportMuting', - 'canImportUserLists', - 'chatAvailability', - 'uploadableFileTypes', - 'noteDraftLimit', - 'watermarkAvailable', -] as const; - export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; export const MFM_PARAMS: Record = { tada: ['speed=', 'delay='], diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend-shared/js/scroll.ts index 9057b896c6..5578cffdec 100644 --- a/packages/frontend-shared/js/scroll.ts +++ b/packages/frontend-shared/js/scroll.ts @@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow // - toleranceの範囲内に収まる程度の微量なスクロールが発生した let prevTopVisible = firstTopVisible; const onScroll = () => { - if (!document.body.contains(el)) return; + if (!window.document.body.contains(el)) return; const topVisible = isHeadVisible(el, tolerance); if (topVisible !== prevTopVisible) { @@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 const containerOrWindow = container ?? window; const onScroll = () => { - if (!document.body.contains(el)) return; + if (!window.document.body.contains(el)) return; if (isTailVisible(el, 1, container)) { cb(); if (once) removeListener(); @@ -145,8 +145,8 @@ export function isTailVisible(el: HTMLElement, tolerance = 1, container = getScr // https://ja.javascript.info/size-and-scroll-window#ref-932 export function getBodyScrollHeight() { return Math.max( - document.body.scrollHeight, document.documentElement.scrollHeight, - document.body.offsetHeight, document.documentElement.offsetHeight, - document.body.clientHeight, document.documentElement.clientHeight, + window.document.body.scrollHeight, window.document.documentElement.scrollHeight, + window.document.body.offsetHeight, window.document.documentElement.offsetHeight, + window.document.body.clientHeight, window.document.documentElement.clientHeight, ); } diff --git a/packages/frontend-shared/js/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts index b1197e68da..a87c1f1bab 100644 --- a/packages/frontend-shared/js/use-document-visibility.ts +++ b/packages/frontend-shared/js/use-document-visibility.ts @@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue'; import type { Ref } from 'vue'; export function useDocumentVisibility(): Ref { - const visibility = ref(document.visibilityState); + const visibility = ref(window.document.visibilityState); const onChange = (): void => { - visibility.value = document.visibilityState; + visibility.value = window.document.visibilityState; }; onMounted(() => { - document.addEventListener('visibilitychange', onChange); + window.document.addEventListener('visibilitychange', onChange); }); onUnmounted(() => { - document.removeEventListener('visibilitychange', onChange); + window.document.removeEventListener('visibilitychange', onChange); }); return visibility; diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 7452ecc918..46f39496b1 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -21,11 +21,11 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "22.17.0", - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "esbuild": "0.25.8", - "eslint-plugin-vue": "10.4.0", + "@types/node": "22.18.6", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", + "esbuild": "0.25.10", + "eslint-plugin-vue": "10.5.0", "nodemon": "3.1.10", "typescript": "5.9.2", "vue-eslint-parser": "10.2.0" @@ -35,6 +35,6 @@ ], "dependencies": { "misskey-js": "workspace:*", - "vue": "3.5.18" + "vue": "3.5.22" } } diff --git a/packages/frontend/.storybook/charts.ts b/packages/frontend/.storybook/charts.ts index 31bb9e51c5..93e1287d69 100644 --- a/packages/frontend/.storybook/charts.ts +++ b/packages/frontend/.storybook/charts.ts @@ -6,7 +6,7 @@ import { HttpResponse, http } from 'msw'; import type { DefaultBodyType, HttpResponseResolver, JsonBodyType, PathParams } from 'msw'; import seedrandom from 'seedrandom'; -import { action } from '@storybook/addon-actions'; +import { action } from 'storybook/actions'; function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] { const rng = seedrandom(seed); diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 91ef41eedf..ed29c63471 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -127,7 +127,7 @@ export function galleryPost(isSensitive = false) { } } -export function file(isSensitive = false) { +export function file(isSensitive = false): entities.DriveFile { return { id: 'somefileid', createdAt: '2016-12-28T22:49:51.000Z', @@ -207,6 +207,7 @@ export function federationInstance(): entities.FederationInstance { isSuspended: false, suspensionState: 'none', isBlocked: false, + isMediaSilenced: false, softwareName: 'misskey', softwareVersion: '2024.5.0', openRegistrations: false, @@ -311,6 +312,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host: enti alsoKnownAs: null, notify: 'none', memo: null, + canChat: true, + chatScope: 'everyone', }; } @@ -378,6 +381,7 @@ export function role(params: { asBadge: params.asBadge ?? true, canEditMembersByModerator: params.canEditMembersByModerator ?? false, usersCount: params.usersCount ?? 10, + preserveAssignmentOnMoveAccount: false, condFormula: { id: '', type: 'or', diff --git a/packages/frontend/.vscode/storybook.code-snippets b/packages/frontend/.vscode/storybook.code-snippets index 785d0a1608..d7063f7200 100644 --- a/packages/frontend/.vscode/storybook.code-snippets +++ b/packages/frontend/.vscode/storybook.code-snippets @@ -42,7 +42,7 @@ "prefix": "storyimplevent", "body": [ "/* eslint-disable @typescript-eslint/explicit-function-return-type */", - "import { action } from '@storybook/addon-actions';", + "import { action } from 'storybook/actions';", "import { StoryObj } from '@storybook/vue3';", "import $1 from './$1.vue';", "export const Default = {", diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts index 4e20828909..f17b43b0e3 100644 --- a/packages/frontend/lib/vite-plugin-create-search-index.ts +++ b/packages/frontend/lib/vite-plugin-create-search-index.ts @@ -8,7 +8,7 @@ import { parse as vueSfcParse } from 'vue/compiler-sfc'; import { createLogger, - EnvironmentModuleGraph, + type EnvironmentModuleGraph, type LogErrorOptions, type LogOptions, normalizePath, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index eaefe92612..0200269fcd 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -23,15 +23,15 @@ "@misskey-dev/browser-image-resizer": "2024.1.0", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", - "@rollup/pluginutils": "5.2.0", - "@sentry/vue": "10.0.0", - "@syuilo/aiscript": "1.0.0", + "@rollup/pluginutils": "5.3.0", + "@sentry/vue": "10.15.0", + "@syuilo/aiscript": "1.1.2", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@twemoji/parser": "16.0.0", "@vitejs/plugin-vue": "6.0.1", - "@vue/compiler-sfc": "3.5.18", + "@vue/compiler-sfc": "3.5.22", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", - "analytics": "0.8.16", + "analytics": "0.8.19", "astring": "1.9.0", "broadcast-channel": "7.1.0", "buraha": "0.0.1", @@ -41,7 +41,7 @@ "chartjs-chart-matrix": "3.0.0", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.2.0", - "chromatic": "13.1.3", + "chromatic": "13.2.1", "compare-versions": "6.1.1", "cropperjs": "2.0.1", "date-fns": "4.1.0", @@ -52,40 +52,43 @@ "icons-subsetter": "workspace:*", "idb-keyval": "6.2.2", "insert-text-at-cursor": "0.3.0", + "ios-haptics": "0.1.4", "is-file-animated": "1.0.2", "json5": "2.2.3", - "magic-string": "0.30.17", + "magic-string": "0.30.19", "matter-js": "0.20.0", + "mediabunny": "1.21.0", "mfm-js": "0.25.0", "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", "photoswipe": "5.4.4", "punycode.js": "2.3.1", - "rollup": "4.46.2", + "qr-code-styling": "1.9.2", + "qr-scanner": "1.4.2", + "rollup": "4.52.2", "sanitize-html": "2.17.0", - "sass": "1.89.2", - "shiki": "3.9.1", + "sass": "1.93.2", + "shiki": "3.13.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.179.1", + "three": "0.180.0", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", "typescript": "5.9.2", "v-code-diff": "1.13.1", - "vite": "7.0.6", - "vue": "3.5.18", + "vite": "7.1.7", + "vue": "3.5.22", "vuedraggable": "next", "wanakana": "5.3.1" }, "devDependencies": { "@misskey-dev/summaly": "5.2.3", - "@storybook/addon-actions": "9.0.8", "@storybook/addon-essentials": "8.6.14", "@storybook/addon-interactions": "8.6.14", - "@storybook/addon-links": "9.1.0", + "@storybook/addon-links": "9.1.8", "@storybook/addon-mdx-gfm": "8.6.14", "@storybook/addon-storysource": "8.6.14", "@storybook/blocks": "8.6.14", @@ -93,57 +96,57 @@ "@storybook/core-events": "8.6.14", "@storybook/manager-api": "8.6.14", "@storybook/preview-api": "8.6.14", - "@storybook/react": "9.1.0", - "@storybook/react-vite": "9.1.0", + "@storybook/react": "9.1.8", + "@storybook/react-vite": "9.1.8", "@storybook/test": "8.6.14", "@storybook/theming": "8.6.14", "@storybook/types": "8.6.14", - "@storybook/vue3": "9.1.0", - "@storybook/vue3-vite": "9.1.0", - "@tabler/icons-webfont": "3.34.1", + "@storybook/vue3": "9.1.8", + "@storybook/vue3-vite": "9.1.8", + "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", "@types/estree": "1.0.8", - "@types/matter-js": "0.19.8", + "@types/matter-js": "0.20.2", "@types/micromatch": "4.0.9", - "@types/node": "22.17.0", + "@types/node": "22.18.6", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/sanitize-html": "2.16.0", "@types/seedrandom": "3.0.8", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "@vitest/coverage-v8": "3.2.4", - "@vue/compiler-core": "3.5.18", - "@vue/runtime-core": "3.5.18", + "@vue/compiler-core": "3.5.22", + "@vue/runtime-core": "3.5.22", "acorn": "8.15.0", "cross-env": "10.0.0", - "cypress": "14.5.3", + "cypress": "14.5.4", "eslint-plugin-import": "2.32.0", - "eslint-plugin-vue": "10.4.0", + "eslint-plugin-vue": "10.5.0", "fast-glob": "3.3.3", "happy-dom": "18.0.1", "intersection-observer": "0.12.2", "micromatch": "4.0.8", "minimatch": "10.0.3", - "msw": "2.10.4", + "msw": "2.11.3", "msw-storybook-addon": "2.0.5", "nodemon": "3.1.10", "prettier": "3.6.2", "react": "19.1.1", "react-dom": "19.1.1", "seedrandom": "3.0.5", - "start-server-and-test": "2.0.12", - "storybook": "9.1.0", + "start-server-and-test": "2.1.2", + "storybook": "9.1.8", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", - "tsx": "4.20.3", + "tsx": "4.20.6", "vite-plugin-turbosnap": "1.0.3", "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5", - "vue-component-type-helpers": "3.0.5", + "vue-component-type-helpers": "3.0.8", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.0.5" + "vue-tsc": "3.0.8" } } diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 354fb95544..111a4abbfd 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -16,7 +16,7 @@ import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; import { subBoot } from '@/boot/sub-boot.js'; -const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete', '/install-extensions']; +const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete', '/verify-email', '/install-extensions']; if (subBootPaths.some(i => window.location.pathname === i || window.location.pathname.startsWith(i + '/'))) { subBoot(); diff --git a/packages/frontend/src/accounts.ts b/packages/frontend/src/accounts.ts index 3693ac3308..60f7cd0b4b 100644 --- a/packages/frontend/src/accounts.ts +++ b/packages/frontend/src/accounts.ts @@ -23,7 +23,7 @@ export async function getAccounts(): Promise<{ host: string; id: Misskey.entities.User['id']; username: Misskey.entities.User['username']; - user?: Misskey.entities.User | null; + user?: Misskey.entities.MeDetailed | null; token: string | null; }[]> { const tokens = store.s.accountTokens; @@ -38,7 +38,7 @@ export async function getAccounts(): Promise<{ })); } -async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) { +async function addAccount(host: string, user: Misskey.entities.MeDetailed, token: AccountWithToken['token']) { if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) { store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token }); store.set('accountInfos', { ...store.s.accountInfos, [host + '/' + user.id]: user }); @@ -149,9 +149,10 @@ export function updateCurrentAccountPartial(accountData: Partial { if (reason === isAccountDeleted) { - removeAccount(host, $i.id); + removeAccount(host, me.id); if (Object.keys(store.s.accountTokens).length > 0) { login(Object.values(store.s.accountTokens)[0]); } else { @@ -214,46 +215,75 @@ export async function openAccountMenu(opts: { includeCurrentAccount?: boolean; withExtraOperation: boolean; active?: Misskey.entities.User['id']; - onChoose?: (account: Misskey.entities.User) => void; + onChoose?: (account: Misskey.entities.MeDetailed) => void; }, ev: MouseEvent) { if (!$i) return; + const me = $i; - function createItem(host: string, id: Misskey.entities.User['id'], username: Misskey.entities.User['username'], account: Misskey.entities.User | null | undefined, token: string): MenuItem { + const callback = opts.onChoose; + + function createItem(host: string, id: Misskey.entities.User['id'], username: Misskey.entities.User['username'], account: Misskey.entities.MeDetailed | null | undefined, token: string | null): MenuItem { if (account) { return { type: 'user' as const, user: account, active: opts.active != null ? opts.active === id : false, action: async () => { - if (opts.onChoose) { - opts.onChoose(account); + if (callback) { + callback(account); } else { switchAccount(host, id); } }, }; - } else { + } else if (token != null) { return { type: 'button' as const, text: username, active: opts.active != null ? opts.active === id : false, action: async () => { - if (opts.onChoose) { + if (callback) { fetchAccount(token, id).then(account => { - opts.onChoose(account); + callback(account); }); } else { switchAccount(host, id); } }, }; + } else { // プロファイルを復元した場合などはアカウントのトークンや詳細情報はstoreにキャッシュされていない + return { + type: 'button' as const, + text: username, + active: opts.active != null ? opts.active === id : false, + action: async () => { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { + initialUsername: username, + }, { + done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => { + store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i }); + + if (callback) { + fetchAccount(res.i, id).then(account => { + callback(account); + }); + } else { + switchAccount(host, id); + } + }, + closed: () => { + dispose(); + }, + }); + }, + }; } } const menuItems: MenuItem[] = []; // TODO: $iのホストも比較したいけど通常null - const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id))).map(a => createItem(a.host, a.id, a.username, a.user, a.token)); + const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.id !== me.id))).map(a => createItem(a.host, a.id, a.username, a.user, a.token)); if (opts.withExtraOperation) { menuItems.push({ diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts index a876e94ee8..0549ab76a0 100644 --- a/packages/frontend/src/aiscript/api.ts +++ b/packages/frontend/src/aiscript/api.ts @@ -86,7 +86,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string }) throw new errors.AiScriptRuntimeError('expected param'); } utils.assertObject(param); - return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => { + return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); diff --git a/packages/frontend/src/aiscript/ui.ts b/packages/frontend/src/aiscript/ui.ts index a27ece512e..9c330da3c5 100644 --- a/packages/frontend/src/aiscript/ui.ts +++ b/packages/frontend/src/aiscript/ui.ts @@ -4,11 +4,11 @@ */ import { utils, values } from '@syuilo/aiscript'; -import { genId } from '@/utility/id.js'; import { ref } from 'vue'; -import type { Ref } from 'vue'; import * as Misskey from 'misskey-js'; import { assertStringAndIsIn } from './common.js'; +import type { Ref } from 'vue'; +import { genId } from '@/utility/id.js'; const ALIGNS = ['left', 'center', 'right'] as const; const FONTS = ['serif', 'sans-serif', 'monospace'] as const; @@ -21,16 +21,15 @@ type BorderStyle = (typeof BORDER_STYLES)[number]; export type AsUiComponentBase = { id: string; hidden?: boolean; + children?: AsUiComponent['id'][]; }; export type AsUiRoot = AsUiComponentBase & { type: 'root'; - children: AsUiComponent['id'][]; }; export type AsUiContainer = AsUiComponentBase & { type: 'container'; - children?: AsUiComponent['id'][]; align?: Align; bgColor?: string; fgColor?: string; @@ -123,7 +122,6 @@ export type AsUiSelect = AsUiComponentBase & { export type AsUiFolder = AsUiComponentBase & { type: 'folder'; - children?: AsUiComponent['id'][]; title?: string; opened?: boolean; }; diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 395d1e5e7e..4becf32ab5 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -29,6 +29,7 @@ import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { prefer } from '@/preferences.js'; import { $i } from '@/i.js'; +import { launchPlugins } from '@/plugin.js'; export async function common(createVue: () => Promise>) { console.info(`Misskey v${version}`); @@ -150,7 +151,21 @@ export async function common(createVue: () => Promise>) { } //#endregion + //#region Sync dark mode + if (prefer.s.syncDeviceDarkMode) { + store.set('darkMode', isDeviceDarkmode()); + } + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { + if (prefer.s.syncDeviceDarkMode) { + store.set('darkMode', mql.matches); + } + }); + //#endregion + // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) + // NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため) + // see: https://github.com/misskey-dev/misskey/issues/16562 watch(store.r.darkMode, (darkMode) => { const theme = (() => { if (darkMode) { @@ -182,18 +197,6 @@ export async function common(createVue: () => Promise>) { }); } - //#region Sync dark mode - if (prefer.s.syncDeviceDarkMode) { - store.set('darkMode', isDeviceDarkmode()); - } - - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { - if (prefer.s.syncDeviceDarkMode) { - store.set('darkMode', mql.matches); - } - }); - //#endregion - if (!isSafeMode) { if (prefer.s.darkTheme && store.s.darkMode) { if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme); @@ -338,6 +341,12 @@ export async function common(createVue: () => Promise>) { }); } + try { + await launchPlugins(); + } catch (error) { + console.error('Failed to launch plugins:', error); + } + app.mount(rootEl); // boot.jsのやつを解除 diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 46e690a55f..18817d3f79 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -26,7 +26,6 @@ import { mainRouter } from '@/router.js'; import { makeHotkey } from '@/utility/hotkey.js'; import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; import { prefer } from '@/preferences.js'; -import { launchPlugins } from '@/plugin.js'; import { updateCurrentAccountPartial } from '@/accounts.js'; import { migrateOldSettings } from '@/pref-migrate.js'; import { unisonReload } from '@/utility/unison-reload.js'; @@ -79,8 +78,6 @@ export async function mainBoot() { } } - launchPlugins(); - try { if (prefer.s.enableSeasonalScreenEffect) { const month = new Date().getMonth() + 1; @@ -371,11 +368,6 @@ export async function mainBoot() { }); }); - main.on('unreadAntenna', () => { - updateCurrentAccountPartial({ hasUnreadAntenna: true }); - sound.playMisskeySfx('antenna'); - }); - main.on('newChatMessage', () => { updateCurrentAccountPartial({ hasUnreadChatMessages: true }); sound.playMisskeySfx('chatMessage'); @@ -421,7 +413,7 @@ export async function mainBoot() { } }, allowRepeat: true, - } + }, } as const satisfies Keymap; window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false }); diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 70078b410d..39cf73feb8 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -7,8 +7,8 @@ import * as Misskey from 'misskey-js'; import { Cache } from '@/utility/cache.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -export const clipsCache = new Cache(1000 * 60 * 30, () => misskeyApi('clips/list')); -export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list')); +export const clipsCache = new Cache(1000 * 60 * 30, () => misskeyApi('clips/list', { limit: 30 })); +export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list', { limit: 30 })); export const userListsCache = new Cache(1000 * 60 * 30, () => misskeyApi('users/lists/list')); -export const antennasCache = new Cache(1000 * 60 * 30, () => misskeyApi('antennas/list')); +export const antennasCache = new Cache(1000 * 60 * 30, () => misskeyApi('antennas/list', { limit: 30 })); export const favoritedChannelsCache = new Cache(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 })); diff --git a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts index b62096bbe9..2eb17b1b9e 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts +++ b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts @@ -2,14 +2,13 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; -import type { StoryObj } from '@storybook/vue3'; + +import { action } from 'storybook/actions'; import { HttpResponse, http } from 'msw'; import { userDetailed } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkAbuseReportWindow from './MkAbuseReportWindow.vue'; +import type { StoryObj } from '@storybook/vue3'; export const Default = { render(args) { return { diff --git a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts index b907b5b25a..fff29262f1 100644 --- a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts +++ b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts @@ -4,7 +4,7 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; +import { action } from 'storybook/actions'; import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { commonHandlers } from '../../.storybook/mocks.js'; diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index 70766634ce..c786e9fe9f 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum', }]" > -
+
@@ -61,8 +61,8 @@ import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utili const props = withDefaults(defineProps<{ user: Misskey.entities.User; - withLocked: boolean; - withDescription: boolean; + withLocked?: boolean; + withDescription?: boolean; }>(), { withLocked: true, withDescription: true, @@ -71,7 +71,7 @@ const props = withDefaults(defineProps<{ const achievements = ref(null); const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x))); -function fetch() { +function _fetch_() { misskeyApi('users/achievements', { userId: props.user.id }).then(res => { achievements.value = []; for (const t of ACHIEVEMENT_TYPES) { @@ -84,11 +84,11 @@ function fetch() { function clickHere() { claimAchievement('clickedClickHere'); - fetch(); + _fetch_(); } onMounted(() => { - fetch(); + _fetch_(); }); diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue index e57fbcdee3..0e1018dcbf 100644 --- a/packages/frontend/src/components/MkAnimBg.vue +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts b/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts index 627cb0c4ff..743bdda032 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts @@ -4,7 +4,7 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; +import { action } from 'storybook/actions'; import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { commonHandlers } from '../../.storybook/mocks.js'; diff --git a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts index 4d921a4c48..5c4b05481a 100644 --- a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts +++ b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts @@ -4,7 +4,7 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; +import { action } from 'storybook/actions'; import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { commonHandlers } from '../../.storybook/mocks.js'; diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue index e2febf7225..a41fdbc45d 100644 --- a/packages/frontend/src/components/MkAntennaEditor.vue +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only - + - - - - - - + - @@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkButton.stories.impl.ts b/packages/frontend/src/components/MkButton.stories.impl.ts index 0a569b3beb..4420cc4f05 100644 --- a/packages/frontend/src/components/MkButton.stories.impl.ts +++ b/packages/frontend/src/components/MkButton.stories.impl.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable import/no-default-export */ -import { action } from '@storybook/addon-actions'; +import { action } from 'storybook/actions'; import type { StoryObj } from '@storybook/vue3'; import MkButton from './MkButton.vue'; export const Default = { diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index a77ebd6ac5..b729128a21 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue index 2c6185fd33..5ce514f93e 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
{{ i18n.ts.preview }}
+
+ +
@@ -212,6 +215,147 @@ watch(enabled, () => { renderer.render(); } }); + +const penMode = ref<'fill' | 'blur' | 'pixelate' | null>(null); + +function showPenMenu(ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts._imageEffector._fxs.fill, + action: () => { + penMode.value = 'fill'; + }, + }, { + text: i18n.ts._imageEffector._fxs.blur, + action: () => { + penMode.value = 'blur'; + }, + }, { + text: i18n.ts._imageEffector._fxs.pixelate, + action: () => { + penMode.value = 'pixelate'; + }, + }], ev.currentTarget ?? ev.target); +} + +function onImagePointerdown(ev: PointerEvent) { + if (canvasEl.value == null || imageBitmap == null || penMode.value == null) return; + + const AW = canvasEl.value.clientWidth; + const AH = canvasEl.value.clientHeight; + const BW = imageBitmap.width; + const BH = imageBitmap.height; + + let xOffset = 0; + let yOffset = 0; + + if (AW / AH < BW / BH) { // 横長 + yOffset = AH - BH * (AW / BW); + } else { // 縦長 + xOffset = AW - BW * (AH / BH); + } + + xOffset /= 2; + yOffset /= 2; + + let startX = ev.offsetX - xOffset; + let startY = ev.offsetY - yOffset; + + if (AW / AH < BW / BH) { // 横長 + startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1)); + startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1)); + } else { // 縦長 + startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1)); + startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1)); + } + + const id = genId(); + if (penMode.value === 'fill') { + layers.push({ + id, + fxId: 'fill', + params: { + offsetX: 0, + offsetY: 0, + scaleX: 0.1, + scaleY: 0.1, + angle: 0, + opacity: 1, + color: [1, 1, 1], + }, + }); + } else if (penMode.value === 'blur') { + layers.push({ + id, + fxId: 'blur', + params: { + offsetX: 0, + offsetY: 0, + scaleX: 0.1, + scaleY: 0.1, + angle: 0, + radius: 3, + }, + }); + } else if (penMode.value === 'pixelate') { + layers.push({ + id, + fxId: 'pixelate', + params: { + offsetX: 0, + offsetY: 0, + scaleX: 0.1, + scaleY: 0.1, + angle: 0, + strength: 0.2, + }, + }); + } + + _move(ev.offsetX, ev.offsetY); + + function _move(pointerX: number, pointerY: number) { + let x = pointerX - xOffset; + let y = pointerY - yOffset; + + if (AW / AH < BW / BH) { // 横長 + x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1)); + y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1)); + } else { // 縦長 + x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1)); + y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1)); + } + + const scaleX = Math.abs(x - startX); + const scaleY = Math.abs(y - startY); + + const layerIndex = layers.findIndex((l) => l.id === id); + const layer = layerIndex !== -1 ? layers[layerIndex] : null; + if (layer != null) { + layer.params.offsetX = (x + startX) - 1; + layer.params.offsetY = (y + startY) - 1; + layer.params.scaleX = scaleX; + layer.params.scaleY = scaleY; + layers[layerIndex] = layer; + } + } + + function move(ev: PointerEvent) { + _move(ev.offsetX, ev.offsetY); + } + + function up() { + canvasEl.value?.removeEventListener('pointermove', move); + canvasEl.value?.removeEventListener('pointerup', up); + canvasEl.value?.removeEventListener('pointercancel', up); + canvasEl.value?.releasePointerCapture(ev.pointerId); + + penMode.value = null; + } + + canvasEl.value.addEventListener('pointermove', move); + canvasEl.value.addEventListener('pointerup', up); + canvasEl.value.setPointerCapture(ev.pointerId); +} diff --git a/packages/frontend/src/components/MkImgPreviewDialog.stories.impl.ts b/packages/frontend/src/components/MkImgPreviewDialog.stories.impl.ts index 339e6d10f3..7da705a23f 100644 --- a/packages/frontend/src/components/MkImgPreviewDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkImgPreviewDialog.stories.impl.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { file } from '../../.storybook/fakes.js'; import MkImgPreviewDialog from './MkImgPreviewDialog.vue'; export const Default = { diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index cc7ad8bb78..7f052dff94 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -43,7 +43,15 @@ SPDX-License-Identifier: AGPL-3.0-only
- + + @@ -220,4 +298,11 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) { padding-top: 16px; border-top: solid 1px var(--MI_THEME-divider); } + +.tabs { + background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-bottom: solid 0.5px var(--MI_THEME-divider); +} diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index f1107527b7..ed0b3ad555 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only