Merge tag '13.14.1' into merge-upstream

This commit is contained in:
まっちゃとーにゅ 2023-07-23 03:08:40 +09:00
commit 72244c7ce6
No known key found for this signature in database
GPG key ID: 6AFBBF529601C1DB
560 changed files with 12755 additions and 8764 deletions

View file

@ -30,6 +30,10 @@ url: https://example.tld/
# The port that your Misskey server should listen on. # The port that your Misskey server should listen on.
port: 3000 port: 3000
# You can also use UNIX domain socket.
# socket: /path/to/misskey.sock
# chmodSocket: '777'
# ┌──────────────────────────┐ # ┌──────────────────────────┐
#───┘ PostgreSQL configuration └──────────────────────────────── #───┘ PostgreSQL configuration └────────────────────────────────
@ -78,6 +82,8 @@ redis:
#pass: example-pass #pass: example-pass
#prefix: example-prefix #prefix: example-prefix
#db: 1 #db: 1
# You can specify more ioredis options...
#username: example-username
#redisForPubsub: #redisForPubsub:
# host: localhost # host: localhost
@ -86,6 +92,8 @@ redis:
# #pass: example-pass # #pass: example-pass
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# # You can specify more ioredis options...
# #username: example-username
#redisForJobQueue: #redisForJobQueue:
# host: localhost # host: localhost
@ -94,6 +102,8 @@ redis:
# #pass: example-pass # #pass: example-pass
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# # You can specify more ioredis options...
# #username: example-username
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View file

@ -6,7 +6,7 @@
"features": { "features": {
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}, "ghcr.io/devcontainers-contrib/features/pnpm:2": {},
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "18.16.0" "version": "20.3.1"
} }
}, },
"forwardPorts": [3000], "forwardPorts": [3000],

View file

@ -6,6 +6,10 @@ indent_size = 2
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
indent_style = space indent_style = space

View file

@ -54,7 +54,7 @@ Please include errors from the developer console and/or server log files if you
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment --> * Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
* Misskey: 13.x.x * Misskey: 13.x.x
* Node: 18.x.x * Node: 20.x.x
* PostgreSQL: 15.x.x * PostgreSQL: 15.x.x
* Redis: 7.x.x * Redis: 7.x.x
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 --> * OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->

View file

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [18.x] node-version: [20.x]
services: services:
postgres: postgres:

View file

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [18.x] node-version: [20.x]
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0
@ -51,7 +51,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [18.x] node-version: [20.x]
browser: [chrome] browser: [chrome]
services: services:

View file

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [18.x] node-version: [20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:

View file

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [18.x] node-version: [20.x]
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0

3
.gitignore vendored
View file

@ -64,3 +64,6 @@ temp
*.blend3 *.blend3
*.blend4 *.blend4
*.blend5 *.blend5
# VSCode addon
.favorites.json

View file

@ -1 +1 @@
18.16.0 20.3.1

View file

@ -12,6 +12,70 @@
--> -->
## 13.14.1
### General
- 招待機能を改善しました
* 過去に発行した招待コードを確認できるようになりました
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
### Client
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
- ドライブファイルのメニューで画像をクロップできるように
- 画像を動画と同様に簡単に隠せるように
- Enhance: ノートの埋め込みが複数画像と動画を表示されるように
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
- プロフィールURLをコピーできるボタンを追加 #11190
- `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript)
- ユーザーのContextMenuに「アンテナに追加」ボタンを追加
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
- オフライン時の画面にリロードボタンを追加
- Renote時に公開範囲のデフォルト設定が適用されるように
- Deckで非ルートページにアクセスした際に簡易UIで表示しない設定を追加
- ロール設定画面でロールIDを確認できるように
- コンテキストメニュー表示時のパフォーマンスを改善
- フォロー/フォロワー非公開時の表示を改善
- 本文にMFMが含まれている場合に自動でたたまれる機能が、返信先や引用RNにも適用されるように
- position は対象外になりました
- AiScriptを0.15.0に更新
- Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
- Fix: フォルダーのページネーションが機能しない #11180
- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
- Fix: システムフォント設定が正しく反映されない問題を修正
- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正
- Fix: MasterVolumeが0の時だけでなく各通知音の音量設定が0のときも、HTMLAudioElement.playが実行されないように変更
### Server
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
- 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用
- featuredートのsignedGet回数を減らしました
- ActivityPubの署名用鍵長を2048bitに変更しパフォーマンスを向上(新規アカウントのみ)
- リモートサーバーのセンシティブなファイルのキャッシュだけを無効化できるオプションを追加
- MeilisearchにIndexするートの範囲を設定できるように
- Export notes with file detail
- Add unix socket support
- 設定ファイルでioredisの全てのオプションを指定可能に
- Fix: エクスポートしたカスタム絵文字のzipが大きいと読み込めない問題を修正
- Fix: リモートサーバーに無意味なActivityPubの配信を行うことがあるのを修正
- Fix: Remove Meilisearch index when notes are deleted
- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正
- Fix: インスタンスのアイコンがbase64の場合の挙動を修正
- Fix: ローカルの `Person` を指す `acct` URI を解析するときのバグを修正しました
- Fix: 無効化されたアンテナが再度有効化されないことがある問題を修正
## 13.13.2 ## 13.13.2
### General ### General

View file

@ -214,30 +214,13 @@ Misskey uses [Storybook](https://storybook.js.org/) for UI development.
### Setup & Run ### Setup & Run
#### Universal #### Setup
##### Setup
```bash
pnpm --filter misskey-js build
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
```
##### Run
```bash
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
```
#### macOS & Linux
##### Setup
```bash ```bash
pnpm --filter misskey-js build pnpm --filter misskey-js build
``` ```
##### Run #### Run
```bash ```bash
pnpm --filter frontend storybook-dev pnpm --filter frontend storybook-dev

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4 # syntax = docker/dockerfile:1.4
ARG NODE_VERSION=18.16.0-bullseye ARG NODE_VERSION=20.3.1-bullseye
# build assets & compile TypeScript # build assets & compile TypeScript

View file

@ -54,6 +54,7 @@ describe('After setup instance', () => {
cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click(); cy.get('[data-cy-signup-rules-continue]').click();
@ -78,6 +79,7 @@ describe('After setup instance', () => {
cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click(); cy.get('[data-cy-signup-rules-continue]').click();

View file

@ -2,14 +2,14 @@
* Gulp tasks * Gulp tasks
*/ */
const fs = require('fs'); import * as fs from 'node:fs';
const gulp = require('gulp'); import gulp from 'gulp';
const replace = require('gulp-replace'); import replace from 'gulp-replace';
const terser = require('gulp-terser'); import terser from 'gulp-terser';
const cssnano = require('gulp-cssnano'); import cssnano from 'gulp-cssnano';
const locales = require('./locales'); import locales from './locales/index.js';
const meta = require('./package.json'); import meta from './package.json' assert { type: "json" };
gulp.task('copy:backend:views', () => gulp.task('copy:backend:views', () =>
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views')) gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))

View file

@ -41,17 +41,23 @@ unfavorite: "إزالة من المفضلة"
favorited: "أُضيف إلى المفضلة." favorited: "أُضيف إلى المفضلة."
alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة." alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة."
cantFavorite: "تعذرت الإضافة إلى المفضلة." cantFavorite: "تعذرت الإضافة إلى المفضلة."
pin: "دبّسها على الصفحة الشخصية" pin: "ثبتها على الصفحة الشخصية"
unpin: "ألغ تدبيسها من ملفك الشخصي" unpin: "فكها من ملفك الشخصي"
copyContent: "انسخ المحتوى" copyContent: "انسخ المحتوى"
copyLink: "انسخ الرابط" copyLink: "انسخ الرابط"
delete: "حذف" delete: "حذف"
deleteAndEdit: "إزالة وإعادة الصياغة" deleteAndEdit: "إزالة وإعادة الصياغة"
deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها." deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها."
addToList: "أضفه إلى قائمة" addToList: "أضفه إلى قائمة"
addToAntenna: "أضف إلى هوائي"
sendMessage: "أرسل رسالة" sendMessage: "أرسل رسالة"
copyRSS: "انسخ رابط RSS" copyRSS: "انسخ رابط RSS"
copyUsername: "انسخ اسم المستخدم" copyUsername: "انسخ اسم المستخدم"
copyUserId: "انسخ معرف المستخدم"
copyNoteId: "انسخ معرف الملاحظة"
copyFileId: "انسخ معرّف الملف"
copyFolderId: "انسخ معرّف المجلد"
copyProfileUrl: "انسخ رابط الملف الشخصي"
searchUser: "ابحث عن مستخدمين" searchUser: "ابحث عن مستخدمين"
reply: "رد" reply: "رد"
loadMore: "عرض المزيد" loadMore: "عرض المزيد"
@ -106,8 +112,8 @@ cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشره
quote: "اقتبس" quote: "اقتبس"
inChannelRenote: "إعادة نشر في قناة" inChannelRenote: "إعادة نشر في قناة"
inChannelQuote: "اقتباس في قناة" inChannelQuote: "اقتباس في قناة"
pinnedNote: "ملاحظة مدبسة" pinnedNote: "ملاحظة مثبتة"
pinned: "دبّسها على الصفحة الشخصية" pinned: "ثبتها على الصفحة الشخصية"
you: "أنت" you: "أنت"
clickToShow: "اضغط للعرض" clickToShow: "اضغط للعرض"
sensitive: "محتوى حساس" sensitive: "محتوى حساس"
@ -134,8 +140,10 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟"
suspendConfirm: "أمتأكد من تعليق الحساب؟" suspendConfirm: "أمتأكد من تعليق الحساب؟"
unsuspendConfirm: "أمتأكد من إلغاء تعليق؟" unsuspendConfirm: "أمتأكد من إلغاء تعليق؟"
selectList: "اختر قائمة" selectList: "اختر قائمة"
editList: "عدّل القائمة"
selectChannel: "اختر قناة" selectChannel: "اختر قناة"
selectAntenna: "اختر هوائيًا" selectAntenna: "اختر هوائيًا"
editAntenna: "عدّل الهوائي"
selectWidget: "اختر ودجة" selectWidget: "اختر ودجة"
editWidgets: "عدّل الودجات" editWidgets: "عدّل الودجات"
editWidgetsExit: "تم" editWidgetsExit: "تم"
@ -206,7 +214,7 @@ blockedUsers: "الحسابات المحجوبة"
noUsers: "ليس هناك مستخدمون" noUsers: "ليس هناك مستخدمون"
editProfile: "تعديل الملف التعريفي" editProfile: "تعديل الملف التعريفي"
noteDeleteConfirm: "هل تريد حذف هذه الملاحظة؟" noteDeleteConfirm: "هل تريد حذف هذه الملاحظة؟"
pinLimitExceeded: "لا يمكنك تدبيس الملاحظات بعد الآن." pinLimitExceeded: "لا يمكنك تثبيت الملاحظات بعد الآن."
intro: "لقد انتهت عملية تنصيب Misskey. الرجاء إنشاء حساب إداري." intro: "لقد انتهت عملية تنصيب Misskey. الرجاء إنشاء حساب إداري."
done: "تمّ" done: "تمّ"
processing: "المعالجة جارية" processing: "المعالجة جارية"
@ -305,7 +313,7 @@ copyUrl: "انسخ الرابط"
rename: "إعادة التسمية" rename: "إعادة التسمية"
avatar: "الصورة الرمزية" avatar: "الصورة الرمزية"
banner: "الصورة الرأسية" banner: "الصورة الرأسية"
nsfw: "محتوى حساس" displayOfSensitiveMedia: "عرض المحتوى الحساس"
whenServerDisconnected: "عند فقدان الاتصال بالخادم" whenServerDisconnected: "عند فقدان الاتصال بالخادم"
disconnectedFromServer: "قُطِع الإتصال بالخادم" disconnectedFromServer: "قُطِع الإتصال بالخادم"
reload: "انعش" reload: "انعش"
@ -344,12 +352,12 @@ iconUrl: "رابط الأيقونة"
bannerUrl: "رابط صورة اللافتة" bannerUrl: "رابط صورة اللافتة"
backgroundImageUrl: "رابط صورة الخلفية" backgroundImageUrl: "رابط صورة الخلفية"
basicInfo: "المعلومات الأساسية " basicInfo: "المعلومات الأساسية "
pinnedUsers: "المستخدمون المدبسون" pinnedUsers: "المستخدمون المثبتون"
pinnedUsersDescription: "قائمة المستخدمين المدبسين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده." pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده."
pinnedPages: "الصفحات المدبسة" pinnedPages: "الصفحات المثبتة"
pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تدبيسها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده." pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده."
pinnedClipId: "معرّف المشبك المدبس" pinnedClipId: "معرّف المشبك المثبت"
pinnedNotes: "ملاحظة مدبسة" pinnedNotes: "ملاحظة مثبتة"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "فعّل hCaptcha" enableHcaptcha: "فعّل hCaptcha"
hcaptchaSiteKey: "مفتاح الموقع" hcaptchaSiteKey: "مفتاح الموقع"
@ -398,6 +406,7 @@ totp: "تطبيق استيثاق"
moderator: "مشرِف" moderator: "مشرِف"
moderation: "الإشراف" moderation: "الإشراف"
nUsersMentioned: "{n} مستخدمين أُشير إليهم" nUsersMentioned: "{n} مستخدمين أُشير إليهم"
securityKeyAndPasskey: "الأمن ومفاتيح الأمان"
securityKey: "مفتاح الأمان" securityKey: "مفتاح الأمان"
lastUsed: "آخر استخدام" lastUsed: "آخر استخدام"
lastUsedAt: "آخر استخدام: {t}" lastUsedAt: "آخر استخدام: {t}"
@ -452,6 +461,7 @@ language: "اللغة"
uiLanguage: "لغة واجهة المستخدم" uiLanguage: "لغة واجهة المستخدم"
aboutX: "عن {x}" aboutX: "عن {x}"
emojiStyle: "نمط الوجوه التعبيرية" emojiStyle: "نمط الوجوه التعبيرية"
showNoteActionsOnlyHover: "أظهر الإجراءات عند التمرير فوق الملاحظة"
noHistory: "السجل فارغ" noHistory: "السجل فارغ"
signinHistory: "تاريخ تسجيل الدخول" signinHistory: "تاريخ تسجيل الدخول"
doing: "انتظر لحظة" doing: "انتظر لحظة"
@ -484,10 +494,12 @@ objectStoragePrefix: "البادئة"
objectStoragePrefixDesc: "ستُحفظ الملفات في مجلدات تحوي اسماءها هذه البادئة." objectStoragePrefixDesc: "ستُحفظ الملفات في مجلدات تحوي اسماءها هذه البادئة."
objectStorageEndpoint: "نقطة النهاية" objectStorageEndpoint: "نقطة النهاية"
objectStorageRegion: "المنطقة" objectStorageRegion: "المنطقة"
objectStorageRegionDesc: "حدد منطقة مثل \"xx-east-1\". إذا كانت خدمتك لا تميز بين المناطق استخدم \"us-east-1\" أو اتركها فارغة إذا كنت تستخدم متغيرات البيئة أو ملفات ضبط AWS."
objectStorageUseSSL: "استخدم SSL" objectStorageUseSSL: "استخدم SSL"
objectStorageUseSSLDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر HTTPS" objectStorageUseSSLDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر HTTPS"
objectStorageUseProxy: "اتصل عبر وكيل" objectStorageUseProxy: "اتصل عبر وكيل"
objectStorageUseProxyDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر وكيل" objectStorageUseProxyDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر وكيل"
objectStorageSetPublicRead: "عينها ك\"علنية\" عند الرفع"
serverLogs: "سجلات الخادم" serverLogs: "سجلات الخادم"
deleteAll: "حذف الكل" deleteAll: "حذف الكل"
showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة" showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة"
@ -531,6 +543,7 @@ accountDeletedDescription: "حُذف هذا الحساب."
menu: "القائمة" menu: "القائمة"
divider: "فاصل" divider: "فاصل"
addItem: "إضافة عنصر" addItem: "إضافة عنصر"
rearrange: "أعد الترتيب"
relays: "المُرَحلات" relays: "المُرَحلات"
addRelay: "إضافة مُرحّل" addRelay: "إضافة مُرحّل"
inboxUrl: "رابط صندوق الوارد" inboxUrl: "رابط صندوق الوارد"
@ -554,6 +567,7 @@ leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المت
manage: "إدارة " manage: "إدارة "
plugins: "الإضافات" plugins: "الإضافات"
preferencesBackups: "النُسخ الاحتياطية للإعدادات" preferencesBackups: "النُسخ الاحتياطية للإعدادات"
useBlurEffectForModal: "استخدم تأثير الطمس في المشروط"
useFullReactionPicker: "استخدم الحجم الكامل لمنتقي التفاعلات" useFullReactionPicker: "استخدم الحجم الكامل لمنتقي التفاعلات"
width: "العرض" width: "العرض"
height: "الإرتفاع" height: "الإرتفاع"
@ -629,6 +643,7 @@ clip: "مِشبك"
createNew: "أنشِئ جديد" createNew: "أنشِئ جديد"
optional: "اختياري" optional: "اختياري"
createNewClip: "أنشئ مِشبكَا جديدًا" createNewClip: "أنشئ مِشبكَا جديدًا"
confirmToUnclipAlreadyClippedNote: "هذه الملاحظة تنتمي للمشبك {name} سلفًا، أتريد حذفها منه⸮"
public: "علني" public: "علني"
i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}" i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}"
manageAccessTokens: "إدارة رموز الوصول" manageAccessTokens: "إدارة رموز الوصول"
@ -650,6 +665,7 @@ driveFilesCount: "عدد الملفات في قرص التخزين"
driveUsage: "المستغل من قرص التخزين" driveUsage: "المستغل من قرص التخزين"
noCrawle: "ارفض فهرسة زاحف الويب" noCrawle: "ارفض فهرسة زاحف الويب"
noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه." noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه."
lockedAccountInfo: "ستكون هذه الملاحظة مرئية للجميع مالم تحدد مرئتيها إلى \"للمتابعين فقط\""
alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس" alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس"
loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات" loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات"
disableShowingAnimatedImages: "لا تشغّل الصور المتحركة" disableShowingAnimatedImages: "لا تشغّل الصور المتحركة"
@ -668,6 +684,8 @@ developer: "المطور"
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\"" makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\"" makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني" showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني"
left: "يسار"
center: "وسط"
wide: "عريض" wide: "عريض"
narrow: "رفيع" narrow: "رفيع"
reloadToApplySetting: "سيُطبق هذا الإعداد بعد إعادة تحميل الصفحة، أتريد إعادة تحميلها الآن؟" reloadToApplySetting: "سيُطبق هذا الإعداد بعد إعادة تحميل الصفحة، أتريد إعادة تحميلها الآن؟"
@ -705,6 +723,7 @@ editCode: "حرر الشفرة"
apply: "تطبيق" apply: "تطبيق"
receiveAnnouncementFromInstance: "استلم إشعارات من هذا المثيل" receiveAnnouncementFromInstance: "استلم إشعارات من هذا المثيل"
emailNotification: "إشعارات البريد الكتروني" emailNotification: "إشعارات البريد الكتروني"
publish: "علني"
inChannelSearch: "ابحث عن قناة" inChannelSearch: "ابحث عن قناة"
useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند النقر بالزر الأيمن" useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند النقر بالزر الأيمن"
typingUsers: "{users} يكتب(ون)..." typingUsers: "{users} يكتب(ون)..."
@ -717,7 +736,7 @@ unlikeConfirm: "أتريد إلغاء إعجابك؟"
fullView: "ملء الشاشة" fullView: "ملء الشاشة"
quitFullView: "اخرج من وضع ملء للشاشة" quitFullView: "اخرج من وضع ملء للشاشة"
addDescription: "أضف وصفًا" addDescription: "أضف وصفًا"
userPagePinTip: "لعرض ملاحظة هنا اختر \"دبسها على الصفحة الشخصية\" من قائمة تلك الملاحظة." userPagePinTip: "لعرض ملاحظة هنا اختر \"ثبتها على الصفحة الشخصية\" من قائمة تلك الملاحظة."
notSpecifiedMentionWarning: "في الملاحظة ذكر لمستخدمين لن يستلموها." notSpecifiedMentionWarning: "في الملاحظة ذكر لمستخدمين لن يستلموها."
info: "عن" info: "عن"
userInfo: "معلومات المستخدم" userInfo: "معلومات المستخدم"
@ -744,6 +763,7 @@ noMaintainerInformationWarning: "لم تُضبط معلومات المدير"
noBotProtectionWarning: "لم تضبط الحماية من الحسابات الآلية" noBotProtectionWarning: "لم تضبط الحماية من الحسابات الآلية"
configure: "اضبط" configure: "اضبط"
postToGallery: "انشر في المعرض" postToGallery: "انشر في المعرض"
postToHashtag: "انشر بهذا الوسم"
gallery: "المعرض" gallery: "المعرض"
recentPosts: "المشاركات الحديثة" recentPosts: "المشاركات الحديثة"
popularPosts: "المشاركات المتداولة" popularPosts: "المشاركات المتداولة"
@ -776,6 +796,7 @@ translate: "ترجم"
translatedFrom: "تُرجم من {x}" translatedFrom: "تُرجم من {x}"
accountDeletionInProgress: "حذف الحساب جارٍ" accountDeletionInProgress: "حذف الحساب جارٍ"
usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله." usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله."
devMode: "وضع المُطوّر"
keepCw: "أبقِ على تحذيرات المحتوى" keepCw: "أبقِ على تحذيرات المحتوى"
lastCommunication: "آخر تواصل" lastCommunication: "آخر تواصل"
resolved: "عولج" resolved: "عولج"
@ -784,6 +805,7 @@ breakFollow: "إلغاء الاشتراك"
breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟" breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟"
itsOn: "مفعّل" itsOn: "مفعّل"
itsOff: "معطّل" itsOff: "معطّل"
on: "مفعل"
emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل" emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
unread: "غير مقروءة" unread: "غير مقروءة"
filter: "رشّح" filter: "رشّح"
@ -820,6 +842,9 @@ oneDay: "يوم"
oneWeek: "أسبوع" oneWeek: "أسبوع"
oneMonth: "شهر" oneMonth: "شهر"
failedToFetchAccountInformation: "تعذر جلب معلومات الحساب" failedToFetchAccountInformation: "تعذر جلب معلومات الحساب"
cropImage: "اقتصاص الصورة"
cropImageAsk: "أتريد اقتصاص هذه الصورة"
cropYes: "اقتص"
cropNo: "استخدمها كما هي" cropNo: "استخدمها كما هي"
file: "الملفات" file: "الملفات"
recentNHours: "آخر {n} ساعة" recentNHours: "آخر {n} ساعة"
@ -830,16 +855,19 @@ recommended: "مقترح"
driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم" driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم"
driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل." driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل."
requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير." requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير."
isSystemAccount: "حساب أنشأه النظام ويُدار من قِبله."
typeToConfirm: "أدخل {x} للتأكيد" typeToConfirm: "أدخل {x} للتأكيد"
deleteAccount: "احذف الحساب" deleteAccount: "احذف الحساب"
document: "التوثيق" document: "التوثيق"
numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا" numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا"
numberOfPageCacheDescription: "رفع الرقم سيسحن تجربة المستخدم لكن سيرفع استهلاك الذاكرة."
logoutConfirm: "أتريد الخروج؟" logoutConfirm: "أتريد الخروج؟"
lastActiveDate: "آخر استخدام" lastActiveDate: "آخر استخدام"
statusbar: "شريط الحالة" statusbar: "شريط الحالة"
pleaseSelect: "حدد خيارًا" pleaseSelect: "حدد خيارًا"
reverse: "اقلب" reverse: "اقلب"
colored: "ملوّن" colored: "ملوّن"
refreshInterval: "مهلة التحديث"
label: "التسمية" label: "التسمية"
type: "نوع" type: "نوع"
speed: "سرعة" speed: "سرعة"
@ -852,6 +880,9 @@ cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محت
cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين." cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين."
cannotUploadBecauseExceedsFileSizeLimit: "تعذر رفع الملف بسبب تجاوز حجمه للحد المسموح" cannotUploadBecauseExceedsFileSizeLimit: "تعذر رفع الملف بسبب تجاوز حجمه للحد المسموح"
beta: "بيتا" beta: "بيتا"
enableAutoSensitive: "تعيين تلقائي كمحتوى حساس NSFW"
enableAutoSensitiveDescription: "عند الاستطاعة يسمح باكتشاف المحتوى حساس NSFW تلقائيًا في الوسائط باستخدام تعلم الآلة ووسمها تبعًا لذلك. قد يكون هذا الخيار مفعلا من جهة الخادم وسيعمل حتى وان عُطل."
activeEmailValidationDescription: "يتحقق من صحة عنوان البريد الإلكتروني بشكل أكثر حزمًا وذلك عبر تحديد ما إذا كان عنوان بريد إلكتروني مؤقت وإمكانية التواصل معه. إذا لم يحدد هذا الخيار فسيتحقق من نسق عنوان البريد الإلكتروني."
navbar: "شريط التنقل" navbar: "شريط التنقل"
shuffle: "خلط" shuffle: "خلط"
account: "الحسابات" account: "الحسابات"
@ -863,22 +894,32 @@ pushNotificationAlreadySubscribed: "إرسال الإشعارات مفعل سل
pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها." pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها."
sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها" sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها"
sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك." sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك."
windowRestore: "استرجاع"
caption: "التعليق التوضيحي" caption: "التعليق التوضيحي"
loggedInAsBot: "والج كآلي"
tools: "أدوات" tools: "أدوات"
cannotLoad: "تعذر التحميل" cannotLoad: "تعذر التحميل"
numberOfProfileView: "مشاهدات الملف الشخصي"
like: "أعجبني" like: "أعجبني"
unlike: "ألغِ الإعجاب" unlike: "ألغِ الإعجاب"
numberOfLikes: "الإعجابات"
show: "المظهر" show: "المظهر"
neverShow: "لا تظهره مجددًا" neverShow: "لا تظهره مجددًا"
remindMeLater: "ربما لاحقا"
didYouLikeMisskey: "هل أعجبك ميسكي؟" didYouLikeMisskey: "هل أعجبك ميسكي؟"
pleaseDonate: "يستخدم {host} البرمجية الحرة ميسكي. نتمنى أن تتبرعوا للمشروع مما سيسمح لنا متابعة تطويره!"
roles: "الأدوار" roles: "الأدوار"
role: "الدور" role: "الدور"
noRole: "لم يُعثر على دور" noRole: "لم يُعثر على دور"
normalUser: "مستخدم عادي" normalUser: "مستخدم عادي"
undefined: "غير معرّف" undefined: "غير معرّف"
assign: "أسند"
unassign: "ألغ الإسناد"
color: "اللون" color: "اللون"
manageCustomEmojis: "إدارة الإيموجي المخصصة" manageCustomEmojis: "إدارة الإيموجي المخصصة"
youCannotCreateAnymore: "وصلت لسقف الإنشاء."
cannotPerformTemporary: "غير متاح مؤقتاً" cannotPerformTemporary: "غير متاح مؤقتاً"
invalidParamError: "معاملات غير صالحة"
permissionDeniedError: "رُفضة العملية" permissionDeniedError: "رُفضة العملية"
preset: "إعدادات مسبقة" preset: "إعدادات مسبقة"
selectFromPresets: "اختر من الإعدادات المسبقة" selectFromPresets: "اختر من الإعدادات المسبقة"
@ -900,6 +941,8 @@ cannotBeChangedLater: "لا يمكن تغييره لاحقًا."
reactionAcceptance: "قبول التفاعلات" reactionAcceptance: "قبول التفاعلات"
rolesAssignedToMe: "الأدوار المسندة إلي" rolesAssignedToMe: "الأدوار المسندة إلي"
resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟" resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟"
license: "الرخصة"
unfavoriteConfirm: "أتريد إزالتها من المفضلة؟"
noteIdOrUrl: "معرف الملاحظة أو رابطها" noteIdOrUrl: "معرف الملاحظة أو رابطها"
video: "فيديو" video: "فيديو"
videos: "فيديوهات" videos: "فيديوهات"
@ -908,6 +951,11 @@ accountMoved: "نقل هذا المستخدم حسابه:"
accountMovedShort: "رُحل هذا الحساب." accountMovedShort: "رُحل هذا الحساب."
operationForbidden: "عملية ممنوعة" operationForbidden: "عملية ممنوعة"
forceShowAds: "أظهر الإعلانات التجارية دائما" forceShowAds: "أظهر الإعلانات التجارية دائما"
leftTop: "أعلى اليسار"
rightTop: "أعلى اليمين"
leftBottom: "أسفل اليسار"
rightBottom: "أسفل اليمين"
stackAxis: "اتجاه التكديس"
vertical: "عمودي" vertical: "عمودي"
horizontal: "جانبي" horizontal: "جانبي"
position: "الموضع" position: "الموضع"
@ -917,9 +965,105 @@ pleaseAgreeAllToContinue: "للمتابعة وافق على الحقول أعل
continue: "متابعة" continue: "متابعة"
preservedUsernames: "أسماء المستخدمين المحجوزة" preservedUsernames: "أسماء المستخدمين المحجوزة"
preservedUsernamesDescription: "قائمة بأسماء المستخدمين المحجوزة كلٌ في سطر. لن يُقبل التسجيل بهذه الأسماء وستبقى محصورة على التسجيل اليدوي بواسطة المديرين. لن يتأثر المستخدمون الذين يملكون هذه الأسماء سلفًا." preservedUsernamesDescription: "قائمة بأسماء المستخدمين المحجوزة كلٌ في سطر. لن يُقبل التسجيل بهذه الأسماء وستبقى محصورة على التسجيل اليدوي بواسطة المديرين. لن يتأثر المستخدمون الذين يملكون هذه الأسماء سلفًا."
createNoteFromTheFile: "أنشئ ملاحظة من هذا الملف"
archive: "الأرشيف" archive: "الأرشيف"
channelArchiveConfirmTitle: "أتريد أرشفت {name}؟"
channelArchiveConfirmDescription: "لن يمكنك نشر ملاحظات في القناة المأرشفة ولن تظهر في قائمة القنوات ولا في نتائج البحث."
thisChannelArchived: "أُرشفت هذه القناة."
displayOfNote: "عرض الملاحظة"
initialAccountSetting: "إعداد الملف الشخصي"
youFollowing: "متابَع" youFollowing: "متابَع"
options: "خيارات" options: "خيارات"
specifyUser: "مستخدم محدد"
failedToPreviewUrl: "تتعذر المعاينة"
update: "حدِّث"
rolesThatCanBeUsedThisEmojiAsReaction: "الأدوار التي يُسمح لأصحابها استخدام هذا اإيموجي في اللتفاعل"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "إذا لم تحدد دورًا يمكن للجميع استخدام هذا الإيموجي في التفاعل."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "يجب أن تكون الأدوار علنية."
cancelReactionConfirm: "أتريد حذف تفاعلك؟"
changeReactionConfirm: "أتريد تعديل تفاعلك؟"
later: "لاحقاً"
goToMisskey: "لميسكي"
additionalEmojiDictionary: "قواميس إيموجي إضافية"
installed: "مُثبت"
_initialAccountSetting:
accountCreated: "نجح إنشاء حسابك!"
letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي."
letsFillYourProfile: "أولًا لنعد ملفك الشخصي."
profileSetting: "إعدادات الملف الشخصي"
privacySetting: "إعدادات الخصوصية"
theseSettingsCanEditLater: "يمكنك تغيير هذه الإعدادات لاحقًا."
skipAreYouSure: "أتريد تخطي إعداد الملف الشخصي؟"
laterAreYouSure: "أتريد إعداد الملف الشخصي لاحقًا؟"
_serverRules:
description: "مجموعة من القواعد لعرضها عند التسجيل، من المستحسن كتابة ملخصٍ للشروط الخدمة."
_accountMigration:
moveFrom: "انقل حسابًا آخر لهذا الحساب"
moveFromLabel: "الحساب الأصلي #{n}"
moveTo: "انقل هذا الحساب لحساب آخر"
moveToLabel: "الحساب الوجهة:"
moveCannotBeUndone: "لا يمكن التراجع عن نقل الحساب."
movedTo: "الحساب الوجهة:"
_achievements:
_types:
_notes1:
description: "انشر ملاحظتك الأولى"
flavor: "تمتع باستخدام ميسكي!"
_notes10:
title: "بعض الملاحظات"
description: "انشر 10 ملاحظات"
_notes100:
title: "كثير من الملاحظات"
description: "انشر 100 ملاحظة"
_notes500:
description: "انشر 500 ملاحظة"
_notes1000:
title: "جبل ملاحظات"
description: "انشر 1000 ملاحظة"
_notes5000:
description: "انشر 5000 ملاحظة"
_notes10000:
description: "انشر 10000 ملاحظة"
_notes20000:
title: "أريد...ملاحظات...أكثر"
description: "انشر 20000 ملاحظة"
_notes30000:
title: "ملاحظات وملاحظات وملاحظات"
description: "انشر 30000 ملاحظة"
_notes40000:
title: "مصنع ملاحظات"
description: "انشر 40000 ملاحظة"
_notes50000:
title: "كوكب ملاحظات"
description: "انشر 50000 ملاحظة"
_notes60000:
title: "نجم ملاحظات"
description: "انشر 60000 ملاحظة"
_notes70000:
title: "ثقب أسود للملاحظات"
description: "انشر 70000 ملاحظة"
_notes80000:
title: "مجرة ملاحظات"
description: "انشر 80000 ملاحظة"
_notes90000:
title: "كوْن ملاحظات"
description: "انشر 90000 ملاحظة"
_notes100000:
title: "كل ملاحظاتك لنا"
description: "انشر 100000 ملاحظة"
flavor: "حقًا لديك الكثير من القصص"
_login3:
title: "مبتدأ I"
_noteFavorited1:
description: "فضًِل ملاحظتك الأولى"
_myNoteFavorited1:
title: "ساعٍ للنجوم"
description: "أعجب شخص آخر بإحدى ملاحظاتك"
_profileFilled:
title: "مستعد"
description: "أعدّ حسابك"
_markedAsCat:
title: "أنا قط"
_role: _role:
new: "دور جديد" new: "دور جديد"
edit: "حرر الأدوار" edit: "حرر الأدوار"
@ -936,6 +1080,7 @@ _role:
high: "عالية" high: "عالية"
_options: _options:
canManageCustomEmojis: "إدارة الإيموجي المخصصة" canManageCustomEmojis: "إدارة الإيموجي المخصصة"
pinMax: "حد عدد الملاحظات المثبتة"
_condition: _condition:
isLocal: "مستخدم محلي" isLocal: "مستخدم محلي"
isRemote: "مستخدم بعيد" isRemote: "مستخدم بعيد"
@ -996,10 +1141,6 @@ _aboutMisskey:
donate: "تبرع لميسكي" donate: "تبرع لميسكي"
morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا لكم 🥰" morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا لكم 🥰"
patrons: "الداعمون" patrons: "الداعمون"
_nsfw:
respect: "اخف الوسائط ذات المحتوى الحساس"
ignore: "اعرض الوسائط ذات المحتوى الحساس"
force: "اخف كل الوسائط"
_instanceTicker: _instanceTicker:
none: "لا تظهره بتاتًا" none: "لا تظهره بتاتًا"
remote: "أظهر للمستخدمين البِعاد" remote: "أظهر للمستخدمين البِعاد"
@ -1316,7 +1457,7 @@ _pages:
url: "رابط الصفحة" url: "رابط الصفحة"
summary: "ملخص الصفحة" summary: "ملخص الصفحة"
alignCenter: "توسيط العناصر" alignCenter: "توسيط العناصر"
hideTitleWhenPinned: "اخف عنوان الصفحة عند تدبيسها في ملف الشخصي" hideTitleWhenPinned: "اخف عنوان الصفحة عند تثبيتها في ملف الشخصي"
font: "الخط" font: "الخط"
fontSerif: "Serif" fontSerif: "Serif"
fontSansSerif: "Sans Serif" fontSansSerif: "Sans Serif"

View file

@ -294,7 +294,6 @@ copyUrl: "URL কপি করুন"
rename: "পুনঃনামকরণ" rename: "পুনঃনামকরণ"
avatar: "প্রোফাইল ছবি" avatar: "প্রোফাইল ছবি"
banner: "ব্যানার" banner: "ব্যানার"
nsfw: "সংবেদনশীল বিষয়বস্তু"
whenServerDisconnected: "সার্ভারের সাথে সংযোগ বিচ্ছিন্ন হয়ে গেলে" whenServerDisconnected: "সার্ভারের সাথে সংযোগ বিচ্ছিন্ন হয়ে গেলে"
disconnectedFromServer: "সার্ভার থেকে সংযোগ বিচ্ছিন্ন হয়েছে" disconnectedFromServer: "সার্ভার থেকে সংযোগ বিচ্ছিন্ন হয়েছে"
reload: "আবার লোড করুন" reload: "আবার লোড করুন"
@ -902,10 +901,6 @@ _aboutMisskey:
donate: "Misskey তে দান করুন" donate: "Misskey তে দান করুন"
morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰" morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰"
patrons: "সমর্থনকারী" patrons: "সমর্থনকারী"
_nsfw:
respect: "স্পর্শকাতর মিডিয়া লুকান"
ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না"
force: "সকল মিডিয়া লুকান"
_instanceTicker: _instanceTicker:
none: "দেখাবেন না" none: "দেখাবেন না"
remote: "রিমোট ব্যাবহারকারীদের জন্য দেখান" remote: "রিমোট ব্যাবহারকারীদের জন্য দেখান"

View file

@ -263,7 +263,6 @@ emptyFolder: "La carpeta està buida"
unableToDelete: "No es pot eliminar" unableToDelete: "No es pot eliminar"
copyUrl: "Copia l'URL" copyUrl: "Copia l'URL"
rename: "Canvia el nom" rename: "Canvia el nom"
nsfw: "NSFW"
reload: "Actualitza" reload: "Actualitza"
doNothing: "Ignora" doNothing: "Ignora"
accept: "Accepta" accept: "Accepta"

View file

@ -276,7 +276,6 @@ copyUrl: "Kopírovat URL"
rename: "Přejmenovat" rename: "Přejmenovat"
avatar: "Avatar" avatar: "Avatar"
banner: "Baner" banner: "Baner"
nsfw: "NSFW"
disconnectedFromServer: "Spojení bylo přerušeno" disconnectedFromServer: "Spojení bylo přerušeno"
reload: "Aktualizovat" reload: "Aktualizovat"
doNothing: "Ignorovat" doNothing: "Ignorovat"

View file

@ -49,11 +49,15 @@ delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten" deleteAndEdit: "Löschen und Bearbeiten"
deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen." deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen."
addToList: "Zu Liste hinzufügen" addToList: "Zu Liste hinzufügen"
addToAntenna: "Zu Antenne hinzufügen"
sendMessage: "Nachricht senden" sendMessage: "Nachricht senden"
copyRSS: "RSS kopieren" copyRSS: "RSS kopieren"
copyUsername: "Benutzernamen kopieren" copyUsername: "Benutzernamen kopieren"
copyUserId: "Benutzer-ID kopieren" copyUserId: "Benutzer-ID kopieren"
copyNoteId: "Notiz-ID kopieren" copyNoteId: "Notiz-ID kopieren"
copyFileId: "Datei-ID kopieren"
copyFolderId: "Ordner-ID kopieren"
copyProfileUrl: "Profil-URL kopieren"
searchUser: "Nach einem Benutzer suchen" searchUser: "Nach einem Benutzer suchen"
reply: "Antworten" reply: "Antworten"
loadMore: "Mehr laden" loadMore: "Mehr laden"
@ -112,7 +116,7 @@ pinnedNote: "Angeheftete Notiz"
pinned: "Angeheftet" pinned: "Angeheftet"
you: "Du" you: "Du"
clickToShow: "Zum Anzeigen anklicken" clickToShow: "Zum Anzeigen anklicken"
sensitive: "NSFW" sensitive: "Sensibel"
add: "Hinzufügen" add: "Hinzufügen"
reaction: "Reaktionen" reaction: "Reaktionen"
reactions: "Reaktionen" reactions: "Reaktionen"
@ -120,8 +124,8 @@ reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen"
reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
rememberNoteVisibility: "Notizsichtbarkeit merken" rememberNoteVisibility: "Notizsichtbarkeit merken"
attachCancel: "Anhang entfernen" attachCancel: "Anhang entfernen"
markAsSensitive: "Als NSFW markieren" markAsSensitive: "Als sensibel markieren"
unmarkAsSensitive: "Als nicht NSFW markieren" unmarkAsSensitive: "Als nicht sensibel markieren"
enterFileName: "Dateinamen eingeben" enterFileName: "Dateinamen eingeben"
mute: "Stummschalten" mute: "Stummschalten"
unmute: "Stummschaltung aufheben" unmute: "Stummschaltung aufheben"
@ -136,8 +140,10 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?"
suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?" suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?"
unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?" unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?"
selectList: "Liste auswählen" selectList: "Liste auswählen"
editList: "Liste bearbeiten"
selectChannel: "Kanal auswählen" selectChannel: "Kanal auswählen"
selectAntenna: "Antenne auswählen" selectAntenna: "Antenne auswählen"
editAntenna: "Antenne bearbeiten"
selectWidget: "Widget auswählen" selectWidget: "Widget auswählen"
editWidgets: "Widgets bearbeiten" editWidgets: "Widgets bearbeiten"
editWidgetsExit: "Fertig" editWidgetsExit: "Fertig"
@ -150,6 +156,8 @@ addEmoji: "Emoji hinzufügen"
settingGuide: "Empfohlene Einstellung" settingGuide: "Empfohlene Einstellung"
cacheRemoteFiles: "Dateien von fremden Instanzen im Cache speichern" cacheRemoteFiles: "Dateien von fremden Instanzen im Cache speichern"
cacheRemoteFilesDescription: "Ist diese Einstellung deaktiviert, so werden Dateien fremder Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz auf diesem Server gespart, aber durch fehlende Generierung von Vorschaubildern mehr Bandbreite verwendet." cacheRemoteFilesDescription: "Ist diese Einstellung deaktiviert, so werden Dateien fremder Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz auf diesem Server gespart, aber durch fehlende Generierung von Vorschaubildern mehr Bandbreite verwendet."
cacheRemoteSensitiveFiles: "Sensitive Dateien von fremden Instanzen im Cache speichern"
cacheRemoteSensitiveFilesDescription: "Ist diese Einstellung deaktiviert, so werden sensitive Dateien fremder Instanzen direkt von dort ohne Zwischenspeicherung geladen."
flagAsBot: "Als Bot markieren" flagAsBot: "Als Bot markieren"
flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Misskeys interne Systeme dieses Benutzerkonto als Bot behandeln." flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Misskeys interne Systeme dieses Benutzerkonto als Bot behandeln."
flagAsCat: "Als Katze markieren" flagAsCat: "Als Katze markieren"
@ -311,7 +319,7 @@ copyUrl: "URL kopieren"
rename: "Umbenennen" rename: "Umbenennen"
avatar: "Profilbild" avatar: "Profilbild"
banner: "Banner" banner: "Banner"
nsfw: "NSFW" displayOfSensitiveMedia: "Anzeige von sensiblen Medien"
whenServerDisconnected: "Bei Verbindungsverlust zum Server" whenServerDisconnected: "Bei Verbindungsverlust zum Server"
disconnectedFromServer: "Die Verbindung zum Server wurde getrennt" disconnectedFromServer: "Die Verbindung zum Server wurde getrennt"
reload: "Aktualisieren" reload: "Aktualisieren"
@ -693,7 +701,7 @@ driveUsage: "Drive-Auslastung"
noCrawle: "Crawler-Indexierung ablehnen" noCrawle: "Crawler-Indexierung ablehnen"
noCrawleDescription: "Suchmaschinen bitten, die eigene Profilseite, Notizen, Seiten usw. nicht zu indexieren." noCrawleDescription: "Suchmaschinen bitten, die eigene Profilseite, Notizen, Seiten usw. nicht zu indexieren."
lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt, wird jede deiner Notizen öffentlich sichtbar sein, sofern du ihre Notizsichtbarkeit nicht auf \"Nur Follower\" setzt." lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt, wird jede deiner Notizen öffentlich sichtbar sein, sofern du ihre Notizsichtbarkeit nicht auf \"Nur Follower\" setzt."
alwaysMarkSensitive: "Medien standardmäßig als NSFW markieren" alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren"
loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen" loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen"
disableShowingAnimatedImages: "Animierte Bilder nicht abspielen" disableShowingAnimatedImages: "Animierte Bilder nicht abspielen"
verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen." verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen."
@ -912,16 +920,16 @@ type: "Art"
speed: "Geschwindigkeit" speed: "Geschwindigkeit"
slow: "Langsam" slow: "Langsam"
fast: "Schnell" fast: "Schnell"
sensitiveMediaDetection: "Erkennung von NSFW-Medien" sensitiveMediaDetection: "Erkennung von sensiblen Medien"
localOnly: "Nur Lokal" localOnly: "Nur Lokal"
remoteOnly: "Nur für fremde Instanzen" remoteOnly: "Nur für fremde Instanzen"
failedToUpload: "Hochladen fehlgeschlagen" failedToUpload: "Hochladen fehlgeschlagen"
cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise NSFW festgestellt wurden." cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise unangebracht festgestellt wurden."
cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist." cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist."
cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden." cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden."
beta: "Beta" beta: "Beta"
enableAutoSensitive: "NSFW-Automarkierung" enableAutoSensitive: "Automarkierung sensibler Medien"
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch NSFW-Markierungen für Medien, die NSFW-Anteile beinhalten. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert." enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch Markierungen für sensible Medien. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft." activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft."
navbar: "Navigationsleiste" navbar: "Navigationsleiste"
shuffle: "Mischen" shuffle: "Mischen"
@ -1063,6 +1071,29 @@ goToMisskey: "Zu Misskey"
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
installed: "Installiert" installed: "Installiert"
branding: "Branding" branding: "Branding"
enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen"
enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren"
turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen."
createInviteCode: "Einladung erstellen"
createWithOptions: "Einladung mit Optionen erstellen"
createCount: "Einladungsanzahl"
inviteCodeCreated: "Einladung erstellt"
inviteLimitExceeded: "Du hast das Maximum an erstellbaren Einladungen erreicht."
createLimitRemaining: "Erstellbare Einladungen: Noch {limit}"
inviteLimitResetCycle: "Am {time} wird dies auf {limit} zurückgesetzt."
expirationDate: "Ablaufdatum"
noExpirationDate: "Keins"
inviteCodeUsedAt: "Einladung verwendet am"
registeredUserUsingInviteCode: "Einladung verwendet von"
waitingForMailAuth: "Bestätigungsemail ausstehend"
inviteCodeCreator: "Einladung erstellt von"
usedAt: "Benutzt am"
unused: "Unbenutzt"
used: "Benutzt"
expired: "Abgelaufen"
doYouAgree: "Zustimmen?"
beSureToReadThisAsItIsImportant: "Lies bitte diese wichtige Informationen."
iHaveReadXCarefullyAndAgree: "Ich habe den Text \"{x}\" gelesen und stimme zu."
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!" accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten." letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
@ -1373,6 +1404,9 @@ _role:
ltlAvailable: "Kann auf die lokale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen" canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Erstellung von Einladungscodes für diese Instanz" canInvite: "Erstellung von Einladungscodes für diese Instanz"
inviteLimit: "Maximalanzahl an Einladungen"
inviteLimitCycle: "Zyklus des Einladungslimits"
inviteExpirationTime: "Gültigkeitsdauer von Einladungen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität" driveCapacity: "Drive-Kapazität"
alwaysMarkNsfw: "Dateien immer als NSFW markieren" alwaysMarkNsfw: "Dateien immer als NSFW markieren"
@ -1403,10 +1437,10 @@ _role:
or: "ODER-Bedingung" or: "ODER-Bedingung"
not: "NICHT-Bedingung" not: "NICHT-Bedingung"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von sensiblen Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
sensitivity: "Erkennungssensitivität" sensitivity: "Erkennungssensitivität"
sensitivityDescription: "Durch das Senken der Sensitivität kann die Anzahl an Fehlerkennungen (sog. false positives) reduziert werden. Durch ein Erhöhen dieser kann die Anzahl an verpassten Erkennungen (sog. false negatives) reduziert werden." sensitivityDescription: "Durch das Senken der Sensitivität kann die Anzahl an Fehlerkennungen (sog. false positives) reduziert werden. Durch ein Erhöhen dieser kann die Anzahl an verpassten Erkennungen (sog. false negatives) reduziert werden."
setSensitiveFlagAutomatically: "Als NSFW markieren" setSensitiveFlagAutomatically: "Als sensibel markieren"
setSensitiveFlagAutomaticallyDescription: "Die Resultate der internen Erkennung werden beibehalten, auch wenn diese Option deaktiviert ist." setSensitiveFlagAutomaticallyDescription: "Die Resultate der internen Erkennung werden beibehalten, auch wenn diese Option deaktiviert ist."
analyzeVideos: "Videoanalyse aktivieren" analyzeVideos: "Videoanalyse aktivieren"
analyzeVideosDescription: "Analysiert zusätzlich zu Bildern auch Videos. Die Last des Servers wird hierdurch etwas erhöht." analyzeVideosDescription: "Analysiert zusätzlich zu Bildern auch Videos. Die Last des Servers wird hierdurch etwas erhöht."
@ -1435,6 +1469,7 @@ _ad:
back: "Zurück" back: "Zurück"
reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen" reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen"
hide: "Ausblenden" hide: "Ausblenden"
timezoneinfo: "Der Wochentag wird durch die Serverzeitzone bestimmt."
_forgotPassword: _forgotPassword:
enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst." enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst."
ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator." ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator."
@ -1486,9 +1521,9 @@ _aboutMisskey:
donate: "An Misskey spenden" donate: "An Misskey spenden"
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰"
patrons: "UnterstützerInnen" patrons: "UnterstützerInnen"
_nsfw: _displayOfSensitiveMedia:
respect: "Als NSFW markierte Bilder verbergen" respect: "Sensible Medien verbergen"
ignore: "Als NSFW markierte Bilder nicht verbergen" ignore: "Sensible Medien anzeigen"
force: "Alle Medien verbergen" force: "Alle Medien verbergen"
_instanceTicker: _instanceTicker:
none: "Nie anzeigen" none: "Nie anzeigen"
@ -1964,6 +1999,7 @@ _deck:
introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!" introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!"
introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen." introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen."
widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu." widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu."
useSimpleUiForNonRootPages: "Simple Benutzeroberfläche für navigierte Seiten verwenden"
_columns: _columns:
main: "Hauptspalte" main: "Hauptspalte"
widgets: "Widgets" widgets: "Widgets"

View file

@ -49,11 +49,15 @@ delete: "Delete"
deleteAndEdit: "Delete and edit" deleteAndEdit: "Delete and edit"
deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it." deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it."
addToList: "Add to list" addToList: "Add to list"
addToAntenna: "Add to antenna"
sendMessage: "Send a message" sendMessage: "Send a message"
copyRSS: "Copy RSS" copyRSS: "Copy RSS"
copyUsername: "Copy username" copyUsername: "Copy username"
copyUserId: "Copy user ID" copyUserId: "Copy user ID"
copyNoteId: "Copy note ID" copyNoteId: "Copy note ID"
copyFileId: "Copy file ID"
copyFolderId: "Copy folder ID"
copyProfileUrl: "Copy profile URL"
searchUser: "Search for a user" searchUser: "Search for a user"
reply: "Reply" reply: "Reply"
loadMore: "Load more" loadMore: "Load more"
@ -112,7 +116,7 @@ pinnedNote: "Pinned note"
pinned: "Pin to profile" pinned: "Pin to profile"
you: "You" you: "You"
clickToShow: "Click to show" clickToShow: "Click to show"
sensitive: "NSFW" sensitive: "Sensitive"
add: "Add" add: "Add"
reaction: "Reactions" reaction: "Reactions"
reactions: "Reactions" reactions: "Reactions"
@ -120,8 +124,8 @@ reactionSetting: "Reactions to show in the reaction picker"
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
rememberNoteVisibility: "Remember note visibility settings" rememberNoteVisibility: "Remember note visibility settings"
attachCancel: "Remove attachment" attachCancel: "Remove attachment"
markAsSensitive: "Mark as NSFW" markAsSensitive: "Mark as sensitive"
unmarkAsSensitive: "Unmark as NSFW" unmarkAsSensitive: "Unmark as sensitive"
enterFileName: "Enter filename" enterFileName: "Enter filename"
mute: "Mute" mute: "Mute"
unmute: "Unmute" unmute: "Unmute"
@ -136,8 +140,10 @@ unblockConfirm: "Are you sure that you want to unblock this account?"
suspendConfirm: "Are you sure that you want to suspend this account?" suspendConfirm: "Are you sure that you want to suspend this account?"
unsuspendConfirm: "Are you sure that you want to unsuspend this account?" unsuspendConfirm: "Are you sure that you want to unsuspend this account?"
selectList: "Select a list" selectList: "Select a list"
editList: "Edit list"
selectChannel: "Select a channel" selectChannel: "Select a channel"
selectAntenna: "Select an antenna" selectAntenna: "Select an antenna"
editAntenna: "Edit antenna"
selectWidget: "Select a widget" selectWidget: "Select a widget"
editWidgets: "Edit widgets" editWidgets: "Edit widgets"
editWidgetsExit: "Done" editWidgetsExit: "Done"
@ -150,6 +156,8 @@ addEmoji: "Add an emoji"
settingGuide: "Recommended settings" settingGuide: "Recommended settings"
cacheRemoteFiles: "Cache remote files" cacheRemoteFiles: "Cache remote files"
cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
cacheRemoteSensitiveFiles: "Cache sensitive remote files"
cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching."
flagAsBot: "Mark this account as a bot" flagAsBot: "Mark this account as a bot"
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot." flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot."
flagAsCat: "Mark this account as a cat" flagAsCat: "Mark this account as a cat"
@ -311,7 +319,7 @@ copyUrl: "Copy URL"
rename: "Rename" rename: "Rename"
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
nsfw: "NSFW" displayOfSensitiveMedia: "Display of sensitive media"
whenServerDisconnected: "When losing connection to the server" whenServerDisconnected: "When losing connection to the server"
disconnectedFromServer: "Connection to server has been lost" disconnectedFromServer: "Connection to server has been lost"
reload: "Refresh" reload: "Refresh"
@ -693,7 +701,7 @@ driveUsage: "Drive space usage"
noCrawle: "Reject crawler indexing" noCrawle: "Reject crawler indexing"
noCrawleDescription: "Ask search engines to not index your profile page, notes, Pages, etc." noCrawleDescription: "Ask search engines to not index your profile page, notes, Pages, etc."
lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved." lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved."
alwaysMarkSensitive: "Mark as NSFW by default" alwaysMarkSensitive: "Mark as sensitive by default"
loadRawImages: "Load original images instead of showing thumbnails" loadRawImages: "Load original images instead of showing thumbnails"
disableShowingAnimatedImages: "Don't play animated images" disableShowingAnimatedImages: "Don't play animated images"
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
@ -912,16 +920,16 @@ type: "Type"
speed: "Speed" speed: "Speed"
slow: "Slow" slow: "Slow"
fast: "Fast" fast: "Fast"
sensitiveMediaDetection: "Detection of NSFW media" sensitiveMediaDetection: "Detection of sensitive media"
localOnly: "Local only" localOnly: "Local only"
remoteOnly: "Remote only" remoteOnly: "Remote only"
failedToUpload: "Upload failed" failedToUpload: "Upload failed"
cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially NSFW." cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially inappropriate."
cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity." cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity."
cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit." cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit."
beta: "Beta" beta: "Beta"
enableAutoSensitive: "Automatic NSFW-Marking" enableAutoSensitive: "Automatic marking as sensitive"
enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide." enableAutoSensitiveDescription: "Allows automatic detection and marking of sensitive media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated." activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated."
navbar: "Navigation bar" navbar: "Navigation bar"
shuffle: "Shuffle" shuffle: "Shuffle"
@ -1063,6 +1071,29 @@ goToMisskey: "To Misskey"
additionalEmojiDictionary: "Additional emoji dictionaries" additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed" installed: "Installed"
branding: "Branding" branding: "Branding"
enableServerMachineStats: "Publish server hardware stats"
enableIdenticonGeneration: "Enable user identicon generation"
turnOffToImprovePerformance: "Turning this off can increase performance."
createInviteCode: "Generate invite"
createWithOptions: "Generate with options"
createCount: "Invite count"
inviteCodeCreated: "Invite generated"
inviteLimitExceeded: "You've exceeded the limit of invites you can generate."
createLimitRemaining: "Invite limit: {limit} remaining"
inviteLimitResetCycle: "This limit will reset to {limit} at {time}."
expirationDate: "Expiration date"
noExpirationDate: "No expiration"
inviteCodeUsedAt: "Invite code used at"
registeredUserUsingInviteCode: "Invite used by"
waitingForMailAuth: "Email verification pending"
inviteCodeCreator: "Invite created by"
usedAt: "Used at"
unused: "Unused"
used: "Used"
expired: "Expired"
doYouAgree: "Agree?"
beSureToReadThisAsItIsImportant: "Please read this important information."
iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree."
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Your account was successfully created!" accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile." letsStartAccountSetup: "For starters, let's set up your profile."
@ -1373,6 +1404,9 @@ _role:
ltlAvailable: "Can view the local timeline" ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes" canPublicNote: "Can send public notes"
canInvite: "Can create instance invite codes" canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown"
inviteExpirationTime: "Invite expiration interval"
canManageCustomEmojis: "Can manage custom emojis" canManageCustomEmojis: "Can manage custom emojis"
driveCapacity: "Drive capacity" driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW" alwaysMarkNsfw: "Always mark files as NSFW"
@ -1403,10 +1437,10 @@ _role:
or: "OR-Condition" or: "OR-Condition"
not: "NOT-Condition" not: "NOT-Condition"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." description: "Reduces the effort of server moderation through automatically recognizing sensitive media via Machine Learning. This will slightly increase the load on the server."
sensitivity: "Detection sensitivity" sensitivity: "Detection sensitivity"
sensitivityDescription: "Reducing the sensitivity will lead to fewer misdetections (false positives) whereas increasing it will lead to fewer missed detections (false negatives)." sensitivityDescription: "Reducing the sensitivity will lead to fewer misdetections (false positives) whereas increasing it will lead to fewer missed detections (false negatives)."
setSensitiveFlagAutomatically: "Mark as NSFW" setSensitiveFlagAutomatically: "Mark as sensitive"
setSensitiveFlagAutomaticallyDescription: "The results of the internal detection will be retained even if this option is turned off." setSensitiveFlagAutomaticallyDescription: "The results of the internal detection will be retained even if this option is turned off."
analyzeVideos: "Enable analysis of videos" analyzeVideos: "Enable analysis of videos"
analyzeVideosDescription: "Analyzes videos in addition to images. This will slightly increase the load on the server." analyzeVideosDescription: "Analyzes videos in addition to images. This will slightly increase the load on the server."
@ -1435,6 +1469,7 @@ _ad:
back: "Back" back: "Back"
reduceFrequencyOfThisAd: "Show this ad less" reduceFrequencyOfThisAd: "Show this ad less"
hide: "Hide" hide: "Hide"
timezoneinfo: "The day of the week is determined from the server's timezone."
_forgotPassword: _forgotPassword:
enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it." enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it."
ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead." ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead."
@ -1486,9 +1521,9 @@ _aboutMisskey:
donate: "Donate to Misskey" donate: "Donate to Misskey"
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
patrons: "Patrons" patrons: "Patrons"
_nsfw: _displayOfSensitiveMedia:
respect: "Hide NSFW media" respect: "Hide media marked as sensitive"
ignore: "Don't hide NSFW media" ignore: "Display media marked as sensitive"
force: "Hide all media" force: "Hide all media"
_instanceTicker: _instanceTicker:
none: "Never show" none: "Never show"
@ -1964,6 +1999,7 @@ _deck:
introduction: "Create the perfect interface for you by arranging columns freely!" introduction: "Create the perfect interface for you by arranging columns freely!"
introduction2: "Click on the + on the right of the screen to add new colums whenever you want." introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget." widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."
useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
_columns: _columns:
main: "Main" main: "Main"
widgets: "Widgets" widgets: "Widgets"

View file

@ -49,9 +49,15 @@ delete: "Borrar"
deleteAndEdit: "Borrar y editar" deleteAndEdit: "Borrar y editar"
deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas." deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas."
addToList: "Agregar a lista" addToList: "Agregar a lista"
addToAntenna: "Añadir a la antena"
sendMessage: "Enviar un mensaje" sendMessage: "Enviar un mensaje"
copyRSS: "Copiar RSS" copyRSS: "Copiar RSS"
copyUsername: "Copiar nombre de usuario" copyUsername: "Copiar nombre de usuario"
copyUserId: "Copiar ID del usuario"
copyNoteId: "Copiar ID de la nota"
copyFileId: "Copiar un archivo ID"
copyFolderId: "Copiar carpeta ID"
copyProfileUrl: "Copiar la URL del perfil"
searchUser: "Buscar un usuario" searchUser: "Buscar un usuario"
reply: "Responder" reply: "Responder"
loadMore: "Ver más" loadMore: "Ver más"
@ -134,8 +140,10 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
suspendConfirm: "¿Quiere suspender esta cuenta?" suspendConfirm: "¿Quiere suspender esta cuenta?"
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?" unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
selectList: "Seleccione una lista" selectList: "Seleccione una lista"
editList: "Editar lista"
selectChannel: "Seleccionar canal" selectChannel: "Seleccionar canal"
selectAntenna: "Seleccionar antena" selectAntenna: "Seleccionar antena"
editAntenna: "Editar antena"
selectWidget: "Seleccionar widget" selectWidget: "Seleccionar widget"
editWidgets: "Editar widgets" editWidgets: "Editar widgets"
editWidgetsExit: "Terminar edición" editWidgetsExit: "Terminar edición"
@ -148,6 +156,8 @@ addEmoji: "Agregar emoji"
settingGuide: "Configuración sugerida" settingGuide: "Configuración sugerida"
cacheRemoteFiles: "Mantener en cache los archivos remotos" cacheRemoteFiles: "Mantener en cache los archivos remotos"
cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas." cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas."
cacheRemoteSensitiveFiles: "Cachear archivos remotos sensibles"
cacheRemoteSensitiveFilesDescription: "Cuando esta opción está desactivada, los archivos remotos sensibles son cargador directamente de la instancia origen sin ser cacheados."
flagAsBot: "Esta cuenta es un bot" flagAsBot: "Esta cuenta es un bot"
flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de Misskey para que trate a esta cuenta como un bot." flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de Misskey para que trate a esta cuenta como un bot."
flagAsCat: "Esta cuenta es un gato" flagAsCat: "Esta cuenta es un gato"
@ -229,10 +239,10 @@ instanceFollowers: "Seguidores de la instancia"
instanceUsers: "Usuarios de la instancia" instanceUsers: "Usuarios de la instancia"
changePassword: "Cambiar contraseña" changePassword: "Cambiar contraseña"
security: "Seguridad" security: "Seguridad"
retypedNotMatch: "No hay coincidencia" retypedNotMatch: "La información no coincide."
currentPassword: "Contraseña actual" currentPassword: "Contraseña actual"
newPassword: "Contraseña nueva" newPassword: "Contraseña nueva"
newPasswordRetype: "Contraseña nueva (repetir)" newPasswordRetype: "Reescribe contraseña nueva"
attachFile: "Añadir archivo" attachFile: "Añadir archivo"
more: "¡Más!" more: "¡Más!"
featured: "Destacados" featured: "Destacados"
@ -309,7 +319,7 @@ copyUrl: "Copiar URL"
rename: "Renombrar" rename: "Renombrar"
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
nsfw: "Marcado como sensible" displayOfSensitiveMedia: "Mostrar contenido sensible"
whenServerDisconnected: "Cuando se pierda la conexión con el servidor" whenServerDisconnected: "Cuando se pierda la conexión con el servidor"
disconnectedFromServer: "Desconectado del servidor" disconnectedFromServer: "Desconectado del servidor"
reload: "Recargar" reload: "Recargar"
@ -433,7 +443,7 @@ title: "Título"
text: "Texto" text: "Texto"
enable: "Activar" enable: "Activar"
next: "Siguiente" next: "Siguiente"
retype: "Intentar de nuevo" retype: "Ingrese de nuevo"
noteOf: "Notas de {user}" noteOf: "Notas de {user}"
quoteAttached: "Cita añadida" quoteAttached: "Cita añadida"
quoteQuestion: "¿Quiere añadir una cita?" quoteQuestion: "¿Quiere añadir una cita?"
@ -453,7 +463,7 @@ weakPassword: "Contraseña débil"
normalPassword: "Buena contraseña" normalPassword: "Buena contraseña"
strongPassword: "Muy buena contraseña" strongPassword: "Muy buena contraseña"
passwordMatched: "Correcto" passwordMatched: "Correcto"
passwordNotMatched: "Las contraseñas no son las mismas" passwordNotMatched: "Las contraseñas no coinciden"
signinWith: "Inicie sesión con {x}" signinWith: "Inicie sesión con {x}"
signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos." signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos."
or: "O" or: "O"
@ -790,8 +800,9 @@ noMaintainerInformationWarning: "No se ha establecido la información del admini
noBotProtectionWarning: "La protección contra los bots no está configurada" noBotProtectionWarning: "La protección contra los bots no está configurada"
configure: "Configurar" configure: "Configurar"
postToGallery: "Crear una nueva publicación en la galería" postToGallery: "Crear una nueva publicación en la galería"
postToHashtag: "Publicar a este hashtag"
gallery: "Galería" gallery: "Galería"
recentPosts: "Posts recientes" recentPosts: "Publicaciones recientes"
popularPosts: "Más vistos" popularPosts: "Más vistos"
shareWithNote: "Compartir con una nota" shareWithNote: "Compartir con una nota"
ads: "Anuncios" ads: "Anuncios"
@ -823,6 +834,7 @@ translatedFrom: "Traducido de {x}"
accountDeletionInProgress: "La eliminación de la cuenta está en curso" accountDeletionInProgress: "La eliminación de la cuenta está en curso"
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente." usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
aiChanMode: "Modo Ai" aiChanMode: "Modo Ai"
devMode: "Modo de desarrollador"
keepCw: "Mantener la advertencia de contenido" keepCw: "Mantener la advertencia de contenido"
pubSub: "Cuentas Pub/Sub" pubSub: "Cuentas Pub/Sub"
lastCommunication: "Última comunicación" lastCommunication: "Última comunicación"
@ -832,15 +844,17 @@ breakFollow: "Dejar de seguir"
breakFollowConfirm: "¿Quieres dejar de seguir?" breakFollowConfirm: "¿Quieres dejar de seguir?"
itsOn: "¡Está encendido!" itsOn: "¡Está encendido!"
itsOff: "¡Está apagado!" itsOff: "¡Está apagado!"
emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro de la cuenta" on: "Activado"
off: "Desactivado"
emailRequiredForSignup: "Se requiere una dirección de correo electrónico para el registro de la cuenta"
unread: "No leído" unread: "No leído"
filter: "Filtro" filter: "Filtrar"
controlPanel: "Panel de control" controlPanel: "Panel de control"
manageAccounts: "Administrar cuenta" manageAccounts: "Administrar cuenta"
makeReactionsPublic: "Hacer el historial de reacciones público" makeReactionsPublic: "Hacer el historial de reacciones público"
makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles." makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles."
classic: "Clásico" classic: "Clásico"
muteThread: "Ocultar hilo" muteThread: "Silenciar hilo"
unmuteThread: "Mostrar hilo" unmuteThread: "Mostrar hilo"
ffVisibility: "Visibilidad de seguidores y seguidos" ffVisibility: "Visibilidad de seguidores y seguidos"
ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen" ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen"
@ -912,6 +926,7 @@ remoteOnly: "Sólo remoto"
failedToUpload: "La subida falló" failedToUpload: "La subida falló"
cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas partes han sido detectadas comoNSFW." cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas partes han sido detectadas comoNSFW."
cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en la unidad del usuario." cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en la unidad del usuario."
cannotUploadBecauseExceedsFileSizeLimit: "Este archivo supera el peso máximo y no puede ser subido."
beta: "Beta" beta: "Beta"
enableAutoSensitive: "Marcar automáticamente contenido NSFW" enableAutoSensitive: "Marcar automáticamente contenido NSFW"
enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está desactivada, puede ser activado para toda la instancia." enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está desactivada, puede ser activado para toda la instancia."
@ -942,18 +957,23 @@ show: "Apariencia"
neverShow: "No mostrar de nuevo" neverShow: "No mostrar de nuevo"
remindMeLater: "Recordar después" remindMeLater: "Recordar después"
didYouLikeMisskey: "¿Te gusta Misskey?" didYouLikeMisskey: "¿Te gusta Misskey?"
pleaseDonate: "Misskey es software libre, y es usado por {host} . Por favor, ¡considera donar al proyecto principal para que podamos continuar!" pleaseDonate: "{host} usa el software gratuito Misskey. Por favor ¡Considera donar al proyecto principal para que podamos continuar!"
roles: "Roles" roles: "Roles"
role: "Roles" role: "Rol"
noRole: "Rol no encontrado"
normalUser: "Usuario normal" normalUser: "Usuario normal"
undefined: "Indefinido" undefined: "Indefinido"
assign: "Asignar" assign: "Asignar"
unassign: "Quitar" unassign: "Quitar"
color: "Color" color: "Color"
manageCustomEmojis: "Administrar emojis personalizados" manageCustomEmojis: "Administrar emojis personalizados"
youCannotCreateAnymore: "Se alcanzó el límite de creación" youCannotCreateAnymore: "Has llegado al límite de creaciones."
cannotPerformTemporary: "Indisponible temporalmente" cannotPerformTemporary: "Temporalmente no disponible"
cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo." cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo."
invalidParamError: "Parámetros inválidos"
invalidParamErrorDescription: "Los parámetros de la solicitud son inválidos. Normalmente se trata de un error, pero también puede haberse excedido algún límite o similares."
permissionDeniedError: "Operación denegada"
permissionDeniedErrorDescription: "Esta cuenta no tiene permisos para hacer esa acción."
preset: "Predefinido" preset: "Predefinido"
selectFromPresets: "Escoger desde predefinidos" selectFromPresets: "Escoger desde predefinidos"
achievements: "Logros" achievements: "Logros"
@ -969,7 +989,7 @@ internalServerErrorDescription: "El servidor tuvo un error inesperado."
copyErrorInfo: "Copiar detalles del error" copyErrorInfo: "Copiar detalles del error"
joinThisServer: "Registrarse en esta instancia" joinThisServer: "Registrarse en esta instancia"
exploreOtherServers: "Buscar otra instancia" exploreOtherServers: "Buscar otra instancia"
letsLookAtTimeline: "Mirar la línea de tiempo local" letsLookAtTimeline: "Mira la línea de tiempo"
disableFederationConfirm: "¿Estas seguro que quieres desactivar la federación?" disableFederationConfirm: "¿Estas seguro que quieres desactivar la federación?"
disableFederationConfirmWarn: "Aunque no exista federación los posts no serán marcados como privados. En la mayoría de los casos, no es necesario hacer los posts no federar." disableFederationConfirmWarn: "Aunque no exista federación los posts no serán marcados como privados. En la mayoría de los casos, no es necesario hacer los posts no federar."
disableFederationOk: "Desactivar." disableFederationOk: "Desactivar."
@ -980,10 +1000,13 @@ cannotBeChangedLater: "Esto no podrá ser cambiado después."
reactionAcceptance: "Aceptación de reacciones" reactionAcceptance: "Aceptación de reacciones"
likeOnly: "Sólo 'me gusta'" likeOnly: "Sólo 'me gusta'"
likeOnlyForRemote: "Sólo reacciones de instancias remotas" likeOnlyForRemote: "Sólo reacciones de instancias remotas"
nonSensitiveOnly: "Solo no sensible"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Sólo no contenido sensible (sólo me gusta en remote)"
rolesAssignedToMe: "Roles asignados a mí" rolesAssignedToMe: "Roles asignados a mí"
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
sensitiveWords: "Palabras sensibles" sensitiveWords: "Palabras sensibles"
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares."
notesSearchNotAvailable: "No se puede buscar una nota" notesSearchNotAvailable: "No se puede buscar una nota"
license: "Licencia" license: "Licencia"
unfavoriteConfirm: "¿Desea quitar de favoritos?" unfavoriteConfirm: "¿Desea quitar de favoritos?"
@ -997,22 +1020,108 @@ enableChartsForFederatedInstances: "Generar gráficos de servidores remotos"
showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas" showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas"
largeNoteReactions: "Agrandar las reacciones de las notas" largeNoteReactions: "Agrandar las reacciones de las notas"
noteIdOrUrl: "ID o URL de la nota" noteIdOrUrl: "ID o URL de la nota"
video: "Video"
videos: "Video"
dataSaver: "Ahorro de datos"
accountMigration: "Migración de cuenta" accountMigration: "Migración de cuenta"
accountMoved: "Este usuario se ha mudado a una nueva cuenta:" accountMoved: "Este usuario se movió a una nueva cuenta:"
accountMovedShort: "Esta cuenta ha sido migrada." accountMovedShort: "Esta cuenta ha sido migrada."
operationForbidden: "Operación prohibida"
forceShowAds: "Siempre mostrar anuncios"
addMemo: "Añadir nota"
editMemo: "Editar nota"
reactionsList: "Lista de reacciones"
renotesList: "Renotas"
notificationDisplay: "Notificaciones"
leftTop: "Arriba a la izquierda"
rightTop: "Arriba a la derecha"
leftBottom: "Abajo a la izquierda"
rightBottom: "Abajo a la derecha"
stackAxis: "Dirección de apilado"
vertical: "Vertical"
horizontal: "Horizontal" horizontal: "Horizontal"
position: "Posición"
serverRules: "Reglas del servidor"
pleaseConfirmBelowBeforeSignup: "Por favor confirma antes de continuar el registro"
pleaseAgreeAllToContinue: "Tienes que estar de acuerdo con los campos anteriores para contnuar."
continue: "Continuar"
preservedUsernames: "Nombre de usuario reservado"
preservedUsernamesDescription: "La lista de nombres de usuario para reservar tienen que separarse con saltos de línea.\nEstos estarán indisponibles durante la creación de cuentas, pero pueden ser usados para que los administradores puedan crear esas cuentas manualmente. Las cuentas existentes con esos nombres de usuario no se verán afectadas."
createNoteFromTheFile: "Componer una nota desde éste archivo"
archive: "Archivo"
channelArchiveConfirmTitle: "¿Seguro de archivar {name}?"
channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas."
thisChannelArchived: "El canal ha sido archivado."
displayOfNote: "Mostrar notas"
initialAccountSetting: "Configración inicial de su cuenta\nか\nConfigración de inicio"
youFollowing: "Siguiendo" youFollowing: "Siguiendo"
preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generativa)"
preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada."
options: "Opción" options: "Opción"
specifyUser: "Especificar usuario"
failedToPreviewUrl: "No se pudo generar la vista previa"
update: "Actualizar"
rolesThatCanBeUsedThisEmojiAsReaction: "Roles que pueden usar este emoji como reacción"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si no se especifican roles, cualquiera podrá usar éste emoji como reacción."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Éstos roles deben ser públicos."
cancelReactionConfirm: "¿Realmente quieres eliminar la reacción?"
changeReactionConfirm: "¿Realmente quieres cambiar la reacción?"
later: "Ahora no"
goToMisskey: "ir a Misskey"
additionalEmojiDictionary: "Diccionario adicional de Emoji"
installed: "Instalado"
branding: "Marca"
enableServerMachineStats: "Publicar estadísticas de hardware del servidor"
enableIdenticonGeneration: "Activar generación de identicon por usuario"
turnOffToImprovePerformance: "Desactivar esto puede aumentar el rendimiento."
createInviteCode: "Generar invitación"
createWithOptions: "Generar con opciones"
createCount: "Conteo de invitaciones"
inviteCodeCreated: "Invitación generada"
inviteLimitExceeded: "Has excedido el límite de invitaciones que puedes generar."
createLimitRemaining: "Límite de invitaciones: quedan {limit}"
inviteLimitResetCycle: "El límite ha sido reiniciado a {limit} por {time}."
expirationDate: "Fecha de caducidad"
noExpirationDate: "Sin caducidad"
inviteCodeUsedAt: "Código de invitación usado el"
registeredUserUsingInviteCode: "Invitación usada por"
waitingForMailAuth: "Verificación de correo pendiente"
inviteCodeCreator: "Invitación creada por"
usedAt: "Usada el"
unused: "Sin usar"
used: "Usada"
expired: "Caducada"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "¡La cuenta ha sido creada!" accountCreated: "¡La cuenta ha sido creada!"
letsStartAccountSetup: "Para empezar, creemos tu perfil."
letsFillYourProfile: "Primero, creemos tu perfil."
profileSetting: "Configuración del perfil"
privacySetting: "Configuración de privacidad"
theseSettingsCanEditLater: "Puedes cambiar estos ajustes más tarde."
youCanEditMoreSettingsInSettingsPageLater: "Desde la pestaña de \"Configuración\" puedes modificar más ajustes. Asegúrate de visitarla después."
followUsers: "Comienza a seguir a usuarios que te interesen para construir tu línea de tiempo."
pushNotificationDescription: "Habilitar las notificaciones push te permitirá recibir notificaciones de {name} directamente en tu dispositivo."
initialAccountSettingCompleted: "¡Configuración del perfil completada!"
haveFun: "¡Disfruta de {name}!"
ifYouNeedLearnMore: "Si quieres aprender cómo usar {name} (Misskey), por favor, visita {link}."
skipAreYouSure: "¿Realmente quieres saltarte la configuración del perfil?"
laterAreYouSure: "¿Realmente quieres configurar tu perfil después?"
_serverRules:
description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado."
_accountMigration: _accountMigration:
moveFrom: "Trasladar de otra cuenta a ésta" moveFrom: "Trasladar de otra cuenta a ésta"
moveFromSub: "Crear un alias para otra cuenta."
moveFromLabel: "Cuenta desde la que se realiza el traslado:" moveFromLabel: "Cuenta desde la que se realiza el traslado:"
moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com" moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com"
moveTo: "Mover esta cuenta a una nueva" moveTo: "Mover esta cuenta a una nueva"
moveToLabel: "Cuenta destino:" moveToLabel: "Cuenta destino:"
moveCannotBeUndone: "La migración de la cuenta no puede ser revertida."
moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com" moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com"
moveAccountHowTo: "Para migrar, primero crea un alias para ésta cuenta en la cuenta a donde te moverás.\nDespués de crear el alias, ingresa la cuenta a mover de la siguiente forma:\n@usuario@servidor.ejempo.com"
startMigration: "Migrar"
migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado." migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado."
movedAndCannotBeUndone: "\nLa migración decuenta ha sido completada.\nNo se puede revertir éste proceso."
postMigrationNote: "Ésta cuenta dejará de seguir a todas las cuentas en las siguientes 24 horas después de que finalice la migración.\nEl número de seguidos y seguidores serán 0. Para evitar que Para evitar que tus seguidores dejen de ver las publicaciones, todas serán marcadas como \"sólo seguidores\"."
movedTo: "Cuenta destino:" movedTo: "Cuenta destino:"
_achievements: _achievements:
earnedAt: "Desbloqueado el" earnedAt: "Desbloqueado el"
@ -1187,6 +1296,7 @@ _achievements:
description: "30 minutos dedicados a Misskey" description: "30 minutos dedicados a Misskey"
_client60min: _client60min:
title: "Viendo mucho Misskey." title: "Viendo mucho Misskey."
description: "Dejar abierto Misskey por al menos 60 minutos"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Ah... Mejor no..." title: "Ah... Mejor no..."
description: "Borrar una nota antes que de pase 1 minuto" description: "Borrar una nota antes que de pase 1 minuto"
@ -1275,6 +1385,8 @@ _role:
iconUrl: "URL del ícono" iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema" asBadge: "Mostrar como emblema"
descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo."
isExplorable: "Hacer el rol explorable"
descriptionOfIsExplorable: "La línea de tiempo de éste rol y la lista de usuarios serán públicos si se activa.."
displayOrder: "Posición" displayOrder: "Posición"
descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz." descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz."
canEditMembersByModerator: "Permitir a los moderadores editar los miembros" canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
@ -1289,8 +1401,12 @@ _role:
ltlAvailable: "Explorar la línea de tiempo local" ltlAvailable: "Explorar la línea de tiempo local"
canPublicNote: "Permitir la publicación" canPublicNote: "Permitir la publicación"
canInvite: "Puede crear códigos de invitación" canInvite: "Puede crear códigos de invitación"
inviteLimit: "Límite de invitaciones"
inviteLimitCycle: "Enfriamiento del límite de invitaciones"
inviteExpirationTime: "Intervalo de caducidad de invitaciones"
canManageCustomEmojis: "Administrar emojis personalizados" canManageCustomEmojis: "Administrar emojis personalizados"
driveCapacity: "Capacidad de almacenamiento" driveCapacity: "Capacidad del drive"
alwaysMarkNsfw: "Siempre marcar archivos como NSFW"
pinMax: "Máximo de notas fijadas" pinMax: "Máximo de notas fijadas"
antennaMax: "Máximo de antenas" antennaMax: "Máximo de antenas"
wordMuteMax: "Máximo de caracteres en palabras silenciadas" wordMuteMax: "Máximo de caracteres en palabras silenciadas"
@ -1350,6 +1466,7 @@ _ad:
back: "Deseleccionar" back: "Deseleccionar"
reduceFrequencyOfThisAd: "Mostrar menos este anuncio." reduceFrequencyOfThisAd: "Mostrar menos este anuncio."
hide: "No mostrar" hide: "No mostrar"
timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor."
_forgotPassword: _forgotPassword:
enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña." enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña."
ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador." ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador."
@ -1401,10 +1518,10 @@ _aboutMisskey:
donate: "Donar a Misskey" donate: "Donar a Misskey"
morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰" morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰"
patrons: "Patrocinadores" patrons: "Patrocinadores"
_nsfw: _displayOfSensitiveMedia:
respect: "Ocultar medios NSFW" respect: "Esconder medios marcados como sensibles"
ignore: "No esconder medios NSFW " ignore: "Mostrar medios marcados como sensibles"
force: "Ocultar todos los medios" force: "Esconder todala multimedia"
_instanceTicker: _instanceTicker:
none: "No mostrar" none: "No mostrar"
remote: "Mostrar a usuarios remotos" remote: "Mostrar a usuarios remotos"
@ -1423,6 +1540,8 @@ _channel:
following: "Siguiendo" following: "Siguiendo"
usersCount: "{n} participantes" usersCount: "{n} participantes"
notesCount: "{n} notas" notesCount: "{n} notas"
nameAndDescription: "Nombre y descripción"
nameOnly: "Sólo nombre"
_menuDisplay: _menuDisplay:
sideFull: "Horizontal" sideFull: "Horizontal"
sideIcon: "Horizontal (ícono)" sideIcon: "Horizontal (ícono)"
@ -1526,7 +1645,7 @@ _sfx:
channel: "Notificaciones del canal" channel: "Notificaciones del canal"
_ago: _ago:
future: "Futuro" future: "Futuro"
justNow: "Recién ahora" justNow: "Justo ahora"
secondsAgo: "Hace {n} segundos" secondsAgo: "Hace {n} segundos"
minutesAgo: "Hace {n} minutos" minutesAgo: "Hace {n} minutos"
hoursAgo: "Hace {n} horas" hoursAgo: "Hace {n} horas"
@ -1540,6 +1659,16 @@ _time:
minute: "Minutos" minute: "Minutos"
hour: "Horas" hour: "Horas"
day: "Días" day: "Días"
_timelineTutorial:
title: "Cómo usar Misskey"
step1_1: "Ésta es la \"línea de tiempo\". Todas las \"notas\" que sean publicadas en {name} serán mostradas cronológicamente aquí."
step1_2: "Hay varias líneas de tiempo. Por ejemplo, la línea temporal \"Inicio\" contiene las notas de otros usuarios que sigues, y la línea \"Local\" contandrá las notas de todos los usuarios de {name}."
step2_1: "Ahora probemos publicar una nota. Puedes hacerlo presionando el botón que tiene un ícono de lápiz."
step2_2: "¿Qué tal si escribimos una introducción? o sólo un \"¡Hola {name}!\" ¿No te apetece?"
step3_1: "¿Terminaste de publicar tu primera nota?"
step3_2: "Tu primera nota ahora se mostrará en tu línea de tiempo."
step4_1: "También puedes añadir \"Reacciones\" a notas."
step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar."
_2fa: _2fa:
alreadyRegistered: "Ya has completado la configuración." alreadyRegistered: "Ya has completado la configuración."
registerTOTP: "Registrar aplicación autenticadora" registerTOTP: "Registrar aplicación autenticadora"
@ -1685,14 +1814,14 @@ _visibility:
homeDescription: "Visible sólo en la linea de tiempo de inicio" homeDescription: "Visible sólo en la linea de tiempo de inicio"
followers: "Seguidores" followers: "Seguidores"
followersDescription: "Visible sólo para tus seguidores" followersDescription: "Visible sólo para tus seguidores"
specified: "Mensaje directo" specified: "Nota directa"
specifiedDescription: "Visible sólo para los usuarios elegidos" specifiedDescription: "Visible sólo para los usuarios elegidos"
disableFederation: "No federado" disableFederation: "No federado"
disableFederationDescription: "No enviar a otras instancias" disableFederationDescription: "No enviar a otras instancias"
_postForm: _postForm:
replyPlaceholder: "Responder a esta nota" replyPlaceholder: "Responder a esta nota"
quotePlaceholder: "Citar esta nota" quotePlaceholder: "Citar esta nota"
channelPlaceholder: "Postear en el canal" channelPlaceholder: "Publicar en el canal"
_placeholders: _placeholders:
a: "¿Qué haces?" a: "¿Qué haces?"
b: "¿Te pasó algo?" b: "¿Te pasó algo?"
@ -1867,6 +1996,7 @@ _deck:
introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!" introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!"
introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras." introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras."
widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget." widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget."
useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple"
_columns: _columns:
main: "Principal" main: "Principal"
widgets: "Widgets" widgets: "Widgets"
@ -1876,7 +2006,8 @@ _deck:
list: "Listas" list: "Listas"
channel: "Canal" channel: "Canal"
mentions: "Menciones" mentions: "Menciones"
direct: "Mensaje directo" direct: "Notas directas"
roleTimeline: "Linea de tiempo del rol"
_dialog: _dialog:
charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." 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}." charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}."
@ -1884,8 +2015,8 @@ _disabledTimeline:
title: "Línea de tiempo deshabilitada" title: "Línea de tiempo deshabilitada"
description: "No puedes usar esta línea de tiempo con tus roles actuales." description: "No puedes usar esta línea de tiempo con tus roles actuales."
_drivecleaner: _drivecleaner:
orderBySizeDesc: "Más grandes" orderBySizeDesc: "Tamaño descendiente"
orderByCreatedAtAsc: "Más antiguos" orderByCreatedAtAsc: "Fecha ascendente"
_webhookSettings: _webhookSettings:
createWebhook: "Crear Webhook" createWebhook: "Crear Webhook"
name: "Nombre" name: "Nombre"

View file

@ -52,6 +52,11 @@ addToList: "Ajouter à une liste"
sendMessage: "Envoyer un message" sendMessage: "Envoyer un message"
copyRSS: "Copier le RSS" copyRSS: "Copier le RSS"
copyUsername: "Copier le nom dutilisateur·rice" copyUsername: "Copier le nom dutilisateur·rice"
copyUserId: "Copier l'identifiant de l'utilisateur"
copyNoteId: "Copier l'identifiant de la note"
copyFileId: "Copier l'identifiant du fichier"
copyFolderId: "Copier l'identifiant du dossier"
copyProfileUrl: "Copier l'URL du profil"
searchUser: "Chercher un·e utilisateur·rice" searchUser: "Chercher un·e utilisateur·rice"
reply: "Répondre" reply: "Répondre"
loadMore: "Afficher plus …" loadMore: "Afficher plus …"
@ -134,6 +139,7 @@ unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce com
selectList: "Sélectionner une liste" selectList: "Sélectionner une liste"
selectChannel: "Sélectionner un canal" selectChannel: "Sélectionner un canal"
selectAntenna: "Sélectionner une antenne" selectAntenna: "Sélectionner une antenne"
editAntenna: "Modifier l'antenne"
selectWidget: "Sélectionner un widget" selectWidget: "Sélectionner un widget"
editWidgets: "Modifier les widgets" editWidgets: "Modifier les widgets"
editWidgetsExit: "Valider les modifications" editWidgetsExit: "Valider les modifications"
@ -146,6 +152,8 @@ addEmoji: "Ajouter un émoji"
settingGuide: "Configuration proposée" settingGuide: "Configuration proposée"
cacheRemoteFiles: "Mise en cache des fichiers distants" cacheRemoteFiles: "Mise en cache des fichiers distants"
cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis linstance distante. La désactiver diminuera certes lutilisation de lespace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées." cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis linstance distante. La désactiver diminuera certes lutilisation de lespace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées."
cacheRemoteSensitiveFiles: "Mettre en cache les fichiers distants sensibles"
cacheRemoteSensitiveFilesDescription: "Si vous désactivez ce paramètre, les fichiers sensibles distants ne seront pas mis en cache et un lien direct sera utilisé à la place"
flagAsBot: "Ce compte est un robot" flagAsBot: "Ce compte est un robot"
flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot." flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot."
flagAsCat: "Ce compte est un chat" flagAsCat: "Ce compte est un chat"
@ -154,6 +162,7 @@ flagShowTimelineReplies: "Afficher les réponses dans le fil"
flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée." flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée."
autoAcceptFollowed: "Accepter automatiquement les demandes dabonnement venant dutilisateur·rice·s que vous suivez" autoAcceptFollowed: "Accepter automatiquement les demandes dabonnement venant dutilisateur·rice·s que vous suivez"
addAccount: "Ajouter un compte" addAccount: "Ajouter un compte"
reloadAccountsList: "Rafraichir la liste des comptes"
loginFailed: "Échec de la connexion" loginFailed: "Échec de la connexion"
showOnRemote: "Voir sur linstance distante" showOnRemote: "Voir sur linstance distante"
general: "Général" general: "Général"
@ -260,6 +269,8 @@ noMoreHistory: "Il ny a plus dhistorique"
startMessaging: "Commencer à discuter" startMessaging: "Commencer à discuter"
nUsersRead: "Lu par {n} personnes" nUsersRead: "Lu par {n} personnes"
agreeTo: "Jaccepte {0}" agreeTo: "Jaccepte {0}"
agree: "Accepter"
termsOfService: "Conditions d'utilisation"
start: "Commencer" start: "Commencer"
home: "Principal" home: "Principal"
remoteUserCaution: "Les informations de ce compte risqueraient dêtre incomplètes du fait que lutilisateur·rice provient dune instance distante." remoteUserCaution: "Les informations de ce compte risqueraient dêtre incomplètes du fait que lutilisateur·rice provient dune instance distante."
@ -302,7 +313,7 @@ copyUrl: "Copier lURL"
rename: "Renommer" rename: "Renommer"
avatar: "Avatar" avatar: "Avatar"
banner: "Bannière" banner: "Bannière"
nsfw: "Contenu sensible" displayOfSensitiveMedia: "Afficher les médias sensibles"
whenServerDisconnected: "Lorsque la connexion au serveur est perdue" whenServerDisconnected: "Lorsque la connexion au serveur est perdue"
disconnectedFromServer: "Déconnecté·e du serveur" disconnectedFromServer: "Déconnecté·e du serveur"
reload: "Rafraîchir" reload: "Rafraîchir"
@ -392,11 +403,15 @@ about: "Informations"
aboutMisskey: "À propos de Misskey" aboutMisskey: "À propos de Misskey"
administrator: "Administrateur" administrator: "Administrateur"
token: "Jeton" token: "Jeton"
2fa: "Authentification à deux facteurs"
totp: "Application d'authentification"
totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification"
moderator: "Modérateur·rice·s" moderator: "Modérateur·rice·s"
moderation: "Modérations" moderation: "Modérations"
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
securityKey: "Clé de sécurité" securityKey: "Clé de sécurité"
lastUsed: "Dernier utilisé" lastUsed: "Dernier utilisé"
lastUsedAt: "Dernière utilisation : {t}"
unregister: "Se désinscrire" unregister: "Se désinscrire"
passwordLessLogin: "Se connecter sans mot de passe" passwordLessLogin: "Se connecter sans mot de passe"
resetPassword: "Réinitialiser le mot de passe" resetPassword: "Réinitialiser le mot de passe"
@ -534,9 +549,14 @@ userSuspended: "Cet·te utilisateur·rice a été suspendu·e."
userSilenced: "Cette utilisateur·trice a été mis·e en sourdine." userSilenced: "Cette utilisateur·trice a été mis·e en sourdine."
yourAccountSuspendedTitle: "Ce compte est suspendu" yourAccountSuspendedTitle: "Ce compte est suspendu"
yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte." yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte."
tokenRevoked: "Ce jeton est invalide."
tokenRevokedDescription: "Votre jeton de connexion a expiré. Veuillez vous reconnecter."
accountDeleted: "Compte supprimé"
accountDeletedDescription: "Ce compte a été supprimé."
menu: "Menu" menu: "Menu"
divider: "Séparateur" divider: "Séparateur"
addItem: "Ajouter un élément" addItem: "Ajouter un élément"
rearrange: "Trier par"
relays: "Relais" relays: "Relais"
addRelay: "Ajouter un relais" addRelay: "Ajouter un relais"
inboxUrl: "Inbox URL" inboxUrl: "Inbox URL"
@ -678,6 +698,8 @@ contact: "Contact"
useSystemFont: "Utiliser la police par défaut du système" useSystemFont: "Utiliser la police par défaut du système"
clips: "Clips" clips: "Clips"
experimentalFeatures: "Fonctionnalités expérimentales" experimentalFeatures: "Fonctionnalités expérimentales"
experimental: "Expérimental"
thisIsExperimentalFeature: "Ceci est une fonctionnalité expérimentale. Il y a une possibilité que les spécifications changent ou qu'elle ne fonctionne pas correctement."
developer: "Développeur" developer: "Développeur"
makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"." makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"."
makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"." makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"."
@ -762,6 +784,7 @@ noMaintainerInformationWarning: "Informations administrateur non configurées."
noBotProtectionWarning: "La protection contre les bots n'est pas configurée." noBotProtectionWarning: "La protection contre les bots n'est pas configurée."
configure: "Configurer" configure: "Configurer"
postToGallery: "Publier dans la galerie" postToGallery: "Publier dans la galerie"
postToHashtag: "Publier avec ce hashtag"
gallery: "Galerie" gallery: "Galerie"
recentPosts: "Les plus récentes" recentPosts: "Les plus récentes"
popularPosts: "Les plus consultées" popularPosts: "Les plus consultées"
@ -800,6 +823,7 @@ lastCommunication: "Dernière communication"
resolved: "Résolu" resolved: "Résolu"
unresolved: "En attente" unresolved: "En attente"
breakFollow: "Ne plus suivre" breakFollow: "Ne plus suivre"
breakFollowConfirm: "Êtes-vous sûr de vouloir vous désabonner?"
itsOn: "Activé" itsOn: "Activé"
itsOff: "Désactivé" itsOff: "Désactivé"
emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte" emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"
@ -1085,10 +1109,6 @@ _aboutMisskey:
donate: "Soutenir Misskey" donate: "Soutenir Misskey"
morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰" morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰"
patrons: "Contributeurs" patrons: "Contributeurs"
_nsfw:
respect: "Cacher les médias marqués comme contenu sensible"
ignore: "Afficher les médias sensibles"
force: "Cacher tous les médias"
_instanceTicker: _instanceTicker:
none: "Cacher " none: "Cacher "
remote: "Montrer pour les utilisateur·ice·s distant·e·s" remote: "Montrer pour les utilisateur·ice·s distant·e·s"

View file

@ -1,6 +1,6 @@
const fs = require('fs'); import * as fs from 'node:fs';
const yaml = require('js-yaml'); import * as yaml from 'js-yaml';
const ts = require('typescript'); import * as ts from 'typescript';
function createMembers(record) { function createMembers(record) {
return Object.entries(record) return Object.entries(record)
@ -14,7 +14,7 @@ function createMembers(record) {
)); ));
} }
module.exports = function generateDTS() { export default function generateDTS() {
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
const members = createMembers(locale); const members = createMembers(locale);
const elements = [ const elements = [
@ -51,11 +51,7 @@ module.exports = function generateDTS() {
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
), ),
), ),
ts.factory.createExportAssignment( ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
undefined,
true,
ts.factory.createIdentifier('locales'),
),
]; ];
const printed = ts.createPrinter({ const printed = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed, newLine: ts.NewLineKind.LineFeed,

View file

@ -5,7 +5,7 @@ introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersif
poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>." poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>."
monthAndDay: "{day} {month}" monthAndDay: "{day} {month}"
search: "Penelusuran" search: "Penelusuran"
notifications: "Pemberitahuan" notifications: "Notifikasi"
username: "Nama Pengguna" username: "Nama Pengguna"
password: "Kata sandi" password: "Kata sandi"
forgotPassword: "Lupa Kata Sandi" forgotPassword: "Lupa Kata Sandi"
@ -17,7 +17,7 @@ noThankYou: "Tidak sekarang."
enterUsername: "Masukkan nama pengguna" enterUsername: "Masukkan nama pengguna"
renotedBy: "direnote oleh {user}" renotedBy: "direnote oleh {user}"
noNotes: "Tidak ada catatan" noNotes: "Tidak ada catatan"
noNotifications: "Tidak ada pemberitahuan" noNotifications: "Tidak ada notifikasi"
instance: "Instansi" instance: "Instansi"
settings: "Pengaturan" settings: "Pengaturan"
notificationSettings: "Atur Notifikasi" notificationSettings: "Atur Notifikasi"
@ -52,6 +52,8 @@ addToList: "Tambahkan ke daftar"
sendMessage: "Kirim pesan" sendMessage: "Kirim pesan"
copyRSS: "Salin RSS" copyRSS: "Salin RSS"
copyUsername: "Salin nama pengguna" copyUsername: "Salin nama pengguna"
copyUserId: "Salin ID pengguna"
copyNoteId: "Salin ID catatan"
searchUser: "Cari pengguna" searchUser: "Cari pengguna"
reply: "Balas" reply: "Balas"
loadMore: "Selebihnya" loadMore: "Selebihnya"
@ -90,7 +92,7 @@ serverIsDead: "Tidak ada respon dari peladen. Mohon tunggu dan coba beberapa saa
youShouldUpgradeClient: "Untuk melihat halaman ini, mohon muat ulang untuk memutakhirkan klienmu." youShouldUpgradeClient: "Untuk melihat halaman ini, mohon muat ulang untuk memutakhirkan klienmu."
enterListName: "Masukkan nama daftar" enterListName: "Masukkan nama daftar"
privacy: "Privasi" privacy: "Privasi"
makeFollowManuallyApprove: "Permintaan mengikuti membutuhkan persetujuan" makeFollowManuallyApprove: "Permintaan mengikuti butuh persetujuan"
defaultNoteVisibility: "Privasi bawaan catatan" defaultNoteVisibility: "Privasi bawaan catatan"
follow: "Ikuti" follow: "Ikuti"
followRequest: "Permintaan mengikuti" followRequest: "Permintaan mengikuti"
@ -115,7 +117,7 @@ add: "Tambahkan"
reaction: "Reaksi" reaction: "Reaksi"
reactions: "Reaksi" reactions: "Reaksi"
reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi" reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi"
reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan" reactionSettingDescription2: "Geser untuk memindah urutan emoji, klik untuk menghapus, tekan \"+\" untuk menambahkan"
rememberNoteVisibility: "Ingat pengaturan visibilitas catatan" rememberNoteVisibility: "Ingat pengaturan visibilitas catatan"
attachCancel: "Hapus lampiran" attachCancel: "Hapus lampiran"
markAsSensitive: "Tandai sebagai konten sensitif" markAsSensitive: "Tandai sebagai konten sensitif"
@ -134,8 +136,10 @@ unblockConfirm: "Apakah kamu yakin ingin membuka blokir akun ini?"
suspendConfirm: "Apakah kamu yakin ingin membekukan akun ini?" suspendConfirm: "Apakah kamu yakin ingin membekukan akun ini?"
unsuspendConfirm: "Apakah kamu yakin ingin membuka pembekuan akun ini?" unsuspendConfirm: "Apakah kamu yakin ingin membuka pembekuan akun ini?"
selectList: "Pilih daftar" selectList: "Pilih daftar"
editList: "Sunting daftar"
selectChannel: "Pilih kanal" selectChannel: "Pilih kanal"
selectAntenna: "Pilih Antena" selectAntenna: "Pilih Antena"
editAntenna: "Sunting antena"
selectWidget: "Pilih gawit" selectWidget: "Pilih gawit"
editWidgets: "Sunting gawit" editWidgets: "Sunting gawit"
editWidgetsExit: "Selesai" editWidgetsExit: "Selesai"
@ -146,16 +150,17 @@ emojiName: "Nama emoji"
emojiUrl: "URL Emoji" emojiUrl: "URL Emoji"
addEmoji: "Tambahkan emoji" addEmoji: "Tambahkan emoji"
settingGuide: "Pengaturan rekomendasi" settingGuide: "Pengaturan rekomendasi"
cacheRemoteFiles: "Tembolokkan berkas remote" cacheRemoteFiles: "Tembolokkan berkas dari instansi luar"
cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas luar akan dimuat langsung dari instansi luar. Menonaktifkan ini akan mengurangi penggunaan penyimpanan, namun dapat menyebabkan meningkatkan lalu lintas bandwidth, karena thumbnail tidak dihasilkan." cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas dari instansi luar akan dimuat langsung. Menonaktifkan ini akan mengurangi penggunaan penyimpanan peladen, namun dapat menyebabkan peningkatan lalu lintas bandwidth, karena keluku tidak dihasilkan."
flagAsBot: "Atur akun ini sebagai Bot" flagAsBot: "Atur akun ini sebagai Bot"
flagAsBotDescription: "Jika akun ini dikendalikan oleh program, tetapkanlah opsi ini. Jika diaktifkan, ini akan berfungsi sebagai tanda bagi pengembang lain untuk mencegah interaksi berantai dengan bot lain dan menyesuaikan sistem internal Misskey untuk memperlakukan akun ini sebagai bot." flagAsBotDescription: "Jika akun ini dikendalikan oleh program, tetapkanlah opsi ini. Jika diaktifkan, ini akan berfungsi sebagai tanda bagi pengembang lain untuk mencegah interaksi berantai dengan bot lain dan menyesuaikan sistem internal Misskey untuk memperlakukan akun ini sebagai bot."
flagAsCat: "Atur akun ini sebagai kucing" flagAsCat: "Atur akun ini sebagai kucing"
flagAsCatDescription: "Nyalakan tanda ini untuk menandai akun ini sebagai kucing." flagAsCatDescription: "Nyalakan tanda ini untuk menandai akun ini sebagai kucing."
flagShowTimelineReplies: "Tampilkan balasan di lini masa" flagShowTimelineReplies: "Tampilkan balasan di lini masa"
flagShowTimelineRepliesDescription: "Menampilkan balasan pengguna dari note pengguna lain di linimasa apabila dinyalakan." flagShowTimelineRepliesDescription: "Menampilkan balasan pengguna dari catatan pengguna lain di lini masa apabila dinyalakan."
autoAcceptFollowed: "Setujui otomatis permintaan mengikuti dari pengguna yang kamu ikuti" autoAcceptFollowed: "Setujui otomatis permintaan mengikuti dari pengguna yang kamu ikuti"
addAccount: "Tambahkan akun" addAccount: "Tambahkan akun"
reloadAccountsList: "Muat ulang daftar akun"
loginFailed: "Gagal untuk masuk" loginFailed: "Gagal untuk masuk"
showOnRemote: "Lihat profil asli" showOnRemote: "Lihat profil asli"
general: "Umum" general: "Umum"
@ -166,7 +171,7 @@ searchWith: "Cari: {q}"
youHaveNoLists: "Kamu tidak memiliki daftar apapun" youHaveNoLists: "Kamu tidak memiliki daftar apapun"
followConfirm: "Apakah kamu yakin ingin mengikuti {name}?" followConfirm: "Apakah kamu yakin ingin mengikuti {name}?"
proxyAccount: "Akun proksi" proxyAccount: "Akun proksi"
proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna luar ke dalam daftar, aktivitas dari pengguna luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya." proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut instansi luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna instansi luar ke dalam daftar, aktivitas dari pengguna instansi luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya."
host: "Host" host: "Host"
selectUser: "Pilih pengguna" selectUser: "Pilih pengguna"
recipient: "Penerima" recipient: "Penerima"
@ -198,7 +203,7 @@ clearQueue: "Bersihkan antrian"
clearQueueConfirmTitle: "Apakah kamu yakin ingin membersihkan antrian?" clearQueueConfirmTitle: "Apakah kamu yakin ingin membersihkan antrian?"
clearQueueConfirmText: "Seluruh sisa catatan yang tidak tersampaikan di dalam antrian tidak akan difederasi. Biasanya operasi ini TIDAK dibutuhkan." clearQueueConfirmText: "Seluruh sisa catatan yang tidak tersampaikan di dalam antrian tidak akan difederasi. Biasanya operasi ini TIDAK dibutuhkan."
clearCachedFiles: "Hapus tembolok" clearCachedFiles: "Hapus tembolok"
clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas remote?" clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas instansi luar?"
blockedInstances: "Instansi terblokir" blockedInstances: "Instansi terblokir"
blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini." blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini."
muteAndBlock: "Bisukan / Blokir" muteAndBlock: "Bisukan / Blokir"
@ -223,9 +228,9 @@ all: "Semua"
subscribing: "Berlangganan" subscribing: "Berlangganan"
publishing: "Sedang menyiarkan langsung" publishing: "Sedang menyiarkan langsung"
notResponding: "Tidak ada respon" notResponding: "Tidak ada respon"
instanceFollowing: "Mengikuti instance" instanceFollowing: "Mengikuti instansi"
instanceFollowers: "Pengikut instance" instanceFollowers: "Pengikut instansi"
instanceUsers: "Pengguna pada instance ini" instanceUsers: "Pengguna pada instansi ini"
changePassword: "Ubah kata sandi" changePassword: "Ubah kata sandi"
security: "Keamanan" security: "Keamanan"
retypedNotMatch: "Input tidak sama" retypedNotMatch: "Input tidak sama"
@ -233,11 +238,11 @@ currentPassword: "Kata sandi saat ini"
newPassword: "Kata sandi baru" newPassword: "Kata sandi baru"
newPasswordRetype: "Ulangi kata sandi baru" newPasswordRetype: "Ulangi kata sandi baru"
attachFile: "Lampirkan berkas" attachFile: "Lampirkan berkas"
more: "Lagi !" more: "Lainnya"
featured: "Sorotan" featured: "Sorotan"
usernameOrUserId: "Nama pengguna atau User ID" usernameOrUserId: "Nama pengguna atau User ID"
noSuchUser: "Pengguna tidak ditemukan" noSuchUser: "Pengguna tidak ditemukan"
lookup: "Mencari" lookup: "Cari"
announcements: "Pengumuman" announcements: "Pengumuman"
imageUrl: "URL Gambar" imageUrl: "URL Gambar"
remove: "Hapus" remove: "Hapus"
@ -268,7 +273,7 @@ basicNotesBeforeCreateAccount: "Catatan penting"
termsOfService: "Syarat dan ketentuan" termsOfService: "Syarat dan ketentuan"
start: "Mulai" start: "Mulai"
home: "Beranda" home: "Beranda"
remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari instansi luar." remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari peladen instansi luar."
activity: "Aktivitas" activity: "Aktivitas"
images: "Gambar" images: "Gambar"
image: "Gambar" image: "Gambar"
@ -308,7 +313,6 @@ copyUrl: "Salin tautan"
rename: "Ubah nama" rename: "Ubah nama"
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
nsfw: "Konten sensitif"
whenServerDisconnected: "Ketika kehilangan koneksi dengan peladen" whenServerDisconnected: "Ketika kehilangan koneksi dengan peladen"
disconnectedFromServer: "Terputus koneksi dari peladen" disconnectedFromServer: "Terputus koneksi dari peladen"
reload: "Muat ulang" reload: "Muat ulang"
@ -319,8 +323,8 @@ unwatch: "Batal tonton"
accept: "Terima" accept: "Terima"
reject: "Tolak" reject: "Tolak"
normal: "Normal" normal: "Normal"
instanceName: "Nama instance" instanceName: "Nama instansi"
instanceDescription: "Tentang instance" instanceDescription: "Tentang instansi"
maintainerName: "Pengelola" maintainerName: "Pengelola"
maintainerEmail: "Surel pengelola" maintainerEmail: "Surel pengelola"
tosUrl: "URL Syarat dan Ketentuan" tosUrl: "URL Syarat dan Ketentuan"
@ -379,7 +383,7 @@ enableServiceworker: "Aktifkan ServiceWorker"
antennaUsersDescription: "Tuliskan satu nama pengguna per baris" antennaUsersDescription: "Tuliskan satu nama pengguna per baris"
caseSensitive: "Peka huruf besar dan huruf kecil" caseSensitive: "Peka huruf besar dan huruf kecil"
withReplies: "Termasuk balasan" withReplies: "Termasuk balasan"
connectedTo: "Akun yang mengikuti telah terhubung" connectedTo: "Akun berikut terhubung"
notesAndReplies: "Catatan dan balasan" notesAndReplies: "Catatan dan balasan"
withFiles: "Media" withFiles: "Media"
silence: "Bungkam" silence: "Bungkam"
@ -398,6 +402,7 @@ about: "Informasi"
aboutMisskey: "Tentang Misskey" aboutMisskey: "Tentang Misskey"
administrator: "Admin" administrator: "Admin"
token: "Token" token: "Token"
2fa: "Autentikasi 2-faktor"
totp: "Aplikasi autentikator" totp: "Aplikasi autentikator"
totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai" totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai"
moderator: "Moderator" moderator: "Moderator"
@ -409,6 +414,7 @@ lastUsed: "Terakhir digunakan"
lastUsedAt: "Penggunaan terakhir: {t}" lastUsedAt: "Penggunaan terakhir: {t}"
unregister: "Batalkan pendaftaran" unregister: "Batalkan pendaftaran"
passwordLessLogin: "Setel login tanpa kata sandi" passwordLessLogin: "Setel login tanpa kata sandi"
passwordLessLoginDescription: "Bolehkan masuk tanpa kata sandi dengan menggunakan hanya security-key atau passkey"
resetPassword: "Atur ulang kata sandi" resetPassword: "Atur ulang kata sandi"
newPasswordIs: "Kata sandi baru adalah \"{password}\"" newPasswordIs: "Kata sandi baru adalah \"{password}\""
reduceUiAnimation: "Kurangi animasi antarmuka" reduceUiAnimation: "Kurangi animasi antarmuka"
@ -417,7 +423,7 @@ notFound: "Tidak dapat ditemukan"
notFoundDescription: "Tidak ada halaman sesuai dengan URL yang ditentukan." notFoundDescription: "Tidak ada halaman sesuai dengan URL yang ditentukan."
uploadFolder: "Lokasi unggah folder bawaan" uploadFolder: "Lokasi unggah folder bawaan"
cacheClear: "Bersihkan tembolok" cacheClear: "Bersihkan tembolok"
markAsReadAllNotifications: "Tandai semua pemberitahuan telah dibaca" markAsReadAllNotifications: "Tandai semua notifikasi telah dibaca"
markAsReadAllUnreadNotes: "Tandai semua catatan telah dibaca" markAsReadAllUnreadNotes: "Tandai semua catatan telah dibaca"
markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca" markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca"
help: "Bantuan" help: "Bantuan"
@ -460,6 +466,7 @@ aboutX: "Tentang {x}"
emojiStyle: "Gaya emoji" emojiStyle: "Gaya emoji"
native: "Native" native: "Native"
disableDrawer: "Jangan gunakan menu bergaya laci" disableDrawer: "Jangan gunakan menu bergaya laci"
showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk"
noHistory: "Tidak ada riwayat" noHistory: "Tidak ada riwayat"
signinHistory: "Riwayat masuk" signinHistory: "Riwayat masuk"
enableAdvancedMfm: "Nyalakan MFM tingkat lanjut" enableAdvancedMfm: "Nyalakan MFM tingkat lanjut"
@ -472,6 +479,8 @@ createAccount: "Buat akun"
existingAccount: "Akun yang ada" existingAccount: "Akun yang ada"
regenerate: "Buat ulang" regenerate: "Buat ulang"
fontSize: "Ukuran huruf" fontSize: "Ukuran huruf"
mediaListWithOneImageAppearance: "Tinggi daftar media dengan satu gambar saja"
limitTo: "Batasi pada {x}"
noFollowRequests: "Kamu tidak memiliki permintaan mengikuti yang menunggu" noFollowRequests: "Kamu tidak memiliki permintaan mengikuti yang menunggu"
openImageInNewTab: "Buka gambar di tab baru" openImageInNewTab: "Buka gambar di tab baru"
dashboard: "Dasbor" dashboard: "Dasbor"
@ -505,9 +514,11 @@ objectStorageUseSSLDesc: "Matikan ini jika kamu tidak akan menggunakan HTTPS unt
objectStorageUseProxy: "Hubungkan melalui Proxy" objectStorageUseProxy: "Hubungkan melalui Proxy"
objectStorageUseProxyDesc: "Matikan ini jika kamu tidak akan menggunakan Proxy untuk koneksi ObjectStorage" objectStorageUseProxyDesc: "Matikan ini jika kamu tidak akan menggunakan Proxy untuk koneksi ObjectStorage"
objectStorageSetPublicRead: "Setel \"public-read\" disaat mengunggah" objectStorageSetPublicRead: "Setel \"public-read\" disaat mengunggah"
s3ForcePathStyleDesc: "Jika s3ForcePathStyle dinyalakan, nama bucket harus dimasukkan dalam path URL dan bukan URL nama host tersebut. Kamu perlu menyalakan pengaturan ini jika menggunakan layanan seperti instansi Minio yang self-hosted."
serverLogs: "Log Peladen" serverLogs: "Log Peladen"
deleteAll: "Hapus semua" deleteAll: "Hapus semua"
showFixedPostForm: "Tampilkan form posting di atas lini masa." showFixedPostForm: "Tampilkan form posting di atas lini masa."
showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)"
newNoteRecived: "Kamu mendapat catatan baru" newNoteRecived: "Kamu mendapat catatan baru"
sounds: "Bunyi" sounds: "Bunyi"
sound: "Bunyi" sound: "Bunyi"
@ -536,23 +547,28 @@ scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScr
output: "Keluaran" output: "Keluaran"
script: "Script" script: "Script"
disablePagesScript: "Nonaktifkan script pada halaman" disablePagesScript: "Nonaktifkan script pada halaman"
updateRemoteUser: "Perbaharui informasi pengguna luar" updateRemoteUser: "Perbaharui informasi pengguna instansi luar"
deleteAllFiles: "Hapus semua berkas" deleteAllFiles: "Hapus semua berkas"
deleteAllFilesConfirm: "Apakah kamu yakin ingin menghapus semua berkas?" deleteAllFilesConfirm: "Apakah kamu yakin ingin menghapus semua berkas?"
removeAllFollowing: "Tahan semua mengikuti" removeAllFollowing: "Batalkan mengikuti semua pengguna"
removeAllFollowingDescription: "Batal mengikuti semua akun dari {host}. Mohon jalankan ini ketika instansi sudah tidak ada lagi." removeAllFollowingDescription: "Batal mengikuti semua akun dari {host}. Mohon jalankan ini ketika instansi sudah tidak ada lagi."
userSuspended: "Pengguna ini telah dibekukan." userSuspended: "Pengguna ini telah dibekukan."
userSilenced: "Pengguna ini telah dibungkam." userSilenced: "Pengguna ini telah dibungkam."
yourAccountSuspendedTitle: "Akun ini dibekukan" yourAccountSuspendedTitle: "Akun ini dibekukan"
yourAccountSuspendedDescription: "Akun ini dibekukan karena melanggar ketentuan penggunaan layanan peladen atau semacamnya. Hubungi admin apabila ingin tahu alasan lebih lanjut. Mohon untuk tidak membuat akun baru." yourAccountSuspendedDescription: "Akun ini dibekukan karena melanggar ketentuan penggunaan layanan peladen atau semacamnya. Hubungi admin apabila ingin tahu alasan lebih lanjut. Mohon untuk tidak membuat akun baru."
tokenRevoked: "Token tidak valid"
tokenRevokedDescription: "Token ini telah kedaluwarsa. Mohon masuk lagi."
accountDeleted: "Akun telah dihapus"
accountDeletedDescription: "Akun ini telah dihapus."
menu: "Menu" menu: "Menu"
divider: "Pembagi" divider: "Pembagi"
addItem: "Tambahkan item" addItem: "Tambahkan item"
rearrange: "Tata ulang"
relays: "Relay" relays: "Relay"
addRelay: "Tambahkan relay" addRelay: "Tambahkan relay"
inboxUrl: "URL Kotak masuk" inboxUrl: "URL Kotak masuk"
addedRelays: "Relay yang ditambahkan" addedRelays: "Relay yang ditambahkan"
serviceworkerInfo: "Harus diaktifkan untuk pemberitahuan push." serviceworkerInfo: "Harus diaktifkan untuk notifikasi dorong."
deletedNote: "Catatan yang dihapus" deletedNote: "Catatan yang dihapus"
invisibleNote: "Catatan yang disembunyikan" invisibleNote: "Catatan yang disembunyikan"
enableInfiniteScroll: "Aktifkan gulir tak terbatas" enableInfiniteScroll: "Aktifkan gulir tak terbatas"
@ -580,13 +596,13 @@ height: "Tinggi"
large: "Besar" large: "Besar"
medium: "Sedang" medium: "Sedang"
small: "Kecil" small: "Kecil"
generateAccessToken: "Buat access token" generateAccessToken: "Buat token akses"
permission: "Izin" permission: "Izin"
enableAll: "Aktifkan semua" enableAll: "Aktifkan semua"
disableAll: "Nonaktifkan semua" disableAll: "Nonaktifkan semua"
tokenRequested: "Berikan ijin akses ke akun" tokenRequested: "Berikan ijin akses ke akun"
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini." pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
notificationType: "Jenis pemberitahuan" notificationType: "Jenis notifikasi"
edit: "Sunting" edit: "Sunting"
emailServer: "Peladen surel" emailServer: "Peladen surel"
enableEmail: "Nyalakan distribusi surel" enableEmail: "Nyalakan distribusi surel"
@ -617,10 +633,10 @@ delayed: "Terlambat"
database: "Basis data" database: "Basis data"
channel: "Kanal" channel: "Kanal"
create: "Buat" create: "Buat"
notificationSetting: "Pengaturan Pemberitahuan" notificationSetting: "Pengaturan Notifikasi"
notificationSettingDesc: "Pilih tipe pemberitahuan untuk ditampilkan" notificationSettingDesc: "Pilih tipe notifikasi untuk ditampilkan"
useGlobalSetting: "Gunakan setelan global" useGlobalSetting: "Gunakan setelan global"
useGlobalSettingDesc: "Jika dinyalakan, setelan pemberitahuan akun kamu akan digunakan. Jika dimatikan, konfigurasi secara individu dapat dibuat." useGlobalSettingDesc: "Jika dinyalakan, setelan notifikasi akun kamu akan digunakan. Jika dimatikan, pengaturan secara individu dapat dibuat."
other: "Lainnya" other: "Lainnya"
regenerateLoginToken: "Perbarui token login" regenerateLoginToken: "Perbarui token login"
regenerateLoginTokenDescription: "Perbarui token yang digunakan secara internal saat login. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat akan dilogout." regenerateLoginTokenDescription: "Perbarui token yang digunakan secara internal saat login. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat akan dilogout."
@ -657,8 +673,8 @@ createNewClip: "Buat klip baru"
unclip: "Batalkan klip" unclip: "Batalkan klip"
confirmToUnclipAlreadyClippedNote: "Catatan ini sudah disertakan di klip \"{name}\". Yakin ingin membatalkan catatan dari klip ini?" confirmToUnclipAlreadyClippedNote: "Catatan ini sudah disertakan di klip \"{name}\". Yakin ingin membatalkan catatan dari klip ini?"
public: "Publik" public: "Publik"
i18nInfo: "Misskey diterjemahkan ke dalam banyak bahasa oleh sukarelawan. Kamu dapat ikut membantu di {link}." i18nInfo: "Misskey diterjemahkan ke dalam banyak bahasa oleh sukarelawan. Kamu juga dapat ikut membantu menerjemahkannya di {link}."
manageAccessTokens: "Kelola access token" manageAccessTokens: "Kelola token akses"
accountInfo: "Informasi akun" accountInfo: "Informasi akun"
notesCount: "Jumlah catatan" notesCount: "Jumlah catatan"
repliesCount: "Jumlah balasan terkirim" repliesCount: "Jumlah balasan terkirim"
@ -675,7 +691,7 @@ yes: "Iya"
no: "Tidak" no: "Tidak"
driveFilesCount: "Jumlah berkas drive" driveFilesCount: "Jumlah berkas drive"
driveUsage: "Penggunaan ruang penyimpanan drive" driveUsage: "Penggunaan ruang penyimpanan drive"
noCrawle: "Tolak pengindeksan crawler" noCrawle: "Tolak pengindeksan perayap web"
noCrawleDescription: "Meminta mesin pencari untuk tidak mengindeks halaman profil kamu, catatan, Halaman, dll." noCrawleDescription: "Meminta mesin pencari untuk tidak mengindeks halaman profil kamu, catatan, Halaman, dll."
lockedAccountInfo: "Kecuali kamu menyetel visibilitas catatan milikmu ke \"Hanya pengikut\", catatan milikmu akan dapat dilihat oleh siapa saja, bahkan jika kamu memerlukan pengikut untuk disetujui secara manual." lockedAccountInfo: "Kecuali kamu menyetel visibilitas catatan milikmu ke \"Hanya pengikut\", catatan milikmu akan dapat dilihat oleh siapa saja, bahkan jika kamu memerlukan pengikut untuk disetujui secara manual."
alwaysMarkSensitive: "Tandai media dalam catatan sebagai media sensitif" alwaysMarkSensitive: "Tandai media dalam catatan sebagai media sensitif"
@ -691,9 +707,11 @@ contact: "Kontak"
useSystemFont: "Gunakan font bawaan sistem operasi" useSystemFont: "Gunakan font bawaan sistem operasi"
clips: "Klip" clips: "Klip"
experimentalFeatures: "Fitur eksperimental" experimentalFeatures: "Fitur eksperimental"
experimental: "Eksperimental"
thisIsExperimentalFeature: "Fitur ini eksperimental. Fungsionalitas dari fitur ini dapat berubah sewaktu-waktu dan mungkin tidak bekerja sesuai semestinya."
developer: "Pengembang" developer: "Pengembang"
makeExplorable: "Buat akun tampil di \"Jelajahi\"" makeExplorable: "Buat akun tampil di \"Jelajahi\""
makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di bagian \"Jelajahi:" makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di menu \"Jelajahi\""
showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada lini masa" showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada lini masa"
duplicate: "Duplikat" duplicate: "Duplikat"
left: "Kiri" left: "Kiri"
@ -733,8 +751,8 @@ capacity: "Kapasitas"
inUse: "Digunakan" inUse: "Digunakan"
editCode: "Sunting kode" editCode: "Sunting kode"
apply: "Terapkan" apply: "Terapkan"
receiveAnnouncementFromInstance: "Terima pemberitahuan surel dari instansi ini" receiveAnnouncementFromInstance: "Terima pengumuman dari instansi ini"
emailNotification: "Pemberitahuan surel" emailNotification: "Notifikasi surel"
publish: "Terbitkan" publish: "Terbitkan"
inChannelSearch: "Cari di kanal" inChannelSearch: "Cari di kanal"
useReactionPickerForContextMenu: "Buka pemilih reaksi dengan klik-kanan" useReactionPickerForContextMenu: "Buka pemilih reaksi dengan klik-kanan"
@ -755,7 +773,7 @@ userInfo: "Informasi pengguna"
unknown: "Tidak diketahui" unknown: "Tidak diketahui"
onlineStatus: "Status daring" onlineStatus: "Status daring"
hideOnlineStatus: "Sembunyikan status daring" hideOnlineStatus: "Sembunyikan status daring"
hideOnlineStatusDescription: "Menyembunyikan status daring kamu umengurangi kenyamanan untuk beberapa fungsi seperti contohnya pencarian." hideOnlineStatusDescription: "Menyembunyikan status daring kamu akan mengurangi kenyamanan untuk beberapa fungsi, seperti contohnya pencarian."
online: "Daring" online: "Daring"
active: "Aktif" active: "Aktif"
offline: "Luring" offline: "Luring"
@ -775,12 +793,14 @@ noMaintainerInformationWarning: "Informasi pengelola belum disetel."
noBotProtectionWarning: "Proteksi bot belum disetel." noBotProtectionWarning: "Proteksi bot belum disetel."
configure: "Setel" configure: "Setel"
postToGallery: "Posting ke galeri" postToGallery: "Posting ke galeri"
postToHashtag: "Catat ke tagar ini"
gallery: "Galeri" gallery: "Galeri"
recentPosts: "Postingan terbaru" recentPosts: "Postingan terbaru"
popularPosts: "Postingan populer" popularPosts: "Postingan populer"
shareWithNote: "Bagikan dengan catatan" shareWithNote: "Bagikan dengan catatan"
ads: "Iklan" ads: "Iklan"
expiration: "Batas akhir" expiration: "Batas akhir"
startingperiod: "Mulai"
memo: "Memo" memo: "Memo"
priority: "Prioritas" priority: "Prioritas"
high: "Tinggi" high: "Tinggi"
@ -807,14 +827,18 @@ translatedFrom: "Terjemahkan dari {x}"
accountDeletionInProgress: "Penghapusan akun sedang dalam proses" accountDeletionInProgress: "Penghapusan akun sedang dalam proses"
usernameInfo: "Nama yang mengidentifikasikan akun kamu dari yang lain pada peladen ini. Kamu dapat menggunakan alfabet (a~z, A~Z), digit (0~9) atau garis bawah (_). Username tidak dapat diubah setelahnya." usernameInfo: "Nama yang mengidentifikasikan akun kamu dari yang lain pada peladen ini. Kamu dapat menggunakan alfabet (a~z, A~Z), digit (0~9) atau garis bawah (_). Username tidak dapat diubah setelahnya."
aiChanMode: "Mode Ai" aiChanMode: "Mode Ai"
keepCw: "Biarkan Peringatan Konten" devMode: "Mode pengembang"
keepCw: "Biarkan peringatan konten"
pubSub: "Akun Pub/Sub" pubSub: "Akun Pub/Sub"
lastCommunication: "Komunikasi terakhir" lastCommunication: "Komunikasi terakhir"
resolved: "Selesai" resolved: "Selesai"
unresolved: "Belum selesai" unresolved: "Belum selesai"
breakFollow: "Batalkan mengikuti" breakFollow: "Hapus pengikut"
breakFollowConfirm: "Yakin untuk menghapus pengikut ini?"
itsOn: "Aktif" itsOn: "Aktif"
itsOff: "Nonaktif" itsOff: "Nonaktif"
on: "Nyala"
off: "Mati"
emailRequiredForSignup: "Membutuhkan alamat surel untuk mendaftar" emailRequiredForSignup: "Membutuhkan alamat surel untuk mendaftar"
unread: "Belum dibaca" unread: "Belum dibaca"
filter: "Saring" filter: "Saring"
@ -891,25 +915,26 @@ slow: "Lambat"
fast: "Cepat" fast: "Cepat"
sensitiveMediaDetection: "Deteksi media NSFW" sensitiveMediaDetection: "Deteksi media NSFW"
localOnly: "Hanya lokal" localOnly: "Hanya lokal"
remoteOnly: "Hanya remot" remoteOnly: "Hanya luar instansi"
failedToUpload: "Gagal mengunggah" failedToUpload: "Gagal mengunggah"
cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW." cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW."
cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive." cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive."
cannotUploadBecauseExceedsFileSizeLimit: "Berkas ini tidak dapat diunggah karena melebihi batas ukuran berkas."
beta: "Beta" beta: "Beta"
enableAutoSensitive: "Penandaan NSFW otomatis" enableAutoSensitive: "Penandaan NSFW otomatis"
enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Machine Learning jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen." enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Pembelajaran Mesin jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen."
activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi." activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi."
navbar: "Bilah navigasi" navbar: "Bilah navigasi"
shuffle: "Acak" shuffle: "Acak"
account: "Akun" account: "Akun"
move: "Pindah" move: "Pindah"
pushNotification: "Pemberitahuan push" pushNotification: "Notifikasi dorong"
subscribePushNotification: "Nyalakan pemberitahuan push" subscribePushNotification: "Nyalakan notifikasi dorong"
unsubscribePushNotification: "Matikan pemberitahuan push" unsubscribePushNotification: "Matikan notifikasi dorong"
pushNotificationAlreadySubscribed: "Pemberitahuan push telah dinyalakan" pushNotificationAlreadySubscribed: "Notifikasi dorong telah dinyalakan"
pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pemberitahuan push" pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung notifikasi dorong"
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca" sendPushNotificationReadMessage: "Hapus notifikasi dorong ketika notifikasi relevan atau pesan telah dibaca"
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu." sendPushNotificationReadMessageCaption: "Notifikasi berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
windowMaximize: "Maksimalkan" windowMaximize: "Maksimalkan"
windowMinimize: "Minimalkan" windowMinimize: "Minimalkan"
windowRestore: "Kembalikan" windowRestore: "Kembalikan"
@ -928,6 +953,7 @@ didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?"
pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!" pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!"
roles: "Peran" roles: "Peran"
role: "Peran" role: "Peran"
noRole: "Peran tidak temukan"
normalUser: "Pengguna umum" normalUser: "Pengguna umum"
undefined: "Tak terdefinisi" undefined: "Tak terdefinisi"
assign: "Tetapkan\n" assign: "Tetapkan\n"
@ -937,6 +963,10 @@ manageCustomEmojis: "Kelola Emoji Kustom"
youCannotCreateAnymore: "Kamu melewati batas pembuatan." youCannotCreateAnymore: "Kamu melewati batas pembuatan."
cannotPerformTemporary: "Sementara Tidak Tersedia" cannotPerformTemporary: "Sementara Tidak Tersedia"
cannotPerformTemporaryDescription: "Aksi ini tidak dapat dilakukan sementara karena melewati batas eksekusi. Mohon tunggu sejenak dan coba lagi." cannotPerformTemporaryDescription: "Aksi ini tidak dapat dilakukan sementara karena melewati batas eksekusi. Mohon tunggu sejenak dan coba lagi."
invalidParamError: "Parameter tidak valid"
invalidParamErrorDescription: "Parameter permintaan tidak valid. Hal ini biasanya disebabkan oleh bug, namun juga dapat terjadi karena input melebihi batas ukuran atau semacamnya."
permissionDeniedError: "Operasi ditolak"
permissionDeniedErrorDescription: "Akun ini tidak memiliki izin untuk melakukan aksi ini."
preset: "Prasetel" preset: "Prasetel"
selectFromPresets: "Pilih dari prasetel" selectFromPresets: "Pilih dari prasetel"
achievements: "Pencapaian" achievements: "Pencapaian"
@ -950,15 +980,124 @@ collapseRenotes: "Tutup renote yang sudah kamu lihat"
internalServerError: "Kesalahan internal peladen" internalServerError: "Kesalahan internal peladen"
internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga" internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga"
copyErrorInfo: "Salin detil galat" copyErrorInfo: "Salin detil galat"
joinThisServer: "Gabung server ini" joinThisServer: "Gabung peladen ini"
exploreOtherServers: "Cari server lain" exploreOtherServers: "Cari peladen lain"
letsLookAtTimeline: "LIhat timeline" letsLookAtTimeline: "LIhat timeline"
disableFederationConfirm: "Matikan federasi?" disableFederationConfirm: "Matikan federasi?"
disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan." disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan."
disableFederationOk: "Matikan federasi" disableFederationOk: "Matikan federasi"
invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar."
emailNotSupported: "Instansi ini tidak mendukung mengirim surel"
postToTheChannel: "Catat ke kanal"
cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi."
reactionAcceptance: "Penerimaan reaksi"
likeOnly: "Hanya suka"
likeOnlyForRemote: "Semua (Hanya suka dari instansi luar)"
nonSensitiveOnly: "Hanya non-sensitif"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Hanya non-sensitif (Hanya suka dari instansi luar)"
rolesAssignedToMe: "Peran yang ditugaskan ke saya"
resetPasswordConfirm: "Yakin untuk mereset kata sandimu?"
sensitiveWords: "Kata sensitif"
sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru."
sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler."
notesSearchNotAvailable: "Pencarian catatan tidak tersedia."
license: "Lisensi"
unfavoriteConfirm: "Yakin ingin menghapusnya dari favorit?"
myClips: "Klip saya"
drivecleaner: "Pembersih Drive"
retryAllQueuesNow: "Coba jalankan lagi semua antrian"
retryAllQueuesConfirmTitle: "Yakin ingin mencoba lagi semuanya?"
retryAllQueuesConfirmText: "Hal ini akan meningkatkan beban sementara ke peladen."
enableChartsForRemoteUser: "Buat bagan data pengguna instansi luar"
enableChartsForFederatedInstances: "Buat bagan data peladen instansi luar"
showClipButtonInNoteFooter: "Tambahkan \"Klip\" ke menu aksi catatan"
largeNoteReactions: "Besarkan reaksi yang ditampilkan"
noteIdOrUrl: "ID catatan atau URL"
video: "Video"
videos: "Video"
dataSaver: "Penghemat data"
accountMigration: "Pemindahan akun"
accountMoved: "Pengguna ini telah berpindah ke akun baru:"
accountMovedShort: "Akun ini telah dipindahkan."
operationForbidden: "Operasi dilarang"
forceShowAds: "Selalu tampilkan iklan"
addMemo: "Tambahkan memo"
editMemo: "Sunting memo"
reactionsList: "Reaksi"
renotesList: "Renote"
notificationDisplay: "Notifikasi"
leftTop: "Kiri atas"
rightTop: "Kanan atas"
leftBottom: "Kiri bawah"
rightBottom: "Kanan bawah"
stackAxis: "Arah tumpukan"
vertical: "Vertikal"
horizontal: "Horisontal" horizontal: "Horisontal"
position: "Posisi"
serverRules: "Aturan peladen"
pleaseConfirmBelowBeforeSignup: "Mohon konfirmasi di bawah ini sebelum mendaftar."
pleaseAgreeAllToContinue: "Kamu harus menyetujui semua kolom di atas untuk melanjutkan."
continue: "Lanjutkan"
preservedUsernames: "Nama pengguna tercadangkan"
preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah dengan baris baru. Nama pengguna berikut akan tidak dapat dipakai pada pembuatan akun normal, namun dapat digunakan oleh admin untuk membuat akun baru. Akun yang sudah ada dengan menggunakan nama pengguna ini tidak akan terpengaruh."
createNoteFromTheFile: "Buat catatan dari berkas ini"
archive: "Arsipkan"
channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?"
channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi."
thisChannelArchived: "Kanal ini telah diarsipkan."
displayOfNote: "Tampilan catatan"
initialAccountSetting: "Atur profil"
youFollowing: "Mengikuti" youFollowing: "Mengikuti"
preventAiLearning: "Tolak penggunaan Pembelajaran Mesin (AI Generatif)"
preventAiLearningDescription: "Minta perayap web untuk tidak menggunakan materi teks atau gambar yang telah diposting ke dalam set data Pembelajaran Mesin (Prediktif / Generatif). Hal ini dicapai dengan menambahkan flag HTML-Response \"noai\" ke masing-masing konten. Pencegahan penuh mungkin tidak dapat dicapai dengan flag ini, karena juga dapat diabaikan begitu saja."
options: "Opsi peran" options: "Opsi peran"
specifyUser: "Pengguna spesifik"
failedToPreviewUrl: "Tidak dapat dipratinjau"
update: "Perbarui"
rolesThatCanBeUsedThisEmojiAsReaction: "Peran yang dapat menggunakan emoji ini sebagai reaksi"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Jika peran tidak ditentukan, semua pengguna dapat menggunakan emoji ini sebagai reaksi."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Peran ini harus publik."
cancelReactionConfirm: "Yakin untuk menghapus reaksimu?"
changeReactionConfirm: "Yakin untuk mengganti reaksimu?"
later: "Nanti saja"
goToMisskey: "Ke Misskey"
additionalEmojiDictionary: "Kamus emoji tambahan"
installed: "Terpasang"
branding: "Merek"
enableServerMachineStats: "Tampilkan informasi mesin peladen menjadi publik"
enableIdenticonGeneration: "Nyalakan pembuatan Identicon per pengguna"
_initialAccountSetting:
accountCreated: "Akun kamu telah sukses dibuat!"
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
letsFillYourProfile: "Pertama, ayo atur profilmu dulu."
profileSetting: "Pengaturan profil"
privacySetting: "Pengaturan privasi"
theseSettingsCanEditLater: "Kamu selalu bisa mengganti pengaturan ini lain kali."
youCanEditMoreSettingsInSettingsPageLater: "Ada banyak pengaturan yang dapat kamu atur dari halaman \"Pengaturan\". Pastikan untuk mengunjungi halaman tersebut nanti."
followUsers: "Coba ikuti beberapa pengguna yang menarik bagimu untuk membangun lini masa akunmu."
pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu."
initialAccountSettingCompleted: "Pengaturan profil selesai!"
haveFun: "Selamat menikmati, {name}!"
ifYouNeedLearnMore: "Kalau kamu ingin mempelajari lebih lanjut bagaimana cara menggunakan {name} (Misskey), silahkan kunjungi {link}."
skipAreYouSure: "Yakin melewati atur profil?"
laterAreYouSure: "Yakin banget untuk atur profil nanti?"
_serverRules:
description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan."
_accountMigration:
moveFrom: "Pindahkan akun lain ke akun ini"
moveFromSub: "Buat alias ke akun lain"
moveFromLabel: "Akun asli #{n}"
moveFromDescription: "Kamu harus membuat alias untuk akun asal kamu berpindah ke akun ini\nMasukkan alias akun asal kamu berpindah ke dalam format berikut: @namapengguna@nama.server.com\nUntuk menghapus alias, kosongkan kolom ini (tidak direkomendasikan)."
moveTo: "Pindahkan akun ini ke akun lain"
moveToLabel: "Akun tujuan pindah:"
moveCannotBeUndone: "Pemindahan akun tidak dapat diurungkan."
moveAccountDescription: "Hal ini akan memindahkan akun kamu ke akun lain.\n ・Pengikut dari akun ini akan secara otomatis dipindahkan ke akun baru\n ・Akun ini akan berhenti mengikuti dari semua pengguna yang sedang kamu ikuti\n ・Kamu akan tidak dapat membuat catatan baru dan lain-lain pada akun ini\n\nMeskipun pemindahan pengikut dilakukan secara otomatis, kamu harus mempersiapkan beberapa langkah secara manual untuk memindahkan daftar pengguna yang sedang kamu ikuti. Untuk melakukan tersebut, lakukan ekspor daftar ikuti yang nantinya dapat kamu impor pada menu pengaturan di akun baru kamu. Prosedur yang sama juga dapat diterapkan pada daftar seperti pengguna yang kamu bisukan atau blokir.\n\n(Penjelasan ini hanya berlaku pada Misskey versi 13.12.0 dan setelahnya. Perangkat lunak ActivityPub lainnya seperti Mastodon berkemungkinan befungsi berbeda.)"
moveAccountHowTo: "Untuk pindah, pertama buat alias untuk akun ini pada akun tujuan kamu berpindah.\nSetelah kamu membuat alias, masukkan akun tujuan kamu berpindah ke dalam format berikut:\n@namapengguna@nama.server.com"
startMigration: "Pindahkan"
migrationConfirm: "Yakin untuk memindahkan akun ini ke {account}? Sekali dimulai, proses ini tidak dapat dihentikan atau ditarik kembali, dan kamu tidak dapat menggunakan akun ini lagi dalam keadaan asli semula."
movedAndCannotBeUndone: "\nAkun ini telah dipindahkan.\nPemindahan tidak dapat diurungkan."
postMigrationNote: "24 jam setelah pemindahan akun selesai, akun ini akan berhenti mengikuti semua akun yang sedang diikuti. Angka mengikut dan pengikut akan menjadi nol. Untuk menghindari pengikut kamu tidak dapat melihat postingan hanya pengikut saja dalam postingan ini, mereka akan tetap mengikuti akun ini."
movedTo: "Akun baru tujuan pindah:"
_achievements: _achievements:
earnedAt: "Terbuka pada" earnedAt: "Terbuka pada"
_types: _types:
@ -1130,6 +1269,9 @@ _achievements:
_client30min: _client30min:
title: "Istirahat pendek" title: "Istirahat pendek"
description: "Habiskan waktu 30 menit di Misskey" description: "Habiskan waktu 30 menit di Misskey"
_client60min:
title: "Tidak ada \"Miss\" dalam Misskey"
description: "Biarkan Misskey tetap terbuka setidaknya selama 60 menit"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Eh, salah coy!" title: "Eh, salah coy!"
description: "Hapus catatan kurang dari semenit kamu catat" description: "Hapus catatan kurang dari semenit kamu catat"
@ -1218,6 +1360,10 @@ _role:
iconUrl: "URL ikon" iconUrl: "URL ikon"
asBadge: "Tampilkan sebagai lencana" asBadge: "Tampilkan sebagai lencana"
descriptionOfAsBadge: "Ikon peran ini akan ditampilkan bersebelahan dengan username pengguna yang memiliki peran ini jika dinyalakan." descriptionOfAsBadge: "Ikon peran ini akan ditampilkan bersebelahan dengan username pengguna yang memiliki peran ini jika dinyalakan."
isExplorable: "Buat peran dapat terjelajahi"
descriptionOfIsExplorable: "Lini masa peran ini dan daftar pengguna dengan peran ini akan dibuat publik apabila dinyalakan."
displayOrder: "Urutan"
descriptionOfDisplayOrder: "Semakin tinggi angka, semakin tinggi posisi antarmukanya."
canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini" canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini"
descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini." descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini."
priority: "Prioritas" priority: "Prioritas"
@ -1232,6 +1378,7 @@ _role:
canInvite: "Dapat membuat kode undangan instansi" canInvite: "Dapat membuat kode undangan instansi"
canManageCustomEmojis: "Dapat mengelola Emoji kustom" canManageCustomEmojis: "Dapat mengelola Emoji kustom"
driveCapacity: "Kapasitas Drive" driveCapacity: "Kapasitas Drive"
alwaysMarkNsfw: "Selalu tandai berkas sebagai NSFW"
pinMax: "Jumlah maksimal catatan yang disematkan" pinMax: "Jumlah maksimal catatan yang disematkan"
antennaMax: "Jumlah maksimum antena" antennaMax: "Jumlah maksimum antena"
wordMuteMax: "Jumlah maksimum karakter yang diperbolehkan dalam membisukan kata" wordMuteMax: "Jumlah maksimum karakter yang diperbolehkan dalam membisukan kata"
@ -1243,6 +1390,7 @@ _role:
rateLimitFactor: "Batas kecepatan" rateLimitFactor: "Batas kecepatan"
descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. " descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. "
canHideAds: "Dapat menyembunyikan iklan" canHideAds: "Dapat menyembunyikan iklan"
canSearchNotes: "Penggunaan pencarian catatan"
_condition: _condition:
isLocal: "Pengguna lokal" isLocal: "Pengguna lokal"
isRemote: "Pengguna remote" isRemote: "Pengguna remote"
@ -1252,11 +1400,13 @@ _role:
followersMoreThanOrEq: "Memiliki pengikut X atau lebih dari tersebut" followersMoreThanOrEq: "Memiliki pengikut X atau lebih dari tersebut"
followingLessThanOrEq: "Mengikuti X pengguna atau kurang dari itu" followingLessThanOrEq: "Mengikuti X pengguna atau kurang dari itu"
followingMoreThanOrEq: "Mengikuti X pengguna atau lebih dari itu" followingMoreThanOrEq: "Mengikuti X pengguna atau lebih dari itu"
notesLessThanOrEq: "Jumlah postingan kurang dari sama dengan"
notesMoreThanOrEq: "Jumlah postingan lebih dari sama dengan"
and: "Kondisi-AND" and: "Kondisi-AND"
or: "Kondisi-OR" or: "Kondisi-OR"
not: "Kondisi-NOT" not: "Kondisi-NOT"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Mengurangi usaha moderasi server dengan mengenali media NSFW srcara otomatis menggunakan Machine Learning. Fungsi ini akan sedikit menaikkan beban peladen." description: "Mengurangi usaha moderasi peladen dengan mengenali media NSFW secara otomatis menggunakan Pembelajaran Mesin. Fungsi ini akan sedikit menaikkan beban peladen."
sensitivity: "Sensitivitas deteksi" sensitivity: "Sensitivitas deteksi"
sensitivityDescription: "Mengurangi sensitivitas akan mengurangi misdeteksi (false positive) sedangkan meningkatkannya akan menambah misdeteksi (false positive)." sensitivityDescription: "Mengurangi sensitivitas akan mengurangi misdeteksi (false positive) sedangkan meningkatkannya akan menambah misdeteksi (false positive)."
setSensitiveFlagAutomatically: "Tandai sebagai NSFW" setSensitiveFlagAutomatically: "Tandai sebagai NSFW"
@ -1339,21 +1489,17 @@ _aboutMisskey:
donate: "Donasi ke Misskey" donate: "Donasi ke Misskey"
morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang tidak tercantum disini. Terima kasih! 🥰" morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang tidak tercantum disini. Terima kasih! 🥰"
patrons: "Pendukung" patrons: "Pendukung"
_nsfw:
respect: "Sembunyikan media NSFW"
ignore: "Jangan sembunyikan media NSFW"
force: "Sembunyikan semua media"
_instanceTicker: _instanceTicker:
none: "Jangan tampilkan" none: "Jangan tampilkan"
remote: "Tampilkan untuk pengguna luar" remote: "Tampilkan untuk pengguna instansi luar"
always: "Selalu tampilkan" always: "Selalu tampilkan"
_serverDisconnectedBehavior: _serverDisconnectedBehavior:
reload: "Muat ulang otomatis" reload: "Muat ulang otomatis"
dialog: "Tampilkan dialog peringatan" dialog: "Tampilkan dialog peringatan"
quiet: "Tampilkan peringatan tidak mengganggu" quiet: "Tampilkan peringatan tidak mengganggu"
_channel: _channel:
create: "Buat saluran" create: "Buat Kanal"
edit: "Sunting saluran" edit: "Sunting Kanal"
setBanner: "Setel banner" setBanner: "Setel banner"
removeBanner: "Hapus banner" removeBanner: "Hapus banner"
featured: "Tren" featured: "Tren"
@ -1361,6 +1507,8 @@ _channel:
following: "Mengikuti" following: "Mengikuti"
usersCount: "{n} Partisipan" usersCount: "{n} Partisipan"
notesCount: "terdapat {n} catatan" notesCount: "terdapat {n} catatan"
nameAndDescription: "Nama dan deskripsi"
nameOnly: "Hanya nama"
_menuDisplay: _menuDisplay:
sideFull: "Horisontal" sideFull: "Horisontal"
sideIcon: "Horisontal (Ikon)" sideIcon: "Horisontal (Ikon)"
@ -1369,7 +1517,7 @@ _menuDisplay:
_wordMute: _wordMute:
muteWords: "Kata yang dibisukan" muteWords: "Kata yang dibisukan"
muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR."
muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan regular expressions." muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler."
softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa." softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa."
hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah." hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah."
soft: "Lembut" soft: "Lembut"
@ -1441,8 +1589,8 @@ _theme:
cwBg: "Latar belakang tombol Sembunyikan Konten" cwBg: "Latar belakang tombol Sembunyikan Konten"
cwFg: "Teks tombol Sembunyikan Konten" cwFg: "Teks tombol Sembunyikan Konten"
cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)" cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)"
toastBg: "Latar belakang pemberitahuan" toastBg: "Latar belakang notifikasi"
toastFg: "Teks pemberitahuan" toastFg: "Teks notifikasi"
buttonBg: "Latar belakang tombol" buttonBg: "Latar belakang tombol"
buttonHoverBg: "Latar belakang tombol (Mengambang)" buttonHoverBg: "Latar belakang tombol (Mengambang)"
inputBorder: "Batas bidang masukan" inputBorder: "Batas bidang masukan"
@ -1457,11 +1605,11 @@ _theme:
_sfx: _sfx:
note: "Catatan" note: "Catatan"
noteMy: "Catatan (Saya)" noteMy: "Catatan (Saya)"
notification: "Pemberitahuan" notification: "Notifikasi"
chat: "Pesan" chat: "Pesan"
chatBg: "Obrolan (Latar Belakang)" chatBg: "Obrolan (Latar Belakang)"
antenna: "Penerimaan Antenna" antenna: "Penerimaan Antenna"
channel: "Pemberitahuan saluran" channel: "Notifikasi Kanal"
_ago: _ago:
future: "Masa depan" future: "Masa depan"
justNow: "Baru saja" justNow: "Baru saja"
@ -1478,15 +1626,40 @@ _time:
minute: "menit" minute: "menit"
hour: "jam" hour: "jam"
day: "hari" day: "hari"
_timelineTutorial:
title: "Bagaimana cara menggunakan Misskey"
step1_1: "Ini adalah \"lini masa\". Semua \"catatan\" yang dikirimkan oleh {name} akan dimunculkan secara kronologis di sini."
step1_2: "Ada beberapa lini masa yang berbeda. Seperti contoh, \"Lini masa Beranda\" berisi catatan dari pengguna yang kamu ikuti, dan \"Lini masa lokal\" berisi catatan dari semua pengguna dari {name}."
step2_1: "Selanjutnya, mari kita coba memposting sebuah catatan. Kamu dapat melakukanya dengan menekan tombol dengan ikon pensil."
step2_2: "Bagaimana dengan menuliskan sedikit perkenalan diri, atau hanya \"Hello {name}\" kalau kamu lagi ngga feeling?"
step3_1: "Udah selesai memposting catatan pertamamu?"
step3_2: "Catatan pertamamu seharusnya sekarang sudah tampil di lini masa kamu."
step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan."
step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut."
_2fa: _2fa:
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
registerTOTP: "Daftarkan aplikasi autentikator"
passwordToTOTP: "Masukkan kata sandimu"
step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu."
step2: "Lalu, pindai kode QR yang ada di layar." step2: "Lalu, pindai kode QR yang ada di layar."
step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel."
step2Url: "Di aplikasi desktop, masukkan URL berikut:" step2Url: "Di aplikasi desktop, masukkan URL berikut:"
step3Title: "Masukkan kode autentikasi"
step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan."
step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu." step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu."
securityKeyNotSupported: "Peramban kamu tidak mendukung security key."
registerTOTPBeforeKey: "Mohon atur aplikasi autentikator untuk mendaftarkan security key atau passkey."
securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu." securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu."
chromePasskeyNotSupported: "Passkey Chrome saat ini tidak didukung."
registerSecurityKey: "Daftarkan security key atau passkey."
securityKeyName: "Masukkan nama key."
tapSecurityKey: "Mohon ikuti peramban kamu untuk mendaftarkan security key atau passkey"
removeKey: "Hapus security key"
removeKeyConfirm: "Hapus cadangan {name}?" removeKeyConfirm: "Hapus cadangan {name}?"
whyTOTPOnlyRenew: "Aplikasi autentikator tidak dapat dihapus selama security key masih terdaftar."
renewTOTP: "Atur ulang aplikasi autentikator"
renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja"
renewTOTPOk: "Atur ulang"
renewTOTPCancel: "Tidak sekarang." renewTOTPCancel: "Tidak sekarang."
_permissions: _permissions:
"read:account": "Lihat informasi akun" "read:account": "Lihat informasi akun"
@ -1504,8 +1677,8 @@ _permissions:
"read:mutes": "Lihat daftar orang yang dibisukan" "read:mutes": "Lihat daftar orang yang dibisukan"
"write:mutes": "Sunting daftar orang yang dibisukan" "write:mutes": "Sunting daftar orang yang dibisukan"
"write:notes": "Buat atau hapus catatan" "write:notes": "Buat atau hapus catatan"
"read:notifications": "Lihat pemberitahuan" "read:notifications": "Lihat notifikasi"
"write:notifications": "Sunting pemberitahuan" "write:notifications": "Sunting notifikasi"
"read:reactions": "Lihat reaksi" "read:reactions": "Lihat reaksi"
"write:reactions": "Sunting reaksi" "write:reactions": "Sunting reaksi"
"write:votes": "Beri suara" "write:votes": "Beri suara"
@ -1515,8 +1688,8 @@ _permissions:
"write:page-likes": "Sunting suka pada Halaman" "write:page-likes": "Sunting suka pada Halaman"
"read:user-groups": "Lihat grup pengguna" "read:user-groups": "Lihat grup pengguna"
"write:user-groups": "Sunting atau hapus grup pengguna" "write:user-groups": "Sunting atau hapus grup pengguna"
"read:channels": "Lihat saluran" "read:channels": "Lihat Kanal"
"write:channels": "Sunting saluran" "write:channels": "Sunting Kanal"
"read:gallery": "Lihat galeri" "read:gallery": "Lihat galeri"
"write:gallery": "Sunting galeri" "write:gallery": "Sunting galeri"
"read:gallery-likes": "Lihat daftar postingan galeri yang disukai" "read:gallery-likes": "Lihat daftar postingan galeri yang disukai"
@ -1548,7 +1721,7 @@ _widgets:
profile: "Profil" profile: "Profil"
instanceInfo: "Informasi Instansi" instanceInfo: "Informasi Instansi"
memo: "Catatan memo" memo: "Catatan memo"
notifications: "Pemberitahuan" notifications: "Notifikasi"
timeline: "Lini masa" timeline: "Lini masa"
calendar: "Kalender" calendar: "Kalender"
trends: "Tren" trends: "Tren"
@ -1610,6 +1783,8 @@ _visibility:
followersDescription: "Catat ke pengikut saja" followersDescription: "Catat ke pengikut saja"
specified: "Langsung" specified: "Langsung"
specifiedDescription: "Catat ke pengguna yang ditentukan saja" specifiedDescription: "Catat ke pengguna yang ditentukan saja"
disableFederation: "Matikan federasi"
disableFederationDescription: "Jangan kirimkan ke instansi lain"
_postForm: _postForm:
replyPlaceholder: "Balas ke catatan ini..." replyPlaceholder: "Balas ke catatan ini..."
quotePlaceholder: "Kutip catatan ini..." quotePlaceholder: "Kutip catatan ini..."
@ -1650,7 +1825,7 @@ _charts:
activeUsers: "Pengguna aktif" activeUsers: "Pengguna aktif"
notesIncDec: "Perbedaan # dalam catatan" notesIncDec: "Perbedaan # dalam catatan"
localNotesIncDec: "Perbedaan # dalam catatan lokal" localNotesIncDec: "Perbedaan # dalam catatan lokal"
remoteNotesIncDec: "Perbedaan # dalam catatan luar" remoteNotesIncDec: "Perbedaan # dalam catatan instansi luar"
notesTotal: "Total # catatan" notesTotal: "Total # catatan"
filesIncDec: "Perbedaan # dalam berkas" filesIncDec: "Perbedaan # dalam berkas"
filesTotal: "Jumlah # berkas" filesTotal: "Jumlah # berkas"
@ -1765,7 +1940,8 @@ _notification:
pollEnded: "Jajak pendapat berakhir" pollEnded: "Jajak pendapat berakhir"
receiveFollowRequest: "Permintaan mengikuti diterima" receiveFollowRequest: "Permintaan mengikuti diterima"
followRequestAccepted: "Permintaan mengikuti disetujui" followRequestAccepted: "Permintaan mengikuti disetujui"
app: "Pemberitahuan dari aplikasi" achievementEarned: "Pencapaian didapatkan"
app: "Notifikasi dari aplikasi tertaut"
_actions: _actions:
followBack: "Ikuti Kembali" followBack: "Ikuti Kembali"
reply: "Balas" reply: "Balas"
@ -1790,13 +1966,34 @@ _deck:
_columns: _columns:
main: "Utama" main: "Utama"
widgets: "Widget" widgets: "Widget"
notifications: "Pemberitahuan" notifications: "Notifikasi"
tl: "Lini masa" tl: "Lini masa"
antenna: "Antena" antenna: "Antena"
list: "Daftar" list: "Daftar"
channel: "Kanal" channel: "Kanal"
mentions: "Sebutan" mentions: "Sebutan"
direct: "Langsung" direct: "Langsung"
roleTimeline: "Lini masa peran"
_dialog:
charactersExceeded: "Kamu telah melebihi batas karakter maksimum! Saat ini pada {current} dari {max}."
charactersBelow: "Kamu berada di bawah batas minimum karakter! Saat ini pada {current} dari {min}."
_disabledTimeline:
title: "Lini masa dinonaktifkan"
description: "Saat ini kamu tidak dapat menggunakan lini masa ini karena peran kamu saat ini."
_drivecleaner:
orderBySizeDesc: "Ukuran berkas (Turun)"
orderByCreatedAtAsc: "Tanggal (Naik)"
_webhookSettings: _webhookSettings:
createWebhook: "Buat Webhook"
name: "Nama" name: "Nama"
secret: "Secret"
events: "Webhook Events"
active: "Aktif" active: "Aktif"
_events:
follow: "Ketika mengikuti pengguna"
followed: "Ketika diikuti pengguna"
note: "Ketika memposting catatan"
reply: "Ketika menerima balasan"
renote: "Ketika direnote"
reaction: "Ketika menerima reaksi"
mention: "Ketika sedang disebut"

42
locales/index.d.ts vendored
View file

@ -52,11 +52,15 @@ export interface Locale {
"deleteAndEdit": string; "deleteAndEdit": string;
"deleteAndEditConfirm": string; "deleteAndEditConfirm": string;
"addToList": string; "addToList": string;
"addToAntenna": string;
"sendMessage": string; "sendMessage": string;
"copyRSS": string; "copyRSS": string;
"copyUsername": string; "copyUsername": string;
"copyUserId": string; "copyUserId": string;
"copyNoteId": string; "copyNoteId": string;
"copyFileId": string;
"copyFolderId": string;
"copyProfileUrl": string;
"searchUser": string; "searchUser": string;
"reply": string; "reply": string;
"loadMore": string; "loadMore": string;
@ -139,8 +143,10 @@ export interface Locale {
"suspendConfirm": string; "suspendConfirm": string;
"unsuspendConfirm": string; "unsuspendConfirm": string;
"selectList": string; "selectList": string;
"editList": string;
"selectChannel": string; "selectChannel": string;
"selectAntenna": string; "selectAntenna": string;
"editAntenna": string;
"selectWidget": string; "selectWidget": string;
"editWidgets": string; "editWidgets": string;
"editWidgetsExit": string; "editWidgetsExit": string;
@ -153,6 +159,8 @@ export interface Locale {
"settingGuide": string; "settingGuide": string;
"cacheRemoteFiles": string; "cacheRemoteFiles": string;
"cacheRemoteFilesDescription": string; "cacheRemoteFilesDescription": string;
"cacheRemoteSensitiveFiles": string;
"cacheRemoteSensitiveFilesDescription": string;
"flagAsBot": string; "flagAsBot": string;
"flagAsBotDescription": string; "flagAsBotDescription": string;
"flagAsCat": string; "flagAsCat": string;
@ -314,7 +322,7 @@ export interface Locale {
"rename": string; "rename": string;
"avatar": string; "avatar": string;
"banner": string; "banner": string;
"nsfw": string; "displayOfSensitiveMedia": string;
"whenServerDisconnected": string; "whenServerDisconnected": string;
"disconnectedFromServer": string; "disconnectedFromServer": string;
"reload": string; "reload": string;
@ -1066,6 +1074,29 @@ export interface Locale {
"additionalEmojiDictionary": string; "additionalEmojiDictionary": string;
"installed": string; "installed": string;
"branding": string; "branding": string;
"enableServerMachineStats": string;
"enableIdenticonGeneration": string;
"turnOffToImprovePerformance": string;
"createInviteCode": string;
"createWithOptions": string;
"createCount": string;
"inviteCodeCreated": string;
"inviteLimitExceeded": string;
"createLimitRemaining": string;
"inviteLimitResetCycle": string;
"expirationDate": string;
"noExpirationDate": string;
"inviteCodeUsedAt": string;
"registeredUserUsingInviteCode": string;
"waitingForMailAuth": string;
"inviteCodeCreator": string;
"usedAt": string;
"unused": string;
"used": string;
"expired": string;
"doYouAgree": string;
"beSureToReadThisAsItIsImportant": string;
"iHaveReadXCarefullyAndAgree": string;
"_initialAccountSetting": { "_initialAccountSetting": {
"accountCreated": string; "accountCreated": string;
"letsStartAccountSetup": string; "letsStartAccountSetup": string;
@ -1456,6 +1487,9 @@ export interface Locale {
"ltlAvailable": string; "ltlAvailable": string;
"canPublicNote": string; "canPublicNote": string;
"canInvite": string; "canInvite": string;
"inviteLimit": string;
"inviteLimitCycle": string;
"inviteExpirationTime": string;
"canManageCustomEmojis": string; "canManageCustomEmojis": string;
"driveCapacity": string; "driveCapacity": string;
"alwaysMarkNsfw": string; "alwaysMarkNsfw": string;
@ -1526,6 +1560,7 @@ export interface Locale {
"back": string; "back": string;
"reduceFrequencyOfThisAd": string; "reduceFrequencyOfThisAd": string;
"hide": string; "hide": string;
"timezoneinfo": string;
}; };
"_forgotPassword": { "_forgotPassword": {
"enterEmail": string; "enterEmail": string;
@ -1587,7 +1622,7 @@ export interface Locale {
"morePatrons": string; "morePatrons": string;
"patrons": string; "patrons": string;
}; };
"_nsfw": { "_displayOfSensitiveMedia": {
"respect": string; "respect": string;
"ignore": string; "ignore": string;
"force": string; "force": string;
@ -2103,6 +2138,7 @@ export interface Locale {
"introduction": string; "introduction": string;
"introduction2": string; "introduction2": string;
"widgetsIntroduction": string; "widgetsIntroduction": string;
"useSimpleUiForNonRootPages": string;
"_columns": { "_columns": {
"main": string; "main": string;
"widgets": string; "widgets": string;
@ -2148,4 +2184,4 @@ export interface Locale {
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;
}; };
export = locales; export default locales;

View file

@ -2,8 +2,8 @@
* Languages Loader * Languages Loader
*/ */
const fs = require('fs'); import * as fs from 'node:fs';
const yaml = require('js-yaml'); import * as yaml from 'js-yaml';
const merge = (...args) => args.reduce((a, c) => ({ const merge = (...args) => args.reduce((a, c) => ({
...a, ...a,
@ -51,9 +51,9 @@ const primaries = {
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く // 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {}); const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
module.exports = Object.entries(locales) export default Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => { .reduce((a, [k ,v]) => (a[k] = (() => {
const [lang] = k.split('-'); const [lang] = k.split('-');
switch (k) { switch (k) {

View file

@ -26,15 +26,15 @@ otherSettings: "Altre impostazioni"
openInWindow: "Apri in una finestra" openInWindow: "Apri in una finestra"
profile: "Profilo" profile: "Profilo"
timeline: "Timeline" timeline: "Timeline"
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo." noAccountDescription: "La persona non ha ancora scritto alcuna autobiografia."
login: "Accedi" login: "Accedi"
loggingIn: "Accesso in corso..." loggingIn: "Accesso in corso..."
logout: "Uscita" logout: "Uscita"
signup: "Iscriviti" signup: "Iscriviti"
uploading: "Caricamento..." uploading: "Caricamento..."
save: "Salva" save: "Salva"
users: "Utente" users: "Profili"
addUser: "Aggiungi utente" addUser: "Aggiungi profilo"
favorite: "Preferiti" favorite: "Preferiti"
favorites: "Preferiti" favorites: "Preferiti"
unfavorite: "Rimuovi nota dai preferiti" unfavorite: "Rimuovi nota dai preferiti"
@ -49,10 +49,16 @@ delete: "Elimina"
deleteAndEdit: "Elimina e modifica" deleteAndEdit: "Elimina e modifica"
deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate." deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate."
addToList: "Aggiungi alla lista" addToList: "Aggiungi alla lista"
addToAntenna: "Aggiungi all'antenna"
sendMessage: "Invia messaggio" sendMessage: "Invia messaggio"
copyRSS: "Copia RSS" copyRSS: "Copia RSS"
copyUsername: "Copia nome utente" copyUsername: "Copia nome utente"
searchUser: "Cerca utente" copyUserId: "Copia ID del profilo"
copyNoteId: "Copia ID della Nota"
copyFileId: "Copia ID del file"
copyFolderId: "Copia ID della cartella"
copyProfileUrl: "Copia URL del profilo"
searchUser: "Cerca profilo"
reply: "Rispondi" reply: "Rispondi"
loadMore: "Mostra di più" loadMore: "Mostra di più"
showMore: "Espandi" showMore: "Espandi"
@ -134,8 +140,10 @@ unblockConfirm: "Vuoi davvero sbloccare il profilo?"
suspendConfirm: "Vuoi sospendere questo profilo?" suspendConfirm: "Vuoi sospendere questo profilo?"
unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?" unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?"
selectList: "Seleziona una lista" selectList: "Seleziona una lista"
editList: "Modifica Lista"
selectChannel: "Seleziona canale" selectChannel: "Seleziona canale"
selectAntenna: "Scegli un'antenna" selectAntenna: "Scegli un'antenna"
editAntenna: "Modifica Antenna"
selectWidget: "Seleziona il riquadro" selectWidget: "Seleziona il riquadro"
editWidgets: "Modifica i riquadri" editWidgets: "Modifica i riquadri"
editWidgetsExit: "Conferma le modifiche" editWidgetsExit: "Conferma le modifiche"
@ -148,12 +156,14 @@ addEmoji: "Aggiungi un emoji"
settingGuide: "Configurazione suggerita" settingGuide: "Configurazione suggerita"
cacheRemoteFiles: "Memorizza i file remoti nella cache" cacheRemoteFiles: "Memorizza i file remoti nella cache"
cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime." cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime."
cacheRemoteSensitiveFiles: "Memorizza nella cache i file sensibili remoti"
cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file sensibili verranno caricati direttamente dall'istanza remota senza essere salvati dal server."
flagAsBot: "Io sono un robot" flagAsBot: "Io sono un robot"
flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot." flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot."
flagAsCat: "Sono un gatto" flagAsCat: "Sono un gatto"
flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo" flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo"
flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline." flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline."
flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso." flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note"
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui"
addAccount: "Aggiungi profilo" addAccount: "Aggiungi profilo"
reloadAccountsList: "Ricarica l'elenco dei profili" reloadAccountsList: "Ricarica l'elenco dei profili"
@ -205,7 +215,7 @@ blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga.
muteAndBlock: "Silenziati / Bloccati" muteAndBlock: "Silenziati / Bloccati"
mutedUsers: "Profili silenziati" mutedUsers: "Profili silenziati"
blockedUsers: "Profili bloccati" blockedUsers: "Profili bloccati"
noUsers: "Nessun utente trovato" noUsers: "Non ci sono profili"
editProfile: "Modifica profilo" editProfile: "Modifica profilo"
noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?" noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?"
pinLimitExceeded: "Non puoi fissare altre note " pinLimitExceeded: "Non puoi fissare altre note "
@ -236,8 +246,8 @@ newPasswordRetype: "Conferma password"
attachFile: "Allega file" attachFile: "Allega file"
more: "Di più!" more: "Di più!"
featured: "Tendenze" featured: "Tendenze"
usernameOrUserId: "Nome utente o ID utente" usernameOrUserId: "Nome utente o ID"
noSuchUser: "Nessun utente trovato" noSuchUser: "Profilo non trovato"
lookup: "Ricerca remota" lookup: "Ricerca remota"
announcements: "Annunci" announcements: "Annunci"
imageUrl: "URL dell'immagine" imageUrl: "URL dell'immagine"
@ -269,7 +279,7 @@ basicNotesBeforeCreateAccount: "Note importanti"
termsOfService: "Informativa Privacy" termsOfService: "Informativa Privacy"
start: "Inizia!" start: "Inizia!"
home: "Home" home: "Home"
remoteUserCaution: "Può darsi che le informazioni siano incomplete perché questo è un utente remoto." remoteUserCaution: "Le informazioni potrebbero essere incomplete poiché questo profilo remoto potrebbe non essere completamente federato."
activity: "Attività" activity: "Attività"
images: "Immagini" images: "Immagini"
image: "Immagini" image: "Immagini"
@ -309,7 +319,7 @@ copyUrl: "Copia URL"
rename: "Modifica nome" rename: "Modifica nome"
avatar: "Foto del profilo" avatar: "Foto del profilo"
banner: "Intestazione" banner: "Intestazione"
nsfw: "Contenuti sensibili" displayOfSensitiveMedia: "Visibilità dei media sensibili"
whenServerDisconnected: "Quando la connessione col server è persa" whenServerDisconnected: "Quando la connessione col server è persa"
disconnectedFromServer: "Il server si è disconnesso" disconnectedFromServer: "Il server si è disconnesso"
reload: "Ricarica" reload: "Ricarica"
@ -444,7 +454,7 @@ signinRequired: "Occorre avere un profilo registrato su questa istanza"
invitations: "Invita" invitations: "Invita"
invitationCode: "Codice di invito" invitationCode: "Codice di invito"
checking: "Confermando" checking: "Confermando"
available: "Consigliati" available: "Disponibile"
unavailable: "Il nome utente è già in uso" unavailable: "Il nome utente è già in uso"
usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'" usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'"
tooShort: "Troppo breve" tooShort: "Troppo breve"
@ -560,6 +570,7 @@ accountDeletedDescription: "Questo profilo è stato eliminato."
menu: "Menù" menu: "Menù"
divider: "Linea di separazione" divider: "Linea di separazione"
addItem: "Aggiungi elemento" addItem: "Aggiungi elemento"
rearrange: "Riordina"
relays: "Ripetitori" relays: "Ripetitori"
addRelay: "Aggiungi ripetitore" addRelay: "Aggiungi ripetitore"
inboxUrl: "Inbox URL" inboxUrl: "Inbox URL"
@ -660,7 +671,7 @@ instanceTicker: "Informazioni sull'istanza da cui vengono le note"
waitingFor: "Aspettando {x}" waitingFor: "Aspettando {x}"
random: "Casuale" random: "Casuale"
system: "Sistema" system: "Sistema"
switchUi: "Cambiare interfaccia" switchUi: "Cambia interfaccia grafica"
desktop: "Desktop" desktop: "Desktop"
clip: "Clip" clip: "Clip"
createNew: "Crea" createNew: "Crea"
@ -703,6 +714,8 @@ contact: "Contatti"
useSystemFont: "Usa il carattere predefinito del sistema" useSystemFont: "Usa il carattere predefinito del sistema"
clips: "Clip" clips: "Clip"
experimentalFeatures: "Funzioni sperimentali" experimentalFeatures: "Funzioni sperimentali"
experimental: "Sperimentale"
thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe essere malfunzionante o cambiare in futuro."
developer: "Sviluppatore" developer: "Sviluppatore"
makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\"" makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\""
makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"." makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"."
@ -766,10 +779,10 @@ info: "Informazioni"
userInfo: "Informazioni utente" userInfo: "Informazioni utente"
unknown: "Sconosciuto" unknown: "Sconosciuto"
onlineStatus: "Stato di connessione" onlineStatus: "Stato di connessione"
hideOnlineStatus: "Stato invisibile" hideOnlineStatus: "Modalità invisibile"
hideOnlineStatusDescription: "Abilitare l'opzione di stato invisibile può guastare la praticità di singole funzioni, come la ricerca." hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca."
online: "Online" online: "Online"
active: "Attiv@" active: "Attività"
offline: "Offline" offline: "Offline"
notRecommended: "Sconsigliato" notRecommended: "Sconsigliato"
botProtection: "Protezione contro i bot" botProtection: "Protezione contro i bot"
@ -779,7 +792,7 @@ switchAccount: "Cambia profilo"
enabled: "Attivo" enabled: "Attivo"
disabled: "Inattivo" disabled: "Inattivo"
quickAction: "Azioni rapide" quickAction: "Azioni rapide"
user: "Utente" user: "Profilo"
administration: "Gestione" administration: "Gestione"
accounts: "Profilo" accounts: "Profilo"
switch: "Cambia" switch: "Cambia"
@ -787,6 +800,7 @@ noMaintainerInformationWarning: "Le informazioni amministratore non sono imposta
noBotProtectionWarning: "Nessuna protezione impostata contro i bot." noBotProtectionWarning: "Nessuna protezione impostata contro i bot."
configure: "Imposta" configure: "Imposta"
postToGallery: "Pubblicare nella galleria" postToGallery: "Pubblicare nella galleria"
postToHashtag: "Pubblica a questo hashtag"
gallery: "Galleria" gallery: "Galleria"
recentPosts: "Le più recenti" recentPosts: "Le più recenti"
popularPosts: "Le più visualizzate" popularPosts: "Le più visualizzate"
@ -811,7 +825,7 @@ received: "Ricevuto"
searchResult: "Risultati della Ricerca" searchResult: "Risultati della Ricerca"
hashtags: "Hashtag" hashtags: "Hashtag"
troubleshooting: "Risoluzione problemi" troubleshooting: "Risoluzione problemi"
useBlurEffect: "Utilizza effetto sfocatura nell'interfaccia" useBlurEffect: "Utilizza effetto sfocatura"
learnMore: "Più dettagli" learnMore: "Più dettagli"
misskeyUpdated: "Misskey è stato aggiornato!" misskeyUpdated: "Misskey è stato aggiornato!"
whatIsNew: "Visualizza le informazioni sull'aggiornamento" whatIsNew: "Visualizza le informazioni sull'aggiornamento"
@ -820,6 +834,7 @@ translatedFrom: "Tradotto da {x}"
accountDeletionInProgress: "È in corso l'eliminazione del profilo" accountDeletionInProgress: "È in corso l'eliminazione del profilo"
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito." usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
aiChanMode: "Modalità Ai" aiChanMode: "Modalità Ai"
devMode: "Modalità sviluppatori"
keepCw: "Mantieni il Content Warning" keepCw: "Mantieni il Content Warning"
pubSub: "Publish/Subscribe del profilo" pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente" lastCommunication: "La comunicazione più recente"
@ -829,6 +844,8 @@ breakFollow: "Non seguire"
breakFollowConfirm: "Vuoi davvero togliere follower?" breakFollowConfirm: "Vuoi davvero togliere follower?"
itsOn: "Abilitato" itsOn: "Abilitato"
itsOff: "Disabilitato" itsOff: "Disabilitato"
on: "Acceso"
off: "Spento"
emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi" emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi"
unread: "Non lette" unread: "Non lette"
filter: "Filtri" filter: "Filtri"
@ -839,8 +856,8 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di
classic: "Classico" classic: "Classico"
muteThread: "Silenzia la conversazione" muteThread: "Silenzia la conversazione"
unmuteThread: "Riattiva la conversazione" unmuteThread: "Riattiva la conversazione"
ffVisibility: "Ambito pubblico del collegamento" ffVisibility: "Visibilità delle connessioni"
ffVisibilityDescription: "È possibile impostare la portata pubblica delle informazioni sui propri follower/seguaci." ffVisibilityDescription: "Puoi scegliere a chi mostrare le tue relazioni con altri profili nel fediverso."
continueThread: "Altri thread." continueThread: "Altri thread."
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
incorrectPassword: "La password è errata." incorrectPassword: "La password è errata."
@ -848,7 +865,7 @@ voteConfirm: "Votare per「{choice}」?"
hide: "Nascondere" hide: "Nascondere"
useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile" useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
welcomeBackWithName: "Ciao, {name}! Eccoti di nuovo!" welcomeBackWithName: "Ciao, {name}! Eccoti di nuovo!"
clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email." clickToFinishEmailVerification: "Premi il bottone \"{ok}\" per completare la verifica dell'indirizzo email."
overridedDeviceKind: "Tipo di dispositivo" overridedDeviceKind: "Tipo di dispositivo"
smartphone: "Smartphone" smartphone: "Smartphone"
tablet: "Tablet" tablet: "Tablet"
@ -909,6 +926,7 @@ remoteOnly: "Solo remoto"
failedToUpload: "errore di caricamento" failedToUpload: "errore di caricamento"
cannotUploadBecauseInappropriate: "Non è possibile caricarlo perché è stato stabilito che potrebbe contenere contenuti inappropriati." cannotUploadBecauseInappropriate: "Non è possibile caricarlo perché è stato stabilito che potrebbe contenere contenuti inappropriati."
cannotUploadBecauseNoFreeSpace: "Impossibile caricare a causa della mancanza di spazio libero sul drive." cannotUploadBecauseNoFreeSpace: "Impossibile caricare a causa della mancanza di spazio libero sul drive."
cannotUploadBecauseExceedsFileSizeLimit: "Il file non può essere caricato perché eccede le dimensioni consentite."
beta: "Versione beta" beta: "Versione beta"
enableAutoSensitive: "Determinazione automatica del NSFW" enableAutoSensitive: "Determinazione automatica del NSFW"
enableAutoSensitiveDescription: "Se disponibile, il flag NSFW viene impostato automaticamente sui media utilizzando l'apprendimento automatico. Anche se questa funzione è disattivata, in alcuni casi può essere impostata automaticamente." enableAutoSensitiveDescription: "Se disponibile, il flag NSFW viene impostato automaticamente sui media utilizzando l'apprendimento automatico. Anche se questa funzione è disattivata, in alcuni casi può essere impostata automaticamente."
@ -942,6 +960,7 @@ didYouLikeMisskey: "Ti piace Misskey?"
pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!"
roles: "Ruoli" roles: "Ruoli"
role: "Ruolo" role: "Ruolo"
noRole: "Ruolo non trovato"
normalUser: "Profilo standard" normalUser: "Profilo standard"
undefined: "Indefinito" undefined: "Indefinito"
assign: "Assegna" assign: "Assegna"
@ -981,10 +1000,13 @@ cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Reazioni consentite" reactionAcceptance: "Reazioni consentite"
likeOnly: "Solo i Like" likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti" likeOnlyForRemote: "Solo Like remoti"
nonSensitiveOnly: "Solamente non sensibili"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Solamente non sensibili (solo Mi piace remoti)"
rolesAssignedToMe: "I miei ruoli" rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi davvero ripristinare la password?" resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
sensitiveWords: "Parole sensibili" sensitiveWords: "Parole sensibili"
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare."
notesSearchNotAvailable: "Non è possibile cercare tra le Note." notesSearchNotAvailable: "Non è possibile cercare tra le Note."
license: "Licenza" license: "Licenza"
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
@ -1003,9 +1025,13 @@ videos: "Video"
dataSaver: "Risparmia dati" dataSaver: "Risparmia dati"
accountMigration: "Migrazione del profilo" accountMigration: "Migrazione del profilo"
accountMoved: "Questo profilo ha migrato altrove:" accountMoved: "Questo profilo ha migrato altrove:"
accountMovedShort: "Questo profilo è stato migrato"
operationForbidden: "Operazione non consentita"
forceShowAds: "Mostra sempre i banner" forceShowAds: "Mostra sempre i banner"
addMemo: "Aggiungi Memo" addMemo: "Aggiungi Memo"
editMemo: "Modifica Memo" editMemo: "Modifica Memo"
reactionsList: "Elenco delle reazioni"
renotesList: "Elenco di Rinota"
notificationDisplay: "Stile delle notifiche" notificationDisplay: "Stile delle notifiche"
leftTop: "In alto a sinistra" leftTop: "In alto a sinistra"
rightTop: "In alto a destra" rightTop: "In alto a destra"
@ -1019,18 +1045,83 @@ serverRules: "Regolamento"
pleaseConfirmBelowBeforeSignup: "Ai sensi del regolamento EU 679/2016 GDPR, autorizzo il trattamento dati personali come descritto nella informativa Privacy." pleaseConfirmBelowBeforeSignup: "Ai sensi del regolamento EU 679/2016 GDPR, autorizzo il trattamento dati personali come descritto nella informativa Privacy."
pleaseAgreeAllToContinue: "Per continuare, occorre selezionare ed essere d'accordo su tutto." pleaseAgreeAllToContinue: "Per continuare, occorre selezionare ed essere d'accordo su tutto."
continue: "Continua" continue: "Continua"
preservedUsernames: "Nomi utente riservati"
preservedUsernamesDescription: "Elenca, uno per linea, i nomi utente che non possono essere registrati durante la creazione del profilo. La restrizione non si applica agli amministratori. Inoltre, i profili già registrati sono esenti."
createNoteFromTheFile: "Crea Nota da questo file"
archive: "Archivio"
channelArchiveConfirmTitle: "Vuoi davvero archiviare {name}?"
channelArchiveConfirmDescription: "Un canale archiviato non compare nell'elenco canali, nemmeno nei risultati di ricerca. Non può ricevere nemmeno nuove Note."
thisChannelArchived: "Questo canale è stato archiviato."
displayOfNote: "Visualizzazione delle Note"
initialAccountSetting: "Impostazioni iniziali del profilo"
youFollowing: "Seguiti" youFollowing: "Seguiti"
preventAiLearning: "Impedisci l'apprendimento della IA"
preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata."
options: "Opzioni del ruolo" options: "Opzioni del ruolo"
specifyUser: "Profilo specifico"
failedToPreviewUrl: "Anteprima non disponibile"
update: "Aggiorna"
rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Se non viene specificato alcun ruolo, chiunque può reagire con questa emoji."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Questi ruoli devono essere pubblici"
cancelReactionConfirm: "Vuoi annullare la tua reazione?"
changeReactionConfirm: "Vuoi cambiare la tua reazione?"
later: "Non ora"
goToMisskey: "Vai a Misskey"
additionalEmojiDictionary: "Dizionario aggiuntivo emoji"
installed: "Installazione avvenuta"
branding: "Branding"
enableServerMachineStats: "Pubblicare le informazioni sul server"
enableIdenticonGeneration: "Generazione automatica delle Identicon"
turnOffToImprovePerformance: "Disattiva, per migliorare le prestazioni"
createInviteCode: "Genera codice di invito"
createWithOptions: "Genera con opzioni"
createCount: "Conteggio inviti"
inviteCodeCreated: "Inviti generati"
inviteLimitExceeded: "Hai raggiunto il numero massimo di codici invito generabili."
createLimitRemaining: "Inviti generabili: {limit} rimanenti"
inviteLimitResetCycle: "Alle {time}, il limite verrà ripristinato a {limit}"
expirationDate: "Scadenza"
noExpirationDate: "Perpetuo"
inviteCodeUsedAt: "Codice di invito usato alle"
registeredUserUsingInviteCode: "Codice di invito usato da"
waitingForMailAuth: "In attesa della verifica email"
inviteCodeCreator: "Codice di invito creato da"
usedAt: "Usato alle"
unused: "Inutilizzato"
used: "Utilizzato"
expired: "Scaduto"
_initialAccountSetting:
accountCreated: "Il tuo profilo è stato creato!"
letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
letsFillYourProfile: "Innanzitutto, compila il tuo profilo."
profileSetting: "Impostazioni del profilo"
privacySetting: "Impostazioni sulla privacy"
theseSettingsCanEditLater: "In seguito, potrai cambiare la tua scelta."
youCanEditMoreSettingsInSettingsPageLater: "Nella pagina \"Impostazioni\", è possibile personalizzare di più il tuo profilo. Dacci un'occhiata dopo!"
followUsers: "Per comporre la tua Timeline Home (personale) segui i profili delle persone che ti interessano."
pushNotificationDescription: "Attivare le notifiche push ti permettera di ricevere informazioni sulla attività di {name} direttamente sul tuo dispositivo."
initialAccountSettingCompleted: "Hai completato la configurazione iniziale!"
haveFun: "Divertiti con {name}!"
ifYouNeedLearnMore: "Per saperne di più su come usare {name} (Misskey), visita la pagina {link}"
skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?"
laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?"
_serverRules: _serverRules:
description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio." description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio."
_accountMigration: _accountMigration:
moveFrom: "Migra un altro profilo dentro a questo" moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto"
moveFromLabel: "Profilo da cui migrare:" moveFromLabel: "Profilo da cui migrare:"
moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it" moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
moveTo: "Migrare questo profilo verso un un altro" moveTo: "Migrare questo profilo verso un un altro"
moveToLabel: "Profilo verso cui migrare" moveToLabel: "Profilo verso cui migrare"
moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata."
moveAccountDescription: "Questa attività è irreversibile! Innanzitutto, assicurati di aver creato, nella istanza di destinazione, un alias con l'indirizzo di questo profilo. Successivamente, indica qui il profilo di destinazione in questo modo: @persona@istanza.it" moveAccountDescription: "Questa attività è irreversibile! Innanzitutto, assicurati di aver creato, nella istanza di destinazione, un alias con l'indirizzo di questo profilo. Successivamente, indica qui il profilo di destinazione in questo modo: @persona@istanza.it"
moveAccountHowTo: "Per migrare su un profilo remoto, crea prima un alias di questo profilo, sulla istanza di destinazione.\nDopo aver creato l'alias, inserisci l'indirizzo di destinazione, indicando ad esempio: @profilo@altra.istanza"
startMigration: "Avvia la migrazione"
migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo." migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo."
movedAndCannotBeUndone: "Il tuo profilo è stato migrato.\nLa migrazione non può essere annullata."
postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Follow che i Follower scenderanno a zero. I tuoi follower saranno comunque in grado di vedere le Note per soli follower, poiché non smetteranno di seguirti."
movedTo: "Profilo verso cui migrare" movedTo: "Profilo verso cui migrare"
_achievements: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
@ -1310,8 +1401,12 @@ _role:
ltlAvailable: "Disponibilità della Timeline Locale" ltlAvailable: "Disponibilità della Timeline Locale"
canPublicNote: "Può scrivere Note con Visibilità Pubblica" canPublicNote: "Può scrivere Note con Visibilità Pubblica"
canInvite: "Genera codici di invito all'istanza" canInvite: "Genera codici di invito all'istanza"
inviteLimit: "Limite di codici invito"
inviteLimitCycle: "Intervallo di emissione del codice di invito"
inviteExpirationTime: "Scadenza del codice di invito"
canManageCustomEmojis: "Gestire le emoji personalizzate" canManageCustomEmojis: "Gestire le emoji personalizzate"
driveCapacity: "Capienza del Drive" driveCapacity: "Capienza del Drive"
alwaysMarkNsfw: "Imposta sempre come NSFW"
pinMax: "Quantità massima di Note in primo piano" pinMax: "Quantità massima di Note in primo piano"
antennaMax: "Quantità massima di Antenne" antennaMax: "Quantità massima di Antenne"
wordMuteMax: "Lunghezza massima del filtro parole" wordMuteMax: "Lunghezza massima del filtro parole"
@ -1371,6 +1466,7 @@ _ad:
back: "Indietro" back: "Indietro"
reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso" reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso"
hide: "Nascondi" hide: "Nascondi"
timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server."
_forgotPassword: _forgotPassword:
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
@ -1422,10 +1518,10 @@ _aboutMisskey:
donate: "Sostieni Misskey" donate: "Sostieni Misskey"
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
patrons: "Sostenitori" patrons: "Sostenitori"
_nsfw: _displayOfSensitiveMedia:
respect: "Nascondere i media segnati come sensibli" respect: "Nascondere i media sensibili"
ignore: "Visualizzare i media segnati come sensibili" ignore: "Non nascondere i media sensibili"
force: "Nascondere tutti i media" force: "Nascondi tutti i media"
_instanceTicker: _instanceTicker:
none: "Nascondi" none: "Nascondi"
remote: "Mostra solo per i profili remoti" remote: "Mostra solo per i profili remoti"
@ -1563,6 +1659,16 @@ _time:
minute: "min" minute: "min"
hour: "ore" hour: "ore"
day: "giorni" day: "giorni"
_timelineTutorial:
title: "Come usare Misskey"
step1_1: "Questa è la \"Timeline\". tutte le \"Note\" pubblicate su {name} vengono elencate in ordine cronologico."
step1_2: "Le Timeline sono diverse, ad esempio, la \"Home\" elenca le Note dei profili che segui. Quella \"Locale\" elenca quelle di tutti i profili attivi su {name}."
step2_1: "Prova a pubblicare una Nota. Semplicemente premendo il bottone con l'icona di una matita."
step2_2: "Potresti scrivere la tua presentazione, oppure semplicemente \"Ciao da {name}!\""
step3_1: "Hai pubblicato qualcosa?"
step3_2: "In tal caso, dovrebbe comparire subito nella tua \"Home\""
step4_1: "Puoi reagire con un emoji alle Note."
step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with.\nPer reagire con una emoji, premi il bottone \"+\" (più) visibile vicino ad ogni Nota e scegli dall'elenco la emoji che rappresenta la tua reazione."
_2fa: _2fa:
alreadyRegistered: "La configurazione è stata già completata." alreadyRegistered: "La configurazione è stata già completata."
registerTOTP: "Registra un'app di autenticazione" registerTOTP: "Registra un'app di autenticazione"
@ -1710,7 +1816,7 @@ _visibility:
followersDescription: "Visibile solo ai tuoi follower" followersDescription: "Visibile solo ai tuoi follower"
specified: "Nota diretta" specified: "Nota diretta"
specifiedDescription: "Visibile solo ai profili menzionati" specifiedDescription: "Visibile solo ai profili menzionati"
disableFederation: "Federazione disabilitata" disableFederation: "Non federare"
disableFederationDescription: "Non spedire attività alle altre istanze remote" disableFederationDescription: "Non spedire attività alle altre istanze remote"
_postForm: _postForm:
replyPlaceholder: "Rispondi a questa nota..." replyPlaceholder: "Rispondi a questa nota..."
@ -1890,6 +1996,7 @@ _deck:
introduction: "Combinate le colonne per creare la vostra interfaccia!" introduction: "Combinate le colonne per creare la vostra interfaccia!"
introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo." introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo."
widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità" widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità"
useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice"
_columns: _columns:
main: "Principale" main: "Principale"
widgets: "Riquadri" widgets: "Riquadri"

View file

@ -49,11 +49,15 @@ delete: "削除"
deleteAndEdit: "削除して編集" deleteAndEdit: "削除して編集"
deleteAndEditConfirm: "このートを削除してもう一度編集しますかこのートへのリアクション、Renote、返信も全て削除されます。" deleteAndEditConfirm: "このートを削除してもう一度編集しますかこのートへのリアクション、Renote、返信も全て削除されます。"
addToList: "リストに追加" addToList: "リストに追加"
addToAntenna: "アンテナに追加"
sendMessage: "メッセージを送信" sendMessage: "メッセージを送信"
copyRSS: "RSSをコピー" copyRSS: "RSSをコピー"
copyUsername: "ユーザー名をコピー" copyUsername: "ユーザー名をコピー"
copyUserId: "ユーザーIDをコピー" copyUserId: "ユーザーIDをコピー"
copyNoteId: "ートIDをコピー" copyNoteId: "ートIDをコピー"
copyFileId: "ファイルIDをコピー"
copyFolderId: "フォルダーIDをコピー"
copyProfileUrl: "プロフィールURLをコピー"
searchUser: "ユーザーを検索" searchUser: "ユーザーを検索"
reply: "返信" reply: "返信"
loadMore: "もっと見る" loadMore: "もっと見る"
@ -112,7 +116,7 @@ pinnedNote: "ピン留めされたノート"
pinned: "ピン留め" pinned: "ピン留め"
you: "あなた" you: "あなた"
clickToShow: "クリックして表示" clickToShow: "クリックして表示"
sensitive: "閲覧注意" sensitive: "センシティブ"
add: "追加" add: "追加"
reaction: "リアクション" reaction: "リアクション"
reactions: "リアクション" reactions: "リアクション"
@ -120,8 +124,8 @@ reactionSetting: "ピッカーに表示するリアクション"
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
rememberNoteVisibility: "公開範囲を記憶する" rememberNoteVisibility: "公開範囲を記憶する"
attachCancel: "添付取り消し" attachCancel: "添付取り消し"
markAsSensitive: "閲覧注意にする" markAsSensitive: "センシティブとして設定"
unmarkAsSensitive: "閲覧注意を解除する" unmarkAsSensitive: "センシティブを解除する"
enterFileName: "ファイル名を入力" enterFileName: "ファイル名を入力"
mute: "ミュート" mute: "ミュート"
unmute: "ミュート解除" unmute: "ミュート解除"
@ -136,8 +140,10 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?" suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?" unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択" selectList: "リストを選択"
editList: "リストを編集"
selectChannel: "チャンネルを選択" selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択" selectAntenna: "アンテナを選択"
editAntenna: "アンテナを編集"
selectWidget: "ウィジェットを選択" selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集" editWidgets: "ウィジェットを編集"
editWidgetsExit: "編集を終了" editWidgetsExit: "編集を終了"
@ -150,6 +156,8 @@ addEmoji: "絵文字を追加"
settingGuide: "おすすめ設定" settingGuide: "おすすめ設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする" cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。" cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
flagAsBot: "Botとして設定" flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。" flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!" flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
@ -311,7 +319,7 @@ copyUrl: "URLをコピー"
rename: "名前を変更" rename: "名前を変更"
avatar: "アイコン" avatar: "アイコン"
banner: "バナー" banner: "バナー"
nsfw: "閲覧注意" displayOfSensitiveMedia: "センシティブなメディアの表示"
whenServerDisconnected: "サーバーとの接続が失われたとき" whenServerDisconnected: "サーバーとの接続が失われたとき"
disconnectedFromServer: "サーバーから切断されました" disconnectedFromServer: "サーバーから切断されました"
reload: "リロード" reload: "リロード"
@ -693,7 +701,7 @@ driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否" noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。" noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。"
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。" lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする" alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする" loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない" disableShowingAnimatedImages: "アニメーション画像を再生しない"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。" verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
@ -920,8 +928,8 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
beta: "ベータ" beta: "ベータ"
enableAutoSensitive: "自動NSFW判定" enableAutoSensitive: "自動センシティブ判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。" activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
navbar: "ナビゲーションバー" navbar: "ナビゲーションバー"
shuffle: "シャッフル" shuffle: "シャッフル"
@ -1034,7 +1042,7 @@ vertical: "縦"
horizontal: "横" horizontal: "横"
position: "位置" position: "位置"
serverRules: "サーバールール" serverRules: "サーバールール"
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。" pleaseConfirmBelowBeforeSignup: "このサーバーに登録するには、以下の内容を確認し同意する必要があります。"
pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。" pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。"
continue: "続ける" continue: "続ける"
preservedUsernames: "予約ユーザー名" preservedUsernames: "予約ユーザー名"
@ -1063,6 +1071,29 @@ goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書" additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み" installed: "インストール済み"
branding: "ブランディング" branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
createInviteCode: "招待コードを作成"
createWithOptions: "オプションを指定して作成"
createCount: "作成数"
inviteCodeCreated: "招待コードを作成しました"
inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。"
createLimitRemaining: "作成できる招待コード: 残り {limit} 個"
inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。"
expirationDate: "有効期限"
noExpirationDate: "有効期限を設けない"
inviteCodeUsedAt: "招待コードが使用された日時"
registeredUserUsingInviteCode: "招待コードを使用したユーザー"
waitingForMailAuth: "メール認証待ち"
inviteCodeCreator: "招待コードを作成したユーザー"
usedAt: "使用日時"
unused: "未使用"
used: "使用済み"
expired: "期限切れ"
doYouAgree: "同意しますか?"
beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。"
iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!" accountCreated: "アカウントの作成が完了しました!"
@ -1378,6 +1409,9 @@ _role:
ltlAvailable: "ローカルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可" canPublicNote: "パブリック投稿の許可"
canInvite: "サーバー招待コードの発行" canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"
inviteExpirationTime: "招待コードの有効期限"
canManageCustomEmojis: "カスタム絵文字の管理" canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与" alwaysMarkNsfw: "ファイルにNSFWを常に付与"
@ -1412,7 +1446,7 @@ _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
sensitivity: "検出感度" sensitivity: "検出感度"
sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。" sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。"
setSensitiveFlagAutomatically: "NSFWフラグを設定する" setSensitiveFlagAutomatically: "センシティブフラグを設定する"
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。" setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
analyzeVideos: "動画の解析を有効化" analyzeVideos: "動画の解析を有効化"
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。" analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
@ -1446,6 +1480,7 @@ _ad:
back: "戻る" back: "戻る"
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
hide: "表示しない" hide: "表示しない"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
@ -1505,9 +1540,9 @@ _aboutMisskey:
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
patrons: "支援者" patrons: "支援者"
_nsfw: _displayOfSensitiveMedia:
respect: "閲覧注意のメディアは隠す" respect: "センシティブ設定されたメディアを隠す"
ignore: "閲覧注意のメディアを隠さない" ignore: "センシティブ設定されたメディアを隠さない"
force: "常にメディアを隠す" force: "常にメディアを隠す"
_instanceTicker: _instanceTicker:
@ -2018,6 +2053,7 @@ _deck:
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!" introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。" introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
_columns: _columns:
main: "メイン" main: "メイン"

View file

@ -1,7 +1,7 @@
--- ---
_lang_: "日本語 (関西弁)" _lang_: "日本語 (関西弁)"
headlineMisskey: "ノートでつながるネットワーク" headlineMisskey: "ノートでつながるネットワーク"
introMisskey: "ようお越しMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀" introMisskey: "ようお越しMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀"
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。" poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "探す" search: "探す"
@ -136,8 +136,10 @@ unblockConfirm: "ブロックやめたるってほんまか?"
suspendConfirm: "凍結してしもうてええか?" suspendConfirm: "凍結してしもうてええか?"
unsuspendConfirm: "解凍するけどええか?" unsuspendConfirm: "解凍するけどええか?"
selectList: "リストを選ぶ" selectList: "リストを選ぶ"
editList: "リスト直すで"
selectChannel: "チャンネルを選ぶ" selectChannel: "チャンネルを選ぶ"
selectAntenna: "アンテナを選ぶ" selectAntenna: "アンテナを選ぶ"
editAntenna: "アンテナを編集"
selectWidget: "ウィジェットを選ぶ" selectWidget: "ウィジェットを選ぶ"
editWidgets: "ウィジェットをいじる" editWidgets: "ウィジェットをいじる"
editWidgetsExit: "編集終ったで" editWidgetsExit: "編集終ったで"
@ -311,7 +313,6 @@ copyUrl: "URLをコピー"
rename: "名前を変えるで" rename: "名前を変えるで"
avatar: "アイコン" avatar: "アイコン"
banner: "バナー" banner: "バナー"
nsfw: "見るんは気いつけてな"
whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき" whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき"
disconnectedFromServer: "サーバーが機嫌悪いねん" disconnectedFromServer: "サーバーが機嫌悪いねん"
reload: "リロード" reload: "リロード"
@ -1062,6 +1063,15 @@ later: "あとで"
goToMisskey: "Misskeyへ" goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書" additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み" installed: "インストール済み"
branding: "あ"
enableServerMachineStats: "サーバーのマシン情報見せびらかすで"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。"
inviteCodeCreated: "招待コード作ったで"
inviteLimitExceeded: "招待コード作りすぎやで。"
createLimitRemaining: "作成できる招待コード: 残り {limit} 個やで"
unused: "つこてへん"
used: "もうつこてる"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウント作り終わったで。" accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。" letsStartAccountSetup: "アカウントの初期設定をしよか。"
@ -1485,9 +1495,9 @@ _aboutMisskey:
donate: "Misskeyに寄付" donate: "Misskeyに寄付"
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰" morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
patrons: "支援者" patrons: "支援者"
_nsfw: _displayOfSensitiveMedia:
respect: "閲覧注意のメディアは隠すで" respect: "きわどいのは見とうない"
ignore: "閲覧注意のメディアは隠さへんで" ignore: "きわどいのも見たい"
force: "常にメディアを隠すで" force: "常にメディアを隠すで"
_instanceTicker: _instanceTicker:
none: "表示せん" none: "表示せん"
@ -1635,7 +1645,7 @@ _timelineTutorial:
step3_1: "投稿できた?" step3_1: "投稿できた?"
step3_2: "あんたのノートがタイムラインに出てきたら成功や。" step3_2: "あんたのノートがタイムラインに出てきたら成功や。"
step4_1: "ノートには、「ツッコミ」を付けれるで。" step4_1: "ノートには、「ツッコミ」を付けれるで。"
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。" step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶんやで。"
_2fa: _2fa:
alreadyRegistered: "もう設定終わっとるわ。" alreadyRegistered: "もう設定終わっとるわ。"
registerTOTP: "認証アプリの設定はじめる" registerTOTP: "認証アプリの設定はじめる"

View file

@ -40,7 +40,7 @@ favorites: "즐겨찾기"
unfavorite: "즐겨찾기에서 제거" unfavorite: "즐겨찾기에서 제거"
favorited: "즐겨찾기에 등록했습니다" favorited: "즐겨찾기에 등록했습니다"
alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다" alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다"
cantFavorite: "즐겨찾기에 등록하지 못했습니다." cantFavorite: "즐겨찾기에 등록하지 못했습니다"
pin: "프로필에 고정" pin: "프로필에 고정"
unpin: "프로필에서 고정 해제" unpin: "프로필에서 고정 해제"
copyContent: "내용 복사" copyContent: "내용 복사"
@ -49,11 +49,15 @@ delete: "삭제"
deleteAndEdit: "삭제 후 편집" deleteAndEdit: "삭제 후 편집"
deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니까? 이 노트에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다." deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니까? 이 노트에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다."
addToList: "리스트에 추가" addToList: "리스트에 추가"
addToAntenna: "안테나에 추가"
sendMessage: "메시지 보내기" sendMessage: "메시지 보내기"
copyRSS: "RSS 복사" copyRSS: "RSS 복사"
copyUsername: "유저명 복사" copyUsername: "유저명 복사"
copyUserId: "유저 ID 복사" copyUserId: "유저 ID 복사"
copyNoteId: "노트 ID 복사" copyNoteId: "노트 ID 복사"
copyFileId: "파일 ID 복사"
copyFolderId: "폴더 ID 복사"
copyProfileUrl: "프로필 URL 복사"
searchUser: "사용자 검색" searchUser: "사용자 검색"
reply: "답글" reply: "답글"
loadMore: "더 보기" loadMore: "더 보기"
@ -136,8 +140,10 @@ unblockConfirm: "이 계정의 차단을 해제하시겠습니까?"
suspendConfirm: "이 계정을 정지하시겠습니까?" suspendConfirm: "이 계정을 정지하시겠습니까?"
unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?" unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?"
selectList: "리스트 선택" selectList: "리스트 선택"
editList: "리스트 편집"
selectChannel: "채널 선택" selectChannel: "채널 선택"
selectAntenna: "안테나 선택" selectAntenna: "안테나 선택"
editAntenna: "안테나 편집"
selectWidget: "위젯 선택" selectWidget: "위젯 선택"
editWidgets: "위젯 편집" editWidgets: "위젯 편집"
editWidgetsExit: "편집 종료" editWidgetsExit: "편집 종료"
@ -150,6 +156,8 @@ addEmoji: "이모지 추가"
settingGuide: "추천 설정" settingGuide: "추천 설정"
cacheRemoteFiles: "리모트 파일을 캐시" cacheRemoteFiles: "리모트 파일을 캐시"
cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다." cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다."
cacheRemoteSensitiveFiles: "리모트의 민감한 파일을 캐시"
cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다."
flagAsBot: "나는 봇입니다" flagAsBot: "나는 봇입니다"
flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다." flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다."
flagAsCat: "나는 고양이다냥" flagAsCat: "나는 고양이다냥"
@ -311,7 +319,7 @@ copyUrl: "URL 복사"
rename: "이름 변경" rename: "이름 변경"
avatar: "아바타" avatar: "아바타"
banner: "배너" banner: "배너"
nsfw: "열람주의" displayOfSensitiveMedia: "민감한 미디어 표시"
whenServerDisconnected: "서버와의 접속이 끊겼을 때" whenServerDisconnected: "서버와의 접속이 끊겼을 때"
disconnectedFromServer: "서버와의 연결이 끊어졌습니다" disconnectedFromServer: "서버와의 연결이 끊어졌습니다"
reload: "새로고침" reload: "새로고침"
@ -992,8 +1000,8 @@ cannotBeChangedLater: "나중에 변경할 수 없습니다."
reactionAcceptance: "리액션 수신" reactionAcceptance: "리액션 수신"
likeOnly: "좋아요만 받기" likeOnly: "좋아요만 받기"
likeOnlyForRemote: "리모트에서는 좋아요만 받기" likeOnlyForRemote: "리모트에서는 좋아요만 받기"
nonSensitiveOnly: "열람 주의로 설정되지 않았을 때만 받기" nonSensitiveOnly: "민감한 이모지를 제외하고 받기"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "열람 주의로 설정되지 않았을 때만 받기 (리모트에서는 좋아요만 받기)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 받기 (리모트에서는 좋아요만 받기)"
rolesAssignedToMe: "나에게 할당된 역할" rolesAssignedToMe: "나에게 할당된 역할"
resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?" resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
sensitiveWords: "민감한 단어" sensitiveWords: "민감한 단어"
@ -1062,6 +1070,27 @@ later: "나중에"
goToMisskey: "Misskey로" goToMisskey: "Misskey로"
additionalEmojiDictionary: "이모지 추가 사전" additionalEmojiDictionary: "이모지 추가 사전"
installed: "설치됨" installed: "설치됨"
branding: "브랜딩"
enableServerMachineStats: "서버의 머신 사양을 공개하기"
enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화"
turnOffToImprovePerformance: "이 기능을 끄면 성능이 향상될 수 있습니다."
createInviteCode: "초대 코드 생성"
createWithOptions: "옵션을 지정하여 생성"
createCount: "초대 수"
inviteCodeCreated: "초대 코드 생성됨"
inviteLimitExceeded: "초대 코드 생성 한도를 초과했습니다."
createLimitRemaining: "초대 한도: {limit}회 남음"
inviteLimitResetCycle: " {time}시간 이내에 최대 {limit}개의 초대 코드를 생성할 수 있습니다."
expirationDate: "만료 날짜"
noExpirationDate: "만료기간 없음"
inviteCodeUsedAt: "다음에 사용된 초대 코드"
registeredUserUsingInviteCode: "초대 코드 사용 대상"
waitingForMailAuth: "이메일 인증 보류 중"
inviteCodeCreator: "초대 코드 생성자"
usedAt: "사용 시각"
unused: "사용되지 않음"
used: "사용됨"
expired: "만료됨"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "계정 생성이 완료되었습니다!" accountCreated: "계정 생성이 완료되었습니다!"
letsStartAccountSetup: "계정의 초기 설정을 진행합니다." letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
@ -1372,6 +1401,9 @@ _role:
ltlAvailable: "로컬 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용" canPublicNote: "공개 노트 허용"
canInvite: "서버 초대 코드 발행" canInvite: "서버 초대 코드 발행"
inviteLimit: "초대 한도"
inviteLimitCycle: "초대 발급 간격"
inviteExpirationTime: "초대 만료 기간"
canManageCustomEmojis: "커스텀 이모지 관리" canManageCustomEmojis: "커스텀 이모지 관리"
driveCapacity: "드라이브 용량" driveCapacity: "드라이브 용량"
alwaysMarkNsfw: "파일을 항상 NSFW로 지정" alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
@ -1434,6 +1466,7 @@ _ad:
back: "뒤로" back: "뒤로"
reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기"
hide: "보이지 않음" hide: "보이지 않음"
timezoneinfo: "요일은 서버의 표준 시간대에 따라 결정됩니다."
_forgotPassword: _forgotPassword:
enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다." enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다."
ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오." ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오."
@ -1485,9 +1518,9 @@ _aboutMisskey:
donate: "Misskey에 기부하기" donate: "Misskey에 기부하기"
morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰" morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
patrons: "후원자" patrons: "후원자"
_nsfw: _displayOfSensitiveMedia:
respect: "열람주의로 설정된 미디어 숨기기" respect: "민감한 콘텐츠로 표시된 미디어 숨기기"
ignore: "열람 주의 미디어 항상 표시" ignore: "민감한 콘텐츠로 표시된 미디어 보이기"
force: "미디어 항상 숨기기" force: "미디어 항상 숨기기"
_instanceTicker: _instanceTicker:
none: "보이지 않음" none: "보이지 않음"
@ -1963,6 +1996,7 @@ _deck:
introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!" introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!"
introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다." introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다."
widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요" widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요"
useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기"
_columns: _columns:
main: "메인" main: "메인"
widgets: "위젯" widgets: "위젯"

View file

@ -20,6 +20,7 @@ noNotes: "ບໍ່ມີຫມາຍເຫດ"
noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ" noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
instance: "ອີນສະແຕນ" instance: "ອີນສະແຕນ"
settings: "ກຳນົດຄ່າ" settings: "ກຳນົດຄ່າ"
notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ"
basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ" basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
otherSettings: "ການຕັ້ງຄ່າອື່ນໆ" otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ" openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ"
@ -48,9 +49,15 @@ delete: "ລຶບ"
deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​" deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​"
deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ" deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ"
addToList: "ເພີ່ມໃສ່ລາຍຊື່" addToList: "ເພີ່ມໃສ່ລາຍຊື່"
addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ"
sendMessage: "ສົ່ງຂໍ້ຄວາມ" sendMessage: "ສົ່ງຂໍ້ຄວາມ"
copyRSS: "ສຳເນົາ RSS" copyRSS: "ສຳເນົາ RSS"
copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້" copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້"
copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້"
copyNoteId: "ສຳເນົາ ID ບັນທຶກ"
copyFileId: "ສຳເນົາ ID ໄຟລ໌"
copyFolderId: "ສຳເນົາ ID ໂຟນເດີ"
copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌"
searchUser: "ຄົ້ນຫາຜູ້ໃຊ້" searchUser: "ຄົ້ນຫາຜູ້ໃຊ້"
reply: "ຕອບ​ໄປ​ທີ" reply: "ຕອບ​ໄປ​ທີ"
loadMore: "ໂຫຼດເພີ່ມເຕີມ" loadMore: "ໂຫຼດເພີ່ມເຕີມ"
@ -109,6 +116,7 @@ sensitive: "NSFW"
add: "ເພີ່ມ" add: "ເພີ່ມ"
reaction: "ປະຕິກິລິຍາ" reaction: "ປະຕິກິລິຍາ"
reactions: "ປະຕິກິລິຍາ" reactions: "ປະຕິກິລິຍາ"
attachCancel: "ເອົາໄຟລ໌ແນບ"
mute: "ປີດສຽງ" mute: "ປີດສຽງ"
unmute: "ເປີດສຽງ" unmute: "ເປີດສຽງ"
block: "ບ໋ອກ" block: "ບ໋ອກ"
@ -116,6 +124,10 @@ unblock: "ຍົກເລີກກາຮົບລັອກ"
suspend: "ລະງັບ" suspend: "ລະງັບ"
unsuspend: "ເຊົາ​ລະ​ງັບ" unsuspend: "ເຊົາ​ລະ​ງັບ"
selectList: "ເລືອກບັນຊີລາຍການ" selectList: "ເລືອກບັນຊີລາຍການ"
editList: "ແກ້ໄຂລາຍຊື່"
selectChannel: "ເລືອກຊ່ອງ"
selectAntenna: "ເລືອກເສົາອາກາດ"
editAntenna: "ແກ້ໄຂເສົາອາກາດ"
selectWidget: "ເລືອກວິກເຈັດ" selectWidget: "ເລືອກວິກເຈັດ"
editWidgets: "ແກ້ໄຂ Widget" editWidgets: "ແກ້ໄຂ Widget"
editWidgetsExit: "ສຳເລັດແລ້ວ" editWidgetsExit: "ສຳເລັດແລ້ວ"
@ -125,6 +137,7 @@ emojis: "ອີໂມຈິ"
emojiName: "ຊື່ Emoji" emojiName: "ຊື່ Emoji"
emojiUrl: "URL ອີໂມຈິ" emojiUrl: "URL ອີໂມຈິ"
addEmoji: "ຕື່ມອີໂມຈິ" addEmoji: "ຕື່ມອີໂມຈິ"
settingGuide: "ການຕັ້ງຄ່າທີ່ແນະນໍາ"
flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ" flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ"
flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ" flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ"
flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ" flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ"
@ -133,10 +146,13 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ
autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່" autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່"
addAccount: "ເພີ່ມບັນຊີ" addAccount: "ເພີ່ມບັນຊີ"
loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ" loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ"
showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລຍະໄກ"
general: "ທົ່ວໄປ" general: "ທົ່ວໄປ"
wallpaper: "ພາບພື້ນຫລັງ" wallpaper: "ພາບພື້ນຫລັງ"
setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ"
removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ"
searchWith: "ຊອກຫາ: {q}" searchWith: "ຊອກຫາ: {q}"
youHaveNoLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
proxyAccount: "ບັນຊີພຣັອກຊີ" proxyAccount: "ບັນຊີພຣັອກຊີ"
host: "ໂຮດສ" host: "ໂຮດສ"
selectUser: "ເລືອກຜູ້ໃຊ້" selectUser: "ເລືອກຜູ້ໃຊ້"
@ -155,7 +171,9 @@ operations: "ການດຳເນີນງານ"
software: "ຊອບແວ" software: "ຊອບແວ"
version: "ສະບັບ" version: "ສະບັບ"
metadata: "Metadata" metadata: "Metadata"
withNFiles: "{n} ໄຟລ໌(s)"
monitor: "ຈໍພາບ" monitor: "ຈໍພາບ"
jobQueue: "ຄິວວຽກ"
cpuAndMemory: "CPU ແລະ ຫນ່ວຍຄວາມຈໍາ" cpuAndMemory: "CPU ແລະ ຫນ່ວຍຄວາມຈໍາ"
network: "ເຄືອຂ່າຍ" network: "ເຄືອຂ່າຍ"
disk: "ດິສກ໌" disk: "ດິສກ໌"
@ -248,7 +266,6 @@ inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່"
inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່" inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່"
circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ" circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ"
rename: "ປ່ຽນຊື່" rename: "ປ່ຽນຊື່"
nsfw: "NSFW"
watch: "ເບິ່ງ" watch: "ເບິ່ງ"
unwatch: "ຢຸດເບິ່ງ" unwatch: "ຢຸດເບິ່ງ"
accept: "ອະນຸຍາດ" accept: "ອະນຸຍາດ"
@ -344,6 +361,7 @@ _widgets:
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
activity: "ກິດຈະກຳ" activity: "ກິດຈະກຳ"
federation: "ສະຫະພັນ" federation: "ສະຫະພັນ"
jobQueue: "ຄິວວຽກ"
_userList: _userList:
chooseList: "ເລືອກບັນຊີລາຍການ" chooseList: "ເລືອກບັນຊີລາຍການ"
_cw: _cw:

View file

@ -20,6 +20,7 @@ noNotes: "Geen notities"
noNotifications: "Geen meldingen" noNotifications: "Geen meldingen"
instance: "Server" instance: "Server"
settings: "Instellingen" settings: "Instellingen"
notificationSettings: "Notificatie instellingen"
basicSettings: "Basisinstellingen" basicSettings: "Basisinstellingen"
otherSettings: "Overige instellingen" otherSettings: "Overige instellingen"
openInWindow: "In een venster openen" openInWindow: "In een venster openen"
@ -48,8 +49,15 @@ delete: "Verwijderen"
deleteAndEdit: "Verwijderen en bewerken" deleteAndEdit: "Verwijderen en bewerken"
deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop." deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop."
addToList: "Aan lijst toevoegen" addToList: "Aan lijst toevoegen"
addToAntenna: "Voeg toe aan antenne"
sendMessage: "Verstuur bericht" sendMessage: "Verstuur bericht"
copyRSS: "Kopieer RSS"
copyUsername: "Kopiëren gebruikersnaam " copyUsername: "Kopiëren gebruikersnaam "
copyUserId: "Kopieer gebruiker ID"
copyNoteId: "Kopieer notitie ID"
copyFileId: "Kopieer veld ID"
copyFolderId: "Kopieer folder ID"
copyProfileUrl: "Kopieer profiel URL"
searchUser: "Zoeken een gebruiker" searchUser: "Zoeken een gebruiker"
reply: "Antwoord" reply: "Antwoord"
loadMore: "Laad meer" loadMore: "Laad meer"
@ -296,7 +304,6 @@ copyUrl: "URL kopiëren"
rename: "Hernoemen" rename: "Hernoemen"
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
nsfw: "NSFW"
whenServerDisconnected: "Wanneer de verbinding met de server wordt onderbroken" whenServerDisconnected: "Wanneer de verbinding met de server wordt onderbroken"
disconnectedFromServer: "Verbinding met de server onderbroken." disconnectedFromServer: "Verbinding met de server onderbroken."
reload: "Verversen" reload: "Verversen"

3
locales/package.json Normal file
View file

@ -0,0 +1,3 @@
{
"type": "module"
}

View file

@ -299,7 +299,6 @@ copyUrl: "Skopiuj adres URL"
rename: "Zmień nazwę" rename: "Zmień nazwę"
avatar: "Awatar" avatar: "Awatar"
banner: "Baner" banner: "Baner"
nsfw: "NSFW"
whenServerDisconnected: "Po utracie połączenia z serwerem" whenServerDisconnected: "Po utracie połączenia z serwerem"
disconnectedFromServer: "Utracono połączenie z serwerem." disconnectedFromServer: "Utracono połączenie z serwerem."
reload: "Odśwież" reload: "Odśwież"
@ -955,10 +954,6 @@ _aboutMisskey:
donate: "Przekaż darowiznę na Misskey" donate: "Przekaż darowiznę na Misskey"
morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób. Dziękuję! 🥰" morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób. Dziękuję! 🥰"
patrons: "Wspierający" patrons: "Wspierający"
_nsfw:
respect: "Ukrywaj media NSFW"
ignore: "Nie ukrywaj mediów NSFW"
force: "Ukrywaj wszystkie media"
_instanceTicker: _instanceTicker:
none: "Nigdy nie pokazuj" none: "Nigdy nie pokazuj"
remote: "Pokaż dla zdalnych użytkowników" remote: "Pokaż dla zdalnych użytkowników"

View file

@ -294,7 +294,6 @@ copyUrl: "Copiar URL"
rename: "Renomear" rename: "Renomear"
avatar: "Avatar" avatar: "Avatar"
banner: "Capa" banner: "Capa"
nsfw: "Conteúdo sensível"
whenServerDisconnected: "Quando a conexão com o servidor é perdida" whenServerDisconnected: "Quando a conexão com o servidor é perdida"
disconnectedFromServer: "Desconectado do servidor" disconnectedFromServer: "Desconectado do servidor"
reload: "Recarregar" reload: "Recarregar"

View file

@ -294,7 +294,6 @@ copyUrl: "Copiază URL"
rename: "Redenumește" rename: "Redenumește"
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
nsfw: "NSFW"
whenServerDisconnected: "Când pierzi conexiunea cu serverul" whenServerDisconnected: "Când pierzi conexiunea cu serverul"
disconnectedFromServer: "Conecțiunea cu serverul a fost pierdută" disconnectedFromServer: "Conecțiunea cu serverul a fost pierdută"
reload: "Reîncarcă" reload: "Reîncarcă"

View file

@ -52,6 +52,11 @@ addToList: "Добавить в список"
sendMessage: "Отправить сообщение" sendMessage: "Отправить сообщение"
copyRSS: "Скопировать RSS" copyRSS: "Скопировать RSS"
copyUsername: "Скопировать имя пользователя" copyUsername: "Скопировать имя пользователя"
copyUserId: "Скопировать ID пользователя"
copyNoteId: "Скопировать ID заметки"
copyFileId: "Скопировать ID файла"
copyFolderId: "Скопировать ID папки"
copyProfileUrl: "Скопировать URL профиля "
searchUser: "Поиск людей" searchUser: "Поиск людей"
reply: "Ответить" reply: "Ответить"
loadMore: "Показать еще" loadMore: "Показать еще"
@ -134,8 +139,10 @@ unblockConfirm: "Разблокировать этот аккаунт?"
suspendConfirm: "Заморозить этот аккаунт?" suspendConfirm: "Заморозить этот аккаунт?"
unsuspendConfirm: "Разморозить этот аккаунт?" unsuspendConfirm: "Разморозить этот аккаунт?"
selectList: "Выберите список" selectList: "Выберите список"
editList: "Редактировать список"
selectChannel: "Выберите канал" selectChannel: "Выберите канал"
selectAntenna: "Выберите антенну" selectAntenna: "Выберите антенну"
editAntenna: "Редактировать антенну"
selectWidget: "Выберите виджет" selectWidget: "Выберите виджет"
editWidgets: "Редактировать виджеты" editWidgets: "Редактировать виджеты"
editWidgetsExit: "Готово" editWidgetsExit: "Готово"
@ -309,7 +316,6 @@ copyUrl: "Копировать ссылку"
rename: "Переименовать" rename: "Переименовать"
avatar: "Аватар" avatar: "Аватар"
banner: "Шапка" banner: "Шапка"
nsfw: "Содержимое не для всех"
whenServerDisconnected: "Когда соединение с сервером потеряно" whenServerDisconnected: "Когда соединение с сервером потеряно"
disconnectedFromServer: "Разорвано соединение с сервером" disconnectedFromServer: "Разорвано соединение с сервером"
reload: "Перезагрузить" reload: "Перезагрузить"
@ -790,6 +796,7 @@ noMaintainerInformationWarning: "Не заполнены сведения об
noBotProtectionWarning: "Ботозащита не настроена" noBotProtectionWarning: "Ботозащита не настроена"
configure: "Настроить" configure: "Настроить"
postToGallery: "Опубликовать в галерею" postToGallery: "Опубликовать в галерею"
postToHashtag: "Опубликовать пост с этим хештегом"
gallery: "Галерея" gallery: "Галерея"
recentPosts: "Недавние публикации" recentPosts: "Недавние публикации"
popularPosts: "Популярные публикации" popularPosts: "Популярные публикации"
@ -1395,10 +1402,6 @@ _aboutMisskey:
donate: "Пожертвование на Misskey" donate: "Пожертвование на Misskey"
morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте! 🥰" morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте! 🥰"
patrons: "Материальная поддержка" patrons: "Материальная поддержка"
_nsfw:
respect: "Скрывать содержимое не для всех"
ignore: "Показывать содержимое не для всех"
force: "Скрывать вообще все файлы"
_instanceTicker: _instanceTicker:
none: "Не показывать" none: "Не показывать"
remote: "Только для других сайтов" remote: "Только для других сайтов"

View file

@ -303,7 +303,6 @@ copyUrl: "Kopírovať URL"
rename: "Premenovať" rename: "Premenovať"
avatar: "Avatar" avatar: "Avatar"
banner: "BAnner" banner: "BAnner"
nsfw: "NSFW"
whenServerDisconnected: "Keď sa stratí spojenie so serverom" whenServerDisconnected: "Keď sa stratí spojenie so serverom"
disconnectedFromServer: "Spojenie so serverom bolo prerušené" disconnectedFromServer: "Spojenie so serverom bolo prerušené"
reload: "Obnoviť" reload: "Obnoviť"
@ -1009,10 +1008,6 @@ _aboutMisskey:
donate: "Podporiť Misskey" donate: "Podporiť Misskey"
morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení. Ďakujeme! 🥰" morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení. Ďakujeme! 🥰"
patrons: "Prispievatelia" patrons: "Prispievatelia"
_nsfw:
respect: "Skryť NSFW médiá"
ignore: "Neskrývať NSFW médiá"
force: "Skryť všetky médiá"
_instanceTicker: _instanceTicker:
none: "Nikdy nezobrazovať" none: "Nikdy nezobrazovať"
remote: "Zobraziť pre vzdialených používateľov" remote: "Zobraziť pre vzdialených používateľov"

View file

@ -20,6 +20,7 @@ noNotes: "Inga noteringar"
noNotifications: "Inga notifikationer" noNotifications: "Inga notifikationer"
instance: "Instanser" instance: "Instanser"
settings: "Inställningar" settings: "Inställningar"
notificationSettings: "Notifieringsinställningar"
basicSettings: "Basinställningar" basicSettings: "Basinställningar"
otherSettings: "Andra inställningar" otherSettings: "Andra inställningar"
openInWindow: "Öppna i ett fönster" openInWindow: "Öppna i ett fönster"
@ -51,6 +52,10 @@ addToList: "Lägg till i lista"
sendMessage: "Skicka ett meddelande" sendMessage: "Skicka ett meddelande"
copyRSS: "Kopiera RSS" copyRSS: "Kopiera RSS"
copyUsername: "Kopiera användarnamn" copyUsername: "Kopiera användarnamn"
copyUserId: "Kopiera användar-ID"
copyNoteId: "Kopiera noter-ID"
copyFileId: "Kopiera Fil-ID"
copyFolderId: "Kopiera mapp-ID"
searchUser: "Sök användare" searchUser: "Sök användare"
reply: "Svara" reply: "Svara"
loadMore: "Ladda mer" loadMore: "Ladda mer"
@ -103,6 +108,8 @@ renoted: "Omnoterad."
cantRenote: "Inlägget kunde inte bli omnoterat." cantRenote: "Inlägget kunde inte bli omnoterat."
cantReRenote: "En omnotering kan inte bli omnoterad." cantReRenote: "En omnotering kan inte bli omnoterad."
quote: "Citat" quote: "Citat"
inChannelRenote: "Omnotera inom kanalen"
inChannelQuote: "I kanal citat"
pinnedNote: "Fästad not" pinnedNote: "Fästad not"
pinned: "Fäst till profil" pinned: "Fäst till profil"
you: "Du" you: "Du"
@ -129,7 +136,10 @@ unblockConfirm: "Är du säkert att du vill avblockera kontot?"
suspendConfirm: "Är du säker att du vill suspendera detta konto?" suspendConfirm: "Är du säker att du vill suspendera detta konto?"
unsuspendConfirm: "Är du säker att du vill avsuspendera detta konto?" unsuspendConfirm: "Är du säker att du vill avsuspendera detta konto?"
selectList: "Välj lista" selectList: "Välj lista"
editList: "Redigera lista"
selectChannel: "Välj en kanal"
selectAntenna: "Välj en antenn" selectAntenna: "Välj en antenn"
editAntenna: "Redigera en antenn"
selectWidget: "Välj en widget" selectWidget: "Välj en widget"
editWidgets: "Redigera widgets" editWidgets: "Redigera widgets"
editWidgetsExit: "Avsluta redigering" editWidgetsExit: "Avsluta redigering"
@ -256,6 +266,9 @@ noMoreHistory: "Det finns ingen mer historik"
startMessaging: "Starta en chatt" startMessaging: "Starta en chatt"
nUsersRead: "läst av {n}" nUsersRead: "läst av {n}"
agreeTo: "Jag accepterar {0}" agreeTo: "Jag accepterar {0}"
agree: "Överens"
termsOfService: "Användarvillkor"
start: "Kom igång"
home: "Hem" home: "Hem"
remoteUserCaution: "Då denna användaren kommer från en fjärrinstans, kan informationen visad vara ofullständig." remoteUserCaution: "Då denna användaren kommer från en fjärrinstans, kan informationen visad vara ofullständig."
activity: "Aktivitet" activity: "Aktivitet"
@ -297,10 +310,10 @@ copyUrl: "Kopiera URL"
rename: "Byt namn" rename: "Byt namn"
avatar: "Profilbild" avatar: "Profilbild"
banner: "Banner" banner: "Banner"
nsfw: "Känsligt innehåll"
reload: "Ladda om" reload: "Ladda om"
doNothing: "Ignorera" doNothing: "Ignorera"
reloadConfirm: "Vill du ladda om tidslinjen?" reloadConfirm: "Vill du ladda om tidslinjen?"
watch: "Titta"
accept: "Tillåt" accept: "Tillåt"
reject: "Neka" reject: "Neka"
normal: "Normal" normal: "Normal"
@ -320,16 +333,31 @@ connectService: "Anslut"
disconnectService: "Koppla från" disconnectService: "Koppla från"
enableLocalTimeline: "Aktivera lokal tidslinje" enableLocalTimeline: "Aktivera lokal tidslinje"
enableGlobalTimeline: "Aktivera global tidslinje" enableGlobalTimeline: "Aktivera global tidslinje"
registration: "Registrera"
enableRegistration: "Aktivera registrering av nya användare" enableRegistration: "Aktivera registrering av nya användare"
invite: "Inbjudan"
inMb: "I megabyte" inMb: "I megabyte"
iconUrl: "URL till profilbilden" iconUrl: "URL till profilbilden"
bannerUrl: "URL till banner-bilden" bannerUrl: "URL till banner-bilden"
basicInfo: "Grundläggande info"
pinnedUsers: "Fästa användare"
pinnedPages: "Fästa sidor"
pinnedNotes: "Fästad not" pinnedNotes: "Fästad not"
hcaptcha: "hCaptcha"
enableHcaptcha: "Aktivera hCaptcha" enableHcaptcha: "Aktivera hCaptcha"
hcaptchaSiteKey: "Webbplatsnyckel"
hcaptchaSecretKey: "Hemlig nyckel"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Aktivera reCAPTCHA" enableRecaptcha: "Aktivera reCAPTCHA"
recaptchaSiteKey: "Webbplatsnyckel"
recaptchaSecretKey: "Hemlig nyckel"
turnstile: "Turnstile"
enableTurnstile: "Aktivera Turnstile" enableTurnstile: "Aktivera Turnstile"
turnstileSiteKey: "Webbplatsnyckel"
turnstileSecretKey: "Hemlig nyckel"
antennas: "Antenner" antennas: "Antenner"
manageAntennas: "Hantera Antenner" manageAntennas: "Hantera Antenner"
name: "Namn"
antennaSource: "Antennkälla" antennaSource: "Antennkälla"
antennaKeywords: "Nyckelord att lyssna efter" antennaKeywords: "Nyckelord att lyssna efter"
antennaExcludeKeywords: "Nyckelord att exkludera" antennaExcludeKeywords: "Nyckelord att exkludera"
@ -338,39 +366,112 @@ notifyAntenna: "Notifiera om nya noter"
withFileAntenna: "Endast noter med filer" withFileAntenna: "Endast noter med filer"
enableServiceworker: "Aktivera pushnotiser i denna webbläsaren" enableServiceworker: "Aktivera pushnotiser i denna webbläsaren"
antennaUsersDescription: "Ange ett användarnamn per linje" antennaUsersDescription: "Ange ett användarnamn per linje"
withReplies: "Med svar"
notesAndReplies: "Inlägg och svar"
silence: "Tystnad"
recentlyUpdatedUsers: "Nyligen aktiva användare" recentlyUpdatedUsers: "Nyligen aktiva användare"
recentlyRegisteredUsers: "Nyligen registrerade användare" recentlyRegisteredUsers: "Nyligen registrerade användare"
exploreFediverse: "Utforska Fediverse"
popularTags: "Populära taggar"
userList: "Listor" userList: "Listor"
about: "Om"
aboutMisskey: "Om Misskey" aboutMisskey: "Om Misskey"
administrator: "Administratör" administrator: "Administratör"
2fa: "Tvåfaktorsautentisering"
totp: "Autentiseringsapp"
moderator: "Moderator"
passwordLessLogin: "Lösenordsfri inloggning" passwordLessLogin: "Lösenordsfri inloggning"
passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey." passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey."
resetPassword: "Återställ Lösenord" resetPassword: "Återställ Lösenord"
newPasswordIs: "Det nya lösenordet är \"{password}\"" newPasswordIs: "Det nya lösenordet är \"{password}\""
share: "Dela" share: "Dela"
help: "Hjälp"
close: "Stäng"
invites: "Inbjudan"
members: "Medlemmar"
transfer: "Överför"
text: "Text"
enable: "Aktivera" enable: "Aktivera"
next: "Nästa"
invitations: "Inbjudan"
invitationCode: "Inbjudningskod"
available: "Tillgängligt"
weakPassword: "Svagt Lösenord" weakPassword: "Svagt Lösenord"
normalPassword: "Medel Lösenord" normalPassword: "Medel Lösenord"
strongPassword: "Starkt Lösenord" strongPassword: "Starkt Lösenord"
signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt." signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt."
or: "eller"
language: "Språk"
aboutX: "Om {x}"
category: "Kategori"
tags: "Taggar"
createAccount: "Skapa ett konto"
existingAccount: "Existerande konto"
regenerate: "Regenerera"
fontSize: "Textstorlek"
openImageInNewTab: "Öppna bild i ny flik"
clientSettings: "Klientinställningar"
accountSettings: "Kontoinställningar"
numberOfDays: "Antal dagar"
deleteAll: "Radera alla"
sounds: "Ljud"
sound: "Ljud"
listen: "Lyssna"
none: "Ingen"
volume: "Volym"
chooseEmoji: "Välj en emoji"
recentUsed: "Senast använd"
install: "Installera"
uninstall: "Avinstallera"
menu: "Meny"
serviceworkerInfo: "Måste vara aktiverad för pushnotiser." serviceworkerInfo: "Måste vara aktiverad för pushnotiser."
enableInfiniteScroll: "Ladda mer automatiskt" enableInfiniteScroll: "Ladda mer automatiskt"
enablePlayer: "Öppna videospelare" enablePlayer: "Öppna videospelare"
permission: "Behörigheter"
enableAll: "Aktivera alla" enableAll: "Aktivera alla"
edit: "Ändra"
enableEmail: "Aktivera epost-utskick" enableEmail: "Aktivera epost-utskick"
email: "E-post"
smtpHost: "Värd" smtpHost: "Värd"
smtpUser: "Användarnamn" smtpUser: "Användarnamn"
smtpPass: "Lösenord" smtpPass: "Lösenord"
emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering" emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering"
logs: "Logg"
channel: "kanal"
create: "Skapa"
other: "Mer"
send: "Skicka"
openInNewTab: "Öppna i ny flik"
createNew: "Skapa ny"
i18nInfo: "Misskey översätts till många olika språk av volontärer. Du kan hjälpa till med översättningen på {link}."
accountInfo: "Kontoinformation"
clips: "Klipp"
duplicate: "Duplicera"
reloadToApplySetting: "Inställningen tillämpas efter sidan laddas om. Vill du göra det nu?"
clearCache: "Rensa cache" clearCache: "Rensa cache"
onlineUsersCount: "{n} användare är online" onlineUsersCount: "{n} användare är online"
nNotes: "{n} Noter"
backgroundColor: "Bakgrundsbild"
textColor: "Text"
youAreRunningUpToDateClient: "Klienten du använder är uppdaterat."
newVersionOfClientAvailable: "Ny version av klienten är tillgänglig."
publish: "Publicera"
typingUsers: "{users} skriver"
info: "Om"
enabled: "Aktiverad" enabled: "Aktiverad"
user: "Användare" user: "Användare"
customCssWarn: "Den här inställningen borde bara ändrats av en som har rätta kunskaper. Om du ställer in det här fel så kan klienten sluta fungera rätt."
global: "Global" global: "Global"
squareAvatars: "Visa fyrkantiga profilbilder" squareAvatars: "Visa fyrkantiga profilbilder"
sent: "Skicka"
misskeyUpdated: "Misskey har uppdaterats!"
incorrectPassword: "Fel lösenord." incorrectPassword: "Fel lösenord."
welcomeBackWithName: "Välkommen tillbaka, {name}"
clickToFinishEmailVerification: "Tryck på [{ok}] för att slutföra bekräftelsen på e-postadressen."
searchByGoogle: "Sök" searchByGoogle: "Sök"
file: "Filer" file: "Filer"
cannotUploadBecauseNoFreeSpace: "Kan inte ladda upp filen för att det finns inget lagringsutrymme kvar."
cannotUploadBecauseExceedsFileSizeLimit: "Kan inte ladda upp filen för att den är större än filstorleksgränsen."
enableAutoSensitive: "Automatisk NSFW markering" enableAutoSensitive: "Automatisk NSFW markering"
enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen." enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen."
pushNotification: "Pushnotiser" pushNotification: "Pushnotiser"
@ -381,12 +482,16 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för
windowMaximize: "Maximera" windowMaximize: "Maximera"
windowMinimize: "Minimera" windowMinimize: "Minimera"
windowRestore: "Återställ" windowRestore: "Återställ"
pleaseDonate: "Misskey är en gratis programvara som används på {host}. Donera gärna för att göra utvecklingen ständigt, tack!"
resetPasswordConfirm: "Återställ verkligen ditt lösenord?" resetPasswordConfirm: "Återställ verkligen ditt lösenord?"
dataSaver: "Databesparing"
_achievements: _achievements:
_types: _types:
_open3windows: _open3windows:
title: "Flera Fönster" title: "Flera Fönster"
description: "Ha minst 3 fönster öppna samtidigt" description: "Ha minst 3 fönster öppna samtidigt"
_ffVisibility:
public: "Publicera"
_email: _email:
_follow: _follow:
title: "följde dig" title: "följde dig"
@ -426,6 +531,7 @@ _visibility:
home: "Hem" home: "Hem"
followers: "Följare" followers: "Följare"
_profile: _profile:
name: "Namn"
username: "Användarnamn" username: "Användarnamn"
changeAvatar: "Ändra profilbild" changeAvatar: "Ändra profilbild"
changeBanner: "Ändra banner" changeBanner: "Ändra banner"
@ -461,6 +567,8 @@ _deck:
tl: "Tidslinje" tl: "Tidslinje"
antenna: "Antenner" antenna: "Antenner"
list: "Listor" list: "Listor"
channel: "kanal"
mentions: "Omnämningar" mentions: "Omnämningar"
_webhookSettings: _webhookSettings:
name: "Namn"
active: "Aktiverad" active: "Aktiverad"

View file

@ -1,9 +1,9 @@
--- ---
_lang_: "ภาษาไทย" _lang_: "ภาษาไทย"
headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต" headlineMisskey: "เชื่อมต่อระบบ Network ด้วย Note"
introMisskey: "ยินดีต้อนรับจ้าาา! Misskey เป็นบริการไมโครบล็อกโอเพ่นซอร์ส แบบการกระจายอำนาจ\nสร้าง \"โน้ต\" เพื่อแบ่งปันความคิดของคุณกับทุกคนรอบตัวคุณกันเถอะ 📡\nด้วยการ \"รีแอคชั่นผู้คน\" คุณยังสามารถแสดงความรู้สึกของคุณเกี่ยวกับบันทึกของทุกคนได้อย่างรวดเร็ว 👍\n\nแล้วมาท่องสำรวจโลกใบใหม่กันเถอะ! 🚀" introMisskey: "ยินดีต้อนรับทุกคนจ้า! Misskey คือ บริการไมโครบล็อกกิ้ง (MicroBlogging) แบบกระจายศูนย์อำนาจ (Decentralized) \n\nเขียน \"โน้ต (Note)\" เพื่อส่งต่อเรื่องราวของคุณให้ทั้งโลกได้รับรู้📡\nและอย่าลืมที่จะ \"React\" กับเรื่องราวของคนอื่น ๆ ด้วย! 👍\n\nมุ่งสู่โลกใบใหม่กันเถอะ🚀"
poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส <b>Misskey</b> (เรียกว่า \"อินสแตนซ์ Misskey\")" poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส <b>Misskey</b> (เรียกว่า \"อินสแตนซ์ Misskey\")"
monthAndDay: "{เดือน}/{วัน}" monthAndDay: "{month}/{day}"
search: "ค้นหา" search: "ค้นหา"
notifications: "การเเจ้งเตือน" notifications: "การเเจ้งเตือน"
username: "ชื่อผู้ใช้" username: "ชื่อผู้ใช้"
@ -15,7 +15,7 @@ gotIt: "เข้าใจแล้ว !"
cancel: "ยกเลิก" cancel: "ยกเลิก"
noThankYou: "ไม่เป็นไร" noThankYou: "ไม่เป็นไร"
enterUsername: "ใส่ชื่อผู้ใช้" enterUsername: "ใส่ชื่อผู้ใช้"
renotedBy: "รีโน้ตโดย {ผู้ใช้}" renotedBy: "รีโน้ตโดย {user}"
noNotes: "ไม่มีโน้ต" noNotes: "ไม่มีโน้ต"
noNotifications: "ไม่มีการแจ้งเตือน" noNotifications: "ไม่มีการแจ้งเตือน"
instance: "อินสแตนซ์" instance: "อินสแตนซ์"
@ -49,9 +49,15 @@ delete: "ลบ"
deleteAndEdit: "ลบและแก้ไข" deleteAndEdit: "ลบและแก้ไข"
deleteAndEditConfirm: "นายแน่ใจแล้วเหรอ? ว่าต้องการลบโน้ตนี้และแก้ไข คุณอาจจะสูญเสียการโต้ตอบ, โน้ต, และการตอบกลับทั้งหมดได้นะ" deleteAndEditConfirm: "นายแน่ใจแล้วเหรอ? ว่าต้องการลบโน้ตนี้และแก้ไข คุณอาจจะสูญเสียการโต้ตอบ, โน้ต, และการตอบกลับทั้งหมดได้นะ"
addToList: "เพิ่มในลิสต์" addToList: "เพิ่มในลิสต์"
addToAntenna: "เพิ่มไปยังเสาอากาศ"
sendMessage: "ส่งข้อความ" sendMessage: "ส่งข้อความ"
copyRSS: "คัดลอก RSS" copyRSS: "คัดลอก RSS"
copyUsername: "คัดลอกชื่อผู้ใช้" copyUsername: "คัดลอกชื่อผู้ใช้"
copyUserId: "คัดลอก ID ผู้ใช้"
copyNoteId: "คัดลอก ID โน้ต "
copyFileId: "คัดลอกไฟล์ ID"
copyFolderId: "คัดลอกโฟลเดอร์ ID"
copyProfileUrl: "คัดลอกโปรไฟล์ URL"
searchUser: "ค้นหาผู้ใช้งาน" searchUser: "ค้นหาผู้ใช้งาน"
reply: "ตอบกลับ" reply: "ตอบกลับ"
loadMore: "โหลดเพิ่มเติม" loadMore: "โหลดเพิ่มเติม"
@ -73,8 +79,8 @@ unfollowConfirm: "นายแน่ใจแล้วหรอว่าต้
exportRequested: "เมื่อคุณได้ร้องขอการส่งออก อาจจะต้องใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว" exportRequested: "เมื่อคุณได้ร้องขอการส่งออก อาจจะต้องใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว"
importRequested: "เมื่อคุณได้ร้องขอการนำเข้า อาจจะต้องใช้เวลาสักครู่นะ" importRequested: "เมื่อคุณได้ร้องขอการนำเข้า อาจจะต้องใช้เวลาสักครู่นะ"
lists: "รายการ" lists: "รายการ"
noLists: "คุณไม่มีลิสต์ใดนะ" noLists: "คุณไม่มีลิสต์ใด ๆ"
note: "ตัวโน้ต" note: " โน้ต"
notes: "ตัวโน้ต" notes: "ตัวโน้ต"
following: "กำลังติดตาม" following: "กำลังติดตาม"
followers: "ผู้ติดตาม" followers: "ผู้ติดตาม"
@ -100,7 +106,7 @@ followRequestPending: "กำลังรอดำเนินการร้อ
enterEmoji: "ใส่อีโมจิ" enterEmoji: "ใส่อีโมจิ"
renote: "รีโน้ต" renote: "รีโน้ต"
unrenote: "เลิกรีโน้ต" unrenote: "เลิกรีโน้ต"
renoted: "รีโน้ตแล้วนะ" renoted: "รีโน้ตแล้ว"
cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตไว้ใหม่ได้นะ" cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตไว้ใหม่ได้นะ"
cantReRenote: "ไม่สามารถรีโน้ตเอาไว้ใหม่ได้นะ" cantReRenote: "ไม่สามารถรีโน้ตเอาไว้ใหม่ได้นะ"
quote: "อ้างคำพูด" quote: "อ้างคำพูด"
@ -108,7 +114,7 @@ inChannelRenote: "รีโน้ตช่องแชลแนลเท่า
inChannelQuote: "อ้างช่องเท่านั้น" inChannelQuote: "อ้างช่องเท่านั้น"
pinnedNote: "โน้ตที่ปักหมุดเอาไว้" pinnedNote: "โน้ตที่ปักหมุดเอาไว้"
pinned: "ปักหมุดไปยังโปรไฟล์" pinned: "ปักหมุดไปยังโปรไฟล์"
you: "ตัวเอง" you: "คุณ"
clickToShow: "คลิกเพื่อแสดง" clickToShow: "คลิกเพื่อแสดง"
sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW" sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW"
add: "เพิ่ม" add: "เพิ่ม"
@ -134,8 +140,10 @@ unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต
suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?" suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?"
unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้" unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้"
selectList: "เลือกรายการ" selectList: "เลือกรายการ"
editList: "แก้ไขรายการ"
selectChannel: "เลือกแชนแนล" selectChannel: "เลือกแชนแนล"
selectAntenna: "เลือกเสาอากาศ" selectAntenna: "เลือกเสาอากาศ"
editAntenna: "แก้ไขเสาอากาศ"
selectWidget: "เลือกวิดเจ็ต" selectWidget: "เลือกวิดเจ็ต"
editWidgets: "แก้ไขวิดเจ็ต" editWidgets: "แก้ไขวิดเจ็ต"
editWidgetsExit: "เรียบร้อย" editWidgetsExit: "เรียบร้อย"
@ -148,6 +156,8 @@ addEmoji: "แทรกอีโมจิ"
settingGuide: "การตั้งค่าที่แนะนำ" settingGuide: "การตั้งค่าที่แนะนำ"
cacheRemoteFiles: "แคชไฟล์ระยะไกล" cacheRemoteFiles: "แคชไฟล์ระยะไกล"
cacheRemoteFilesDescription: "เมื่อปิดใช้งานการตั้งค่านี้ ไฟล์ระยะไกลนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกล แต่กรณีการปิดใช้งานนี้จะช่วยลดปริมาณการใช้พื้นที่จัดเก็บข้อมูล แต่เพิ่มปริมาณการใช้งาน เพราะเนื่องจากจะไม่มีการสร้างภาพขนาดย่อ" cacheRemoteFilesDescription: "เมื่อปิดใช้งานการตั้งค่านี้ ไฟล์ระยะไกลนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกล แต่กรณีการปิดใช้งานนี้จะช่วยลดปริมาณการใช้พื้นที่จัดเก็บข้อมูล แต่เพิ่มปริมาณการใช้งาน เพราะเนื่องจากจะไม่มีการสร้างภาพขนาดย่อ"
cacheRemoteSensitiveFiles: "ไฟล์ระยะไกลที่มีความละเอียดอ่อนแคช"
cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานแล้วการตั้งค่านี้ ไฟล์รีโมตที่มีความละเอียดอ่อนนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกลโดยที่ไม่มีการแคช"
flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอท" flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอท"
flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ Misskey เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท" flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ Misskey เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท"
flagAsCat: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว" flagAsCat: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว"
@ -164,7 +174,7 @@ wallpaper: "วอลล์เปเปอร์"
setWallpaper: "ตั้งวอลเปเปอร์" setWallpaper: "ตั้งวอลเปเปอร์"
removeWallpaper: "นำวอลเปเปอร์ออก" removeWallpaper: "นำวอลเปเปอร์ออก"
searchWith: "ค้นหา: {q}" searchWith: "ค้นหา: {q}"
youHaveNoLists: "รายการนี้ว่างเปล่า" youHaveNoLists: "คุณไม่มีลิสต์ใด ๆ "
followConfirm: "คุณแน่ใจแล้วหรอว่าต้องการที่จะติดตาม {name}?" followConfirm: "คุณแน่ใจแล้วหรอว่าต้องการที่จะติดตาม {name}?"
proxyAccount: "บัญชี พร็อกซี่" proxyAccount: "บัญชี พร็อกซี่"
proxyAccountDescription: "บัญชีพร็อกซี่ คือ บัญชีที่จะทำหน้าที่เป็นผู้ติดตามระยะไกลสำหรับผู้ใช้งานที่อยู่ภายใต้ด้วยเงื่อนไขบางอย่าง ยกตัวอย่าง เช่น เมื่อมีผู้ใช้งานนั้นได้เพิ่มผู้ใช้งานจากระยะไกลลงในรายการ แต่กิจกรรมของผู้ใช้ในระยะไกลนั้นจะไม่ถูกส่งไปยังอินสแตนซ์หากไม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัญชีพร็อกซีนี้จะติดตามแทน" proxyAccountDescription: "บัญชีพร็อกซี่ คือ บัญชีที่จะทำหน้าที่เป็นผู้ติดตามระยะไกลสำหรับผู้ใช้งานที่อยู่ภายใต้ด้วยเงื่อนไขบางอย่าง ยกตัวอย่าง เช่น เมื่อมีผู้ใช้งานนั้นได้เพิ่มผู้ใช้งานจากระยะไกลลงในรายการ แต่กิจกรรมของผู้ใช้ในระยะไกลนั้นจะไม่ถูกส่งไปยังอินสแตนซ์หากไม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัญชีพร็อกซีนี้จะติดตามแทน"
@ -186,7 +196,7 @@ blockThisInstance: "บล็อกอินสแตนซ์นี้"
operations: "ดำเนินการ" operations: "ดำเนินการ"
software: "ซอฟต์แวร์" software: "ซอฟต์แวร์"
version: "เวอร์ชั่น" version: "เวอร์ชั่น"
metadata: "ข้อมูลเมตา" metadata: "Metadata"
withNFiles: "{n} ไฟล์(s)" withNFiles: "{n} ไฟล์(s)"
monitor: "มอนิเตอร์" monitor: "มอนิเตอร์"
jobQueue: "คิวงาน" jobQueue: "คิวงาน"
@ -213,7 +223,7 @@ intro: "การติดตั้ง Misskey เสร็จสิ้นแล
done: "เสร็จสิ้น" done: "เสร็จสิ้น"
processing: "กำลังประมวลผล..." processing: "กำลังประมวลผล..."
preview: "แสดงตัวอย่าง" preview: "แสดงตัวอย่าง"
default: "ค่าตั้งต้น" default: "ค่าเริ่มต้น"
defaultValueIs: "ค่าเริ่มต้น: {value}" defaultValueIs: "ค่าเริ่มต้น: {value}"
noCustomEmojis: "ไม่มีอีโมจิ" noCustomEmojis: "ไม่มีอีโมจิ"
noJobs: "ไม่มีชิ้นงาน" noJobs: "ไม่มีชิ้นงาน"
@ -234,10 +244,10 @@ currentPassword: "รหัสผ่านปัจจุบัน"
newPassword: "รหัสผ่านใหม่" newPassword: "รหัสผ่านใหม่"
newPasswordRetype: "ใส่รหัสผ่านใหม่อีกครั้ง" newPasswordRetype: "ใส่รหัสผ่านใหม่อีกครั้ง"
attachFile: "แนบไฟล์" attachFile: "แนบไฟล์"
more: "เพิ่มเติม!" more: "เพิ่มเติม"
featured: "ไฮไลท์" featured: "ไฮไลท์"
usernameOrUserId: "ชื่อผู้ใช้หรือรหัสผู้ใช้งาน" usernameOrUserId: "ชื่อผู้ใช้หรือรหัสผู้ใช้งาน"
noSuchUser: "ไม่มีผู้ใช้นี้อยู่ในระบบ" noSuchUser: "ไม่พบผู้ใช้"
lookup: "การค้นหา" lookup: "การค้นหา"
announcements: "ประกาศ" announcements: "ประกาศ"
imageUrl: "url รูปภาพ" imageUrl: "url รูปภาพ"
@ -282,7 +292,7 @@ themeForLightMode: "ธีมที่จะใช้ในโหมดแสง
themeForDarkMode: "ธีมที่จะใช้ในโหมดมืด" themeForDarkMode: "ธีมที่จะใช้ในโหมดมืด"
light: "สว่าง" light: "สว่าง"
dark: "มืด" dark: "มืด"
lightThemes: "ธีมสีสว่าง" lightThemes: "ธีมสว่าง"
darkThemes: "ธีมมืด" darkThemes: "ธีมมืด"
syncDeviceDarkMode: "ซิงค์โหมดมืดด้วยการตั้งค่ากับอุปกรณ์" syncDeviceDarkMode: "ซิงค์โหมดมืดด้วยการตั้งค่ากับอุปกรณ์"
drive: "ไดรฟ์" drive: "ไดรฟ์"
@ -309,7 +319,7 @@ copyUrl: "คัดลอก URL"
rename: "เปลี่ยนชื่อ" rename: "เปลี่ยนชื่อ"
avatar: "ไอคอน" avatar: "ไอคอน"
banner: "แบนเนอร์" banner: "แบนเนอร์"
nsfw: "เนื้อหาที่ละเอียดอ่อน NSFW" displayOfSensitiveMedia: "แสดงผลสื่อละเอียดอ่อน"
whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์" whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์"
disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์" disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์"
reload: "รีโหลด" reload: "รีโหลด"
@ -319,7 +329,7 @@ watch: "ดู"
unwatch: "หยุดดู" unwatch: "หยุดดู"
accept: "ยอมรับ" accept: "ยอมรับ"
reject: "ปฏิเสธ" reject: "ปฏิเสธ"
normal: "โหมดปกติ" normal: "ปกติ"
instanceName: "ชื่อ อินสแตนซ์" instanceName: "ชื่อ อินสแตนซ์"
instanceDescription: "คำอธิบายอินสแตนซ์" instanceDescription: "คำอธิบายอินสแตนซ์"
maintainerName: "ผู้ดูแล" maintainerName: "ผู้ดูแล"
@ -328,9 +338,9 @@ tosUrl: "เงื่อนไขการให้บริการ URL"
thisYear: "ปีนี้" thisYear: "ปีนี้"
thisMonth: "เดือนนี้" thisMonth: "เดือนนี้"
today: "วันนี้" today: "วันนี้"
dayX: "{วัน}" dayX: "{day}"
monthX: "{เดือน}" monthX: "เดือน {month}"
yearX: "{ปี}" yearX: "{year}"
pages: "หน้า" pages: "หน้า"
integration: "รวบรวม" integration: "รวบรวม"
connectService: "เชื่อมต่อ" connectService: "เชื่อมต่อ"
@ -790,6 +800,7 @@ noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม
noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ" noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ"
configure: "กำหนดค่า" configure: "กำหนดค่า"
postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่" postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่"
postToHashtag: "โพสต์ไปที่แฮชแท็กนี้"
gallery: "แกลเลอรี่" gallery: "แกลเลอรี่"
recentPosts: "โพสต์ล่าสุด" recentPosts: "โพสต์ล่าสุด"
popularPosts: "โพสต์ติดอันดับ" popularPosts: "โพสต์ติดอันดับ"
@ -823,6 +834,7 @@ translatedFrom: "แปลมาจาก {x}"
accountDeletionInProgress: "กำลังดำเนินการลบบัญชีอยู่" accountDeletionInProgress: "กำลังดำเนินการลบบัญชีอยู่"
usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง" usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง"
aiChanMode: "โหมด Ai " aiChanMode: "โหมด Ai "
devMode: "โหมดนักพัฒนา"
keepCw: "เก็บคำเตือนเนื้อหา" keepCw: "เก็บคำเตือนเนื้อหา"
pubSub: "บัญชีผับ/ย่อย" pubSub: "บัญชีผับ/ย่อย"
lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด" lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด"
@ -832,6 +844,8 @@ breakFollow: "ลบผู้ติดตาม"
breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?" breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?"
itsOn: "เปิดใช้งาน" itsOn: "เปิดใช้งาน"
itsOff: "ปิดใช้งาน" itsOff: "ปิดใช้งาน"
on: "เปิด"
off: "ปิด"
emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร" emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร"
unread: "ไม่ได้อ่าน" unread: "ไม่ได้อ่าน"
filter: "กรอง" filter: "กรอง"
@ -850,7 +864,7 @@ incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?" voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?"
hide: "ซ่อน" hide: "ซ่อน"
useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ" useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ"
welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, {name}" welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, {name}"
clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ" clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ"
overridedDeviceKind: "ประเภทอุปกรณ์" overridedDeviceKind: "ประเภทอุปกรณ์"
smartphone: "สมาร์ทโฟน" smartphone: "สมาร์ทโฟน"
@ -955,7 +969,7 @@ color: "สี"
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ" youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ"
cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว" cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว"
cannotPerformTemporaryDescription: "การดําเนินการนี้ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้งนะค่ะ" cannotPerformTemporaryDescription: "ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้ง"
invalidParamError: "ข้อผิดพลาดพารามิเตอร์" invalidParamError: "ข้อผิดพลาดพารามิเตอร์"
invalidParamErrorDescription: "คำขอพารามิเตอร์ไม่ถูกต้อง สิ่งนี้มักจะเกิดจากข้อผิดพลาด แต่อาจเกิดจากอินพุตเกินขีดจำกัดของขนาดหรือที่คล้ายกัน" invalidParamErrorDescription: "คำขอพารามิเตอร์ไม่ถูกต้อง สิ่งนี้มักจะเกิดจากข้อผิดพลาด แต่อาจเกิดจากอินพุตเกินขีดจำกัดของขนาดหรือที่คล้ายกัน"
permissionDeniedError: "การดำเนินถูกปฏิเสธ" permissionDeniedError: "การดำเนินถูกปฏิเสธ"
@ -977,20 +991,23 @@ joinThisServer: "ลงชื่อสมัครใช้ในอินสแ
exploreOtherServers: "มองหาอินสแตนซ์อื่น" exploreOtherServers: "มองหาอินสแตนซ์อื่น"
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์" letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?" disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?"
disableFederationConfirmWarn: "แม้ว่าจะถูกยกเลิกเอาไว้โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป เว้นแต่ว่า...จะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องทำตรงนี้หรอกนะค่ะ" disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น"
disableFederationOk: "ปิดการใช้งาน" disableFederationOk: "ปิดการใช้งาน"
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ" invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญ เพื่องลงทะเบียนเข้าใช้งาน"
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมล"
postToTheChannel: "โพสต์ลงช่อง" postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
reactionAcceptance: "การยอมรับรีแอคชั่น" reactionAcceptance: "การยอมรับรีแอคชั่น"
likeOnly: "ที่ชอบเท่านั้น" likeOnly: "ที่ชอบเท่านั้น"
likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น" likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น"
nonSensitiveOnly: "ไม่มีความอ่อนไหวเท่านั้น"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "ไม่มีความอ่อนไหวเท่านั้น (เฉพาะไลค์จากระยะไกลเท่านั้น)"
rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน" rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?" resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
sensitiveWords: "คำที่ละเอียดอ่อน" sensitiveWords: "คำที่ละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ" sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน"
license: "ใบอนุญาต" license: "ใบอนุญาต"
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?" unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
myClips: "คลิปของฉัน" myClips: "คลิปของฉัน"
@ -1025,29 +1042,84 @@ vertical: "แนวตั้ง"
horizontal: "ด้านข้าง" horizontal: "ด้านข้าง"
position: "ตำแหน่ง" position: "ตำแหน่ง"
serverRules: "กฎของเซิฟเวอร์" serverRules: "กฎของเซิฟเวอร์"
pleaseConfirmBelowBeforeSignup: "โปรดยืนยันด้านล่างก่อนกำลังลงชื่อสมัครนะค่ะ" pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างก่อนสมัครใช้งาน"
pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ" pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ"
continue: "ดำเนินการต่อ" continue: "ดำเนินการต่อ"
preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้" preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร" preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร"
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้" createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
archive: "เก็บถาวร" archive: "เก็บถาวร"
channelArchiveConfirmTitle: "เก็บถาวรจริงๆ {name} มั้ย?"
channelArchiveConfirmDescription: "ช่องที่ถูกเก็บถาวรแล้วนั้นจะไม่ปรากฏในรายการช่องหรือผลการค้นหานั้นอีกต่อไปไม่สามารถเพิ่มโพสต์ใหม่ได้อีกต่อไปนะ"
thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ"
displayOfNote: "การแสดงโน้ต"
initialAccountSetting: "ตั้งค่าโปรไฟล์"
youFollowing: "ติดตามแล้ว" youFollowing: "ติดตามแล้ว"
preventAiLearning: "ปฏิเสธการใช้งาน ในการเรียนรู้ของเครื่อง (Generative AI)"
preventAiLearningDescription: "การส่งคำร้องขอโปรแกรมรวบรวมข้อมูลไม่ให้ใช้ข้อความที่โพสต์หรือรูปภาพ ฯลฯ ในชุดข้อมูลแมชชีนเลิร์นนิง (Predictive / Generative AI) สิ่งนี้นั้นทำได้โดยการเพิ่มแฟล็กการตอบสนอง \"noai\" HTML ให้กับเนื้อหาที่เกี่ยวข้อง แต่อย่างไรก็ตามแล้ว การป้องกันโดยสมบูรณ์นั้นไม่สามารถทำได้ผ่านแฟล็กนี้เนื่องจากอาจจะทำให้ถูกเพิกเฉยได้"
options: "ตัวเลือกบทบาท" options: "ตัวเลือกบทบาท"
specifyUser: "ผู้ใช้เฉพาะ"
failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้"
update: "อัปเดต"
rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้อิโมจินี้เป็นรีแอคชั่นได้"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ถ้าหากไม่ได้ระบุบทบาท ทุกคนนั้นก็สามารถใช้อิโมจินี้เป็นการแสดงความรู้สึกได้นะ"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "บทบาทเหล่านี้ต้องเป็นสาธารณะ"
cancelReactionConfirm: "ต้องการลบรีแอคชั่นของคุณจริงๆหรอ?"
changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นของคุณจริงๆหรอ?"
later: "ไว้ทีหลัง"
goToMisskey: "ถึง Misskey"
additionalEmojiDictionary: "พจนานุกรมอีโมจิเพิ่มเติม"
installed: "ติดตั้งแล้ว"
branding: "แบรนดิ้ง"
enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์"
enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ"
turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้"
createInviteCode: "สร้างคำเชิญ"
createWithOptions: "สร้างด้วยตัวเลือก"
createCount: "จำนวนการเชิญ"
inviteCodeCreated: "สร้างคำเชิญแล้ว"
inviteLimitExceeded: "คุณสร้างคำเชิญเกินถึงขีดจำกัดแล้วนะ"
createLimitRemaining: "ขีดจำกัดการเชิญ: {limit} ที่เหลืออยู่"
inviteLimitResetCycle: "ขีดจำกัดนี้จะถูกรีเซ็ตเป็น {limit} ที่ {time}."
expirationDate: "วันที่หมดอายุ"
noExpirationDate: "ไม่มีหมดอายุ"
inviteCodeUsedAt: "รหัสคำเชิญใช้แล้วที่"
registeredUserUsingInviteCode: "ใช้คำเชิญแล้วโดย"
waitingForMailAuth: "กำลังรอการยืนยันอีเมล"
inviteCodeCreator: "สร้างการเชิญแล้วโดย"
usedAt: "ใช้แล้วที่"
unused: "ไม่ใช้แล้ว"
used: "ใช้แล้ว"
expired: "หมดอายุแล้ว"
_initialAccountSetting:
accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!"
letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ"
letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ"
profileSetting: "ตั้งค่าโปรไฟล์"
privacySetting: "ตั้งค่าความเป็นส่วนตัว"
theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ"
youCanEditMoreSettingsInSettingsPageLater: "ยังมีการตั้งค่าอื่นๆ อีกมากมายที่คุณนั้นสามารถกำหนดค่าได้จาก \"การตั้งค่า\" เพื่อให้แน่ใจว่าได้เยี่ยมชมมันได้ภายหลังนะ"
followUsers: "ลองติดตามผู้ใช้บางคนที่คุณอาจจะสนใจเพื่อสร้างไทม์ไลน์ของคุณสิ !"
pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ"
initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!"
haveFun: "สนุกกับ {name}!"
ifYouNeedLearnMore: "ถ้าหากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ {ชื่อ} (Misskey) กรุณาไปที่ {link}"
skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?"
laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?"
_serverRules: _serverRules:
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
_accountMigration: _accountMigration:
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น" moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
moveFromLabel: "บัญชีที่จะย้ายจาก:" moveFromLabel: "บัญชีที่จะย้ายจาก:"
moveFromDescription: "สร้างนามแฝงสำหรับบัญชีที่จะย้ายจากบัญชีนี้ ถ้าหากคุณต้องการโอนผู้ติดตาม สิ่งนี้ต้องทำก่อนโอนก่อนนะค่ะ! หลังจากนั้น ป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com" moveFromDescription: "ถ้าหากคุณต้องการโอนข้อมูล คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี หลังจากนั้นป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com"
moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง" moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง"
moveToLabel: "บัญชีที่จะย้ายไปที่:" moveToLabel: "บัญชีที่จะย้ายไปที่:"
moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้" moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้"
moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com" moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com"
moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com" moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com"
startMigration: "โอนย้าย" startMigration: "โอนย้าย"
migrationConfirm: "ย้ายข้อมูลบัญชีนี้ไปที่ {account} จริงๆนะ เมื่อมีการเริ่มต้นแล้ว กระบวนการนี้จะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ เพื่อให้แน่ใจยืนยันว่าคุณได้สร้างนามแฝงในบัญชีที่จะย้ายข้อมูลนะค่ะ" migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี"
movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้" movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้"
postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป" postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป"
movedTo: "บัญชีที่จะย้ายไปที่:" movedTo: "บัญชีที่จะย้ายไปที่:"
@ -1223,7 +1295,7 @@ _achievements:
title: "พักผ่อนสักหน่อย" title: "พักผ่อนสักหน่อย"
description: "ใช้เวลา 30 นาทีบน Misskey" description: "ใช้เวลา 30 นาทีบน Misskey"
_client60min: _client60min:
title: "ไม่มี \"Miss\" ใน Misskey นะค่ะ !" title: "ไม่พบ \"Miss\" ใน Misskey "
description: "เปิด Misskey ค้างไว้แล้วอย่างน้อย 60 นาที" description: "เปิด Misskey ค้างไว้แล้วอย่างน้อย 60 นาที"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "ไม่เป็นไร" title: "ไม่เป็นไร"
@ -1329,6 +1401,9 @@ _role:
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
canPublicNote: "สามารถส่งโน้ตสาธารณะ" canPublicNote: "สามารถส่งโน้ตสาธารณะ"
canInvite: "สร้างรหัสเชิญอินสแตนซ์" canInvite: "สร้างรหัสเชิญอินสแตนซ์"
inviteLimit: "จำกัดการเชิญ"
inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์"
inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ"
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
driveCapacity: "ความจุของไดรฟ์" driveCapacity: "ความจุของไดรฟ์"
alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ"
@ -1391,6 +1466,7 @@ _ad:
back: "ย้อนกลับ" back: "ย้อนกลับ"
reduceFrequencyOfThisAd: "แสดงโฆษณานี้ให้น้อยลง" reduceFrequencyOfThisAd: "แสดงโฆษณานี้ให้น้อยลง"
hide: "ไม่ต้องแสดง" hide: "ไม่ต้องแสดง"
timezoneinfo: "วันในสัปดาห์นี้จะถูกกำหนดจากโซนเวลาของเซิร์ฟเวอร์"
_forgotPassword: _forgotPassword:
enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ" enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ"
ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ" ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ"
@ -1442,9 +1518,9 @@ _aboutMisskey:
donate: "บริจาคให้กับ Misskey" donate: "บริจาคให้กับ Misskey"
morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ ขอขอบคุณ! 🥰" morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ ขอขอบคุณ! 🥰"
patrons: "สมาชิกพันธมิตร" patrons: "สมาชิกพันธมิตร"
_nsfw: _displayOfSensitiveMedia:
respect: "ซ่อนสื่อ NSFW" respect: "ซ่อนสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน"
ignore: "อย่าซ่อนสื่อ NSFW" ignore: "แสดงผลสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน"
force: "ซ่อนสื่อทั้งหมด" force: "ซ่อนสื่อทั้งหมด"
_instanceTicker: _instanceTicker:
none: "ไม่ต้องแสดง" none: "ไม่ต้องแสดง"
@ -1585,9 +1661,13 @@ _time:
day: "วัน" day: "วัน"
_timelineTutorial: _timelineTutorial:
title: "วิธีใช้งาน Misskey" title: "วิธีใช้งาน Misskey"
step1_1: "นี่คือ \"ไทม์ไลน์\" \"โน้ต\" ทั้งหมดที่ส่งใน {name} จะแสดงรายการตามลำดับเวลาที่นี่นะ"
step1_2: "อาจจะมีไทม์ไลน์ที่แตกต่างกันเล็กน้อยยกตัวอย่างเช่น \"ไทม์ไลน์หน้าแรก\" จะมีโน้ตของผู้ใช้ที่คุณติดตามและ \"ไทม์ไลน์ท้องถิ่น\" จะมีโน้ตจากผู้ใช้ทั้งหมดของ {name}"
step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ"
step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?"
step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?" step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?"
step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ" step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ"
step4_1: "คุณยังสามารถแนบ \"ปฏิกิริยา\" ไปกับโน้ตได้อีกด้วยนะค่ะ" step4_1: "คุณสามารถเพิ่ม \"การตอบสนอง\" ในโน้ตได้"
step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย" step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย"
_2fa: _2fa:
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
@ -1916,6 +1996,7 @@ _deck:
introduction: "สร้างอินเทอร์เฟซที่สมบูรณ์แบบสำหรับคุณโดยจัดเรียงคอลัมน์ได้อย่างอิสระ!" introduction: "สร้างอินเทอร์เฟซที่สมบูรณ์แบบสำหรับคุณโดยจัดเรียงคอลัมน์ได้อย่างอิสระ!"
introduction2: "คลิกที่เครื่องหมาย + ทางขวาของหน้าจอเพื่อเพิ่มคอลัมน์ใหม่ทุกครั้งที่คุณต้องการ" introduction2: "คลิกที่เครื่องหมาย + ทางขวาของหน้าจอเพื่อเพิ่มคอลัมน์ใหม่ทุกครั้งที่คุณต้องการ"
widgetsIntroduction: "กรุณาเลือก \"แก้ไขวิดเจ็ต\" ในเมนูคอลัมน์และเพิ่มวิดเจ็ต" widgetsIntroduction: "กรุณาเลือก \"แก้ไขวิดเจ็ต\" ในเมนูคอลัมน์และเพิ่มวิดเจ็ต"
useSimpleUiForNonRootPages: "แสดง UI ของ Root Page อย่างง่าย "
_columns: _columns:
main: "หลัก" main: "หลัก"
widgets: "วิดเจ็ต" widgets: "วิดเจ็ต"

View file

@ -1,5 +1,6 @@
--- ---
_lang_: "Türkçe" _lang_: "Türkçe"
headlineMisskey: "Notlarla bağlanmış bir ağ"
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>Misskey</b>Dünya'nın en sunucularında biri。" poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>Misskey</b>Dünya'nın en sunucularında biri。"
monthAndDay: "{month}Ay {day}Gün" monthAndDay: "{month}Ay {day}Gün"
@ -11,7 +12,9 @@ forgotPassword: "şifremi unuttum"
ok: "TAMAM" ok: "TAMAM"
gotIt: "Anladım" gotIt: "Anladım"
cancel: "İptal" cancel: "İptal"
noThankYou: "Hayır, teşekkürler"
enterUsername: "Kullanıcı adınızı giriniz" enterUsername: "Kullanıcı adınızı giriniz"
renotedBy: "{user} tarafından Renotelandı"
noNotes: "Notlar mevcut değil." noNotes: "Notlar mevcut değil."
noNotifications: "Bildirim bulunmuyor" noNotifications: "Bildirim bulunmuyor"
instance: "Sunucu" instance: "Sunucu"
@ -45,15 +48,35 @@ delete: "Sil"
deleteAndEdit: "Sil ve yeniden düzenle" deleteAndEdit: "Sil ve yeniden düzenle"
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
addToList: "Listeye ekle" addToList: "Listeye ekle"
addToAntenna: "Antene ekle"
sendMessage: "Mesaj Gönder" sendMessage: "Mesaj Gönder"
copyRSS: "RSSKopyala" copyRSS: "RSSKopyala"
copyUsername: "Kullanıcı Adını Kopyala" copyUsername: "Kullanıcı Adını Kopyala"
copyUserId: "KullanıcıyıKopyala" copyUserId: "KullanıcıyıKopyala"
copyNoteId: "Kimlik notunu kopyala" copyNoteId: "Kimlik notunu kopyala"
copyFileId: "Dosya ID'sini kopyala"
copyFolderId: "Klasör ID'sini kopyala"
copyProfileUrl: "Profil URL'sini kopyala"
searchUser: "Kullanıcıları ara" searchUser: "Kullanıcıları ara"
reply: "yanıt" reply: "yanıt"
loadMore: "Devamını yükle" loadMore: "Devamını yükle"
showMore: "Devamını yükle" showMore: "Devamını yükle"
showLess: "Kapat"
youGotNewFollower: "seni takip etti"
receiveFollowRequest: "Takip isteği alındı"
followRequestAccepted: "Takip isteği kabul edildi"
mention: "Bahset"
mentions: "Bahsetmeler"
directNotes: "Kişisel mesajlar"
importAndExport: "İçeri/Dışarı aktar"
import: "İçeri aktar"
export: "Dışa aktar"
files: "Dosyalar"
download: "İndir"
driveFileDeleteConfirm: "\"{name}\" dosyası silinsin mi? Dosya kullanıldığı tüm notlardan kaybolacaktır."
unfollowConfirm: "{name} takipten çıkarılsın mı?"
exportRequested: "Dışa aktarım talep ettiniz. Bu biraz zaman alabilir. İşlem bitince Sürücünüze eklenecektir."
importRequested: "Dışa aktarım talep ettiniz. Bu işlem biraz zaman alabilir."
lists: "Listeler" lists: "Listeler"
noLists: "Liste yok" noLists: "Liste yok"
note: "not" note: "not"
@ -64,6 +87,16 @@ followsYou: "seni takip ediyor"
createList: "Liste oluştur" createList: "Liste oluştur"
manageLists: "Yönetici Listeleri" manageLists: "Yönetici Listeleri"
error: "hata" error: "hata"
somethingHappened: "Bir hata oluştu"
retry: "Tekrar dene"
pageLoadError: "Sayfa yüklenemedi."
pageLoadErrorDescription: "Bu genelde ağ veya tarayıcı ön belleği hatalarından olur. Lütfen ön belleği temizlemeyi veya birkaç dakika beklemeyi ve sayfayı yenilemeyi deneyin."
serverIsDead: "Sunucu yanıt vermiyor. Birkaç dakika sonra tekrar deneyin."
youShouldUpgradeClient: "Sayfayı görüntülemek için yenileyin."
enterListName: "Liste ismi"
privacy: "Gizlilik"
makeFollowManuallyApprove: "Takip istekleri elle onaylansın"
defaultNoteVisibility: "Varsayılan görünürlük"
follow: "takipçi" follow: "takipçi"
followRequest: "Takip isteği" followRequest: "Takip isteği"
followRequests: "Takip istekleri" followRequests: "Takip istekleri"
@ -76,9 +109,24 @@ renoted: "yeniden adlandırılmış"
cantRenote: "Ayrılamama" cantRenote: "Ayrılamama"
cantReRenote: "not alabilirmiyim" cantReRenote: "not alabilirmiyim"
quote: "alıntı" quote: "alıntı"
inChannelRenote: "Kanal içi Renote"
inChannelQuote: "Kanal içi Alıntı"
pinnedNote: "Sabitlenen" pinnedNote: "Sabitlenen"
pinned: "Sabitlenmiş" pinned: "Sabitlenmiş"
you: "sen" you: "sen"
clickToShow: "Görüntülemek için tıkla"
sensitive: "Hassas içerik"
add: "Ekle"
reaction: "Tepkiler"
reactions: "Tepkiler"
reactionSetting: "Palette görünecek tepkiler"
reactionSettingDescription2: "Sıralamak için sürükleyin, silmek için tıklayın, eklemek için \"+\" tuşuna tıklayın."
rememberNoteVisibility: "Görünürlük ayarlarını hatırla"
attachCancel: "Eki sil"
markAsSensitive: "Hassas içerik olarak işaretle"
unmarkAsSensitive: "Hassas içerik işaretini kaldır"
enterFileName: "Dosya ismini gir"
mute: "Gizle"
unmute: "sesi aç" unmute: "sesi aç"
renoteMute: "sesi kapat" renoteMute: "sesi kapat"
renoteUnmute: "sesi açmayı iptal et" renoteUnmute: "sesi açmayı iptal et"
@ -88,46 +136,280 @@ suspend: "askıya al"
unsuspend: "askıya alma" unsuspend: "askıya alma"
blockConfirm: "Onayı engelle" blockConfirm: "Onayı engelle"
unblockConfirm: "engellemeyi kaldır onayla" unblockConfirm: "engellemeyi kaldır onayla"
suspendConfirm: "Hesap askıya alınsın mı?"
unsuspendConfirm: "Hesap askıdan kaldırılsın mı"
selectList: "Bir liste seç"
editList: "Listeyi düzenle"
selectChannel: "Kanal seç" selectChannel: "Kanal seç"
selectAntenna: "Bir anten seç"
editAntenna: "Anteni düzenle"
selectWidget: "Araç seç"
editWidgets: "Araçları düzenle"
editWidgetsExit: "Tamam"
customEmojis: "Özel Emoji"
emoji: "Emoji"
emojis: "Emoji"
emojiName: "Emoji adı"
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."
cacheRemoteSensitiveFiles: "Hassas uzak dosyalar ön belleğe alınsın"
cacheRemoteSensitiveFilesDescription: "Bu ayar kapalı olduğunda hassas uzak dosyalar ön belleğe alınmadan doğrudan uzak sunucudan yüklenecektir."
flagAsBot: "Bot olarak işaretle" flagAsBot: "Bot olarak işaretle"
flagAsBotDescription: "Bu seçeneği hesap bir program tarafından kontrol ediliyorsa işaretleyin. Bu, diğer geliştiricilerin sonsuz etkileşim zincirleri oluşturmasını engellemeye yardımcı olur ve Misskey'in iç sisteminin hesaba bir bot gibi davranmasını sağlar."
flagAsCat: "Kedi hesabı"
flagAsCatDescription: "Kedi hesabı"
flagShowTimelineReplies: "Zaman akışında notlara gelen cevapları göster"
flagShowTimelineRepliesDescription: "Açık olduğu durumda, zaman akışında kullanıcıların başkalarına verdiği cevaplar gözükür."
autoAcceptFollowed: "Takip edilen hesapların takip isteklerini kabul et"
addAccount: "Hesap ekle"
reloadAccountsList: "Hesap listesini güncelle"
loginFailed: "Giriş başarısız oldu"
showOnRemote: "Uzak sunucuda görüntüle"
general: "Genel"
wallpaper: "Duvar kağıdı"
setWallpaper: "Duvar kağıdını ayarla"
removeWallpaper: "Duvar kağıdını sil"
searchWith: "Arama: {q}"
youHaveNoLists: "Hiç listeniz yok"
followConfirm: "{name} takip edilsin mi?"
proxyAccount: "Vekil hesabı"
proxyAccountDescription: "Proxy hesabı, belirli koşullar altında kullanıcılar için uzaktan takipçi işlevi gören bir hesaptır. Örneğin, bir kullanıcı listeye bir uzak kullanıcı eklediğinde, o kullanıcıyı takip eden yerel bir kullanıcı yoksa uzak kullanıcının etkinliği örneğe teslim edilmeyecektir, dolayısıyla bunun yerine proxy hesabı takip edilecektir."
host: "Sağlayıcı"
selectUser: "Kullanıcı seç"
recipient: "Kime"
annotation: "Açıklamalar"
federation: "Federasyon"
instances: "Sunucu" instances: "Sunucu"
registeredAt: "Katılma tarihi"
latestRequestReceivedAt: "Alınan son talep"
latestStatus: "En son durum"
storageUsage: "Depolama kullanımı"
charts: "Çizelgeler"
perHour: "Saatlik"
perDay: "Günlük"
stopActivityDelivery: "Durum güncellemelerini gönderme"
blockThisInstance: "Bu sunucuyu engelle"
operations: "İşlemler"
software: "Yazılımlar"
version: "Sürüm"
metadata: "Meta Verileri"
withNFiles: "{n} tane dosya"
monitor: "Monitör"
jobQueue: "İşlem sırası"
cpuAndMemory: "İşlemci ve Hafıza"
network: "Ağ"
disk: "Disk"
instanceInfo: "Sunucu Bilgisi"
statistics: "İstatistikler"
clearQueue: "Sırayı temizle"
clearQueueConfirmTitle: "Sıra silinsin mi?"
clearQueueConfirmText: "Sırada kalan hiçbir şey iletilmeyecek. Genelde bu işlem gerekli değildir."
clearCachedFiles: "Ön belleği temizle"
clearCachedFilesConfirm: "Ön belleğe alınmış tüm uzak sunucu dosyaları silinsin mi?"
blockedInstances: "Engellenen sunucular"
blockedInstancesDescription: "Engellemek istediğiniz sunucuların alan adlarını satır sonlarıyla ayırarak yazın. Yazılan sunucular bu sunucuyla iletişime geçemeyecek."
muteAndBlock: "Susturma ve Engelleme"
mutedUsers: "Susturulan kullanıcılar"
blockedUsers: "Engellenen kullanıcılar"
noUsers: "Kullanıcı yok"
editProfile: "Profili düzenle"
noteDeleteConfirm: "Bu notu silmek istediğinizden emin misiniz?"
pinLimitExceeded: "Daha fazla not sabitlenemez"
intro: "Misskey yüklemesi tamamlandı! Lütfen yönetici hesabını oluşturun."
done: "Tamamlandı"
preview: "Önizleme"
default: "Varsayılan"
defaultValueIs: "Varsayılan: {value}"
noCustomEmojis: "Emoji bulunamadı"
noJobs: "Hiç işlem yok"
federating: "Federe ediliyor"
blocked: "Engellenmiş"
suspended: "Askıya alınmış"
all: "Tümü"
subscribing: "Abonelik"
publishing: "Paylaşım"
notResponding: "Cevap yok"
instanceFollowing: "Sunucuda takip edenler"
instanceFollowers: "Sunucu takipçileri"
instanceUsers: "Sunucu kullanıcıları"
changePassword: "Şifreyi değiştir"
security: "Güvenlik"
retypedNotMatch: "Girişler uyuşmuyor."
currentPassword: "Geçerli şifre"
newPassword: "Yeni şifre"
newPasswordRetype: "Yeni şifre (tekrar)"
attachFile: "Dosya ekle"
more: "Daha!"
featured: "Öne Çıkan"
usernameOrUserId: "Kullanıcı adı veya ID'si"
noSuchUser: "Kullanıcı bulunamadı"
lookup: "Sorgu"
announcements: "Duyurular"
imageUrl: "Görsel URL'si"
remove: "Sil" remove: "Sil"
removed: "Silindi"
removeAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?"
deleteAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?"
resetAreYouSure: "Sıfırlansın mı?"
saved: "Kaydedildi"
messaging: "Mesajlar"
upload: "Yükle"
keepOriginalUploading: "Orijinal görseli koru"
keepOriginalUploadingDescription: "Orijinal olarak yüklenen görüntüyü olduğu gibi kaydeder. Kapatılırsa, yükleme sırasında web'de görüntülenecek bir sürüm oluşturulur."
fromUrl: "Bağlantıdan"
uploadFromUrl: "Bağlantıdan yükle"
uploadFromUrlDescription: "Yüklemek istediğiniz dosyanın bağlantısı"
uploadFromUrlRequested: "Yükleme talep edildi"
uploadFromUrlMayTakeTime: "Yüklemenin tamamlanması biraz süre alabilir."
explore: "Keşfet"
messageRead: "Okundu"
noMoreHistory: "Bundan öncesi yok"
startMessaging: "Yeni bir sohbet başlat"
nUsersRead: "{n} kişi okudu"
agreeTo: "Kabul Ediyorum: {0}"
agree: "Kabul Et"
agreeBelow: "Aşağıdakileri kabul ederim"
basicNotesBeforeCreateAccount: "Önemli notlar"
termsOfService: "Şartlar ve Koşullar"
start: "Başla"
home: "Ana sayfa"
remoteUserCaution: "Bu kullanıcı bir uzak sunucudan olduğu için alınan bilgiler tam olmayabilir."
activity: "Etkinlik"
images: "Görseller"
image: "Görseller"
birthday: "Doğum günü"
yearsOld: "{age} yaşında"
registeredDate: "Kayıt tarihi"
location: "Konum"
theme: "Temalar"
themeForLightMode: "Aydınlık Tema"
themeForDarkMode: "Karanlık Tema"
light: "Aydınlık"
dark: "Karanlık"
lightThemes: "Aydınlık Temalar"
darkThemes: "Karanlık Temalar"
syncDeviceDarkMode: "Sistem Koyu Modu ile senkronize et"
drive: "Sürücü"
fileName: "Dosya adı"
selectFile: "Dosya seç"
selectFiles: "Dosya seç"
selectFolder: "Klasör seç"
selectFolders: "Klasör seç"
renameFile: "Dosyayı yeniden adlandır"
folderName: "Klasör adı"
createFolder: "Klasör oluştur"
renameFolder: "Klasörü Yeniden Adlandır"
deleteFolder: "Klasörü sil"
addFile: "Dosya ekle"
emptyDrive: "Sürücü boş"
hasChildFilesOrFolders: "Klasör boş olmadığından silinemiyor"
doNothing: "Bir şey yapma"
reloadConfirm: "Zaman akışı yenilensin mi?"
maintainerName: "Yönetici ismi"
monthX: "{month} ay"
enableRegistration: "Kayıtlara izin ver"
pinnedNotes: "Sabitlenen" pinnedNotes: "Sabitlenen"
manageAntennas: "Anten ayarları"
userList: "Listeler" userList: "Listeler"
resetPassword: "Şifre sıfırlama"
noMessagesYet: "Şimdilik mesaj yok"
details: "Detaylar"
deck: "Güverte"
smtpHost: "Sağlayıcı"
smtpUser: "Kullanıcı Adı" smtpUser: "Kullanıcı Adı"
smtpPass: "Şifre" smtpPass: "Şifre"
notificationSetting: "Bildirim ayarları"
noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et."
clearCache: "Ön belleği temizle"
onlineUsersCount: "{n} kullanıcı çevrim içi"
user: "Kullanıcı" user: "Kullanıcı"
global: "Küresel"
squareAvatars: "Kare avatarlar"
searchByGoogle: "Arama" searchByGoogle: "Arama"
file: "Dosyalar"
pushNotification: "Push bildirimleri"
subscribePushNotification: "Push bildirimlerini etkinleştir"
unsubscribePushNotification: "Push bildirimlerini kapat"
pushNotificationAlreadySubscribed: "Push bildirimleri zaten açık"
pushNotificationNotSupported: "Push bildirimleri sunucu veya tarayıcı tarafından desteklenmiyor"
noRole: "Rol bulunamadı"
color: "Renk"
addMemo: "Kısa not ekle"
_accountDelete:
started: "Silme işlemi başlatıldı"
_email:
_follow:
title: "seni takip etti"
_theme: _theme:
color: "Renk"
keys: keys:
mention: "Bahset"
renote: "vazgeçme" renote: "vazgeçme"
_sfx: _sfx:
note: "notlar" note: "notlar"
notification: "Bildirim" notification: "Bildirim"
chat: "Mesajlar"
_2fa:
renewTOTPCancel: "Hayır, teşekkürler"
_permissions:
"read:blocks": "Engellenen hesapları gör"
"write:blocks": "Engellenen hesap listesini düzenle"
_widgets: _widgets:
profile: "Profil" profile: "Profil"
instanceInfo: "Sunucu Bilgisi"
notifications: "Bildirim" notifications: "Bildirim"
timeline: "Zaman çizelgesi" timeline: "Zaman çizelgesi"
calendar: "Takvim"
clock: "Saat"
activity: "Etkinlik"
federation: "Federasyon"
jobQueue: "İşlem sırası"
_userList:
chooseList: "Bir liste seç"
_cw: _cw:
show: "Devamını yükle" show: "Devamını yükle"
_poll:
vote: "Oy kullan"
_visibility: _visibility:
publicDescription: "Herkese açık"
home: "Ana sayfa"
followers: "takipçi" followers: "takipçi"
_profile: _profile:
username: "Kullanıcı Adı" username: "Kullanıcı Adı"
_exportOrImport: _exportOrImport:
followingList: "takipçi" followingList: "takipçi"
muteList: "Gizle"
blockingList: "engelle" blockingList: "engelle"
userLists: "Listeler" userLists: "Listeler"
_charts:
federation: "Federasyon"
_timelines:
home: "Ana sayfa"
global: "Küresel"
_pages:
blocks:
image: "Görseller"
_notification: _notification:
youWereFollowed: "seni takip etti"
unreadAntennaNote: "{name} anteni"
_types: _types:
follow: "takipçi" follow: "takipçi"
mention: "Bahset"
renote: "vazgeçme" renote: "vazgeçme"
quote: "alıntı" quote: "alıntı"
reaction: "Tepkiler"
receiveFollowRequest: "Takip isteği alındı"
followRequestAccepted: "Takip isteği kabul edildi"
_actions: _actions:
reply: "yanıt" reply: "yanıt"
renote: "vazgeçme" renote: "vazgeçme"
_deck: _deck:
configureColumn: "Sütun seçenekleri"
_columns: _columns:
notifications: "Bildirim" notifications: "Bildirim"
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"
list: "Listeler" list: "Listeler"
mentions: "Bahsetmeler"

View file

@ -300,7 +300,6 @@ copyUrl: "Копіювати URL"
rename: "Перейменувати" rename: "Перейменувати"
avatar: "Аватар" avatar: "Аватар"
banner: "Банер" banner: "Банер"
nsfw: "NSFW"
whenServerDisconnected: "Коли зв’язок із сервером втрачено" whenServerDisconnected: "Коли зв’язок із сервером втрачено"
disconnectedFromServer: "Зв’язок із сервером було перервано" disconnectedFromServer: "Зв’язок із сервером було перервано"
reload: "Оновити" reload: "Оновити"
@ -1200,10 +1199,6 @@ _aboutMisskey:
donate: "Пожертвувати Misskey" donate: "Пожертвувати Misskey"
morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених тут. Дякуємо! 🥰" morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених тут. Дякуємо! 🥰"
patrons: "Підтримали" patrons: "Підтримали"
_nsfw:
respect: "Приховувати NSFW медіа"
ignore: "Не приховувати NSFW медіа"
force: "Приховувати всі медіа файли"
_instanceTicker: _instanceTicker:
none: "Не відображати" none: "Не відображати"
remote: "Відображати для віддалених користувачів" remote: "Відображати для віддалених користувачів"

View file

@ -303,7 +303,6 @@ copyUrl: "Sao chép URL"
rename: "Đổi tên" rename: "Đổi tên"
avatar: "Ảnh đại diện" avatar: "Ảnh đại diện"
banner: "Ảnh bìa" banner: "Ảnh bìa"
nsfw: "Nhạy cảm"
whenServerDisconnected: "Khi mất kết nối tới máy chủ" whenServerDisconnected: "Khi mất kết nối tới máy chủ"
disconnectedFromServer: "Mất kết nối tới máy chủ" disconnectedFromServer: "Mất kết nối tới máy chủ"
reload: "Tải lại" reload: "Tải lại"
@ -1212,10 +1211,6 @@ _aboutMisskey:
donate: "Ủng hộ Misskey" donate: "Ủng hộ Misskey"
morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác không được liệt kê ở đây. Cảm ơn! 🥰" morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác không được liệt kê ở đây. Cảm ơn! 🥰"
patrons: "Người ủng hộ" patrons: "Người ủng hộ"
_nsfw:
respect: "Ẩn nội dung NSFW"
ignore: "Hiện nội dung NSFW"
force: "Ẩn mọi media"
_instanceTicker: _instanceTicker:
none: "Không hiển thị" none: "Không hiển thị"
remote: "Hiện cho người dùng từ máy chủ khác" remote: "Hiện cho người dùng từ máy chủ khác"

View file

@ -49,11 +49,15 @@ delete: "删除"
deleteAndEdit: "删除并编辑" deleteAndEdit: "删除并编辑"
deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。" deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。"
addToList: "添加至列表" addToList: "添加至列表"
addToAntenna: "添加到天线"
sendMessage: "发送" sendMessage: "发送"
copyRSS: "复制RSS" copyRSS: "复制RSS"
copyUsername: "复制用户名" copyUsername: "复制用户名"
copyUserId: "复制用户 ID" copyUserId: "复制用户 ID"
copyNoteId: "复制帖子 ID" copyNoteId: "复制帖子 ID"
copyFileId: "复制文件ID"
copyFolderId: "复制文件夹ID"
copyProfileUrl: "复制配置文件URL"
searchUser: "搜索用户" searchUser: "搜索用户"
reply: "回复" reply: "回复"
loadMore: "查看更多" loadMore: "查看更多"
@ -136,8 +140,10 @@ unblockConfirm: "确定要解除拉黑吗?"
suspendConfirm: "要冻结吗?" suspendConfirm: "要冻结吗?"
unsuspendConfirm: "要解除冻结吗?" unsuspendConfirm: "要解除冻结吗?"
selectList: "选择列表" selectList: "选择列表"
editList: "编辑列表"
selectChannel: "选择频道" selectChannel: "选择频道"
selectAntenna: "选择天线" selectAntenna: "选择天线"
editAntenna: "编辑天线"
selectWidget: "选择小工具" selectWidget: "选择小工具"
editWidgets: "编辑部件" editWidgets: "编辑部件"
editWidgetsExit: "完成编辑" editWidgetsExit: "完成编辑"
@ -150,6 +156,8 @@ addEmoji: "添加表情符号"
settingGuide: "推荐配置" settingGuide: "推荐配置"
cacheRemoteFiles: "缓存远程文件" cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
flagAsBot: "这是一个机器人账号" flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让 Misskey 的内部系统将此账户识别为机器人。" flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让 Misskey 的内部系统将此账户识别为机器人。"
flagAsCat: "将这个账户设定为一只猫" flagAsCat: "将这个账户设定为一只猫"
@ -199,11 +207,11 @@ instanceInfo: "服务器信息"
statistics: "统计" statistics: "统计"
clearQueue: "清除队列" clearQueue: "清除队列"
clearQueueConfirmTitle: "确定清除队列?" clearQueueConfirmTitle: "确定清除队列?"
clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。" clearQueueConfirmText: "未送达的帖子将不会投递。 通常,您不需要这样做。"
clearCachedFiles: "清除缓存" clearCachedFiles: "清除缓存"
clearCachedFilesConfirm: "确定要清除缓存文件?" clearCachedFilesConfirm: "确定要清除缓存文件?"
blockedInstances: "被阻拦的服务器" blockedInstances: "被封锁的服务器"
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。" blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。"
muteAndBlock: "屏蔽/拉黑" muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户" mutedUsers: "已屏蔽用户"
blockedUsers: "已拉黑的用户" blockedUsers: "已拉黑的用户"
@ -211,7 +219,7 @@ noUsers: "无用户"
editProfile: "编辑资料" editProfile: "编辑资料"
noteDeleteConfirm: "要删除该帖子吗?" noteDeleteConfirm: "要删除该帖子吗?"
pinLimitExceeded: "无法置顶更多了" pinLimitExceeded: "无法置顶更多了"
intro: "Misskey的部署结束啦!填写管理员账号吧!" intro: "Misskey 的部署结束啦!创建管理员账号吧!"
done: "完成" done: "完成"
processing: "正在处理" processing: "正在处理"
preview: "预览" preview: "预览"
@ -305,13 +313,13 @@ unableToDelete: "无法删除"
inputNewFileName: "请输入新文件名" inputNewFileName: "请输入新文件名"
inputNewDescription: "请输入新标题" inputNewDescription: "请输入新标题"
inputNewFolderName: "请输入新文件夹名" inputNewFolderName: "请输入新文件夹名"
circularReferenceFolder: "目标文件夹是要移动的文件夹的子文件夹。" circularReferenceFolder: "目标文件夹是要移动的文件夹的子文件夹。"
hasChildFilesOrFolders: "此文件夹中有文件,无法删除。" hasChildFilesOrFolders: "此文件夹中有文件,无法删除。"
copyUrl: "复制链接" copyUrl: "复制链接"
rename: "重命名" rename: "重命名"
avatar: "头像" avatar: "头像"
banner: "横幅" banner: "横幅"
nsfw: "敏感内容" displayOfSensitiveMedia: "显示敏感媒体"
whenServerDisconnected: "与服务器连接中断时" whenServerDisconnected: "与服务器连接中断时"
disconnectedFromServer: "已和服务器断开连接" disconnectedFromServer: "已和服务器断开连接"
reload: "重新加载" reload: "重新加载"
@ -337,13 +345,13 @@ pages: "页面"
integration: "关联" integration: "关联"
connectService: "连接" connectService: "连接"
disconnectService: "断开连接" disconnectService: "断开连接"
enableLocalTimeline: "启用本地时间线功能" enableLocalTimeline: "启用本地时间线"
enableGlobalTimeline: "启用全局时间线" enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。" disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和协作者也可以继续使用。"
registration: "注册" registration: "注册"
enableRegistration: "允许任何人注册" enableRegistration: "允许任何人注册"
invite: "邀请" invite: "邀请"
driveCapacityPerLocalAccount: "每个用户的网盘空间" driveCapacityPerLocalAccount: "每个用户的网盘容量"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
inMb: "以兆字节(MegaByte)为单位" inMb: "以兆字节(MegaByte)为单位"
iconUrl: "图标 URL" iconUrl: "图标 URL"
@ -351,15 +359,15 @@ bannerUrl: "横幅URL"
backgroundImageUrl: "背景图 URL" backgroundImageUrl: "背景图 URL"
basicInfo: "基本信息" basicInfo: "基本信息"
pinnedUsers: "置顶用户" pinnedUsers: "置顶用户"
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。" pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。"
pinnedPages: "固定页面" pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。" pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,一行一个。"
pinnedClipId: "置顶的便签 ID" pinnedClipId: "置顶的便签 ID"
pinnedNotes: "已置顶的帖子" pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha" enableHcaptcha: "启用 hCaptcha"
hcaptchaSiteKey: "网站密钥" hcaptchaSiteKey: "网站密钥"
hcaptchaSecretKey: "密钥" hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥" recaptchaSiteKey: "网站密钥"
@ -368,18 +376,18 @@ turnstile: "Turnstile"
enableTurnstile: "启用 Turnstile" enableTurnstile: "启用 Turnstile"
turnstileSiteKey: "网站密钥" turnstileSiteKey: "网站密钥"
turnstileSecretKey: "Turnstile 密钥(SecretKey)" turnstileSecretKey: "Turnstile 密钥(SecretKey)"
avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其他验证方式吗?您可以按“取消”按钮,仍然保持启用多种验证方式。" avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其他验证方式吗?您可以按“取消”按钮,继续保持启用多种验证方式。"
antennas: "天线" antennas: "天线"
manageAntennas: "天线管理" manageAntennas: "天线管理"
name: "名称" name: "名称"
antennaSource: "接收来源" antennaSource: "接收来源"
antennaKeywords: "包含关键字" antennaKeywords: "包含关键字"
antennaExcludeKeywords: "排除关键字" antennaExcludeKeywords: "排除关键字"
antennaKeywordsDescription: "使用空格分隔会产生AND规范并且使用换行符分隔会产生OR规范" antennaKeywordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
notifyAntenna: "开启通知" notifyAntenna: "开启通知"
withFileAntenna: "仅带有附件的帖子" withFileAntenna: "仅带有附件的帖子"
enableServiceworker: "启用 ServiceWorker" enableServiceworker: "启用 ServiceWorker"
antennaUsersDescription: "指定用户名,用换行符分隔" antennaUsersDescription: "指定用户名,一行一个"
caseSensitive: "区分大小写" caseSensitive: "区分大小写"
withReplies: "包括回复" withReplies: "包括回复"
connectedTo: "您的账号已连到接以下第三方账号" connectedTo: "您的账号已连到接以下第三方账号"
@ -499,15 +507,15 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐"
objectStorage: "对象存储" objectStorage: "对象存储"
useObjectStorage: "使用对象存储" useObjectStorage: "使用对象存储"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "这里是用于参考的URL如果您正在使用CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”" objectStorageBaseUrlDesc: "这里是用于引用的 URL如果您正在使用 CDN 或反向代理,请指定其 URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”"
objectStorageBucket: "存储桶" objectStorageBucket: "存储桶"
objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。"
objectStoragePrefix: "前缀" objectStoragePrefix: "前缀"
objectStoragePrefixDesc: "文件将存储在此前缀的目录下。" objectStoragePrefixDesc: "文件将存储在此前缀的目录下。"
objectStorageEndpoint: "端点" objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "如果你使用AWS S3请留空。否则请根据你使用的服务商的说明来进行设置指定端点形式为“<host>”或“<host>:<port>”。" objectStorageEndpointDesc: "如果你使用 AWS S3 请留空。否则请根据你使用的服务商的说明来进行设置,指定 Endpoint 形式为“<host>”或“<host>:<port>”。"
objectStorageRegion: "可用区" objectStorageRegion: "可用区"
objectStorageRegionDesc: "指定一个可用区例如“xx-east-1”。 如果您的对象存储服务没有可用区概念请将其留空或填写“us-east-1”。" objectStorageRegionDesc: "指定一个可用区例如“xx-east-1”。 如果您的对象存储服务没有可用区概念请将其留空或填写“us-east-1”。如果引用 AWS 的配置文件或环境变量,则留空。"
objectStorageUseSSL: "使用 SSL" objectStorageUseSSL: "使用 SSL"
objectStorageUseSSLDesc: "如果不使用 https 进行 API 连接,请关闭。" objectStorageUseSSLDesc: "如果不使用 https 进行 API 连接,请关闭。"
objectStorageUseProxy: "使用代理" objectStorageUseProxy: "使用代理"
@ -542,7 +550,7 @@ sort: "排序"
ascendingOrder: "升序" ascendingOrder: "升序"
descendingOrder: "降序" descendingOrder: "降序"
scratchpad: "AiScript 控制台" scratchpad: "AiScript 控制台"
scratchpadDescription: "AiScript控制台为AiScript提供了实验环境。您可以编写代码与Misskey交互运行并查看结果。" scratchpadDescription: "AiScript 控制台为 AiScript 提供了实验环境。您可以编写代码与 Misskey 交互,运行并查看结果。"
output: "输出" output: "输出"
script: "脚本" script: "脚本"
disablePagesScript: "禁用页面脚本" disablePagesScript: "禁用页面脚本"
@ -600,7 +608,7 @@ permission: "权限"
enableAll: "启用全部" enableAll: "启用全部"
disableAll: "禁用全部" disableAll: "禁用全部"
tokenRequested: "允许访问账户" tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" pluginTokenRequestedDescription: "此插件将能够拥有这里设置的权限"
notificationType: "通知类型" notificationType: "通知类型"
edit: "编辑" edit: "编辑"
emailServer: "邮件服务器" emailServer: "邮件服务器"
@ -652,7 +660,7 @@ reporter: "举报者"
reporteeOrigin: "举报来源" reporteeOrigin: "举报来源"
reporterOrigin: "举报者来源" reporterOrigin: "举报者来源"
forwardReport: "将该举报信息转发给远程服务器" forwardReport: "将该举报信息转发给远程服务器"
forwardReportIsAnonymous: "勾选则在远程服务器上显示的举报者是匿名的系统账号,而不是您的账号。" forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。"
send: "发送" send: "发送"
abuseMarkAsResolved: "处理完毕" abuseMarkAsResolved: "处理完毕"
openInNewTab: "在新标签页中打开" openInNewTab: "在新标签页中打开"
@ -718,14 +726,14 @@ center: "中央"
wide: "宽" wide: "宽"
narrow: "窄" narrow: "窄"
reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?" reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?"
needReloadToApply: "重后应用才会生效。" needReloadToApply: "重新载入后应用才会生效。"
showTitlebar: "显示标题栏" showTitlebar: "显示标题栏"
clearCache: "清除缓存" clearCache: "清除缓存"
onlineUsersCount: "{n} 人在线" onlineUsersCount: "{n} 人在线"
nUsers: "{n} 用户" nUsers: "{n} 用户"
nNotes: "{n} 帖子" nNotes: "{n} 帖子"
sendErrorReports: "发送错误报告" sendErrorReports: "发送错误报告"
sendErrorReportsDescription: "启用后,如果出现问题,可以与Misskey共享详细的错误信息从而帮助提高软件的质量。" sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。错误信息包括操作系统版本、浏览器类型、行为历史记录等。"
myTheme: "我的主题" myTheme: "我的主题"
backgroundColor: "背景" backgroundColor: "背景"
accentColor: "强调色" accentColor: "强调色"
@ -789,7 +797,7 @@ administration: "管理"
accounts: "账户" accounts: "账户"
switch: "切换" switch: "切换"
noMaintainerInformationWarning: "管理人员信息未设置。" noMaintainerInformationWarning: "管理人员信息未设置。"
noBotProtectionWarning: "Bot保护未设置。" noBotProtectionWarning: "Bot 防御未设置。"
configure: "设置" configure: "设置"
postToGallery: "发送到图库" postToGallery: "发送到图库"
postToHashtag: "投稿到这个标签" postToHashtag: "投稿到这个标签"
@ -868,7 +876,7 @@ numberOfColumn: "列数"
searchByGoogle: "Google" searchByGoogle: "Google"
instanceDefaultLightTheme: "服务器默认浅色主题" instanceDefaultLightTheme: "服务器默认浅色主题"
instanceDefaultDarkTheme: "服务器默认深色主题" instanceDefaultDarkTheme: "服务器默认深色主题"
instanceDefaultThemeDescription: "以对象格式入主题代码" instanceDefaultThemeDescription: "以对象格式入主题代码"
mutePeriod: "屏蔽期限" mutePeriod: "屏蔽期限"
period: "截止时间" period: "截止时间"
indefinitely: "永久" indefinitely: "永久"
@ -879,8 +887,8 @@ oneWeek: "1周"
oneMonth: "1 个月" oneMonth: "1 个月"
reflectMayTakeTime: "可能需要一些时间才能体现出效果。" reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
failedToFetchAccountInformation: "获取账户信息失败" failedToFetchAccountInformation: "获取账户信息失败"
rateLimitExceeded: "已超速率限制" rateLimitExceeded: "已超速率限制"
cropImage: "裁图像" cropImage: "图像"
cropImageAsk: "是否要裁剪图像?" cropImageAsk: "是否要裁剪图像?"
cropYes: "去裁剪" cropYes: "去裁剪"
cropNo: "就这样吧!" cropNo: "就这样吧!"
@ -891,7 +899,7 @@ noEmailServerWarning: "电子邮件服务器未设置。"
thereIsUnresolvedAbuseReportWarning: "有未解决的报告" thereIsUnresolvedAbuseReportWarning: "有未解决的报告"
recommended: "推荐" recommended: "推荐"
check: "检查" check: "检查"
driveCapOverrideLabel: "變更此用戶的雲端硬碟容量上限" driveCapOverrideLabel: "更改此用户的网盘容量上限"
driveCapOverrideCaption: "设定为 0 以下则会解除此限制。" driveCapOverrideCaption: "设定为 0 以下则会解除此限制。"
requireAdminForView: "需要使用管理员账户登录才能查看。" requireAdminForView: "需要使用管理员账户登录才能查看。"
isSystemAccount: "该账号由系统自动创建和管理。" isSystemAccount: "该账号由系统自动创建和管理。"
@ -899,7 +907,7 @@ typeToConfirm: "输入 {x} 以确认操作。"
deleteAccount: "删除账户" deleteAccount: "删除账户"
document: "文档" document: "文档"
numberOfPageCache: "缓存页数" numberOfPageCache: "缓存页数"
numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。" numberOfPageCacheDescription: "设置较高的值会更方便用户,但设备的负载和内存使用量会增加。"
logoutConfirm: "是否确认登出?" logoutConfirm: "是否确认登出?"
lastActiveDate: "最后活跃时间" lastActiveDate: "最后活跃时间"
statusbar: "状态栏" statusbar: "状态栏"
@ -921,7 +929,7 @@ cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
cannotUploadBecauseExceedsFileSizeLimit: "无法上传文件,超过文件大小限制。" cannotUploadBecauseExceedsFileSizeLimit: "无法上传文件,超过文件大小限制。"
beta: "测试" beta: "测试"
enableAutoSensitive: "自动 NSFW 识别" enableAutoSensitive: "自动 NSFW 识别"
enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据服务器自动设置。" enableAutoSensitiveDescription: "使用机器学习在可用时自动使用 NSFW 标记来标记媒体。即使您关闭此功能,根据服务器的不同,它仍然可能会自动设置。"
activeEmailValidationDescription: "开启用户的电子邮件地址验证,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。" activeEmailValidationDescription: "开启用户的电子邮件地址验证,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。"
navbar: "导航栏" navbar: "导航栏"
shuffle: "随机" shuffle: "随机"
@ -966,8 +974,8 @@ invalidParamError: "参数错误"
invalidParamErrorDescription: "请求参数出现问题。通常是因为 bug 造成的,但也可能是输入文字数量过多之类的原因。" invalidParamErrorDescription: "请求参数出现问题。通常是因为 bug 造成的,但也可能是输入文字数量过多之类的原因。"
permissionDeniedError: "操作被拒绝" permissionDeniedError: "操作被拒绝"
permissionDeniedErrorDescription: "本账户没有执行该操作的权限。" permissionDeniedErrorDescription: "本账户没有执行该操作的权限。"
preset: "預設值" preset: "预设值"
selectFromPresets: "從預設值中選擇" selectFromPresets: "从预设值中选择"
achievements: "成就" achievements: "成就"
gotInvalidResponseError: "服务器无应答" gotInvalidResponseError: "服务器无应答"
gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。" gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。"
@ -998,7 +1006,7 @@ rolesAssignedToMe: "指派给自己的角色"
resetPasswordConfirm: "确定重置密码?" resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词" sensitiveWords: "敏感词"
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
sensitiveWordsDescription2: "用空格分割关键词作为AND格式用斜线包裹关键字来构成正则表达式。" sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
notesSearchNotAvailable: "帖子检索不可用" notesSearchNotAvailable: "帖子检索不可用"
license: "许可信息" license: "许可信息"
unfavoriteConfirm: "确定要取消收藏吗?" unfavoriteConfirm: "确定要取消收藏吗?"
@ -1062,6 +1070,27 @@ later: "一会再说"
goToMisskey: "去往 Misskey" goToMisskey: "去往 Misskey"
additionalEmojiDictionary: "表情符号追加字典" additionalEmojiDictionary: "表情符号追加字典"
installed: "已安装" installed: "已安装"
branding: "品牌"
enableServerMachineStats: "公开服务器硬件统计信息"
enableIdenticonGeneration: "启用生成用户 Identicon"
turnOffToImprovePerformance: "关闭该选项可以提高性能。"
createInviteCode: "发行邀请码"
createWithOptions: "使用选项来创建"
createCount: "发行数"
inviteCodeCreated: "已创建邀请码"
inviteLimitExceeded: "可供发行的邀请码已达上限。"
createLimitRemaining: "可供发行的邀请码:剩余{limit}个"
inviteLimitResetCycle: "可以在{time}内发行最多{limit}个邀请码。"
expirationDate: "有效日期"
noExpirationDate: "不设置有效日期"
inviteCodeUsedAt: "邀请码被使用的日期和时间"
registeredUserUsingInviteCode: "使用了邀请码的用户"
waitingForMailAuth: "等待验证电子邮件"
inviteCodeCreator: "发行邀请码的用户"
usedAt: "使用时间"
unused: "未使用"
used: "已使用"
expired: "已过期"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "账户创建完成了!" accountCreated: "账户创建完成了!"
letsStartAccountSetup: "来进行帐户的初始设置吧。" letsStartAccountSetup: "来进行帐户的初始设置吧。"
@ -1071,7 +1100,7 @@ _initialAccountSetting:
theseSettingsCanEditLater: "也可以在稍后修改这里的设置。" theseSettingsCanEditLater: "也可以在稍后修改这里的设置。"
youCanEditMoreSettingsInSettingsPageLater: "还可以在「设置」页面进行其它各种设置,稍后就来确认一下看看吧。" youCanEditMoreSettingsInSettingsPageLater: "还可以在「设置」页面进行其它各种设置,稍后就来确认一下看看吧。"
followUsers: "为了建立属于你自己的时间线,试着去关注你感兴趣的用户吧。" followUsers: "为了建立属于你自己的时间线,试着去关注你感兴趣的用户吧。"
pushNotificationDescription: "启用推送通知的话,就可以在设备上受到来自{name}的通知了。" pushNotificationDescription: "启用推送通知的话,就可以在设备上接收来自 {name} 的通知了。"
initialAccountSettingCompleted: "初始设定已经完成了!" initialAccountSettingCompleted: "初始设定已经完成了!"
haveFun: "希望 {name} 在这里玩得开心!" haveFun: "希望 {name} 在这里玩得开心!"
ifYouNeedLearnMore: "关于 {name}(Misskey) 的使用方法,详见 {link}。" ifYouNeedLearnMore: "关于 {name}(Misskey) 的使用方法,详见 {link}。"
@ -1083,11 +1112,11 @@ _accountMigration:
moveFrom: "从别的账号迁移到此账户" moveFrom: "从别的账号迁移到此账户"
moveFromSub: "为另一个账户建立别名" moveFromSub: "为另一个账户建立别名"
moveFromLabel: "迁移前的账户" moveFromLabel: "迁移前的账户"
moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com" moveFromDescription: "如果迁移时需要继承其他账户的关注者,你需要创建一个别名。此操作需要在迁移前完成!\n请像这样输入要迁移的账户@username@server.example.com\n如果要删除请将输入字段留空并保存不推荐"
moveTo: "把这个账户迁移到新的账户" moveTo: "把这个账户迁移到新的账户"
moveToLabel: "迁移后的账户" moveToLabel: "迁移后的账户"
moveCannotBeUndone: "一旦迁移账户,就无法撤销。" moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com" moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表并在迁移后立即在目标帐户上执行导入。\n屏蔽列表也是如此因此您必须手动迁移它。\n此描述适用于该服务器Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon的行为可能有所不同。"
moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后请像这样输入目标账户@username@server.example.com" moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后请像这样输入目标账户@username@server.example.com"
startMigration: "迁移" startMigration: "迁移"
migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时请确认迁移后的账户已创造别名。" migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时请确认迁移后的账户已创造别名。"
@ -1149,53 +1178,53 @@ _achievements:
flavor: "真的有那么多可以写的东西吗?" flavor: "真的有那么多可以写的东西吗?"
_login3: _login3:
title: "初学者 I" title: "初学者 I"
description: "连续登录3天" description: "累计登录 3 天"
flavor: "今天开始我就是 Misskist" flavor: "今天开始我就是 Misskist"
_login7: _login7:
title: "初学者 II" title: "初学者 II"
description: "连续登录7天" description: "累计登录 7 天"
flavor: "您开始习惯了吗?" flavor: "您开始习惯了吗?"
_login15: _login15:
title: "初学者 III" title: "初学者 III"
description: "连续登录15天" description: "累计登录 15 天"
_login30: _login30:
title: "Misskist " title: "Misskist "
description: "连续登录30天" description: "累计登录 30 天"
_login60: _login60:
title: "Misskist Ⅱ" title: "Misskist Ⅱ"
description: "连续登录60天" description: "累计登录 60 天"
_login100: _login100:
title: "Misskist Ⅲ" title: "Misskist Ⅲ"
description: "总登入100天" description: "累计登入 100 天"
flavor: "那个用户,是 Misskist 喔" flavor: "那个用户,是 Misskist 喔"
_login200: _login200:
title: "定期联系Ⅰ" title: "定期联系Ⅰ"
description: "总登录天数200天" description: "累计登录 200 天"
_login300: _login300:
title: "定期联系Ⅱ" title: "定期联系Ⅱ"
description: "总登录天数300天" description: "累计登录 300 天"
_login400: _login400:
title: "定期联系Ⅲ" title: "定期联系Ⅲ"
description: "总登录天数400天" description: "累计登录 400 天"
_login500: _login500:
title: "老熟人Ⅰ" title: "老熟人Ⅰ"
description: "总登录天数500天" description: "累计登录 500 天"
flavor: "诸君,我喜欢贴文" flavor: "诸君,我喜欢贴文"
_login600: _login600:
title: "老熟人Ⅱ" title: "老熟人Ⅱ"
description: "总登录天数600天" description: "累计登录 600 天"
_login700: _login700:
title: "老熟人Ⅲ" title: "老熟人Ⅲ"
description: "总登录天数700天" description: "累计登录 700 天"
_login800: _login800:
title: "帖子大师 " title: "帖子大师 "
description: "总登录天数800天" description: "累计登录 800 天"
_login900: _login900:
title: "帖子大师 Ⅱ" title: "帖子大师 Ⅱ"
description: "总登录天数900天" description: "累计登录 900 天"
_login1000: _login1000:
title: "帖子大师 Ⅲ" title: "帖子大师 Ⅲ"
description: "总登录天数1000天" description: "累计登录 1000 天"
flavor: "感谢您使用 Misskey" flavor: "感谢您使用 Misskey"
_noteClipped1: _noteClipped1:
title: "忍不住要收藏到便签" title: "忍不住要收藏到便签"
@ -1223,7 +1252,7 @@ _achievements:
title: "我的朋友很多" title: "我的朋友很多"
description: "关注超过 50 人" description: "关注超过 50 人"
_following100: _following100:
title: "我的朋友很多" title: "胜友如云"
description: "关注超过 100 人" description: "关注超过 100 人"
_following300: _following300:
title: "朋友成群" title: "朋友成群"
@ -1372,6 +1401,9 @@ _role:
ltlAvailable: "查看本地时间线" ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖" canPublicNote: "允许公开发帖"
canInvite: "发放服务器邀请码" canInvite: "发放服务器邀请码"
inviteLimit: "可发行邀请码的数量"
inviteLimitCycle: "邀请码的发行间隔"
inviteExpirationTime: "邀请码的有效日期"
canManageCustomEmojis: "管理自定义表情符号" canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量" driveCapacity: "网盘容量"
alwaysMarkNsfw: "总是将文件标记为 NSFW" alwaysMarkNsfw: "总是将文件标记为 NSFW"
@ -1402,7 +1434,7 @@ _role:
or: "符合以下任一条件" or: "符合以下任一条件"
not: "不符合以下任何条件" not: "不符合以下任何条件"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" description: "使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度" sensitivity: "检测敏感度"
sensitivityDescription: "敏感度较低,则误检(假阳性)会减少;敏感度较高,则漏检(假阴性)会减少。" sensitivityDescription: "敏感度较低,则误检(假阳性)会减少;敏感度较高,则漏检(假阴性)会减少。"
setSensitiveFlagAutomatically: "自动设置 NSFW 标签" setSensitiveFlagAutomatically: "自动设置 NSFW 标签"
@ -1434,9 +1466,10 @@ _ad:
back: "返回" back: "返回"
reduceFrequencyOfThisAd: "减少此广告的频率" reduceFrequencyOfThisAd: "减少此广告的频率"
hide: "不显示" hide: "不显示"
timezoneinfo: "星期几是由服务器的时区所指定的。"
_forgotPassword: _forgotPassword:
enterEmail: "请输入您验证账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。" enterEmail: "请输入您设置的电子邮箱地址,密码重置链接将发送至该邮箱上。"
ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系管理员。" ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。"
contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。" contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。"
_gallery: _gallery:
my: "我的图库" my: "我的图库"
@ -1483,12 +1516,12 @@ _aboutMisskey:
source: "源代码" source: "源代码"
translation: "翻译 Misskey" translation: "翻译 Misskey"
donate: "赞助 Misskey" donate: "赞助 Misskey"
morePatrons: "还有很多其的人也在支持我们,非常感谢🥰" morePatrons: "还有很多其的人也在支持我们,非常感谢🥰"
patrons: "支持者" patrons: "支持者"
_nsfw: _displayOfSensitiveMedia:
respect: "隐藏敏感内容" respect: "隐藏敏感媒体"
ignore: "不隐藏敏感内容" ignore: "显示敏感媒体"
force: "总是隐藏内容" force: "隐藏所有内容"
_instanceTicker: _instanceTicker:
none: "不显示" none: "不显示"
remote: "仅远程用户" remote: "仅远程用户"
@ -1516,16 +1549,16 @@ _menuDisplay:
hide: "隐藏" hide: "隐藏"
_wordMute: _wordMute:
muteWords: "禁用词" muteWords: "禁用词"
muteWordsDescription: "使用空格分隔表示AND逻辑使用换行符分隔表示OR逻辑。" muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。" muteWordsDescription2: "正则表达式用斜线包裹"
softDescription: "隐藏时间线中指定条件的帖子。" softDescription: "隐藏时间线中指定条件的帖子。"
hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。" hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。"
soft: "软屏蔽" soft: "软屏蔽"
hard: "硬屏蔽" hard: "硬屏蔽"
mutedNotes: "被屏蔽的帖子" mutedNotes: "被屏蔽的帖子"
_instanceMute: _instanceMute:
instanceMuteDescription: "屏蔽配置服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。"
instanceMuteDescription2: "设置时用换行符来分隔" instanceMuteDescription2: "一行一个"
title: "隐藏服务器已设置的帖子。" title: "隐藏服务器已设置的帖子。"
heading: "屏蔽服务器" heading: "屏蔽服务器"
_theme: _theme:
@ -1642,7 +1675,7 @@ _2fa:
passwordToTOTP: "请输入您的密码" passwordToTOTP: "请输入您的密码"
step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。" step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。"
step2: "然后,扫描屏幕上显示的二维码。" step2: "然后,扫描屏幕上显示的二维码。"
step2Click: "通过点击QR码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册"
step2Url: "在桌面应用程序中输入以下 URL" step2Url: "在桌面应用程序中输入以下 URL"
step3Title: "输入验证码" step3Title: "输入验证码"
step3: "输入您的应用提供的动态口令以完成设置。" step3: "输入您的应用提供的动态口令以完成设置。"
@ -1963,6 +1996,7 @@ _deck:
introduction: "将各列进行组合以创建您自己的界面!" introduction: "将各列进行组合以创建您自己的界面!"
introduction2: "您可以随时通过屏幕右侧的 + 来添加列" introduction2: "您可以随时通过屏幕右侧的 + 来添加列"
widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具" widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具"
useSimpleUiForNonRootPages: "用简易UI表示非根页面"
_columns: _columns:
main: "主列" main: "主列"
widgets: "小工具" widgets: "小工具"

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.13.2", "version": "13.14.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@8.6.0", "packageManager": "pnpm@8.6.9",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -25,7 +25,7 @@
"migrateandstart": "pnpm migrate && pnpm start", "migrateandstart": "pnpm migrate && pnpm start",
"gulp": "pnpm exec gulp build", "gulp": "pnpm exec gulp build",
"watch": "pnpm dev", "watch": "pnpm dev",
"dev": "node ./scripts/dev.js", "dev": "node ./scripts/dev.mjs",
"lint": "pnpm -r lint", "lint": "pnpm -r lint",
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run", "cy:run": "pnpm cypress run",
@ -44,23 +44,23 @@
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"dependencies": { "dependencies": {
"execa": "5.1.1", "execa": "7.1.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "5.1.3" "typescript": "5.1.6"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.59.8", "@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.59.8", "@typescript-eslint/parser": "5.61.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.13.0", "cypress": "12.17.1",
"eslint": "8.41.0", "eslint": "8.45.0",
"start-server-and-test": "2.0.0" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -17,7 +17,7 @@
"paths": { "paths": {
"@/*": ["*"] "@/*": ["*"]
}, },
"target": "es2021" "target": "es2022"
}, },
"minify": false "minify": false
} }

View file

@ -2,14 +2,7 @@ import Redis from 'ioredis';
import { loadConfig } from './built/config.js'; import { loadConfig } from './built/config.js';
const config = loadConfig(); const config = loadConfig();
const redis = new Redis({ const redis = new Redis(config.redis);
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
redis.on('connect', () => redis.disconnect()); redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => { redis.on('error', (e) => {

View file

@ -0,0 +1,9 @@
export class ad1677054292210 {
name = 'ad1677054292210';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`);
}
}

View file

@ -0,0 +1,13 @@
export class AddMetaOptions1688280713783 {
name = 'AddMetaOptions1688280713783'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`);
}
}

View file

@ -0,0 +1,25 @@
export class RefactorInviteSystem1688720440658 {
name = 'RefactorInviteSystem1688720440658'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "pendingUserId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "createdById" character varying(32)`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedById" character varying(32)`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189" UNIQUE ("usedById")`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_beba993576db0261a15364ea96e" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189" FOREIGN KEY ("usedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_beba993576db0261a15364ea96e"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedById"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "createdById"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "pendingUserId"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "expiresAt"`);
}
}

View file

@ -0,0 +1,13 @@
export class AddIndexToRelations1688880985544 {
name = 'AddIndexToRelations1688880985544'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_beba993576db0261a15364ea96" ON "registration_ticket" ("createdById") `);
await queryRunner.query(`CREATE INDEX "IDX_b6f93f2f30bdbb9a5ebdc7c718" ON "registration_ticket" ("usedById") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_b6f93f2f30bdbb9a5ebdc7c718"`);
await queryRunner.query(`DROP INDEX "public"."IDX_beba993576db0261a15364ea96"`);
}
}

View file

@ -0,0 +1,11 @@
export class NsfwCache1689102832143 {
name = 'NsfwCache1689102832143'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "cacheRemoteSensitiveFiles" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "cacheRemoteSensitiveFiles"`);
}
}

View file

@ -3,6 +3,9 @@
"main": "./index.js", "main": "./index.js",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"node": ">=18.16.0"
},
"scripts": { "scripts": {
"start": "node ./built/index.js", "start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js",
@ -51,63 +54,62 @@
"utf-8-validate": "^6.0.3" "utf-8-validate": "^6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.321.1", "@aws-sdk/client-s3": "3.367.0",
"@aws-sdk/lib-storage": "3.321.1", "@aws-sdk/lib-storage": "3.367.0",
"@aws-sdk/node-http-handler": "3.321.1", "@aws-sdk/node-http-handler": "3.360.0",
"@bull-board/api": "5.2.0", "@bull-board/api": "5.6.1",
"@bull-board/fastify": "5.2.0", "@bull-board/fastify": "5.6.1",
"@bull-board/ui": "5.2.0", "@bull-board/ui": "5.6.1",
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.2.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.3.0", "@fastify/cors": "8.3.0",
"@fastify/http-proxy": "9.1.0", "@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.6.0", "@fastify/multipart": "7.7.1",
"@fastify/static": "6.10.2", "@fastify/static": "6.10.2",
"@fastify/view": "7.4.1", "@fastify/view": "8.0.0",
"@nestjs/common": "9.4.2", "@nestjs/common": "10.1.0",
"@nestjs/core": "9.4.2", "@nestjs/core": "10.1.0",
"@nestjs/testing": "9.4.2", "@nestjs/testing": "10.1.0",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.2.0", "@sinonjs/fake-timers": "10.3.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.61", "@swc/core": "1.3.70",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
"autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bullmq": "3.15.0", "bullmq": "4.4.0",
"cacheable-lookup": "6.1.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.0", "cbor": "9.0.0",
"chalk": "5.2.0", "chalk": "5.3.0",
"chalk-template": "0.4.0", "chalk-template": "1.1.0",
"chokidar": "3.5.3", "chokidar": "3.5.3",
"cli-highlight": "2.1.11", "cli-highlight": "2.1.11",
"color-convert": "2.0.1", "color-convert": "2.0.1",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "fastify": "4.20.0",
"fastify": "4.17.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.4.0", "file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "12.6.0", "got": "13.0.0",
"happy-dom": "9.20.3", "happy-dom": "10.0.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"is-svg": "4.3.2", "ipaddr.js": "2.1.0",
"is-svg": "5.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "22.1.0", "jsdom": "22.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.2.0", "jsonld": "8.2.0",
"jsrsasign": "10.8.6", "jsrsasign": "10.8.6",
"meilisearch": "0.32.5", "meilisearch": "0.33.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
@ -118,10 +120,9 @@
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.1.2", "otpauth": "9.1.3",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.0", "pg": "8.11.1",
"private-ip": "3.0.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
@ -130,41 +131,36 @@
"qrcode": "1.5.3", "qrcode": "1.5.3",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.19.0", "re2": "1.19.1",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rename": "1.0.4", "rename": "1.0.4",
"rndstr": "1.0.0",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"s-age": "1.1.2", "sanitize-html": "2.11.0",
"sanitize-html": "2.10.0", "semver": "7.5.4",
"seedrandom": "3.0.5", "sharp": "0.32.3",
"semver": "7.5.1",
"sharp": "0.32.1",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.9", "slacc": "0.0.9",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.17.16", "systeminformation": "5.18.7",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.6", "tsc-alias": "1.8.7",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.16", "typeorm": "0.3.17",
"typescript": "5.1.3", "typescript": "5.1.6",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.14",
"uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.1", "web-push": "3.6.3",
"ws": "8.13.0", "ws": "8.13.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.5.0", "@jest/globals": "29.6.1",
"@swc/jest": "0.2.26", "@swc/jest": "0.2.26",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.2", "@types/archiver": "5.3.2",
@ -172,25 +168,24 @@
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "2.0.0", "@types/color-convert": "2.0.0",
"@types/content-disposition": "0.5.5", "@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/jest": "29.5.2", "@types/jest": "29.5.3",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1", "@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.9",
"@types/jsrsasign": "10.5.8", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "20.2.5", "@types/ms": "^0.7.31",
"@types/node": "20.4.2",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8", "@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
"@types/pg": "8.10.1", "@types/pg": "8.10.2",
"@types/pug": "2.0.6", "@types/pug": "2.0.6",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/qrcode": "1.5.0", "@types/qrcode": "1.5.1",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"@types/semver": "7.5.0", "@types/semver": "7.5.0",
@ -198,20 +193,17 @@
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/unzipper": "0.10.6",
"@types/uuid": "9.0.1",
"@types/vary": "1.1.0", "@types/vary": "1.1.0",
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/ws": "8.5.5",
"@types/ws": "8.5.4", "@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/eslint-plugin": "5.59.8", "@typescript-eslint/parser": "5.61.0",
"@typescript-eslint/parser": "5.59.8", "aws-sdk-client-mock": "3.0.0",
"aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.41.0", "eslint": "8.45.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "7.1.1",
"jest": "29.5.0", "jest": "29.6.1",
"jest-mock": "29.5.0" "jest-mock": "29.6.1"
} }
} }

View file

@ -41,14 +41,7 @@ const $meilisearch: Provider = {
const $redis: Provider = { const $redis: Provider = {
provide: DI.redis, provide: DI.redis,
useFactory: (config: Config) => { useFactory: (config: Config) => {
return new Redis.Redis({ return new Redis.Redis(config.redis);
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
}, },
inject: [DI.config], inject: [DI.config],
}; };
@ -56,14 +49,7 @@ const $redis: Provider = {
const $redisForPub: Provider = { const $redisForPub: Provider = {
provide: DI.redisForPub, provide: DI.redisForPub,
useFactory: (config: Config) => { useFactory: (config: Config) => {
const redis = new Redis.Redis({ const redis = new Redis.Redis(config.redisForPubsub);
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
return redis; return redis;
}, },
inject: [DI.config], inject: [DI.config],
@ -72,14 +58,7 @@ const $redisForPub: Provider = {
const $redisForSub: Provider = { const $redisForSub: Provider = {
provide: DI.redisForSub, provide: DI.redisForSub,
useFactory: (config: Config) => { useFactory: (config: Config) => {
const redis = new Redis.Redis({ const redis = new Redis.Redis(config.redisForPubsub);
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
redis.subscribe(config.host); redis.subscribe(config.host);
return redis; return redis;
}, },

View file

@ -31,7 +31,7 @@ function greet() {
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ ')); console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |')); console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |')); console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |'));
console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substr(v.length))); console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substring(v.length)));
//#endregion //#endregion
console.log(' Misskey is an open-source decentralized microblogging platform.'); console.log(' Misskey is an open-source decentralized microblogging platform.');
@ -78,7 +78,7 @@ export async function masterMain() {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimit);
} }
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true);
} }
function showEnvironment(): void { function showEnvironment(): void {
@ -96,12 +96,6 @@ function showNodejsVersion(): void {
const nodejsLogger = bootLogger.createSubLogger('nodejs'); const nodejsLogger = bootLogger.createSubLogger('nodejs');
nodejsLogger.info(`Version ${process.version} detected.`); nodejsLogger.info(`Version ${process.version} detected.`);
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim();
if (semver.lt(process.version, minVersion)) {
nodejsLogger.error(`At least Node.js ${minVersion} required!`);
process.exit(1);
}
} }
function loadConfigBoot(): Config { function loadConfigBoot(): Config {

View file

@ -6,6 +6,16 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path'; import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial<RedisOptions> & {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
/** /**
* *
@ -14,7 +24,9 @@ export type Source = {
repository_url?: string; repository_url?: string;
feedback_url?: string; feedback_url?: string;
url: string; url: string;
port: number; port?: number;
socket?: string;
chmodSocket?: string;
disableHsts?: boolean; disableHsts?: boolean;
db: { db: {
host: string; host: string;
@ -33,30 +45,9 @@ export type Source = {
user: string; user: string;
pass: string; pass: string;
}[]; }[];
redis: { redis: RedisOptionsSource;
host: string; redisForPubsub?: RedisOptionsSource;
port: number; redisForJobQueue?: RedisOptionsSource;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
redisForPubsub?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
redisForJobQueue?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
meilisearch?: { meilisearch?: {
host: string; host: string;
port: string; port: string;
@ -119,8 +110,9 @@ export type Mixin = {
mediaProxy: string; mediaProxy: string;
externalMediaProxyEnabled: boolean; externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null; videoThumbnailGenerator: string | null;
redisForPubsub: NonNullable<Source['redisForPubsub']>; redis: RedisOptions & RedisOptionsSource;
redisForJobQueue: NonNullable<Source['redisForJobQueue']>; redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
}; };
export type Config = Source & Mixin; export type Config = Source & Mixin;
@ -182,9 +174,9 @@ export function loadConfig() {
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
: null; : null;
if (!config.redis.prefix) config.redis.prefix = mixin.host; mixin.redis = convertRedisOptions(config.redis, mixin.host);
if (config.redisForPubsub == null) config.redisForPubsub = config.redis; mixin.redisForPubsub = config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, mixin.host) : mixin.redis;
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis; mixin.redisForJobQueue = config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, mixin.host) : mixin.redis;
return Object.assign(config, mixin); return Object.assign(config, mixin);
} }
@ -196,3 +188,14 @@ function tryCreateUrl(url: string) {
throw new Error(`url="${url}" is not a valid URL.`); throw new Error(`url="${url}" is not a valid URL.`);
} }
} }
function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOptions & RedisOptionsSource {
return {
...options,
password: options.pass,
prefix: options.prefix ?? host,
family: options.family == null ? 0 : options.family,
keyPrefix: `${options.prefix ?? host}:`,
db: options.db ?? 0,
};
}

View file

@ -4,10 +4,9 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import type { User } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';

View file

@ -32,11 +32,6 @@ export class AppLockService {
return this.lock(`ap-object:${uri}`, timeout); return this.lock(`ap-object:${uri}`, timeout);
} }
@bindThis
public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> {
return this.lock(`instance:${host}`, timeout);
}
@bindThis @bindThis
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> { public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
return this.lock(`chart-insert:${lockKey}`, timeout); return this.lock(`chart-insert:${lockKey}`, timeout);

View file

@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class CacheService implements OnApplicationShutdown { export class CacheService implements OnApplicationShutdown {
public userByIdCache: MemoryKVCache<User>; public userByIdCache: MemoryKVCache<User, User | string>;
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>; public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null, string | null>;
public localUserByIdCache: MemoryKVCache<LocalUser>; public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null>; public uriPersonCache: MemoryKVCache<User | null, string | null>;
public userProfileCache: RedisKVCache<UserProfile>; public userProfileCache: RedisKVCache<UserProfile>;
public userMutingsCache: RedisKVCache<Set<string>>; public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>; public userBlockingCache: RedisKVCache<Set<string>>;
@ -55,10 +55,41 @@ export class CacheService implements OnApplicationShutdown {
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new MemoryKVCache<User>(Infinity); const localUserByIdCache = new MemoryKVCache<LocalUser>(1000 * 60 * 60 * 6 /* 6h */);
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity); this.localUserByIdCache = localUserByIdCache;
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity); // ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する
const userByIdCache = new MemoryKVCache<User, User | string>(1000 * 60 * 60 * 6 /* 6h */, {
toMapConverter: user => {
if (user.host === null) {
localUserByIdCache.set(user.id, user as LocalUser);
return user.id;
}
return user;
},
fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId,
});
this.userByIdCache = userByIdCache;
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null, string | null>(Infinity, {
toMapConverter: user => {
if (user === null) return null;
localUserByIdCache.set(user.id, user);
return user.id;
},
fromMapConverter: id => id === null ? null : localUserByIdCache.get(id),
});
this.uriPersonCache = new MemoryKVCache<User | null, string | null>(Infinity, {
toMapConverter: user => {
if (user === null) return null;
userByIdCache.set(user.id, user);
return user.id;
},
fromMapConverter: id => id === null ? null : userByIdCache.get(id),
});
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', { this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m lifetime: 1000 * 60 * 30, // 30m
@ -131,7 +162,7 @@ export class CacheService implements OnApplicationShutdown {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }); const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user); this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) { for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) { if (v.value === user.id) {
this.uriPersonCache.set(k, user); this.uriPersonCache.set(k, user);
} }
} }

View file

@ -81,6 +81,7 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
import { HashtagEntityService } from './entities/HashtagEntityService.js'; import { HashtagEntityService } from './entities/HashtagEntityService.js';
import { InstanceEntityService } from './entities/InstanceEntityService.js'; import { InstanceEntityService } from './entities/InstanceEntityService.js';
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
import { MutingEntityService } from './entities/MutingEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js';
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js'; import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
@ -205,6 +206,7 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService }; const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService }; const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService }; const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService }; const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
@ -329,6 +331,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
GalleryPostEntityService, GalleryPostEntityService,
HashtagEntityService, HashtagEntityService,
InstanceEntityService, InstanceEntityService,
InviteCodeEntityService,
ModerationLogEntityService, ModerationLogEntityService,
MutingEntityService, MutingEntityService,
RenoteMutingEntityService, RenoteMutingEntityService,
@ -448,6 +451,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$GalleryPostEntityService, $GalleryPostEntityService,
$HashtagEntityService, $HashtagEntityService,
$InstanceEntityService, $InstanceEntityService,
$InviteCodeEntityService,
$ModerationLogEntityService, $ModerationLogEntityService,
$MutingEntityService, $MutingEntityService,
$RenoteMutingEntityService, $RenoteMutingEntityService,
@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
GalleryPostEntityService, GalleryPostEntityService,
HashtagEntityService, HashtagEntityService,
InstanceEntityService, InstanceEntityService,
InviteCodeEntityService,
ModerationLogEntityService, ModerationLogEntityService,
MutingEntityService, MutingEntityService,
RenoteMutingEntityService, RenoteMutingEntityService,
@ -685,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$GalleryPostEntityService, $GalleryPostEntityService,
$HashtagEntityService, $HashtagEntityService,
$InstanceEntityService, $InstanceEntityService,
$InviteCodeEntityService,
$ModerationLogEntityService, $ModerationLogEntityService,
$MutingEntityService, $MutingEntityService,
$RenoteMutingEntityService, $RenoteMutingEntityService,

View file

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { v4 as uuid } from 'uuid';
import { IsNull, DataSource } from 'typeorm'; import { IsNull, DataSource } from 'typeorm';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
@ -24,7 +24,7 @@ export class CreateSystemUserService {
@bindThis @bindThis
public async createSystemUser(username: string): Promise<User> { public async createSystemUser(username: string): Promise<User> {
const password = uuid(); const password = randomUUID();
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
@ -33,7 +33,7 @@ export class CreateSystemUserService {
// Generate secret // Generate secret
const secret = generateNativeUserToken(); const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair(4096); const keyPair = await genRsaKeyPair();
let account!: User; let account!: User;

View file

@ -2,8 +2,7 @@ import * as fs from 'node:fs';
import * as stream from 'node:stream'; import * as stream from 'node:stream';
import * as util from 'node:util'; import * as util from 'node:util';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import IPCIDR from 'ip-cidr'; import ipaddr from 'ipaddr.js';
import PrivateIp from 'private-ip';
import chalk from 'chalk'; import chalk from 'chalk';
import got, * as Got from 'got'; import got, * as Got from 'got';
import { parse } from 'content-disposition'; import { parse } from 'content-disposition';
@ -140,13 +139,14 @@ export class DownloadService {
@bindThis @bindThis
private isPrivateIp(ip: string): boolean { private isPrivateIp(ip: string): boolean {
const parsedIp = ipaddr.parse(ip);
for (const net of this.config.allowedPrivateNetworks ?? []) { for (const net of this.config.allowedPrivateNetworks ?? []) {
const cidr = new IPCIDR(net); if (parsedIp.match(ipaddr.parseCIDR(net))) {
if (cidr.contains(ip)) {
return false; return false;
} }
} }
return PrivateIp(ip) ?? false; return parsedIp.range() !== 'unicast';
} }
} }

View file

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import sharp from 'sharp'; import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp'; import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
@ -162,7 +162,7 @@ export class DriveService {
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
// for original // for original
const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`; const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`; const url = `${ baseUrl }/${ key }`;
// for alts // for alts
@ -179,7 +179,7 @@ export class DriveService {
]; ];
if (alts.webpublic) { if (alts.webpublic) {
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`; webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
@ -187,7 +187,7 @@ export class DriveService {
} }
if (alts.thumbnail) { if (alts.thumbnail) {
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`; thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
@ -212,9 +212,9 @@ export class DriveService {
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
} else { // use internal storage } else { // use internal storage
const accessKey = uuid(); const accessKey = randomUUID();
const thumbnailAccessKey = 'thumbnail-' + uuid(); const thumbnailAccessKey = 'thumbnail-' + randomUUID();
const webpublicAccessKey = 'webpublic-' + uuid(); const webpublicAccessKey = 'webpublic-' + randomUUID();
const url = this.internalStorageService.saveFromPath(accessKey, path); const url = this.internalStorageService.saveFromPath(accessKey, path);
@ -584,9 +584,9 @@ export class DriveService {
if (isLink) { if (isLink) {
file.url = url; file.url = url;
// ローカルプロキシ用 // ローカルプロキシ用
file.accessKey = uuid(); file.accessKey = randomUUID();
file.thumbnailAccessKey = 'thumbnail-' + uuid(); file.thumbnailAccessKey = 'thumbnail-' + randomUUID();
file.webpublicAccessKey = 'webpublic-' + uuid(); file.webpublicAccessKey = 'webpublic-' + randomUUID();
} }
} }
@ -713,9 +713,9 @@ export class DriveService {
webpublicUrl: null, webpublicUrl: null,
storedInternal: false, storedInternal: false,
// ローカルプロキシ用 // ローカルプロキシ用
accessKey: uuid(), accessKey: randomUUID(),
thumbnailAccessKey: 'thumbnail-' + uuid(), thumbnailAccessKey: 'thumbnail-' + randomUUID(),
webpublicAccessKey: 'webpublic-' + uuid(), webpublicAccessKey: 'webpublic-' + randomUUID(),
}); });
} else { } else {
this.driveFilesRepository.delete(file.id); this.driveFilesRepository.delete(file.id);

View file

@ -2,9 +2,8 @@ import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import * as Redis from 'ioredis';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import type { InstancesRepository } from '@/models/index.js';
import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
@ -37,33 +36,43 @@ export class FetchInstanceMetadataService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private appLockService: AppLockService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private loggerService: LoggerService, private loggerService: LoggerService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
@Inject(DI.redis)
private redisClient: Redis.Redis,
) { ) {
this.logger = this.loggerService.getLogger('metadata', 'cyan'); this.logger = this.loggerService.getLogger('metadata', 'cyan');
} }
@bindThis @bindThis
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> { public async tryLock(host: string): Promise<boolean> {
const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host); const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
return mutex !== '1';
}
@bindThis
public unlock(host: string): Promise<'OK'> {
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
}
@bindThis
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
const host = instance.host;
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return;
try {
if (!force) { if (!force) {
const _instance = await this.instancesRepository.findOneBy({ host: instance.host }); const _instance = await this.federatedInstanceService.fetch(host);
const now = Date.now(); const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
unlock(); // unlock at the finally caluse
return; return;
} }
} }
this.logger.info(`Fetching metadata of ${instance.host} ...`); this.logger.info(`Fetching metadata of ${instance.host} ...`);
try {
const [info, dom, manifest] = await Promise.all([ const [info, dom, manifest] = await Promise.all([
this.fetchNodeinfo(instance).catch(() => null), this.fetchNodeinfo(instance).catch(() => null),
this.fetchDom(instance).catch(() => null), this.fetchDom(instance).catch(() => null),
@ -94,7 +103,7 @@ export class FetchInstanceMetadataService {
if (name) updates.name = name; if (name) updates.name = name;
if (description) updates.description = description; if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = icon ?? favicon; if (icon || favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
if (favicon) updates.faviconUrl = favicon; if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor; if (themeColor) updates.themeColor = themeColor;
@ -104,7 +113,7 @@ export class FetchInstanceMetadataService {
} catch (e) { } catch (e) {
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally { } finally {
unlock(); await this.unlock(host);
} }
} }

View file

@ -304,11 +304,11 @@ export class FileInfoService {
@bindThis @bindThis
public fixMime(mime: string | fileType.MimeType): string { public fixMime(mime: string | fileType.MimeType): string {
// see https://github.com/misskey-dev/misskey/pull/10686 // see https://github.com/misskey-dev/misskey/pull/10686
if (mime === "audio/x-flac") { if (mime === 'audio/x-flac') {
return "audio/flac"; return 'audio/flac';
} }
if (mime === "audio/vnd.wave") { if (mime === 'audio/vnd.wave') {
return "audio/wav"; return 'audio/wav';
} }
return mime; return mime;
@ -355,11 +355,12 @@ export class FileInfoService {
* Check the file is SVG or not * Check the file is SVG or not
*/ */
@bindThis @bindThis
public async checkSvg(path: string) { public async checkSvg(path: string): Promise<boolean> {
try { try {
const size = await this.getFileSize(path); const size = await this.getFileSize(path);
if (size > 1 * 1024 * 1024) return false; if (size > 1 * 1024 * 1024) return false;
return isSvg(fs.readFileSync(path)); const buffer = await fs.promises.readFile(path);
return isSvg(buffer.toString());
} catch { } catch {
return false; return false;
} }

View file

@ -20,7 +20,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Role } from '@/models'; import { Role } from '@/models/index.js';
@Injectable() @Injectable()
export class GlobalEventService { export class GlobalEventService {

View file

@ -1,5 +1,6 @@
import * as http from 'node:http'; import * as http from 'node:http';
import * as https from 'node:https'; import * as https from 'node:https';
import * as net from 'node:net';
import CacheableLookup from 'cacheable-lookup'; import CacheableLookup from 'cacheable-lookup';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
@ -46,14 +47,14 @@ export class HttpRequestService {
this.http = new http.Agent({ this.http = new http.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup as unknown as net.LookupFunction,
} as http.AgentOptions); });
this.https = new https.Agent({ this.https = new https.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup as unknown as net.LookupFunction,
} as https.AgentOptions); });
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
@ -144,7 +145,7 @@ export class HttpRequestService {
method: args.method ?? 'GET', method: args.method ?? 'GET',
headers: { headers: {
'User-Agent': this.config.userAgent, 'User-Agent': this.config.userAgent,
...(args.headers ?? {}) ...(args.headers ?? {}),
}, },
body: args.body, body: args.body,
size: args.size ?? 10 * 1024 * 1024, size: args.size ?? 10 * 1024 * 1024,

View file

@ -5,7 +5,7 @@ import type { Config } from '@/config.js';
import { genAid, parseAid } from '@/misc/id/aid.js'; import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid, parseMeid } from '@/misc/id/meid.js'; import { genMeid, parseMeid } from '@/misc/id/meid.js';
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js'; import { genObjectId, parseObjectId } from '@/misc/id/object-id.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { parseUlid } from '@/misc/id/ulid.js'; import { parseUlid } from '@/misc/id/ulid.js';
@ -38,7 +38,7 @@ export class IdService {
public parse(id: string): { date: Date; } { public parse(id: string): { date: Date; } {
switch (this.method) { switch (this.method) {
case 'aid': return parseAid(id); case 'aid': return parseAid(id);
case 'objectid': case 'objectid': return parseObjectId(id);
case 'meid': return parseMeid(id); case 'meid': return parseMeid(id);
case 'meidg': return parseMeidg(id); case 'meidg': return parseMeidg(id);
case 'ulid': return parseUlid(id); case 'ulid': return parseUlid(id);

View file

@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { KEYWORD } from 'color-convert/conversions'; import type { KEYWORD } from 'color-convert/conversions.js';
@Injectable() @Injectable()
export class LoggerService { export class LoggerService {

View file

@ -570,12 +570,14 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.reply) { if (data.reply) {
// 通知 // 通知
if (data.reply.userHost === null) { if (data.reply.userHost === null) {
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ const isThreadMuted = await this.noteThreadMutingsRepository.exist({
where: {
userId: data.reply.userId, userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id, threadId: data.reply.threadId ?? data.reply.id,
},
}); });
if (!threadMuted) { if (!isThreadMuted) {
nm.push(data.reply.userId, 'reply'); nm.push(data.reply.userId, 'reply');
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
@ -712,12 +714,14 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) { private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) { for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ const isThreadMuted = await this.noteThreadMutingsRepository.exist({
where: {
userId: u.id, userId: u.id,
threadId: note.threadId ?? note.id, threadId: note.threadId ?? note.id,
},
}); });
if (threadMuted) { if (isThreadMuted) {
continue; continue;
} }

View file

@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
@Injectable() @Injectable()
export class NoteDeleteService { export class NoteDeleteService {
@ -41,6 +42,7 @@ export class NoteDeleteService {
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService, private metaService: MetaService,
private searchService: SearchService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
@ -53,6 +55,7 @@ export class NoteDeleteService {
*/ */
async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; isBot: User['isBot']; }, note: Note, quiet = false) { async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; isBot: User['isBot']; }, note: Note, quiet = false) {
const deletedAt = new Date(); const deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note);
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) { if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
@ -88,8 +91,8 @@ export class NoteDeleteService {
} }
// also deliever delete activity to cascaded notes // also deliever delete activity to cascaded notes
const cascadingNotes = (await this.findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
for (const cascadingNote of cascadingNotes) { for (const cascadingNote of federatedLocalCascadingNotes) {
if (!cascadingNote.user) continue; if (!cascadingNote.user) continue;
if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue;
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
@ -114,6 +117,11 @@ export class NoteDeleteService {
} }
} }
for (const cascadingNote of cascadingNotes) {
this.searchService.unindexNote(cascadingNote);
}
this.searchService.unindexNote(note);
await this.notesRepository.delete({ await this.notesRepository.delete({
id: note.id, id: note.id,
userId: user.id, userId: user.id,
@ -121,10 +129,8 @@ export class NoteDeleteService {
} }
@bindThis @bindThis
private async findCascadingNotes(note: Note) { private async findCascadingNotes(note: Note): Promise<Note[]> {
const cascadingNotes: Note[] = []; const recursive = async (noteId: string): Promise<Note[]> => {
const recursive = async (noteId: string) => {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId }) .where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => { .orWhere(new Brackets(q => {
@ -133,14 +139,16 @@ export class NoteDeleteService {
})) }))
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany(); const replies = await query.getMany();
for (const reply of replies) {
cascadingNotes.push(reply);
await recursive(reply.id);
}
};
await recursive(note.id);
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users return [
replies,
...await Promise.all(replies.map(reply => recursive(reply.id))),
].flat();
};
const cascadingNotes: Note[] = await recursive(note.id);
return cascadingNotes;
} }
@bindThis @bindThis

View file

@ -43,11 +43,13 @@ export class NoteReadService implements OnApplicationShutdown {
//#endregion //#endregion
// スレッドミュート // スレッドミュート
const threadMute = await this.noteThreadMutingsRepository.findOneBy({ const isThreadMuted = await this.noteThreadMutingsRepository.exist({
where: {
userId: userId, userId: userId,
threadId: note.threadId ?? note.id, threadId: note.threadId ?? note.id,
},
}); });
if (threadMute) return; if (isThreadMuted) return;
const unread = { const unread = {
id: this.idService.genId(), id: this.idService.genId(),
@ -62,9 +64,9 @@ export class NoteReadService implements OnApplicationShutdown {
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
if (exist == null) return; if (!exist) return;
if (params.isMentioned) { if (params.isMentioned) {
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);

View file

@ -3,7 +3,7 @@ import push from 'web-push';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema'; import type { Packed } from '@/misc/json-schema.js';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js'; import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';

View file

@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import type { IActivity } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
@ -101,15 +101,14 @@ export class QueueService {
const opts = { const opts = {
attempts: this.config.deliverJobMaxAttempts ?? 12, attempts: this.config.deliverJobMaxAttempts ?? 12,
timeout: 1 * 60 * 1000, // 1min
backoff: { backoff: {
type: 'apBackoff', type: 'custom',
}, },
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true, removeOnFail: true,
}; };
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({ await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({
name: d[0], name: d[0],
data: { data: {
user, user,
@ -417,7 +416,7 @@ export class QueueService {
to: webhook.url, to: webhook.url,
secret: webhook.secret, secret: webhook.secret,
createdAt: Date.now(), createdAt: Date.now(),
eventId: uuid(), eventId: randomUUID(),
}; };
return this.webhookDeliverQueue.add(webhook.id, data, { return this.webhookDeliverQueue.add(webhook.id, data, {
@ -435,11 +434,11 @@ export class QueueService {
this.deliverQueue.once('cleaned', (jobs, status) => { this.deliverQueue.once('cleaned', (jobs, status) => {
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
}); });
this.deliverQueue.clean(0, Infinity, 'delayed'); this.deliverQueue.clean(0, 0, 'delayed');
this.inboxQueue.once('cleaned', (jobs, status) => { this.inboxQueue.once('cleaned', (jobs, status) => {
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
}); });
this.inboxQueue.clean(0, Infinity, 'delayed'); this.inboxQueue.clean(0, 0, 'delayed');
} }
} }

View file

@ -8,8 +8,9 @@ import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { WebfingerService } from '@/core/WebfingerService.js'; import { ILink, WebfingerService } from '@/core/WebfingerService.js';
import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -27,6 +28,7 @@ export class RemoteUserResolveService {
private utilityService: UtilityService, private utilityService: UtilityService,
private webfingerService: WebfingerService, private webfingerService: WebfingerService,
private remoteLoggerService: RemoteLoggerService, private remoteLoggerService: RemoteLoggerService,
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
) { ) {
this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user');
@ -67,11 +69,27 @@ export class RemoteUserResolveService {
if (user == null) { if (user == null) {
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
if (self.href.startsWith(this.config.url)) {
const local = this.apDbResolverService.parseUri(self.href);
if (local.local && local.type === 'users') {
// the LR points to local
return (await this.apDbResolverService
.getUserFromApId(self.href)
.then((u) => {
if (u == null) {
throw new Error('local user not found');
} else {
return u;
}
})) as LocalUser;
}
}
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href); return await this.apPersonService.createPerson(self.href);
} }
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す // ユーザー情報が古い場合は、WebFingerからやりなおして返す
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
@ -119,7 +137,7 @@ export class RemoteUserResolveService {
} }
@bindThis @bindThis
private async resolveSelf(acctLower: string) { private async resolveSelf(acctLower: string): Promise<ILink> {
this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
const finger = await this.webfingerService.webfinger(acctLower).catch(err => { const finger = await this.webfingerService.webfinger(acctLower).catch(err => {
this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`); this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`);

View file

@ -13,7 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Packed } from '@/misc/json-schema'; import type { Packed } from '@/misc/json-schema.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = { export type RolePolicies = {
@ -21,6 +21,9 @@ export type RolePolicies = {
ltlAvailable: boolean; ltlAvailable: boolean;
canPublicNote: boolean; canPublicNote: boolean;
canInvite: boolean; canInvite: boolean;
inviteLimit: number;
inviteLimitCycle: number;
inviteExpirationTime: number;
canManageCustomEmojis: boolean; canManageCustomEmojis: boolean;
canSearchNotes: boolean; canSearchNotes: boolean;
canHideAds: boolean; canHideAds: boolean;
@ -42,6 +45,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
ltlAvailable: true, ltlAvailable: true,
canPublicNote: true, canPublicNote: true,
canInvite: false, canInvite: false,
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
inviteExpirationTime: 0,
canManageCustomEmojis: false, canManageCustomEmojis: false,
canSearchNotes: false, canSearchNotes: false,
canHideAds: false, canHideAds: false,
@ -214,14 +220,19 @@ export class RoleService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async getUserRoles(userId: User['id']) { public async getUserAssigns(userId: User['id']) {
const now = Date.now(); const now = Date.now();
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); return assigns;
}
@bindThis
public async getUserRoles(userId: User['id']) {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); const assigns = await this.getUserAssigns(userId);
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
return [...assignedRoles, ...matchedCondRoles]; return [...assignedRoles, ...matchedCondRoles];
@ -277,6 +288,9 @@ export class RoleService implements OnApplicationShutdown {
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),

View file

@ -135,6 +135,15 @@ export class SearchService {
} }
} }
@bindThis
public async unindexNote(note: Note): Promise<void> {
if (!['home', 'public'].includes(note.visibility)) return;
if (this.meilisearch) {
this.meilisearchNoteIndex!.deleteDocument(note.id);
}
}
@bindThis @bindThis
public async searchNote(q: string, me: User | null, opts: { public async searchNote(q: string, me: User | null, opts: {
userId?: Note['userId'] | null; userId?: Note['userId'] | null;
@ -194,7 +203,7 @@ export class SearchService {
if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me);
return await query.take(pagination.limit).getMany(); return await query.limit(pagination.limit).getMany();
} }
} }
} }

View file

@ -71,12 +71,12 @@ export class SignupService {
const secret = generateUserToken(); const secret = generateUserToken();
// Check username duplication // Check username duplication
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new Error('DUPLICATED_USERNAME'); throw new Error('DUPLICATED_USERNAME');
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
} }
@ -92,7 +92,7 @@ export class SignupService {
const keyPair = await new Promise<string[]>((res, rej) => const keyPair = await new Promise<string[]>((res, rej) =>
generateKeyPair('rsa', { generateKeyPair('rsa', {
modulusLength: 4096, modulusLength: 2048,
publicKeyEncoding: { publicKeyEncoding: {
type: 'spki', type: 'spki',
format: 'pem', format: 'pem',

View file

@ -1,5 +1,6 @@
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common'; import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { IsNull } from 'typeorm';
import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js'; import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
@ -21,9 +22,8 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import Logger from '../logger.js';
import { IsNull } from 'typeorm';
import { AccountMoveService } from '@/core/AccountMoveService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -122,22 +122,26 @@ export class UserFollowingService implements OnModuleInit {
let autoAccept = false; let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー // 鍵アカウントであっても、既にフォローされていた場合はスルー
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
where: {
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
},
}); });
if (following) { if (isFollowing) {
autoAccept = true; autoAccept = true;
} }
// フォローしているユーザーは自動承認オプション // フォローしているユーザーは自動承認オプション
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
const followed = await this.followingsRepository.findOneBy({ const isFollowed = await this.followingsRepository.exist({
where: {
followerId: followee.id, followerId: followee.id,
followeeId: follower.id, followeeId: follower.id,
},
}); });
if (followed) autoAccept = true; if (isFollowed) autoAccept = true;
} }
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
@ -206,12 +210,14 @@ export class UserFollowingService implements OnModuleInit {
this.cacheService.userFollowingsCache.refresh(follower.id); this.cacheService.userFollowingsCache.refresh(follower.id);
const req = await this.followRequestsRepository.findOneBy({ const requestExist = await this.followRequestsRepository.exist({
where: {
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
},
}); });
if (req) { if (requestExist) {
await this.followRequestsRepository.delete({ await this.followRequestsRepository.delete({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
@ -316,7 +322,7 @@ export class UserFollowingService implements OnModuleInit {
where: { where: {
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
} },
}); });
if (following === null || !following.follower || !following.followee) { if (following === null || !following.follower || !following.followee) {
@ -406,8 +412,8 @@ export class UserFollowingService implements OnModuleInit {
followerId: user.id, followerId: user.id,
followee: { followee: {
movedToUri: IsNull(), movedToUri: IsNull(),
} },
} },
}); });
const nonMovedFollowers = await this.followingsRepository.count({ const nonMovedFollowers = await this.followingsRepository.count({
relations: { relations: {
@ -417,8 +423,8 @@ export class UserFollowingService implements OnModuleInit {
followeeId: user.id, followeeId: user.id,
follower: { follower: {
movedToUri: IsNull(), movedToUri: IsNull(),
} },
} },
}); });
await this.usersRepository.update( await this.usersRepository.update(
{ id: user.id }, { id: user.id },
@ -505,12 +511,14 @@ export class UserFollowingService implements OnModuleInit {
} }
} }
const request = await this.followRequestsRepository.findOneBy({ const requestExist = await this.followRequestsRepository.exist({
where: {
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
},
}); });
if (request == null) { if (!requestExist) {
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
} }
@ -638,7 +646,7 @@ export class UserFollowingService implements OnModuleInit {
where: { where: {
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
} },
}); });
if (!following || !following.followee || !following.follower) return; if (!following || !following.followee || !following.follower) return;

View file

@ -52,7 +52,7 @@ export class VideoProcessingService {
query({ query({
thumbnail: '1', thumbnail: '1',
url, url,
}) }),
); );
} }
} }

View file

@ -6,12 +6,12 @@ import { query as urlQuery } from '@/misc/prelude/url.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
type ILink = { export type ILink = {
href: string; href: string;
rel?: string; rel?: string;
}; };
type IWebFinger = { export type IWebFinger = {
links: ILink[]; links: ILink[];
subject: string; subject: string;
}; };

View file

@ -16,6 +16,8 @@ type AudienceInfo = {
visibleUsers: User[], visibleUsers: User[],
}; };
type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>;
@Injectable() @Injectable()
export class ApAudienceService { export class ApAudienceService {
constructor( constructor(
@ -67,11 +69,11 @@ export class ApAudienceService {
} }
@bindThis @bindThis
private groupingAudience(ids: string[], actor: RemoteUser) { private groupingAudience(ids: string[], actor: RemoteUser): GroupedAudience {
const groups = { const groups: GroupedAudience = {
public: [] as string[], public: [],
followers: [] as string[], followers: [],
other: [] as string[], other: [],
}; };
for (const id of ids) { for (const id of ids) {
@ -90,7 +92,7 @@ export class ApAudienceService {
} }
@bindThis @bindThis
private isPublic(id: string) { private isPublic(id: string): boolean {
return [ return [
'https://www.w3.org/ns/activitystreams#Public', 'https://www.w3.org/ns/activitystreams#Public',
'as#Public', 'as#Public',
@ -99,9 +101,7 @@ export class ApAudienceService {
} }
@bindThis @bindThis
private isFollowers(id: string, actor: RemoteUser) { private isFollowers(id: string, actor: RemoteUser): boolean {
return ( return id === (actor.followersUri ?? `${actor.uri}/followers`);
id === (actor.followersUri ?? `${actor.uri}/followers`)
);
} }
} }

View file

@ -1,5 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -56,25 +55,18 @@ export class ApDbResolverService implements OnApplicationShutdown {
@bindThis @bindThis
public parseUri(value: string | IObject): UriParseResult { public parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value); const separator = '/';
// the host part of a URL is case insensitive, so use the 'i' flag. const uri = new URL(getApId(value));
const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
const matchLocal = uri.match(localRegex);
if (matchLocal) { const [, type, id, ...rest] = uri.pathname.split(separator);
return { return {
local: true, local: true,
type: matchLocal[1], type,
id: matchLocal[2], id,
rest: matchLocal[3], rest: rest.length === 0 ? undefined : rest.join(separator),
}; };
} else {
return {
local: false,
uri,
};
}
} }
/** /**
@ -107,13 +99,15 @@ export class ApDbResolverService implements OnApplicationShutdown {
if (parsed.local) { if (parsed.local) {
if (parsed.type !== 'users') return null; if (parsed.type !== 'users') return null;
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ return await this.cacheService.userByIdCache.fetchMaybe(
id: parsed.id, parsed.id,
}).then(x => x ?? undefined)) as LocalUser | undefined ?? null; () => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined),
) as LocalUser | undefined ?? null;
} else { } else {
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ return await this.cacheService.uriPersonCache.fetch(
uri: parsed.uri, parsed.uri,
})) as RemoteUser | null; () => this.usersRepository.findOneBy({ uri: parsed.uri }),
) as RemoteUser | null;
} }
} }
@ -153,9 +147,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
} | null> { } | null> {
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser; const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
if (user == null) return null; const key = await this.publicKeyByUserIdCache.fetch(
user.id,
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); () => this.userPublickeysRepository.findOneBy({ userId: user.id }),
v => v != null,
);
return { return {
user, user,

View file

@ -7,6 +7,7 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { IActivity } from '@/core/activitypub/type.js';
import { ThinUser } from '@/queue/types.js'; import { ThinUser } from '@/queue/types.js';
interface IRecipe { interface IRecipe {
@ -22,12 +23,127 @@ interface IDirectRecipe extends IRecipe {
to: RemoteUser; to: RemoteUser;
} }
const isFollowers = (recipe: any): recipe is IFollowersRecipe => const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
recipe.type === 'Followers'; recipe.type === 'Followers';
const isDirect = (recipe: any): recipe is IDirectRecipe => const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
recipe.type === 'Direct'; recipe.type === 'Direct';
class DeliverManager {
private actor: ThinUser;
private activity: IActivity | null;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param userEntityService
* @param followingsRepository
* @param queueService
* @param actor Actor
* @param activity Activity to deliver
*/
constructor(
private userEntityService: UserEntityService,
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
actor: { id: User['id']; host: null; },
activity: IActivity | null,
) {
// 型で弾いてはいるが一応ローカルユーザーかチェック
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (actor.host != null) throw new Error('actor.host must be null');
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
this.actor = {
id: actor.id,
};
this.activity = activity;
}
/**
* Add recipe for followers deliver
*/
@bindThis
public addFollowersRecipe(): void {
const deliver: IFollowersRecipe = {
type: 'Followers',
};
this.addRecipe(deliver);
}
/**
* Add recipe for direct deliver
* @param to To
*/
@bindThis
public addDirectRecipe(to: RemoteUser): void {
const recipe: IDirectRecipe = {
type: 'Direct',
to,
};
this.addRecipe(recipe);
}
/**
* Add recipe
* @param recipe Recipe
*/
@bindThis
public addRecipe(recipe: IRecipe): void {
this.recipes.push(recipe);
}
/**
* Execute delivers
*/
@bindThis
public async execute(): Promise<void> {
// The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
// build inbox list
// Process follower recipes first to avoid duplication when processing direct recipes later.
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await this.followingsRepository.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
});
for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox;
if (inbox === null) throw new Error('inbox is null');
inboxes.set(inbox, following.followerSharedInbox != null);
}
}
for (const recipe of this.recipes.filter(isDirect)) {
// check that shared inbox has not been added yet
if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue;
// check that they actually have an inbox
if (recipe.to.inbox === null) continue;
inboxes.set(recipe.to.inbox, false);
}
// deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes);
}
}
@Injectable() @Injectable()
export class ApDeliverManagerService { export class ApDeliverManagerService {
constructor( constructor(
@ -47,11 +163,11 @@ export class ApDeliverManagerService {
/** /**
* Deliver activity to followers * Deliver activity to followers
* @param actor
* @param activity Activity * @param activity Activity
* @param from Followee
*/ */
@bindThis @bindThis
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) { public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
const manager = new DeliverManager( const manager = new DeliverManager(
this.userEntityService, this.userEntityService,
this.followingsRepository, this.followingsRepository,
@ -65,11 +181,12 @@ export class ApDeliverManagerService {
/** /**
* Deliver activity to user * Deliver activity to user
* @param actor
* @param activity Activity * @param activity Activity
* @param to Target user * @param to Target user
*/ */
@bindThis @bindThis
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) { public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser): Promise<void> {
const manager = new DeliverManager( const manager = new DeliverManager(
this.userEntityService, this.userEntityService,
this.followingsRepository, this.followingsRepository,
@ -82,7 +199,7 @@ export class ApDeliverManagerService {
} }
@bindThis @bindThis
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) { public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager( return new DeliverManager(
this.userEntityService, this.userEntityService,
this.followingsRepository, this.followingsRepository,
@ -93,120 +210,3 @@ export class ApDeliverManagerService {
); );
} }
} }
class DeliverManager {
private actor: ThinUser;
private activity: any;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param actor Actor
* @param activity Activity to deliver
*/
constructor(
private userEntityService: UserEntityService,
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
actor: { id: User['id']; host: null; },
activity: any,
) {
// 型で弾いてはいるが一応ローカルユーザーかチェック
if (actor.host != null) throw new Error('actor.host must be null');
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
this.actor = {
id: actor.id,
};
this.activity = activity;
}
/**
* Add recipe for followers deliver
*/
@bindThis
public addFollowersRecipe() {
const deliver = {
type: 'Followers',
} as IFollowersRecipe;
this.addRecipe(deliver);
}
/**
* Add recipe for direct deliver
* @param to To
*/
@bindThis
public addDirectRecipe(to: RemoteUser) {
const recipe = {
type: 'Direct',
to,
} as IDirectRecipe;
this.addRecipe(recipe);
}
/**
* Add recipe
* @param recipe Recipe
*/
@bindThis
public addRecipe(recipe: IRecipe) {
this.recipes.push(recipe);
}
/**
* Execute delivers
*/
@bindThis
public async execute() {
// The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
/*
build inbox list
Process follower recipes first to avoid duplication when processing
direct recipes later.
*/
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await this.followingsRepository.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox;
inboxes.set(inbox, following.followerSharedInbox != null);
}
}
this.recipes.filter((recipe): recipe is IDirectRecipe =>
// followers recipes have already been processed
isDirect(recipe)
// check that shared inbox has not been added yet
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
// check that they actually have an inbox
&& recipe.to.inbox != null,
)
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes);
}
}

View file

@ -21,10 +21,10 @@ import { CacheService } from '@/core/CacheService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { RemoteUser } from '@/models/entities/User.js'; import type { RemoteUser } from '@/models/entities/User.js';
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js'; import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js'; import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js'; import { ApDbResolverService } from './ApDbResolverService.js';
@ -86,7 +86,7 @@ export class ApInboxService {
} }
@bindThis @bindThis
public async performActivity(actor: RemoteUser, activity: IObject) { public async performActivity(actor: RemoteUser, activity: IObject): Promise<void> {
if (isCollectionOrOrderedCollection(activity)) { if (isCollectionOrOrderedCollection(activity)) {
const resolver = this.apResolverService.createResolver(); const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
@ -107,7 +107,7 @@ export class ApInboxService {
if (actor.uri) { if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => { setImmediate(() => {
this.apPersonService.updatePerson(actor.uri!); this.apPersonService.updatePerson(actor.uri);
}); });
} }
} }
@ -229,7 +229,7 @@ export class ApInboxService {
@bindThis @bindThis
private async add(actor: RemoteUser, activity: IAdd): Promise<void> { private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -292,7 +292,7 @@ export class ApInboxService {
return; return;
} }
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`); this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
} }
throw err; throw err;
} }
@ -409,7 +409,7 @@ export class ApInboxService {
@bindThis @bindThis
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> { private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -420,7 +420,7 @@ export class ApInboxService {
// typeが不明だけど、どうせ消えてるのでremote resolveしない // typeが不明だけど、どうせ消えてるのでremote resolveしない
formerType = undefined; formerType = undefined;
} else { } else {
const object = activity.object as IObject; const object = activity.object;
if (isTombstone(object)) { if (isTombstone(object)) {
formerType = toSingle(object.formerType); formerType = toSingle(object.formerType);
} else { } else {
@ -503,7 +503,10 @@ export class ApInboxService {
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object); const uris = getApIds(activity.object);
const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!); const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
.filter((userId): userId is string => userId !== undefined);
const users = await this.usersRepository.findBy({ const users = await this.usersRepository.findBy({
id: In(userIds), id: In(userIds),
}); });
@ -566,7 +569,7 @@ export class ApInboxService {
@bindThis @bindThis
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> { private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -586,7 +589,7 @@ export class ApInboxService {
@bindThis @bindThis
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> { private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -618,12 +621,14 @@ export class ApInboxService {
return 'skip: follower not found'; return 'skip: follower not found';
} }
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
where: {
followerId: follower.id, followerId: follower.id,
followeeId: actor.id, followeeId: actor.id,
},
}); });
if (following) { if (isFollowing) {
await this.userFollowingService.unfollow(follower, actor); await this.userFollowingService.unfollow(follower, actor);
return 'ok: unfollowed'; return 'ok: unfollowed';
} }
@ -673,22 +678,26 @@ export class ApInboxService {
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
} }
const req = await this.followRequestsRepository.findOneBy({ const requestExist = await this.followRequestsRepository.exist({
where: {
followerId: actor.id, followerId: actor.id,
followeeId: followee.id, followeeId: followee.id,
},
}); });
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
where: {
followerId: actor.id, followerId: actor.id,
followeeId: followee.id, followeeId: followee.id,
},
}); });
if (req) { if (requestExist) {
await this.userFollowingService.cancelFollowRequest(followee, actor); await this.userFollowingService.cancelFollowRequest(followee, actor);
return 'ok: follow request canceled'; return 'ok: follow request canceled';
} }
if (following) { if (isFollowing) {
await this.userFollowingService.unfollow(actor, followee); await this.userFollowingService.unfollow(actor, followee);
return 'ok: unfollowed'; return 'ok: unfollowed';
} }
@ -713,7 +722,7 @@ export class ApInboxService {
@bindThis @bindThis
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> { private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
return 'skip: invalid actor'; return 'skip: invalid actor';
} }
@ -727,7 +736,7 @@ export class ApInboxService {
}); });
if (isActor(object)) { if (isActor(object)) {
await this.apPersonService.updatePerson(actor.uri!, resolver, object); await this.apPersonService.updatePerson(actor.uri, resolver, object);
return 'ok: Person updated'; return 'ok: Person updated';
} else if (getApType(object) === 'Question') { } else if (getApType(object) === 'Question') {
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));

View file

@ -4,9 +4,9 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MfmService } from '@/core/MfmService.js'; import { MfmService } from '@/core/MfmService.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js';
import { extractApHashtagObjects } from './models/tag.js'; import { extractApHashtagObjects } from './models/tag.js';
import type { IObject } from './type.js'; import type { IObject } from './type.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ApMfmService { export class ApMfmService {
@ -19,14 +19,13 @@ export class ApMfmService {
} }
@bindThis @bindThis
public htmlToMfm(html: string, tag?: IObject | IObject[]) { public htmlToMfm(html: string, tag?: IObject | IObject[]): string {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); const hashtagNames = extractApHashtagObjects(tag).map(x => x.name);
return this.mfmService.fromHtml(html, hashtagNames); return this.mfmService.fromHtml(html, hashtagNames);
} }
@bindThis @bindThis
public getNoteHtml(note: Note) { public getNoteHtml(note: Note): string | null {
if (!note.text) return ''; if (!note.text) return '';
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
} }

View file

@ -1,7 +1,6 @@
import { createPublicKey } from 'node:crypto'; import { createPublicKey, randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In, IsNull } from 'typeorm'; import { In } from 'typeorm';
import { v4 as uuid } from 'uuid';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -26,7 +25,6 @@ import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js'; import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import type { IIdentifier } from './models/identifier.js';
@Injectable() @Injectable()
export class ApRendererService { export class ApRendererService {
@ -63,7 +61,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept { public renderAccept(object: string | IObject, user: { id: User['id']; host: null }): IAccept {
return { return {
type: 'Accept', type: 'Accept',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -72,7 +70,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderAdd(user: LocalUser, target: any, object: any): IAdd { public renderAdd(user: LocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
return { return {
type: 'Add', type: 'Add',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -82,7 +80,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderAnnounce(object: any, note: Note): IAnnounce { public renderAnnounce(object: string | IObject, note: Note): IAnnounce {
const attributedTo = this.userEntityService.genLocalUserUri(note.userId); const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
let to: string[] = []; let to: string[] = [];
@ -133,13 +131,13 @@ export class ApRendererService {
@bindThis @bindThis
public renderCreate(object: IObject, note: Note): ICreate { public renderCreate(object: IObject, note: Note): ICreate {
const activity = { const activity: ICreate = {
id: `${this.config.url}/notes/${note.id}/activity`, id: `${this.config.url}/notes/${note.id}/activity`,
actor: this.userEntityService.genLocalUserUri(note.userId), actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Create', type: 'Create',
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),
object, object,
} as ICreate; };
if (object.to) activity.to = object.to; if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc; if (object.cc) activity.cc = object.cc;
@ -209,7 +207,7 @@ export class ApRendererService {
* @param id Follower|Followee ID * @param id Follower|Followee ID
*/ */
@bindThis @bindThis
public async renderFollowUser(id: User['id']) { public async renderFollowUser(id: User['id']): Promise<string> {
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser; const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
return this.userEntityService.getUserUri(user); return this.userEntityService.getUserUri(user);
} }
@ -223,8 +221,8 @@ export class ApRendererService {
return { return {
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow', type: 'Follow',
actor: this.userEntityService.getUserUri(follower)!, actor: this.userEntityService.getUserUri(follower),
object: this.userEntityService.getUserUri(followee)!, object: this.userEntityService.getUserUri(followee),
}; };
} }
@ -264,14 +262,14 @@ export class ApRendererService {
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> { public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
const reaction = noteReaction.reaction; const reaction = noteReaction.reaction;
const object = { const object: ILike = {
type: 'Like', type: 'Like',
id: `${this.config.url}/likes/${noteReaction.id}`, id: `${this.config.url}/likes/${noteReaction.id}`,
actor: `${this.config.url}/users/${noteReaction.userId}`, actor: `${this.config.url}/users/${noteReaction.userId}`,
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
content: reaction, content: reaction,
_misskey_reaction: reaction, _misskey_reaction: reaction,
} as ILike; };
if (reaction.startsWith(':')) { if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', ''); const name = reaction.replaceAll(':', '');
@ -287,7 +285,7 @@ export class ApRendererService {
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention { public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
return { return {
type: 'Mention', type: 'Mention',
href: this.userEntityService.getUserUri(mention)!, href: this.userEntityService.getUserUri(mention),
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`, name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
}; };
} }
@ -297,8 +295,8 @@ export class ApRendererService {
src: PartialLocalUser | PartialRemoteUser, src: PartialLocalUser | PartialRemoteUser,
dst: PartialLocalUser | PartialRemoteUser, dst: PartialLocalUser | PartialRemoteUser,
): IMove { ): IMove {
const actor = this.userEntityService.getUserUri(src)!; const actor = this.userEntityService.getUserUri(src);
const target = this.userEntityService.getUserUri(dst)!; const target = this.userEntityService.getUserUri(dst);
return { return {
id: `${this.config.url}/moves/${src.id}/${dst.id}`, id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor, actor,
@ -310,10 +308,10 @@ export class ApRendererService {
@bindThis @bindThis
public async renderNote(note: Note, dive = true): Promise<IPost> { public async renderNote(note: Note, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]) => { const getPromisedFiles = async (ids: string[]): Promise<DriveFile[]> => {
if (!ids || ids.length === 0) return []; if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) }); const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; return ids.map(id => items.find(item => item.id === id)).filter((item): item is DriveFile => item != null);
}; };
let inReplyTo; let inReplyTo;
@ -323,9 +321,9 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) { if (inReplyToNote != null) {
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
if (inReplyToUser != null) { if (inReplyToUserExist) {
if (inReplyToNote.uri) { if (inReplyToNote.uri) {
inReplyTo = inReplyToNote.uri; inReplyTo = inReplyToNote.uri;
} else { } else {
@ -375,7 +373,7 @@ export class ApRendererService {
id: In(note.mentions), id: In(note.mentions),
}) : []; }) : [];
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser)); const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
const files = await getPromisedFiles(note.fileIds); const files = await getPromisedFiles(note.fileIds);
@ -451,37 +449,26 @@ export class ApRendererService {
@bindThis @bindThis
public async renderPerson(user: LocalUser) { public async renderPerson(user: LocalUser) {
const id = this.userEntityService.genLocalUserUri(user.id); const id = this.userEntityService.genLocalUserUri(user.id);
const isSystem = !!user.username.match(/\./); const isSystem = user.username.includes('.');
const [avatar, banner, profile] = await Promise.all([ const [avatar, banner, profile] = await Promise.all([
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined), user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined), user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
this.userProfilesRepository.findOneByOrFail({ userId: user.id }), this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
]); ]);
const attachment: { const attachment = profile.fields.map(field => ({
type: 'PropertyValue',
name: string,
value: string,
identifier?: IIdentifier,
}[] = [];
if (profile.fields) {
for (const field of profile.fields) {
attachment.push({
type: 'PropertyValue', type: 'PropertyValue',
name: field.name, name: field.name,
value: (field.value != null && field.value.match(/^https?:/)) value: /^https?:/.test(field.value)
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>` ? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
: field.value, : field.value,
}); }));
}
}
const emojis = await this.getEmojis(user.emojis); const emojis = await this.getEmojis(user.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag)); const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
const tag = [ const tag = [
...apemojis, ...apemojis,
@ -490,7 +477,7 @@ export class ApRendererService {
const keypair = await this.userKeypairService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
const person = { const person: any = {
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
id, id,
inbox: `${id}/inbox`, inbox: `${id}/inbox`,
@ -508,11 +495,11 @@ export class ApRendererService {
image: banner ? this.renderImage(banner) : null, image: banner ? this.renderImage(banner) : null,
tag, tag,
manuallyApprovesFollowers: user.isLocked, manuallyApprovesFollowers: user.isLocked,
discoverable: !!user.isExplorable, discoverable: user.isExplorable,
publicKey: this.renderKey(user, keypair, '#main-key'), publicKey: this.renderKey(user, keypair, '#main-key'),
isCat: user.isCat, isCat: user.isCat,
attachment: attachment.length ? attachment : undefined, attachment: attachment.length ? attachment : undefined,
} as any; };
if (user.movedToUri) { if (user.movedToUri) {
person.movedTo = user.movedToUri; person.movedTo = user.movedToUri;
@ -552,7 +539,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderReject(object: any, user: { id: User['id'] }): IReject { public renderReject(object: string | IObject, user: { id: User['id'] }): IReject {
return { return {
type: 'Reject', type: 'Reject',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -561,7 +548,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove { public renderRemove(user: { id: User['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
return { return {
type: 'Remove', type: 'Remove',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -579,8 +566,8 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderUndo(object: any, user: { id: User['id'] }): IUndo { public renderUndo(object: string | IObject, user: { id: User['id'] }): IUndo {
const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
return { return {
type: 'Undo', type: 'Undo',
@ -592,7 +579,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate { public renderUpdate(object: string | IObject, user: { id: User['id'] }): IUpdate {
return { return {
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -625,7 +612,7 @@ export class ApRendererService {
@bindThis @bindThis
public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } { public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } {
if (typeof x === 'object' && x.id == null) { if (typeof x === 'object' && x.id == null) {
x.id = `${this.config.url}/${uuid()}`; x.id = `${this.config.url}/${randomUUID()}`;
} }
return Object.assign({ return Object.assign({
@ -658,7 +645,7 @@ export class ApRendererService {
vcard: 'http://www.w3.org/2006/vcard/ns#', vcard: 'http://www.w3.org/2006/vcard/ns#',
}, },
], ],
}, x as T & { id: string; }); }, x as T & { id: string });
} }
@bindThis @bindThis
@ -683,13 +670,13 @@ export class ApRendererService {
*/ */
@bindThis @bindThis
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
const page = { const page: any = {
id, id,
partOf, partOf,
type: 'OrderedCollectionPage', type: 'OrderedCollectionPage',
totalItems, totalItems,
orderedItems, orderedItems,
} as any; };
if (prev) page.prev = prev; if (prev) page.prev = prev;
if (next) page.next = next; if (next) page.next = next;
@ -706,7 +693,7 @@ export class ApRendererService {
* @param orderedItems attached objects (optional) * @param orderedItems attached objects (optional)
*/ */
@bindThis @bindThis
public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) { public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) {
const page: any = { const page: any = {
id, id,
type: 'OrderedCollection', type: 'OrderedCollection',
@ -722,7 +709,7 @@ export class ApRendererService {
@bindThis @bindThis
private async getEmojis(names: string[]): Promise<Emoji[]> { private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return []; if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);

View file

@ -140,7 +140,7 @@ export class ApRequestService {
} }
@bindThis @bindThis
public async signedPost(user: { id: User['id'] }, url: string, object: any) { public async signedPost(user: { id: User['id'] }, url: string, object: unknown): Promise<void> {
const body = JSON.stringify(object); const body = JSON.stringify(object);
const keypair = await this.userKeypairService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
@ -169,7 +169,7 @@ export class ApRequestService {
* @param url URL to fetch * @param url URL to fetch
*/ */
@bindThis @bindThis
public async signedGet(url: string, user: { id: User['id'] }) { public async signedGet(url: string, user: { id: User['id'] }): Promise<unknown> {
const keypair = await this.userKeypairService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({ const req = ApRequestCreator.createSignedGet({

View file

@ -61,10 +61,6 @@ export class Resolver {
@bindThis @bindThis
public async resolve(value: string | IObject): Promise<IObject> { public async resolve(value: string | IObject): Promise<IObject> {
if (value == null) {
throw new Error('resolvee is null (or undefined)');
}
if (typeof value !== 'string') { if (typeof value !== 'string') {
return value; return value;
} }
@ -104,11 +100,11 @@ export class Resolver {
? await this.apRequestService.signedGet(value, this.user) as IObject ? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
if (object == null || ( if (
Array.isArray(object['@context']) ? Array.isArray(object['@context']) ?
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams' object['@context'] !== 'https://www.w3.org/ns/activitystreams'
)) { ) {
throw new Error('invalid response'); throw new Error('invalid response');
} }

View file

@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js'; import { CONTEXTS } from './misc/contexts.js';
import type { JsonLdDocument } from 'jsonld';
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 // RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
@ -18,22 +20,21 @@ class LdSignature {
@bindThis @bindThis
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> { public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
const options = { const options: {
type: 'RsaSignature2017',
creator,
domain,
nonce: crypto.randomBytes(16).toString('hex'),
created: (created ?? new Date()).toISOString(),
} as {
type: string; type: string;
creator: string; creator: string;
domain?: string; domain?: string;
nonce: string; nonce: string;
created: string; created: string;
} = {
type: 'RsaSignature2017',
creator,
nonce: crypto.randomBytes(16).toString('hex'),
created: (created ?? new Date()).toISOString(),
}; };
if (!domain) { if (domain) {
delete options.domain; options.domain = domain;
} }
const toBeSigned = await this.createVerifyData(data, options); const toBeSigned = await this.createVerifyData(data, options);
@ -62,7 +63,7 @@ class LdSignature {
} }
@bindThis @bindThis
public async createVerifyData(data: any, options: any) { public async createVerifyData(data: any, options: any): Promise<string> {
const transformedOptions = { const transformedOptions = {
...options, ...options,
'@context': 'https://w3id.org/identity/v1', '@context': 'https://w3id.org/identity/v1',
@ -82,7 +83,7 @@ class LdSignature {
} }
@bindThis @bindThis
public async normalize(data: any) { public async normalize(data: JsonLdDocument): Promise<string> {
const customLoader = this.getLoader(); const customLoader = this.getLoader();
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
@ -93,14 +94,14 @@ class LdSignature {
@bindThis @bindThis
private getLoader() { private getLoader() {
return async (url: string): Promise<any> => { return async (url: string): Promise<RemoteDocument> => {
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`); if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) { if (this.preLoad) {
if (url in CONTEXTS) { if (url in CONTEXTS) {
if (this.debug) console.debug(`HIT: ${url}`); if (this.debug) console.debug(`HIT: ${url}`);
return { return {
contextUrl: null, contextUrl: undefined,
document: CONTEXTS[url], document: CONTEXTS[url],
documentUrl: url, documentUrl: url,
}; };
@ -110,7 +111,7 @@ class LdSignature {
if (this.debug) console.debug(`MISS: ${url}`); if (this.debug) console.debug(`MISS: ${url}`);
const document = await this.fetchDocument(url); const document = await this.fetchDocument(url);
return { return {
contextUrl: null, contextUrl: undefined,
document: document, document: document,
documentUrl: url, documentUrl: url,
}; };
@ -118,13 +119,17 @@ class LdSignature {
} }
@bindThis @bindThis
private async fetchDocument(url: string) { private async fetchDocument(url: string): Promise<JsonLd> {
const json = await this.httpRequestService.send(url, { const json = await this.httpRequestService.send(
url,
{
headers: { headers: {
Accept: 'application/ld+json, application/json', Accept: 'application/ld+json, application/json',
}, },
timeout: this.loderTimeout, timeout: this.loderTimeout,
}, { throwErrorWhenResponseNotOk: false }).then(res => { },
{ throwErrorWhenResponseNotOk: false },
).then(res => {
if (!res.ok) { if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`); throw new Error(`${res.status} ${res.statusText}`);
} else { } else {
@ -132,7 +137,7 @@ class LdSignature {
} }
}); });
return json; return json as JsonLd;
} }
@bindThis @bindThis

View file

@ -1,3 +1,5 @@
import type { JsonLd } from 'jsonld/jsonld-spec.js';
/* eslint:disable:quotemark indent */ /* eslint:disable:quotemark indent */
const id_v1 = { const id_v1 = {
'@context': { '@context': {
@ -86,7 +88,7 @@ const id_v1 = {
'accessControl': { '@id': 'perm:accessControl', '@type': '@id' }, 'accessControl': { '@id': 'perm:accessControl', '@type': '@id' },
'writePermission': { '@id': 'perm:writePermission', '@type': '@id' }, 'writePermission': { '@id': 'perm:writePermission', '@type': '@id' },
}, },
}; } satisfies JsonLd;
const security_v1 = { const security_v1 = {
'@context': { '@context': {
@ -137,7 +139,7 @@ const security_v1 = {
'signatureAlgorithm': 'sec:signingAlgorithm', 'signatureAlgorithm': 'sec:signingAlgorithm',
'signatureValue': 'sec:signatureValue', 'signatureValue': 'sec:signatureValue',
}, },
}; } satisfies JsonLd;
const activitystreams = { const activitystreams = {
'@context': { '@context': {
@ -517,9 +519,9 @@ const activitystreams = {
'@type': '@id', '@type': '@id',
}, },
}, },
}; } satisfies JsonLd;
export const CONTEXTS: Record<string, unknown> = { export const CONTEXTS: Record<string, JsonLd> = {
'https://w3id.org/identity/v1': id_v1, 'https://w3id.org/identity/v1': id_v1,
'https://w3id.org/security/v1': security_v1, 'https://w3id.org/security/v1': security_v1,
'https://www.w3.org/ns/activitystreams': activitystreams, 'https://www.w3.org/ns/activitystreams': activitystreams,

View file

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository } from '@/models/index.js'; import type { DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js'; import type { RemoteUser } from '@/models/entities/User.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -10,18 +9,16 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { checkHttps } from '@/misc/check-https.js'; import type { IObject } from '../type.js';
@Injectable() @Injectable()
export class ApImageService { export class ApImageService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
@ -37,16 +34,20 @@ export class ApImageService {
* Imageを作成します * Imageを作成します
*/ */
@bindThis @bindThis
public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> { public async createImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new Error('actor has been suspended');
} }
const image = await this.apResolverService.createResolver().resolve(value) as any; const image = await this.apResolverService.createResolver().resolve(value);
if (image.url == null) { if (image.url == null) {
throw new Error('invalid image: url not privided'); throw new Error('invalid image: url not provided');
}
if (typeof image.url !== 'string') {
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
} }
if (!checkHttps(image.url)) { if (!checkHttps(image.url)) {
@ -57,29 +58,24 @@ export class ApImageService {
const instance = await this.metaService.fetch(); const instance = await this.metaService.fetch();
let file = await this.driveService.uploadFromUrl({ // Cache if remote file cache is on AND either
// 1. remote sensitive file is also on
// 2. or the image is not sensitive
const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
const file = await this.driveService.uploadFromUrl({
url: image.url, url: image.url,
user: actor, user: actor,
uri: image.url, uri: image.url,
sensitive: image.sensitive, sensitive: image.sensitive,
isLink: !instance.cacheRemoteFiles, isLink: !shouldBeCached,
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
}); });
if (!file.isLink || file.url === image.url) return file;
if (file.isLink) { // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url });
// URLを更新する return await this.driveFilesRepository.findOneByOrFail({ id: file.id });
if (file.url !== image.url) {
await this.driveFilesRepository.update({ id: file.id }, {
url: image.url,
uri: image.url,
});
file = await this.driveFilesRepository.findOneByOrFail({ id: file.id });
}
}
return file;
} }
/** /**
@ -89,7 +85,7 @@ export class ApImageService {
* Misskeyに登録しそれを返します * Misskeyに登録しそれを返します
*/ */
@bindThis @bindThis
public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> { public async resolveImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
// TODO // TODO
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録

View file

@ -22,8 +22,8 @@ export class ApMentionService {
} }
@bindThis @bindThis
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) { public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> {
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string)); const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href));
const limit = promiseLimit<User | null>(2); const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(

View file

@ -20,7 +20,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js'; import { ApDbResolverService } from '../ApDbResolverService.js';
@ -72,13 +71,9 @@ export class ApNoteService {
} }
@bindThis @bindThis
public validateNote(object: IObject, uri: string) { public validateNote(object: IObject, uri: string): Error | null {
const expectHost = this.utilityService.extractDbHost(uri); const expectHost = this.utilityService.extractDbHost(uri);
if (object == null) {
return new Error('invalid Note: object is null');
}
if (!validPost.includes(getApType(object))) { if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`); return new Error(`invalid Note: invalid object type ${getApType(object)}`);
} }
@ -110,6 +105,7 @@ export class ApNoteService {
*/ */
@bindThis @bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> { public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value); const object = await resolver.resolve(value);
@ -117,12 +113,10 @@ export class ApNoteService {
const entryUri = getApId(value); const entryUri = getApId(value);
const err = this.validateNote(object, entryUri); const err = this.validateNote(object, entryUri);
if (err) { if (err) {
this.logger.error(`${err.message}`, { this.logger.error(err.message, {
resolver: { resolver: { history: resolver.getHistory() },
history: resolver.getHistory(), value,
}, object,
value: value,
object: object,
}); });
throw new Error('invalid note'); throw new Error('invalid note');
} }
@ -144,7 +138,11 @@ export class ApNoteService {
this.logger.info(`Creating the Note: ${note.id}`); this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser; if (note.attributedTo == null) {
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser;
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
@ -164,59 +162,49 @@ export class ApNoteService {
} }
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = await extractApHashtags(note.tag); const apHashtags = extractApHashtags(note.tag);
// 添付ファイル // 添付ファイル
// TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない // TODO: attachmentは必ずしも配列ではない
// Noteがsensitiveなら添付もsensitiveにする const limit = promiseLimit<DriveFile>(2);
const limit = promiseLimit(2); const files = (await Promise.all(toArray(note.attachment).map(attach => (
limit(() => this.apImageService.resolveImage(actor, {
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; ...attach,
const files = note.attachment sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
.map(attach => attach.sensitive = note.sensitive) }))
? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>))) ))));
.filter(image => image != null)
: [];
// リプライ // リプライ
const reply: Note | null = note.inReplyTo const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver).then(x => { ? await this.resolveNote(note.inReplyTo, { resolver })
.then(x => {
if (x == null) { if (x == null) {
this.logger.warn('Specified inReplyTo, but not found'); this.logger.warn('Specified inReplyTo, but not found');
throw new Error('inReplyTo not found'); throw new Error('inReplyTo not found');
} else {
return x;
} }
}).catch(async err => {
return x;
})
.catch(async err => {
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
throw err; throw err;
}) })
: null; : null;
// 引用 // 引用
let quote: Note | undefined | null; let quote: Note | undefined | null = null;
if (note._misskey_quote || note.quoteUrl) { if (note._misskey_quote || note.quoteUrl) {
const tryResolveNote = async (uri: string): Promise<{ const tryResolveNote = async (uri: string): Promise<
status: 'ok'; | { status: 'ok'; res: Note }
res: Note | null; | { status: 'permerror' | 'temperror' }
} | { > => {
status: 'permerror' | 'temperror'; if (!/^https?:/.test(uri)) return { status: 'permerror' };
}> => {
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
try { try {
const res = await this.resolveNote(uri); const res = await this.resolveNote(uri);
if (res) { if (res == null) return { status: 'permerror' };
return { return { status: 'ok', res };
status: 'ok',
res,
};
} else {
return {
status: 'permerror',
};
}
} catch (e) { } catch (e) {
return { return {
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
@ -225,9 +213,9 @@ export class ApNoteService {
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0);
if (!quote) { if (!quote) {
if (results.some(x => x.status === 'temperror')) { if (results.some(x => x.status === 'temperror')) {
throw new Error('quote resolve failed'); throw new Error('quote resolve failed');
@ -271,7 +259,7 @@ export class ApNoteService {
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return [] as Emoji[]; return [];
}); });
const apEmojis = emojis.map(emoji => emoji.name); const apEmojis = emojis.map(emoji => emoji.name);
@ -305,23 +293,21 @@ export class ApNoteService {
* Misskeyに登録しそれを返します * Misskeyに登録しそれを返します
*/ */
@bindThis @bindThis
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<Note | null> {
const uri = typeof value === 'string' ? value : value.id; const uri = getApId(value);
if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断 // ブロックしてたら中断
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451); if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
throw new StatusError('blocked host', 451);
}
const unlock = await this.appLockService.getApLock(uri); const unlock = await this.appLockService.getApLock(uri);
try { try {
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchNote(uri); const exist = await this.fetchNote(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion //#endregion
if (uri.startsWith(this.config.url)) { if (uri.startsWith(this.config.url)) {
@ -331,7 +317,8 @@ export class ApNoteService {
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
return await this.createNote(uri, resolver, true); const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
return await this.createNote(createFrom, options.resolver, true);
} finally { } finally {
unlock(); unlock();
} }
@ -339,43 +326,41 @@ export class ApNoteService {
@bindThis @bindThis
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> { public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
// eslint-disable-next-line no-param-reassign
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
if (!tags) return [];
const eomjiTags = toArray(tags).filter(isEmoji); const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({ const existingEmojis = await this.emojisRepository.findBy({
host, host,
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))), name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))),
}); });
return await Promise.all(eomjiTags.map(async tag => { return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name!.replaceAll(':', ''); const name = tag.name.replaceAll(':', '');
tag.icon = toSingle(tag.icon); tag.icon = toSingle(tag.icon);
const exists = existingEmojis.find(x => x.name === name); const exists = existingEmojis.find(x => x.name === name);
if (exists) { if (exists) {
if ((tag.updated != null && exists.updatedAt == null) if ((exists.updatedAt == null)
|| (tag.id != null && exists.uri == null) || (tag.id != null && exists.uri == null)
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) || (new Date(tag.updated) > exists.updatedAt)
|| (tag.icon!.url !== exists.originalUrl) || (tag.icon.url !== exists.originalUrl)
) { ) {
await this.emojisRepository.update({ await this.emojisRepository.update({
host, host,
name, name,
}, { }, {
uri: tag.id, uri: tag.id,
originalUrl: tag.icon!.url, originalUrl: tag.icon.url,
publicUrl: tag.icon!.url, publicUrl: tag.icon.url,
updatedAt: new Date(), updatedAt: new Date(),
}); });
return await this.emojisRepository.findOneBy({ const emoji = await this.emojisRepository.findOneBy({ host, name });
host, if (emoji == null) throw new Error('emoji update failed');
name, return emoji;
}) as Emoji;
} }
return exists; return exists;
@ -388,11 +373,11 @@ export class ApNoteService {
host, host,
name, name,
uri: tag.id, uri: tag.id,
originalUrl: tag.icon!.url, originalUrl: tag.icon.url,
publicUrl: tag.icon!.url, publicUrl: tag.icon.url,
updatedAt: new Date(), updatedAt: new Date(),
aliases: [], aliases: [],
} as Partial<Emoji>).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
})); }));
} }
} }

View file

@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
@ -15,7 +15,6 @@ import type Logger from '@/logger.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import type { IdService } from '@/core/IdService.js'; import type { IdService } from '@/core/IdService.js';
import type { MfmService } from '@/core/MfmService.js'; import type { MfmService } from '@/core/MfmService.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import { toArray } from '@/misc/prelude/array.js'; import { toArray } from '@/misc/prelude/array.js';
import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js';
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@ -48,6 +47,8 @@ import type { IActor, IObject } from '../type.js';
const nameLength = 128; const nameLength = 128;
const summaryLength = 2048; const summaryLength = 2048;
type Field = Record<'name' | 'value', string>;
@Injectable() @Injectable()
export class ApPersonService implements OnModuleInit { export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService; private utilityService: UtilityService;
@ -94,28 +95,10 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
//private utilityService: UtilityService,
//private userEntityService: UserEntityService,
//private idService: IdService,
//private globalEventService: GlobalEventService,
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private cacheService: CacheService,
//private apResolverService: ApResolverService,
//private apNoteService: ApNoteService,
//private apImageService: ApImageService,
//private apMfmService: ApMfmService,
//private mfmService: MfmService,
//private hashtagService: HashtagService,
//private usersChart: UsersChart,
//private instanceChart: InstanceChart,
//private apLoggerService: ApLoggerService,
) { ) {
} }
onModuleInit() { onModuleInit(): void {
this.utilityService = this.moduleRef.get('UtilityService'); this.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
@ -153,10 +136,6 @@ export class ApPersonService implements OnModuleInit {
private validateActor(x: IObject, uri: string): IActor { private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.punyHost(uri); const expectHost = this.punyHost(uri);
if (x == null) {
throw new Error('invalid Actor: object is null');
}
if (!isActor(x)) { if (!isActor(x)) {
throw new Error(`invalid Actor type '${x.type}'`); throw new Error(`invalid Actor type '${x.type}'`);
} }
@ -218,21 +197,19 @@ export class ApPersonService implements OnModuleInit {
*/ */
@bindThis @bindThis
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> { public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
if (typeof uri !== 'string') throw new Error('uri is not string'); const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null | undefined;
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
if (cached) return cached; if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(`${this.config.url}/`)) { if (uri.startsWith(`${this.config.url}/`)) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
const u = await this.usersRepository.findOneBy({ id }) as LocalUser; const u = await this.usersRepository.findOneBy({ id }) as LocalUser | null;
if (u) this.cacheService.uriPersonCache.set(uri, u); if (u) this.cacheService.uriPersonCache.set(uri, u);
return u; return u;
} }
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser; const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null;
if (exist) { if (exist) {
this.cacheService.uriPersonCache.set(uri, exist); this.cacheService.uriPersonCache.set(uri, exist);
@ -243,6 +220,23 @@ export class ApPersonService implements OnModuleInit {
return null; return null;
} }
private async resolveAvatarAndBanner(user: RemoteUser, icon: any, image: any): Promise<Pick<RemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
const [avatar, banner] = await Promise.all([icon, image].map(img => {
if (img == null) return null;
if (user == null) throw new Error('failed to create user: user is null');
return this.apImageService.resolveImage(user, img).catch(() => null);
}));
return {
avatarId: avatar?.id ?? null,
bannerId: banner?.id ?? null,
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
avatarBlurhash: avatar?.blurhash ?? null,
bannerBlurhash: banner?.blurhash ?? null,
};
}
/** /**
* Personを作成します * Personを作成します
*/ */
@ -254,9 +248,11 @@ export class ApPersonService implements OnModuleInit {
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
} }
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri) as any; const object = await resolver.resolve(uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
const person = this.validateActor(object, uri); const person = this.validateActor(object, uri);
@ -264,9 +260,9 @@ export class ApPersonService implements OnModuleInit {
const host = this.punyHost(object.id); const host = this.punyHost(object.id);
const { fields } = this.analyzeAttachments(person.attachment ?? []); const fields = this.analyzeAttachments(person.attachment ?? []);
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const isBot = getApType(object) === 'Service'; const isBot = getApType(object) === 'Service';
@ -279,7 +275,17 @@ export class ApPersonService implements OnModuleInit {
} }
// Create user // Create user
let user: RemoteUser; let user: RemoteUser | null = null;
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host)
.then(_emojis => _emojis.map(emoji => emoji.name))
.catch(err => {
this.logger.error(`error occured while fetching user emojis`, { stack: err });
return [];
});
//#endregion
try { try {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
@ -290,30 +296,31 @@ export class ApPersonService implements OnModuleInit {
createdAt: new Date(), createdAt: new Date(),
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
name: truncate(person.name, nameLength), name: truncate(person.name, nameLength),
isLocked: !!person.manuallyApprovesFollowers, isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo, movedToUri: person.movedTo,
movedAt: person.movedTo ? new Date() : null, movedAt: person.movedTo ? new Date() : null,
alsoKnownAs: person.alsoKnownAs, alsoKnownAs: person.alsoKnownAs,
isExplorable: !!person.discoverable, isExplorable: person.discoverable,
username: person.preferredUsername, username: person.preferredUsername,
usernameLower: person.preferredUsername!.toLowerCase(), usernameLower: person.preferredUsername?.toLowerCase(),
host, host,
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
followersUri: person.followers ? getApId(person.followers) : undefined, followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured ? getApId(person.featured) : undefined, featured: person.featured ? getApId(person.featured) : undefined,
uri: person.id, uri: person.id,
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
emojis,
})) as RemoteUser; })) as RemoteUser;
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
userId: user.id, userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url: url, url,
fields, fields,
birthday: bday ? bday[0] : null, birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
userHost: host, userHost: host,
})); }));
@ -330,21 +337,21 @@ export class ApPersonService implements OnModuleInit {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
const u = await this.usersRepository.findOneBy({ const u = await this.usersRepository.findOneBy({ uri: person.id });
uri: person.id, if (u == null) throw new Error('already registered');
});
if (u) {
user = u as RemoteUser; user = u as RemoteUser;
} else {
throw new Error('already registered');
}
} else { } else {
this.logger.error(e instanceof Error ? e : new Error(e as string)); this.logger.error(e instanceof Error ? e : new Error(e as string));
throw e; throw e;
} }
} }
if (user == null) throw new Error('failed to create user: user is null');
// Register to the cache
this.cacheService.uriPersonCache.set(user.uri, user);
// Register host // Register host
this.federatedInstanceService.fetch(host).then(async i => { this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
@ -354,61 +361,27 @@ export class ApPersonService implements OnModuleInit {
} }
}); });
this.usersChart.update(user!, true); this.usersChart.update(user, true);
// ハッシュタグ更新 // ハッシュタグ更新
this.hashtagService.updateUsertags(user!, tags); this.hashtagService.updateUsertags(user, tags);
//#region アバターとヘッダー画像をフェッチ //#region アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([ try {
person.icon, const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image);
person.image, await this.usersRepository.update(user.id, updates);
].map(img => user = { ...user, ...updates };
img == null
? Promise.resolve(null)
: this.apImageService.resolveImage(user!, img).catch(() => null),
));
const avatarId = avatar ? avatar.id : null; // Register to the cache
const bannerId = banner ? banner.id : null; this.cacheService.uriPersonCache.set(user.uri, user);
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null; } catch (err) {
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null; this.logger.error('error occured while fetching user avatar/banner', { stack: err });
const avatarBlurhash = avatar ? avatar.blurhash : null; }
const bannerBlurhash = banner ? banner.blurhash : null;
await this.usersRepository.update(user!.id, {
avatarId,
bannerId,
avatarUrl,
bannerUrl,
avatarBlurhash,
bannerBlurhash,
});
user!.avatarId = avatarId;
user!.bannerId = bannerId;
user!.avatarUrl = avatarUrl;
user!.bannerUrl = bannerUrl;
user!.avatarBlurhash = avatarBlurhash;
user!.bannerBlurhash = bannerBlurhash;
//#endregion //#endregion
//#region カスタム絵文字取得 await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name); return user;
await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
return user!;
} }
/** /**
@ -426,18 +399,14 @@ export class ApPersonService implements OnModuleInit {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
if (uri.startsWith(`${this.config.url}/`)) { if (uri.startsWith(`${this.config.url}/`)) return;
return;
}
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; const exist = await this.fetchPerson(uri) as RemoteUser | null;
if (exist === null) return;
if (exist === null) {
return;
}
//#endregion //#endregion
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri); const object = hint ?? await resolver.resolve(uri);
@ -446,27 +415,17 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the Person: ${person.id}`); this.logger.info(`Updating the Person: ${person.id}`);
// アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([
person.icon,
person.image,
].map(img =>
img == null
? Promise.resolve(null)
: this.apImageService.resolveImage(exist, img).catch(() => null),
));
// カスタム絵文字取得 // カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return [] as Emoji[]; return [];
}); });
const emojiNames = emojis.map(emoji => emoji.name); const emojiNames = emojis.map(emoji => emoji.name);
const { fields } = this.analyzeAttachments(person.attachment ?? []); const fields = this.analyzeAttachments(person.attachment ?? []);
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@ -479,7 +438,7 @@ export class ApPersonService implements OnModuleInit {
const updates = { const updates = {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
followersUri: person.followers ? getApId(person.followers) : undefined, followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured, featured: person.featured,
emojis: emojiNames, emojis: emojiNames,
@ -487,33 +446,33 @@ export class ApPersonService implements OnModuleInit {
tags, tags,
isBot: getApType(object) === 'Service', isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers, isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null, movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null, alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: !!person.discoverable, isExplorable: person.discoverable,
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; } as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
const moving = const moving = ((): boolean => {
// 移行先がない→ある // 移行先がない→ある
(!exist.movedToUri && updates.movedToUri) || if (
exist.movedToUri === null &&
updates.movedToUri
) return true;
// 移行先がある→別のもの // 移行先がある→別のもの
(exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri); if (
exist.movedToUri !== null &&
updates.movedToUri !== null &&
exist.movedToUri !== updates.movedToUri
) return true;
// 移行先がある→ない、ない→ないは無視 // 移行先がある→ない、ない→ないは無視
return false;
})();
if (moving) updates.movedAt = new Date(); if (moving) updates.movedAt = new Date();
if (avatar) {
updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
updates.avatarBlurhash = avatar.blurhash;
}
if (banner) {
updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
updates.bannerBlurhash = banner.blurhash;
}
// Update user // Update user
await this.usersRepository.update(exist.id, updates); await this.usersRepository.update(exist.id, updates);
@ -525,10 +484,10 @@ export class ApPersonService implements OnModuleInit {
} }
await this.userProfilesRepository.update({ userId: exist.id }, { await this.userProfilesRepository.update({ userId: exist.id }, {
url: url, url,
fields, fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
birthday: bday ? bday[0] : null, birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
}); });
@ -538,11 +497,10 @@ export class ApPersonService implements OnModuleInit {
this.hashtagService.updateUsertags(exist, tags); this.hashtagService.updateUsertags(exist, tags);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await this.followingsRepository.update({ await this.followingsRepository.update(
followerId: exist.id, { followerId: exist.id },
}, { { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), );
});
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
@ -580,27 +538,22 @@ export class ApPersonService implements OnModuleInit {
*/ */
@bindThis @bindThis
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> { public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchPerson(uri); const exist = await this.fetchPerson(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
return await this.createPerson(uri, resolver); return await this.createPerson(uri, resolver);
} }
@bindThis @bindThis
public analyzeAttachments(attachments: IObject | IObject[] | undefined) { // TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか?
const fields: { public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] {
name: string, const fields: Field[] = [];
value: string
}[] = [];
if (Array.isArray(attachments)) { if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) { for (const attachment of attachments.filter(isPropertyValue)) {
fields.push({ fields.push({
@ -610,11 +563,11 @@ export class ApPersonService implements OnModuleInit {
} }
} }
return { fields }; return fields;
} }
@bindThis @bindThis
public async updateFeatured(userId: User['id'], resolver?: Resolver) { public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> {
const user = await this.usersRepository.findOneByOrFail({ id: userId }); const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (!this.userEntityService.isRemoteUser(user)) return; if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
@ -636,20 +589,23 @@ export class ApPersonService implements OnModuleInit {
const featuredNotes = await Promise.all(items const featuredNotes = await Promise.all(items
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
.slice(0, 5) .slice(0, 5)
.map(item => limit(() => this.apNoteService.resolveNote(item, _resolver)))); .map(item => limit(() => this.apNoteService.resolveNote(item, {
resolver: _resolver,
sentFrom: new URL(user.uri),
}))));
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持
let td = 0; let td = 0;
for (const note of featuredNotes.filter(note => note != null)) { for (const note of featuredNotes.filter((note): note is Note => note != null)) {
td -= 1000; td -= 1000;
transactionalEntityManager.insert(UserNotePining, { transactionalEntityManager.insert(UserNotePining, {
id: this.idService.genId(new Date(Date.now() + td)), id: this.idService.genId(new Date(Date.now() + td)),
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
noteId: note!.id, noteId: note.id,
}); });
} }
}); });

View file

@ -4,12 +4,12 @@ import type { NotesRepository, PollsRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { IPoll } from '@/models/entities/Poll.js'; import type { IPoll } from '@/models/entities/Poll.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { isQuestion } from '../type.js'; import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
import type { Resolver } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js';
import type { IObject, IQuestion } from '../type.js'; import type { IObject, IQuestion } from '../type.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ApQuestionService { export class ApQuestionService {
@ -33,33 +33,25 @@ export class ApQuestionService {
@bindThis @bindThis
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> { public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(source); const question = await resolver.resolve(source);
if (!isQuestion(question)) throw new Error('invalid type');
if (!isQuestion(question)) { const multiple = question.oneOf === undefined;
throw new Error('invalid type'); if (multiple && question.anyOf === undefined) throw new Error('invalid question');
}
const multiple = !question.oneOf;
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
if (multiple && !question.anyOf) { const choices = question[multiple ? 'anyOf' : 'oneOf']
throw new Error('invalid question'); ?.map((x) => x.name)
} .filter((x): x is string => typeof x === 'string')
?? [];
const choices = question[multiple ? 'anyOf' : 'oneOf']! const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
.map((x, i) => x.name!);
const votes = question[multiple ? 'anyOf' : 'oneOf']! return { choices, votes, multiple, expiresAt };
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
return {
choices,
votes,
multiple,
expiresAt,
};
} }
/** /**
@ -68,8 +60,9 @@ export class ApQuestionService {
* @returns true if updated * @returns true if updated
*/ */
@bindThis @bindThis
public async updateQuestion(value: any, resolver?: Resolver) { public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('uri is null');
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local'); if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
@ -83,6 +76,7 @@ export class ApQuestionService {
//#endregion //#endregion
// resolve new Question object // resolve new Question object
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(value) as IQuestion; const question = await resolver.resolve(value) as IQuestion;
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
@ -90,12 +84,14 @@ export class ApQuestionService {
if (question.type !== 'Question') throw new Error('object is not a Question'); if (question.type !== 'Question') throw new Error('object is not a Question');
const apChoices = question.oneOf ?? question.anyOf; const apChoices = question.oneOf ?? question.anyOf;
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
let changed = false; let changed = false;
for (const choice of poll.choices) { for (const choice of poll.choices) {
const oldCount = poll.votes[poll.choices.indexOf(choice)]; const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
if (newCount == null) throw new Error('invalid newCount: ' + newCount);
if (oldCount !== newCount) { if (oldCount !== newCount) {
changed = true; changed = true;
@ -103,9 +99,7 @@ export class ApQuestionService {
} }
} }
await this.pollsRepository.update({ noteId: note.id }, { await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes });
votes: poll.votes,
});
return changed; return changed;
} }

View file

@ -2,7 +2,7 @@ import { toArray } from '@/misc/prelude/array.js';
import { isHashtag } from '../type.js'; import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js';
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] {
if (tags == null) return []; if (tags == null) return [];
const hashtags = extractApHashtagObjects(tags); const hashtags = extractApHashtagObjects(tags);

View file

@ -194,7 +194,6 @@ export interface IApPropertyValue extends IObject {
} }
export const isPropertyValue = (object: IObject): object is IApPropertyValue => export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
object &&
getApType(object) === 'PropertyValue' && getApType(object) === 'PropertyValue' &&
typeof object.name === 'string' && typeof object.name === 'string' &&
'value' in object && 'value' in object &&

Some files were not shown because too many files have changed in this diff Show more