Merge branch 'develop' into mkusername-empty

This commit is contained in:
Kagami Sascha Rosylight 2023-03-06 07:51:17 +01:00 committed by GitHub
commit 5651353c27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
201 changed files with 4852 additions and 2521 deletions

View file

@ -7,5 +7,18 @@
"ghcr.io/devcontainers-contrib/features/pnpm:2": {} "ghcr.io/devcontainers-contrib/features/pnpm:2": {}
}, },
"forwardPorts": [3000], "forwardPorts": [3000],
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh" "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
"customizations": {
"vscode": {
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"Orta.vscode-jest",
"dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"
]
}
}
} }

View file

@ -16,7 +16,7 @@ services:
- external_network - external_network
redis: redis:
restart: always restart: unless-stopped
image: redis:7-alpine image: redis:7-alpine
networks: networks:
- internal_network - internal_network

View file

@ -4,6 +4,7 @@ set -xe
sudo chown -R node /workspace sudo chown -R node /workspace
git submodule update --init git submodule update --init
pnpm config set store-dir /home/node/.local/share/pnpm/store
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml cp .devcontainer/devcontainer.yml .config/default.yml
pnpm build pnpm build

View file

@ -25,6 +25,8 @@ fluent-emojis/
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.pnpm-store
.idea/ .idea/
packages/*/.vscode/ packages/*/.vscode/
packages/backend/test/docker-compose.yml packages/backend/test/docker-compose.yml

View file

@ -7,5 +7,5 @@ charset = utf-8
insert_final_newline = true insert_final_newline = true
end_of_line = lf end_of_line = lf
[*.yml] [*.{yml,yaml}]
indent_style = space indent_style = space

View file

@ -1,10 +1,20 @@
# Summary ## Summary
This is a release PR. This is a release PR.
For more information on the release instructions, please see: For more information on the release instructions, please see:
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md#release https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md#release
# Checklist ## For reviewers
- CHANGELOGに抜け漏れは無いか
- バージョンの上げ方は適切か
- 他にこのリリースに含めなければならない変更は無いか
- 全体的な変更内容を俯瞰し問題は無いか
- レビューされていないコミットがある場合は、それが問題ないか
- 最終的な動作確認を行い問題は無いか
などを確認し、リリースする準備が整っていると思われる場合は approve してください。
## Checklist
- [ ] package.jsonのバージョンが正しく更新されている - [ ] package.jsonのバージョンが正しく更新されている
- [ ] CHANGELOGが過不足無く更新されている - [ ] CHANGELOGが過不足無く更新されている
- [ ] CIが全て通っている - [ ] CIが全て通っている

10
.github/reviewer-lottery.yml vendored Normal file
View file

@ -0,0 +1,10 @@
groups:
- name: devs
reviewers: 2
internal_reviewers: 1
usernames:
- syuilo
- acid-chicken
- EbiseLutica
- rinsuki
- tamaina

13
.github/workflows/reviewer_lottery.yml vendored Normal file
View file

@ -0,0 +1,13 @@
name: "Reviewer lottery"
on:
pull_request_target:
types: [opened, ready_for_review, reopened]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: uesteibar/reviewer-lottery@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View file

@ -20,6 +20,9 @@ packages/frontend/.yarn/cache
packages/backend/.yarn/cache packages/backend/.yarn/cache
packages/sw/.yarn/cache packages/sw/.yarn/cache
# pnpm
.pnpm-store
# Cypress # Cypress
cypress/screenshots cypress/screenshots
cypress/videos cypress/videos

View file

@ -1,9 +1,11 @@
{ {
"recommendations": [ "recommendations": [
"editorconfig.editorconfig", "editorconfig.editorconfig",
"eg2.vscode-npm-script",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"Vue.volar", "Vue.volar",
"Vue.vscode-typescript-vue-plugin" "Vue.vscode-typescript-vue-plugin",
"Orta.vscode-jest",
"dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"
] ]
} }

View file

@ -2,5 +2,9 @@
"search.exclude": { "search.exclude": {
"**/node_modules": true "**/node_modules": true
}, },
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
"files.associations": {
"*.test.ts": "typescript"
},
"jest.autoRun": "off"
} }

View file

@ -2,17 +2,61 @@
## 13.x.x (unreleased) ## 13.x.x (unreleased)
### Improvements ### Improvements
- feat: 検索画面の統合 (Khsmty) -
### Bugfixes ### Bugfixes
- x
You should also include the user name that made the change. You should also include the user name that made the change.
--> -->
## 13.x.x (unreleased)
## 13.9.2 (2023/03/06)
### Improvements
- クリップ、チャンネルページに共有ボタンを追加
- チャンネルでタイムライン上部に投稿フォームを表示するかどうかのオプションを追加
- ブラウザでメディアプロキシ(/proxy)からファイルを保存した際に、なるべくオリジナルのファイル名を継承するように
- ドライブの「URLからアップロード」で、content-dispositionのfilenameがあればそれをファイル名に
- Identiconがローカルとリモートで同じになるように
- これまでのIdenticonは異なる画像になります
- サーバーのパフォーマンスを改善
### Bugfixes
- ロールの権限で「一般ユーザー」のロールがいきなり設定できない問題を修正
- ユーザーページのバッジ表示を適切に折り返すように @arrow2nd
- fix(client): みつけるのロール一覧でコンディショナルロールが含まれるのを修正
- macOSでDev Containerが動作しない問題を修正 @RyotaK
## 13.9.1 (2023/03/03)
### Bugfixes
- ノートに添付したファイルが表示されない場合があるのを修正
## 13.9.0 (2023/03/03)
### Improvements
- 時限ロール
- アンテナでCWも検索対象にするように
- ノートの操作部をホバー時のみ表示するオプションを追加
- サウンドを追加
- サーバーのパフォーマンスを改善
### Bugfixes
- 外部メディアプロキシ使用時にアバタークロップができない問題を修正
- fix(server): メールアドレス更新時にバリデーションが正しく行われていないのを修正
- fix(server): チャンネルでミュートが正しく機能していないのを修正
- プッシュ通知でカスタム絵文字リアクションを表示できなかった問題を修正
## 13.8.1 (2023/02/26)
### Bugfixes
- モバイルでドロワーメニューが表示されない問題を修正
## 13.8.0 (2023/02/26)
### Improvements ### Improvements
- チャンネル内ハイライト - チャンネル内ハイライト
- ホームタイムラインのパフォーマンスを改善
- renoteした際の表示を改善 - renoteした際の表示を改善
- バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない - バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない
- enhance(client): MkUrlPreviewの閉じるボタンを見やすく - enhance(client): MkUrlPreviewの閉じるボタンを見やすく
@ -20,12 +64,15 @@ You should also include the user name that made the change.
- enhance(client): improve clip menu ux - enhance(client): improve clip menu ux
- 検索画面の統合 - 検索画面の統合
- enhance(client): ノートメニューからユーザーメニューを開けるように - enhance(client): ノートメニューからユーザーメニューを開けるように
- photoswipe 表示時に戻る操作をしても前の画面に戻らないように
### Bugfixes ### Bugfixes
- Windows環境でswcを使うと正しくビルドできない問題の修正 - Windows環境でswcを使うと正しくビルドできない問題の修正
- fix(client): Android ChromeでPWAとしてインストールできない問題を修正 - fix(client): Android ChromeでPWAとしてインストールできない問題を修正
- 未知のユーザーが deleteActor されたら処理をスキップする - 未知のユーザーが deleteActor されたら処理をスキップする
- fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする - fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする
- fix(server): notes/createのバリデーションが機能していないのを修正
- fix(server): エラーのスタックトレースは返さないように
## 13.7.5 (2023/02/24) ## 13.7.5 (2023/02/24)

View file

@ -15,7 +15,7 @@ Before creating an issue, please check the following:
- To avoid duplication, please search for similar issues before creating a new issue. - To avoid duplication, please search for similar issues before creating a new issue.
- Do not use Issues to ask questions or troubleshooting. - Do not use Issues to ask questions or troubleshooting.
- Issues should only be used to feature requests, suggestions, and bug tracking. - Issues should only be used to feature requests, suggestions, and bug tracking.
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3). - Please ask questions or troubleshooting in ~~the [Misskey Forum](https://forum.misskey.io/)~~ [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3).
> **Warning** > **Warning**
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
@ -299,6 +299,27 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
- 生成後、ファイルをmigration下に移してください - 生成後、ファイルをmigration下に移してください
- 作成されたスクリプトは不必要な変更を含むため除去してください - 作成されたスクリプトは不必要な変更を含むため除去してください
### JSON SchemaのobjectでanyOfを使うとき
JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
バリデーションが効かないため。SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます
https://github.com/misskey-dev/misskey/pull/10082
テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合:
```
export const paramDef = {
type: 'object',
properties: {
hoge: { type: 'string', minLength: 1 },
fuga: { type: 'string', minLength: 1 },
},
anyOf: [
{ required: ['hoge'] },
{ required: ['fuga'] },
],
} as const;
```
### コネクションには`markRaw`せよ ### コネクションには`markRaw`せよ
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。 **Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。

View file

@ -2,7 +2,9 @@
ARG NODE_VERSION=18.13.0-bullseye ARG NODE_VERSION=18.13.0-bullseye
FROM node:${NODE_VERSION} AS builder # build assets & compile TypeScript
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS native-builder
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \
@ -33,33 +35,49 @@ RUN git submodule update --init
RUN pnpm build RUN pnpm build
RUN rm -rf .git/ RUN rm -rf .git/
FROM node:${NODE_VERSION}-slim AS runner # build native dependencies for target platform
FROM --platform=$TARGETPLATFORM node:${NODE_VERSION} AS target-builder
RUN apt-get update \
&& apt-get install -yqq --no-install-recommends \
build-essential
RUN corepack enable
WORKDIR /misskey
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
COPY --link ["scripts", "./scripts"]
COPY --link ["packages/backend/package.json", "./packages/backend/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output
FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner
ARG UID="991" ARG UID="991"
ARG GID="991" ARG GID="991"
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN apt-get update \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
ffmpeg tini curl \ ffmpeg tini curl \
&& corepack enable \ && corepack enable \
&& groupadd -g "${GID}" misskey \ && groupadd -g "${GID}" misskey \
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
&& find / -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ && find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
&& find / -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; && find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists
USER misskey USER misskey
WORKDIR /misskey WORKDIR /misskey
COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=builder /misskey/built ./built COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/built ./packages/backend/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
COPY --chown=misskey:misskey --from=builder /misskey/packages/frontend/node_modules ./packages/frontend/node_modules COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
COPY --chown=misskey:misskey --from=builder /misskey/fluent-emojis /misskey/fluent-emojis
COPY --chown=misskey:misskey . ./ COPY --chown=misskey:misskey . ./
ENV NODE_ENV=production ENV NODE_ENV=production

View file

@ -3,16 +3,16 @@ kind: Deployment
metadata: metadata:
name: {{ include "misskey.fullname" . }} name: {{ include "misskey.fullname" . }}
labels: labels:
{{- include "misskey.labels" . | nindent 4 }} {{- include "misskey.labels" . | nindent 4 }}
spec: spec:
selector: selector:
matchLabels: matchLabels:
{{- include "misskey.selectorLabels" . | nindent 6 }} {{- include "misskey.selectorLabels" . | nindent 6 }}
replicas: 1 replicas: 1
template: template:
metadata: metadata:
labels: labels:
{{- include "misskey.selectorLabels" . | nindent 8 }} {{- include "misskey.selectorLabels" . | nindent 8 }}
spec: spec:
containers: containers:
- name: misskey - name: misskey

View file

@ -11,4 +11,4 @@ spec:
protocol: TCP protocol: TCP
name: http name: http
selector: selector:
{{- include "misskey.selectorLabels" . | nindent 4 }} {{- include "misskey.selectorLabels" . | nindent 4 }}

View file

@ -1,6 +1,4 @@
coverage: coverage:
status: status:
project: project: false
default: patch: false
only_pulls: true
if_ci_failed: success

View file

@ -10,14 +10,14 @@ describe('Before setup instance', () => {
}); });
it('successfully loads', () => { it('successfully loads', () => {
cy.visit('/'); cy.visitHome();
}); });
it('setup instance', () => { it('setup instance', () => {
cy.visit('/'); cy.visitHome();
cy.intercept('POST', '/api/admin/accounts/create').as('signup'); cy.intercept('POST', '/api/admin/accounts/create').as('signup');
cy.get('[data-cy-admin-username] input').type('admin'); cy.get('[data-cy-admin-username] input').type('admin');
cy.get('[data-cy-admin-password] input').type('admin1234'); cy.get('[data-cy-admin-password] input').type('admin1234');
cy.get('[data-cy-admin-ok]').click(); cy.get('[data-cy-admin-ok]').click();
@ -43,11 +43,11 @@ describe('After setup instance', () => {
}); });
it('successfully loads', () => { it('successfully loads', () => {
cy.visit('/'); cy.visitHome();
}); });
it('signup', () => { it('signup', () => {
cy.visit('/'); cy.visitHome();
cy.intercept('POST', '/api/signup').as('signup'); cy.intercept('POST', '/api/signup').as('signup');
@ -79,11 +79,11 @@ describe('After user signup', () => {
}); });
it('successfully loads', () => { it('successfully loads', () => {
cy.visit('/'); cy.visitHome();
}); });
it('signin', () => { it('signin', () => {
cy.visit('/'); cy.visitHome();
cy.intercept('POST', '/api/signin').as('signin'); cy.intercept('POST', '/api/signin').as('signin');
@ -101,7 +101,7 @@ describe('After user signup', () => {
userId: this.alice.id, userId: this.alice.id,
}); });
cy.visit('/'); cy.visitHome();
cy.get('[data-cy-signin]').click(); cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice'); cy.get('[data-cy-signin-username] input').type('alice');
@ -112,7 +112,7 @@ describe('After user signup', () => {
}); });
}); });
describe('After user singed in', () => { describe('After user signed in', () => {
beforeEach(() => { beforeEach(() => {
cy.resetState(); cy.resetState();
@ -141,6 +141,19 @@ describe('After user singed in', () => {
cy.get('[data-cy-open-post-form-submit]').click(); cy.get('[data-cy-open-post-form-submit]').click();
cy.contains('Hello, Misskey!'); cy.contains('Hello, Misskey!');
});
it('open note form with hotkey', () => {
// Wait until the page loads
cy.get('[data-cy-open-post-form]').should('be.visible');
// Use trigger() to give different `code` to test if hotkeys also work on non-QWERTY keyboards.
cy.document().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "n", code: "KeyL" });
// See if the form is opened
cy.get('[data-cy-post-form-text]').should('be.visible');
// Close it
cy.focused().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "Escape", code: "Escape" });
// See if the form is closed
cy.get('[data-cy-post-form-text]').should('not.be.visible');
}); });
}); });

View file

@ -24,6 +24,11 @@
// -- This will overwrite an existing command -- // -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
Cypress.Commands.add('visitHome', () => {
cy.visit('/');
cy.get('button', { timeout: 30000 }).should('be.visible');
})
Cypress.Commands.add('resetState', () => { Cypress.Commands.add('resetState', () => {
cy.window(win => { cy.window(win => {
win.indexedDB.deleteDatabase('keyval-store'); win.indexedDB.deleteDatabase('keyval-store');
@ -43,7 +48,7 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
}); });
Cypress.Commands.add('login', (username, password) => { Cypress.Commands.add('login', (username, password) => {
cy.visit('/'); cy.visitHome();
cy.intercept('POST', '/api/signin').as('signin'); cy.intercept('POST', '/api/signin').as('signin');

View file

@ -785,6 +785,7 @@ size: "الحجم"
numberOfColumn: "عدد الأعمدة" numberOfColumn: "عدد الأعمدة"
searchByGoogle: "غوغل" searchByGoogle: "غوغل"
mutePeriod: "مدة الكتم" mutePeriod: "مدة الكتم"
period: "ينتهي استطلاع الرأي في"
indefinitely: "أبدًا" indefinitely: "أبدًا"
tenMinutes: "10 دقائق" tenMinutes: "10 دقائق"
oneHour: "ساعة" oneHour: "ساعة"
@ -971,6 +972,7 @@ _ago:
weeksAgo: "منذ {n} أسابيع" weeksAgo: "منذ {n} أسابيع"
monthsAgo: "منذ {n} أشهر" monthsAgo: "منذ {n} أشهر"
yearsAgo: "منذ {n} سنوات" yearsAgo: "منذ {n} سنوات"
invalid: "لا يوجد شيء هنا"
_time: _time:
second: "ثا" second: "ثا"
minute: "د" minute: "د"

View file

@ -819,6 +819,7 @@ instanceDefaultLightTheme: "ইন্সট্যান্সের ডিফল
instanceDefaultDarkTheme: "ইন্সট্যান্সের ডিফল্ট ডার্ক থিম" instanceDefaultDarkTheme: "ইন্সট্যান্সের ডিফল্ট ডার্ক থিম"
instanceDefaultThemeDescription: "অবজেক্ট ফরম্যাটে থিম কোড লিখুন" instanceDefaultThemeDescription: "অবজেক্ট ফরম্যাটে থিম কোড লিখুন"
mutePeriod: "মিউটের সময়কাল" mutePeriod: "মিউটের সময়কাল"
period: "পোলের সময়সীমা"
indefinitely: "অনির্দিষ্ট" indefinitely: "অনির্দিষ্ট"
tenMinutes: "১০ মিনিট" tenMinutes: "১০ মিনিট"
oneHour: "১ ঘণ্টা" oneHour: "১ ঘণ্টা"
@ -1033,6 +1034,7 @@ _ago:
weeksAgo: "{n} সপ্তাহ আগে" weeksAgo: "{n} সপ্তাহ আগে"
monthsAgo: "{n} মাস আগে" monthsAgo: "{n} মাস আগে"
yearsAgo: "{n} বছর আগে" yearsAgo: "{n} বছর আগে"
invalid: "এখানে কিছুই নাই"
_time: _time:
second: "সেকেন্ড" second: "সেকেন্ড"
minute: "মিনিট" minute: "মিনিট"

View file

@ -659,6 +659,7 @@ _sfx:
_ago: _ago:
future: "Budoucí" future: "Budoucí"
justNow: "Teď" justNow: "Teď"
invalid: "Nic nebylo nalezeno"
_time: _time:
second: "Sekund" second: "Sekund"
minute: "Minut" minute: "Minut"

View file

@ -345,7 +345,7 @@ basicInfo: "Grundlegende Informationen"
pinnedUsers: "Angeheftete Benutzer" pinnedUsers: "Angeheftete Benutzer"
pinnedUsersDescription: "Gib durch Leerzeichen getrennte Benutzer an, die an die \"Erkunden\"-Seite angeheftet werden sollen." pinnedUsersDescription: "Gib durch Leerzeichen getrennte Benutzer an, die an die \"Erkunden\"-Seite angeheftet werden sollen."
pinnedPages: "Angeheftete Seiten" pinnedPages: "Angeheftete Seiten"
pinnedPagesDescription: "Gib durch Leerzeilen getrennte Pfäde zu Seiten an, die an die Startseite dieser Instanz angeheftet werden sollen.\n" pinnedPagesDescription: "Gib durch Leerzeilen getrennte Pfade zu Seiten an, die an die Startseite dieser Instanz angeheftet werden sollen."
pinnedClipId: "ID des anzuheftenden Clips" pinnedClipId: "ID des anzuheftenden Clips"
pinnedNotes: "Angeheftete Notizen" pinnedNotes: "Angeheftete Notizen"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
@ -393,13 +393,19 @@ about: "Über"
aboutMisskey: "Über Misskey" aboutMisskey: "Über Misskey"
administrator: "Administrator" administrator: "Administrator"
token: "Token" token: "Token"
2fa: "Zwei-Faktor-Authentifizierung"
totp: "Authentifizierungs-App"
totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein"
moderator: "Moderator" moderator: "Moderator"
moderation: "Moderation" moderation: "Moderation"
nUsersMentioned: "Von {n} Benutzern erwähnt" nUsersMentioned: "Von {n} Benutzern erwähnt"
securityKeyAndPasskey: "Security-Tokens und Passkeys"
securityKey: "Sicherheitsschlüssel" securityKey: "Sicherheitsschlüssel"
lastUsed: "Zuletzt benutzt" lastUsed: "Zuletzt benutzt"
lastUsedAt: "Zuletzt verwendet: {t}"
unregister: "Deaktivieren" unregister: "Deaktivieren"
passwordLessLogin: "Passwortloses Anmelden einrichten" passwordLessLogin: "Passwortloses Anmelden"
passwordLessLoginDescription: "Ermöglicht passwortfreies Einloggen, nur via Security-Token oder Passkey"
resetPassword: "Passwort zurücksetzen" resetPassword: "Passwort zurücksetzen"
newPasswordIs: "Das neue Passwort ist „{password}“" newPasswordIs: "Das neue Passwort ist „{password}“"
reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren" reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren"
@ -451,6 +457,7 @@ aboutX: "Über {x}"
emojiStyle: "Emoji-Stil" emojiStyle: "Emoji-Stil"
native: "Nativ" native: "Nativ"
disableDrawer: "Keine ausfahrbaren Menüs verwenden" disableDrawer: "Keine ausfahrbaren Menüs verwenden"
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen"
noHistory: "Kein Verlauf gefunden" noHistory: "Kein Verlauf gefunden"
signinHistory: "Anmeldungsverlauf" signinHistory: "Anmeldungsverlauf"
enableAdvancedMfm: "Erweitertes MFM aktivieren" enableAdvancedMfm: "Erweitertes MFM aktivieren"
@ -499,6 +506,7 @@ objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen"
serverLogs: "Serverprotokolle" serverLogs: "Serverprotokolle"
deleteAll: "Alle löschen" deleteAll: "Alle löschen"
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen" showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)"
newNoteRecived: "Es gibt neue Notizen" newNoteRecived: "Es gibt neue Notizen"
sounds: "Töne" sounds: "Töne"
sound: "Töne" sound: "Töne"
@ -773,6 +781,7 @@ popularPosts: "Beliebte Beiträge"
shareWithNote: "Mit Notiz teilen" shareWithNote: "Mit Notiz teilen"
ads: "Werbung" ads: "Werbung"
expiration: "Frist" expiration: "Frist"
startingperiod: "Start"
memo: "Merkzettel" memo: "Merkzettel"
priority: "Priorität" priority: "Priorität"
high: "Hoch" high: "Hoch"
@ -805,6 +814,7 @@ lastCommunication: "Letzte Kommunikation"
resolved: "Gelöst" resolved: "Gelöst"
unresolved: "Ungelöst" unresolved: "Ungelöst"
breakFollow: "Follower entfernen" breakFollow: "Follower entfernen"
breakFollowConfirm: "Diesen Follower wirklich entfernen?"
itsOn: "Eingeschaltet" itsOn: "Eingeschaltet"
itsOff: "Ausgeschaltet" itsOff: "Ausgeschaltet"
emailRequiredForSignup: "Angabe einer Email-Adresse als benötigt markieren" emailRequiredForSignup: "Angabe einer Email-Adresse als benötigt markieren"
@ -839,11 +849,13 @@ instanceDefaultLightTheme: "Instanzweites Standardfarbschema (Hell)"
instanceDefaultDarkTheme: "Instanzweites Standardfarbschema (Dunkel)" instanceDefaultDarkTheme: "Instanzweites Standardfarbschema (Dunkel)"
instanceDefaultThemeDescription: "Gib den Farbschemencode im Objektformat ein." instanceDefaultThemeDescription: "Gib den Farbschemencode im Objektformat ein."
mutePeriod: "Stummschaltungsdauer" mutePeriod: "Stummschaltungsdauer"
period: "Zeitlimit"
indefinitely: "Dauerhaft" indefinitely: "Dauerhaft"
tenMinutes: "10 Minuten" tenMinutes: "10 Minuten"
oneHour: "Eine Stunde" oneHour: "Eine Stunde"
oneDay: "Einen Tag" oneDay: "Einen Tag"
oneWeek: "Eine Woche" oneWeek: "Eine Woche"
oneMonth: "1 Monat"
reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt." reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt."
failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden" failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden"
rateLimitExceeded: "Versuchsanzahl überschritten" rateLimitExceeded: "Versuchsanzahl überschritten"
@ -939,6 +951,14 @@ collapseRenotes: "Bereits gesehene Renotes verkürzt anzeigen"
internalServerError: "Serverinterner Fehler" internalServerError: "Serverinterner Fehler"
internalServerErrorDescription: "Im Server ist ein unerwarteter Fehler aufgetreten." internalServerErrorDescription: "Im Server ist ein unerwarteter Fehler aufgetreten."
copyErrorInfo: "Fehlerdetails kopieren" copyErrorInfo: "Fehlerdetails kopieren"
joinThisServer: "Bei dieser Instanz registrieren"
exploreOtherServers: "Eine andere Instanz finden"
letsLookAtTimeline: "Die Chronik durchstöbern"
disableFederationWarn: "Dies deaktiviert Föderation, aber alle Notizen bleiben, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird diese Option nicht benötigt."
invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren."
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
postToTheChannel: "In Kanal senden"
cannotBeChangedLater: "Kann später nicht mehr geändert werden."
_achievements: _achievements:
earnedAt: "Freigeschaltet am" earnedAt: "Freigeschaltet am"
_types: _types:
@ -1147,7 +1167,7 @@ _achievements:
description: "Du hast hier geklickt" description: "Du hast hier geklickt"
_justPlainLucky: _justPlainLucky:
title: "Pures Glück" title: "Pures Glück"
description: "Kann alle 10 Sekunden mit einer Warscheinlichkeit von 0.01% erhalten werden" description: "Kann alle 10 Sekunden mit einer Warscheinlichkeit von 0.005% erhalten werden"
_setNameToSyuilo: _setNameToSyuilo:
title: "Gottkomplex" title: "Gottkomplex"
description: "Setze deinen Namen auf \"syuilo\"" description: "Setze deinen Namen auf \"syuilo\""
@ -1452,6 +1472,7 @@ _ago:
weeksAgo: "vor {n} Woche(n)" weeksAgo: "vor {n} Woche(n)"
monthsAgo: "vor {n} Monat(en)" monthsAgo: "vor {n} Monat(en)"
yearsAgo: "vor {n} Jahr(en)" yearsAgo: "vor {n} Jahr(en)"
invalid: "Ungültig"
_time: _time:
second: "Sekunde(n)" second: "Sekunde(n)"
minute: "Minute(n)" minute: "Minute(n)"
@ -1485,14 +1506,29 @@ _tutorial:
step8_3: "Diese Einstellung kannst du jederzeit ändern." step8_3: "Diese Einstellung kannst du jederzeit ändern."
_2fa: _2fa:
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert." alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert."
registerTOTP: "Authentifizierungs-App registrieren"
passwordToTOTP: "Bitte Passwort eingeben"
step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät."
step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät."
step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren."
step2Url: "Nutzt du ein Desktopprogramm kannst du alternativ diese URL eingeben:" step2Url: "Nutzt du ein Desktopprogramm kannst du alternativ diese URL eingeben:"
step3Title: "Authentifizierungsscode eingeben"
step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird."
step4: "Alle folgenden Anmeldungsversuche werden ab sofort die Eingabe eines solchen Tokens benötigen." step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen."
securityKeyNotSupported: "Dein Browser unterstützt keine Security-Tokens."
registerTOTPBeforeKey: "Um einen Security-Token oder einen Passkey zu registrieren, musst du zuerst eine Authentifizierungs-App registrieren."
securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf deinem Gerät auch Anmeldung mit Hilfe eines FIDO2-kompatiblen Hardware-Sicherheitsschlüssels einrichten." securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf deinem Gerät auch Anmeldung mit Hilfe eines FIDO2-kompatiblen Hardware-Sicherheitsschlüssels einrichten."
removeKeyConfirm: "Das Backup {name} löschen?" chromePasskeyNotSupported: "Chrome-Passkeys werden zur Zeit nicht unterstützt."
renewTOTPCancel: "Nein, danke" registerSecurityKey: "Security-Token oder Passkey registrieren"
securityKeyName: "Schlüsselname eingeben"
tapSecurityKey: "Bitten folge den Anweisungen deines Browsers zur Registrierung"
removeKey: "Sicherheitsschlüssel entfernen"
removeKeyConfirm: "Den Schlüssel {name} wirklich löschen?"
whyTOTPOnlyRenew: "Solange ein Sicherheitsschlüssel registriert ist, kann die Authentifizierungs-App nicht entfernt werden."
renewTOTP: "Authentifizierungs-App neu einrichten"
renewTOTPConfirm: "Codes der bisherigen App werden hierdurch nutzlos"
renewTOTPOk: "Neu einrichten"
renewTOTPCancel: "Abbrechen"
_permissions: _permissions:
"read:account": "Deine Benutzerkontoinformationen lesen" "read:account": "Deine Benutzerkontoinformationen lesen"
"write:account": "Deine Benutzerkontoinformationen bearbeiten" "write:account": "Deine Benutzerkontoinformationen bearbeiten"
@ -1615,6 +1651,8 @@ _visibility:
followersDescription: "Nur für Follower sichtbar" followersDescription: "Nur für Follower sichtbar"
specified: "Direkt" specified: "Direkt"
specifiedDescription: "Nur für bestimmte Benutzer sichtbar" specifiedDescription: "Nur für bestimmte Benutzer sichtbar"
disableFederation: "Deförderiert"
disableFederationDescription: "Nicht an andere Instanzen übertragen"
_postForm: _postForm:
replyPlaceholder: "Dieser Notiz antworten …" replyPlaceholder: "Dieser Notiz antworten …"
quotePlaceholder: "Diese Notiz zitieren …" quotePlaceholder: "Diese Notiz zitieren …"
@ -1770,6 +1808,7 @@ _notification:
pollEnded: "Ende von Umfragen" pollEnded: "Ende von Umfragen"
receiveFollowRequest: "Erhaltene Follow-Anfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen"
followRequestAccepted: "Akzeptierte Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen"
achievementEarned: "Errungenschaft freigeschaltet"
app: "Benachrichtigungen von Apps" app: "Benachrichtigungen von Apps"
_actions: _actions:
followBack: "folgt dir nun auch" followBack: "folgt dir nun auch"
@ -1802,3 +1841,6 @@ _deck:
channel: "Kanal" channel: "Kanal"
mentions: "Erwähnungen" mentions: "Erwähnungen"
direct: "Direktnachrichten" direct: "Direktnachrichten"
_dialog:
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}"

View file

@ -197,7 +197,7 @@ clearQueueConfirmText: "Any undelivered notes remaining in the queue will not be
clearCachedFiles: "Clear cache" clearCachedFiles: "Clear cache"
clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?" clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
blockedInstances: "Blocked Instances" blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to block. Listed instances will no longer be able to communicate with this instance." blockedInstancesDescription: "List the hostnames of the instances that you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance."
muteAndBlock: "Mutes and Blocks" muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users" mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"
@ -393,13 +393,19 @@ about: "About"
aboutMisskey: "About Misskey" aboutMisskey: "About Misskey"
administrator: "Administrator" administrator: "Administrator"
token: "Token" token: "Token"
2fa: "Two-factor authentication"
totp: "Authenticator App"
totpDescription: "Use an authenticator app to enter one-time passwords"
moderator: "Moderator" moderator: "Moderator"
moderation: "Moderation" moderation: "Moderation"
nUsersMentioned: "Mentioned by {n} users" nUsersMentioned: "Mentioned by {n} users"
securityKeyAndPasskey: "Security- and passkeys"
securityKey: "Security key" securityKey: "Security key"
lastUsed: "Last used" lastUsed: "Last used"
lastUsedAt: "Last used: {t}"
unregister: "Unregister" unregister: "Unregister"
passwordLessLogin: "Password-less login" passwordLessLogin: "Password-less login"
passwordLessLoginDescription: "Allows password-less login using a security- or passkey only"
resetPassword: "Reset password" resetPassword: "Reset password"
newPasswordIs: "The new password is \"{password}\"" newPasswordIs: "The new password is \"{password}\""
reduceUiAnimation: "Reduce UI animations" reduceUiAnimation: "Reduce UI animations"
@ -451,6 +457,7 @@ aboutX: "About {x}"
emojiStyle: "Emoji style" emojiStyle: "Emoji style"
native: "Native" native: "Native"
disableDrawer: "Don't use drawer-style menus" disableDrawer: "Don't use drawer-style menus"
showNoteActionsOnlyHover: "Only show note actions on hover"
noHistory: "No history available" noHistory: "No history available"
signinHistory: "Login history" signinHistory: "Login history"
enableAdvancedMfm: "Enable advanced MFM" enableAdvancedMfm: "Enable advanced MFM"
@ -499,6 +506,7 @@ objectStorageSetPublicRead: "Set \"public-read\" on upload"
serverLogs: "Server logs" serverLogs: "Server logs"
deleteAll: "Delete all" deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline" showFixedPostForm: "Display the posting form at the top of the timeline"
showFixedPostFormInChannel: "Display the posting form at the top of the timeline (Channels)"
newNoteRecived: "There are new notes" newNoteRecived: "There are new notes"
sounds: "Sounds" sounds: "Sounds"
sound: "Sounds" sound: "Sounds"
@ -773,6 +781,7 @@ popularPosts: "Popular posts"
shareWithNote: "Share with note" shareWithNote: "Share with note"
ads: "Advertisements" ads: "Advertisements"
expiration: "Deadline" expiration: "Deadline"
startingperiod: "Start"
memo: "Memo" memo: "Memo"
priority: "Priority" priority: "Priority"
high: "High" high: "High"
@ -805,7 +814,7 @@ lastCommunication: "Last communication"
resolved: "Resolved" resolved: "Resolved"
unresolved: "Unresolved" unresolved: "Unresolved"
breakFollow: "Remove follower" breakFollow: "Remove follower"
breakFollowConfirm: "Are you sure want to remove follower?" breakFollowConfirm: "Really remove this follower?"
itsOn: "Enabled" itsOn: "Enabled"
itsOff: "Disabled" itsOff: "Disabled"
emailRequiredForSignup: "Require email address for sign-up" emailRequiredForSignup: "Require email address for sign-up"
@ -840,11 +849,13 @@ instanceDefaultLightTheme: "Instance-wide default light theme"
instanceDefaultDarkTheme: "Instance-wide default dark theme" instanceDefaultDarkTheme: "Instance-wide default dark theme"
instanceDefaultThemeDescription: "Enter the theme code in object format." instanceDefaultThemeDescription: "Enter the theme code in object format."
mutePeriod: "Mute duration" mutePeriod: "Mute duration"
period: "Time limit"
indefinitely: "Permanently" indefinitely: "Permanently"
tenMinutes: "10 minutes" tenMinutes: "10 minutes"
oneHour: "One hour" oneHour: "One hour"
oneDay: "One day" oneDay: "One day"
oneWeek: "One week" oneWeek: "One week"
oneMonth: "One month"
reflectMayTakeTime: "It may take some time for this to be reflected." reflectMayTakeTime: "It may take some time for this to be reflected."
failedToFetchAccountInformation: "Could not fetch account information" failedToFetchAccountInformation: "Could not fetch account information"
rateLimitExceeded: "Rate limit exceeded" rateLimitExceeded: "Rate limit exceeded"
@ -940,6 +951,14 @@ collapseRenotes: "Collapse renotes you've already seen"
internalServerError: "Internal Server Error" internalServerError: "Internal Server Error"
internalServerErrorDescription: "The server has run into an unexpected error." internalServerErrorDescription: "The server has run into an unexpected error."
copyErrorInfo: "Copy error details" copyErrorInfo: "Copy error details"
joinThisServer: "Sign up at this instance"
exploreOtherServers: "Look for another instance"
letsLookAtTimeline: "Have a look at the timeline"
disableFederationWarn: "This will disable federation, but posts will continue to be public unless set otherwise. You usually do not need to use this setting."
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
emailNotSupported: "This instance does not support sending emails"
postToTheChannel: "Post to channel"
cannotBeChangedLater: "This cannot be changed later."
_achievements: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@ -1453,6 +1472,7 @@ _ago:
weeksAgo: "{n}w ago" weeksAgo: "{n}w ago"
monthsAgo: "{n}mo ago" monthsAgo: "{n}mo ago"
yearsAgo: "{n}y ago" yearsAgo: "{n}y ago"
invalid: "None"
_time: _time:
second: "Second(s)" second: "Second(s)"
minute: "Minute(s)" minute: "Minute(s)"
@ -1486,14 +1506,29 @@ _tutorial:
step8_3: "You can always change this setting later." step8_3: "You can always change this setting later."
_2fa: _2fa:
alreadyRegistered: "You have already registered a 2-factor authentication device." alreadyRegistered: "You have already registered a 2-factor authentication device."
registerTOTP: "Register authenticator app"
passwordToTOTP: "Enter your password"
step1: "First, install an authentication app (such as {a} or {b}) on your device." step1: "First, install an authentication app (such as {a} or {b}) on your device."
step2: "Then, scan the QR code displayed on this screen." step2: "Then, scan the QR code displayed on this screen."
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app."
step2Url: "You can also enter this URL if you're using a desktop program:" step2Url: "You can also enter this URL if you're using a desktop program:"
step3Title: "Enter an authentication code"
step3: "Enter the token provided by your app to finish setup." step3: "Enter the token provided by your app to finish setup."
step4: "From now on, any future login attempts will ask for such a login token." step4: "From now on, any future login attempts will ask for such a login token."
securityKeyNotSupported: "Your browser does not support security keys."
registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key."
securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account." securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account."
removeKeyConfirm: "Delete the {name} backup?" chromePasskeyNotSupported: "Chrome passkeys are currently not supported."
renewTOTPCancel: "Not now" registerSecurityKey: "Register a security or pass key"
securityKeyName: "Enter a key name"
tapSecurityKey: "Please follow your browser to register the security or pass key"
removeKey: "Remove security key"
removeKeyConfirm: "Really delete the {name} key?"
whyTOTPOnlyRenew: "The authenticator app cannot be removed as long as a security key is registered."
renewTOTP: "Reconfigure authenticator app"
renewTOTPConfirm: "This will cause verification codes from your previous app to stop working"
renewTOTPOk: "Reconfigure"
renewTOTPCancel: "Cancel"
_permissions: _permissions:
"read:account": "View your account information" "read:account": "View your account information"
"write:account": "Edit your account information" "write:account": "Edit your account information"
@ -1616,6 +1651,8 @@ _visibility:
followersDescription: "Make visible to your followers only" followersDescription: "Make visible to your followers only"
specified: "Direct" specified: "Direct"
specifiedDescription: "Make visible for specified users only" specifiedDescription: "Make visible for specified users only"
disableFederation: "Unfederated"
disableFederationDescription: "Don't transmit to other instances"
_postForm: _postForm:
replyPlaceholder: "Reply to this note..." replyPlaceholder: "Reply to this note..."
quotePlaceholder: "Quote this note..." quotePlaceholder: "Quote this note..."
@ -1771,6 +1808,7 @@ _notification:
pollEnded: "Polls ending" pollEnded: "Polls ending"
receiveFollowRequest: "Received follow requests" receiveFollowRequest: "Received follow requests"
followRequestAccepted: "Accepted follow requests" followRequestAccepted: "Accepted follow requests"
achievementEarned: "Achievement unlocked"
app: "Notifications from linked apps" app: "Notifications from linked apps"
_actions: _actions:
followBack: "followed you back" followBack: "followed you back"
@ -1803,3 +1841,6 @@ _deck:
channel: "Channel" channel: "Channel"
mentions: "Mentions" mentions: "Mentions"
direct: "Direct notes" direct: "Direct notes"
_dialog:
charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}."
charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}."

View file

@ -103,6 +103,8 @@ renoted: "Renotado"
cantRenote: "No se puede renotar este post" cantRenote: "No se puede renotar este post"
cantReRenote: "No se puede renotar una renota" cantReRenote: "No se puede renotar una renota"
quote: "Citar" quote: "Citar"
inChannelRenote: "Renota sólo del canal"
inChannelQuote: "Cita sólo del canal"
pinnedNote: "Nota fijada" pinnedNote: "Nota fijada"
pinned: "Fijar al perfil" pinned: "Fijar al perfil"
you: "Tú" you: "Tú"
@ -391,13 +393,19 @@ about: "Información"
aboutMisskey: "Sobre Misskey" aboutMisskey: "Sobre Misskey"
administrator: "Administrador" administrator: "Administrador"
token: "Token" token: "Token"
2fa: "Autenticación de doble factor"
totp: "Aplicación autentícadora"
totpDescription: "Ingresa una contaseña de un sólo uso usando la aplicación autenticadora"
moderator: "Moderador" moderator: "Moderador"
moderation: "Moderación" moderation: "Moderación"
nUsersMentioned: "{n} usuarios mencionados" nUsersMentioned: "{n} usuarios mencionados"
securityKeyAndPasskey: "Clave de seguridad / clave de paso"
securityKey: "Clave de seguridad" securityKey: "Clave de seguridad"
lastUsed: "Última vez usado" lastUsed: "Última vez usado"
lastUsedAt: "Último uso: {t}"
unregister: "Cancelar registro" unregister: "Cancelar registro"
passwordLessLogin: "Iniciar sesión sin contraseña" passwordLessLogin: "Iniciar sesión sin contraseña"
passwordLessLoginDescription: "Iniciar sesión con sólo una clave se seguridad / de paso sin usar una contraseña"
resetPassword: "Resetear contraseña" resetPassword: "Resetear contraseña"
newPasswordIs: "La nueva contraseña es \"{password}\"" newPasswordIs: "La nueva contraseña es \"{password}\""
reduceUiAnimation: "Reducir la animación de la UI" reduceUiAnimation: "Reducir la animación de la UI"
@ -449,8 +457,11 @@ aboutX: "Acerca de {x}"
emojiStyle: "Estilo de emoji" emojiStyle: "Estilo de emoji"
native: "Nativo" native: "Nativo"
disableDrawer: "No mostrar los menús en cajones" disableDrawer: "No mostrar los menús en cajones"
showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor"
noHistory: "No hay datos en el historial" noHistory: "No hay datos en el historial"
signinHistory: "Historial de ingresos" signinHistory: "Historial de ingresos"
enableAdvancedMfm: "Habilitar MFM avanzado"
enableAnimatedMfm: "Habilitar MFM con movimiento"
doing: "Voy en camino" doing: "Voy en camino"
category: "Categoría" category: "Categoría"
tags: "Etiqueta" tags: "Etiqueta"
@ -769,6 +780,7 @@ popularPosts: "Más vistos"
shareWithNote: "Compartir con una nota" shareWithNote: "Compartir con una nota"
ads: "Anuncios" ads: "Anuncios"
expiration: "Termina el" expiration: "Termina el"
startingperiod: "periodo de inicio"
memo: "Notas" memo: "Notas"
priority: "Prioridad" priority: "Prioridad"
high: "Alta" high: "Alta"
@ -801,6 +813,7 @@ lastCommunication: "Última comunicación"
resolved: "Resuelto" resolved: "Resuelto"
unresolved: "Sin resolver" unresolved: "Sin resolver"
breakFollow: "Dejar de seguir" breakFollow: "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" emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro de la cuenta"
@ -835,16 +848,20 @@ instanceDefaultLightTheme: "Tema claro por defecto de la instancia"
instanceDefaultDarkTheme: "Tema oscuro por defecto de la instancia" instanceDefaultDarkTheme: "Tema oscuro por defecto de la instancia"
instanceDefaultThemeDescription: "Ingrese el código del tema en formato objeto" instanceDefaultThemeDescription: "Ingrese el código del tema en formato objeto"
mutePeriod: "Período de silenciamiento" mutePeriod: "Período de silenciamiento"
period: "Termina el"
indefinitely: "Sin límite de tiempo" indefinitely: "Sin límite de tiempo"
tenMinutes: "10 minutos" tenMinutes: "10 minutos"
oneHour: "1 hora" oneHour: "1 hora"
oneDay: "1 día" oneDay: "1 día"
oneWeek: "1 semana" oneWeek: "1 semana"
oneMonth: "1 mes"
reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios" reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios"
failedToFetchAccountInformation: "No se pudo obtener información de la cuenta" failedToFetchAccountInformation: "No se pudo obtener información de la cuenta"
rateLimitExceeded: "Se excedió el límite de peticiones" rateLimitExceeded: "Se excedió el límite de peticiones"
cropImage: "Recortar imágen" cropImage: "Recortar imágen"
cropImageAsk: "¿Desea recortar la imagen?" cropImageAsk: "¿Desea recortar la imagen?"
cropYes: "Recortar"
cropNo: "Usar como está"
file: "Archivos" file: "Archivos"
recentNHours: "Últimas {n} horas" recentNHours: "Últimas {n} horas"
recentNDays: "Últimos {n} días" recentNDays: "Últimos {n} días"
@ -925,6 +942,19 @@ selectFromPresets: "Escoger desde predefinidos"
achievements: "Logros" achievements: "Logros"
gotInvalidResponseError: "Respuesta del servidor inválida" gotInvalidResponseError: "Respuesta del servidor inválida"
gotInvalidResponseErrorDescription: "Puede que el servidor esté caído o en mantenimiento. Favor de intentar más tarde" gotInvalidResponseErrorDescription: "Puede que el servidor esté caído o en mantenimiento. Favor de intentar más tarde"
thisPostMayBeAnnoying: "Ésta publicación puede resultar molesta."
thisPostMayBeAnnoyingHome: "Publicar en línea de tiempo 'Inicio'"
thisPostMayBeAnnoyingCancel: "detener"
thisPostMayBeAnnoyingIgnore: "Publicar de todos modos"
collapseRenotes: "Colapsar renotas que ya hayas visto"
internalServerError: "Error interno del servidor"
internalServerErrorDescription: "El servidor tuvo un error inesperado."
copyErrorInfo: "Copiar detalles del error"
joinThisServer: "Registrarse en esta instancia"
exploreOtherServers: "Buscar otra instancia"
letsLookAtTimeline: "Mirar la línea de tiempo local"
disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración."
invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido."
_achievements: _achievements:
earnedAt: "Desbloqueado el" earnedAt: "Desbloqueado el"
_types: _types:
@ -1438,6 +1468,7 @@ _ago:
weeksAgo: "Hace {n} semanas" weeksAgo: "Hace {n} semanas"
monthsAgo: "Hace {n} meses" monthsAgo: "Hace {n} meses"
yearsAgo: "Hace {n} años" yearsAgo: "Hace {n} años"
invalid: "No hay nada que ver aqui"
_time: _time:
second: "Segundos" second: "Segundos"
minute: "Minutos" minute: "Minutos"
@ -1471,13 +1502,28 @@ _tutorial:
step8_3: "La configuración de las notificaciones puede modificarse posteriormente." step8_3: "La configuración de las notificaciones puede modificarse posteriormente."
_2fa: _2fa:
alreadyRegistered: "Ya has completado la configuración." alreadyRegistered: "Ya has completado la configuración."
registerTOTP: "Registrar aplicación autenticadora"
passwordToTOTP: "Ingresa tu contraseña"
step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra." step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra."
step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla." step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla."
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora."
step2Url: "En una aplicación de escritorio se puede ingresar la siguiente URL:" step2Url: "En una aplicación de escritorio se puede ingresar la siguiente URL:"
step3Title: "Ingresa un código de autenticación"
step3: "Para terminar, ingrese el token mostrado en la aplicación." step3: "Para terminar, ingrese el token mostrado en la aplicación."
step4: "Ahora cuando inicie sesión, ingrese el mismo token" step4: "Ahora cuando inicie sesión, ingrese el mismo token"
securityKeyNotSupported: "Tu navegador no soporta claves de autenticación."
registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key.\npor favor. configura una aplicación de autenticación para registrar una llave de seguridad."
securityKeyInfo: "Se puede configurar el inicio de sesión usando una clave de seguridad de hardware que soporte FIDO2 o con un certificado de huella digital o con un PIN" securityKeyInfo: "Se puede configurar el inicio de sesión usando una clave de seguridad de hardware que soporte FIDO2 o con un certificado de huella digital o con un PIN"
chromePasskeyNotSupported: "Las llaves de seguridad de Chrome no son soportadas por el momento."
registerSecurityKey: "Registrar una llave de seguridad"
securityKeyName: "Ingresa un nombre para la clave"
tapSecurityKey: "Por favor, sigue tu navegador para registrar una llave de seguridad"
removeKey: "Quitar la llave de seguridad"
removeKeyConfirm: "¿Borrar el respaldo \"{name}\"?" removeKeyConfirm: "¿Borrar el respaldo \"{name}\"?"
whyTOTPOnlyRenew: "The authenticator app cannot be removed as long as a security key is registered.\nLa aplicación autenticadora no puede ser eliminada mientras la llave de seguridad se encuentre registrada."
renewTOTP: "Reconfigurar la aplicación autenticadora"
renewTOTPConfirm: "This will cause verification codes from your previous app to stop working\nEsto hará que los códigos de verificación de la aplicación anterior dejen de funcionar"
renewTOTPOk: "Reconfigurar"
renewTOTPCancel: "No gracias" renewTOTPCancel: "No gracias"
_permissions: _permissions:
"read:account": "Ver información de la cuenta" "read:account": "Ver información de la cuenta"
@ -1601,6 +1647,8 @@ _visibility:
followersDescription: "Visible sólo para tus seguidores" followersDescription: "Visible sólo para tus seguidores"
specified: "Mensaje directo" specified: "Mensaje directo"
specifiedDescription: "Visible sólo para los usuarios elegidos" specifiedDescription: "Visible sólo para los usuarios elegidos"
disableFederation: "No federado"
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"
@ -1756,6 +1804,7 @@ _notification:
pollEnded: "La encuesta terminó" pollEnded: "La encuesta terminó"
receiveFollowRequest: "Recibió una solicitud de seguimiento" receiveFollowRequest: "Recibió una solicitud de seguimiento"
followRequestAccepted: "El seguimiento fue aceptado" followRequestAccepted: "El seguimiento fue aceptado"
achievementEarned: "Logro desbloqueado"
app: "Notificaciones desde aplicaciones" app: "Notificaciones desde aplicaciones"
_actions: _actions:
followBack: "Te sigue de vuelta" followBack: "Te sigue de vuelta"
@ -1788,3 +1837,6 @@ _deck:
channel: "Canal" channel: "Canal"
mentions: "Menciones" mentions: "Menciones"
direct: "Mensaje directo" direct: "Mensaje directo"
_dialog:
charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}."
charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}."

View file

@ -103,6 +103,8 @@ renoted: "Renoté !"
cantRenote: "Ce message ne peut pas être renoté." cantRenote: "Ce message ne peut pas être renoté."
cantReRenote: "Impossible de renoter une Renote." cantReRenote: "Impossible de renoter une Renote."
quote: "Citer" quote: "Citer"
inChannelRenote: "Renoter dans le canal"
inChannelQuote: "Citer dans le canal"
pinnedNote: "Note épinglée" pinnedNote: "Note épinglée"
pinned: "Épingler sur le profil" pinned: "Épingler sur le profil"
you: "Vous" you: "Vous"
@ -129,6 +131,7 @@ unblockConfirm: "Êtes-vous sûr·e de vouloir débloquer ce compte ?"
suspendConfirm: "Êtes-vous sûr·e de vouloir suspendre ce compte ?" suspendConfirm: "Êtes-vous sûr·e de vouloir suspendre ce compte ?"
unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce compte ?" unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce compte ?"
selectList: "Sélectionner une liste" selectList: "Sélectionner une liste"
selectChannel: "Sélectionner un canal"
selectAntenna: "Sélectionner une antenne" selectAntenna: "Sélectionner une antenne"
selectWidget: "Sélectionner un widget" selectWidget: "Sélectionner un widget"
editWidgets: "Modifier les widgets" editWidgets: "Modifier les widgets"
@ -524,7 +527,7 @@ updateRemoteUser: "Mettre à jour les informations de lutilisateur·rice dist
deleteAllFiles: "Supprimer tous les fichiers" deleteAllFiles: "Supprimer tous les fichiers"
deleteAllFilesConfirm: "Êtes-vous sûr·e de vouloir supprimer tous les fichiers ?" deleteAllFilesConfirm: "Êtes-vous sûr·e de vouloir supprimer tous les fichiers ?"
removeAllFollowing: "Retenir tous les abonnements" removeAllFollowing: "Retenir tous les abonnements"
removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Veuillez lancer cette action uniquement si linstance nexiste plus." removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Veuillez lancer cette action dans les cas où linstance nexiste plus, etc."
userSuspended: "Cet·te utilisateur·rice a été suspendu·e." 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"
@ -830,6 +833,7 @@ instanceDefaultLightTheme: "Thème clair par défaut sur toute linstance"
instanceDefaultDarkTheme: "Thème sombre par défaut sur toute linstance" instanceDefaultDarkTheme: "Thème sombre par défaut sur toute linstance"
instanceDefaultThemeDescription: "Saisissez le code du thème en format objet." instanceDefaultThemeDescription: "Saisissez le code du thème en format objet."
mutePeriod: "Durée de mise en sourdine" mutePeriod: "Durée de mise en sourdine"
period: "Fin du sondage"
indefinitely: "Illimité" indefinitely: "Illimité"
tenMinutes: "10 minutes" tenMinutes: "10 minutes"
oneHour: "1 heure" oneHour: "1 heure"
@ -898,6 +902,17 @@ show: "Affichage"
neverShow: "Ne plus afficher" neverShow: "Ne plus afficher"
remindMeLater: "Peut-être plus tard" remindMeLater: "Peut-être plus tard"
color: "Couleur" color: "Couleur"
_achievements:
_types:
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
_login1000:
flavor: "Merci d'utiliser Misskey !"
_markedAsCat:
title: "Je suis un chat"
flavor: "Je n'ai pas encore de nom"
_following50:
title: "Beaucoup d'amis"
_role: _role:
priority: "Priorité" priority: "Priorité"
_priority: _priority:
@ -1121,6 +1136,7 @@ _ago:
weeksAgo: "Il y a {n} semaines" weeksAgo: "Il y a {n} semaines"
monthsAgo: "Il y a {n} mois" monthsAgo: "Il y a {n} mois"
yearsAgo: "Il y a {n} ans" yearsAgo: "Il y a {n} ans"
invalid: "Il n'y a rien à voir ici"
_time: _time:
second: "s" second: "s"
minute: "min" minute: "min"

View file

@ -839,6 +839,7 @@ instanceDefaultLightTheme: "Bawaan instan tema terang"
instanceDefaultDarkTheme: "Bawaan instan tema gelap" instanceDefaultDarkTheme: "Bawaan instan tema gelap"
instanceDefaultThemeDescription: "Masukkan kode tema di format obyek." instanceDefaultThemeDescription: "Masukkan kode tema di format obyek."
mutePeriod: "Batas waktu bisu" mutePeriod: "Batas waktu bisu"
period: "Batas akhir"
indefinitely: "Selamanya" indefinitely: "Selamanya"
tenMinutes: "10 Menit" tenMinutes: "10 Menit"
oneHour: "1 Jam" oneHour: "1 Jam"
@ -1452,6 +1453,7 @@ _ago:
weeksAgo: "{n} minggu lalu" weeksAgo: "{n} minggu lalu"
monthsAgo: "{n} bulan lalu" monthsAgo: "{n} bulan lalu"
yearsAgo: "{n} tahun lalu" yearsAgo: "{n} tahun lalu"
invalid: "Tidak ada sama sekali disini"
_time: _time:
second: "detik" second: "detik"
minute: "menit" minute: "menit"

View file

@ -54,8 +54,8 @@ copyUsername: "Copia nome utente"
searchUser: "Cerca utente" searchUser: "Cerca utente"
reply: "Rispondi" reply: "Rispondi"
loadMore: "Mostra di più" loadMore: "Mostra di più"
showMore: "Mostra di più" showMore: "Espandi"
showLess: "Chiudi" showLess: "Comprimi"
youGotNewFollower: "Ha iniziato a seguirti" youGotNewFollower: "Ha iniziato a seguirti"
receiveFollowRequest: "Hai ricevuto una richiesta di follow" receiveFollowRequest: "Hai ricevuto una richiesta di follow"
followRequestAccepted: "Richiesta di follow accettata" followRequestAccepted: "Richiesta di follow accettata"
@ -75,8 +75,8 @@ lists: "Liste"
noLists: "Nessuna lista" noLists: "Nessuna lista"
note: "Nota" note: "Nota"
notes: "Note" notes: "Note"
following: "Lǝ segui" following: "Follow"
followers: "Followers" followers: "Follower"
followsYou: "Ti segue" followsYou: "Ti segue"
createList: "Aggiungi una nuova lista" createList: "Aggiungi una nuova lista"
manageLists: "Gestisci liste" manageLists: "Gestisci liste"
@ -94,7 +94,7 @@ defaultNoteVisibility: "Privacy predefinita delle note"
follow: "Segui" follow: "Segui"
followRequest: "Richiesta di follow" followRequest: "Richiesta di follow"
followRequests: "Richieste di follow" followRequests: "Richieste di follow"
unfollow: "Smetti di seguire" unfollow: "Non seguire"
followRequestPending: "Richiesta in approvazione" followRequestPending: "Richiesta in approvazione"
enterEmoji: "Inserisci emoji" enterEmoji: "Inserisci emoji"
renote: "Rinota" renote: "Rinota"
@ -154,14 +154,14 @@ flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le rispost
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"
loginFailed: "Accesso non riuscito" loginFailed: "Accesso non riuscito"
showOnRemote: "Visualizza sull'istanza remota" showOnRemote: "Leggi sull'istanza remota"
general: "Generali" general: "Generali"
wallpaper: "Sfondo" wallpaper: "Sfondo"
setWallpaper: "Imposta sfondo" setWallpaper: "Imposta sfondo"
removeWallpaper: "Elimina lo sfondo" removeWallpaper: "Elimina lo sfondo"
searchWith: "Cerca: {q}" searchWith: "Cerca: {q}"
youHaveNoLists: "Non hai ancora creato nessuna lista" youHaveNoLists: "Non hai ancora creato nessuna lista"
followConfirm: "Sei sicur@ di voler seguire {name}?" followConfirm: "Vuoi seguire {name}?"
proxyAccount: "Profilo proxy" proxyAccount: "Profilo proxy"
proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti." proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti."
host: "Server remoto" host: "Server remoto"
@ -221,7 +221,7 @@ subscribing: "Iscrizione"
publishing: "Pubblicazione" publishing: "Pubblicazione"
notResponding: "Nessuna risposta" notResponding: "Nessuna risposta"
instanceFollowing: "Seguiti dall'istanza" instanceFollowing: "Seguiti dall'istanza"
instanceFollowers: "Followers dell'istanza" instanceFollowers: "Follower dell'istanza"
instanceUsers: "Utenti dell'istanza" instanceUsers: "Utenti dell'istanza"
changePassword: "Aggiorna Password" changePassword: "Aggiorna Password"
security: "Sicurezza" security: "Sicurezza"
@ -393,13 +393,19 @@ about: "Informazioni"
aboutMisskey: "Informazioni di Misskey" aboutMisskey: "Informazioni di Misskey"
administrator: "Amministratore" administrator: "Amministratore"
token: "Token" token: "Token"
2fa: "Autenticazione a due fattori"
totp: "App di autenticazione"
totpDescription: "Inserisci un codice OTP tramite un'app di autenticazione"
moderator: "Moderatore" moderator: "Moderatore"
moderation: "moderazione" moderation: "moderazione"
nUsersMentioned: "{n} profili menzionati" nUsersMentioned: "{n} profili menzionati"
securityKeyAndPasskey: "Chiave di sicurezza e accesso"
securityKey: "Chiave di sicurezza" securityKey: "Chiave di sicurezza"
lastUsed: "Ultima attività" lastUsed: "Ultima attività"
lastUsedAt: "Uso più recente: {t}"
unregister: "Annulla l'iscrizione" unregister: "Annulla l'iscrizione"
passwordLessLogin: "Accedi senza password" passwordLessLogin: "Accedi senza password"
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
resetPassword: "Reimposta password" resetPassword: "Reimposta password"
newPasswordIs: "La tua nuova password è「{password}」" newPasswordIs: "La tua nuova password è「{password}」"
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia" reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
@ -451,6 +457,7 @@ aboutX: "Informazioni su {x}"
emojiStyle: "Stile emoji" emojiStyle: "Stile emoji"
native: "Nativo" native: "Nativo"
disableDrawer: "Non mostrare il menù sul drawer" disableDrawer: "Non mostrare il menù sul drawer"
showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mouse"
noHistory: "Nessuna cronologia" noHistory: "Nessuna cronologia"
signinHistory: "Storico degli accessi al profilo" signinHistory: "Storico degli accessi al profilo"
enableAdvancedMfm: "Attiva MFM avanzati" enableAdvancedMfm: "Attiva MFM avanzati"
@ -499,6 +506,7 @@ objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di cari
serverLogs: "Log del server" serverLogs: "Log del server"
deleteAll: "Cancella cronologia" deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline"
newNoteRecived: "Vedi le nuove note" newNoteRecived: "Vedi le nuove note"
sounds: "Impostazioni suoni" sounds: "Impostazioni suoni"
sound: "Impostazioni suoni" sound: "Impostazioni suoni"
@ -551,7 +559,7 @@ visibility: "Visibilità"
poll: "Sondaggio" poll: "Sondaggio"
useCw: "Nascondere media" useCw: "Nascondere media"
enablePlayer: "Apri in lettore video" enablePlayer: "Apri in lettore video"
disablePlayer: "Chiudi lettore video" disablePlayer: "Chiudi il lettore"
expandTweet: "Espandi tweet" expandTweet: "Espandi tweet"
themeEditor: "Editor di temi" themeEditor: "Editor di temi"
description: "Descrizione" description: "Descrizione"
@ -773,6 +781,7 @@ popularPosts: "Le più visualizzate"
shareWithNote: "Condividere in nota" shareWithNote: "Condividere in nota"
ads: "Pubblicità" ads: "Pubblicità"
expiration: "Scadenza" expiration: "Scadenza"
startingperiod: "Periodo di inizio"
memo: "Promemoria" memo: "Promemoria"
priority: "Priorità" priority: "Priorità"
high: "Alta" high: "Alta"
@ -804,7 +813,8 @@ pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente" lastCommunication: "La comunicazione più recente"
resolved: "Risolto" resolved: "Risolto"
unresolved: "Non risolto" unresolved: "Non risolto"
breakFollow: "Smetti di seguire" breakFollow: "Non seguire"
breakFollowConfirm: "Vuoi davvero togliere follower?"
itsOn: "Abilitato" itsOn: "Abilitato"
itsOff: "Disabilitato" itsOff: "Disabilitato"
emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi" emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi"
@ -839,11 +849,13 @@ instanceDefaultLightTheme: "Istanza, tema luminoso predefinito."
instanceDefaultDarkTheme: "Istanza, tema scuro predefinito." instanceDefaultDarkTheme: "Istanza, tema scuro predefinito."
instanceDefaultThemeDescription: "Compilare il codice del tema nel modulo dell'oggetto." instanceDefaultThemeDescription: "Compilare il codice del tema nel modulo dell'oggetto."
mutePeriod: "Durata del mute" mutePeriod: "Durata del mute"
period: "Scadenza"
indefinitely: "Non scade" indefinitely: "Non scade"
tenMinutes: "10 minuti" tenMinutes: "10 minuti"
oneHour: "1 ora" oneHour: "1 ora"
oneDay: "1 giorno" oneDay: "1 giorno"
oneWeek: "1 settimana" oneWeek: "1 settimana"
oneMonth: "Un mese"
reflectMayTakeTime: "Potrebbe essere necessario un po' di tempo perché ciò abbia effetto." reflectMayTakeTime: "Potrebbe essere necessario un po' di tempo perché ciò abbia effetto."
failedToFetchAccountInformation: "Impossibile recuperare le informazioni sul profilo" failedToFetchAccountInformation: "Impossibile recuperare le informazioni sul profilo"
rateLimitExceeded: "Superato il limite di richieste." rateLimitExceeded: "Superato il limite di richieste."
@ -939,6 +951,14 @@ collapseRenotes: "Comprimi i Rinota già letti"
internalServerError: "Errore interno del server" internalServerError: "Errore interno del server"
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server" internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
copyErrorInfo: "Copia le informazioni sull'errore" copyErrorInfo: "Copia le informazioni sull'errore"
joinThisServer: "Registrati su questa istanza"
exploreOtherServers: "Trova altre istanze"
letsLookAtTimeline: "Sbircia la timeline"
disableFederationWarn: "Disabilita la federazione. Questo cambiamento non rende le pubblicazioni private. Di solito non è necessario abilitare questa opzione."
invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore."
emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica sul canale"
cannotBeChangedLater: "Non sarà più modificabile"
_achievements: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
_types: _types:
@ -1075,7 +1095,7 @@ _achievements:
description: "Hai seguito 300 profili" description: "Hai seguito 300 profili"
_followers1: _followers1:
title: "Il primo profilo tuo Follower" title: "Il primo profilo tuo Follower"
description: "Hai ottenuto il tuo primo Follower" description: "Hai ottenuto il tuo primo profilo Follower"
_followers10: _followers10:
title: "Follow me!" title: "Follow me!"
description: "Hai ottenuto 10 profili Follower" description: "Hai ottenuto 10 profili Follower"
@ -1452,6 +1472,7 @@ _ago:
weeksAgo: "{n} sett. fa" weeksAgo: "{n} sett. fa"
monthsAgo: "{n} mesi fa" monthsAgo: "{n} mesi fa"
yearsAgo: "{n} anni fa" yearsAgo: "{n} anni fa"
invalid: "Niente da visualizzare"
_time: _time:
second: "s" second: "s"
minute: "min" minute: "min"
@ -1485,13 +1506,28 @@ _tutorial:
step8_3: "Potrai modificare questa impostazione." step8_3: "Potrai modificare questa impostazione."
_2fa: _2fa:
alreadyRegistered: "La configurazione è stata già completata." alreadyRegistered: "La configurazione è stata già completata."
registerTOTP: "Registra un'app di autenticazione"
passwordToTOTP: "Inserire la password"
step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}." step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}."
step2: "Quindi, scansionare il codice QR visualizzato con l'app." step2: "Quindi, scansionare il codice QR visualizzato con l'app."
step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo."
step2Url: "Nell'applicazione desktop inserire il seguente URL: " step2Url: "Nell'applicazione desktop inserire il seguente URL: "
step3Title: "Inserisci il codice di verifica"
step3: "Inserite il token visualizzato nell'app e il gioco è fatto." step3: "Inserite il token visualizzato nell'app e il gioco è fatto."
step4: "D'ora in poi, quando si accede, si inserisce il token nello stesso modo." step4: "D'ora in poi, quando si accede, si inserisce il token nello stesso modo."
securityKeyNotSupported: "Il tuo browser non supporta le chiavi di sicurezza."
registerTOTPBeforeKey: "Ti occorre un'app di autenticazione con OTP, prima di registrare la chiave di sicurezza."
securityKeyInfo: "È possibile impostare il dispositivo per accedere utilizzando una chiave di sicurezza hardware che supporta FIDO2 o un'impronta digitale o un PIN sul dispositivo." securityKeyInfo: "È possibile impostare il dispositivo per accedere utilizzando una chiave di sicurezza hardware che supporta FIDO2 o un'impronta digitale o un PIN sul dispositivo."
chromePasskeyNotSupported: "Le passkey di Chrome non sono attualmente supportate."
registerSecurityKey: "Registra la chiave di sicurezza"
securityKeyName: "Inserisci il nome della chiave"
tapSecurityKey: "Segui le istruzioni del browser e registra la chiave di sicurezza."
removeKey: "Elimina la chiave di sicurezza"
removeKeyConfirm: "Vuoi davvero eliminare \"{name}\"?" removeKeyConfirm: "Vuoi davvero eliminare \"{name}\"?"
whyTOTPOnlyRenew: "Se c'è una chiave di sicurezza attiva, non è possibile rimuovere l'app di autenticazione."
renewTOTP: "Riconfigura l'app di autenticazione"
renewTOTPConfirm: "I codici di verifica nelle app di autenticazione esistenti smetteranno di funzionare"
renewTOTPOk: "Ripristina"
renewTOTPCancel: "No grazie" renewTOTPCancel: "No grazie"
_permissions: _permissions:
"read:account": "Visualizza le informazioni sul profilo" "read:account": "Visualizza le informazioni sul profilo"
@ -1503,7 +1539,7 @@ _permissions:
"read:favorites": "Visualizza i tuoi preferiti" "read:favorites": "Visualizza i tuoi preferiti"
"write:favorites": "Gestisci i tuoi preferiti" "write:favorites": "Gestisci i tuoi preferiti"
"read:following": "Vedi le informazioni di follow" "read:following": "Vedi le informazioni di follow"
"write:following": "Seguiti/ Smetti di seguire" "write:following": "Seguire / Non seguire altri profili"
"read:messaging": "Visualizzare la chat" "read:messaging": "Visualizzare la chat"
"write:messaging": "Gestire la chat" "write:messaging": "Gestire la chat"
"read:mutes": "Vedi i profili silenziati" "read:mutes": "Vedi i profili silenziati"
@ -1581,7 +1617,7 @@ _widgets:
clicker: "Cliccaggio" clicker: "Cliccaggio"
_cw: _cw:
hide: "Nascondere" hide: "Nascondere"
show: "Mostra di più" show: "Apri..."
chars: "{count} caratteri" chars: "{count} caratteri"
files: "{count} file" files: "{count} file"
_poll: _poll:
@ -1611,10 +1647,12 @@ _visibility:
publicDescription: "Visibile per tutti sul Fediverso" publicDescription: "Visibile per tutti sul Fediverso"
home: "Home" home: "Home"
homeDescription: "Visibile solo sulla timeline \"Home\"" homeDescription: "Visibile solo sulla timeline \"Home\""
followers: "Followers" followers: "Follower"
followersDescription: "Visibile solo per i tuoi followers" followersDescription: "Visibile solo per i tuoi follower"
specified: "Nota diretta" specified: "Nota diretta"
specifiedDescription: "Visibile solo ai profili menzionati" specifiedDescription: "Visibile solo ai profili menzionati"
disableFederation: "Interrompi la federazione"
disableFederationDescription: "Non spedire attività alle altre istanze remote"
_postForm: _postForm:
replyPlaceholder: "Rispondi a questa nota..." replyPlaceholder: "Rispondi a questa nota..."
quotePlaceholder: "Cita questa nota..." quotePlaceholder: "Cita questa nota..."
@ -1641,7 +1679,7 @@ _profile:
_exportOrImport: _exportOrImport:
allNotes: "Tutte le note" allNotes: "Tutte le note"
favoritedNotes: "Note preferite" favoritedNotes: "Note preferite"
followingList: "Follows" followingList: "Follow"
muteList: "Elenco profili silenziati" muteList: "Elenco profili silenziati"
blockingList: "Elenco profili bloccati" blockingList: "Elenco profili bloccati"
userLists: "Liste" userLists: "Liste"
@ -1770,6 +1808,7 @@ _notification:
pollEnded: "Sondaggio chiuso." pollEnded: "Sondaggio chiuso."
receiveFollowRequest: "Richiesta di follow ricevuta" receiveFollowRequest: "Richiesta di follow ricevuta"
followRequestAccepted: "Richiesta di follow accettata" followRequestAccepted: "Richiesta di follow accettata"
achievementEarned: "Risultato raggiunto"
app: "Notifiche da applicazioni" app: "Notifiche da applicazioni"
_actions: _actions:
followBack: "Segui" followBack: "Segui"
@ -1802,3 +1841,6 @@ _deck:
channel: "Canale" channel: "Canale"
mentions: "Menzioni" mentions: "Menzioni"
direct: "Diretta" direct: "Diretta"
_dialog:
charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})"
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"

View file

@ -2,7 +2,7 @@ _lang_: "日本語"
headlineMisskey: "ノートでつながるネットワーク" headlineMisskey: "ノートでつながるネットワーク"
introMisskey: "ようこそMisskeyは、オープンソースの分散型マイクロブログサービスです。\n「ート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀" introMisskey: "ようこそMisskeyは、オープンソースの分散型マイクロブログサービスです。\n「ート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>を使ったサービス(Misskeyインスタンスと呼ばれます)のひとつです。" poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつです。"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "検索" search: "検索"
notifications: "通知" notifications: "通知"
@ -18,7 +18,7 @@ enterUsername: "ユーザー名を入力"
renotedBy: "{user}がRenote" renotedBy: "{user}がRenote"
noNotes: "ノートはありません" noNotes: "ノートはありません"
noNotifications: "通知はありません" noNotifications: "通知はありません"
instance: "インスタンス" instance: "サーバー"
settings: "設定" settings: "設定"
basicSettings: "基本設定" basicSettings: "基本設定"
otherSettings: "その他の設定" otherSettings: "その他の設定"
@ -163,13 +163,13 @@ searchWith: "検索: {q}"
youHaveNoLists: "リストがありません" youHaveNoLists: "リストがありません"
followConfirm: "{name}をフォローしますか?" followConfirm: "{name}をフォローしますか?"
proxyAccount: "プロキシアカウント" proxyAccount: "プロキシアカウント"
proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがインスタンスに配達されないため、代わりにプロキシアカウントがフォローするようにします。" proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
host: "ホスト" host: "ホスト"
selectUser: "ユーザーを選択" selectUser: "ユーザーを選択"
recipient: "宛先" recipient: "宛先"
annotation: "注釈" annotation: "注釈"
federation: "連合" federation: "連合"
instances: "インスタンス" instances: "サーバー"
registeredAt: "初観測" registeredAt: "初観測"
latestRequestReceivedAt: "直近のリクエスト受信" latestRequestReceivedAt: "直近のリクエスト受信"
latestStatus: "直近のステータス" latestStatus: "直近のステータス"
@ -178,7 +178,7 @@ charts: "チャート"
perHour: "1時間ごと" perHour: "1時間ごと"
perDay: "1日ごと" perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送を停止" stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このインスタンスをブロック" blockThisInstance: "このサーバーをブロック"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -189,15 +189,15 @@ jobQueue: "ジョブキュー"
cpuAndMemory: "CPUとメモリ" cpuAndMemory: "CPUとメモリ"
network: "ネットワーク" network: "ネットワーク"
disk: "ディスク" disk: "ディスク"
instanceInfo: "インスタンス情報" instanceInfo: "サーバー情報"
statistics: "統計" statistics: "統計"
clearQueue: "キューをクリア" clearQueue: "キューをクリア"
clearQueueConfirmTitle: "キューをクリアしますか?" clearQueueConfirmTitle: "キューをクリアしますか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。" clearQueueConfirmText: "未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。"
clearCachedFiles: "キャッシュをクリア" clearCachedFiles: "キャッシュをクリア"
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
blockedInstances: "ブロックしたインスタンス" blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。サブドメインもブロックされます。" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このサーバーとやり取りできなくなります。サブドメインもブロックされます。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"
@ -220,9 +220,9 @@ all: "全て"
subscribing: "購読中" subscribing: "購読中"
publishing: "配信中" publishing: "配信中"
notResponding: "応答なし" notResponding: "応答なし"
instanceFollowing: "インスタンスのフォロー" instanceFollowing: "サーバーのフォロー"
instanceFollowers: "インスタンスのフォロワー" instanceFollowers: "サーバーのフォロワー"
instanceUsers: "インスタンスのユーザー" instanceUsers: "サーバーのユーザー"
changePassword: "パスワードを変更" changePassword: "パスワードを変更"
security: "セキュリティ" security: "セキュリティ"
retypedNotMatch: "入力が一致しません。" retypedNotMatch: "入力が一致しません。"
@ -314,8 +314,8 @@ unwatch: "ウォッチ解除"
accept: "許可" accept: "許可"
reject: "拒否" reject: "拒否"
normal: "正常" normal: "正常"
instanceName: "インスタンス名" instanceName: "サーバー名"
instanceDescription: "インスタンスの紹介" instanceDescription: "サーバーの紹介"
maintainerName: "管理者の名前" maintainerName: "管理者の名前"
maintainerEmail: "管理者のメールアドレス" maintainerEmail: "管理者のメールアドレス"
tosUrl: "利用規約URL" tosUrl: "利用規約URL"
@ -345,7 +345,7 @@ basicInfo: "基本情報"
pinnedUsers: "ピン留めユーザー" pinnedUsers: "ピン留めユーザー"
pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。" pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。"
pinnedPages: "ピン留めページ" pinnedPages: "ピン留めページ"
pinnedPagesDescription: "インスタンスのトップページにピン留めしたいページのパスを改行で区切って記述します。" pinnedPagesDescription: "サーバーのトップページにピン留めしたいページのパスを改行で区切って記述します。"
pinnedClipId: "ピン留めするクリップのID" pinnedClipId: "ピン留めするクリップのID"
pinnedNotes: "ピン留めされたノート" pinnedNotes: "ピン留めされたノート"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
@ -457,6 +457,7 @@ aboutX: "{x}について"
emojiStyle: "絵文字のスタイル" emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない" disableDrawer: "メニューをドロワーで表示しない"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
noHistory: "履歴はありません" noHistory: "履歴はありません"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする" enableAdvancedMfm: "高度なMFMを有効にする"
@ -505,6 +506,7 @@ objectStorageSetPublicRead: "アップロード時に'public-read'を設定す
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全て削除" deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する" showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)"
newNoteRecived: "新しいノートがあります" newNoteRecived: "新しいノートがあります"
sounds: "サウンド" sounds: "サウンド"
sound: "サウンド" sound: "サウンド"
@ -537,7 +539,7 @@ updateRemoteUser: "リモートユーザー情報の更新"
deleteAllFiles: "すべてのファイルを削除" deleteAllFiles: "すべてのファイルを削除"
deleteAllFilesConfirm: "すべてのファイルを削除しますか?" deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
removeAllFollowing: "フォローを全解除" removeAllFollowing: "フォローを全解除"
removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのサーバーがもう存在しなくなった場合などに実行してください。"
userSuspended: "このユーザーは凍結されています。" userSuspended: "このユーザーは凍結されています。"
userSilenced: "このユーザーはサイレンスされています。" userSilenced: "このユーザーはサイレンスされています。"
yourAccountSuspendedTitle: "アカウントが凍結されています" yourAccountSuspendedTitle: "アカウントが凍結されています"
@ -603,7 +605,7 @@ testEmail: "配信テスト"
wordMute: "ワードミュート" wordMute: "ワードミュート"
regexpError: "正規表現エラー" regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "インスタンスミュート" instanceMute: "サーバーミュート"
userSaysSomething: "{name}が何かを言いました" userSaysSomething: "{name}が何かを言いました"
makeActive: "アクティブにする" makeActive: "アクティブにする"
display: "表示" display: "表示"
@ -634,15 +636,15 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ
reporter: "通報者" reporter: "通報者"
reporteeOrigin: "通報先" reporteeOrigin: "通報先"
reporterOrigin: "通報元" reporterOrigin: "通報元"
forwardReport: "リモートインスタンスに通報を転送する" forwardReport: "リモートサーバーに通報を転送する"
forwardReportIsAnonymous: "リモートインスタンスからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
send: "送信" send: "送信"
abuseMarkAsResolved: "対応済みにする" abuseMarkAsResolved: "対応済みにする"
openInNewTab: "新しいタブで開く" openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く" openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション" defaultNavigationBehaviour: "デフォルトのナビゲーション"
editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。" editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。"
instanceTicker: "ノートのインスタンス情報" instanceTicker: "ノートのサーバー情報"
waitingFor: "{x}を待っています" waitingFor: "{x}を待っています"
random: "ランダム" random: "ランダム"
system: "システム" system: "システム"
@ -731,7 +733,7 @@ capacity: "容量"
inUse: "使用中" inUse: "使用中"
editCode: "コードを編集" editCode: "コードを編集"
apply: "適用" apply: "適用"
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る" receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る"
emailNotification: "メール通知" emailNotification: "メール通知"
publish: "公開" publish: "公開"
inChannelSearch: "チャンネル内検索" inChannelSearch: "チャンネル内検索"
@ -759,7 +761,7 @@ active: "アクティブ"
offline: "オフライン" offline: "オフライン"
notRecommended: "非推奨" notRecommended: "非推奨"
botProtection: "Botプロテクション" botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック" instanceBlocking: "サーバーブロック"
selectAccount: "アカウントを選択" selectAccount: "アカウントを選択"
switchAccount: "アカウントを切り替え" switchAccount: "アカウントを切り替え"
enabled: "有効" enabled: "有効"
@ -843,15 +845,17 @@ themeColor: "テーマカラー"
size: "サイズ" size: "サイズ"
numberOfColumn: "列の数" numberOfColumn: "列の数"
searchByGoogle: "検索" searchByGoogle: "検索"
instanceDefaultLightTheme: "インスタンスデフォルトのライトテーマ" instanceDefaultLightTheme: "サーバーデフォルトのライトテーマ"
instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ" instanceDefaultDarkTheme: "サーバーデフォルトのダークテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
mutePeriod: "ミュートする期限" mutePeriod: "ミュートする期限"
period: "期限"
indefinitely: "無期限" indefinitely: "無期限"
tenMinutes: "10分" tenMinutes: "10分"
oneHour: "1時間" oneHour: "1時間"
oneDay: "1日" oneDay: "1日"
oneWeek: "1週間" oneWeek: "1週間"
oneMonth: "1ヶ月"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
rateLimitExceeded: "レート制限を超えました" rateLimitExceeded: "レート制限を超えました"
@ -895,7 +899,7 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
beta: "ベータ" beta: "ベータ"
enableAutoSensitive: "自動NSFW判定" enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、インスタンスによっては自動で設定されることがあります。" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。" activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
navbar: "ナビゲーションバー" navbar: "ナビゲーションバー"
shuffle: "シャッフル" shuffle: "シャッフル"
@ -905,7 +909,7 @@ pushNotification: "プッシュ通知"
subscribePushNotification: "プッシュ通知を有効化" subscribePushNotification: "プッシュ通知を有効化"
unsubscribePushNotification: "プッシュ通知を停止する" unsubscribePushNotification: "プッシュ通知を停止する"
pushNotificationAlreadySubscribed: "プッシュ通知は有効です" pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に非対応" pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する" sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
windowMaximize: "最大化" windowMaximize: "最大化"
@ -951,6 +955,10 @@ joinThisServer: "このサーバーに登録する"
exploreOtherServers: "他のサーバーを探す" exploreOtherServers: "他のサーバーを探す"
letsLookAtTimeline: "タイムラインを見てみる" letsLookAtTimeline: "タイムラインを見てみる"
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。" disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
emailNotSupported: "このサーバーではメール配信はサポートされていません"
postToTheChannel: "チャンネルに投稿"
cannotBeChangedLater: "後から変更できません。"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"
@ -1142,7 +1150,7 @@ _achievements:
description: "ホームタイムラインの流速が20npmを越す" description: "ホームタイムラインの流速が20npmを越す"
_viewInstanceChart: _viewInstanceChart:
title: "アナリスト" title: "アナリスト"
description: "インスタンスのチャートを表示した" description: "サーバーのチャートを表示した"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Hello, world!" title: "Hello, world!"
description: "スクラッチパッドで hello world を出力した" description: "スクラッチパッドで hello world を出力した"
@ -1179,7 +1187,7 @@ _achievements:
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "あけましておめでとうございます" title: "あけましておめでとうございます"
description: "元日にログインした" description: "元日にログインした"
flavor: "今年も弊インスタンスをよろしくお願いします" flavor: "今年も弊サーバーをよろしくお願いします"
_cookieClicked: _cookieClicked:
title: "クッキーをクリックするゲーム" title: "クッキーをクリックするゲーム"
description: "クッキーをクリックした" description: "クッキーをクリックした"
@ -1195,7 +1203,7 @@ _role:
name: "ロール名" name: "ロール名"
description: "ロールの説明" description: "ロールの説明"
permission: "ロールの権限" permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。" descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はサーバーの全ての設定を変更できます。"
assignTarget: "アサイン" assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。" descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル" manual: "マニュアル"
@ -1223,7 +1231,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧" gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可" canPublicNote: "パブリック投稿の許可"
canInvite: "インスタンス招待コードの発行" canInvite: "サーバー招待コードの発行"
canManageCustomEmojis: "カスタム絵文字の管理" canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
pinMax: "ノートのピン留めの最大数" pinMax: "ノートのピン留めの最大数"
@ -1292,7 +1300,7 @@ _ad:
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。" ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。"
contactAdmin: "このインスタンスではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。" contactAdmin: "このサーバーではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。"
_gallery: _gallery:
my: "自分の投稿" my: "自分の投稿"
@ -1390,10 +1398,10 @@ _wordMute:
mutedNotes: "ミュートされたノート" mutedNotes: "ミュートされたノート"
_instanceMute: _instanceMute:
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートします。" instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのートとRenoteをミュートします。"
instanceMuteDescription2: "改行で区切って設定します" instanceMuteDescription2: "改行で区切って設定します"
title: "設定したインスタンスのノートを隠します。" title: "設定したサーバーのノートを隠します。"
heading: "ミュートするインスタンス" heading: "ミュートするサーバー"
_theme: _theme:
explore: "テーマを探す" explore: "テーマを探す"
@ -1613,7 +1621,7 @@ _weekday:
_widgets: _widgets:
profile: "プロフィール" profile: "プロフィール"
instanceInfo: "インスタンス情報" instanceInfo: "サーバー情報"
memo: "付箋" memo: "付箋"
notifications: "通知" notifications: "通知"
timeline: "タイムライン" timeline: "タイムライン"
@ -1627,7 +1635,7 @@ _widgets:
digitalClock: "デジタル時計" digitalClock: "デジタル時計"
unixClock: "UNIX時計" unixClock: "UNIX時計"
federation: "連合" federation: "連合"
instanceCloud: "インスタンスクラウド" instanceCloud: "サーバークラウド"
postForm: "投稿フォーム" postForm: "投稿フォーム"
slideshow: "スライドショー" slideshow: "スライドショー"
button: "ボタン" button: "ボタン"
@ -1681,7 +1689,7 @@ _visibility:
specified: "ダイレクト" specified: "ダイレクト"
specifiedDescription: "指定したユーザーのみに公開" specifiedDescription: "指定したユーザーのみに公開"
disableFederation: "連合なし" disableFederation: "連合なし"
disableFederationDescription: "他インスタンスへの配信を行いません" disableFederationDescription: "他サーバーへの配信を行いません"
_postForm: _postForm:
replyPlaceholder: "このノートに返信..." replyPlaceholder: "このノートに返信..."

View file

@ -393,13 +393,19 @@ about: "情報"
aboutMisskey: "Misskeyってなんや" aboutMisskey: "Misskeyってなんや"
administrator: "管理者" administrator: "管理者"
token: "トークン" token: "トークン"
2fa: "二要素認証"
totp: "認証アプリ"
totpDescription: "認証アプリ使てワンタイムパスワードを入れる"
moderator: "モデレーター" moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
securityKeyAndPasskey: "セキュリティキー・パスキー"
securityKey: "セキュリティキー" securityKey: "セキュリティキー"
lastUsed: "最後につこうた日" lastUsed: "最後につこうた日"
lastUsedAt: "最後に使たん: {t}"
unregister: "登録やめる" unregister: "登録やめる"
passwordLessLogin: "パスワード無くてもログインできるようにする" passwordLessLogin: "パスワード無くてもログインできるようにする"
passwordLessLoginDescription: "パスワードやなくて、セキュリティキーとかパスキーだけでログインするわ"
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
newPasswordIs: "今度のパスワードは「{password}」や" newPasswordIs: "今度のパスワードは「{password}」や"
reduceUiAnimation: "UIの動きやアニメーションを減らす" reduceUiAnimation: "UIの動きやアニメーションを減らす"
@ -451,6 +457,7 @@ aboutX: "{x}について"
emojiStyle: "絵文字のスタイル" emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示せぇへん" disableDrawer: "メニューをドロワーで表示せぇへん"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで"
noHistory: "履歴はあらへんねぇ。" noHistory: "履歴はあらへんねぇ。"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "ややこしいMFMもありにする" enableAdvancedMfm: "ややこしいMFMもありにする"
@ -499,6 +506,7 @@ objectStorageSetPublicRead: "アップロードした時に'public-read'を設
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全て削除してや" deleteAll: "全て削除してや"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?" showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)"
newNoteRecived: "新しいノートがあるで" newNoteRecived: "新しいノートがあるで"
sounds: "サウンド" sounds: "サウンド"
sound: "サウンド" sound: "サウンド"
@ -575,7 +583,7 @@ generateAccessToken: "アクセストークンの発行"
permission: "権限" permission: "権限"
enableAll: "全部使えるようにする" enableAll: "全部使えるようにする"
disableAll: "全部使えへんようにする" disableAll: "全部使えへんようにする"
tokenRequested: "アカウントへのアクセス許" tokenRequested: "アカウントへのアクセス許してやったらどうや"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
notificationType: "通知の種類" notificationType: "通知の種類"
edit: "編集" edit: "編集"
@ -773,6 +781,7 @@ popularPosts: "人気の投稿"
shareWithNote: "ノートで共有" shareWithNote: "ノートで共有"
ads: "広告" ads: "広告"
expiration: "期限" expiration: "期限"
startingperiod: "始めた期間"
memo: "メモ" memo: "メモ"
priority: "優先度" priority: "優先度"
high: "高い" high: "高い"
@ -805,6 +814,7 @@ lastCommunication: "直近の通信"
resolved: "解決したで" resolved: "解決したで"
unresolved: "まだ解決してないで" unresolved: "まだ解決してないで"
breakFollow: "フォロワーを解除するで" breakFollow: "フォロワーを解除するで"
breakFollowConfirm: "フォロワー解除してもええか?"
itsOn: "オンになっとるよ" itsOn: "オンになっとるよ"
itsOff: "オフになってるで" itsOff: "オフになってるで"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
@ -839,11 +849,13 @@ instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ"
instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ" instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
mutePeriod: "ミュートする期間" mutePeriod: "ミュートする期間"
period: "期限"
indefinitely: "無期限" indefinitely: "無期限"
tenMinutes: "10分" tenMinutes: "10分"
oneHour: "1時間" oneHour: "1時間"
oneDay: "1日" oneDay: "1日"
oneWeek: "1週間" oneWeek: "1週間"
oneMonth: "1ヶ月"
reflectMayTakeTime: "反映されるまで時間がかかることがあるで" reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…" failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
rateLimitExceeded: "レート制限が超えたみたいやで" rateLimitExceeded: "レート制限が超えたみたいやで"
@ -898,7 +910,7 @@ subscribePushNotification: "プッシュ通知をオンにするで"
unsubscribePushNotification: "プッシュ通知を止めるで" unsubscribePushNotification: "プッシュ通知を止めるで"
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで" pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。" pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。"
sendPushNotificationReadMessage: "通知やメッセージが既読ったらプッシュ通知を消すで" sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
windowMaximize: "最大化" windowMaximize: "最大化"
windowRestore: "元に戻す" windowRestore: "元に戻す"
@ -939,6 +951,14 @@ collapseRenotes: "見たことあるRenoteは省略やで"
internalServerError: "サーバー内部エラー" internalServerError: "サーバー内部エラー"
internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ" internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ"
copyErrorInfo: "エラー情報をコピー" copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録するわ"
exploreOtherServers: "他のサーバー見てみる"
letsLookAtTimeline: "タイムライン見てみーや"
disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。"
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
postToTheChannel: "チャンネルに投稿"
cannotBeChangedLater: "後からは変えられへんで。"
_achievements: _achievements:
earnedAt: "貰った日ぃ" earnedAt: "貰った日ぃ"
_types: _types:
@ -1051,21 +1071,42 @@ _achievements:
_myNoteFavorited1: _myNoteFavorited1:
title: "星ぃ欲しい" title: "星ぃ欲しい"
description: "ワレのノートが他のひとにお気に入り登録されたで" description: "ワレのノートが他のひとにお気に入り登録されたで"
_profileFilled:
title: "準備万端や"
description: "プロフィールを設定した"
_markedAsCat:
title: "吾輩は猫やねん"
description: "アカウントをCatにしたった"
flavor: "名前はまだないねん。"
_following1:
title: "はじめてのフォロー"
description: "初めてフォローした"
_following10: _following10:
title: "ついてく、ついてく"
description: "フォローが10人超えた" description: "フォローが10人超えた"
_following50: _following50:
title: "友達ぎょうさん"
description: "フォローが50人超えた" description: "フォローが50人超えた"
_following100: _following100:
title: "友達100人"
description: "フォローが100人超えた" description: "フォローが100人超えた"
_following300: _following300:
title: "いや友達多すぎやろ"
description: "フォローが300人超えた" description: "フォローが300人超えた"
_followers1:
title: "はじめてのフォロワー"
description: "初めてフォローされた"
_followers10: _followers10:
title: "フォローみぃ!"
description: "フォロワーが10人超えた" description: "フォロワーが10人超えた"
_followers50: _followers50:
title: "ぞろぞろ"
description: "フォロワーが50人超えた" description: "フォロワーが50人超えた"
_followers100: _followers100:
title: "人気もん"
description: "フォロワーが100人超えた" description: "フォロワーが100人超えた"
_followers300: _followers300:
title: "ほらそこ一列に並んで!"
description: "フォロワーが300人超えた" description: "フォロワーが300人超えた"
_followers500: _followers500:
title: "基地局" title: "基地局"
@ -1150,6 +1191,10 @@ _achievements:
title: "クッキー叩くやつ" title: "クッキー叩くやつ"
description: "クッキー叩いてもうた" description: "クッキー叩いてもうた"
flavor: "兄ちゃんソフト間違っとんで" flavor: "兄ちゃんソフト間違っとんで"
_brainDiver:
title: "Brain Diver"
description: "Brain Diverへのリンクを投稿したった"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
edit: "ロールの編集" edit: "ロールの編集"
@ -1170,6 +1215,8 @@ _role:
baseRole: "ベースロール" baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用" useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択" chooseRoleToAssign: "アサインするロールを選択"
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして見せる"
descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。" descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。"
@ -1425,6 +1472,7 @@ _ago:
weeksAgo: "{n}週間前" weeksAgo: "{n}週間前"
monthsAgo: "{n}ヶ月前" monthsAgo: "{n}ヶ月前"
yearsAgo: "{n}年前" yearsAgo: "{n}年前"
invalid: "あらへん"
_time: _time:
second: "秒" second: "秒"
minute: "分" minute: "分"
@ -1458,13 +1506,28 @@ _tutorial:
step8_3: "通知の設定はあとから変更できるで" step8_3: "通知の設定はあとから変更できるで"
_2fa: _2fa:
alreadyRegistered: "もう設定終わっとるわ。" alreadyRegistered: "もう設定終わっとるわ。"
registerTOTP: "認証アプリの設定はじめる"
passwordToTOTP: "パスワードを入れてーや"
step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。"
step2: "次に、ここにあるQRコードをアプリでスキャンしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな。"
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
step2Url: "デスクトップアプリやったら次のURLを入力してや:" step2Url: "デスクトップアプリやったら次のURLを入力してや:"
step3Title: "確認コードを入れてーや"
step3: "アプリに表示されているトークンを入力して終わりや。" step3: "アプリに表示されているトークンを入力して終わりや。"
step4: "これからログインするときも、同じようにトークンを入力するんやで" step4: "これからログインするときも、同じようにトークンを入力するんやで"
securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。"
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。"
securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーか端末の指紋認証やPINを使ってログインするように設定できるで。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーか端末の指紋認証やPINを使ってログインするように設定できるで。"
chromePasskeyNotSupported: "Chromeのパスキーは今んとこ対応してないねん。"
registerSecurityKey: "セキュリティキー・パスキーを登録するわ"
securityKeyName: "キーの名前を入れてーや"
tapSecurityKey: "ブラウザが言うこと聞いて、セキュリティキーとかパスキー登録しといでや"
removeKey: "セキュリティキーをほかす"
removeKeyConfirm: "{name}を消すん?" removeKeyConfirm: "{name}を消すん?"
whyTOTPOnlyRenew: "セキュリティキーが登録されとったら、認証アプリの設定は解除できへんで。"
renewTOTP: "認証アプリをもっかい設定"
renewTOTPConfirm: "今までの人称アプリの確認コードは使えんくなるけどええか?"
renewTOTPOk: "もっかい設定する"
renewTOTPCancel: "やめとく" renewTOTPCancel: "やめとく"
_permissions: _permissions:
"read:account": "アカウントの情報を見るで" "read:account": "アカウントの情報を見るで"
@ -1500,6 +1563,7 @@ _permissions:
"read:gallery-likes": "ギャラリーのいいねを見るで" "read:gallery-likes": "ギャラリーのいいねを見るで"
"write:gallery-likes": "ギャラリーのいいねを操作するで" "write:gallery-likes": "ギャラリーのいいねを操作するで"
_auth: _auth:
shareAccessTitle: "アプリへのアクセス許してやったらどうや"
shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?" shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?"
shareAccessAsk: "アカウントのアクセスを許可してもええか?" shareAccessAsk: "アカウントのアクセスを許可してもええか?"
permission: "{name}に次の権限つけたってやって" permission: "{name}に次の権限つけたってやって"
@ -1587,14 +1651,16 @@ _visibility:
followersDescription: "自分のフォロワーのみに公開するで" followersDescription: "自分のフォロワーのみに公開するで"
specified: "ダイレクト" specified: "ダイレクト"
specifiedDescription: "選んだユーザーのみに公開するで" specifiedDescription: "選んだユーザーのみに公開するで"
disableFederation: "連合なし"
disableFederationDescription: "他インスタンスへは送らんとくわ"
_postForm: _postForm:
replyPlaceholder: "このノートに返信..." replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..." quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..." channelPlaceholder: "チャンネルに投稿..."
_placeholders: _placeholders:
a: "いまどしとるん?" a: "いまどないしとるん?"
b: "何かあったん?" b: "何かあったん?"
c: "何考えとるん?" c: "何考えとるん?"
d: "何か言いたいことあるん?" d: "何か言いたいことあるん?"
e: "ここに書いてーなー" e: "ここに書いてーなー"
f: "あんたが書くの待っとるで" f: "あんたが書くの待っとるで"
@ -1742,6 +1808,7 @@ _notification:
pollEnded: "アンケートが終了したで" pollEnded: "アンケートが終了したで"
receiveFollowRequest: "フォロー許可してほしいみたいやで" receiveFollowRequest: "フォロー許可してほしいみたいやで"
followRequestAccepted: "フォローが受理されたで" followRequestAccepted: "フォローが受理されたで"
achievementEarned: "実績の獲得"
app: "連携アプリからの通知や" app: "連携アプリからの通知や"
_actions: _actions:
followBack: "フォローバック" followBack: "フォローバック"
@ -1774,3 +1841,6 @@ _deck:
channel: "チャンネル" channel: "チャンネル"
mentions: "あんた宛て" mentions: "あんた宛て"
direct: "ダイレクト" direct: "ダイレクト"
_dialog:
charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}"
charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}"

View file

@ -2,7 +2,7 @@
_lang_: "한국어" _lang_: "한국어"
headlineMisskey: "노트로 연결되는 네트워크" headlineMisskey: "노트로 연결되는 네트워크"
introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀" introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀"
poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼<b>Misskey</b>를 사용한 서비스(Misskey 인스턴스라고 불립니다) 중 하나입니다." poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼<b>Misskey</b>를 사용한 서 중 하나입니다."
monthAndDay: "{month}월 {day}일" monthAndDay: "{month}월 {day}일"
search: "검색" search: "검색"
notifications: "알림" notifications: "알림"
@ -18,7 +18,7 @@ enterUsername: "유저명 입력"
renotedBy: "{user}님의 리노트" renotedBy: "{user}님의 리노트"
noNotes: "노트가 없습니다" noNotes: "노트가 없습니다"
noNotifications: "표시할 알림이 없습니다" noNotifications: "표시할 알림이 없습니다"
instance: "인스턴스" instance: "서버"
settings: "설정" settings: "설정"
basicSettings: "기본 설정" basicSettings: "기본 설정"
otherSettings: "기타 설정" otherSettings: "기타 설정"
@ -163,13 +163,13 @@ searchWith: "검색: {q}"
youHaveNoLists: "리스트가 없습니다" youHaveNoLists: "리스트가 없습니다"
followConfirm: "{name}님을 팔로우 하시겠습니까?" followConfirm: "{name}님을 팔로우 하시겠습니까?"
proxyAccount: "프록시 계정" proxyAccount: "프록시 계정"
proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 인스턴스로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다." proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 서버로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다."
host: "호스트" host: "호스트"
selectUser: "유저 선택" selectUser: "유저 선택"
recipient: "수신인" recipient: "수신인"
annotation: "내용에 대한 주석" annotation: "내용에 대한 주석"
federation: "연합" federation: "연합"
instances: "인스턴스" instances: "서버"
registeredAt: "등록 날짜" registeredAt: "등록 날짜"
latestRequestReceivedAt: "마지막으로 요청을 받은 시간" latestRequestReceivedAt: "마지막으로 요청을 받은 시간"
latestStatus: "마지막 상태" latestStatus: "마지막 상태"
@ -178,7 +178,7 @@ charts: "차트"
perHour: "1시간마다" perHour: "1시간마다"
perDay: "1일마다" perDay: "1일마다"
stopActivityDelivery: "액티비티 보내지 않기" stopActivityDelivery: "액티비티 보내지 않기"
blockThisInstance: "이 인스턴스를 차단" blockThisInstance: "이 서버를 차단"
operations: "작업" operations: "작업"
software: "소프트웨어" software: "소프트웨어"
version: "버전" version: "버전"
@ -189,15 +189,15 @@ jobQueue: "작업 대기열"
cpuAndMemory: "CPU와 메모리" cpuAndMemory: "CPU와 메모리"
network: "네트워크" network: "네트워크"
disk: "디스크" disk: "디스크"
instanceInfo: "인스턴스 정보" instanceInfo: "서버 정보"
statistics: "통계" statistics: "통계"
clearQueue: "대기열 비우기" clearQueue: "대기열 비우기"
clearQueueConfirmTitle: "대기열을 비우시겠습니까?" clearQueueConfirmTitle: "대기열을 비우시겠습니까?"
clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다."
clearCachedFiles: "캐시 비우기" clearCachedFiles: "캐시 비우기"
clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?" clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?"
blockedInstances: "차단된 인스턴스" blockedInstances: "차단된 서버"
blockedInstancesDescription: "차단하려는 인스턴스의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다." blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다."
muteAndBlock: "뮤트 및 차단" muteAndBlock: "뮤트 및 차단"
mutedUsers: "뮤트한 유저" mutedUsers: "뮤트한 유저"
blockedUsers: "차단한 유저" blockedUsers: "차단한 유저"
@ -220,9 +220,9 @@ all: "전체"
subscribing: "구독 중" subscribing: "구독 중"
publishing: "배포 중" publishing: "배포 중"
notResponding: "응답 없음" notResponding: "응답 없음"
instanceFollowing: "인스턴스의 팔로잉" instanceFollowing: "서버의 팔로잉"
instanceFollowers: "인스턴스의 팔로워" instanceFollowers: "서버의 팔로워"
instanceUsers: "인스턴스의 유저" instanceUsers: "서버의 유저"
changePassword: "비밀번호 변경" changePassword: "비밀번호 변경"
security: "보안" security: "보안"
retypedNotMatch: "입력이 일치하지 않습니다." retypedNotMatch: "입력이 일치하지 않습니다."
@ -314,8 +314,8 @@ unwatch: "지켜보기 해제"
accept: "허가" accept: "허가"
reject: "거부" reject: "거부"
normal: "정상" normal: "정상"
instanceName: "인스턴스 이름" instanceName: "서버 이름"
instanceDescription: "인스턴스 소개" instanceDescription: "서버 소개"
maintainerName: "관리자 이름" maintainerName: "관리자 이름"
maintainerEmail: "관리자 이메일" maintainerEmail: "관리자 이메일"
tosUrl: "이용약관 URL" tosUrl: "이용약관 URL"
@ -345,7 +345,7 @@ basicInfo: "기본 정보"
pinnedUsers: "고정된 유저" pinnedUsers: "고정된 유저"
pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다." pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
pinnedPages: "고정한 페이지" pinnedPages: "고정한 페이지"
pinnedPagesDescription: "인스턴스의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다." pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다."
pinnedClipId: "고정할 클립의 ID" pinnedClipId: "고정할 클립의 ID"
pinnedNotes: "고정해놓은 노트" pinnedNotes: "고정해놓은 노트"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
@ -393,13 +393,19 @@ about: "정보"
aboutMisskey: "Misskey에 대하여" aboutMisskey: "Misskey에 대하여"
administrator: "관리자" administrator: "관리자"
token: "토큰" token: "토큰"
2fa: "2단계 인증"
totp: "인증 앱"
totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
moderator: "모더레이터" moderator: "모더레이터"
moderation: "모더레이션" moderation: "모더레이션"
nUsersMentioned: "{n}명이 언급함" nUsersMentioned: "{n}명이 언급함"
securityKeyAndPasskey: "보안 키 또는 패스 키"
securityKey: "보안 키" securityKey: "보안 키"
lastUsed: "마지막 사용" lastUsed: "마지막 사용"
lastUsedAt: "마지막 사용: {t}"
unregister: "등록 해제" unregister: "등록 해제"
passwordLessLogin: "비밀번호 없이 로그인" passwordLessLogin: "비밀번호 없이 로그인"
passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다."
resetPassword: "비밀번호 재설정" resetPassword: "비밀번호 재설정"
newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다" newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다"
reduceUiAnimation: "UI의 애니메이션을 줄이기" reduceUiAnimation: "UI의 애니메이션을 줄이기"
@ -451,6 +457,7 @@ aboutX: "{x}에 대하여"
emojiStyle: "이모지 스타일" emojiStyle: "이모지 스타일"
native: "네이티브" native: "네이티브"
disableDrawer: "드로어 메뉴를 사용하지 않기" disableDrawer: "드로어 메뉴를 사용하지 않기"
showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시"
noHistory: "기록이 없습니다" noHistory: "기록이 없습니다"
signinHistory: "로그인 기록" signinHistory: "로그인 기록"
enableAdvancedMfm: "고급 MFM을 활성화" enableAdvancedMfm: "고급 MFM을 활성화"
@ -531,7 +538,7 @@ updateRemoteUser: "리모트 유저 정보 갱신"
deleteAllFiles: "모든 파일 삭제" deleteAllFiles: "모든 파일 삭제"
deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?" deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?"
removeAllFollowing: "모든 팔로잉 해제" removeAllFollowing: "모든 팔로잉 해제"
removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 인스턴스가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요." removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요."
userSuspended: "이 계정은 정지된 상태입니다." userSuspended: "이 계정은 정지된 상태입니다."
userSilenced: "이 계정은 사일런스된 상태입니다." userSilenced: "이 계정은 사일런스된 상태입니다."
yourAccountSuspendedTitle: "계정이 정지되었습니다" yourAccountSuspendedTitle: "계정이 정지되었습니다"
@ -597,7 +604,7 @@ testEmail: "이메일 전송 테스트"
wordMute: "단어 뮤트" wordMute: "단어 뮤트"
regexpError: "정규 표현식 오류" regexpError: "정규 표현식 오류"
regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:" regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:"
instanceMute: "인스턴스 뮤트" instanceMute: "서버 뮤트"
userSaysSomething: "{name}님이 무언가를 말했습니다" userSaysSomething: "{name}님이 무언가를 말했습니다"
makeActive: "활성화" makeActive: "활성화"
display: "표시" display: "표시"
@ -628,15 +635,15 @@ abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
reporter: "신고자" reporter: "신고자"
reporteeOrigin: "피신고자" reporteeOrigin: "피신고자"
reporterOrigin: "신고자" reporterOrigin: "신고자"
forwardReport: "리모트 인스턴스에도 신고 내용 보내기" forwardReport: "리모트 서버에도 신고 내용 보내기"
forwardReportIsAnonymous: "리모트 인스턴스에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다." forwardReportIsAnonymous: "리모트 서버에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다."
send: "전송" send: "전송"
abuseMarkAsResolved: "해결됨으로 표시" abuseMarkAsResolved: "해결됨으로 표시"
openInNewTab: "새 탭에서 열기" openInNewTab: "새 탭에서 열기"
openInSideView: "사이드뷰로 열기" openInSideView: "사이드뷰로 열기"
defaultNavigationBehaviour: "기본 탐색 동작" defaultNavigationBehaviour: "기본 탐색 동작"
editTheseSettingsMayBreakAccount: "이 설정을 변경하면 계정이 손상될 수 있습니다." editTheseSettingsMayBreakAccount: "이 설정을 변경하면 계정이 손상될 수 있습니다."
instanceTicker: "노트의 인스턴스 정보" instanceTicker: "노트의 서버 정보"
waitingFor: "{x}을(를) 기다리고 있습니다" waitingFor: "{x}을(를) 기다리고 있습니다"
random: "랜덤" random: "랜덤"
system: "시스템" system: "시스템"
@ -725,7 +732,7 @@ capacity: "용량"
inUse: "사용중" inUse: "사용중"
editCode: "코드 수정" editCode: "코드 수정"
apply: "적용" apply: "적용"
receiveAnnouncementFromInstance: "이 인스턴스의 알림을 이메일로 수신할게요" receiveAnnouncementFromInstance: "이 서버의 알림을 이메일로 수신할게요"
emailNotification: "메일 알림" emailNotification: "메일 알림"
publish: "게시" publish: "게시"
inChannelSearch: "채널에서 검색" inChannelSearch: "채널에서 검색"
@ -753,7 +760,7 @@ active: "최근에 활동함"
offline: "오프라인" offline: "오프라인"
notRecommended: "추천하지 않음" notRecommended: "추천하지 않음"
botProtection: "Bot 방어" botProtection: "Bot 방어"
instanceBlocking: "인스턴스 차단" instanceBlocking: "서버 차단"
selectAccount: "계정 선택" selectAccount: "계정 선택"
switchAccount: "계정 바꾸기" switchAccount: "계정 바꾸기"
enabled: "활성화" enabled: "활성화"
@ -773,6 +780,7 @@ popularPosts: "인기 포스트"
shareWithNote: "노트로 공유" shareWithNote: "노트로 공유"
ads: "광고" ads: "광고"
expiration: "기한" expiration: "기한"
startingperiod: "시작 기간"
memo: "메모" memo: "메모"
priority: "우선순위" priority: "우선순위"
high: "높음" high: "높음"
@ -805,6 +813,7 @@ lastCommunication: "마지막 통신"
resolved: "해결됨" resolved: "해결됨"
unresolved: "해결되지 않음" unresolved: "해결되지 않음"
breakFollow: "팔로워 해제" breakFollow: "팔로워 해제"
breakFollowConfirm: "팔로우를 해제하시겠습니까?"
itsOn: "켜짐" itsOn: "켜짐"
itsOff: "꺼짐" itsOff: "꺼짐"
emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기" emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기"
@ -835,15 +844,17 @@ themeColor: "테마 컬러"
size: "크기" size: "크기"
numberOfColumn: "한 줄에 보일 리액션의 수" numberOfColumn: "한 줄에 보일 리액션의 수"
searchByGoogle: "검색" searchByGoogle: "검색"
instanceDefaultLightTheme: "인스턴스 기본 라이트 테마" instanceDefaultLightTheme: "서버 기본 라이트 테마"
instanceDefaultDarkTheme: "인스턴스 기본 다크 테마" instanceDefaultDarkTheme: "서버 기본 다크 테마"
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요." instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요."
mutePeriod: "뮤트할 기간" mutePeriod: "뮤트할 기간"
period: "투표 기한"
indefinitely: "무기한" indefinitely: "무기한"
tenMinutes: "10분" tenMinutes: "10분"
oneHour: "1시간" oneHour: "1시간"
oneDay: "1일" oneDay: "1일"
oneWeek: "일주일" oneWeek: "일주일"
oneMonth: "1개월"
reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다." reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다."
failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다" failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다"
rateLimitExceeded: "요청 제한 횟수를 초과하였습니다" rateLimitExceeded: "요청 제한 횟수를 초과하였습니다"
@ -887,7 +898,7 @@ cannotUploadBecauseInappropriate: "이 파일은 부적절한 내용을 포함
cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다." cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다."
beta: "베타" beta: "베타"
enableAutoSensitive: "자동 NSFW 탐지" enableAutoSensitive: "자동 NSFW 탐지"
enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다." enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 서버 정책에 따라 자동으로 설정될 수 있습니다."
activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다." activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다."
navbar: "내비게이션 바" navbar: "내비게이션 바"
shuffle: "셔플" shuffle: "셔플"
@ -897,7 +908,7 @@ pushNotification: "푸시 알림"
subscribePushNotification: "푸시 알림 켜기" subscribePushNotification: "푸시 알림 켜기"
unsubscribePushNotification: "푸시 알림 끄기" unsubscribePushNotification: "푸시 알림 끄기"
pushNotificationAlreadySubscribed: "푸시 알림이 이미 켜져 있습니다" pushNotificationAlreadySubscribed: "푸시 알림이 이미 켜져 있습니다"
pushNotificationNotSupported: "브라우저나 인스턴스에서 푸시 알림이 지원되지 않습니다" pushNotificationNotSupported: "브라우저나 서버에서 푸시 알림이 지원되지 않습니다"
sendPushNotificationReadMessage: "푸시 알림이나 메시지를 읽은 뒤 푸시 알림을 삭제" sendPushNotificationReadMessage: "푸시 알림이나 메시지를 읽은 뒤 푸시 알림을 삭제"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」이라는 알림이 잠깐 표시됩니다. 기기의 전력 소비량이 증가할 수 있습니다." sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」이라는 알림이 잠깐 표시됩니다. 기기의 전력 소비량이 증가할 수 있습니다."
windowMaximize: "최대화" windowMaximize: "최대화"
@ -939,6 +950,11 @@ collapseRenotes: "이미 본 리노트를 간략화하기"
internalServerError: "내부 서버 오류" internalServerError: "내부 서버 오류"
internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다." internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
copyErrorInfo: "오류 정보 복사" copyErrorInfo: "오류 정보 복사"
joinThisServer: "이 서버에 가입"
exploreOtherServers: "다른 서버 둘러보기"
letsLookAtTimeline: "타임라인 구경하기"
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
_achievements: _achievements:
earnedAt: "달성 일시" earnedAt: "달성 일시"
_types: _types:
@ -1129,7 +1145,7 @@ _achievements:
description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다" description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다"
_viewInstanceChart: _viewInstanceChart:
title: "애널리스트" title: "애널리스트"
description: "인스턴스의 차트를 열었습니다" description: "서버의 차트를 열었습니다"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Hello, world!" title: "Hello, world!"
description: "스크래치패드에서 hello world를 출력했습니다" description: "스크래치패드에서 hello world를 출력했습니다"
@ -1166,7 +1182,7 @@ _achievements:
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "새해 복 많이 받으세요" title: "새해 복 많이 받으세요"
description: "새해 첫 날에 로그인했습니다" description: "새해 첫 날에 로그인했습니다"
flavor: "올해에도 저희 인스턴스에 관심을 가져 주셔서 감사합니다" flavor: "올해에도 저희 서버에 관심을 가져 주셔서 감사합니다"
_cookieClicked: _cookieClicked:
title: "쿠키를 클릭하는 게임" title: "쿠키를 클릭하는 게임"
description: "쿠키를 클릭했습니다" description: "쿠키를 클릭했습니다"
@ -1181,7 +1197,7 @@ _role:
name: "역할 이름" name: "역할 이름"
description: "역할 설명" description: "역할 설명"
permission: "역할 권한" permission: "역할 권한"
descriptionOfPermission: "<b>모더레이터</b>는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n<b>관리자</b>는 인스턴스의 모든 설정을 변경할 수 있습니다." descriptionOfPermission: "<b>모더레이터</b>는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n<b>관리자</b>는 서버의 모든 설정을 변경할 수 있습니다."
assignTarget: "할당 대상" assignTarget: "할당 대상"
descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다."
manual: "수동" manual: "수동"
@ -1209,7 +1225,7 @@ _role:
gtlAvailable: "글로벌 타임라인 보이기" gtlAvailable: "글로벌 타임라인 보이기"
ltlAvailable: "로컬 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용" canPublicNote: "공개 노트 허용"
canInvite: "인스턴스 초대 코드 발행" canInvite: "서버 초대 코드 발행"
canManageCustomEmojis: "커스텀 이모지 관리" canManageCustomEmojis: "커스텀 이모지 관리"
driveCapacity: "드라이브 용량" driveCapacity: "드라이브 용량"
pinMax: "고정할 수 있는 노트 수" pinMax: "고정할 수 있는 노트 수"
@ -1271,7 +1287,7 @@ _ad:
_forgotPassword: _forgotPassword:
enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다." enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다."
ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오." ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오."
contactAdmin: "이 인스턴스에서는 메일 기능이 지원되지 않습니다. 비밀번호를 재설정하려면 관리자에게 문의해 주십시오." contactAdmin: "이 서버에서는 메일 기능이 지원되지 않습니다. 비밀번호를 재설정하려면 관리자에게 문의해 주십시오."
_gallery: _gallery:
my: "내 갤러리" my: "내 갤러리"
liked: "좋아요 한 갤러리" liked: "좋아요 한 갤러리"
@ -1356,10 +1372,10 @@ _wordMute:
hard: "보다 높은 수준" hard: "보다 높은 수준"
mutedNotes: "뮤트된 노트" mutedNotes: "뮤트된 노트"
_instanceMute: _instanceMute:
instanceMuteDescription: "뮤트한 인스턴스에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다." instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다."
instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요" instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요"
title: "지정한 인스턴스의 노트를 숨깁니다." title: "지정한 서버의 노트를 숨깁니다."
heading: "뮤트할 인스턴스" heading: "뮤트할 서버"
_theme: _theme:
explore: "테마 찾아보기" explore: "테마 찾아보기"
install: "테마 설치" install: "테마 설치"
@ -1452,6 +1468,7 @@ _ago:
weeksAgo: "{n}주 전" weeksAgo: "{n}주 전"
monthsAgo: "{n}개월 전" monthsAgo: "{n}개월 전"
yearsAgo: "{n}년 전" yearsAgo: "{n}년 전"
invalid: "없음"
_time: _time:
second: "초" second: "초"
minute: "분" minute: "분"
@ -1471,7 +1488,7 @@ _tutorial:
step4_1: "노트 작성을 끝내셨나요?" step4_1: "노트 작성을 끝내셨나요?"
step4_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다." step4_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다."
step5_1: "이제, 다른 사람을 팔로우하여 타임라인을 활기차게 만들어보도록 합시다." step5_1: "이제, 다른 사람을 팔로우하여 타임라인을 활기차게 만들어보도록 합시다."
step5_2: "{featured}에서 이 인스턴스의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요. 마음에 드는 사람을 골라 팔로우해 보세요!" step5_2: "{featured}에서 이 서버의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요. 마음에 드는 사람을 골라 팔로우해 보세요!"
step5_3: "다른 유저를 팔로우하려면 해당 유저의 아이콘을 클릭하여 프로필 페이지를 띄운 후, 팔로우 버튼을 눌러 주세요." step5_3: "다른 유저를 팔로우하려면 해당 유저의 아이콘을 클릭하여 프로필 페이지를 띄운 후, 팔로우 버튼을 눌러 주세요."
step5_4: "사용자에 따라 팔로우가 승인될 때까지 시간이 걸릴 수 있습니다." step5_4: "사용자에 따라 팔로우가 승인될 때까지 시간이 걸릴 수 있습니다."
step6_1: "타임라인에 다른 사용자의 노트가 나타난다면 성공입니다." step6_1: "타임라인에 다른 사용자의 노트가 나타난다면 성공입니다."
@ -1485,14 +1502,29 @@ _tutorial:
step8_3: "알림 설정은 나중에도 변경할 수 있습니다." step8_3: "알림 설정은 나중에도 변경할 수 있습니다."
_2fa: _2fa:
alreadyRegistered: "이미 설정이 완료되었습니다." alreadyRegistered: "이미 설정이 완료되었습니다."
registerTOTP: "인증 앱 설정 시작"
passwordToTOTP: "비밀번호를 입력하세요."
step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다."
step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다."
step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다."
step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:" step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:"
step3Title: "인증 코드 입력"
step3: "앱에 표시된 토큰을 입력하시면 완료됩니다." step3: "앱에 표시된 토큰을 입력하시면 완료됩니다."
step4: "다음 로그인부터는 토큰을 입력해야 합니다." step4: "다음 로그인부터는 토큰을 입력해야 합니다."
securityKeyNotSupported: "이 브라우저는 보안 키를 지원하지 않습니다."
registerTOTPBeforeKey: "보안 키 또는 패스키를 등록하려면 인증 앱을 등록하십시오."
securityKeyInfo: "FIDO2를 지원하는 하드웨어 보안 키 혹은 디바이스의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다." securityKeyInfo: "FIDO2를 지원하는 하드웨어 보안 키 혹은 디바이스의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다."
chromePasskeyNotSupported: "현재 Chrome의 패스키는 지원되지 않습니다."
registerSecurityKey: "보안 키 또는 패스키 등록"
securityKeyName: "키 이름 입력"
tapSecurityKey: "브라우저의 지시에 따라 보안 키 또는 패스키를 등록하여 주십시오"
removeKey: "보안 키를 삭제"
removeKeyConfirm: "{name} 을(를) 삭제하시겠습니까?" removeKeyConfirm: "{name} 을(를) 삭제하시겠습니까?"
renewTOTPCancel: "나중에" whyTOTPOnlyRenew: "보안 키가 등록되어 있는 경우 인증 앱을 해제할 수 없습니다."
renewTOTP: "인증 앱 재설정"
renewTOTPConfirm: "기존에 등록되어 있던 인증 키는 사용하지 못하게 됩니다."
renewTOTPOk: "재설정"
renewTOTPCancel: "취소"
_permissions: _permissions:
"read:account": "계정의 정보를 봅니다" "read:account": "계정의 정보를 봅니다"
"write:account": "계정의 정보를 변경합니다" "write:account": "계정의 정보를 변경합니다"
@ -1551,7 +1583,7 @@ _weekday:
saturday: "토요일" saturday: "토요일"
_widgets: _widgets:
profile: "프로필" profile: "프로필"
instanceInfo: "인스턴스 정보" instanceInfo: "서버 정보"
memo: "스티커 메모" memo: "스티커 메모"
notifications: "알림" notifications: "알림"
timeline: "타임라인" timeline: "타임라인"
@ -1565,7 +1597,7 @@ _widgets:
digitalClock: "디지털 시계" digitalClock: "디지털 시계"
unixClock: "UNIX 시계" unixClock: "UNIX 시계"
federation: "연합" federation: "연합"
instanceCloud: "인스턴스 구름" instanceCloud: "서버 구름"
postForm: "글 입력란" postForm: "글 입력란"
slideshow: "슬라이드 쇼" slideshow: "슬라이드 쇼"
button: "버튼" button: "버튼"
@ -1615,6 +1647,8 @@ _visibility:
followersDescription: "팔로워에게만 공개" followersDescription: "팔로워에게만 공개"
specified: "다이렉트" specified: "다이렉트"
specifiedDescription: "지정한 유저에게만 공개" specifiedDescription: "지정한 유저에게만 공개"
disableFederation: "연합에 보내지 않기"
disableFederationDescription: "다른 서버로 보내지 않습니다"
_postForm: _postForm:
replyPlaceholder: "이 노트에 답글..." replyPlaceholder: "이 노트에 답글..."
quotePlaceholder: "이 노트를 인용..." quotePlaceholder: "이 노트를 인용..."
@ -1770,6 +1804,7 @@ _notification:
pollEnded: "투표가 종료됨" pollEnded: "투표가 종료됨"
receiveFollowRequest: "팔로우 요청을 받았을 때" receiveFollowRequest: "팔로우 요청을 받았을 때"
followRequestAccepted: "팔로우 요청이 승인되었을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때"
achievementEarned: "도전 과제 획득"
app: "연동된 앱을 통한 알림" app: "연동된 앱을 통한 알림"
_actions: _actions:
followBack: "팔로우" followBack: "팔로우"
@ -1802,3 +1837,6 @@ _deck:
channel: "채널" channel: "채널"
mentions: "받은 멘션" mentions: "받은 멘션"
direct: "다이렉트" direct: "다이렉트"
_dialog:
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"

View file

@ -168,7 +168,9 @@ done: "ສຳເລັດ"
processing: "ກຳລັງປະມວນຜົນ" processing: "ກຳລັງປະມວນຜົນ"
preview: "ສະແດງເປັນຕົວຢ່າງ" preview: "ສະແດງເປັນຕົວຢ່າງ"
default: "ຄ່າເລີ່ມຕົ້ນ" default: "ຄ່າເລີ່ມຕົ້ນ"
federating: "ສະຫະພັນ"
blocked: "ບລັອກແລ້ວ " blocked: "ບລັອກແລ້ວ "
suspended: "ໂຈະ"
all: "ທັງໝົດ" all: "ທັງໝົດ"
subscribing: "ສະໝັກສະມາຊິກແລັວ" subscribing: "ສະໝັກສະມາຊິກແລັວ"
publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່" publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່"
@ -177,15 +179,35 @@ instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າ
instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ" instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ"
instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​" instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​"
changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ" changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ"
security: "ຄວາມປອດໄພ"
retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ"
currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ"
more: "ເພີ່ມເຕີມ!"
featured: "ໄຮໄລທ໌" featured: "ໄຮໄລທ໌"
usernameOrUserId: "ຊື່ຜູ້ໃຊ້ ຫຼື id ຜູ້ໃຊ້"
noSuchUser: "ບໍ່ພົບຜູ້ໃຊ້"
lookup: "ຄົ້ນ​ຫາ"
announcements: "ປະກາດ" announcements: "ປະກາດ"
imageUrl: "URL ຮູບພາບ"
remove: "ລຶບ" remove: "ລຶບ"
removed: "ລຶບແລ້ວ"
resetAreYouSure: "ຣີ​ເຊັດບໍ?"
saved: "ບັນທຶກແລ້ວ"
messaging: "ແຊ໋ດ" messaging: "ແຊ໋ດ"
upload: "ອັບໂຫຼດ"
keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ"
fromUrl: "ຈາກ URL"
uploadFromUrl: "ອັບໂຫຼດຈາກ URL"
uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ"
messageRead: "ອ່ານແລ້ວ"
startMessaging: "ເລີ່ມການສົນທະນາໃໝ່"
nUsersRead: "ອ່ານໂດຍ {n}"
tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ" tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ"
start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ" start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ"
home: "ໜ້າຫຼັກ" home: "ໜ້າຫຼັກ"
images: "ຮູບພາບ" images: "ຮູບພາບ"
birthday: "ວັນເກີດ" birthday: "ວັນເກີດ"
yearsOld: "{age} ປີ"
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ" registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
location: "ທີ່ຕັ້ງ" location: "ທີ່ຕັ້ງ"
theme: "ແທ໋ມ" theme: "ແທ໋ມ"
@ -193,17 +215,96 @@ light: "ສະຫວ່າງ"
dark: "ມືດ" dark: "ມືດ"
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ" lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
darkThemes: "ຮູບແບບສີສັນມືດ" darkThemes: "ຮູບແບບສີສັນມືດ"
drive: "ຂັບ"
fileName: "ຊື່ໄຟລ໌" fileName: "ຊື່ໄຟລ໌"
selectFile: "ເລືອກໄຟລ໌" selectFile: "ເລືອກໄຟລ໌"
selectFiles: "ເລືອກໄຟລ໌" selectFiles: "ເລືອກໄຟລ໌"
selectFolder: "ເລືອກໂຟລເດີ"
selectFolders: "ເລືອກໂຟລເດີ"
renameFile: "ປ່ຽນຊື່ໄຟລ໌"
folderName: "ຊື່ໂຟນເດີ"
createFolder: "​ສ້າງ​ໂຟ​ລ​ເດີ"
renameFolder: "ປ່ຽນຊື່ໂຟນເດີນີ້"
deleteFolder: "ລົບໂຟ​ລ​ເດີ​"
addFile: "ເພີ່ມໄຟລ໌"
emptyDrive: "Drive ຂອງທ່ານຫວ່າງເປົ່າ"
emptyFolder: "ໂຟນເດີນີ້ເປົ່າຫວ່າງ"
unableToDelete: "ບໍ່​ສາ​ມາດລົບໄດ້"
inputNewFileName: "ໃສ່ຊື່ໄຟລ໌ໃໝ່"
inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່"
inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່"
circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ"
rename: "ປ່ຽນຊື່"
nsfw: "NSFW" nsfw: "NSFW"
watch: "ເບິ່ງ"
unwatch: "ຢຸດເບິ່ງ"
accept: "ອະນຸຍາດ" accept: "ອະນຸຍາດ"
reject: "ປະຕິເສດ"
normal: "ປົກກະຕິ"
instanceName: "ຊື່ເຊີເວີ້"
instanceDescription: "ຄໍາອະທິບາຍຕົວຢ່າງ"
maintainerName: "ຜູ້ດູແລ"
maintainerEmail: "ອີເມວ admin"
tosUrl: "ເງື່ອນໄຂການໃຫ້ບໍລິການ URL"
thisYear: "ປີນີ້"
thisMonth: "ເດືອນນີ້"
today: "ມື້ນີ້"
dayX: "ວັນ {day}"
monthX: "ເດືອນ {month}"
yearX: "ປີ {year}"
pages: "ໜ້າ"
integration: "ຄວາມສຳພັນຂອງ"
connectService: "ເຊື່ອມຕໍ່"
disconnectService: "ຕັດການເຊື່ອມຕໍ່"
enableLocalTimeline: "ເປີດໃຊ້ທາມລາຍທ້ອງຖິ່ນ"
enableGlobalTimeline: "ເປີດໃຊ້ທາມລາຍທົ່ວໂລກ"
disablingTimelinesInfo: "ຜູ້ເບິ່ງແຍງລະບົບ ແລະຜູ້ຄວບຄຸມຈະມີການເຂົ້າເຖິງທຸກກຳນົດເວລາ, ເຖິງແມ່ນວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍຕາມ"
registration: "ລົງທະບຽນ"
enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜູ້ໃຊ້ໃໝ່"
invite: "ເຊີນ"
driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ"
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້" pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
userList: "ລາຍການ" userList: "ລາຍການ"
about: "ກ່ຽວກັບ"
aboutMisskey: "ກ່ຽວກັບ Misskey"
administrator: "ຜູ້ບໍລິຫານ"
share: "ແບ່ງປັນ"
notFound: "ບໍ່ພົບ"
cacheClear: "ລຶບລ້າງແຄສ"
invites: "ເຊີນ"
title: "ຫົວຂໍ້"
text: "ຂໍ້ຄວາມ"
enable: "ເປີດໃຊ້"
next: "ຕໍ່ໄປ"
invitations: "ເຊີນ"
language: "ພາສາ"
native: "ພາ​ສາ​ແມ່"
category: "ຫມວດຫມູ່"
tags: "ແທ໋ກ"
createAccount: "ສ້າງບັນຊີ"
existingAccount: "ທີ່ມີຢູ່"
dashboard: "ໜ້າປັດ"
local: "ທ້ອງຖິ່ນ"
objectStorageRegion: "ພາກ​ພື້ນ"
sounds: "ສຽງ"
sound: "ສຽງ"
none: "ບໍ່ມີ"
volume: "ລະດັບສຽງ"
details: "ລາຍລະອຽດ"
install: "ຕິດຕັ້ງ"
uninstall: "ຖອນການຕິດຕັ້ງ"
state: "ສະຖານະ"
sort: "ຈັດຮຽງໂດຍ"
ascendingOrder: "ນ້ອຍໄປຫາໃຫຍ່"
descendingOrder: "ໃຫຍ່ຫານ້ອຍ"
output: "ຜົນຜະລິດ"
script: "ບົດ​ຄວາມ"
smtpHost: "ໂຮດສ" smtpHost: "ໂຮດສ"
smtpUser: "ຊື່ຜູ້ໃຊ້" smtpUser: "ຊື່ຜູ້ໃຊ້"
smtpPass: "ລະຫັດຜ່ານ" smtpPass: "ລະຫັດຜ່ານ"
clearCache: "ລຶບລ້າງແຄສ" clearCache: "ລຶບລ້າງແຄສ"
info: "ກ່ຽວກັບ"
user: "ຜູ້ໃຊ້ຕ່າງໆ" user: "ຜູ້ໃຊ້ຕ່າງໆ"
searchByGoogle: "ຄົ້ນຫາ" searchByGoogle: "ຄົ້ນຫາ"
file: "ໄຟລ໌" file: "ໄຟລ໌"
@ -244,6 +345,8 @@ _charts:
federation: "ສະຫະພັນ" federation: "ສະຫະພັນ"
_timelines: _timelines:
home: "ໜ້າຫຼັກ" home: "ໜ້າຫຼັກ"
_play:
script: "ບົດ​ຄວາມ"
_pages: _pages:
blocks: blocks:
image: "ຮູບພາບ" image: "ຮູບພາບ"

View file

@ -810,6 +810,7 @@ auto: "Automatycznie"
size: "Rozmiar" size: "Rozmiar"
numberOfColumn: "Liczba kolumn" numberOfColumn: "Liczba kolumn"
searchByGoogle: "Szukaj" searchByGoogle: "Szukaj"
period: "Ankieta kończy się"
indefinitely: "Nigdy" indefinitely: "Nigdy"
tenMinutes: "10 minut" tenMinutes: "10 minut"
oneHour: "1 godzina" oneHour: "1 godzina"
@ -1061,6 +1062,7 @@ _ago:
weeksAgo: "{n} tyg. temu" weeksAgo: "{n} tyg. temu"
monthsAgo: "{n} mies. temu" monthsAgo: "{n} mies. temu"
yearsAgo: "{n} lat temu" yearsAgo: "{n} lat temu"
invalid: "Nie ma tu niczego"
_time: _time:
second: "sekunda" second: "sekunda"
minute: "minuta" minute: "minuta"

View file

@ -648,6 +648,8 @@ _sfx:
note: "Note" note: "Note"
notification: "Notificări" notification: "Notificări"
chat: "Chat" chat: "Chat"
_ago:
invalid: "Nu e nimic de văzut aici"
_widgets: _widgets:
profile: "Profil" profile: "Profil"
instanceInfo: "Informații despre instanță" instanceInfo: "Informații despre instanță"

View file

@ -393,13 +393,19 @@ about: "Описание"
aboutMisskey: "О Misskey" aboutMisskey: "О Misskey"
administrator: "Администратор" administrator: "Администратор"
token: "Токен" token: "Токен"
2fa: "2-х факторная аутентификация"
totp: "Приложение-аутентификатор"
totpDescription: "Описание приложения-аутентификатора"
moderator: "Модератор" moderator: "Модератор"
moderation: "Модерация" moderation: "Модерация"
nUsersMentioned: "Упомянуло пользователей: {n}" nUsersMentioned: "Упомянуло пользователей: {n}"
securityKeyAndPasskey: "Ключ безопасности и парольная фраза"
securityKey: "Ключ безопасности" securityKey: "Ключ безопасности"
lastUsed: "Последнее использование" lastUsed: "Последнее использование"
lastUsedAt: "Последнее использование: {t}"
unregister: "Отписаться" unregister: "Отписаться"
passwordLessLogin: "Настроить вход без пароля" passwordLessLogin: "Настроить вход без пароля"
passwordLessLoginDescription: "Вход без пароля"
resetPassword: "Сброс пароля:" resetPassword: "Сброс пароля:"
newPasswordIs: "Новый пароль — «{password}»." newPasswordIs: "Новый пароль — «{password}»."
reduceUiAnimation: "Уменьшить анимацию в пользовательском интерфейсе" reduceUiAnimation: "Уменьшить анимацию в пользовательском интерфейсе"
@ -773,6 +779,7 @@ popularPosts: "Популярные публикации"
shareWithNote: "Поделиться заметкой" shareWithNote: "Поделиться заметкой"
ads: "Реклама" ads: "Реклама"
expiration: "Опрос длится" expiration: "Опрос длится"
startingperiod: "Начальный период"
memo: "Памятка" memo: "Памятка"
priority: "Приоритет" priority: "Приоритет"
high: "Высокий" high: "Высокий"
@ -805,6 +812,7 @@ lastCommunication: "Последнее сообщение"
resolved: "Решено" resolved: "Решено"
unresolved: "Без решения" unresolved: "Без решения"
breakFollow: "Отписка" breakFollow: "Отписка"
breakFollowConfirm: "Удалить из подписок пользователя ?"
itsOn: "Включено" itsOn: "Включено"
itsOff: "Выключено" itsOff: "Выключено"
emailRequiredForSignup: "Для регистрации учётной записи нужен адрес электронной почты" emailRequiredForSignup: "Для регистрации учётной записи нужен адрес электронной почты"
@ -839,6 +847,7 @@ instanceDefaultLightTheme: "Светлая тема по умолчанию"
instanceDefaultDarkTheme: "Темная тема по умолчанию" instanceDefaultDarkTheme: "Темная тема по умолчанию"
instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса" instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса"
mutePeriod: "Продолжительность скрытия" mutePeriod: "Продолжительность скрытия"
period: "Опрос длится"
indefinitely: "вечно" indefinitely: "вечно"
tenMinutes: "10 минут" tenMinutes: "10 минут"
oneHour: "1 час" oneHour: "1 час"
@ -939,6 +948,10 @@ collapseRenotes: "Свернуть репосты"
internalServerError: "Внутренняя ошибка сервера" internalServerError: "Внутренняя ошибка сервера"
internalServerErrorDescription: "Внутри сервера произошла непредвиденная ошибка." internalServerErrorDescription: "Внутри сервера произошла непредвиденная ошибка."
copyErrorInfo: "Скопировать код ошибки" copyErrorInfo: "Скопировать код ошибки"
joinThisServer: "Присоединяйтесь к этому серверу"
exploreOtherServers: "Искать другие сервера"
letsLookAtTimeline: "Давайте посмотрим на ленту"
disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию."
_achievements: _achievements:
earnedAt: "Разблокировано в" earnedAt: "Разблокировано в"
_types: _types:
@ -1452,6 +1465,7 @@ _ago:
weeksAgo: "{n} нед. назад" weeksAgo: "{n} нед. назад"
monthsAgo: "{n} мес. назад" monthsAgo: "{n} мес. назад"
yearsAgo: "{n} г. назад" yearsAgo: "{n} г. назад"
invalid: "Ничего нет"
_time: _time:
second: "с" second: "с"
minute: "мин" minute: "мин"
@ -1485,13 +1499,28 @@ _tutorial:
step8_3: "Эту настройку вы всегда сможете поменять" step8_3: "Эту настройку вы всегда сможете поменять"
_2fa: _2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена." alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerTOTP: "Начните настраивать приложение-аутентификатор"
passwordToTOTP: "Пожалуйста, введите свой пароль"
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве."
step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):" step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):"
step3Title: "Введите проверочный код"
step3: "И наконец, введите код, который покажет приложение." step3: "И наконец, введите код, который покажет приложение."
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
securityKeyNotSupported: "Ваш браузер не поддерживает ключи безопасности."
registerTOTPBeforeKey: "Чтобы зарегистрировать ключ безопасности и пароль, сначала настройте приложение аутентификации."
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве." securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
chromePasskeyNotSupported: "В настоящее время Chrome не поддерживает пароль-ключи."
registerSecurityKey: "Зарегистрируйте ключ безопасности ・Passkey"
securityKeyName: "Введите имя для ключа"
tapSecurityKey: "Пожалуйста, следуйте инструкциям в вашем браузере, чтобы зарегистрировать свой ключ безопасности или пароль"
removeKey: "Удалить ключ безопасности"
removeKeyConfirm: "Удалить резервную копию «{name}»?" removeKeyConfirm: "Удалить резервную копию «{name}»?"
whyTOTPOnlyRenew: "Если ключ безопасности зарегистрирован, вы не сможете отключить приложение аутентификации."
renewTOTP: "Перенастроите приложение аутентификации"
renewTOTPConfirm: "Проверочный код предыдущего приложения для аутентификации больше не будет доступен"
renewTOTPOk: "Настроить"
renewTOTPCancel: "Нет, спасибо" renewTOTPCancel: "Нет, спасибо"
_permissions: _permissions:
"read:account": "Просматривать данные учётной записи" "read:account": "Просматривать данные учётной записи"
@ -1615,6 +1644,8 @@ _visibility:
followersDescription: "Только вашим подписчикам" followersDescription: "Только вашим подписчикам"
specified: "Личное" specified: "Личное"
specifiedDescription: "Тем, кого укажете" specifiedDescription: "Тем, кого укажете"
disableFederation: "Отключить федерацию"
disableFederationDescription: "Не доставляет в другие экземпляры"
_postForm: _postForm:
replyPlaceholder: "Ответ на заметку..." replyPlaceholder: "Ответ на заметку..."
quotePlaceholder: "Пояснение к цитате..." quotePlaceholder: "Пояснение к цитате..."
@ -1770,6 +1801,7 @@ _notification:
pollEnded: "Окончания опросов" pollEnded: "Окончания опросов"
receiveFollowRequest: "Получен запрос на подписку" receiveFollowRequest: "Получен запрос на подписку"
followRequestAccepted: "Запрос на подписку одобрен" followRequestAccepted: "Запрос на подписку одобрен"
achievementEarned: "Получение достижений"
app: "Уведомления из приложений" app: "Уведомления из приложений"
_actions: _actions:
followBack: "отвечает взаимной подпиской" followBack: "отвечает взаимной подпиской"
@ -1802,3 +1834,6 @@ _deck:
channel: "Каналы" channel: "Каналы"
mentions: "Упоминания" mentions: "Упоминания"
direct: "Личное" direct: "Личное"
_dialog:
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"

View file

@ -103,6 +103,8 @@ renoted: "Preposlané."
cantRenote: "Tento príspevok sa nedá preposlať." cantRenote: "Tento príspevok sa nedá preposlať."
cantReRenote: "Odpoveď nemôže byť odstránená." cantReRenote: "Odpoveď nemôže byť odstránená."
quote: "Citovať" quote: "Citovať"
inChannelRenote: "Preposlania v kanáli"
inChannelQuote: "Citácie v kanáli"
pinnedNote: "Pripnuté poznámky" pinnedNote: "Pripnuté poznámky"
pinned: "Pripnúť" pinned: "Pripnúť"
you: "Vy" you: "Vy"
@ -129,6 +131,7 @@ unblockConfirm: "Naozaj chcete odblokovať tento účet?"
suspendConfirm: "Naozaj chcete zmraziť tento účet?" suspendConfirm: "Naozaj chcete zmraziť tento účet?"
unsuspendConfirm: "Naozaj chcete odmraziť tento účet?" unsuspendConfirm: "Naozaj chcete odmraziť tento účet?"
selectList: "Vyberte zoznam" selectList: "Vyberte zoznam"
selectChannel: "Zvoľte kanál"
selectAntenna: "Vyberte anténu" selectAntenna: "Vyberte anténu"
selectWidget: "Vyberte widget" selectWidget: "Vyberte widget"
editWidgets: "Upraviť widget" editWidgets: "Upraviť widget"
@ -256,6 +259,8 @@ noMoreHistory: "To je všetko"
startMessaging: "Začať chat" startMessaging: "Začať chat"
nUsersRead: "prečítané {n} používateľmi" nUsersRead: "prečítané {n} používateľmi"
agreeTo: "Súhlasím s {0}" agreeTo: "Súhlasím s {0}"
agreeBelow: "Súhlasím s nasledovným"
basicNotesBeforeCreateAccount: "Základné bezpečnostné opatrenia"
tos: "Podmienky používania" tos: "Podmienky používania"
start: "Začať" start: "Začať"
home: "Domov" home: "Domov"
@ -388,13 +393,19 @@ about: "Informácie"
aboutMisskey: "O Misskey" aboutMisskey: "O Misskey"
administrator: "Administrátor" administrator: "Administrátor"
token: "Token" token: "Token"
2fa: "Dvojfaktorové overenie (2FA)"
totp: "Overovacia aplikácia"
totpDescription: "Zadajte jednorazové heslo z overovacej aplikácie"
moderator: "Moderátor" moderator: "Moderátor"
moderation: "Moderovanie" moderation: "Moderovanie"
nUsersMentioned: "{n} používateľov spomenulo" nUsersMentioned: "{n} používateľov spomenulo"
securityKeyAndPasskey: "Bezpečnostný kľúč/heslo"
securityKey: "Bezpečnostný kľúč" securityKey: "Bezpečnostný kľúč"
lastUsed: "Naposledy použité" lastUsed: "Naposledy použité"
lastUsedAt: "Naposledy použité: {t}"
unregister: "Odregistrovať" unregister: "Odregistrovať"
passwordLessLogin: "Nastaviť bezheslové prihlásenie" passwordLessLogin: "Nastaviť bezheslové prihlásenie"
passwordLessLoginDescription: "Prihlásenie bez hesla, len bezpečnostným kľúčom alebo prístupovým kľúčom"
resetPassword: "Resetovať heslo" resetPassword: "Resetovať heslo"
newPasswordIs: "Nové heslo je \"{password}\"" newPasswordIs: "Nové heslo je \"{password}\""
reduceUiAnimation: "Menej UI animácií" reduceUiAnimation: "Menej UI animácií"
@ -446,8 +457,11 @@ aboutX: "O {x}"
emojiStyle: "Štýl emoji" emojiStyle: "Štýl emoji"
native: "Natívne" native: "Natívne"
disableDrawer: "Nepoužívať šuflíkové menu" disableDrawer: "Nepoužívať šuflíkové menu"
showNoteActionsOnlyHover: "Ovládacie prvky poznámky sa zobrazujú len po nabehnutí myši"
noHistory: "Žiadna história" noHistory: "Žiadna história"
signinHistory: "História prihlásení" signinHistory: "História prihlásení"
enableAdvancedMfm: "Povolenie pokročilého MFM"
enableAnimatedMfm: "Povoliť animované MFM"
doing: "Pracujem..." doing: "Pracujem..."
category: "Kategórie" category: "Kategórie"
tags: "Značky" tags: "Značky"
@ -556,6 +570,7 @@ manage: "Administrácia"
plugins: "Pluginy" plugins: "Pluginy"
preferencesBackups: "Zálohy nastavení" preferencesBackups: "Zálohy nastavení"
deck: "Deck" deck: "Deck"
undeck: "Oddokovať"
useBlurEffectForModal: "Použiť efekt rozmazania na okná" useBlurEffectForModal: "Použiť efekt rozmazania na okná"
useFullReactionPicker: "Použiť plnú veľkosť výberu reakcií" useFullReactionPicker: "Použiť plnú veľkosť výberu reakcií"
width: "Šírka" width: "Šírka"
@ -765,6 +780,7 @@ popularPosts: "Populárne príspevky"
shareWithNote: "Zdieľať s poznámkou" shareWithNote: "Zdieľať s poznámkou"
ads: "Reklamy" ads: "Reklamy"
expiration: "Ukončiť hlasovanie" expiration: "Ukončiť hlasovanie"
startingperiod: "Začiatok"
memo: "Memo" memo: "Memo"
priority: "Priorita" priority: "Priorita"
high: "Vysoká" high: "Vysoká"
@ -831,11 +847,13 @@ instanceDefaultLightTheme: "Predvolená svetlá téma"
instanceDefaultDarkTheme: "Predvolená tmavá téma" instanceDefaultDarkTheme: "Predvolená tmavá téma"
instanceDefaultThemeDescription: "Vložte kód témy v objektovom formáte" instanceDefaultThemeDescription: "Vložte kód témy v objektovom formáte"
mutePeriod: "Trvanie stíšenia" mutePeriod: "Trvanie stíšenia"
period: "Ukončiť hlasovanie"
indefinitely: "Navždy" indefinitely: "Navždy"
tenMinutes: "10 minút" tenMinutes: "10 minút"
oneHour: "1 hodina" oneHour: "1 hodina"
oneDay: "1 deň" oneDay: "1 deň"
oneWeek: "1 týždeň" oneWeek: "1 týždeň"
oneMonth: "1 mesiac"
reflectMayTakeTime: "Zmeny môžu chvíľu trvať kým sa prejavia." reflectMayTakeTime: "Zmeny môžu chvíľu trvať kým sa prejavia."
failedToFetchAccountInformation: "Nepodarilo sa načítať informácie o účte." failedToFetchAccountInformation: "Nepodarilo sa načítať informácie o účte."
rateLimitExceeded: "Prekročený limit rýchlosti" rateLimitExceeded: "Prekročený limit rýchlosti"
@ -1123,6 +1141,7 @@ _ago:
weeksAgo: "pred {n} týždňami" weeksAgo: "pred {n} týždňami"
monthsAgo: "pred {n} mesiacmi" monthsAgo: "pred {n} mesiacmi"
yearsAgo: "pred {n} rokmi" yearsAgo: "pred {n} rokmi"
invalid: "Nič tu nie je"
_time: _time:
second: "s" second: "s"
minute: "min" minute: "min"

View file

@ -268,7 +268,7 @@ remoteUserCaution: "เนื่องจากผู้ใช้งานรา
activity: "กิจกรรม" activity: "กิจกรรม"
images: "รูปภาพ" images: "รูปภาพ"
birthday: "วันเกิด" birthday: "วันเกิด"
yearsOld: "{อายุ} ปี" yearsOld: "{age} ปี"
registeredDate: "วันที่สมัครสมาชิก" registeredDate: "วันที่สมัครสมาชิก"
location: "ตำแหน่งที่ตั้ง" location: "ตำแหน่งที่ตั้ง"
theme: "ธีม" theme: "ธีม"
@ -393,13 +393,19 @@ about: "เกี่ยวกับ"
aboutMisskey: "เกี่ยวกับ Misskey" aboutMisskey: "เกี่ยวกับ Misskey"
administrator: "ผู้ดูแลระบบ" administrator: "ผู้ดูแลระบบ"
token: "โทเค็น" token: "โทเค็น"
2fa: "การยืนยันตัวตนแบบสองชั้น"
totp: "แอป Authenticator"
totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว"
moderator: "ผู้ควบคุม" moderator: "ผู้ควบคุม"
moderation: "การกลั่นกรอง" moderation: "การกลั่นกรอง"
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้" nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้"
securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
securityKey: "กุญแจความปลอดภัย" securityKey: "กุญแจความปลอดภัย"
lastUsed: "ใช้ล่าสุด" lastUsed: "ใช้ล่าสุด"
lastUsedAt: "ใช้งานครั้งล่าสุด: {t}"
unregister: "เลิกติดตาม" unregister: "เลิกติดตาม"
passwordLessLogin: "เข้าสู่ระบบแบบไม่ใช้รหัสผ่าน" passwordLessLogin: "เข้าสู่ระบบแบบไม่ใช้รหัสผ่าน"
passwordLessLoginDescription: "อนุญาตให้เข้าสู่ระบบโดยไม่ต้องใช้รหัสผ่านโดยใช้รหัสรักษาความปลอดภัยหรือรหัสผ่านเท่านั้น"
resetPassword: "รีเซ็ตรหัสผ่าน" resetPassword: "รีเซ็ตรหัสผ่าน"
newPasswordIs: "รหัสผ่านใหม่คือ \"{password}\"" newPasswordIs: "รหัสผ่านใหม่คือ \"{password}\""
reduceUiAnimation: "ลดภาพเคลื่อนไหว UI" reduceUiAnimation: "ลดภาพเคลื่อนไหว UI"
@ -451,6 +457,7 @@ aboutX: "เกี่ยวกับ {x}"
emojiStyle: "สไตล์อิโมจิ" emojiStyle: "สไตล์อิโมจิ"
native: "ภาษาแม่" native: "ภาษาแม่"
disableDrawer: "อย่าใช้ลิ้นชักสไตล์เมนู" disableDrawer: "อย่าใช้ลิ้นชักสไตล์เมนู"
showNoteActionsOnlyHover: "แสดงการดำเนินการเฉพาะโน้ตเมื่อโฮเวอร์"
noHistory: "ไม่มีรายการ" noHistory: "ไม่มีรายการ"
signinHistory: "ประวัติการเข้าสู่ระบบ" signinHistory: "ประวัติการเข้าสู่ระบบ"
enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง" enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง"
@ -499,6 +506,7 @@ objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในกา
serverLogs: "บันทึกของเซิร์ฟเวอร์" serverLogs: "บันทึกของเซิร์ฟเวอร์"
deleteAll: "ลบทั้งหมด" deleteAll: "ลบทั้งหมด"
showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์" showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์"
showFixedPostFormInChannel: "แสดงแบบฟอร์มกำลังโพสต์ที่ด้านบนของไทม์ไลน์ (แชนแนล)"
newNoteRecived: "มีโน้ตใหม่" newNoteRecived: "มีโน้ตใหม่"
sounds: "เสียง" sounds: "เสียง"
sound: "เสียง" sound: "เสียง"
@ -773,6 +781,7 @@ popularPosts: "โพสต์ติดอันดับ"
shareWithNote: "แบ่งปันด้วยโน้ต" shareWithNote: "แบ่งปันด้วยโน้ต"
ads: "โฆษณา" ads: "โฆษณา"
expiration: "กำหนดเวลา" expiration: "กำหนดเวลา"
startingperiod: "เริ่ม"
memo: "ข้อควรจำ" memo: "ข้อควรจำ"
priority: "ลำดับความสำคัญ" priority: "ลำดับความสำคัญ"
high: "สูง" high: "สูง"
@ -805,6 +814,7 @@ lastCommunication: "การสื่อสารครั้งสุดท้
resolved: "คลี่คลายแล้ว" resolved: "คลี่คลายแล้ว"
unresolved: "รอการเฉลย" unresolved: "รอการเฉลย"
breakFollow: "ลบผู้ติดตาม" breakFollow: "ลบผู้ติดตาม"
breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?"
itsOn: "เปิดใช้งาน" itsOn: "เปิดใช้งาน"
itsOff: "ปิดใช้งาน" itsOff: "ปิดใช้งาน"
emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร" emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร"
@ -839,11 +849,13 @@ instanceDefaultLightTheme: "ธีมสว่างค่าเริ่มต
instanceDefaultDarkTheme: "ธีมมืดค่าเริ่มต้นอินสแตนซ์" instanceDefaultDarkTheme: "ธีมมืดค่าเริ่มต้นอินสแตนซ์"
instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์" instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์"
mutePeriod: "ระยะเวลาปิดเสียง" mutePeriod: "ระยะเวลาปิดเสียง"
period: "สิ้นสุดการสำรวจความคิดเห็น"
indefinitely: "ตลอดไป" indefinitely: "ตลอดไป"
tenMinutes: "10 นาที" tenMinutes: "10 นาที"
oneHour: "1 ชั่วโมง" oneHour: "1 ชั่วโมง"
oneDay: "1 วัน" oneDay: "1 วัน"
oneWeek: "1 สัปดาห์" oneWeek: "1 สัปดาห์"
oneMonth: "หนึ่งเดือน"
reflectMayTakeTime: "อาจจำเป็นต้องใช้เวลาสักระยะหนึ่งจึงจะเห็นแสดงผลได้นะ" reflectMayTakeTime: "อาจจำเป็นต้องใช้เวลาสักระยะหนึ่งจึงจะเห็นแสดงผลได้นะ"
failedToFetchAccountInformation: "ไม่สามารถเรียกดึงข้อมูลบัญชีได้" failedToFetchAccountInformation: "ไม่สามารถเรียกดึงข้อมูลบัญชีได้"
rateLimitExceeded: "เกินขีดจำกัดอัตรา" rateLimitExceeded: "เกินขีดจำกัดอัตรา"
@ -939,6 +951,14 @@ collapseRenotes: "ยุบ renotes ที่คุณได้เห็นแ
internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด" internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด"
internalServerErrorDescription: "เซิร์ฟเวอร์รันค้นพบข้อผิดพลาดที่ไม่คาดคิด" internalServerErrorDescription: "เซิร์ฟเวอร์รันค้นพบข้อผิดพลาดที่ไม่คาดคิด"
copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด" copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด"
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ"
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
_achievements: _achievements:
earnedAt: "ได้รับเมื่อ" earnedAt: "ได้รับเมื่อ"
_types: _types:
@ -1452,6 +1472,7 @@ _ago:
weeksAgo: "{n} สัปดาห์ที่แล้ว" weeksAgo: "{n} สัปดาห์ที่แล้ว"
monthsAgo: "{n} เดือนที่แล้ว" monthsAgo: "{n} เดือนที่แล้ว"
yearsAgo: "{n} ปีที่ผ่านมา" yearsAgo: "{n} ปีที่ผ่านมา"
invalid: "ไม่พบผลลัพธ์"
_time: _time:
second: "วินาที" second: "วินาที"
minute: "นาที" minute: "นาที"
@ -1485,13 +1506,28 @@ _tutorial:
step8_3: "คุณสามารถเปลี่ยนการตั้งค่านี้ในภายหลังได้ตลอดเวลานะ" step8_3: "คุณสามารถเปลี่ยนการตั้งค่านี้ในภายหลังได้ตลอดเวลานะ"
_2fa: _2fa:
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์"
passwordToTOTP: "กรอกรหัสผ่าน"
step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ" step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ"
step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้" step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้"
step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้"
step2Url: "คุณยังสามารถป้อนบน URL นี้หากคุณใช้โปรแกรมเดสก์ท็อป:" step2Url: "คุณยังสามารถป้อนบน URL นี้หากคุณใช้โปรแกรมเดสก์ท็อป:"
step3Title: "ป้อนรหัสยืนยัน"
step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า" step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า"
step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว" step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว"
securityKeyNotSupported: "เบราว์เซอร์ของคุณไม่รองรับคีย์ความปลอดภัยนะ"
registerTOTPBeforeKey: "กรุณาตั้งค่าแอปยืนยันตัวตนเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน"
securityKeyInfo: "นอกจากนี้การตรวจสอบความถูกต้องด้วยลายนิ้วมือหรือ PIN แล้ว คุณยังสามารถตั้งค่าการตรวจสอบสิทธิ์ผ่านคีย์ความปลอดภัยของฮาร์ดแวร์ที่รองรับ FIDO2 เพื่อเพิ่มความปลอดภัยให้กับบัญชีของคุณ" securityKeyInfo: "นอกจากนี้การตรวจสอบความถูกต้องด้วยลายนิ้วมือหรือ PIN แล้ว คุณยังสามารถตั้งค่าการตรวจสอบสิทธิ์ผ่านคีย์ความปลอดภัยของฮาร์ดแวร์ที่รองรับ FIDO2 เพื่อเพิ่มความปลอดภัยให้กับบัญชีของคุณ"
chromePasskeyNotSupported: "ขณะนี้ยังไม่รองรับรหัสผ่านของ Chrome"
registerSecurityKey: "ลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน"
securityKeyName: "ป้อนชื่อคีย์"
tapSecurityKey: "กรุณาทำตามเบราว์เซอร์ของคุณเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน"
removeKey: "ลบคีย์ความปลอดภัยออก"
removeKeyConfirm: "ลบข้อมูลสำรอง {name} มั้ย?" removeKeyConfirm: "ลบข้อมูลสำรอง {name} มั้ย?"
whyTOTPOnlyRenew: "ไม่สามารถลบแอปตัวรับรองความถูกต้องได้ตราบใดที่มีการลงทะเบียนคีย์ความปลอดภัยไว้แล้ว"
renewTOTP: "กำหนดค่าแอพตัวตรวจสอบสิทธิ์ใหม่"
renewTOTPConfirm: "วิธีการแบบนี้จะทําให้รหัสยืนยันจากแอพก่อนหน้าของคุณหยุดทํางานเลยนะ"
renewTOTPOk: "ตั้งค่าคอนฟิกใหม่"
renewTOTPCancel: "ไม่เป็นไร" renewTOTPCancel: "ไม่เป็นไร"
_permissions: _permissions:
"read:account": "ดูข้อมูลบัญชีของคุณ" "read:account": "ดูข้อมูลบัญชีของคุณ"
@ -1615,6 +1651,8 @@ _visibility:
followersDescription: "ทำให้ผู้ติดตามนั้นมองเห็นแค่คุณเท่านั้น" followersDescription: "ทำให้ผู้ติดตามนั้นมองเห็นแค่คุณเท่านั้น"
specified: "ไดเร็ค" specified: "ไดเร็ค"
specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น" specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น"
disableFederation: "ไม่มีสหภาพ"
disableFederationDescription: "อย่าส่งไปยังอินสแตนซ์อื่น"
_postForm: _postForm:
replyPlaceholder: "ตอบกลับโน้ตนี้..." replyPlaceholder: "ตอบกลับโน้ตนี้..."
quotePlaceholder: "อ้างโน้ตนี้..." quotePlaceholder: "อ้างโน้ตนี้..."
@ -1770,6 +1808,7 @@ _notification:
pollEnded: "โพลนี้สิ้นสุดลงแล้ว" pollEnded: "โพลนี้สิ้นสุดลงแล้ว"
receiveFollowRequest: "ได้รับคำขอติดตาม\n" receiveFollowRequest: "ได้รับคำขอติดตาม\n"
followRequestAccepted: "ยอมรับคำขอติดตาม" followRequestAccepted: "ยอมรับคำขอติดตาม"
achievementEarned: "ปลดล็อกความสำเร็จแล้ว"
app: "การแจ้งเตือนจากแอปที่มีลิงก์" app: "การแจ้งเตือนจากแอปที่มีลิงก์"
_actions: _actions:
followBack: "ติดตามกลับด้วย" followBack: "ติดตามกลับด้วย"
@ -1802,3 +1841,6 @@ _deck:
channel: "แชนแนล" channel: "แชนแนล"
mentions: "พูดถึง" mentions: "พูดถึง"
direct: "ไดเร็ค" direct: "ไดเร็ค"
_dialog:
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"

View file

@ -49,6 +49,7 @@ deleteAndEdit: "Видалити й редагувати"
deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї." deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї."
addToList: "Додати до списку" addToList: "Додати до списку"
sendMessage: "Надіслати повідомлення" sendMessage: "Надіслати повідомлення"
copyRSS: "Скопіювати RSS"
copyUsername: "Скопіювати ім’я користувача" copyUsername: "Скопіювати ім’я користувача"
searchUser: "Пошук користувачів" searchUser: "Пошук користувачів"
reply: "Відповісти" reply: "Відповісти"
@ -128,6 +129,7 @@ unblockConfirm: "Ви впевнені, що хочете розблокуват
suspendConfirm: "Ви впевнені, що хочете призупинити цей акаунт?" suspendConfirm: "Ви впевнені, що хочете призупинити цей акаунт?"
unsuspendConfirm: "Ви впевнені, що хочете відновити цей акаунт?" unsuspendConfirm: "Ви впевнені, що хочете відновити цей акаунт?"
selectList: "Виберіть список" selectList: "Виберіть список"
selectChannel: "Виберіть канал"
selectAntenna: "Виберіть антену" selectAntenna: "Виберіть антену"
selectWidget: "Виберіть віджет" selectWidget: "Виберіть віджет"
editWidgets: "Редагувати віджети" editWidgets: "Редагувати віджети"
@ -255,6 +257,7 @@ noMoreHistory: "Подальшої історії немає"
startMessaging: "Розпочати діалог" startMessaging: "Розпочати діалог"
nUsersRead: "Прочитали {n}" nUsersRead: "Прочитали {n}"
agreeTo: "Я погоджуюсь з {0}" agreeTo: "Я погоджуюсь з {0}"
agreeBelow: "Я погоджуюся з наведеним нижче"
tos: "Умови використання" tos: "Умови використання"
start: "Розпочати" start: "Розпочати"
home: "Домівка" home: "Домівка"
@ -387,6 +390,8 @@ about: "Інформація"
aboutMisskey: "Про Misskey" aboutMisskey: "Про Misskey"
administrator: "Адмін" administrator: "Адмін"
token: "Токен" token: "Токен"
2fa: "Двофакторна аутентифікація"
totp: "Програма аутентифікації"
moderator: "Модератор" moderator: "Модератор"
moderation: "Модерація" moderation: "Модерація"
nUsersMentioned: "Згадали: {n}" nUsersMentioned: "Згадали: {n}"
@ -445,6 +450,8 @@ aboutX: "Про {x}"
disableDrawer: "Не використовувати висувні меню" disableDrawer: "Не використовувати висувні меню"
noHistory: "Історія порожня" noHistory: "Історія порожня"
signinHistory: "Історія входів" signinHistory: "Історія входів"
enableAdvancedMfm: "Увімкнути розширений MFM"
enableAnimatedMfm: "Увімкнути анімований MFM"
doing: "Виконується" doing: "Виконується"
category: "Категорія" category: "Категорія"
tags: "Теги" tags: "Теги"
@ -697,6 +704,7 @@ accentColor: "Акцент"
textColor: "Текст" textColor: "Текст"
saveAs: "Зберегти як…" saveAs: "Зберегти як…"
advanced: "Розширені" advanced: "Розширені"
advancedSettings: "Розширені налаштування"
value: "Значення" value: "Значення"
createdAt: "Створено" createdAt: "Створено"
updatedAt: "Останнє оновлення" updatedAt: "Останнє оновлення"
@ -761,6 +769,7 @@ popularPosts: "Популярні дописи"
shareWithNote: "Поділитися нотаткою" shareWithNote: "Поділитися нотаткою"
ads: "Реклама" ads: "Реклама"
expiration: "Опитування закінчується" expiration: "Опитування закінчується"
startingperiod: "Початковий період"
memo: "Примітка" memo: "Примітка"
priority: "Пріоритет" priority: "Пріоритет"
high: "Високий" high: "Високий"
@ -822,6 +831,7 @@ searchByGoogle: "Пошук"
instanceDefaultLightTheme: "Світла тема за промовчанням" instanceDefaultLightTheme: "Світла тема за промовчанням"
instanceDefaultDarkTheme: "Темна тема за промовчанням" instanceDefaultDarkTheme: "Темна тема за промовчанням"
mutePeriod: "Тривалість приховування" mutePeriod: "Тривалість приховування"
period: "Опитування закінчується"
indefinitely: "Ніколи" indefinitely: "Ніколи"
tenMinutes: "10 хвилин" tenMinutes: "10 хвилин"
oneHour: "1 година" oneHour: "1 година"
@ -879,8 +889,17 @@ like: "Вподобати"
unlike: "Не вподобати" unlike: "Не вподобати"
numberOfLikes: "Вподобання" numberOfLikes: "Вподобання"
show: "Відображення" show: "Відображення"
roles: "Ролі"
role: "Роль"
normalUser: "Звичайний користувач"
undefined: "Не визначено"
assign: "Призначити"
unassign: "Скасувати призначення"
color: "Колір" color: "Колір"
achievements: "Досягнення" achievements: "Досягнення"
joinThisServer: "Зареєструватися на цьому сервері"
exploreOtherServers: "Знайти інший сервер"
letsLookAtTimeline: "Перегляд історії"
_achievements: _achievements:
earnedAt: "Відкрито" earnedAt: "Відкрито"
_types: _types:
@ -1102,6 +1121,13 @@ _achievements:
description: "Відправити посилання на \"Brain Diver\"" description: "Відправити посилання на \"Brain Diver\""
flavor: "Misskey-Misskey La-Tu-Ma" flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "Нова роль"
edit: "Змінити роль"
name: "Назва ролі"
description: "Опис ролі"
permission: "Права ролі"
assignTarget: "Призначити"
manual: "Вручну"
priority: "Пріоритет" priority: "Пріоритет"
_priority: _priority:
low: "Низький" low: "Низький"
@ -1299,6 +1325,7 @@ _ago:
weeksAgo: "{n} тиж. тому" weeksAgo: "{n} тиж. тому"
monthsAgo: "{n} міс. тому" monthsAgo: "{n} міс. тому"
yearsAgo: "{n} р. тому" yearsAgo: "{n} р. тому"
invalid: "Тут нічого немає"
_time: _time:
second: "с" second: "с"
minute: "х" minute: "х"

View file

@ -2,6 +2,7 @@
_lang_: "Tiếng Việt" _lang_: "Tiếng Việt"
headlineMisskey: "Mạng xã hội liên hợp" headlineMisskey: "Mạng xã hội liên hợp"
introMisskey: "Xin chào! Misskey là một nền tảng tiểu blog phi tập trung mã nguồn mở.\nViết \"tút\" để chia sẻ những suy nghĩ của bạn 📡\nBằng \"biểu cảm\", bạn có thể bày tỏ nhanh chóng cảm xúc của bạn với các tút 👍\nHãy khám phá một thế giới mới! 🚀" introMisskey: "Xin chào! Misskey là một nền tảng tiểu blog phi tập trung mã nguồn mở.\nViết \"tút\" để chia sẻ những suy nghĩ của bạn 📡\nBằng \"biểu cảm\", bạn có thể bày tỏ nhanh chóng cảm xúc của bạn với các tút 👍\nHãy khám phá một thế giới mới! 🚀"
poweredByMisskeyDescription: "{name} là một trong những chủ máy của <b>Misskey</b> là nền tảng mã nguồn mở"
monthAndDay: "{day} tháng {month}" monthAndDay: "{day} tháng {month}"
search: "Tìm kiếm" search: "Tìm kiếm"
notifications: "Thông báo" notifications: "Thông báo"
@ -10,12 +11,13 @@ password: "Mật khẩu"
forgotPassword: "Quên mật khẩu" forgotPassword: "Quên mật khẩu"
fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..." fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..."
ok: "Đồng ý" ok: "Đồng ý"
gotIt: "Đã hiểu!" gotIt: "Hiểu rồi!"
cancel: "Hủy" cancel: "Hủy"
noThankYou: "Không, cảm ơn"
enterUsername: "Nhập tên người dùng" enterUsername: "Nhập tên người dùng"
renotedBy: "Chia sẻ bởi {user}" renotedBy: "Chia sẻ bởi {user}"
noNotes: "Chưa có t nào." noNotes: "Chưa có bài viết nào."
noNotifications: "Không có thông báo" noNotifications: "Chưa có thông báo"
instance: "Máy chủ" instance: "Máy chủ"
settings: "Cài đặt" settings: "Cài đặt"
basicSettings: "Thiết lập chung" basicSettings: "Thiết lập chung"
@ -47,6 +49,7 @@ deleteAndEdit: "Sửa"
deleteAndEditConfirm: "Bạn có chắc muốn sửa tút này? Những biểu cảm, lượt trả lời và đăng lại sẽ bị mất." deleteAndEditConfirm: "Bạn có chắc muốn sửa tút này? Những biểu cảm, lượt trả lời và đăng lại sẽ bị mất."
addToList: "Thêm vào danh sách" addToList: "Thêm vào danh sách"
sendMessage: "Gửi tin nhắn" sendMessage: "Gửi tin nhắn"
copyRSS: "Sao chép RSS"
copyUsername: "Chép tên người dùng" copyUsername: "Chép tên người dùng"
searchUser: "Tìm kiếm người dùng" searchUser: "Tìm kiếm người dùng"
reply: "Trả lời" reply: "Trả lời"
@ -65,13 +68,13 @@ export: "Xuất dữ liệu"
files: "Tập tin" files: "Tập tin"
download: "Tải xuống" download: "Tải xuống"
driveFileDeleteConfirm: "Bạn có chắc muốn xóa tập tin \"{name}\"? Tút liên quan cũng sẽ bị xóa theo." driveFileDeleteConfirm: "Bạn có chắc muốn xóa tập tin \"{name}\"? Tút liên quan cũng sẽ bị xóa theo."
unfollowConfirm: "Bạn có chắc muốn ngưng theo dõi {name}?" unfollowConfirm: "Bạn ngừng theo dõi {name}?"
exportRequested: "Đang chuẩn bị xuất tập tin. Quá trình này có thể mất ít phút. Nó sẽ được tự động thêm vào Drive sau khi hoàn thành." exportRequested: "Đang chuẩn bị xuất tập tin. Quá trình này có thể mất ít phút. Nó sẽ được tự động thêm vào Drive sau khi hoàn thành."
importRequested: "Bạn vừa yêu cầu nhập dữ liệu. Quá trình này có thể mất ít phút." importRequested: "Bạn vừa yêu cầu nhập dữ liệu. Quá trình này có thể mất ít phút."
lists: "Danh sách" lists: "Danh sách"
noLists: "Bạn chưa có danh sách nào" noLists: "Bạn chưa có danh sách nào"
note: "t" note: "Bài viết"
notes: "t" notes: "Bài Viết"
following: "Đang theo dõi" following: "Đang theo dõi"
followers: "Người theo dõi" followers: "Người theo dõi"
followsYou: "Theo dõi bạn" followsYou: "Theo dõi bạn"
@ -88,7 +91,7 @@ enterListName: "Đặt tên cho danh sách"
privacy: "Bảo mật" privacy: "Bảo mật"
makeFollowManuallyApprove: "Yêu cầu theo dõi cần được duyệt" makeFollowManuallyApprove: "Yêu cầu theo dõi cần được duyệt"
defaultNoteVisibility: "Kiểu tút mặc định" defaultNoteVisibility: "Kiểu tút mặc định"
follow: "Đang theo dõi" follow: "Theo dõi"
followRequest: "Gửi yêu cầu theo dõi" followRequest: "Gửi yêu cầu theo dõi"
followRequests: "Yêu cầu theo dõi" followRequests: "Yêu cầu theo dõi"
unfollow: "Ngưng theo dõi" unfollow: "Ngưng theo dõi"
@ -100,7 +103,9 @@ renoted: "Đã đăng lại."
cantRenote: "Không thể đăng lại tút này." cantRenote: "Không thể đăng lại tút này."
cantReRenote: "Không thể đăng lại một tút đăng lại." cantReRenote: "Không thể đăng lại một tút đăng lại."
quote: "Trích dẫn" quote: "Trích dẫn"
pinnedNote: "Tút ghim" inChannelRenote: "Chia sẻ trong kênh này"
inChannelQuote: "Trích dẫn trong kênh này"
pinnedNote: "Bài viết đã ghim"
pinned: "Ghim" pinned: "Ghim"
you: "Bạn" you: "Bạn"
clickToShow: "Nhấn để xem" clickToShow: "Nhấn để xem"
@ -126,6 +131,7 @@ unblockConfirm: "Bạn có chắc muốn bỏ chặn người này?"
suspendConfirm: "Bạn có chắc muốn vô hiệu hóa người này?" suspendConfirm: "Bạn có chắc muốn vô hiệu hóa người này?"
unsuspendConfirm: "Bạn có chắc muốn bỏ vô hiệu hóa người này?" unsuspendConfirm: "Bạn có chắc muốn bỏ vô hiệu hóa người này?"
selectList: "Chọn danh sách" selectList: "Chọn danh sách"
selectChannel: "Lựa chọn kênh"
selectAntenna: "Chọn một antenna" selectAntenna: "Chọn một antenna"
selectWidget: "Chọn tiện ích" selectWidget: "Chọn tiện ích"
editWidgets: "Sửa tiện ích" editWidgets: "Sửa tiện ích"
@ -141,8 +147,8 @@ cacheRemoteFiles: "Tập tin cache từ xa"
cacheRemoteFilesDescription: "Khi tùy chọn này bị tắt, các tập tin từ xa sẽ được tải trực tiếp từ máy chủ khác. Điều này sẽ giúp giảm dung lượng lưu trữ nhưng lại tăng lưu lượng truy cập, vì hình thu nhỏ sẽ không được tạo." cacheRemoteFilesDescription: "Khi tùy chọn này bị tắt, các tập tin từ xa sẽ được tải trực tiếp từ máy chủ khác. Điều này sẽ giúp giảm dung lượng lưu trữ nhưng lại tăng lưu lượng truy cập, vì hình thu nhỏ sẽ không được tạo."
flagAsBot: "Đánh dấu đây là tài khoản bot" flagAsBot: "Đánh dấu đây là tài khoản bot"
flagAsBotDescription: "Bật tùy chọn này nếu tài khoản này được kiểm soát bởi một chương trình. Nếu được bật, nó sẽ được đánh dấu để các nhà phát triển khác ngăn chặn chuỗi tương tác vô tận với các bot khác và điều chỉnh hệ thống nội bộ của Misskey để coi tài khoản này như một bot." flagAsBotDescription: "Bật tùy chọn này nếu tài khoản này được kiểm soát bởi một chương trình. Nếu được bật, nó sẽ được đánh dấu để các nhà phát triển khác ngăn chặn chuỗi tương tác vô tận với các bot khác và điều chỉnh hệ thống nội bộ của Misskey để coi tài khoản này như một bot."
flagAsCat: "Tài khoản này là mèo" flagAsCat: "Chế độ Mèeeeeeeeeeo!!"
flagAsCatDescription: "Bật tùy chọn này để đánh dấu tài khoản là một con mèo." flagAsCatDescription: "Nếu mà em là một con mèo thì cứ bật nó kiu mèo mèo mèeeeeeeo!! "
flagShowTimelineReplies: "Hiện lượt trả lời trong bảng tin" flagShowTimelineReplies: "Hiện lượt trả lời trong bảng tin"
flagShowTimelineRepliesDescription: "Hiện lượt trả lời của người bạn theo dõi trên tút của những người khác." flagShowTimelineRepliesDescription: "Hiện lượt trả lời của người bạn theo dõi trên tút của những người khác."
autoAcceptFollowed: "Tự động phê duyệt theo dõi từ những người mà bạn đang theo dõi" autoAcceptFollowed: "Tự động phê duyệt theo dõi từ những người mà bạn đang theo dõi"
@ -155,7 +161,7 @@ setWallpaper: "Đặt ảnh bìa"
removeWallpaper: "Xóa ảnh bìa" removeWallpaper: "Xóa ảnh bìa"
searchWith: "Tìm kiếm: {q}" searchWith: "Tìm kiếm: {q}"
youHaveNoLists: "Bạn chưa có danh sách nào" youHaveNoLists: "Bạn chưa có danh sách nào"
followConfirm: "Bạn có chắc muốn theo dõi {name}" followConfirm: "Bạn theo dõi {name}"
proxyAccount: "Tài khoản proxy" proxyAccount: "Tài khoản proxy"
proxyAccountDescription: "Tài khoản proxy là tài khoản hoạt động như một người theo dõi từ xa cho người dùng trong những điều kiện nhất định. Ví dụ: khi người dùng thêm người dùng từ xa vào danh sách, hoạt động của người dùng từ xa sẽ không được chuyển đến phiên bản nếu không có người dùng cục bộ nào theo dõi người dùng đó, vì vậy tài khoản proxy sẽ theo dõi." proxyAccountDescription: "Tài khoản proxy là tài khoản hoạt động như một người theo dõi từ xa cho người dùng trong những điều kiện nhất định. Ví dụ: khi người dùng thêm người dùng từ xa vào danh sách, hoạt động của người dùng từ xa sẽ không được chuyển đến phiên bản nếu không có người dùng cục bộ nào theo dõi người dùng đó, vì vậy tài khoản proxy sẽ theo dõi."
host: "Host" host: "Host"
@ -198,7 +204,7 @@ blockedUsers: "Người đã chặn"
noUsers: "Chưa có ai" noUsers: "Chưa có ai"
editProfile: "Sửa hồ sơ" editProfile: "Sửa hồ sơ"
noteDeleteConfirm: "Bạn có chắc muốn xóa tút này?" noteDeleteConfirm: "Bạn có chắc muốn xóa tút này?"
pinLimitExceeded: "Bạn đã đạt giới hạn số lượng tút có thể ghim" pinLimitExceeded: "Bạn không thể ghim bài viết nữa"
intro: "Đã cài đặt Misskey! Xin hãy tạo tài khoản admin." intro: "Đã cài đặt Misskey! Xin hãy tạo tài khoản admin."
done: "Xong" done: "Xong"
processing: "Đang xử lý" processing: "Đang xử lý"
@ -253,6 +259,8 @@ noMoreHistory: "Không còn gì để đọc"
startMessaging: "Bắt đầu trò chuyện" startMessaging: "Bắt đầu trò chuyện"
nUsersRead: "đọc bởi {n}" nUsersRead: "đọc bởi {n}"
agreeTo: "Tôi đồng ý {0}" agreeTo: "Tôi đồng ý {0}"
agreeBelow: "Đồng ý với nội dung dưới đây"
basicNotesBeforeCreateAccount: "Những điều cơ bản cần chú ý "
tos: "Điều khoản dịch vụ" tos: "Điều khoản dịch vụ"
start: "Bắt đầu" start: "Bắt đầu"
home: "Trang chính" home: "Trang chính"
@ -339,7 +347,7 @@ pinnedUsersDescription: "Liệt kê mỗi hàng một tên người dùng xuốn
pinnedPages: "Trang đã ghim" pinnedPages: "Trang đã ghim"
pinnedPagesDescription: "Liệt kê các trang thú vị để ghim trên máy chủ." pinnedPagesDescription: "Liệt kê các trang thú vị để ghim trên máy chủ."
pinnedClipId: "ID của clip muốn ghim" pinnedClipId: "ID của clip muốn ghim"
pinnedNotes: "Tút ghim" pinnedNotes: "Bài viết đã ghim"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Bật hCaptcha" enableHcaptcha: "Bật hCaptcha"
hcaptchaSiteKey: "Khóa của trang" hcaptchaSiteKey: "Khóa của trang"
@ -348,6 +356,8 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "Bật reCAPTCHA" enableRecaptcha: "Bật reCAPTCHA"
recaptchaSiteKey: "Khóa của trang" recaptchaSiteKey: "Khóa của trang"
recaptchaSecretKey: "Khóa bí mật" recaptchaSecretKey: "Khóa bí mật"
turnstile: "Turnstile"
enableTurnstile: "Áp dụng Turnstile"
turnstileSiteKey: "Khóa của trang" turnstileSiteKey: "Khóa của trang"
turnstileSecretKey: "Khóa bí mật" turnstileSecretKey: "Khóa bí mật"
avoidMultiCaptchaConfirm: "Dùng nhiều hệ thống Captcha có thể gây nhiễu giữa chúng. Bạn có muốn tắt các hệ thống Captcha khác hiện đang hoạt động không? Nếu bạn muốn chúng tiếp tục được bật, hãy nhấn hủy." avoidMultiCaptchaConfirm: "Dùng nhiều hệ thống Captcha có thể gây nhiễu giữa chúng. Bạn có muốn tắt các hệ thống Captcha khác hiện đang hoạt động không? Nếu bạn muốn chúng tiếp tục được bật, hãy nhấn hủy."
@ -383,13 +393,19 @@ about: "Giới thiệu"
aboutMisskey: "Về Misskey" aboutMisskey: "Về Misskey"
administrator: "Quản trị viên" administrator: "Quản trị viên"
token: "Token" token: "Token"
2fa: "Xác thực 2 yếu tố"
totp: "Ứng dụng xác thực"
totpDescription: "Nhắn mã OTP bằng ứng dụng xác thực"
moderator: "Kiểm duyệt viên" moderator: "Kiểm duyệt viên"
moderation: "Kiểm duyệt" moderation: "Kiểm duyệt"
nUsersMentioned: "Dùng bởi {n} người" nUsersMentioned: "Dùng bởi {n} người"
securityKeyAndPasskey: "Mã bảo mật・Passkey"
securityKey: "Khóa bảo mật" securityKey: "Khóa bảo mật"
lastUsed: "Dùng lần cuối" lastUsed: "Dùng lần cuối"
lastUsedAt: "Lần cuối sử dụng: {t}"
unregister: "Hủy đăng ký" unregister: "Hủy đăng ký"
passwordLessLogin: "Đăng nhập không mật khẩu" passwordLessLogin: "Đăng nhập không mật khẩu"
passwordLessLoginDescription: "Đăng nhập bằng chỉ mã bảo mật hoặc passkey, không sử dụng mật khẩu."
resetPassword: "Đặt lại mật khẩu" resetPassword: "Đặt lại mật khẩu"
newPasswordIs: "Mật khẩu mới là \"{password}\"" newPasswordIs: "Mật khẩu mới là \"{password}\""
reduceUiAnimation: "Giảm chuyển động UI" reduceUiAnimation: "Giảm chuyển động UI"
@ -423,7 +439,7 @@ invitations: "Mời"
invitationCode: "Mã mời" invitationCode: "Mã mời"
checking: "Đang kiểm tra..." checking: "Đang kiểm tra..."
available: "Khả dụng" available: "Khả dụng"
unavailable: "Không khả dụng" unavailable: "Không sử dụng được"
usernameInvalidFormat: "Bạn có thể dùng viết hoa/viết thường, chữ số, và dấu gạch dưới." usernameInvalidFormat: "Bạn có thể dùng viết hoa/viết thường, chữ số, và dấu gạch dưới."
tooShort: "Quá ngắn" tooShort: "Quá ngắn"
tooLong: "Quá dài" tooLong: "Quá dài"
@ -438,9 +454,13 @@ or: "Hoặc"
language: "Ngôn ngữ" language: "Ngôn ngữ"
uiLanguage: "Ngôn ngữ giao diện" uiLanguage: "Ngôn ngữ giao diện"
aboutX: "Giới thiệu {x}" aboutX: "Giới thiệu {x}"
emojiStyle: "Kiểu cách Emoji"
native: "Bản xứ"
disableDrawer: "Không dùng menu thanh bên" disableDrawer: "Không dùng menu thanh bên"
noHistory: "Không có dữ liệu" noHistory: "Không có dữ liệu"
signinHistory: "Lịch sử đăng nhập" signinHistory: "Lịch sử đăng nhập"
enableAdvancedMfm: "Xem bài MFM chất lượng cao."
enableAnimatedMfm: "Xem bài MFM có chuyển động"
doing: "Đang xử lý..." doing: "Đang xử lý..."
category: "Phân loại" category: "Phân loại"
tags: "Thẻ" tags: "Thẻ"
@ -628,7 +648,7 @@ random: "Ngẫu nhiên"
system: "Hệ thống" system: "Hệ thống"
switchUi: "Chuyển đổi giao diện người dùng" switchUi: "Chuyển đổi giao diện người dùng"
desktop: "Desktop" desktop: "Desktop"
clip: "Ghim" clip: "Lưu bài viết"
createNew: "Tạo mới" createNew: "Tạo mới"
optional: "Không bắt buộc" optional: "Không bắt buộc"
createNewClip: "Tạo một ghim mới" createNewClip: "Tạo một ghim mới"
@ -667,7 +687,7 @@ pageLikesCount: "Số lượng trang đã thích"
pageLikedCount: "Số lượng thích trang đã nhận" pageLikedCount: "Số lượng thích trang đã nhận"
contact: "Liên hệ" contact: "Liên hệ"
useSystemFont: "Dùng phông chữ mặc định của hệ thống" useSystemFont: "Dùng phông chữ mặc định của hệ thống"
clips: "Ghim" clips: "Lưu bài viết"
experimentalFeatures: "Tính năng thử nghiệm" experimentalFeatures: "Tính năng thử nghiệm"
developer: "Nhà phát triển" developer: "Nhà phát triển"
makeExplorable: "Không hiện tôi trong \"Khám phá\"" makeExplorable: "Không hiện tôi trong \"Khám phá\""
@ -693,6 +713,7 @@ accentColor: "Màu phụ"
textColor: "Màu chữ" textColor: "Màu chữ"
saveAs: "Lưu thành" saveAs: "Lưu thành"
advanced: "Nâng cao" advanced: "Nâng cao"
advancedSettings: "Cài đặt nâng cao"
value: "Giá trị" value: "Giá trị"
createdAt: "Ngày tạo" createdAt: "Ngày tạo"
updatedAt: "Cập nhật lúc" updatedAt: "Cập nhật lúc"
@ -758,6 +779,7 @@ popularPosts: "Tút được xem nhiều nhất"
shareWithNote: "Chia sẻ kèm với tút" shareWithNote: "Chia sẻ kèm với tút"
ads: "Quảng cáo" ads: "Quảng cáo"
expiration: "Thời hạn" expiration: "Thời hạn"
startingperiod: "Thời gian bắt đầu\n"
memo: "Lưu ý" memo: "Lưu ý"
priority: "Ưu tiên" priority: "Ưu tiên"
high: "Cao" high: "Cao"
@ -790,6 +812,7 @@ lastCommunication: "Lần giao tiếp cuối"
resolved: "Đã xử lý" resolved: "Đã xử lý"
unresolved: "Chờ xử lý" unresolved: "Chờ xử lý"
breakFollow: "Xóa người theo dõi" breakFollow: "Xóa người theo dõi"
breakFollowConfirm: "Bạn bỏ theo dõi tài khoản này không?"
itsOn: "Đã bật" itsOn: "Đã bật"
itsOff: "Đã tắt" itsOff: "Đã tắt"
emailRequiredForSignup: "Yêu cầu địa chỉ email khi đăng ký" emailRequiredForSignup: "Yêu cầu địa chỉ email khi đăng ký"
@ -824,16 +847,19 @@ instanceDefaultLightTheme: "Theme máy chủ Sáng-Rộng"
instanceDefaultDarkTheme: "Theme máy chủ Tối-Rộng" instanceDefaultDarkTheme: "Theme máy chủ Tối-Rộng"
instanceDefaultThemeDescription: "Nhập mã theme trong định dạng đối tượng." instanceDefaultThemeDescription: "Nhập mã theme trong định dạng đối tượng."
mutePeriod: "Thời hạn ẩn" mutePeriod: "Thời hạn ẩn"
period: "Thời hạn"
indefinitely: "Vĩnh viễn" indefinitely: "Vĩnh viễn"
tenMinutes: "10 phút" tenMinutes: "10 phút"
oneHour: "1 giờ" oneHour: "1 giờ"
oneDay: "1 ngày" oneDay: "1 ngày"
oneWeek: "1 tuần" oneWeek: "1 tuần"
oneMonth: "1 tháng"
reflectMayTakeTime: "Có thể mất một thời gian để điều này được áp dụng." reflectMayTakeTime: "Có thể mất một thời gian để điều này được áp dụng."
failedToFetchAccountInformation: "Không thể lấy thông tin tài khoản" failedToFetchAccountInformation: "Không thể lấy thông tin tài khoản"
rateLimitExceeded: "Giới hạn quá mức" rateLimitExceeded: "Giới hạn quá mức"
cropImage: "Cắt hình ảnh" cropImage: "Cắt hình ảnh"
cropImageAsk: "Bạn có muốn cắt ảnh này?" cropImageAsk: "Bạn có muốn cắt ảnh này?"
cropNo: "Để nguyên"
file: "Tập tin" file: "Tập tin"
recentNHours: "{n}h trước" recentNHours: "{n}h trước"
recentNDays: "{n} ngày trước" recentNDays: "{n} ngày trước"
@ -876,15 +902,231 @@ navbar: "Thanh điều hướng"
shuffle: "Xáo trộn" shuffle: "Xáo trộn"
account: "Tài khoản của bạn" account: "Tài khoản của bạn"
move: "Di chuyển" move: "Di chuyển"
pushNotification: "Thông báo đẩy"
subscribePushNotification: "Bật thông báo đẩy"
unsubscribePushNotification: "Tắt thông báo đẩy"
pushNotificationAlreadySubscribed: "Đang bật thông báo đẩy"
sendPushNotificationReadMessage: "Xóa thông báo đẩy sau khi đọc thông báo hay tin nhắn"
sendPushNotificationReadMessageCaption: "Thông báo như {emptyPushNotificationMessage} sẽ hiển thị trong giây phút. Tiêu tốn pin của máy bạn có thể tăng lên hơn nữa."
windowMaximize: "Phóng to"
windowRestore: "Khôi phục"
caption: "Mô tả"
loggedInAsBot: "Đang đăng nhập bằng tài khoản Bot"
tools: "Công Cụ"
cannotLoad: "Không tải được"
numberOfProfileView: "Số lần mở hồ sơ"
like: "Thích" like: "Thích"
unlike: "Bỏ lượt thích"
numberOfLikes: "Lượt thích"
show: "Hiển thị" show: "Hiển thị"
neverShow: "Không hiển thị nữa"
remindMeLater: "Để sau"
didYouLikeMisskey: "Bạn có ưa thích Mískey không?"
pleaseDonate: "Misskey là phần mềm miễn phí mà {host} đang sử dụng. Xin mong bạn quyên góp cho chúng tôi để chúng tôi có thể tiếp tục phát triển dịch vụ này. Xin cảm ơn!!"
roles: "Vai trò"
role: "Vai trò"
normalUser: "Người dùng bình thường"
undefined: "Chưa xác định"
color: "Màu sắc" color: "Màu sắc"
manageCustomEmojis: "Quản lý CustomEmoji"
cannotPerformTemporary: "Tạm thời không sử dụng được"
cannotPerformTemporaryDescription: "Tạm thời không sử dụng được vì lần số điều kiện quá giới hạn. Thử lại sau mọt lát nữa."
achievements: "Thành tích"
gotInvalidResponseError: "Không nhận được trả lời chủ máy"
gotInvalidResponseErrorDescription: "Chủ máy có lẻ ngừng hoạt động hoặc bảo trí. Thử lại sau một lát nữa. "
thisPostMayBeAnnoying: "Bạn đăng bài này có thể làm phiền cho người ta."
thisPostMayBeAnnoyingHome: "Đăng trên trang chính"
thisPostMayBeAnnoyingCancel: "Từ chối"
thisPostMayBeAnnoyingIgnore: "Đăng bài để nguyên"
collapseRenotes: "Không hiển thị bài viết đã từng xem"
internalServerError: "Lỗi trong chủ máy"
internalServerErrorDescription: "Trong chủ máy lỗi bất ngờ xảy ra"
copyErrorInfo: "Sao chép thông tin lỗi"
joinThisServer: "Đăng ký trên chủ máy này"
exploreOtherServers: "Tìm chủ máy khác"
letsLookAtTimeline: "Thử xem Timeline"
_achievements:
earnedAt: "Ngày thu nhận"
_types:
_notes1:
title: "just setting up my msky"
description: "Lần đầu tiên đăng bài"
flavor: "Chúc bạn trên Miskey vui vẻ nha!!"
_notes10:
title: "Một số bài viết"
description: "Đăng bài 10 lần"
_notes100:
title: "Rất nhiều bài biết"
description: "Đăng bài 100 lần"
_notes500:
title: "Như đầy bài viết"
description: "Đăng bài 500 lần"
_notes1000:
title: "Ngọn núi bài viết"
description: "Đăng bài 1000 lần"
_notes5000:
title: "Bài viết chảy như suối"
description: "Đăng bài 5000 lần"
_notes10000:
title: "Bài Viết siu nhìu"
description: "Đăng bài 10000 lần"
_notes20000:
title: "Need more note"
description: "Đã đăng bài 20,000 lần rồi"
_notes30000:
title: "ĐĂNG VỚI BÀI"
description: "Đã đăng bài 30,000 lần rồi"
_notes40000:
title: "Nhà xưởng dăng bài"
description: "Đã đăng bài 40,000 lần rồi"
_notes50000:
title: "Hàng tinh đăng bài"
description: "Đã đăng bài 50,000 lần rồi"
_notes100000:
flavor: "Liệu viết bài gì tầm này vậy? "
_login3:
title: "Sơ cấp I"
description: "Tổng số ngày đăng nhập đạt 3 ngày"
flavor: "Từ nay các bạn cứ xem như mình là một Misskist đó"
_login7:
title: "Sơ cấp II"
description: "Tổng số ngày đăng nhập đạt 7 ngày"
flavor: "Bạn dần quen chưa? "
_login15:
title: "Sơ cấp III"
description: "Tổng số ngày đăng nhập đạt 7 ngày"
_login30:
title: "Misskist cấp I"
description: "Tổng số ngày đăng nhập đạt 30 ngày"
_login60:
title: "Misskist cấp II"
description: "Tổng số ngày đăng nhập đạt 60 ngày"
_login100:
title: "Misskist cấp III"
description: "Tổng số ngày đăng nhập đạt 100 ngày"
flavor: "Người dùng này, chính vì đó là một Misskist"
_login200:
title: "Khách hàng thường xuyên cấp I"
description: "Tổng số ngày đăng nhập đạt 200 ngày"
_login300:
title: "Khách hàng thường xuyên cấp II"
description: "Tổng số ngày đăng nhập đạt 300 ngày"
_login400:
title: "Khách hàng thường xuyên cấp III"
description: "Tổng số ngày đăng nhập đạt 400 ngày"
_markedAsCat:
title: "Tôi là một con mèo"
description: "Bật chế độ mèo"
flavor: "Mà tên chưa có"
_following1:
title: "Theo dõi đầu tiên"
description: "Lần đầu tiên theo dõi "
_following10:
title: "Cứ theo dõi và theo dõi"
description: "Vừa theo dõi hơn 10 người"
_following50:
title: "Bạn bè nhiều quá"
description: "Vừa theo dõi hơn 50 người"
_following100:
title: "Trăm bạn bè"
description: "Vừa theo dõi vượt lên 100 người"
_following300:
title: "Quá nhiều bạn bè"
description: "Vừa theo dõi vượt lên 300 người"
_followers1:
title: "Ai đầu tiên theo dõi bạn"
description: "Lần đầu tiên được theo dõi"
_followers10:
title: "FOLLOW ME!!"
description: "Người theo dõi bạn vượt lên 10 người"
_followers500:
title: "Trạm phát sóng"
_followers1000:
title: "Người có tầm ảnh hưởng"
description: "Người theo dõi bạn vượt lên 1000 người"
_collectAchievements30:
title: "Người sưu tập thành tích"
description: "Vừa lấy thành tích hơn 30 cái"
_viewAchievements3min:
title: "Yêu Thành tích"
description: "Ngắm danh sách thành tích đến tận hơn 3 phút"
_iLoveMisskey:
title: "Tôi Yêu Misskey"
description: "Đăng lời nói \"I ❤ #Misskey\""
flavor: "Xin chân thành cảm ơn bạn đã sử dụng Misskey!! by Đội ngũ phát triển"
_foundTreasure:
title: "Tìm kiếm kho báu"
description: "Tìm thấy được những kho báu cất giấu"
_client30min:
title: "Giải lao xỉu"
_noteDeletedWithin1min:
title: "Xem như không có gì đâu nha"
_postedAtLateNight:
title: "Loài ăn đêm"
description: "Đăng bài trong đêm khuya "
_postedAt0min0sec:
title: "Tín hiệu báo giờ"
description: "Đăng bài vào 0 phút 0 giây"
flavor: "Piiiiiii ĐÂY LÀ TIẾNG NÓI VIỆT NAM"
_selfQuote:
title: "Nói đến bản thân"
description: "Trích dẫn bài viết của mình"
_htl20npm:
title: "Timeline trôi như con sông"
description: "Timeline trang chính tốc độ vượt lên 20npm"
_viewInstanceChart:
title: "Nhà phân tích"
description: "Xem biểu đồ của chủ máy"
_outputHelloWorldOnScratchpad:
title: "Chào thế giới!"
_open3windows:
title: "Nhiều cửa sổ"
description: "Mở cửa sổ hơn 3 cửa sổ"
_reactWithoutRead:
title: "Bài này bạn đọc kỹ chứ? "
description: "Phản hồi trong vọng 3 giây sau bài viết có hơn 100 ký tự mới được đăng lên"
_clickedClickHere:
title: "Bấm đây"
description: "Bấm chỗ này"
_justPlainLucky:
title: "Chỉ là một cuộc máy mắn"
description: "Mỗi 10 giây thu nhận được với tỷ lệ 0.005%"
_setNameToSyuilo:
title: "Ngưỡng mộ với vị thần"
description: "Đạt tên là syuilo"
_loggedInOnBirthday:
title: "Sinh nhật vủi vẻ"
description: "Đăng nhập vào ngày sinh"
_loggedInOnNewYearsDay:
title: "Chức mừng năm mới"
description: "Đăng nhập vào Tết Nguyên đàn dương lịch"
flavor: "Chúc bạn năm mới AN KHANG THỊNH VƯỢNG, VẠN SỰ NHƯ Ý!!"
_cookieClicked:
flavor: "Bạn nhầm phầm mềm chứ?"
_role: _role:
priority: "Ưu tiên" priority: "Ưu tiên"
_priority: _priority:
low: "Thấp" low: "Thấp"
middle: "Vừa" middle: "Vừa"
high: "Cao" high: "Cao"
_options:
gtlAvailable: "Xem Timeline xã hội"
ltlAvailable: "Xem Timeline trong máy chủ này"
canPublicNote: "Cho phép đăng bài công khai"
canManageCustomEmojis: "Quản lý CustomEmoji"
driveCapacity: "Dữ liệu Drive"
pinMax: "Giới hạn ghim bài viết"
antennaMax: "Giới hạn tạo ăng ten"
canHideAds: "Tắt quảng cáo"
_condition:
createdMoreThan: "Trôi qua sau khi lập tài khoản"
followersLessThanOrEq: "Người theo dõi ít hơn "
followersMoreThanOrEq: "Người theo dõi có trở lên"
followingLessThanOrEq: "Theo dõi ít hơn "
followingMoreThanOrEq: "Theo dõi có trở lên"
and: ""
or: " hay là "
not: "Không phải "
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ." description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ."
sensitivity: "Phát hiện nhạy cảm" sensitivity: "Phát hiện nhạy cảm"
@ -1102,6 +1344,7 @@ _ago:
weeksAgo: "{n} tuần trước" weeksAgo: "{n} tuần trước"
monthsAgo: "{n} tháng trước" monthsAgo: "{n} tháng trước"
yearsAgo: "{n} năm trước" yearsAgo: "{n} năm trước"
invalid: "Không có gì ở đây"
_time: _time:
second: "s" second: "s"
minute: "phút" minute: "phút"
@ -1130,15 +1373,23 @@ _tutorial:
step7_1: "Xin chúc mừng! Bây giờ bạn đã hoàn thành phần hướng dẫn cơ bản của Misskey." step7_1: "Xin chúc mừng! Bây giờ bạn đã hoàn thành phần hướng dẫn cơ bản của Misskey."
step7_2: "Nếu bạn muốn tìm hiểu thêm về Misskey, hãy thử phần {help}." step7_2: "Nếu bạn muốn tìm hiểu thêm về Misskey, hãy thử phần {help}."
step7_3: "Bây giờ, chúc may mắn và vui vẻ với Misskey! 🚀" step7_3: "Bây giờ, chúc may mắn và vui vẻ với Misskey! 🚀"
step8_1: "Cuối cùng, bạn hãy bật thông báo đẩy nha!"
step8_2: "Nhận thông báo đẩy bạn sẽ có thể thấy phản hồi, theo dõi, lượt nhắc được trong khi đóng Misskey"
_2fa: _2fa:
alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước." alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước."
passwordToTOTP: "Nhắn mật mã"
step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn." step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn."
step2: "Sau đó, quét mã QR hiển thị trên màn hình này." step2: "Sau đó, quét mã QR hiển thị trên màn hình này."
step2Url: "Bạn cũng có thể nhập URL này nếu sử dụng một chương trình máy tính:" step2Url: "Bạn cũng có thể nhập URL này nếu sử dụng một chương trình máy tính:"
step3: "Nhập mã token do ứng dụng của bạn cung cấp để hoàn tất thiết lập." step3: "Nhập mã token do ứng dụng của bạn cung cấp để hoàn tất thiết lập."
step4: "Kể từ bây giờ, những lần đăng nhập trong tương lai sẽ yêu cầu mã token đăng nhập đó." step4: "Kể từ bây giờ, những lần đăng nhập trong tương lai sẽ yêu cầu mã token đăng nhập đó."
securityKeyInfo: "Bên cạnh xác minh bằng vân tay hoặc mã PIN, bạn cũng có thể thiết lập xác minh thông qua khóa bảo mật phần cứng hỗ trợ FIDO2 để bảo mật hơn nữa cho tài khoản của mình." securityKeyInfo: "Bên cạnh xác minh bằng vân tay hoặc mã PIN, bạn cũng có thể thiết lập xác minh thông qua khóa bảo mật phần cứng hỗ trợ FIDO2 để bảo mật hơn nữa cho tài khoản của mình."
removeKey: "Xóa mã bảo mật"
removeKeyConfirm: "Xóa bản sao lưu {name}?" removeKeyConfirm: "Xóa bản sao lưu {name}?"
renewTOTP: "Cài đặt lại ứng dụng xác thực"
renewTOTPConfirm: "Mã xác nhận cũ của ứng dụng xác thực không thể sử dụng được nữa"
renewTOTPOk: "Cài đặt lại"
renewTOTPCancel: "Không, cảm ơn"
_permissions: _permissions:
"read:account": "Xem thông tin tài khoản của bạn" "read:account": "Xem thông tin tài khoản của bạn"
"write:account": "Sửa thông tin tài khoản của bạn" "write:account": "Sửa thông tin tài khoản của bạn"
@ -1173,12 +1424,15 @@ _permissions:
"read:gallery-likes": "Xem danh sách các tút đã thích trong thư viện của tôi" "read:gallery-likes": "Xem danh sách các tút đã thích trong thư viện của tôi"
"write:gallery-likes": "Sửa danh sách các tút đã thích trong thư viện của tôi" "write:gallery-likes": "Sửa danh sách các tút đã thích trong thư viện của tôi"
_auth: _auth:
shareAccessTitle: "Cho phép truy cập app"
shareAccess: "Bạn có muốn cho phép \"{name}\" truy cập vào tài khoản này không?" shareAccess: "Bạn có muốn cho phép \"{name}\" truy cập vào tài khoản này không?"
shareAccessAsk: "Bạn có chắc muốn cho phép ứng dụng này truy cập vào tài khoản của mình không?" shareAccessAsk: "Bạn có chắc muốn cho phép ứng dụng này truy cập vào tài khoản của mình không?"
permission: "{name} đang yêu cầu quyền hạn dưới đây"
permissionAsk: "Ứng dụng này yêu cầu các quyền sau" permissionAsk: "Ứng dụng này yêu cầu các quyền sau"
pleaseGoBack: "Vui lòng quay lại ứng dụng" pleaseGoBack: "Vui lòng quay lại ứng dụng"
callback: "Quay lại ứng dụng" callback: "Quay lại ứng dụng"
denied: "Truy cập bị từ chối" denied: "Truy cập bị từ chối"
pleaseLogin: "Bạn phải đăng nhập để cho ứng dụng phép truy cập"
_antennaSources: _antennaSources:
all: "Toàn bộ tút" all: "Toàn bộ tút"
homeTimeline: "Tút từ những người đã theo dõi" homeTimeline: "Tút từ những người đã theo dõi"
@ -1216,9 +1470,12 @@ _widgets:
jobQueue: "Công việc chờ xử lý" jobQueue: "Công việc chờ xử lý"
serverMetric: "Thống kê máy chủ" serverMetric: "Thống kê máy chủ"
aiscript: "AiScript console" aiscript: "AiScript console"
aiscriptApp: "AiScript App"
aichan: "Ai" aichan: "Ai"
userList: "Danh sách người dùng"
_userList: _userList:
chooseList: "Chọn danh sách" chooseList: "Chọn danh sách"
clicker: "clicker"
_cw: _cw:
hide: "Ẩn" hide: "Ẩn"
show: "Tải thêm" show: "Tải thêm"
@ -1255,6 +1512,8 @@ _visibility:
followersDescription: "Dành riêng cho người theo dõi" followersDescription: "Dành riêng cho người theo dõi"
specified: "Nhắn riêng" specified: "Nhắn riêng"
specifiedDescription: "Chỉ người được nhắc đến mới thấy" specifiedDescription: "Chỉ người được nhắc đến mới thấy"
disableFederation: "Không liên hợp"
disableFederationDescription: "Không đưa tin cho chủ máy khác"
_postForm: _postForm:
replyPlaceholder: "Trả lời tút này" replyPlaceholder: "Trả lời tút này"
quotePlaceholder: "Trích dẫn tút này" quotePlaceholder: "Trích dẫn tút này"
@ -1264,7 +1523,7 @@ _postForm:
b: "Hôm nay bạn có gì vui?" b: "Hôm nay bạn có gì vui?"
c: "Bạn đang nghĩ gì?" c: "Bạn đang nghĩ gì?"
d: "Bạn muốn nói gì?" d: "Bạn muốn nói gì?"
e: "Bắt đầu viết..." e: "Cứ viết trên đây"
f: "Đang chờ bạn viết..." f: "Đang chờ bạn viết..."
_profile: _profile:
name: "Tên" name: "Tên"
@ -1280,6 +1539,7 @@ _profile:
changeBanner: "Đổi ảnh bìa" changeBanner: "Đổi ảnh bìa"
_exportOrImport: _exportOrImport:
allNotes: "Toàn bộ tút" allNotes: "Toàn bộ tút"
favoritedNotes: "Bài viết đã thích"
followingList: "Đang theo dõi" followingList: "Đang theo dõi"
muteList: "Ẩn" muteList: "Ẩn"
blockingList: "Chặn" blockingList: "Chặn"
@ -1318,7 +1578,16 @@ _timelines:
social: "Xã hội" social: "Xã hội"
global: "Liên hợp" global: "Liên hợp"
_play: _play:
new: "Tạo Play mới"
edit: "Edit play"
created: "Bạn vừa tạo play rồi"
updated: "Bạn vừa cập nhật play rồi"
deleted: "Bạn vừa xóa play rồi"
pageSetting: "Cài đặt play"
editThisPage: "Edit play này"
viewSource: "Xem mã nguồn" viewSource: "Xem mã nguồn"
my: "Play của mình"
liked: "Play đã thích"
featured: "Nổi tiếng" featured: "Nổi tiếng"
title: "Tựa đề" title: "Tựa đề"
script: "Kịch bản" script: "Kịch bản"
@ -1386,7 +1655,9 @@ _notification:
youReceivedFollowRequest: "Bạn vừa có một yêu cầu theo dõi" youReceivedFollowRequest: "Bạn vừa có một yêu cầu theo dõi"
yourFollowRequestAccepted: "Yêu cầu theo dõi của bạn đã được chấp nhận" yourFollowRequestAccepted: "Yêu cầu theo dõi của bạn đã được chấp nhận"
pollEnded: "Cuộc bình chọn đã kết thúc" pollEnded: "Cuộc bình chọn đã kết thúc"
unreadAntennaNote: "Ăng ten"
emptyPushNotificationMessage: "Đã cập nhật thông báo đẩy" emptyPushNotificationMessage: "Đã cập nhật thông báo đẩy"
achievementEarned: "Hoàn thành Achievement"
_types: _types:
all: "Toàn bộ" all: "Toàn bộ"
follow: "Đang theo dõi" follow: "Đang theo dõi"
@ -1398,6 +1669,7 @@ _notification:
pollEnded: "Bình chọn kết thúc" pollEnded: "Bình chọn kết thúc"
receiveFollowRequest: "Yêu cầu theo dõi" receiveFollowRequest: "Yêu cầu theo dõi"
followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận"
achievementEarned: "Hoàn thành Achievement"
app: "Từ app liên kết" app: "Từ app liên kết"
_actions: _actions:
followBack: "đã theo dõi lại bạn" followBack: "đã theo dõi lại bạn"
@ -1430,3 +1702,6 @@ _deck:
channel: "Kênh" channel: "Kênh"
mentions: "Lượt nhắc" mentions: "Lượt nhắc"
direct: "Nhắn riêng" direct: "Nhắn riêng"
_dialog:
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}"
charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}"

View file

@ -2,7 +2,7 @@
_lang_: "中文(简体)" _lang_: "中文(简体)"
headlineMisskey: "通过帖子连接在一起的网络" headlineMisskey: "通过帖子连接在一起的网络"
introMisskey: "欢迎Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧📡\n通过「回应」功能可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧🚀" introMisskey: "欢迎Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧📡\n通过「回应」功能可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧🚀"
poweredByMisskeyDescription: "{name} 由开源平台 <b>Misskey</b> 驱动(也被称为 Misskey 实例" poweredByMisskeyDescription: "{name} 由开源平台 <b>Misskey</b> 驱动(也被称为 Misskey 服务器"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "搜索" search: "搜索"
notifications: "通知" notifications: "通知"
@ -18,7 +18,7 @@ enterUsername: "输入用户名"
renotedBy: "由 {user} 转贴" renotedBy: "由 {user} 转贴"
noNotes: "没有帖子" noNotes: "没有帖子"
noNotifications: "无通知" noNotifications: "无通知"
instance: "实例" instance: "服务器"
settings: "设置" settings: "设置"
basicSettings: "基本设置" basicSettings: "基本设置"
otherSettings: "其他设置" otherSettings: "其他设置"
@ -144,7 +144,7 @@ emojiUrl: "表情符号地址"
addEmoji: "添加表情符号" addEmoji: "添加表情符号"
settingGuide: "推荐配置" settingGuide: "推荐配置"
cacheRemoteFiles: "远程文件缓存" cacheRemoteFiles: "远程文件缓存"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程实例载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这是一个机器人账号" flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此帐户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此帐户识别为机器人。" flagAsBotDescription: "如果此帐户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此帐户识别为机器人。"
flagAsCat: "将这个账户设定为一只猫" flagAsCat: "将这个账户设定为一只猫"
@ -154,7 +154,7 @@ flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的
autoAcceptFollowed: "自动允许关注者的关注" autoAcceptFollowed: "自动允许关注者的关注"
addAccount: "添加账户" addAccount: "添加账户"
loginFailed: "登录失败" loginFailed: "登录失败"
showOnRemote: "转到所在实例显示" showOnRemote: "转到所在服务器显示"
general: "常规设置" general: "常规设置"
wallpaper: "壁纸" wallpaper: "壁纸"
setWallpaper: "设置壁纸" setWallpaper: "设置壁纸"
@ -169,7 +169,7 @@ selectUser: "选择用户"
recipient: "收件人" recipient: "收件人"
annotation: "注解" annotation: "注解"
federation: "联合" federation: "联合"
instances: "实例" instances: "服务器"
registeredAt: "初次观测" registeredAt: "初次观测"
latestRequestReceivedAt: "上次收到的请求" latestRequestReceivedAt: "上次收到的请求"
latestStatus: "最后状态" latestStatus: "最后状态"
@ -178,7 +178,7 @@ charts: "图表"
perHour: "每小时" perHour: "每小时"
perDay: "每天" perDay: "每天"
stopActivityDelivery: "停止发送活动" stopActivityDelivery: "停止发送活动"
blockThisInstance: "阻止此实例向本实例推流" blockThisInstance: "阻止此服务器向本服务器推流"
operations: "操作" operations: "操作"
software: "软件" software: "软件"
version: "版本" version: "版本"
@ -189,15 +189,15 @@ jobQueue: "作业队列"
cpuAndMemory: "CPU和内存" cpuAndMemory: "CPU和内存"
network: "网络" network: "网络"
disk: "存储" disk: "存储"
instanceInfo: "实例信息" instanceInfo: "服务器信息"
statistics: "统计" statistics: "统计"
clearQueue: "清除队列" clearQueue: "清除队列"
clearQueueConfirmTitle: "确定清除队列?" clearQueueConfirmTitle: "确定清除队列?"
clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。" clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。"
clearCachedFiles: "清除缓存" clearCachedFiles: "清除缓存"
clearCachedFilesConfirm: "确定要清除缓存文件?" clearCachedFilesConfirm: "确定要清除缓存文件?"
blockedInstances: "被阻拦的实例" blockedInstances: "被阻拦的服务器"
blockedInstancesDescription: "设定要阻拦的实例,以换行来进行分割。被阻拦的实例将无法与本实例进行交换通讯。" blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
muteAndBlock: "屏蔽/拉黑" muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户" mutedUsers: "已屏蔽用户"
blockedUsers: "被拉黑的用户" blockedUsers: "被拉黑的用户"
@ -220,9 +220,9 @@ all: "全部"
subscribing: "已订阅" subscribing: "已订阅"
publishing: "投递中" publishing: "投递中"
notResponding: "没有响应" notResponding: "没有响应"
instanceFollowing: "关注实例" instanceFollowing: "关注服务器"
instanceFollowers: "关注实例" instanceFollowers: "关注的服务器"
instanceUsers: "实例用户" instanceUsers: "服务器用户"
changePassword: "修改密码" changePassword: "修改密码"
security: "安全" security: "安全"
retypedNotMatch: "两次输入不一致!" retypedNotMatch: "两次输入不一致!"
@ -264,7 +264,7 @@ basicNotesBeforeCreateAccount: "基本注意事项"
tos: "服务条款" tos: "服务条款"
start: "开始" start: "开始"
home: "首页" home: "首页"
remoteUserCaution: "由于此用户来自其它实例,显示的信息可能不完整。" remoteUserCaution: "由于此用户来自其它服务器,显示的信息可能不完整。"
activity: "活动" activity: "活动"
images: "图片" images: "图片"
birthday: "生日" birthday: "生日"
@ -314,8 +314,8 @@ unwatch: "取消关注"
accept: "允许" accept: "允许"
reject: "拒绝" reject: "拒绝"
normal: "正常" normal: "正常"
instanceName: "实例名称" instanceName: "服务器名称"
instanceDescription: "实例介绍" instanceDescription: "服务器简介"
maintainerName: "管理员名称" maintainerName: "管理员名称"
maintainerEmail: "管理员电子邮箱" maintainerEmail: "管理员电子邮箱"
tosUrl: "服务条款URL" tosUrl: "服务条款URL"
@ -345,7 +345,7 @@ basicInfo: "基本信息"
pinnedUsers: "置顶用户" pinnedUsers: "置顶用户"
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。" pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
pinnedPages: "固定页面" pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。" pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。"
pinnedClipId: "置顶的便签ID" pinnedClipId: "置顶的便签ID"
pinnedNotes: "已置顶的帖子" pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
@ -393,13 +393,19 @@ about: "关于"
aboutMisskey: "关于 Misskey" aboutMisskey: "关于 Misskey"
administrator: "管理员" administrator: "管理员"
token: "Token (令牌)" token: "Token (令牌)"
2fa: "双因素认证"
totp: "身份验证应用"
totpDescription: "使用认证应用输入一次性密码。"
moderator: "监察员" moderator: "监察员"
moderation: "管理" moderation: "管理"
nUsersMentioned: "{n} 被提到" nUsersMentioned: "{n} 被提到"
securityKeyAndPasskey: "安全密钥/密码"
securityKey: "安全密钥" securityKey: "安全密钥"
lastUsed: "最后使用:" lastUsed: "最后使用:"
lastUsedAt: "最后使用: {t}"
unregister: "删除账户" unregister: "删除账户"
passwordLessLogin: "无密码登录" passwordLessLogin: "无密码登录"
passwordLessLoginDescription: "不使用密码仅使用安全密钥或Passkey登录"
resetPassword: "重置密码" resetPassword: "重置密码"
newPasswordIs: "新的密码是「{password}」" newPasswordIs: "新的密码是「{password}」"
reduceUiAnimation: "减少UI动画" reduceUiAnimation: "减少UI动画"
@ -451,6 +457,7 @@ aboutX: "关于 {x}"
emojiStyle: "emoji 的样式" emojiStyle: "emoji 的样式"
native: "原生" native: "原生"
disableDrawer: "不显示抽屉菜单" disableDrawer: "不显示抽屉菜单"
showNoteActionsOnlyHover: "仅在悬停时显示帖子操作"
noHistory: "没有历史记录" noHistory: "没有历史记录"
signinHistory: "登录历史" signinHistory: "登录历史"
enableAdvancedMfm: "启用扩展MFM" enableAdvancedMfm: "启用扩展MFM"
@ -499,6 +506,7 @@ objectStorageSetPublicRead: "上传时设置为public-read"
serverLogs: "服务器日志" serverLogs: "服务器日志"
deleteAll: "全部删除" deleteAll: "全部删除"
showFixedPostForm: "在时间线顶部显示发帖框" showFixedPostForm: "在时间线顶部显示发帖框"
showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)"
newNoteRecived: "有新的帖子" newNoteRecived: "有新的帖子"
sounds: "提示音" sounds: "提示音"
sound: "提示音" sound: "提示音"
@ -531,7 +539,7 @@ updateRemoteUser: "更新远程用户信息"
deleteAllFiles: "删除所有文件" deleteAllFiles: "删除所有文件"
deleteAllFilesConfirm: "要删除所有文件吗?" deleteAllFilesConfirm: "要删除所有文件吗?"
removeAllFollowing: "取消所有关注" removeAllFollowing: "取消所有关注"
removeAllFollowingDescription: "取消{host}的所有关注者。当实例不存在时执行。" removeAllFollowingDescription: "取消{host}的所有关注者。当服务器不再存在时执行。"
userSuspended: "该用户已被冻结。" userSuspended: "该用户已被冻结。"
userSilenced: "该用户已被禁言。" userSilenced: "该用户已被禁言。"
yourAccountSuspendedTitle: "账户已被冻结" yourAccountSuspendedTitle: "账户已被冻结"
@ -628,15 +636,15 @@ abuseReported: "内容已发送。感谢您提交信息。"
reporter: "举报者" reporter: "举报者"
reporteeOrigin: "举报来源" reporteeOrigin: "举报来源"
reporterOrigin: "举报者来源" reporterOrigin: "举报者来源"
forwardReport: "将该举报信息转发给远程实例" forwardReport: "将该举报信息转发给远程服务器"
forwardReportIsAnonymous: "勾选则在远程实例上显示的举报者是匿名的系统账号,而不是您的账号。" forwardReportIsAnonymous: "勾选则在远程服务器上显示的举报者是匿名的系统账号,而不是您的账号。"
send: "发送" send: "发送"
abuseMarkAsResolved: "处理完毕" abuseMarkAsResolved: "处理完毕"
openInNewTab: "在新标签页中打开" openInNewTab: "在新标签页中打开"
openInSideView: "在侧边栏中打开" openInSideView: "在侧边栏中打开"
defaultNavigationBehaviour: "默认导航" defaultNavigationBehaviour: "默认导航"
editTheseSettingsMayBreakAccount: "编辑这些设置可以会损坏您的账号" editTheseSettingsMayBreakAccount: "编辑这些设置可以会损坏您的账号"
instanceTicker: "帖子的实例信息" instanceTicker: "帖子的服务器来源"
waitingFor: "等待{x}" waitingFor: "等待{x}"
random: "随机" random: "随机"
system: "系统" system: "系统"
@ -725,7 +733,7 @@ capacity: "容量"
inUse: "已使用" inUse: "已使用"
editCode: "编辑代码" editCode: "编辑代码"
apply: "应用" apply: "应用"
receiveAnnouncementFromInstance: "从实例接收通知" receiveAnnouncementFromInstance: "从服务器接收通知"
emailNotification: "邮件通知" emailNotification: "邮件通知"
publish: "发布" publish: "发布"
inChannelSearch: "频道内搜索" inChannelSearch: "频道内搜索"
@ -753,7 +761,7 @@ active: "活动"
offline: "离线" offline: "离线"
notRecommended: "不推荐" notRecommended: "不推荐"
botProtection: "Bot防御" botProtection: "Bot防御"
instanceBlocking: "被阻拦的实例" instanceBlocking: "被阻拦的服务器"
selectAccount: "选择账户" selectAccount: "选择账户"
switchAccount: "切换账户" switchAccount: "切换账户"
enabled: "已启用" enabled: "已启用"
@ -773,6 +781,7 @@ popularPosts: "热门投稿"
shareWithNote: "在帖子中分享" shareWithNote: "在帖子中分享"
ads: "广告" ads: "广告"
expiration: "截止时间" expiration: "截止时间"
startingperiod: "开始时间"
memo: "便笺" memo: "便笺"
priority: "优先级" priority: "优先级"
high: "高" high: "高"
@ -799,12 +808,13 @@ translatedFrom: "从 {x} 翻译"
accountDeletionInProgress: "正在删除账户" accountDeletionInProgress: "正在删除账户"
usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
aiChanMode: "小蓝模式" aiChanMode: "小蓝模式"
keepCw: "保留CW" keepCw: "回复时维持隐藏内容"
pubSub: "Pub/Sub账户" pubSub: "Pub/Sub账户"
lastCommunication: "最近通信" lastCommunication: "最近通信"
resolved: "已解决" resolved: "已解决"
unresolved: "未解决" unresolved: "未解决"
breakFollow: "移除关注者" breakFollow: "移除关注者"
breakFollowConfirm: "你想取消关注吗?"
itsOn: "已开启" itsOn: "已开启"
itsOff: "已关闭" itsOff: "已关闭"
emailRequiredForSignup: "注册账户需要电子邮件地址" emailRequiredForSignup: "注册账户需要电子邮件地址"
@ -835,21 +845,23 @@ themeColor: "主题颜色"
size: "大小" size: "大小"
numberOfColumn: "列数" numberOfColumn: "列数"
searchByGoogle: "Google" searchByGoogle: "Google"
instanceDefaultLightTheme: "实例默认浅色主题" instanceDefaultLightTheme: "服务器默认浅色主题"
instanceDefaultDarkTheme: "实例默认深色主题" instanceDefaultDarkTheme: "服务器默认深色主题"
instanceDefaultThemeDescription: "以对象格式键入主题代码" instanceDefaultThemeDescription: "以对象格式键入主题代码"
mutePeriod: "屏蔽期限" mutePeriod: "屏蔽期限"
period: "截止时间"
indefinitely: "永久" indefinitely: "永久"
tenMinutes: "10分钟" tenMinutes: "10分钟"
oneHour: "1小时" oneHour: "1小时"
oneDay: "1天" oneDay: "1天"
oneWeek: "1周" oneWeek: "1周"
oneMonth: "1 个月"
reflectMayTakeTime: "可能需要一些时间才能体现出效果。" reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
failedToFetchAccountInformation: "获取账户信息失败" failedToFetchAccountInformation: "获取账户信息失败"
rateLimitExceeded: "已超過速率限制" rateLimitExceeded: "已超過速率限制"
cropImage: "剪裁图像" cropImage: "剪裁图像"
cropImageAsk: "是否要裁剪图像?" cropImageAsk: "是否要裁剪图像?"
cropYes: "裁剪" cropYes: "裁剪"
cropNo: "就这样吧!" cropNo: "就这样吧!"
file: "文件" file: "文件"
recentNHours: "最近{n}小时" recentNHours: "最近{n}小时"
@ -887,7 +899,7 @@ cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。" cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
beta: "测试" beta: "测试"
enableAutoSensitive: "自动 NSFW 识别" enableAutoSensitive: "自动 NSFW 识别"
enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据实例自动设置。" enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据服务器自动设置。"
activeEmailValidationDescription: "开启用户的电子邮件地址验证,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。" activeEmailValidationDescription: "开启用户的电子邮件地址验证,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。"
navbar: "导航栏" navbar: "导航栏"
shuffle: "随机" shuffle: "随机"
@ -897,7 +909,7 @@ pushNotification: "推送通知"
subscribePushNotification: "启用推送通知消息" subscribePushNotification: "启用推送通知消息"
unsubscribePushNotification: "停用推送通知消息" unsubscribePushNotification: "停用推送通知消息"
pushNotificationAlreadySubscribed: "推送通知消息已启用" pushNotificationAlreadySubscribed: "推送通知消息已启用"
pushNotificationNotSupported: "浏览器或实例不支持推送通知消息" pushNotificationNotSupported: "浏览器或服务器不支持推送通知消息"
sendPushNotificationReadMessage: "删除已读推送通知消息" sendPushNotificationReadMessage: "删除已读推送通知消息"
sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。" sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。"
windowMaximize: "最大化" windowMaximize: "最大化"
@ -939,6 +951,14 @@ collapseRenotes: "省略显示已经看过的转发内容"
internalServerError: "内部服务器错误" internalServerError: "内部服务器错误"
internalServerErrorDescription: "内部服务器发生了预期外的错误" internalServerErrorDescription: "内部服务器发生了预期外的错误"
copyErrorInfo: "复制错误信息" copyErrorInfo: "复制错误信息"
joinThisServer: "在本服务器上注册"
exploreOtherServers: "探索其他服务器"
letsLookAtTimeline: "时间线"
disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。"
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道"
cannotBeChangedLater: "之后不能再更改。"
_achievements: _achievements:
earnedAt: "达成时间" earnedAt: "达成时间"
_types: _types:
@ -1111,7 +1131,7 @@ _achievements:
title: "休息一下!" title: "休息一下!"
description: "启动客户端超过30分钟" description: "启动客户端超过30分钟"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "无话可说" title: "欲言又止"
description: "发帖后一分钟内就将其删除" description: "发帖后一分钟内就将其删除"
_postedAtLateNight: _postedAtLateNight:
title: "夜猫子" title: "夜猫子"
@ -1129,7 +1149,7 @@ _achievements:
description: "在首页时间线的流速超过20npm" description: "在首页时间线的流速超过20npm"
_viewInstanceChart: _viewInstanceChart:
title: "分析师" title: "分析师"
description: "查看了实例信息中的图表" description: "查看了服务器信息中的图表"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Hello, world!" title: "Hello, world!"
description: "在AiScript控制台中输出 hello world" description: "在AiScript控制台中输出 hello world"
@ -1166,7 +1186,7 @@ _achievements:
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "恭贺新禧" title: "恭贺新禧"
description: "在元旦登入" description: "在元旦登入"
flavor: "今年也请对本实例多多指教!" flavor: "今年也请对本服务器多多指教!"
_cookieClicked: _cookieClicked:
title: "点击饼干小游戏" title: "点击饼干小游戏"
description: "点击了可疑的饼干" description: "点击了可疑的饼干"
@ -1181,7 +1201,7 @@ _role:
name: "角色名称" name: "角色名称"
description: "角色描述" description: "角色描述"
permission: "角色权限" permission: "角色权限"
descriptionOfPermission: "<b>监察员</b>可以执行基本的审核操作。\n<b>管理员</b>可以更改实例的所有设置。" descriptionOfPermission: "<b>监察员</b>可以执行基本地审核操作。\n<b>管理员</b>可以更改服务器的所有设置。"
assignTarget: "授权对象" assignTarget: "授权对象"
descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个角色中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。" descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个角色中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。"
manual: "手动" manual: "手动"
@ -1271,7 +1291,7 @@ _ad:
_forgotPassword: _forgotPassword:
enterEmail: "请输入您验证账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。" enterEmail: "请输入您验证账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。"
ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系管理员。" ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系管理员。"
contactAdmin: "该实例不支持发送电子邮件。如果您想重设密码,请联系管理员。" contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。"
_gallery: _gallery:
my: "我的图库" my: "我的图库"
liked: "喜欢的图片" liked: "喜欢的图片"
@ -1356,10 +1376,10 @@ _wordMute:
hard: "硬屏蔽" hard: "硬屏蔽"
mutedNotes: "被屏蔽的帖子" mutedNotes: "被屏蔽的帖子"
_instanceMute: _instanceMute:
instanceMuteDescription: "屏蔽配置实例中的所有帖子和转帖,包括实例的用户回复。" instanceMuteDescription: "屏蔽配置服务器中的所有帖子和转帖,包括这些服务器上的用户回复。"
instanceMuteDescription2: "设置时用换行符来分隔" instanceMuteDescription2: "设置时用换行符来分隔"
title: "隐藏实例已设置的帖子。" title: "隐藏服务器已设置的帖子。"
heading: "屏蔽实例" heading: "屏蔽服务器"
_theme: _theme:
explore: "寻找主题" explore: "寻找主题"
install: "安装主题" install: "安装主题"
@ -1418,9 +1438,9 @@ _theme:
infoFg: "信息文本" infoFg: "信息文本"
infoWarnBg: "警告背景" infoWarnBg: "警告背景"
infoWarnFg: "警告文本" infoWarnFg: "警告文本"
cwBg: "CW 按钮背景" cwBg: "隐藏内容按钮背景"
cwFg: "CW 按钮文本" cwFg: "隐藏内容按钮文本"
cwHoverBg: "CW 按钮背景(悬停)" cwHoverBg: "隐藏内容按钮背景(悬停)"
toastBg: "Toast通知背景" toastBg: "Toast通知背景"
toastFg: "Toast通知文本" toastFg: "Toast通知文本"
buttonBg: "按钮背景" buttonBg: "按钮背景"
@ -1452,6 +1472,7 @@ _ago:
weeksAgo: "{n}周前" weeksAgo: "{n}周前"
monthsAgo: "{n}月前" monthsAgo: "{n}月前"
yearsAgo: "{n}年前" yearsAgo: "{n}年前"
invalid: "没有"
_time: _time:
second: "秒" second: "秒"
minute: "分" minute: "分"
@ -1485,13 +1506,28 @@ _tutorial:
step8_3: "您也可以稍后再更改通知设置。" step8_3: "您也可以稍后再更改通知设置。"
_2fa: _2fa:
alreadyRegistered: "此设备已被注册" alreadyRegistered: "此设备已被注册"
registerTOTP: "开始设置认证应用"
passwordToTOTP: "请输入您的密码"
step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。" step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。"
step2: "然后,扫描屏幕上显示的二维码。" step2: "然后,扫描屏幕上显示的二维码。"
step2Click: "通过点击QR码您可以使用设备上安装的身份验证器应用程序或密钥环进行注册"
step2Url: "在桌面应用程序中输入以下URL" step2Url: "在桌面应用程序中输入以下URL"
step3Title: "输入验证码"
step3: "输入您的应用提供的动态口令以完成设置。" step3: "输入您的应用提供的动态口令以完成设置。"
step4: "从现在开始,任何登录操作都将要求您提供动态口令。" step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
securityKeyNotSupported: "您的浏览器不支持安全密钥。"
registerTOTPBeforeKey: "要注册安全密钥或Passkey请先设置验证器应用程序。"
securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。" securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。"
chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。"
registerSecurityKey: "注册安全密钥或Passkey"
securityKeyName: "输入密钥名称"
tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或Passkey。"
removeKey: "删除安全密钥"
removeKeyConfirm: "您确定要删除 {name} 吗?" removeKeyConfirm: "您确定要删除 {name} 吗?"
whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。"
renewTOTP: "重置验证器应用程序"
renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效"
renewTOTPOk: "重新配置"
renewTOTPCancel: "不用,谢谢" renewTOTPCancel: "不用,谢谢"
_permissions: _permissions:
"read:account": "查看账户信息" "read:account": "查看账户信息"
@ -1551,7 +1587,7 @@ _weekday:
saturday: "星期六" saturday: "星期六"
_widgets: _widgets:
profile: "个人资料" profile: "个人资料"
instanceInfo: "实例信息" instanceInfo: "服务器信息"
memo: "便签" memo: "便签"
notifications: "通知" notifications: "通知"
timeline: "时间线" timeline: "时间线"
@ -1565,7 +1601,7 @@ _widgets:
digitalClock: "数字时钟" digitalClock: "数字时钟"
unixClock: "UNIX时钟" unixClock: "UNIX时钟"
federation: "联邦宇宙" federation: "联邦宇宙"
instanceCloud: "实例云" instanceCloud: "服务器云"
postForm: "投稿窗口" postForm: "投稿窗口"
slideshow: "幻灯片展示" slideshow: "幻灯片展示"
button: "按钮" button: "按钮"
@ -1615,6 +1651,8 @@ _visibility:
followersDescription: "仅发送至关注者" followersDescription: "仅发送至关注者"
specified: "指定用户" specified: "指定用户"
specifiedDescription: "仅发送至指定用户" specifiedDescription: "仅发送至指定用户"
disableFederation: "不参与联合"
disableFederationDescription: "不发送到其他服务器"
_postForm: _postForm:
replyPlaceholder: "回复这个帖子..." replyPlaceholder: "回复这个帖子..."
quotePlaceholder: "引用这个帖子..." quotePlaceholder: "引用这个帖子..."
@ -1630,7 +1668,7 @@ _profile:
name: "昵称" name: "昵称"
username: "用户名" username: "用户名"
description: "个人简介" description: "个人简介"
youCanIncludeHashtags: "你可以在个人简介中包含一#标签。" youCanIncludeHashtags: "你可以在个人简介中包含一#标签。"
metadata: "附加信息" metadata: "附加信息"
metadataEdit: "附加信息编辑" metadataEdit: "附加信息编辑"
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。" metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
@ -1770,6 +1808,7 @@ _notification:
pollEnded: "问卷调查结束" pollEnded: "问卷调查结束"
receiveFollowRequest: "收到关注请求" receiveFollowRequest: "收到关注请求"
followRequestAccepted: "关注请求已通过" followRequestAccepted: "关注请求已通过"
achievementEarned: "取得的成就"
app: "关联应用的通知" app: "关联应用的通知"
_actions: _actions:
followBack: "回关" followBack: "回关"
@ -1802,3 +1841,6 @@ _deck:
channel: "频道" channel: "频道"
mentions: "提及" mentions: "提及"
direct: "指定用户" direct: "指定用户"
_dialog:
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"

View file

@ -46,7 +46,7 @@ copyContent: "複製內容"
copyLink: "複製連結" copyLink: "複製連結"
delete: "刪除" delete: "刪除"
deleteAndEdit: "刪除並編輯" deleteAndEdit: "刪除並編輯"
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有情感、轉發和回覆也將會消失。" deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。"
addToList: "加入至清單" addToList: "加入至清單"
sendMessage: "發送訊息" sendMessage: "發送訊息"
copyRSS: "複製RSS" copyRSS: "複製RSS"
@ -112,7 +112,7 @@ clickToShow: "按一下以顯示"
sensitive: "敏感內容" sensitive: "敏感內容"
add: "新增" add: "新增"
reaction: "反應" reaction: "反應"
reactions: "情感" reactions: "反應"
reactionSetting: "在選擇器中顯示反應" reactionSetting: "在選擇器中顯示反應"
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
rememberNoteVisibility: "記住貼文可見性" rememberNoteVisibility: "記住貼文可見性"
@ -213,7 +213,7 @@ default: "預設"
defaultValueIs: "預設值:{value}" defaultValueIs: "預設值:{value}"
noCustomEmojis: "沒有自訂的表情符號" noCustomEmojis: "沒有自訂的表情符號"
noJobs: "沒有任務" noJobs: "沒有任務"
federating: "整合搜索中" federating: "聯邦運作中"
blocked: "已封鎖" blocked: "已封鎖"
suspended: "已凍結" suspended: "已凍結"
all: "全部" all: "全部"
@ -393,13 +393,19 @@ about: "關於"
aboutMisskey: "關於 Misskey" aboutMisskey: "關於 Misskey"
administrator: "管理員" administrator: "管理員"
token: "權杖" token: "權杖"
2fa: "雙因素驗證"
totp: "驗證應用程式"
totpDescription: "以驗證應用程式輸入一次性密碼"
moderator: "審查員" moderator: "審查員"
moderation: "審查" moderation: "審查"
nUsersMentioned: "提到了{n}" nUsersMentioned: "提到了{n}"
securityKeyAndPasskey: "安全金鑰・Passkey"
securityKey: "安全金鑰" securityKey: "安全金鑰"
lastUsed: "上次使用" lastUsed: "上次使用"
lastUsedAt: "最後使用:{t}"
unregister: "註銷帳號" unregister: "註銷帳號"
passwordLessLogin: "設置無密碼登入" passwordLessLogin: "設置無密碼登入"
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
resetPassword: "重置密碼" resetPassword: "重置密碼"
newPasswordIs: "新密碼為「{password}」" newPasswordIs: "新密碼為「{password}」"
reduceUiAnimation: "減少介面的動態視覺" reduceUiAnimation: "減少介面的動態視覺"
@ -451,6 +457,7 @@ aboutX: "關於{x}"
emojiStyle: "表情符號的風格" emojiStyle: "表情符號的風格"
native: "原生" native: "原生"
disableDrawer: "不顯示下拉式選單" disableDrawer: "不顯示下拉式選單"
showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項"
noHistory: "沒有歷史紀錄" noHistory: "沒有歷史紀錄"
signinHistory: "登入歷史" signinHistory: "登入歷史"
enableAdvancedMfm: "啟用高級MFM" enableAdvancedMfm: "啟用高級MFM"
@ -499,6 +506,7 @@ objectStorageSetPublicRead: "上傳時設定為\"public-read\""
serverLogs: "伺服器日誌" serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄" deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)"
newNoteRecived: "發現新的貼文" newNoteRecived: "發現新的貼文"
sounds: "音效" sounds: "音效"
sound: "音效" sound: "音效"
@ -773,6 +781,7 @@ popularPosts: "熱門的貼文"
shareWithNote: "在貼文中分享" shareWithNote: "在貼文中分享"
ads: "廣告" ads: "廣告"
expiration: "期限" expiration: "期限"
startingperiod: "開始期間"
memo: "備忘錄" memo: "備忘錄"
priority: "優先級" priority: "優先級"
high: "高" high: "高"
@ -805,6 +814,7 @@ lastCommunication: "最近的通信"
resolved: "已解決" resolved: "已解決"
unresolved: "未解決" unresolved: "未解決"
breakFollow: "移除追蹤者" breakFollow: "移除追蹤者"
breakFollowConfirm: "確定要取消被追隨嗎?"
itsOn: "已開啟" itsOn: "已開啟"
itsOff: "已關閉" itsOff: "已關閉"
emailRequiredForSignup: "註冊帳戶需要電子郵件地址" emailRequiredForSignup: "註冊帳戶需要電子郵件地址"
@ -839,11 +849,13 @@ instanceDefaultLightTheme: "實例預設的淺色主題"
instanceDefaultDarkTheme: "實例預設的深色主題" instanceDefaultDarkTheme: "實例預設的深色主題"
instanceDefaultThemeDescription: "輸入物件形式的主题代碼" instanceDefaultThemeDescription: "輸入物件形式的主题代碼"
mutePeriod: "靜音的期限" mutePeriod: "靜音的期限"
period: "期限"
indefinitely: "無期限" indefinitely: "無期限"
tenMinutes: "10分鐘" tenMinutes: "10分鐘"
oneHour: "1小時" oneHour: "1小時"
oneDay: "1天" oneDay: "1天"
oneWeek: "1週" oneWeek: "1週"
oneMonth: "1個月"
reflectMayTakeTime: "可能需要一些時間才會出現效果。" reflectMayTakeTime: "可能需要一些時間才會出現效果。"
failedToFetchAccountInformation: "取得帳戶資訊失敗" failedToFetchAccountInformation: "取得帳戶資訊失敗"
rateLimitExceeded: "已超過速率限制" rateLimitExceeded: "已超過速率限制"
@ -939,6 +951,13 @@ collapseRenotes: "省略顯示已看過的轉發貼文"
internalServerError: "內部伺服器錯誤" internalServerError: "內部伺服器錯誤"
internalServerErrorDescription: "內部伺服器發生了非預期的錯誤。" internalServerErrorDescription: "內部伺服器發生了非預期的錯誤。"
copyErrorInfo: "複製錯誤資訊" copyErrorInfo: "複製錯誤資訊"
joinThisServer: "在此伺服器上註冊"
exploreOtherServers: "探索其他伺服器"
letsLookAtTimeline: "看看時間軸"
disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。"
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
emailNotSupported: "這個伺服器不支援寄送郵件"
postToTheChannel: "發布到頻道"
_achievements: _achievements:
earnedAt: "獲得日期" earnedAt: "獲得日期"
_types: _types:
@ -1083,7 +1102,7 @@ _achievements:
title: "成群結隊" title: "成群結隊"
description: "跟隨者超過50人了" description: "跟隨者超過50人了"
_followers100: _followers100:
title: "紅人" title: "熱門人物"
description: "跟隨者超過100人了" description: "跟隨者超過100人了"
_followers300: _followers300:
title: "請排成一排" title: "請排成一排"
@ -1141,7 +1160,7 @@ _achievements:
description: "試圖遞迴套入雲端硬碟資料夾" description: "試圖遞迴套入雲端硬碟資料夾"
_reactWithoutRead: _reactWithoutRead:
title: "有好好讀過嗎?" title: "有好好讀過嗎?"
description: "對包含100字以上內容的貼文做出情感反應" description: "對包含100字以上內容的貼文在3秒以內做出反應"
_clickedClickHere: _clickedClickHere:
title: "點擊這裡" title: "點擊這裡"
description: "已點擊這裡了" description: "已點擊這裡了"
@ -1452,6 +1471,7 @@ _ago:
weeksAgo: "{n}周前" weeksAgo: "{n}周前"
monthsAgo: "{n}個月前" monthsAgo: "{n}個月前"
yearsAgo: "{n}年前" yearsAgo: "{n}年前"
invalid: "未發現"
_time: _time:
second: "秒" second: "秒"
minute: "分鐘" minute: "分鐘"
@ -1485,13 +1505,28 @@ _tutorial:
step8_3: "通知的設定可以在之後變更。" step8_3: "通知的設定可以在之後變更。"
_2fa: _2fa:
alreadyRegistered: "此設備已經被註冊過了" alreadyRegistered: "此設備已經被註冊過了"
registerTOTP: "開始設定驗證應用程式"
passwordToTOTP: "請輸入密碼"
step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。"
step2: "然後掃描螢幕上的QR code。" step2: "然後掃描螢幕上的QR code。"
step2Click: "點擊QR code可以使用設備上安裝的驗證應用程式或金鑰環進行註冊。"
step2Url: "在桌面版應用中請輸入以下的URL" step2Url: "在桌面版應用中請輸入以下的URL"
step3Title: "輸入驗證碼"
step3: "輸入您的App提供的權杖以完成設定。" step3: "輸入您的App提供的權杖以完成設定。"
step4: "從現在開始,任何登入操作都將要求您提供權杖。" step4: "從現在開始,任何登入操作都將要求您提供權杖。"
securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。"
registerTOTPBeforeKey: "要註冊安全金鑰・Passkey請先設定驗證應用程式。"
securityKeyInfo: "您可以設定使用支援FIDO2的硬體安全鎖、終端設備的指纹認證或者PIN碼來登入。" securityKeyInfo: "您可以設定使用支援FIDO2的硬體安全鎖、終端設備的指纹認證或者PIN碼來登入。"
chromePasskeyNotSupported: "目前不支援Chrome的Passkey。"
registerSecurityKey: "註冊安全金鑰・Passkey"
securityKeyName: "輸入金鑰名稱"
tapSecurityKey: "按照瀏覽器的說明操作註冊安全金鑰和Passkey。"
removeKey: "刪除安全金鑰"
removeKeyConfirm: "要刪除{name}嗎?" removeKeyConfirm: "要刪除{name}嗎?"
whyTOTPOnlyRenew: "如果註冊了安全金鑰,則無法解除驗證應用程式的設定。"
renewTOTP: "重設驗證應用程式"
renewTOTPConfirm: "目前驗證應用程式的驗證碼將無法使用。"
renewTOTPOk: "重設"
renewTOTPCancel: "現在不要" renewTOTPCancel: "現在不要"
_permissions: _permissions:
"read:account": "查看我的帳戶資訊" "read:account": "查看我的帳戶資訊"
@ -1564,7 +1599,7 @@ _widgets:
photos: "照片" photos: "照片"
digitalClock: "電子時鐘" digitalClock: "電子時鐘"
unixClock: "UNIX時間" unixClock: "UNIX時間"
federation: "聯邦宇宙" federation: "站台聯邦"
instanceCloud: "實例雲" instanceCloud: "實例雲"
postForm: "發佈窗口" postForm: "發佈窗口"
slideshow: "幻燈片" slideshow: "幻燈片"
@ -1615,6 +1650,8 @@ _visibility:
followersDescription: "僅發送至關注者" followersDescription: "僅發送至關注者"
specified: "指定使用者" specified: "指定使用者"
specifiedDescription: "僅發送至指定使用者" specifiedDescription: "僅發送至指定使用者"
disableFederation: "停用聯邦"
disableFederationDescription: "不要傳遞給其他實例"
_postForm: _postForm:
replyPlaceholder: "回覆此貼文..." replyPlaceholder: "回覆此貼文..."
quotePlaceholder: "引用此貼文..." quotePlaceholder: "引用此貼文..."
@ -1770,6 +1807,7 @@ _notification:
pollEnded: "問卷調查結束" pollEnded: "問卷調查結束"
receiveFollowRequest: "已收到追隨請求" receiveFollowRequest: "已收到追隨請求"
followRequestAccepted: "追隨請求已接受" followRequestAccepted: "追隨請求已接受"
achievementEarned: "獲得成就"
app: "應用程式通知" app: "應用程式通知"
_actions: _actions:
followBack: "回關" followBack: "回關"
@ -1802,3 +1840,6 @@ _deck:
channel: "頻道" channel: "頻道"
mentions: "提及" mentions: "提及"
direct: "指定使用者" direct: "指定使用者"
_dialog:
charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}"
charactersBelow: "低於最少字數!現在 {current} / 限制 {max}"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.7.5", "version": "13.9.2",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -55,11 +55,11 @@
"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.52.0", "@typescript-eslint/eslint-plugin": "5.53.0",
"@typescript-eslint/parser": "5.52.0", "@typescript-eslint/parser": "5.53.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.6.0", "cypress": "12.7.0",
"eslint": "8.34.0", "eslint": "8.35.0",
"start-server-and-test": "1.15.4" "start-server-and-test": "1.15.4"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -1,7 +1,7 @@
module.exports = { module.exports = {
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json', './test/tsconfig.json'],
}, },
extends: [ extends: [
'../shared/.eslintrc.js', '../shared/.eslintrc.js',

View file

@ -1,10 +1,10 @@
import {loadConfig} from './built/config.js'; import { loadConfig } from './built/config.js';
import {createRedisConnection} from "./built/redis.js"; import { createRedisConnection } from './built/redis.js';
const config = loadConfig(); const config = loadConfig();
const redis = createRedisConnection(config); const redis = createRedisConnection(config);
redis.on('connect', () => redis.disconnect()); redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => { redis.on('error', (e) => {
throw e; throw e;
}); });

View file

@ -20,7 +20,7 @@ module.exports = {
// collectCoverage: false, // collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected // An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ['src/**/*.ts'], collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'],
// The directory where Jest should output its coverage files // The directory where Jest should output its coverage files
coverageDirectory: "coverage", coverageDirectory: "coverage",
@ -91,7 +91,7 @@ module.exports = {
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225 // See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can // TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
// directly import `.ts` files without this hack. // directly import `.ts` files without this hack.
'^(\\.{1,2}/.*)\\.js$': '$1', '^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
}, },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@ -159,7 +159,8 @@ module.exports = {
// The glob patterns Jest uses to detect test files // The glob patterns Jest uses to detect test files
testMatch: [ testMatch: [
"<rootDir>/test/unit/**/*.ts", "<rootDir>/test/unit/**/*.ts",
//"<rootDir>/test/e2e/**/*.ts" "<rootDir>/src/**/*.test.ts",
"<rootDir>/test/e2e/**/*.ts",
], ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
@ -206,4 +207,13 @@ module.exports = {
// watchman: true, // watchman: true,
extensionsToTreatAsEsm: ['.ts'], extensionsToTreatAsEsm: ['.ts'],
testTimeout: 60000,
// Let Jest kill the test worker whenever it grows too much
// (It seems there's a known memory leak issue in Node.js' vm.Script used by Jest)
// https://github.com/facebook/jest/issues/11956
maxWorkers: 1, // Make it use worker (that can be killed and restarted)
logHeapUsage: true, // To debug when out-of-memory happens on CI
workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB)
}; };

View file

@ -0,0 +1,13 @@
export class roleAssignmentExpiresAt1677570181236 {
name = 'roleAssignmentExpiresAt1677570181236'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role_assignment" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`CREATE INDEX "IDX_539b6c08c05067599743bb6389" ON "role_assignment" ("expiresAt") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_539b6c08c05067599743bb6389"`);
await queryRunner.query(`ALTER TABLE "role_assignment" DROP COLUMN "expiresAt"`);
}
}

View file

@ -15,8 +15,8 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"", "eslint": "eslint --quiet \"src/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint", "lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand --detectOpenHandles", "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit",
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand --detectOpenHandles", "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest", "test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage" "test-and-coverage": "pnpm jest-and-coverage"
@ -80,7 +80,7 @@
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "12.5.3", "got": "12.5.3",
"happy-dom": "^8.7.0", "happy-dom": "8.9.0",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "4.28.5", "ioredis": "4.28.5",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
@ -88,7 +88,7 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "21.1.0", "jsdom": "21.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.1.0", "jsonld": "8.1.1",
"jsrsasign": "10.6.1", "jsrsasign": "10.6.1",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
@ -124,10 +124,11 @@
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"semver": "7.3.8", "semver": "7.3.8",
"sharp": "0.31.3", "sharp": "0.31.3",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"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.9", "systeminformation": "5.17.10",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.2",
@ -146,7 +147,6 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.4.3", "@jest/globals": "29.4.3",
"@redocly/openapi-core": "1.0.0-beta.123",
"@swc/jest": "0.2.24", "@swc/jest": "0.2.24",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.1", "@types/archiver": "5.3.1",
@ -156,7 +156,7 @@
"@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/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20", "@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "29.4.0", "@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
@ -164,7 +164,7 @@
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5", "@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "18.14.0", "@types/node": "18.14.1",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -183,18 +183,18 @@
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/unzipper": "0.10.5", "@types/unzipper": "0.10.5",
"@types/uuid": "9.0.0", "@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/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.52.0", "@typescript-eslint/eslint-plugin": "5.52.0",
"@typescript-eslint/parser": "5.52.0", "@typescript-eslint/parser": "5.53.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.34.0", "eslint": "8.35.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.4.3", "jest": "29.4.3",
"jest-mock": "29.4.3" "jest-mock": "29.4.3"
} }
} }

View file

@ -1,3 +1,4 @@
import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common'; import { Global, Inject, Module } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
@ -57,6 +58,14 @@ export class GlobalModule implements OnApplicationShutdown {
) {} ) {}
async onApplicationShutdown(signal: string): Promise<void> { async onApplicationShutdown(signal: string): Promise<void> {
if (process.env.NODE_ENV === 'test') {
// XXX:
// Shutting down the existing connections causes errors on Jest as
// Misskey has asynchronous postgres/redis connections that are not
// awaited.
// Let's wait for some random time for them to finish.
await setTimeout(5000);
}
await Promise.all([ await Promise.all([
this.db.destroy(), this.db.destroy(),
this.redisClient.disconnect(), this.redisClient.disconnect(),

View file

@ -16,12 +16,14 @@ export async function server() {
app.enableShutdownHooks(); app.enableShutdownHooks();
const serverService = app.get(ServerService); const serverService = app.get(ServerService);
serverService.launch(); await serverService.launch();
app.get(ChartManagementService).start(); app.get(ChartManagementService).start();
app.get(JanitorService).start(); app.get(JanitorService).start();
app.get(QueueStatsService).start(); app.get(QueueStatsService).start();
app.get(ServerStatsService).start(); app.get(ServerStatsService).start();
return app;
} }
export async function jobQueue() { export async function jobQueue() {

View file

@ -171,13 +171,15 @@ export class AntennaService implements OnApplicationShutdown {
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (keywords.length > 0) { if (keywords.length > 0) {
if (note.text == null) return false; if (note.text == null && note.cw == null) return false;
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
const matched = keywords.some(and => const matched = keywords.some(and =>
and.every(keyword => and.every(keyword =>
antenna.caseSensitive antenna.caseSensitive
? note.text!.includes(keyword) ? _text.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (!matched) return false; if (!matched) return false;
@ -189,13 +191,15 @@ export class AntennaService implements OnApplicationShutdown {
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (excludeKeywords.length > 0) { if (excludeKeywords.length > 0) {
if (note.text == null) return false; if (note.text == null && note.cw == null) return false;
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
const matched = excludeKeywords.some(and => const matched = excludeKeywords.some(and =>
and.every(keyword => and.every(keyword =>
antenna.caseSensitive antenna.caseSensitive
? note.text!.includes(keyword) ? _text.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (matched) return false; if (matched) return false;

View file

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js'; import type { Notification } from '@/models/entities/Notification.js';
@ -10,7 +11,9 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class CreateNotificationService { export class CreateNotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -40,11 +43,11 @@ export class CreateNotificationService {
if (data.notifierId && (notifieeId === data.notifierId)) { if (data.notifierId && (notifieeId === data.notifierId)) {
return null; return null;
} }
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type); const isMuted = profile?.mutingNotificationTypes.includes(type);
// Create notification // Create notification
const notification = await this.notificationsRepository.insert({ const notification = await this.notificationsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@ -56,18 +59,18 @@ export class CreateNotificationService {
...data, ...data,
} as Partial<Notification>) } as Partial<Notification>)
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
const packed = await this.notificationEntityService.pack(notification, {}); const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event // Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed); this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(async () => { setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
if (fresh == null) return; // 既に削除されているかもしれない if (fresh == null) return; // 既に削除されているかもしれない
if (fresh.isRead) return; if (fresh.isRead) return;
//#region ただしミュートしているユーザーからの通知なら無視 //#region ただしミュートしているユーザーからの通知なら無視
const mutings = await this.mutingsRepository.findBy({ const mutings = await this.mutingsRepository.findBy({
muterId: notifieeId, muterId: notifieeId,
@ -76,14 +79,14 @@ export class CreateNotificationService {
return; return;
} }
//#endregion //#endregion
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
}, 2000); }, () => { /* aborted, ignore it */ });
return notification; return notification;
} }
@ -103,7 +106,7 @@ export class CreateNotificationService {
sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/ */
} }
@bindThis @bindThis
private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) {
/* /*
@ -115,4 +118,8 @@ export class CreateNotificationService {
sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/ */
} }
onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
}
} }

View file

@ -6,6 +6,7 @@ import IPCIDR from 'ip-cidr';
import PrivateIp from 'private-ip'; 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 { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
@ -32,13 +33,18 @@ export class DownloadService {
} }
@bindThis @bindThis
public async downloadUrl(url: string, path: string): Promise<void> { public async downloadUrl(url: string, path: string): Promise<{
filename: string;
}> {
this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`); this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`);
const timeout = 30 * 1000; const timeout = 30 * 1000;
const operationTimeout = 60 * 1000; const operationTimeout = 60 * 1000;
const maxSize = this.config.maxFileSize ?? 262144000; const maxSize = this.config.maxFileSize ?? 262144000;
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
const req = got.stream(url, { const req = got.stream(url, {
headers: { headers: {
'User-Agent': this.config.userAgent, 'User-Agent': this.config.userAgent,
@ -77,6 +83,14 @@ export class DownloadService {
req.destroy(); req.destroy();
} }
} }
const contentDisposition = res.headers['content-disposition'];
if (contentDisposition != null) {
const parsed = parse(contentDisposition);
if (parsed.parameters.filename) {
filename = parsed.parameters.filename;
}
}
}).on('downloadProgress', (progress: Got.Progress) => { }).on('downloadProgress', (progress: Got.Progress) => {
if (progress.transferred > maxSize) { if (progress.transferred > maxSize) {
this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`); this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
@ -95,6 +109,10 @@ export class DownloadService {
} }
this.logger.succ(`Download finished: ${chalk.cyan(url)}`); this.logger.succ(`Download finished: ${chalk.cyan(url)}`);
return {
filename,
};
} }
@bindThis @bindThis

View file

@ -34,6 +34,7 @@ import { FileInfoService } from '@/core/FileInfoService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type S3 from 'aws-sdk/clients/s3.js'; import type S3 from 'aws-sdk/clients/s3.js';
import { correctFilename } from '@/misc/correct-filename.js';
type AddFileArgs = { type AddFileArgs = {
/** User who wish to add file */ /** User who wish to add file */
@ -168,7 +169,7 @@ export class DriveService {
//#region Uploads //#region Uploads
this.registerLogger.info(`uploading original: ${key}`); this.registerLogger.info(`uploading original: ${key}`);
const uploads = [ const uploads = [
this.upload(key, fs.createReadStream(path), type, name), this.upload(key, fs.createReadStream(path), type, ext, name),
]; ];
if (alts.webpublic) { if (alts.webpublic) {
@ -176,7 +177,7 @@ export class DriveService {
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, name)); uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name));
} }
if (alts.thumbnail) { if (alts.thumbnail) {
@ -184,7 +185,7 @@ export class DriveService {
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type)); uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext));
} }
await Promise.all(uploads); await Promise.all(uploads);
@ -360,7 +361,7 @@ export class DriveService {
* Upload to ObjectStorage * Upload to ObjectStorage
*/ */
@bindThis @bindThis
private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, ext?: string | null, filename?: string) {
if (type === 'image/apng') type = 'image/png'; if (type === 'image/apng') type = 'image/png';
if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
@ -374,7 +375,12 @@ export class DriveService {
CacheControl: 'max-age=31536000, immutable', CacheControl: 'max-age=31536000, immutable',
} as S3.PutObjectRequest; } as S3.PutObjectRequest;
if (filename) params.ContentDisposition = contentDisposition('inline', filename); if (filename) params.ContentDisposition = contentDisposition(
'inline',
// 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、
// 許可されているファイル形式でしか拡張子をつけない
ext ? correctFilename(filename, ext) : filename,
);
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
const s3 = this.s3Service.getS3(meta); const s3 = this.s3Service.getS3(meta);
@ -466,7 +472,12 @@ export class DriveService {
//} //}
// detect name // detect name
const detectedName = name ?? (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); const detectedName = correctFilename(
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
// extを付加してデータベースの文字数制限に当たることはまずない
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
info.type.ext
);
if (user && !force) { if (user && !force) {
// Check if there is a file with the same hash // Check if there is a file with the same hash
@ -736,24 +747,19 @@ export class DriveService {
requestIp = null, requestIp = null,
requestHeaders = null, requestHeaders = null,
}: UploadFromUrlArgs): Promise<DriveFile> { }: UploadFromUrlArgs): Promise<DriveFile> {
let name = new URL(url).pathname.split('/').pop() ?? null;
if (name == null || !this.driveFileEntityService.validateFileName(name)) {
name = null;
}
// If the comment is same as the name, skip comment
// (image.name is passed in when receiving attachment)
if (comment !== null && name === comment) {
comment = null;
}
// Create temp file // Create temp file
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();
try { try {
// write content at URL to temp file // write content at URL to temp file
await this.downloadService.downloadUrl(url, path); const { filename: name } = await this.downloadService.downloadUrl(url, path);
// If the comment is same as the name, skip comment
// (image.name is passed in when receiving attachment)
if (comment !== null && name === comment) {
comment = null;
}
const driveFile = await this.addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders }); const driveFile = await this.addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders });
this.downloaderLogger.succ(`Got: ${driveFile.id}`); this.downloaderLogger.succ(`Got: ${driveFile.id}`);
return driveFile!; return driveFile!;

View file

@ -1,6 +1,7 @@
import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm'; import { In, DataSource } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js'; import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
@ -137,7 +138,9 @@ type Option = {
}; };
@Injectable() @Injectable()
export class NoteCreateService { export class NoteCreateService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@ -313,7 +316,10 @@ export class NoteCreateService {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
setImmediate(() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!)); setImmediate('post created', { signal: this.#shutdownController.signal }).then(
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ },
);
return note; return note;
} }
@ -756,4 +762,8 @@ export class NoteCreateService {
return mentionedUsers; return mentionedUsers;
} }
onApplicationShutdown(signal?: string | undefined) {
this.#shutdownController.abort();
}
} }

View file

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In, IsNull, Not } from 'typeorm'; import { In, IsNull, Not } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
@ -15,7 +16,9 @@ import { AntennaService } from './AntennaService.js';
import { PushNotificationService } from './PushNotificationService.js'; import { PushNotificationService } from './PushNotificationService.js';
@Injectable() @Injectable()
export class NoteReadService { export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -60,14 +63,14 @@ export class NoteReadService {
}); });
if (mute.map(m => m.muteeId).includes(note.userId)) return; if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion //#endregion
// スレッドミュート // スレッドミュート
const threadMute = await this.noteThreadMutingsRepository.findOneBy({ const threadMute = await this.noteThreadMutingsRepository.findOneBy({
userId: userId, userId: userId,
threadId: note.threadId ?? note.id, threadId: note.threadId ?? note.id,
}); });
if (threadMute) return; if (threadMute) return;
const unread = { const unread = {
id: this.idService.genId(), id: this.idService.genId(),
noteId: note.id, noteId: note.id,
@ -77,15 +80,15 @@ export class NoteReadService {
noteChannelId: note.channelId, noteChannelId: note.channelId,
noteUserId: note.userId, noteUserId: note.userId,
}; };
await this.noteUnreadsRepository.insert(unread); await this.noteUnreadsRepository.insert(unread);
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(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.findOneBy({ id: unread.id });
if (exist == null) return; if (exist == null) return;
if (params.isMentioned) { if (params.isMentioned) {
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
} }
@ -95,8 +98,8 @@ export class NoteReadService {
if (note.channelId) { if (note.channelId) {
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id); this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
} }
}, 2000); }, () => { /* aborted, ignore it */ });
} }
@bindThis @bindThis
public async read( public async read(
@ -113,24 +116,24 @@ export class NoteReadService {
}, },
select: ['followeeId'], select: ['followeeId'],
})).map(x => x.followeeId)); })).map(x => x.followeeId));
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | Packed<'Note'>)[] = []; const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = []; const readChannelNotes: (Note | Packed<'Note'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = []; const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) { for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) { if (note.mentions && note.mentions.includes(userId)) {
readMentions.push(note); readMentions.push(note);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note); readSpecifiedNotes.push(note);
} }
if (note.channelId && followingChannels.has(note.channelId)) { if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note); readChannelNotes.push(note);
} }
if (note.user != null) { // たぶんnullになることは無いはずだけど一応 if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) { for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) { if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
@ -139,14 +142,14 @@ export class NoteReadService {
} }
} }
} }
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
// Remove the record // Remove the record
await this.noteUnreadsRepository.delete({ await this.noteUnreadsRepository.delete({
userId: userId, userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
}); });
// TODO: ↓まとめてクエリしたい // TODO: ↓まとめてクエリしたい
this.noteUnreadsRepository.countBy({ this.noteUnreadsRepository.countBy({
@ -183,7 +186,7 @@ export class NoteReadService {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
}); });
} }
if (readAntennaNotes.length > 0) { if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({ await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)), antennaId: In(myAntennas.map(a => a.id)),
@ -191,14 +194,14 @@ export class NoteReadService {
}, { }, {
read: true, read: true,
}); });
// TODO: まとめてクエリしたい // TODO: まとめてクエリしたい
for (const antenna of myAntennas) { for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({ const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id, antennaId: antenna.id,
read: false, read: false,
}); });
if (count === 0) { if (count === 0) {
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna); this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
@ -213,4 +216,8 @@ export class NoteReadService {
}); });
} }
} }
onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
}
} }

View file

@ -11,6 +11,8 @@ import { UserCacheService } from '@/core/UserCacheService.js';
import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; 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 { GlobalEventService } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = { export type RolePolicies = {
@ -56,6 +58,9 @@ export class RoleService implements OnApplicationShutdown {
private rolesCache: Cache<Role[]>; private rolesCache: Cache<Role[]>;
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>; private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis, private redisSubscriber: Redis.Redis,
@ -72,6 +77,8 @@ export class RoleService implements OnApplicationShutdown {
private metaService: MetaService, private metaService: MetaService,
private userCacheService: UserCacheService, private userCacheService: UserCacheService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private idService: IdService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
@ -128,6 +135,7 @@ export class RoleService implements OnApplicationShutdown {
cached.push({ cached.push({
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
}); });
} }
break; break;
@ -193,7 +201,10 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async getUserRoles(userId: User['id']) { public async getUserRoles(userId: User['id']) {
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); const now = Date.now();
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
// 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
@ -207,7 +218,10 @@ export class RoleService implements OnApplicationShutdown {
*/ */
@bindThis @bindThis
public async getUserBadgeRoles(userId: User['id']) { public async getUserBadgeRoles(userId: User['id']) {
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); const now = Date.now();
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
// 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
@ -316,6 +330,65 @@ export class RoleService implements OnApplicationShutdown {
return users; return users;
} }
@bindThis
public async assign(userId: User['id'], roleId: Role['id'], expiresAt: Date | null = null): Promise<void> {
const now = new Date();
const existing = await this.roleAssignmentsRepository.findOneBy({
roleId: roleId,
userId: userId,
});
if (existing) {
if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) {
await this.roleAssignmentsRepository.delete({
roleId: roleId,
userId: userId,
});
} else {
throw new RoleService.AlreadyAssignedError();
}
}
const created = await this.roleAssignmentsRepository.insert({
id: this.idService.genId(),
createdAt: now,
expiresAt: expiresAt,
roleId: roleId,
userId: userId,
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
this.rolesRepository.update(roleId, {
lastUsedAt: new Date(),
});
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
}
@bindThis
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
const now = new Date();
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
if (existing == null) {
throw new RoleService.NotAssignedError();
} else if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) {
await this.roleAssignmentsRepository.delete({
roleId: roleId,
userId: userId,
});
throw new RoleService.NotAssignedError();
}
await this.roleAssignmentsRepository.delete(existing.id);
this.rolesRepository.update(roleId, {
lastUsedAt: now,
});
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
}
@bindThis @bindThis
public onApplicationShutdown(signal?: string | undefined) { public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage); this.redisSubscriber.off('message', this.onMessage);

View file

@ -47,6 +47,7 @@ export class WebhookService implements OnApplicationShutdown {
this.webhooks.push({ this.webhooks.push({
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
}); });
} }
break; break;
@ -57,11 +58,13 @@ export class WebhookService implements OnApplicationShutdown {
this.webhooks[i] = { this.webhooks[i] = {
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
}; };
} else { } else {
this.webhooks.push({ this.webhooks.push({
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
}); });
} }
} else { } else {

View file

@ -62,8 +62,10 @@ export class ChartManagementService implements OnApplicationShutdown {
async onApplicationShutdown(signal: string): Promise<void> { async onApplicationShutdown(signal: string): Promise<void> {
clearInterval(this.saveIntervalId); clearInterval(this.saveIntervalId);
await Promise.all( if (process.env.NODE_ENV !== 'test') {
this.charts.map(chart => chart.save()), await Promise.all(
); this.charts.map(chart => chart.save()),
);
}
} }
} }

View file

@ -45,8 +45,8 @@ export default class PerUserNotesChart extends Chart<typeof schema> {
} }
@bindThis @bindThis
public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise<void> { public update(user: { id: User['id'] }, note: Note, isAdditional: boolean): void {
await this.commit({ this.commit({
'total': isAdditional ? 1 : -1, 'total': isAdditional ? 1 : -1,
'inc': isAdditional ? 1 : 0, 'inc': isAdditional ? 1 : 0,
'dec': isAdditional ? 0 : 1, 'dec': isAdditional ? 0 : 1,

View file

@ -1,5 +1,5 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm'; import { DataSource, In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import type { NotesRepository, DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -21,6 +21,7 @@ type PackOptions = {
}; };
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import { isNotNull } from '@/misc/is-not-null.js';
@Injectable() @Injectable()
export class DriveFileEntityService { export class DriveFileEntityService {
@ -255,10 +256,33 @@ export class DriveFileEntityService {
@bindThis @bindThis
public async packMany( public async packMany(
files: (DriveFile['id'] | DriveFile)[], files: DriveFile[],
options?: PackOptions, options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> { ): Promise<Packed<'DriveFile'>[]> {
const items = await Promise.all(files.map(f => this.packNullable(f, options))); const items = await Promise.all(files.map(f => this.packNullable(f, options)));
return items.filter((x): x is Packed<'DriveFile'> => x != null); return items.filter((x): x is Packed<'DriveFile'> => x != null);
} }
@bindThis
public async packManyByIdsMap(
fileIds: DriveFile['id'][],
options?: PackOptions,
): Promise<Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>> {
const files = await this.driveFilesRepository.findBy({ id: In(fileIds) });
const packedFiles = await this.packMany(files, options);
const map = new Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>(packedFiles.map(f => [f.id, f]));
for (const id of fileIds) {
if (!map.has(id)) map.set(id, null);
}
return map;
}
@bindThis
public async packManyByIds(
fileIds: DriveFile['id'][],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
const filesMap = await this.packManyByIdsMap(fileIds, options);
return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
}
} }

View file

@ -41,7 +41,8 @@ export class GalleryPostEntityService {
title: post.title, title: post.title,
description: post.description, description: post.description,
fileIds: post.fileIds, fileIds: post.fileIds,
files: this.driveFileEntityService.packMany(post.fileIds), // TODO: packMany causes N+1 queries
files: this.driveFileEntityService.packManyByIds(post.fileIds),
tags: post.tags.length > 0 ? post.tags : undefined, tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive, isSensitive: post.isSensitive,
likedCount: post.likedCount, likedCount: post.likedCount,

View file

@ -11,6 +11,7 @@ import type { Note } from '@/models/entities/Note.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, DriveFilesRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js'; import type { ReactionService } from '../ReactionService.js';
@ -248,6 +249,21 @@ export class NoteEntityService implements OnModuleInit {
return true; return true;
} }
@bindThis
public async packAttachedFiles(fileIds: Note['fileIds'], packedFiles: Map<Note['fileIds'][number], Packed<'DriveFile'> | null>): Promise<Packed<'DriveFile'>[]> {
const missingIds = [];
for (const id of fileIds) {
if (!packedFiles.has(id)) missingIds.push(id);
}
if (missingIds.length) {
const additionalMap = await this.driveFileEntityService.packManyByIdsMap(missingIds);
for (const [k, v] of additionalMap) {
packedFiles.set(k, v);
}
}
return fileIds.map(id => packedFiles.get(id)).filter(isNotNull);
}
@bindThis @bindThis
public async pack( public async pack(
src: Note['id'] | Note, src: Note['id'] | Note,
@ -257,6 +273,7 @@ export class NoteEntityService implements OnModuleInit {
skipHide?: boolean; skipHide?: boolean;
_hint_?: { _hint_?: {
myReactions: Map<Note['id'], NoteReaction | null>; myReactions: Map<Note['id'], NoteReaction | null>;
packedFiles: Map<Note['fileIds'][number], Packed<'DriveFile'> | null>;
}; };
}, },
): Promise<Packed<'Note'>> { ): Promise<Packed<'Note'>> {
@ -284,6 +301,7 @@ export class NoteEntityService implements OnModuleInit {
const reactionEmojiNames = Object.keys(note.reactions) const reactionEmojiNames = Object.keys(note.reactions)
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packedFiles = options?._hint_?.packedFiles;
const packed: Packed<'Note'> = await awaitAll({ const packed: Packed<'Note'> = await awaitAll({
id: note.id, id: note.id,
@ -304,7 +322,7 @@ export class NoteEntityService implements OnModuleInit {
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined, tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds, fileIds: note.fileIds,
files: this.driveFileEntityService.packMany(note.fileIds), files: packedFiles != null ? this.packAttachedFiles(note.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(note.fileIds),
replyId: note.replyId, replyId: note.replyId,
renoteId: note.renoteId, renoteId: note.renoteId,
channelId: note.channelId ?? undefined, channelId: note.channelId ?? undefined,
@ -388,11 +406,15 @@ export class NoteEntityService implements OnModuleInit {
} }
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes)); await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
return await Promise.all(notes.map(n => this.pack(n, me, { return await Promise.all(notes.map(n => this.pack(n, me, {
...options, ...options,
_hint_: { _hint_: {
myReactions: myReactionsMap, myReactions: myReactionsMap,
packedFiles,
}, },
}))); })));
} }

View file

@ -1,19 +1,21 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In } 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 { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js'; import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js'; import type { Notification } from '@/models/entities/Notification.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { notificationTypes } from '@/types.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
@Injectable() @Injectable()
export class NotificationEntityService implements OnModuleInit { export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
@ -48,13 +50,20 @@ export class NotificationEntityService implements OnModuleInit {
public async pack( public async pack(
src: Notification['id'] | Notification, src: Notification['id'] | Notification,
options: { options: {
_hintForEachNotes_?: { _hint_?: {
myReactions: Map<Note['id'], NoteReaction | null>; packedNotes: Map<Note['id'], Packed<'Note'>>;
}; };
}, },
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src }); const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src });
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null; const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
options._hint_?.packedNotes != null
? options._hint_.packedNotes.get(notification.noteId)
: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
})
) : undefined;
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
@ -63,43 +72,10 @@ export class NotificationEntityService implements OnModuleInit {
isRead: notification.isRead, isRead: notification.isRead,
userId: notification.notifierId, userId: notification.notifierId,
user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null, user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null,
...(notification.type === 'mention' ? { ...(noteIfNeed != null ? { note: noteIfNeed } : {}),
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'reply' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'renote' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'quote' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'reaction' ? { ...(notification.type === 'reaction' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
reaction: notification.reaction, reaction: notification.reaction,
} : {}), } : {}),
...(notification.type === 'pollEnded' ? {
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'achievementEarned' ? { ...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement, achievement: notification.achievement,
} : {}), } : {}),
@ -111,32 +87,32 @@ export class NotificationEntityService implements OnModuleInit {
}); });
} }
/**
* @param notifications you should join "note" property when fetch from DB, and all notifieeId should be same as meId
*/
@bindThis @bindThis
public async packMany( public async packMany(
notifications: Notification[], notifications: Notification[],
meId: User['id'], meId: User['id'],
) { ) {
if (notifications.length === 0) return []; if (notifications.length === 0) return [];
const notes = notifications.filter(x => x.note != null).map(x => x.note!); for (const notification of notifications) {
const noteIds = notes.map(n => n.id); if (meId !== notification.notifieeId) {
const myReactionsMap = new Map<Note['id'], NoteReaction | null>(); // because we call note packMany with meId, all notifieeId should be same as meId
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); throw new Error('TRY_TO_PACK_ANOTHER_USER_NOTIFICATION');
const targets = [...noteIds, ...renoteIds]; }
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
});
for (const target of targets) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
} }
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes)); const notes = notifications.map(x => x.note).filter(isNotNull);
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
detail: true,
});
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
return await Promise.all(notifications.map(x => this.pack(x, { return await Promise.all(notifications.map(x => this.pack(x, {
_hintForEachNotes_: { _hint_: {
myReactions: myReactionsMap, packedNotes,
}, },
}))); })));
} }

View file

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
@ -28,9 +29,13 @@ export class RoleEntityService {
) { ) {
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
const assigns = await this.roleAssignmentsRepository.findBy({ const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
roleId: role.id, .where('assign.roleId = :roleId', { roleId: role.id })
}); .andWhere(new Brackets(qb => { qb
.where('assign.expiresAt IS NULL')
.orWhere('assign.expiresAt > :now', { now: new Date() });
}))
.getCount();
const policies = { ...role.policies }; const policies = { ...role.policies };
for (const [k, v] of Object.entries(DEFAULT_POLICIES)) { for (const [k, v] of Object.entries(DEFAULT_POLICIES)) {
@ -57,7 +62,7 @@ export class RoleEntityService {
asBadge: role.asBadge, asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies, policies: policies,
usersCount: assigns.length, usersCount: assignedCount,
}); });
} }

View file

@ -278,27 +278,27 @@ export class UserEntityService implements OnModuleInit {
@bindThis @bindThis
public async getAvatarUrl(user: User): Promise<string> { public async getAvatarUrl(user: User): Promise<string> {
if (user.avatar) { if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id); return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user);
} else if (user.avatarId) { } else if (user.avatarId) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user.id); return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user);
} else { } else {
return this.getIdenticonUrl(user.id); return this.getIdenticonUrl(user);
} }
} }
@bindThis @bindThis
public getAvatarUrlSync(user: User): string { public getAvatarUrlSync(user: User): string {
if (user.avatar) { if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id); return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user);
} else { } else {
return this.getIdenticonUrl(user.id); return this.getIdenticonUrl(user);
} }
} }
@bindThis @bindThis
public getIdenticonUrl(userId: User['id']): string { public getIdenticonUrl(user: User): string {
return `${this.config.url}/identicon/${userId}`; return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
} }
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>( public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(

View file

@ -0,0 +1,15 @@
// 与えられた拡張子とファイル名が一致しているかどうかを確認し、
// 一致していない場合は拡張子を付与して返す
export function correctFilename(filename: string, ext: string | null) {
const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown';
if (filename.endsWith(dotExt)) {
return filename;
}
if (ext === 'jpg' && filename.endsWith('.jpeg')) {
return filename;
}
if (ext === 'tif' && filename.endsWith('.tiff')) {
return filename;
}
return `${filename}${dotExt}`;
}

View file

@ -4,6 +4,8 @@ const dictionary = {
'safe-file': FILE_TYPE_BROWSERSAFE, 'safe-file': FILE_TYPE_BROWSERSAFE,
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'], 'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'], 'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
'sharp-convertible-image-with-bmp': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
'sharp-animation-convertible-image-with-bmp': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
}; };
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime); export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);

View file

@ -0,0 +1,5 @@
// we are using {} as "any non-nullish value" as expected
// eslint-disable-next-line @typescript-eslint/ban-types
export function isNotNull<T extends {}>(input: T | undefined | null): input is T {
return input != null;
}

View file

@ -116,10 +116,10 @@ export type Obj = Record<string, Schema>;
// https://github.com/misskey-dev/misskey/issues/8535 // https://github.com/misskey-dev/misskey/issues/8535
// To avoid excessive stack depth error, // To avoid excessive stack depth error,
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). // deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
export type ObjType<s extends Obj, RequiredProps extends keyof s> = export type ObjType<s extends Obj, RequiredProps extends ReadonlyArray<keyof s>> =
UnionToIntersection< UnionToIntersection<
{ -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]> } & { -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]> } &
{ -readonly [R in RequiredProps]-?: SchemaType<s[R]> } & { -readonly [R in RequiredProps[number]]-?: SchemaType<s[R]> } &
{ -readonly [P in keyof s]?: SchemaType<s[P]> } { -readonly [P in keyof s]?: SchemaType<s[P]> }
>; >;
@ -136,18 +136,19 @@ type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
// To get union, we use `Foo extends any ? Hoge<Foo> : never` // To get union, we use `Foo extends any ? Hoge<Foo> : never`
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never; type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never;
type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never; //type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never; type ArrayUnion<T> = T extends any ? Array<T> : never;
type ObjectSchemaTypeDef<p extends Schema> = type ObjectSchemaTypeDef<p extends Schema> =
p['ref'] extends keyof typeof refs ? Packed<p['ref']> : p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
p['properties'] extends NonNullable<Obj> ? p['properties'] extends NonNullable<Obj> ?
p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
: : never
ObjType<p['properties'], NonNullable<p['required']>[number]> : ObjType<p['properties'], NonNullable<p['required']>>
: :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> : p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any any

View file

@ -39,4 +39,10 @@ export class RoleAssignment {
}) })
@JoinColumn() @JoinColumn()
public role: Role | null; public role: Role | null;
@Index()
@Column('timestamp with time zone', {
nullable: true,
})
public expiresAt: Date | null;
} }

View file

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { LessThan } from 'typeorm'; import { In, LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, UserIpsRepository } from '@/models/index.js'; import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.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 { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -29,6 +29,9 @@ export class CleanProcessorService {
@Inject(DI.antennaNotesRepository) @Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository, private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private idService: IdService, private idService: IdService,
) { ) {
@ -56,6 +59,17 @@ export class CleanProcessorService {
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
}); });
const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign')
.where('assign.expiresAt IS NOT NULL')
.andWhere('assign.expiresAt < :now', { now: new Date() })
.getMany();
if (expiredRoleAssignments.length > 0) {
await this.roleAssignmentsRepository.delete({
id: In(expiredRoleAssignments.map(x => x.id)),
});
}
this.logger.succ('Cleaned.'); this.logger.succ('Cleaned.');
done(); done();
} }

View file

@ -22,6 +22,8 @@ import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import sharp from 'sharp'; import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { correctFilename } from '@/misc/correct-filename.js';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@ -51,15 +53,6 @@ export class FileServerService {
//this.createServer = this.createServer.bind(this); //this.createServer = this.createServer.bind(this);
} }
@bindThis
public commonReadableHandlerGenerator(reply: FastifyReply) {
return (err: Error): void => {
this.logger.error(err);
reply.code(500);
reply.header('Cache-Control', 'max-age=300');
};
}
@bindThis @bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addHook('onRequest', (request, reply, done) => { fastify.addHook('onRequest', (request, reply, done) => {
@ -140,7 +133,7 @@ export class FileServerService {
let image: IImageStreamable | null = null; let image: IImageStreamable | null = null;
if (file.fileRole === 'thumbnail') { if (file.fileRole === 'thumbnail') {
if (isMimeImage(file.mime, 'sharp-convertible-image')) { if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/static.webp`); const url = new URL(`${this.config.mediaProxy}/static.webp`);
@ -190,13 +183,19 @@ export class FileServerService {
} }
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext)
)
);
return image.data; return image.data;
} }
if (file.fileRole !== 'original') { if (file.fileRole !== 'original') {
const filename = rename(file.file.name, { const filename = rename(file.filename, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web', suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : undefined, extname: file.ext ? `.${file.ext}` : '.unknown',
}).toString(); }).toString();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
@ -204,12 +203,10 @@ export class FileServerService {
reply.header('Content-Disposition', contentDisposition('inline', filename)); reply.header('Content-Disposition', contentDisposition('inline', filename));
return fs.createReadStream(file.path); return fs.createReadStream(file.path);
} else { } else {
const stream = fs.createReadStream(file.path);
stream.on('error', this.commonReadableHandlerGenerator(reply));
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.file.name)); reply.header('Content-Disposition', contentDisposition('inline', file.filename));
return stream; return fs.createReadStream(file.path);
} }
} catch (e) { } catch (e) {
if ('cleanup' in file) file.cleanup(); if ('cleanup' in file) file.cleanup();
@ -226,7 +223,10 @@ export class FileServerService {
return; return;
} }
if (this.config.externalMediaProxyEnabled) { // アバタークロップなど、どうしてもオリジンである必要がある場合
const mustOrigin = 'origin' in request.query;
if (this.config.externalMediaProxyEnabled && !mustOrigin) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト // 外部のメディアプロキシが有効なら、そちらにリダイレクト
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
@ -258,8 +258,8 @@ export class FileServerService {
} }
try { try {
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image'); const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image'); const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
if ( if (
'emoji' in request.query || 'emoji' in request.query ||
@ -283,7 +283,7 @@ export class FileServerService {
type: file.mime, type: file.mime,
}; };
} else { } else {
const data = sharp(file.path, { animated: !('static' in request.query) }) const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
.resize({ .resize({
height: 'emoji' in request.query ? 128 : 320, height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true, withoutEnlargement: true,
@ -297,11 +297,11 @@ export class FileServerService {
}; };
} }
} else if ('static' in request.query) { } else if ('static' in request.query) {
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280); image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 280);
} else if ('preview' in request.query) { } else if ('preview' in request.query) {
image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200); image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
} else if ('badge' in request.query) { } else if ('badge' in request.query) {
const mask = sharp(file.path) const mask = (await sharpBmp(file.path, file.mime))
.resize(96, 96, { .resize(96, 96, {
fit: 'inside', fit: 'inside',
withoutEnlargement: false, withoutEnlargement: false,
@ -357,6 +357,12 @@ export class FileServerService {
reply.header('Content-Type', image.type); reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext)
)
);
return image.data; return image.data;
} catch (e) { } catch (e) {
if ('cleanup' in file) file.cleanup(); if ('cleanup' in file) file.cleanup();
@ -366,8 +372,8 @@ export class FileServerService {
@bindThis @bindThis
private async getStreamAndTypeFromUrl(url: string): Promise< private async getStreamAndTypeFromUrl(url: string): Promise<
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; } { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; } | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404' | '404'
| '204' | '204'
> { > {
@ -383,11 +389,11 @@ export class FileServerService {
@bindThis @bindThis
private async downloadAndDetectTypeFromUrl(url: string): Promise< private async downloadAndDetectTypeFromUrl(url: string): Promise<
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; } { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
> { > {
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();
try { try {
await this.downloadService.downloadUrl(url, path); const { filename } = await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path); const { mime, ext } = await this.fileInfoService.detectType(path);
@ -395,6 +401,7 @@ export class FileServerService {
state: 'remote', state: 'remote',
mime, ext, mime, ext,
path, cleanup, path, cleanup,
filename,
}; };
} catch (e) { } catch (e) {
cleanup(); cleanup();
@ -404,8 +411,8 @@ export class FileServerService {
@bindThis @bindThis
private async getFileFromKey(key: string): Promise< private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; } | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404' | '404'
| '204' | '204'
> { > {
@ -429,6 +436,7 @@ export class FileServerService {
url: file.uri, url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file, file,
filename: file.name,
}; };
} }
@ -440,6 +448,7 @@ export class FileServerService {
state: 'stored_internal', state: 'stored_internal',
fileRole: isThumbnail ? 'thumbnail' : 'webpublic', fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
file, file,
filename: file.name,
mime, ext, mime, ext,
path, path,
}; };
@ -449,6 +458,7 @@ export class FileServerService {
state: 'stored_internal', state: 'stored_internal',
fileRole: 'original', fileRole: 'original',
file, file,
filename: file.name,
mime: file.type, mime: file.type,
ext: null, ext: null,
path, path,

View file

@ -1,7 +1,7 @@
import cluster from 'node:cluster'; import cluster from 'node:cluster';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import Fastify from 'fastify'; import Fastify, { FastifyInstance } from 'fastify';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -23,8 +23,9 @@ import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js'; import { ClientServerService } from './web/ClientServerService.js';
@Injectable() @Injectable()
export class ServerService { export class ServerService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
#fastify: FastifyInstance;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -54,11 +55,12 @@ export class ServerService {
} }
@bindThis @bindThis
public launch() { public async launch() {
const fastify = Fastify({ const fastify = Fastify({
trustProxy: true, trustProxy: true,
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''),
}); });
this.#fastify = fastify;
// HSTS // HSTS
// 6months (15552000sec) // 6months (15552000sec)
@ -75,7 +77,7 @@ export class ServerService {
fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer); fastify.register(this.wellKnownServerService.createServer);
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path; const path = request.params.path;
reply.header('Cache-Control', 'public, max-age=86400'); reply.header('Cache-Control', 'public, max-age=86400');
@ -105,11 +107,19 @@ export class ServerService {
} }
} }
const url = new URL(`${this.config.mediaProxy}/emoji.webp`); let url: URL;
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) if ('badge' in request.query) {
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); url = new URL(`${this.config.mediaProxy}/emoji.png`);
url.searchParams.set('emoji', '1'); // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
if ('static' in request.query) url.searchParams.set('static', '1'); url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('badge', '1');
} else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1');
if ('static' in request.query) url.searchParams.set('static', '1');
}
return await reply.redirect( return await reply.redirect(
301, 301,
@ -195,5 +205,11 @@ export class ServerService {
}); });
fastify.listen({ port: this.config.port, host: '0.0.0.0' }); fastify.listen({ port: this.config.port, host: '0.0.0.0' });
await fastify.ready();
}
async onApplicationShutdown(signal: string): Promise<void> {
await this.#fastify.close();
} }
} }

View file

@ -2,6 +2,7 @@ import { pipeline } from 'node:stream';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js'; import { getIpHash } from '@/misc/get-ip-hash.js';
import type { LocalUser, User } from '@/models/entities/User.js'; import type { LocalUser, User } from '@/models/entities/User.js';
@ -99,9 +100,12 @@ export class ApiCallService implements OnApplicationShutdown {
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>, request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
reply: FastifyReply, reply: FastifyReply,
) { ) {
const multipartData = await request.file(); const multipartData = await request.file().catch(() => {
/* Fastify throws if the remote didn't send multipart data. Return 400 below. */
});
if (multipartData == null) { if (multipartData == null) {
reply.code(400); reply.code(400);
reply.send();
return; return;
} }
@ -320,6 +324,7 @@ export class ApiCallService implements OnApplicationShutdown {
if (err instanceof ApiError) { if (err instanceof ApiError) {
throw err; throw err;
} else { } else {
const errId = uuid();
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
ep: ep.name, ep: ep.name,
ps: data, ps: data,
@ -327,14 +332,15 @@ export class ApiCallService implements OnApplicationShutdown {
message: err.message, message: err.message,
code: err.name, code: err.name,
stack: err.stack, stack: err.stack,
id: errId,
}, },
}); });
console.error(err); console.error(err, errId);
throw new ApiError(null, { throw new ApiError(null, {
e: { e: {
message: err.message, message: err.message,
code: err.name, code: err.name,
stack: err.stack, id: errId,
}, },
}); });
} }

View file

@ -73,28 +73,32 @@ export class ApiServerService {
Params: { endpoint: string; }, Params: { endpoint: string; },
Body: Record<string, unknown>, Body: Record<string, unknown>,
Querystring: Record<string, unknown>, Querystring: Record<string, unknown>,
}>('/' + endpoint.name, (request, reply) => { }>('/' + endpoint.name, async (request, reply) => {
if (request.method === 'GET' && !endpoint.meta.allowGet) { if (request.method === 'GET' && !endpoint.meta.allowGet) {
reply.code(405); reply.code(405);
reply.send(); reply.send();
return; return;
} }
this.apiCallService.handleMultipartRequest(ep, request, reply); // Await so that any error can automatically be translated to HTTP 500
await this.apiCallService.handleMultipartRequest(ep, request, reply);
return reply;
}); });
} else { } else {
fastify.all<{ fastify.all<{
Params: { endpoint: string; }, Params: { endpoint: string; },
Body: Record<string, unknown>, Body: Record<string, unknown>,
Querystring: Record<string, unknown>, Querystring: Record<string, unknown>,
}>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, (request, reply) => { }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, async (request, reply) => {
if (request.method === 'GET' && !endpoint.meta.allowGet) { if (request.method === 'GET' && !endpoint.meta.allowGet) {
reply.code(405); reply.code(405);
reply.send(); reply.send();
return; return;
} }
this.apiCallService.handleRequest(ep, request, reply); // Await so that any error can automatically be translated to HTTP 500
await this.apiCallService.handleRequest(ep, request, reply);
return reply;
}); });
} }
} }
@ -160,6 +164,22 @@ export class ApiServerService {
} }
}); });
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.
fastify.get('*', (request, reply) => {
reply.code(404);
// Mock ApiCallService.send's error handling
reply.send({
error: {
message: 'Unknown API endpoint.',
code: 'UNKNOWN_API_ENDPOINT',
id: '2ca3b769-540a-4f08-9dd5-b5a825b6d0f1',
kind: 'client',
},
});
});
done(); done();
} }
} }

View file

@ -741,8 +741,8 @@ export interface IEndpoint {
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
return { return {
name: name, name: name,
meta: ep.meta ?? {}, get meta() { return ep.meta ?? {}; },
params: ep.paramDef, get params() { return ep.paramDef; },
}; };
}); });

View file

@ -138,19 +138,13 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
url: { type: 'string' },
},
anyOf: [ anyOf: [
{ { required: ['fileId'] },
properties: { { required: ['url'] },
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
},
{
properties: {
url: { type: 'string' },
},
required: ['url'],
},
], ],
} as const; } as const;

View file

@ -16,7 +16,7 @@ export const meta = {
errors: { errors: {
noSuchFile: { noSuchFile: {
message: 'No such file.', message: 'No such file.',
code: 'MO_SUCH_FILE', code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
}, },
}, },

View file

@ -1,10 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; import type { RolesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
export const meta = { export const meta = {
@ -39,6 +37,10 @@ export const paramDef = {
properties: { properties: {
roleId: { type: 'string', format: 'misskey:id' }, roleId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
expiresAt: {
type: 'integer',
nullable: true,
},
}, },
required: [ required: [
'roleId', 'roleId',
@ -56,12 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.rolesRepository) @Inject(DI.rolesRepository)
private rolesRepository: RolesRepository, private rolesRepository: RolesRepository,
@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,
private globalEventService: GlobalEventService,
private roleService: RoleService, private roleService: RoleService,
private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
@ -78,19 +75,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchUser); throw new ApiError(meta.errors.noSuchUser);
} }
const date = new Date(); if (ps.expiresAt && ps.expiresAt <= Date.now()) {
const created = await this.roleAssignmentsRepository.insert({ return;
id: this.idService.genId(), }
createdAt: date,
roleId: role.id,
userId: user.id,
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
this.rolesRepository.update(ps.roleId, { await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null);
lastUsedAt: new Date(),
});
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
}); });
} }
} }

View file

@ -1,10 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; import type { RolesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
export const meta = { export const meta = {
@ -62,12 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.rolesRepository) @Inject(DI.rolesRepository)
private rolesRepository: RolesRepository, private rolesRepository: RolesRepository,
@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,
private globalEventService: GlobalEventService,
private roleService: RoleService, private roleService: RoleService,
private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
@ -84,18 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchUser); throw new ApiError(meta.errors.noSuchUser);
} }
const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id }); await this.roleService.unassign(user.id, role.id);
if (roleAssignment == null) {
throw new ApiError(meta.errors.notAssigned);
}
await this.roleAssignmentsRepository.delete(roleAssignment.id);
this.rolesRepository.update(ps.roleId, {
lastUsedAt: new Date(),
});
this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment);
}); });
} }
} }

View file

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
@ -56,6 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
.andWhere('assign.roleId = :roleId', { roleId: role.id }) .andWhere('assign.roleId = :roleId', { roleId: role.id })
.andWhere(new Brackets(qb => { qb
.where('assign.expiresAt IS NULL')
.orWhere('assign.expiresAt > :now', { now: new Date() });
}))
.innerJoinAndSelect('assign.user', 'user'); .innerJoinAndSelect('assign.user', 'user');
const assigns = await query const assigns = await query
@ -64,7 +69,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return await Promise.all(assigns.map(async assign => ({ return await Promise.all(assigns.map(async assign => ({
id: assign.id, id: assign.id,
createdAt: assign.createdAt,
user: await this.userEntityService.pack(assign.user!, me, { detail: true }), user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
expiresAt: assign.expiresAt,
}))); })));
}); });
} }

View file

@ -82,6 +82,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.leftJoinAndSelect('note.channel', 'channel'); .leftJoinAndSelect('note.channel', 'channel');
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
//#endregion //#endregion
const timeline = await query.take(ps.limit).getMany(); const timeline = await query.take(ps.limit).getMany();

View file

@ -39,19 +39,13 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
url: { type: 'string' },
},
anyOf: [ anyOf: [
{ { required: ['fileId'] },
properties: { { required: ['url'] },
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
},
{
properties: {
url: { type: 'string' },
},
required: ['url'],
},
], ],
} as const; } as const;

View file

@ -73,8 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
if (ps.email != null) { if (ps.email != null) {
const available = await this.emailService.validateEmailForAccount(ps.email); const res = await this.emailService.validateEmailForAccount(ps.email);
if (!available) { if (!res.available) {
throw new ApiError(meta.errors.unavailable); throw new ApiError(meta.errors.unavailable);
} }
} }

View file

@ -0,0 +1,263 @@
process.env.NODE_ENV = 'test';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { describe, test, expect } from '@jest/globals';
import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
import { paramDef } from './create.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const VALID = true;
const INVALID = false;
describe('api:notes/create', () => {
describe('validation', () => {
const v = getValidator(paramDef);
const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8');
test('reject empty', () => {
const valid = v({ });
expect(valid).toBe(INVALID);
});
describe('text', () => {
test('simple post', () => {
expect(v({ text: 'Hello, world!' }))
.toBe(VALID);
});
test('null post', () => {
expect(v({ text: null }))
.toBe(INVALID);
});
test('0 characters post', () => {
expect(v({ text: '' }))
.toBe(INVALID);
});
test('over 3000 characters post', async () => {
expect(v({ text: await tooLong }))
.toBe(INVALID);
});
});
describe('cw', () => {
test('simple cw', () => {
expect(v({ text: 'Hello, world!', cw: 'Hello, world!' }))
.toBe(VALID);
});
test('null cw', () => {
expect(v({ text: 'Body', cw: null }))
.toBe(VALID);
});
test('0 characters cw', () => {
expect(v({ text: 'Body', cw: '' }))
.toBe(VALID);
});
test('reject only cw', () => {
expect(v({ cw: 'Hello, world!' }))
.toBe(INVALID);
});
test('over 100 characters cw', async () => {
expect(v({ text: 'Body', cw: await tooLong }))
.toBe(INVALID);
});
});
describe('visibility', () => {
test('public', () => {
expect(v({ text: 'Hello, world!', visibility: 'public' }))
.toBe(VALID);
});
test('home', () => {
expect(v({ text: 'Hello, world!', visibility: 'home' }))
.toBe(VALID);
});
test('followers', () => {
expect(v({ text: 'Hello, world!', visibility: 'followers' }))
.toBe(VALID);
});
test('reject only visibility', () => {
expect(v({ visibility: 'public' }))
.toBe(INVALID);
});
test('reject invalid visibility', () => {
expect(v({ text: 'Hello, world!', visibility: 'invalid' }))
.toBe(INVALID);
});
test('reject null visibility', () => {
expect(v({ text: 'Hello, world!', visibility: null }))
.toBe(INVALID);
});
describe('visibility:specified', () => {
test('specified without visibleUserIds', () => {
expect(v({ text: 'Hello, world!', visibility: 'specified' }))
.toBe(VALID);
});
test('specified with empty visibleUserIds', () => {
expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: [] }))
.toBe(VALID);
});
test('reject specified with non unique visibleUserIds', () => {
expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: ['1', '1', '2'] }))
.toBe(INVALID);
});
test('reject specified with null visibleUserIds', () => {
expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: null }))
.toBe(INVALID);
});
});
});
describe('fileIds', () => {
test('only fileIds', () => {
expect(v({ fileIds: ['1', '2', '3'] }))
.toBe(VALID);
});
test('text and fileIds', () => {
expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'] }))
.toBe(VALID);
});
test('reject null fileIds', () => {
expect(v({ fileIds: null }))
.toBe(INVALID);
});
test('reject text and null fileIds 複合的なanyOfのバリデーションが正しく動作する', () => {
expect(v({ text: 'Hello, world!', fileIds: null }))
.toBe(INVALID);
});
test('reject 0 files', () => {
expect(v({ fileIds: [] }))
.toBe(INVALID);
});
test('reject non unique', () => {
expect(v({ fileIds: ['1', '1', '2'] }))
.toBe(INVALID);
});
test('reject invalid id', () => {
expect(v({ fileIds: ['あ'] }))
.toBe(INVALID);
});
test('reject over 17 files', () => {
const valid = v({ text: 'Hello, world!', fileIds: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18'] });
expect(valid).toBe(INVALID);
});
});
describe('poll', () => {
test('note with poll', () => {
expect(v({ text: 'Hello, world!', poll: { choices: ['a', 'b', 'c'] } }))
.toBe(VALID);
});
test('null poll', () => {
expect(v({ text: 'Hello, world!', poll: null }))
.toBe(VALID);
});
test('allow only poll', () => {
expect(v({ poll: { choices: ['a', 'b', 'c'] } }))
.toBe(VALID);
});
test('poll with expiresAt', async () => {
expect(v({ poll: { choices: ['a', 'b', 'c'], expiresAt: 1 } }))
.toBe(VALID);
});
test('poll with expiredAfter', async () => {
expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 1 } }))
.toBe(VALID);
});
test('reject poll without choices', () => {
expect(v({ poll: { } }))
.toBe(INVALID);
});
test('reject poll with empty choices', () => {
expect(v({ poll: { choices: [] } }))
.toBe(INVALID);
});
test('reject poll with null choices', () => {
expect(v({ poll: { choices: null } }))
.toBe(INVALID);
});
test('reject poll with 1 choice', () => {
expect(v({ poll: { choices: ['a'] } }))
.toBe(INVALID);
});
test('reject poll with too long choice', async () => {
expect(v({ poll: { choices: [await tooLong, '2'] } }))
.toBe(INVALID);
});
test('reject poll with too many choices', () => {
expect(v({ poll: { choices: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] } }))
.toBe(INVALID);
});
test('reject poll with non unique choices', () => {
expect(v({ poll: { choices: ['a', 'a', 'b', 'c'] } }))
.toBe(INVALID);
});
test('reject poll with expiredAfter 0', async () => {
expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 0 } }))
.toBe(INVALID);
});
});
describe('renote', () => {
test('just a renote', () => {
expect(v({ renoteId: '1' }))
.toBe(VALID);
});
test('just a quote', () => {
expect(v({ text: 'Hello, world!', renoteId: '1' }))
.toBe(VALID);
});
test('reject invalid renoteId', () => {
expect(v({ renoteId: 'あ' }))
.toBe(INVALID);
});
});
test('text, fileIds and poll', () => {
expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'], poll: { choices: ['a', 'b', 'c'] } }))
.toBe(VALID);
});
test('text, invalid fileIds and invalid poll', () => {
expect(v({ text: 'Hello, world!', fileIds: ['あ'], poll: { choices: ['a'] } }))
.toBe(INVALID);
});
});
});

View file

@ -101,74 +101,56 @@ export const paramDef = {
noExtractHashtags: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false },
replyId: { type: 'string', format: 'misskey:id', nullable: true }, replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true },
// anyOf内にバリデーションを書いても最初の一つしかチェックされない
// See https://github.com/misskey-dev/misskey/pull/10082
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false
},
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
mediaIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
}, },
// (re)note with text, files and poll are optional
anyOf: [ anyOf: [
{ { required: ['text'] },
// (re)note with text, files and poll are optional { required: ['renoteId'] },
properties: { { required: ['fileIds'] },
text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, { required: ['mediaIds'] },
}, { required: ['poll'] },
required: ['text'],
},
{
// (re)note with files, text and poll are optional
properties: {
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
},
required: ['fileIds'],
},
{
// (re)note with files, text and poll are optional
properties: {
mediaIds: {
deprecated: true,
description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
},
required: ['mediaIds'],
},
{
// (re)note with poll, text and files are optional
properties: {
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
},
required: ['poll'],
},
{
// pure renote
properties: {
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: ['renoteId'],
},
], ],
} as const; } as const;

View file

@ -36,32 +36,25 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
tag: { type: 'string', minLength: 1 },
query: {
type: 'array',
description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
items: {
type: 'array',
items: {
type: 'string',
minLength: 1,
},
minItems: 1,
},
minItems: 1,
},
}, },
anyOf: [ anyOf: [
{ { required: ['tag'] },
properties: { { required: ['query'] },
tag: { type: 'string', minLength: 1 },
},
required: ['tag'],
},
{
properties: {
query: {
type: 'array',
description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
items: {
type: 'array',
items: {
type: 'string',
minLength: 1,
},
minItems: 1,
},
minItems: 1,
},
},
required: ['query'],
},
], ],
} as const; } as const;

View file

@ -58,25 +58,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const hasFollowing = (await this.followingsRepository.count({ const followees = await this.followingsRepository.createQueryBuilder('following')
where: { .select('following.followeeId')
followerId: me.id, .where('following.followerId = :followerId', { followerId: me.id })
}, .getMany();
take: 1,
})) !== 0;
//#region Construct query //#region Construct query
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで .andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで
.andWhere(new Brackets(qb => { qb
.where('note.userId = :meId', { meId: me.id });
if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
}))
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner') .leftJoinAndSelect('user.banner', 'banner')
@ -87,8 +77,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner') .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.setParameters(followingQuery.getParameters());
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
} else {
query.andWhere('note.userId = :meId', { meId: me.id });
}
this.queryService.generateChannelQuery(query, me); this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, me); this.queryService.generateRepliesQuery(query, me);

View file

@ -29,20 +29,14 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: {
pageId: { type: 'string', format: 'misskey:id' },
name: { type: 'string' },
username: { type: 'string' },
},
anyOf: [ anyOf: [
{ { required: ['pageId'] },
properties: { { required: ['name', 'username'] },
pageId: { type: 'string', format: 'misskey:id' },
},
required: ['pageId'],
},
{
properties: {
name: { type: 'string' },
username: { type: 'string' },
},
required: ['name', 'username'],
},
], ],
} as const; } as const;

View file

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
@ -56,6 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
.andWhere('assign.roleId = :roleId', { roleId: role.id }) .andWhere('assign.roleId = :roleId', { roleId: role.id })
.andWhere(new Brackets(qb => { qb
.where('assign.expiresAt IS NULL')
.orWhere('assign.expiresAt > :now', { now: new Date() });
}))
.innerJoinAndSelect('assign.user', 'user'); .innerJoinAndSelect('assign.user', 'user');
const assigns = await query const assigns = await query

View file

@ -46,25 +46,18 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
userId: { type: 'string', format: 'misskey:id' },
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
}, },
anyOf: [ anyOf: [
{ { required: ['userId'] },
properties: { { required: ['username', 'host'] },
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
properties: {
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
},
required: ['username', 'host'],
},
], ],
} as const; } as const;

View file

@ -46,25 +46,18 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
userId: { type: 'string', format: 'misskey:id' },
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
}, },
anyOf: [ anyOf: [
{ { required: ['userId'] },
properties: { { required: ['username', 'host'] },
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
properties: {
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
},
required: ['username', 'host'],
},
], ],
} as const; } as const;

View file

@ -31,20 +31,13 @@ export const paramDef = {
properties: { properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: { type: 'boolean', default: true }, detail: { type: 'boolean', default: true },
username: { type: 'string', nullable: true },
host: { type: 'string', nullable: true },
}, },
anyOf: [ anyOf: [
{ { required: ['username'] },
properties: { { required: ['host'] },
username: { type: 'string', nullable: true },
},
required: ['username'],
},
{
properties: {
host: { type: 'string', nullable: true },
},
required: ['host'],
},
], ],
} as const; } as const;

View file

@ -54,32 +54,22 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
userIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
},
anyOf: [ anyOf: [
{ { required: ['userId'] },
properties: { { required: ['userIds'] },
userId: { type: 'string', format: 'misskey:id' }, { required: ['username'] },
},
required: ['userId'],
},
{
properties: {
userIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
},
required: ['userIds'],
},
{
properties: {
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
},
required: ['username'],
},
], ],
} as const; } as const;

View file

@ -178,7 +178,14 @@ type EventUnionFromDictionary<
// redis通すとDateのインスタンスはstringに変換されるので // redis通すとDateのインスタンスはstringに変換されるので
type Serialized<T> = { type Serialized<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K] extends Record<string, any> ? Serialized<T[K]> : T[K]; [K in keyof T]:
T[K] extends Date
? string
: T[K] extends (Date | null)
? (string | null)
: T[K] extends Record<string, any>
? Serialized<T[K]>
: T[K];
}; };
type SerializedAll<T> = { type SerializedAll<T> = {

View file

@ -61,6 +61,13 @@
renderError('META_FETCH_V'); renderError('META_FETCH_V');
return; return;
} }
// for https://github.com/misskey-dev/misskey/issues/10202
if (lang == null || lang.toString == null || lang.toString() === 'null') {
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
if (localRes.status === 200) { if (localRes.status === 200) {
localStorage.setItem('lang', lang); localStorage.setItem('lang', lang);

View file

@ -1,18 +1,18 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import * as childProcess from 'child_process'; import { signup, api, post, startServer } from '../utils.js';
import { signup, request, post, startServer, shutdownServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common';
describe('API visibility', () => { describe('API visibility', () => {
let p: childProcess.ChildProcess; let p: INestApplicationContext;
beforeAll(async () => { beforeAll(async () => {
p = await startServer(); p = await startServer();
}, 1000 * 30); }, 1000 * 60 * 2);
afterAll(async () => { afterAll(async () => {
await shutdownServer(p); await p.close();
}); });
describe('Note visibility', () => { describe('Note visibility', () => {
@ -60,7 +60,7 @@ describe('API visibility', () => {
//#endregion //#endregion
const show = async (noteId: any, by: any) => { const show = async (noteId: any, by: any) => {
return await request('/notes/show', { return await api('/notes/show', {
noteId, noteId,
}, by); }, by);
}; };
@ -75,7 +75,7 @@ describe('API visibility', () => {
target2 = await signup({ username: 'target2' }); target2 = await signup({ username: 'target2' });
// follow alice <= follower // follow alice <= follower
await request('/following/create', { userId: alice.id }, follower); await api('/following/create', { userId: alice.id }, follower);
// normal posts // normal posts
pub = await post(alice, { text: 'x', visibility: 'public' }); pub = await post(alice, { text: 'x', visibility: 'public' });
@ -413,21 +413,21 @@ describe('API visibility', () => {
//#region HTL //#region HTL
test('[HTL] public-post が 自分が見れる', async () => { test('[HTL] public-post が 自分が見れる', async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice); const res = await api('/notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[HTL] public-post が 非フォロワーから見れない', async () => { test('[HTL] public-post が 非フォロワーから見れない', async () => {
const res = await request('/notes/timeline', { limit: 100 }, other); const res = await api('/notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[HTL] followers-post が フォロワーから見れる', async () => { test('[HTL] followers-post が フォロワーから見れる', async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower); const res = await api('/notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === fol.id); const notes = res.body.filter((n: any) => n.id === fol.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
@ -436,21 +436,21 @@ describe('API visibility', () => {
//#region RTL //#region RTL
test('[replies] followers-reply が フォロワーから見れる', async () => { test('[replies] followers-reply が フォロワーから見れる', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
@ -459,14 +459,14 @@ describe('API visibility', () => {
//#region MTL //#region MTL
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await request('/notes/mentions', { limit: 100 }, target); const res = await api('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
const res = await request('/notes/mentions', { limit: 100 }, target); const res = await api('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folM.id); const notes = res.body.filter((n: any) => n.id === folM.id);
assert.strictEqual(notes[0].text, '@target x'); assert.strictEqual(notes[0].text, '@target x');
@ -474,4 +474,4 @@ describe('API visibility', () => {
//#endregion //#endregion
}); });
}); });
*/

View file

@ -1,11 +1,11 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import * as childProcess from 'child_process'; import { signup, api, startServer } from '../utils.js';
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common';
describe('API', () => { describe('API', () => {
let p: childProcess.ChildProcess; let p: INestApplicationContext;
let alice: any; let alice: any;
let bob: any; let bob: any;
let carol: any; let carol: any;
@ -15,69 +15,69 @@ describe('API', () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
}, 1000 * 30); }, 1000 * 60 * 2);
afterAll(async () => { afterAll(async () => {
await shutdownServer(p); await p.close();
}); });
describe('General validation', () => { describe('General validation', () => {
test('wrong type', async(async () => { test('wrong type', async () => {
const res = await request('/test', { const res = await api('/test', {
required: true, required: true,
string: 42, string: 42,
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('missing require param', async(async () => { test('missing require param', async () => {
const res = await request('/test', { const res = await api('/test', {
string: 'a', string: 'a',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('invalid misskey:id (empty string)', async(async () => { test('invalid misskey:id (empty string)', async () => {
const res = await request('/test', { const res = await api('/test', {
required: true, required: true,
id: '', id: '',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('valid misskey:id', async(async () => { test('valid misskey:id', async () => {
const res = await request('/test', { const res = await api('/test', {
required: true, required: true,
id: '8wvhjghbxu', id: '8wvhjghbxu',
}); });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
})); });
test('default value', async(async () => { test('default value', async () => {
const res = await request('/test', { const res = await api('/test', {
required: true, required: true,
string: 'a', string: 'a',
}); });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.default, 'hello'); assert.strictEqual(res.body.default, 'hello');
})); });
test('can set null even if it has default value', async(async () => { test('can set null even if it has default value', async () => {
const res = await request('/test', { const res = await api('/test', {
required: true, required: true,
nullableDefault: null, nullableDefault: null,
}); });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.nullableDefault, null); assert.strictEqual(res.body.nullableDefault, null);
})); });
test('cannot set undefined if it has default value', async(async () => { test('cannot set undefined if it has default value', async () => {
const res = await request('/test', { const res = await api('/test', {
required: true, required: true,
nullableDefault: undefined, nullableDefault: undefined,
}); });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.nullableDefault, 'hello'); assert.strictEqual(res.body.nullableDefault, 'hello');
})); });
}); });
}); });

View file

@ -1,11 +1,11 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import * as childProcess from 'child_process'; import { signup, api, post, startServer } from '../utils.js';
import { signup, request, post, startServer, shutdownServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common';
describe('Block', () => { describe('Block', () => {
let p: childProcess.ChildProcess; let p: INestApplicationContext;
// alice blocks bob // alice blocks bob
let alice: any; let alice: any;
@ -17,14 +17,14 @@ describe('Block', () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
}, 1000 * 30); }, 1000 * 60 * 2);
afterAll(async () => { afterAll(async () => {
await shutdownServer(p); await p.close();
}); });
test('Block作成', async () => { test('Block作成', async () => {
const res = await request('/blocking/create', { const res = await api('/blocking/create', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
@ -32,7 +32,7 @@ describe('Block', () => {
}); });
test('ブロックされているユーザーをフォローできない', async () => { test('ブロックされているユーザーをフォローできない', async () => {
const res = await request('/following/create', { userId: alice.id }, bob); const res = await api('/following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
@ -41,7 +41,7 @@ describe('Block', () => {
test('ブロックされているユーザーにリアクションできない', async () => { test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
@ -50,7 +50,7 @@ describe('Block', () => {
test('ブロックされているユーザーに返信できない', async () => { test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -59,7 +59,7 @@ describe('Block', () => {
test('ブロックされているユーザーのートをRenoteできない', async () => { test('ブロックされているユーザーのートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -74,7 +74,7 @@ describe('Block', () => {
const bobNote = await post(bob); const bobNote = await post(bob);
const carolNote = await post(carol); const carolNote = await post(carol);
const res = await request('/notes/local-timeline', {}, bob); const res = await api('/notes/local-timeline', {}, bob);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);

View file

@ -1,29 +1,35 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import * as childProcess from 'child_process'; // node-fetch only supports it's own Blob yet
import * as openapi from '@redocly/openapi-core'; // https://github.com/node-fetch/node-fetch/pull/1664
import { startServer, signup, post, request, simpleGet, port, shutdownServer, api } from '../utils.js'; import { Blob } from 'node-fetch';
import { startServer, signup, post, api, uploadFile } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
describe('Endpoints', () => { describe('Endpoints', () => {
let p: childProcess.ChildProcess; let p: INestApplicationContext;
let alice: any; let alice: any;
let bob: any; let bob: any;
let carol: any;
let dave: any;
beforeAll(async () => { beforeAll(async () => {
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
}, 1000 * 30); carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
}, 1000 * 60 * 2);
afterAll(async () => { afterAll(async () => {
await shutdownServer(p); await p.close();
}); });
describe('signup', () => { describe('signup', () => {
test('不正なユーザー名でアカウントが作成できない', async () => { test('不正なユーザー名でアカウントが作成できない', async () => {
const res = await request('api/signup', { const res = await api('signup', {
username: 'test.', username: 'test.',
password: 'test', password: 'test',
}); });
@ -31,7 +37,7 @@ describe('Endpoints', () => {
}); });
test('空のパスワードでアカウントが作成できない', async () => { test('空のパスワードでアカウントが作成できない', async () => {
const res = await request('api/signup', { const res = await api('signup', {
username: 'test', username: 'test',
password: '', password: '',
}); });
@ -44,7 +50,7 @@ describe('Endpoints', () => {
password: 'test1', password: 'test1',
}; };
const res = await request('api/signup', me); const res = await api('signup', me);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -52,7 +58,7 @@ describe('Endpoints', () => {
}); });
test('同じユーザー名のアカウントは作成できない', async () => { test('同じユーザー名のアカウントは作成できない', async () => {
const res = await request('api/signup', { const res = await api('signup', {
username: 'test1', username: 'test1',
password: 'test1', password: 'test1',
}); });
@ -63,7 +69,7 @@ describe('Endpoints', () => {
describe('signin', () => { describe('signin', () => {
test('間違ったパスワードでサインインできない', async () => { test('間違ったパスワードでサインインできない', async () => {
const res = await request('api/signin', { const res = await api('signin', {
username: 'test1', username: 'test1',
password: 'bar', password: 'bar',
}); });
@ -72,7 +78,7 @@ describe('Endpoints', () => {
}); });
test('クエリをインジェクションできない', async () => { test('クエリをインジェクションできない', async () => {
const res = await request('api/signin', { const res = await api('signin', {
username: 'test1', username: 'test1',
password: { password: {
$gt: '', $gt: '',
@ -83,7 +89,7 @@ describe('Endpoints', () => {
}); });
test('正しい情報でサインインできる', async () => { test('正しい情報でサインインできる', async () => {
const res = await request('api/signin', { const res = await api('signin', {
username: 'test1', username: 'test1',
password: 'test1', password: 'test1',
}); });
@ -111,11 +117,12 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.birthday, myBirthday); assert.strictEqual(res.body.birthday, myBirthday);
}); });
test('名前を空白にできない', async () => { test('名前を空白にでき', async () => {
const res = await api('/i/update', { const res = await api('/i/update', {
name: ' ', name: ' ',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, ' ');
}); });
test('誕生日の設定を削除できる', async () => { test('誕生日の設定を削除できる', async () => {
@ -201,7 +208,6 @@ describe('Endpoints', () => {
test('リアクションできる', async () => { test('リアクションできる', async () => {
const bobPost = await post(bob); const bobPost = await post(bob);
const alice = await signup({ username: 'alice' });
const res = await api('/notes/reactions/create', { const res = await api('/notes/reactions/create', {
noteId: bobPost.id, noteId: bobPost.id,
reaction: '🚀', reaction: '🚀',
@ -214,7 +220,7 @@ describe('Endpoints', () => {
}, alice); }, alice);
assert.strictEqual(resNote.status, 200); assert.strictEqual(resNote.status, 200);
assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]); assert.strictEqual(resNote.body.reactions['🚀'], 1);
}); });
test('自分の投稿にもリアクションできる', async () => { test('自分の投稿にもリアクションできる', async () => {
@ -228,7 +234,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}); });
test('二重にリアクションできない', async () => { test('二重にリアクションすると上書きされる', async () => {
const bobPost = await post(bob); const bobPost = await post(bob);
await api('/notes/reactions/create', { await api('/notes/reactions/create', {
@ -241,7 +247,14 @@ describe('Endpoints', () => {
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', {
noteId: bobPost.id,
}, alice);
assert.strictEqual(resNote.status, 200);
assert.deepStrictEqual(resNote.body.reactions, { '🚀': 1 });
}); });
test('存在しない投稿にはリアクションできない', async () => { test('存在しない投稿にはリアクションできない', async () => {
@ -369,57 +382,22 @@ describe('Endpoints', () => {
}); });
}); });
/*
describe('/i', () => {
test('', async () => {
});
});
*/
});
/*
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js';
describe('API: Endpoints', () => {
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
describe('drive', () => { describe('drive', () => {
test('ドライブ情報を取得できる', async () => { test('ドライブ情報を取得できる', async () => {
await uploadFile({ await uploadFile(alice, {
userId: alice.id, blob: new Blob([new Uint8Array(256)]),
size: 256
}); });
await uploadFile({ await uploadFile(alice, {
userId: alice.id, blob: new Blob([new Uint8Array(512)]),
size: 512
}); });
await uploadFile({ await uploadFile(alice, {
userId: alice.id, blob: new Blob([new Uint8Array(1024)]),
size: 1024
}); });
const res = await api('/drive', {}, alice); const res = await api('/drive', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).have.property('usage').eql(1792); expect(res.body).toHaveProperty('usage', 1792);
})); });
}); });
describe('drive/files/create', () => { describe('drive/files/create', () => {
@ -428,397 +406,400 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.png'); assert.strictEqual(res.body.name, 'Lenna.jpg');
})); });
test('ファイルに名前を付けられる', async () => { test('ファイルに名前を付けられる', async () => {
const res = await assert.request(server) const res = await uploadFile(alice, { name: 'Belmond.jpg' });
.post('/drive/files/create')
.field('i', alice.token)
.field('name', 'Belmond.png')
.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
expect(res).have.status(200); assert.strictEqual(res.status, 200);
expect(res.body).be.a('object'); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).have.property('name').eql('Belmond.png'); assert.strictEqual(res.body.name, 'Belmond.jpg');
})); });
test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => {
const res = await uploadFile(alice, { name: 'Belmond.png' });
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.png.jpg');
});
test('ファイル無しで怒られる', async () => { test('ファイル無しで怒られる', async () => {
const res = await api('/drive/files/create', {}, alice); const res = await api('/drive/files/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('SVGファイルを作成できる', async () => { test('SVGファイルを作成できる', async () => {
const res = await uploadFile(alice, __dirname + '/resources/image.svg'); const res = await uploadFile(alice, { path: 'image.svg' });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'image.svg'); assert.strictEqual(res.body.name, 'image.svg');
assert.strictEqual(res.body.type, 'image/svg+xml'); assert.strictEqual(res.body.type, 'image/svg+xml');
})); });
}); });
describe('drive/files/update', () => { describe('drive/files/update', () => {
test('名前を更新できる', async () => { test('名前を更新できる', async () => {
const file = await uploadFile(alice); const file = (await uploadFile(alice)).body;
const newName = 'いちごパスタ.png'; const newName = 'いちごパスタ.png';
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
name: newName name: newName,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, newName); assert.strictEqual(res.body.name, newName);
})); });
test('他人のファイルは更新できない', async () => { test('他人のファイルは更新できない', async () => {
const file = await uploadFile(bob); const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
name: 'いちごパスタ.png' name: 'いちごパスタ.png',
}, alice); }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const file = await uploadFile(alice); const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: folder.id folderId: folder.id,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, folder.id); assert.strictEqual(res.body.folderId, folder.id);
})); });
test('親フォルダを無しにできる', async () => { test('親フォルダを無しにできる', async () => {
const file = await uploadFile(alice); const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
await api('/drive/files/update', { await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: folder.id folderId: folder.id,
}, alice); }, alice);
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: null folderId: null,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, null); assert.strictEqual(res.body.folderId, null);
})); });
test('他人のフォルダには入れられない', async () => { test('他人のフォルダには入れられない', async () => {
const file = await uploadFile(alice); const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, bob)).body; }, bob)).body;
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: folder.id folderId: folder.id,
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('存在しないフォルダで怒られる', async () => { test('存在しないフォルダで怒られる', async () => {
const file = await uploadFile(alice); const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: '000000000000000000000000' folderId: '000000000000000000000000',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const file = await uploadFile(alice); const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: 'foo' folderId: 'foo',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('ファイルが存在しなかったら怒る', async () => { test('ファイルが存在しなかったら怒る', async () => {
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: '000000000000000000000000', fileId: '000000000000000000000000',
name: 'いちごパスタ.png' name: 'いちごパスタ.png',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: 'kyoppie', fileId: 'kyoppie',
name: 'いちごパスタ.png' name: 'いちごパスタ.png',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
}); });
describe('drive/folders/create', () => { describe('drive/folders/create', () => {
test('フォルダを作成できる', async () => { test('フォルダを作成できる', async () => {
const res = await api('/drive/folders/create', { const res = await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'test'); assert.strictEqual(res.body.name, 'test');
})); });
}); });
describe('drive/folders/update', () => { describe('drive/folders/update', () => {
test('名前を更新できる', async () => { test('名前を更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
name: 'new name' name: 'new name',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'new name'); assert.strictEqual(res.body.name, 'new name');
})); });
test('他人のフォルダを更新できない', async () => { test('他人のフォルダを更新できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, bob)).body; }, bob)).body;
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
name: 'new name' name: 'new name',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('/drive/folders/create', {
name: 'parent' name: 'parent',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id parentId: parentFolder.id,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, parentFolder.id); assert.strictEqual(res.body.parentId, parentFolder.id);
})); });
test('親フォルダを無しに更新できる', async () => { test('親フォルダを無しに更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('/drive/folders/create', {
name: 'parent' name: 'parent',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id parentId: parentFolder.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: null parentId: null,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, null); assert.strictEqual(res.body.parentId, null);
})); });
test('他人のフォルダを親フォルダに設定できない', async () => { test('他人のフォルダを親フォルダに設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('/drive/folders/create', {
name: 'parent' name: 'parent',
}, bob)).body; }, bob)).body;
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id parentId: parentFolder.id,
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('フォルダが循環するような構造にできない', async () => { test('フォルダが循環するような構造にできない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('/drive/folders/create', {
name: 'parent' name: 'parent',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('/drive/folders/update', {
folderId: parentFolder.id, folderId: parentFolder.id,
parentId: folder.id parentId: folder.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id parentId: parentFolder.id,
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('フォルダが循環するような構造にできない(再帰的)', async () => { test('フォルダが循環するような構造にできない(再帰的)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const folderB = (await api('/drive/folders/create', { const folderB = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const folderC = (await api('/drive/folders/create', { const folderC = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('/drive/folders/update', {
folderId: folderB.id, folderId: folderB.id,
parentId: folderA.id parentId: folderA.id,
}, alice); }, alice);
await api('/drive/folders/update', { await api('/drive/folders/update', {
folderId: folderC.id, folderId: folderC.id,
parentId: folderB.id parentId: folderB.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folderA.id, folderId: folderA.id,
parentId: folderC.id parentId: folderC.id,
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('フォルダが循環するような構造にできない(自身)', async () => { test('フォルダが循環するような構造にできない(自身)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folderA.id, folderId: folderA.id,
parentId: folderA.id parentId: folderA.id,
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('存在しない親フォルダを設定できない', async () => { test('存在しない親フォルダを設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: '000000000000000000000000' parentId: '000000000000000000000000',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('不正な親フォルダIDで怒られる', async () => { test('不正な親フォルダIDで怒られる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: 'foo' parentId: 'foo',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('存在しないフォルダを更新できない', async () => { test('存在しないフォルダを更新できない', async () => {
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: '000000000000000000000000' folderId: '000000000000000000000000',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
test('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: 'foo' folderId: 'foo',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); });
}); });
describe('notes/replies', () => { describe('notes/replies', () => {
test('自分に閲覧権限のない投稿は含まれない', async () => { test('自分に閲覧権限のない投稿は含まれない', async () => {
const alicePost = await post(alice, { const alicePost = await post(alice, {
text: 'foo' text: 'foo',
}); });
await post(bob, { await post(bob, {
replyId: alicePost.id, replyId: alicePost.id,
text: 'bar', text: 'bar',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [alice.id] visibleUserIds: [alice.id],
}); });
const res = await api('/notes/replies', { const res = await api('/notes/replies', {
noteId: alicePost.id noteId: alicePost.id,
}, carol); }, carol);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 0); assert.strictEqual(res.body.length, 0);
})); });
}); });
describe('notes/timeline', () => { describe('notes/timeline', () => {
test('フォロワー限定投稿が含まれる', async () => { test('フォロワー限定投稿が含まれる', async () => {
await api('/following/create', { await api('/following/create', {
userId: alice.id userId: carol.id,
}, bob); }, dave);
const alicePost = await post(alice, { const carolPost = await post(carol, {
text: 'foo', text: 'foo',
visibility: 'followers' visibility: 'followers',
}); });
const res = await api('/notes/timeline', {}, bob); const res = await api('/notes/timeline', {}, dave);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 1); assert.strictEqual(res.body.length, 1);
assert.strictEqual(res.body[0].id, alicePost.id); assert.strictEqual(res.body[0].id, carolPost.id);
})); });
}); });
}); });
*/

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