diff --git a/.config/LICENSE b/.config/LICENSE new file mode 100644 index 000000000..342509dec --- /dev/null +++ b/.config/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 Calckey + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/.config/example.yml b/.config/example.yml index c743e2c58..ba9435173 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -67,6 +67,20 @@ redis: #db: 1 #user: default +# ┌─────────────────────────────┐ +#───┘ Cache server configuration └───────────────────────────────────── + +# A Redis-compatible server (DragonflyDB, Keydb, Redis) for caching +# If left blank, it will use the Redis server from above + +#cacheServer: + #host: localhost + #port: 6379 + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + # Please configure either MeiliSearch *or* Sonic. # If both MeiliSearch and Sonic configurations are present, MeiliSearch will take precedence. @@ -107,7 +121,7 @@ redis: # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── -# Maximum length of a post (default 3000, max 8192) +# Maximum length of a post (default 3000, max 250000000) #maxNoteLength: 3000 # Maximum length of an image caption (default 1500, max 8192) diff --git a/.dockerignore b/.dockerignore index 90d15ddd9..ad6ad169a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,8 @@ # Visual Studio Code -/.vscode -!/.vscode/extensions.json +.vscode # Intelij-IDEA -/.idea -packages/backend/.idea/backend.iml -packages/backend/.idea/modules.xml -packages/backend/.idea/vcs.xml +.idea # Node.js node_modules @@ -14,7 +10,7 @@ node_modules report.*.json # Rust -packages/backend/native-utils/target/* +packages/backend/native-utils/target # Cypress cypress/screenshots @@ -24,9 +20,7 @@ cypress/videos coverage # config -/.config/* -!/.config/example.yml -!/.config/docker_example.env +/.config # misskey built diff --git a/.gitignore b/.gitignore index 3a667851c..6bf2ea1b1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ coverage !/.config/devenv.yml !/.config/docker_example.env !/.config/helm_values_example.yml +!/.config/LICENSE #docker dev config /dev/docker-compose.yml @@ -48,6 +49,9 @@ packages/backend/assets/sounds/None.mp3 !packages/backend/src/db +packages/megalodon/lib +packages/megalodon/.idea + # blender backups *.blend1 *.blend2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 988156a72..b9a998f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2704,7 +2704,7 @@ Co-committed-by: naskya Passwords will be automatically re-hashed on sign-in. All new password hashes will be argon2 by default. This uses argon2id and is not configurable. In the very unlikely case someone has more specific needs, a fork is recommended. ChangeLog: Added Co-authored-by: Chloe Kudryavtsev -Breaks Calckey -> Misskey migration, but fixes Foundkey -> Calckey migration +Breaks Calckey -> Misskey migration, but fixes FoundKey -> Calckey migration - Add argon diff --git a/COPYING b/COPYING index 0f05a98cf..387515b06 100644 --- a/COPYING +++ b/COPYING @@ -1,15 +1,24 @@ -Unless otherwise stated this repository is -Copyright © 2014-2022 syuilo and contributers -Copyright © 2022 thatonecalculator and contributers +Unless specified otherwise, the entirety of this repository is subject to the following: +Copyright © 2014-2023 syuilo and contributors +Copyright © 2022-2023 Kainoa Kanter and contributors And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. +--- -Firefish includes several third-party Open-Source softwares. +These specific configuration directories: -Emoji keywords for Unicode 11 and below by Mu-An Chiou -License: MIT -https://github.com/muan/emojilib/blob/master/LICENSE +- .config/ +- custom/assets/ + +and their contents are +Copyright © 2022-2023 Kainoa Kanter and contributors + +And are distributed under The Apache License, Version 2.0, you should have received a copy of the license file as LICENSE in each specified directory. + +--- + +Calckey includes several third-party open-source softwares and software libraries. RsaSignature2017 implementation by Transmute Industries Inc License: MIT @@ -18,3 +27,7 @@ https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE Machine learning model for sensitive images by Infinite Red, Inc. License: MIT https://github.com/infinitered/nsfwjs/blob/master/LICENSE + +Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository: + +pnpm licenses list diff --git a/Dockerfile b/Dockerfile index 1d985954f..e7ba670ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,8 @@ COPY package.json pnpm*.yaml ./ COPY packages/backend/package.json packages/backend/package.json COPY packages/client/package.json packages/client/package.json COPY packages/sw/package.json packages/sw/package.json -COPY packages/firefish-js/package.json packages/firefish-js/package.json +COPY packages/calckey-js/package.json packages/calckey-js/package.json +COPY packages/megalodon/package.json packages/megalodon/package.json COPY packages/backend/native-utils/package.json packages/backend/native-utils/package.json COPY packages/backend/native-utils/npm/linux-x64-musl/package.json packages/backend/native-utils/npm/linux-x64-musl/package.json COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/backend/native-utils/npm/linux-arm64-musl/package.json @@ -29,10 +30,7 @@ COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/ba RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm i --frozen-lockfile # Copy in the rest of the native-utils rust files -COPY packages/backend/native-utils/.cargo packages/backend/native-utils/.cargo -COPY packages/backend/native-utils/build.rs packages/backend/native-utils/ -COPY packages/backend/native-utils/src packages/backend/native-utils/src/ -COPY packages/backend/native-utils/migration/src packages/backend/native-utils/migration/src/ +COPY packages/backend/native-utils packages/backend/native-utils/ # Compile native-utils RUN pnpm run --filter native-utils build @@ -53,6 +51,8 @@ RUN apk add --no-cache --no-progress tini ffmpeg vips-dev zip unzip nodejs-curre COPY . ./ +COPY --from=build /calckey/packages/megalodon /calckey/packages/megalodon + # Copy node modules COPY --from=build /firefish/node_modules /firefish/node_modules COPY --from=build /firefish/packages/backend/node_modules /firefish/packages/backend/node_modules diff --git a/FIREFISH.md b/FIREFISH.md index 334d1bffb..f9ed70bbb 100644 --- a/FIREFISH.md +++ b/FIREFISH.md @@ -137,7 +137,7 @@ - 👍 also triggers generic like/favorite - [Add additional background for acrylic popups if backdrop-filter is unsupported](https://github.com/misskey-dev/misskey/pull/8671) - [Add parameters to MFM rotate](https://github.com/misskey-dev/misskey/pull/8549) -- Many changes from [Foundkey](https://akkoma.dev/FoundKeyGang/Foundkey) +- Many changes from [FoundKey](https://akkoma.dev/FoundKeyGang/FoundKey) - https://akkoma.dev/FoundKeyGang/FoundKey/commit/0ece67b04c3f0365057624c1068808276ccab981: refactor pages/auth.form.vue to composition API - https://akkoma.dev/FoundKeyGang/FoundKey/commit/4bc9610d8bf5af736b5e89e4782395705de45d7d: remove unnecessary joins - https://akkoma.dev/FoundKeyGang/FoundKey/commit/9ee609d70082f7a6dc119a5d83c0e7c5e1208676: enhance privacy of notes diff --git a/README.md b/README.md index add77b650..ce5bf88cb 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,14 @@ # 🌠 Getting started +Want to just join a Calckey server? View the list here, pick one, and join: + +### https://calckey.org/join + +--- + +Want to make your own? Keep reading! + This guide will work for both **starting from scratch** and **migrating from Misskey**. ## 🔰 Easy installers @@ -88,7 +96,6 @@ If you have access to a server that supports one of the sources below, I recomme ## 🧑‍💻 Dependencies - 🐢 At least [NodeJS](https://nodejs.org/en/) v18.16.0 (v20 recommended) - - Install with [nvm](https://github.com/nvm-sh/nvm) - 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12 (v14 recommended) - 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended) - Web Proxy (one of the following) @@ -104,7 +111,11 @@ If you have access to a server that supports one of the sources below, I recomme - 🦔 [Sonic](https://crates.io/crates/sonic-server) - [MeiliSearch](https://www.meilisearch.com/) - [ElasticSearch](https://www.elastic.co/elasticsearch/) - +- Caching server (one of the following) + - 🐲 [DragonflyDB](https://www.dragonflydb.io/) (recommended) + - 👻 [KeyDB](https://keydb.dev/) + - 🍱 Another [Redis](https://redis.io/) server + ### 🏗️ Build dependencies - 🦀 At least [Rust](https://www.rust-lang.org/) v1.68.0 @@ -161,6 +172,10 @@ psql postgres -c "create database firefish with encoding = 'UTF8';" In Firefish's directory, fill out the `db` section of `.config/default.yml` with the correct information, where the `db` key is `firefish`. +## 💰 Caching server + +If you experience a lot of traffic, it's a good idea to set up another Redis-compatible caching server. If you don't set one one up, it'll fall back to the mandatory Redis server. DragonflyDB is the recommended option due to its unrivaled performance and ease of use. + ## 🔎 Set up search ### 🦔 Sonic @@ -201,9 +216,9 @@ Please don't use ElasticSearch unless you already have an ElasticSearch setup an - Edit `.config/default.yml`, making sure to fill out required fields. - Also copy and edit `.config/docker_example.env` to `.config/docker.env` if you're using Docker. -## 🚚 Migrating from Misskey to Firefish +## 🚚 Migrating from Misskey/FoundKey to Calckey -For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](https://codeberg.org/firefish/firefish/src/branch/develop/docs/migrate.md). +For migrating from Misskey v13, Misskey v12, and FoundKey, read [this document](https://codeberg.org/calckey/calckey/src/branch/develop/docs/migrate.md). ## 🌐 Web proxy diff --git a/custom/assets/LICENSE b/custom/assets/LICENSE new file mode 100644 index 000000000..342509dec --- /dev/null +++ b/custom/assets/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 Calckey + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/docs/migrate.md b/docs/migrate.md index 5a478c90a..a071ce0ce 100644 --- a/docs/migrate.md +++ b/docs/migrate.md @@ -1,10 +1,11 @@ -# 🚚 Migrating from Misskey to Firefish +# 🚚 Migrating from Misskey/FoundKey to Calckey -The following procedure may not work depending on your environment and version of Misskey. +All the guides below assume you're starting in the root of the repo directory. -**Make sure you** -- **stopped all master and worker processes of Misskey.** -- **have backups of the database before performing any commands.** +### Before proceeding + +- **Ensure you have stopped all master and worker processes of Misskey.** +- **Ensure you have backups of the database before performing any commands.** ## Misskey v13 and above @@ -77,15 +78,16 @@ NODE_ENV=production pnpm run migrate # build using prefered method ``` -## Foundkey +## FoundKey ```sh cd packages/backend +sed -i '12s/^/\/\//' ./migration/1663399074403-resize-comments-drive-file.js LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n uniformThemecolor1652859567549 | cut -d ':' -f 1)" -NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)" +NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | wc -l)" -for i in $(seq 1 $NUM_MIGRAIONS); do +for i in $(seq 1 $NUM_MIGRATIONS); do npx typeorm migration:revert -d ormconfig.js done @@ -100,4 +102,4 @@ NODE_ENV=production pnpm run migrate ## Reverse -You ***cannot*** migrate back to Misskey from Firefish due to re-hashing passwords on signin with argon2. You can migrate from Firefish to Foundkey, though. +You ***cannot*** migrate back to Misskey from Calckey due to re-hashing passwords on signin with argon2. You can migrate from Calckey to FoundKey, although this is not recommended due to FoundKey being end-of-life. diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 85cd181e5..3fa2f6af4 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -95,7 +95,7 @@ privacy: "Privadesa" makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació" defaultNoteVisibility: "Visibilitat per defecte" follow: "Segueix" -followRequest: "Segueix" +followRequest: "Sol·licitud de Seguiment" followRequests: "Sol·licituds de seguiment" unfollow: "Deixa de seguir" followRequestPending: "Sol·licituds de seguiment pendents" @@ -1382,7 +1382,7 @@ adminCustomCssWarn: Aquesta configuració només s'ha d'utilitzar si sabeu què showUpdates: Mostra una finestra emergent quan Firefish s'actualitzi recommendedInstances: Servidors recomanats recommendedInstancesDescription: Servidors recomanats separats per salts de línia - que apareixen a la línia de temps recomanada. NO afegiu `https://`, NOMÉS el domini. + que apareixen a la línia de temps recomanada. caption: Descripció Automàtica splash: Pantalla de Benvinguda swipeOnDesktop: Permet lliscar a l'estil del mòbil a l'escriptori @@ -1603,6 +1603,12 @@ _aboutMisskey: patrons: Mecenes de Firefish patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació amb l'enllaç de dalt per veure el teu nom aquí! + donateTitle: T'agrada Calckey? + pleaseDonateToCalckey: Penseu en fer una donació a Calckey per donar suport al seu + desenvolupament. + pleaseDonateToHost: Penseu també en fer una donació a la vostre instància, {host}, + per ajudar-lo a suportar els costos de funcionament. + donateHost: Fes una donació a {host} unknown: Desconegut pageLikesCount: Nombre de pàgines amb M'agrada youAreRunningUpToDateClient: Estás fent servir la versió del client més nova. @@ -2142,3 +2148,15 @@ _skinTones: dark: Fosc yellow: Groc swipeOnMobile: Permet lliscar entre pàgines +enableIdenticonGeneration: Habilitar la generació d'Identicon +enableServerMachineStats: Habilitar les estadístiques del maquinari del servidor +showPopup: Notificar els usuaris amb una finestra emergent +showWithSparkles: Mostra amb espurnes +youHaveUnreadAnnouncements: Tens anuncis sense llegir +xl: XL +donationLink: Enllaç a la pàgina de donacions +neverShow: No tornis a mostrar +remindMeLater: Potser després +removeMember: Elimina el membre +removeQuote: Elimina la cita +removeRecipient: Elimina el destinatari diff --git a/locales/en-US.yml b/locales/en-US.yml index 1305cda33..1ac1feabc 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -105,7 +105,7 @@ privacy: "Privacy" makeFollowManuallyApprove: "Follow requests require approval" defaultNoteVisibility: "Default visibility" follow: "Follow" -followRequest: "Follow" +followRequest: "Follow Request" followRequests: "Follow requests" unfollow: "Unfollow" followRequestPending: "Follow request pending" @@ -644,6 +644,7 @@ useBlurEffectForModal: "Use blur effect for modals" useFullReactionPicker: "Use full-size reaction picker" width: "Width" height: "Height" +xl: "XL" large: "Big" medium: "Medium" small: "Small" @@ -1049,7 +1050,7 @@ customSplashIconsDescription: "URLs for custom splash screen icons separated by showUpdates: "Show a popup when Firefish updates" recommendedInstances: "Recommended servers" recommendedInstancesDescription: "Recommended servers separated by line breaks to - appear in the recommended timeline. Do NOT add `https://`, ONLY the domain." + appear in the recommended timeline." caption: "Auto Caption" splash: "Splash Screen" updateAvailable: "There might be an update available!" @@ -1112,6 +1113,17 @@ isModerator: "Moderator" isAdmin: "Administrator" isPatron: "Firefish Patron" reactionPickerSkinTone: "Preferred emoji skin tone" +enableServerMachineStats: "Enable server hardware statistics" +enableIdenticonGeneration: "Enable Identicon generation" +showPopup: "Notify users with popup" +showWithSparkles: "Show with sparkles" +youHaveUnreadAnnouncements: "You have unread announcements" +donationLink: "Link to donation page" +neverShow: "Don't show again" +remindMeLater: "Maybe later" +removeQuote: "Remove quote" +removeRecipient: "Remove recipient" +removeMember: "Remove member" _sensitiveMediaDetection: description: "Reduces the effort of server moderation through automatically recognizing @@ -1208,11 +1220,16 @@ _aboutMisskey: contributors: "Main contributors" allContributors: "All contributors" source: "Source code" - translation: "Translate Firefish" - donate: "Donate to Firefish" + translation: "Translate Calckey" + donate: "Donate to Calckey" + donateTitle: "Enjoying Calckey?" + pleaseDonateToCalckey: "Please consider donating to Calckey to support its development." + pleaseDonateToHost: "Please also consider donating to your home server, {host}, to help support its operation costs." + donateHost: "Donate to {host}" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" - patrons: "Firefish patrons" + sponsors: "Calckey sponsors" + patrons: "Calckey patrons" patronsList: "Listed chronologically, not by donation size. Donate with the link above to get your name on here!" _nsfw: respect: "Hide NSFW media" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index fe1ac34fb..2becd3ecc 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1,10 +1,10 @@ _lang_: "Français" headlineMisskey: "Réseau relié par des notes" -introMisskey: "Bienvenue ! Firefish est un service de microblogage décentralisé, libre\ - \ et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à l’instant présent,\ - \ autour de vous avec les autres \U0001F4E1\nLa fonction « réactions », vous permet\ - \ également d’ajouter une réaction rapide aux notes des autres utilisateur·rice·s\ - \ \U0001F44D\nExplorons un nouveau monde \U0001F680" +introMisskey: "Bienvenue ! Calckey est un service de microblogage décentralisé, libre + et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à l’instant présent, + autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également + d’ajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons + un nouveau monde 🚀" monthAndDay: "{day}/{month}" search: "Rechercher" notifications: "Notifications" @@ -26,8 +26,8 @@ otherSettings: "Paramètres avancés" openInWindow: "Ouvrir dans une nouvelle fenêtre" profile: "Profil" timeline: "Fil" -noAccountDescription: "L’utilisateur·rice n’a pas encore renseigné de biographie de\ - \ présentation sur son profil." +noAccountDescription: "L’utilisateur·rice n’a pas encore renseigné de biographie de + présentation sur son profil." login: "Se connecter" loggingIn: "Connexion en cours" logout: "Se déconnecter" @@ -48,8 +48,8 @@ copyContent: "Copier le contenu" copyLink: "Copier le lien" delete: "Supprimer" deleteAndEdit: "Supprimer et réécrire" -deleteAndEditConfirm: "Êtes-vous sûr·e de vouloir supprimer cette note et la reformuler\ - \ ? Vous perdrez toutes les réactions, renotes et réponses y afférentes." +deleteAndEditConfirm: "Êtes-vous sûr·e de vouloir supprimer cette note et la reformuler + ? Vous perdrez toutes les réactions, renotes et réponses y afférentes." addToList: "Ajouter à une liste" sendMessage: "Envoyer un message" copyUsername: "Copier le nom d’utilisateur·rice" @@ -72,8 +72,8 @@ download: "Télécharger" driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier \"{name}\"\ \ ? Il sera retiré de toutes ses notes liées." unfollowConfirm: "Désirez-vous vous désabonner de {name} ?" -exportRequested: "Vous avez demandé une exportation. L’opération pourrait prendre\ - \ un peu de temps. Une terminée, le fichier résultant sera ajouté au Drive." +exportRequested: "Vous avez demandé une exportation. L’opération pourrait prendre + un peu de temps. Une terminée, le fichier résultant sera ajouté au Drive." importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps." lists: "Listes" noLists: "Vous n’avez aucune liste" @@ -88,12 +88,12 @@ error: "Erreur" somethingHappened: "Une erreur est survenue" retry: "Réessayer" pageLoadError: "Le chargement de la page a échoué." -pageLoadErrorDescription: "Cela est généralement causé par le cache du navigateur\ - \ ou par un problème réseau. Veuillez vider votre cache ou attendre un peu et réessayer." -serverIsDead: "Le serveur ne répond pas. Patientez quelques instants puis essayez\ - \ à nouveau." -youShouldUpgradeClient: "Si la page ne s'affiche pas correctement, rechargez-la pour\ - \ mettre votre client à jour." +pageLoadErrorDescription: "Cela est généralement causé par le cache du navigateur + ou par un problème réseau. Veuillez vider votre cache ou attendre un peu et réessayer." +serverIsDead: "Le serveur ne répond pas. Patientez quelques instants puis essayez + à nouveau." +youShouldUpgradeClient: "Si la page ne s'affiche pas correctement, rechargez-la pour + mettre votre client à jour." enterListName: "Nom de la liste" privacy: "Confidentialité" makeFollowManuallyApprove: "Accepter manuellement les demandes d’abonnement" @@ -118,11 +118,11 @@ sensitive: "Contenu sensible" add: "Ajouter" reaction: "Réactions" reactionSetting: "Réactions à afficher dans le sélecteur de réactions" -reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser\ - \ « + » pour ajouter." -rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes\ - \ \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la\ - \ publication de votre note précédente." +reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser + « + » pour ajouter." +rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes + \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication + de votre note précédente." attachCancel: "Supprimer le fichier attaché" markAsSensitive: "Marquer comme sensible" unmarkAsSensitive: "Supprimer le marquage comme sensible" @@ -150,20 +150,20 @@ emojiUrl: "URL de l’émoji" addEmoji: "Ajouter un émoji" settingGuide: "Configuration proposée" cacheRemoteFiles: "Mise en cache des fichiers distants" -cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants\ - \ sont chargés directement depuis l’instance distante. La désactiver diminuera certes\ - \ l’utilisation de l’espace de stockage local mais augmentera le trafic réseau puisque\ - \ les miniatures ne seront plus générées." +cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants + sont chargés directement depuis l’instance distante. La désactiver diminuera certes + l’utilisation de l’espace de stockage local mais augmentera le trafic réseau puisque + les miniatures ne seront plus générées." flagAsBot: "Ce compte est un robot" -flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette\ - \ option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs\ - \ afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster\ - \ les systèmes internes de Firefish pour traiter ce compte comme un robot." +flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette + option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs + afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster + les systèmes internes de Calckey pour traiter ce compte comme un robot." flagAsCat: "Ce compte est un chat" flagAsCatDescription: "Activer l'option \" Je suis un chat \" pour ce compte." flagShowTimelineReplies: "Afficher les réponses dans le fil" -autoAcceptFollowed: "Accepter automatiquement les demandes d’abonnement venant d’utilisateur·rice·s\ - \ que vous suivez" +autoAcceptFollowed: "Accepter automatiquement les demandes d’abonnement venant d’utilisateur·rice·s + que vous suivez" addAccount: "Ajouter un compte" loginFailed: "Échec de la connexion" showOnRemote: "Voir sur l’instance distante" @@ -175,12 +175,12 @@ searchWith: "Recherche : {q}" youHaveNoLists: "Vous n’avez aucune liste" followConfirm: "Êtes-vous sûr·e de vouloir suivre {name} ?" proxyAccount: "Compte proxy" -proxyAccountDescription: "Un compte proxy se comporte, dans certaines conditions,\ - \ comme un·e abonné·e distant·e pour les utilisateurs d'autres instances. Par exemple,\ - \ quand un·e utilisateur·rice ajoute un·e utilisateur·rice distant·e à une liste,\ - \ ses notes ne seront pas visibles sur l'instance si personne ne suit cet·te utilisateur·rice.\ - \ Le compte proxy va donc suivre cet·te utilisateur·rice pour que ses notes soient\ - \ acheminées." +proxyAccountDescription: "Un compte proxy se comporte, dans certaines conditions, + comme un·e abonné·e distant·e pour les utilisateurs d'autres instances. Par exemple, + quand un·e utilisateur·rice ajoute un·e utilisateur·rice distant·e à une liste, + ses notes ne seront pas visibles sur l'instance si personne ne suit cet·te utilisateur·rice. + Le compte proxy va donc suivre cet·te utilisateur·rice pour que ses notes soient + acheminées." host: "Serveur distant" selectUser: "Sélectionner un·e utilisateur·rice" recipient: "Destinataire" @@ -210,14 +210,14 @@ instanceInfo: "Informations sur l’instance" statistics: "Statistiques" clearQueue: "Vider la file d’attente" clearQueueConfirmTitle: "Êtes-vous sûr·e de vouloir vider la file d’attente ?" -clearQueueConfirmText: "Les notes non distribuées ne seront pas délivrées. Normalement,\ - \ vous n'avez pas besoin d'effectuer cette opération." +clearQueueConfirmText: "Les notes non distribuées ne seront pas délivrées. Normalement, + vous n'avez pas besoin d'effectuer cette opération." clearCachedFiles: "Vider le cache" -clearCachedFilesConfirm: "Êtes-vous sûr·e de vouloir vider tout le cache de fichiers\ - \ distants ?" +clearCachedFilesConfirm: "Êtes-vous sûr·e de vouloir vider tout le cache de fichiers + distants ?" blockedInstances: "Instances bloquées" -blockedInstancesDescription: "Listez les instances que vous désirez bloquer, une par\ - \ ligne. Ces instances ne seront plus en capacité d'interagir avec votre instance." +blockedInstancesDescription: "Listez les instances que vous désirez bloquer, une par + ligne. Ces instances ne seront plus en capacité d'interagir avec votre instance." muteAndBlock: "Masqué·e·s / Bloqué·e·s" mutedUsers: "Utilisateur·rice·s en sourdine" blockedUsers: "Utilisateur·rice·s bloqué·e·s" @@ -270,8 +270,8 @@ fromUrl: "Depuis une URL" uploadFromUrl: "Téléverser via une URL" uploadFromUrlDescription: "URL du fichier que vous souhaitez téléverser" uploadFromUrlRequested: "Téléversement demandé" -uploadFromUrlMayTakeTime: "Le téléversement de votre fichier peut prendre un certain\ - \ temps." +uploadFromUrlMayTakeTime: "Le téléversement de votre fichier peut prendre un certain + temps." explore: "Découvrir" messageRead: "Lu" noMoreHistory: "Il n’y a plus d’historique" @@ -281,8 +281,8 @@ agreeTo: "J’accepte {0}" tos: "les conditions d’utilisation" start: "Commencer" home: "Principal" -remoteUserCaution: "Les informations de ce compte risqueraient d’être incomplètes\ - \ du fait que l’utilisateur·rice provient d’une instance distante." +remoteUserCaution: "Les informations de ce compte risqueraient d’être incomplètes + du fait que l’utilisateur·rice provient d’une instance distante." activity: "Activité" images: "Images" birthday: "Date de naissance" @@ -315,8 +315,8 @@ unableToDelete: "Suppression impossible" inputNewFileName: "Entrez un nouveau nom de fichier" inputNewDescription: "Veuillez entrer une nouvelle description" inputNewFolderName: "Entrez un nouveau nom de dossier" -circularReferenceFolder: "Le dossier de destination est un sous-dossier du dossier\ - \ que vous souhaitez déplacer." +circularReferenceFolder: "Le dossier de destination est un sous-dossier du dossier + que vous souhaitez déplacer." hasChildFilesOrFolders: "Impossible de supprimer ce dossier car il n'est pas vide." copyUrl: "Copier l’URL" rename: "Renommer" @@ -350,8 +350,8 @@ connectService: "Connexion" disconnectService: "Déconnexion" enableLocalTimeline: "Activer le fil local" enableGlobalTimeline: "Activer le fil global" -disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s\ - \ et les modérateur·rice·s pourront toujours y accéder." +disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s + et les modérateur·rice·s pourront toujours y accéder." registration: "S’inscrire" enableRegistration: "Autoriser les nouvelles inscriptions" invite: "Inviter" @@ -363,11 +363,11 @@ bannerUrl: "URL de l’image de la bannière" backgroundImageUrl: "URL de l'image d'arrière-plan" basicInfo: "Informations basiques" pinnedUsers: "Utilisateur·rice épinglé·e" -pinnedUsersDescription: "Listez les utilisateur·rice·s que vous souhaitez voir épinglé·e·s\ - \ sur la page \"Découvrir\", un·e par ligne." +pinnedUsersDescription: "Listez les utilisateur·rice·s que vous souhaitez voir épinglé·e·s + sur la page \"Découvrir\", un·e par ligne." pinnedPages: "Pages épinglées" -pinnedPagesDescription: "Inscrivez le chemin des pages que vous souhaitez épingler\ - \ en haut de la page de l'instance. Séparez les pages d'un retour à la ligne." +pinnedPagesDescription: "Inscrivez le chemin des pages que vous souhaitez épingler + en haut de la page de l'instance. Séparez les pages d'un retour à la ligne." pinnedClipId: "Identifiant du clip épinglé" pinnedNotes: "Note épinglée" hcaptcha: "hCaptcha" @@ -378,17 +378,17 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "Activer reCAPTCHA" recaptchaSiteKey: "Clé du site" recaptchaSecretKey: "Clé secrète" -avoidMultiCaptchaConfirm: "L’utilisation de plusieurs Captchas peut provoquer des\ - \ interférences. Souhaitez-vous désactiver l’autre Captcha ? Vous pouvez laisser\ - \ plusieurs Captcha activés en appuyant sur Annuler." +avoidMultiCaptchaConfirm: "L’utilisation de plusieurs Captchas peut provoquer des + interférences. Souhaitez-vous désactiver l’autre Captcha ? Vous pouvez laisser plusieurs + Captcha activés en appuyant sur Annuler." antennas: "Antennes" manageAntennas: "Gestion des antennes" name: "Nom" antennaSource: "Source de l’antenne" antennaKeywords: "Mots clés à recevoir" antennaExcludeKeywords: "Mots clés à exclure" -antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer\ - \ avec un saut de ligne pour une condition OR." +antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer + avec un saut de ligne pour une condition OR." notifyAntenna: "Je souhaite recevoir les notifications des nouvelles notes" withFileAntenna: "Notes ayant des attachements uniquement" enableServiceworker: "Activer ServiceWorker" @@ -399,11 +399,11 @@ connectedTo: "Vous êtes connectés aux services suivants" notesAndReplies: "Notes et Réponses" withFiles: "Avec fichiers joints" silence: "Mettre en sourdine" -silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en sourdine\ - \ ?" +silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en sourdine + ?" unsilence: "Annuler la sourdine" -unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cet·te\ - \ utilisateur·rice ?" +unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cet·te + utilisateur·rice ?" popularUsers: "Utilisateur·rice·s populaires" recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment" recentlyRegisteredUsers: "Utilisateur·rice·s récemment inscrit·e·s" @@ -468,8 +468,8 @@ invitationCode: "Code d’invitation" checking: "Vérification en cours..." available: "Disponible" unavailable: "Non disponible" -usernameInvalidFormat: "Le nom d'utilisateur peut contenir uniquement des lettres\ - \ (minuscules et/ou majuscules), des chiffres et des _" +usernameInvalidFormat: "Le nom d'utilisateur peut contenir uniquement des lettres + (minuscules et/ou majuscules), des chiffres et des _" tooShort: "Trop court" tooLong: "Trop long" weakPassword: "Mot de passe faible" @@ -478,8 +478,8 @@ strongPassword: "Mot de passe fort" passwordMatched: "Les mots de passe correspondent" passwordNotMatched: "Les mots de passe ne correspondent pas" signinWith: "Se connecter avec {x}" -signinFailed: "Échec d’authentification. Veuillez vérifier que votre nom d’utilisateur\ - \ et mot de passe sont corrects." +signinFailed: "Échec d’authentification. Veuillez vérifier que votre nom d’utilisateur + et mot de passe sont corrects." tapSecurityKey: "Appuyez sur votre clé de sécurité" or: "OU" language: "Langue" @@ -488,8 +488,8 @@ groupInvited: "Invité au groupe" aboutX: "À propos de {x}" useOsNativeEmojis: "Utiliser les émojis natifs du système" youHaveNoGroups: "Vous n’avez aucun groupe" -joinOrCreateGroup: "Vous pouvez être invité·e à rejoindre des groupes existants ou\ - \ créer votre propre nouveau groupe." +joinOrCreateGroup: "Vous pouvez être invité·e à rejoindre des groupes existants ou + créer votre propre nouveau groupe." noHistory: "Pas d'historique" signinHistory: "Historique de connexion" disableAnimatedMfm: "Désactiver MFM ayant des animations" @@ -520,29 +520,29 @@ showFeaturedNotesInTimeline: "Afficher les notes des Tendances dans le fil d'act objectStorage: "Stockage d'objets" useObjectStorage: "Utiliser le stockage d'objets" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "Préfixe d’URL utilisé pour construire l’URL vers le référencement\ - \ d’objet (média). Spécifiez son URL si vous utilisez un CDN ou un proxy, sinon\ - \ spécifiez l’adresse accessible au public selon le guide de service que vous allez\ - \ utiliser. P.ex. 'https://.s3.amazonaws.com' pour AWS S3 et 'https://storage.googleapis.com/'\ - \ pour GCS." +objectStorageBaseUrlDesc: "Préfixe d’URL utilisé pour construire l’URL vers le référencement + d’objet (média). Spécifiez son URL si vous utilisez un CDN ou un proxy, sinon spécifiez + l’adresse accessible au public selon le guide de service que vous allez utiliser. + P.ex. 'https://.s3.amazonaws.com' pour AWS S3 et 'https://storage.googleapis.com/' + pour GCS." objectStorageBucket: "Bucket" -objectStorageBucketDesc: "Veuillez spécifier le nom du compartiment utilisé sur le\ - \ service configuré." +objectStorageBucketDesc: "Veuillez spécifier le nom du compartiment utilisé sur le + service configuré." objectStoragePrefix: "Prefix" objectStoragePrefixDesc: "Les fichiers seront stockés sous le répertoire de ce préfixe." objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "Laissez ce champ vide si vous utilisez AWS S3, sinon spécifiez\ - \ le point de terminaison comme '' ou ': ' selon le guide de service\ - \ que vous allez utiliser." +objectStorageEndpointDesc: "Laissez ce champ vide si vous utilisez AWS S3, sinon spécifiez + le point de terminaison comme '' ou ': ' selon le guide de service + que vous allez utiliser." objectStorageRegion: "Région" -objectStorageRegionDesc: "Spécifiez une région comme 'xx-east-1'. Si votre service\ - \ ne fait pas de distinction entre les régions, laissez-le vide ou remplissez 'us-east-1'." +objectStorageRegionDesc: "Spécifiez une région comme 'xx-east-1'. Si votre service + ne fait pas de distinction entre les régions, laissez-le vide ou remplissez 'us-east-1'." objectStorageUseSSL: "Utiliser SSL" -objectStorageUseSSLDesc: "Désactivez cette option si vous n'utilisez pas HTTPS pour\ - \ la connexion API" +objectStorageUseSSLDesc: "Désactivez cette option si vous n'utilisez pas HTTPS pour + la connexion API" objectStorageUseProxy: "Se connecter via proxy" -objectStorageUseProxyDesc: "Désactivez cette option si vous n'utilisez pas de proxy\ - \ pour la connexion API" +objectStorageUseProxyDesc: "Désactivez cette option si vous n'utilisez pas de proxy + pour la connexion API" objectStorageSetPublicRead: "Régler sur « public » lors de l'envoi" serverLogs: "Journal du serveur" deleteAll: "Supprimer tout" @@ -570,9 +570,9 @@ sort: "Trier" ascendingOrder: "Ascendant" descendingOrder: "Descendant" scratchpad: "ScratchPad" -scratchpadDescription: "ScratchPad fournit un environnement expérimental pour AiScript.\ - \ Vous pouvez vérifier la rédaction de votre code, sa bonne exécution et le résultat\ - \ de son interaction avec Firefish." +scratchpadDescription: "ScratchPad fournit un environnement expérimental pour AiScript. + Vous pouvez vérifier la rédaction de votre code, sa bonne exécution et le résultat + de son interaction avec Calckey." output: "Sortie" script: "Script" disablePagesScript: "Désactiver AiScript sur les Pages" @@ -580,15 +580,15 @@ updateRemoteUser: "Mettre à jour les informations de l’utilisateur·rice dist deleteAllFiles: "Supprimer tous les fichiers" deleteAllFilesConfirm: "Êtes-vous sûr·e de vouloir supprimer tous les fichiers ?" removeAllFollowing: "Retenir tous les abonnements" -removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Veuillez\ - \ lancer cette action uniquement si l’instance n’existe plus." +removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Veuillez + lancer cette action uniquement si l’instance n’existe plus." userSuspended: "Cet·te utilisateur·rice a été suspendu·e." userSilenced: "Cette utilisateur·trice a été mis·e en sourdine." yourAccountSuspendedTitle: "Ce compte est suspendu" -yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les\ - \ conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez\ - \ connaître en détail les raisons de cette suspension, renseignez-vous auprès de\ - \ l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte." +yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les + conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez + connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice + de votre instance. Merci de ne pas créer de nouveau compte." menu: "Menu" divider: "Séparateur" addItem: "Ajouter un élément" @@ -611,8 +611,8 @@ description: "Description" describeFile: "Ajouter une description d'image" enterFileDescription: "Saisissez une description" author: "Auteur·rice" -leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer\ - \ ?" +leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer + ?" manage: "Gestion" plugins: "Extensions" deck: "Deck" @@ -629,14 +629,14 @@ permission: "Autorisations" enableAll: "Tout activer" disableAll: "Tout désactiver" tokenRequested: "Autoriser l'accès au compte" -pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies\ - \ ici." +pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies + ici." notificationType: "Type de notifications" edit: "Editer" emailServer: "Serveur mail" enableEmail: "Activer la distribution de courriel" -emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation\ - \ de votre mot de passe en cas d’oubli." +emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation + de votre mot de passe en cas d’oubli." email: "E-mail " emailAddress: "Adresses e-mail" smtpConfig: "Paramètres du serveur SMTP" @@ -644,8 +644,8 @@ smtpHost: "Serveur distant" smtpPort: "Port" smtpUser: "Nom d’utilisateur·rice" smtpPass: "Mot de passe" -emptyToDisableSmtpAuth: "Laisser le nom d’utilisateur et le mot de passe vides pour\ - \ désactiver la vérification SMTP" +emptyToDisableSmtpAuth: "Laisser le nom d’utilisateur et le mot de passe vides pour + désactiver la vérification SMTP" smtpSecure: "Utiliser SSL/TLS implicitement dans les connexions SMTP" smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé" testEmail: "Tester la distribution de courriel" @@ -666,24 +666,24 @@ create: "Créer" notificationSetting: "Paramètres des notifications " notificationSettingDesc: "Sélectionnez le type de notification à afficher" useGlobalSetting: "Utiliser paramètre général" -useGlobalSettingDesc: "S'il est activé, les paramètres de notification de votre compte\ - \ seront utilisés. S'il est désactivé, des configurations individuelles peuvent\ - \ être effectuées." +useGlobalSettingDesc: "S'il est activé, les paramètres de notification de votre compte + seront utilisés. S'il est désactivé, des configurations individuelles peuvent être + effectuées." other: "Autre" regenerateLoginToken: "Régénérer le jeton de connexion" -regenerateLoginTokenDescription: "Générer un nouveau jeton d'authentification. Cette\ - \ opération ne devrait pas être nécessaire ; lors de la génération d'un nouveau\ - \ jeton, tous les appareils seront déconnectés. " -setMultipleBySeparatingWithSpace: "Vous pouvez en définir plusieurs, en les séparant\ - \ par des espaces." +regenerateLoginTokenDescription: "Générer un nouveau jeton d'authentification. Cette + opération ne devrait pas être nécessaire ; lors de la génération d'un nouveau jeton, + tous les appareils seront déconnectés. " +setMultipleBySeparatingWithSpace: "Vous pouvez en définir plusieurs, en les séparant + par des espaces." fileIdOrUrl: "ID du fichier ou URL" behavior: "Comportement" sample: "Exemple" abuseReports: "Signalements" reportAbuse: "Signaler" reportAbuseOf: "Signaler {name}" -fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement. S'il s'agit\ - \ d'une note précise, veuillez en donner le lien." +fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement. S'il s'agit + d'une note précise, veuillez en donner le lien." abuseReported: "Le rapport est envoyé. Merci." reporter: "Signalé par" reporteeOrigin: "Origine du signalement" @@ -694,8 +694,8 @@ abuseMarkAsResolved: "Marquer le signalement comme résolu" openInNewTab: "Ouvrir dans un nouvel onglet" openInSideView: "Ouvrir en vue latérale" defaultNavigationBehaviour: "Navigation par défaut" -editTheseSettingsMayBreakAccount: "La modification de ces paramètres peut endommager\ - \ votre compte." +editTheseSettingsMayBreakAccount: "La modification de ces paramètres peut endommager + votre compte." instanceTicker: "Nom de l'instance d'origine des notes" waitingFor: "En attente de {x}" random: "Aléatoire" @@ -707,8 +707,8 @@ createNew: "Créer nouveau" optional: "Facultatif" createNewClip: "Créer un nouveau clip" public: "Public" -i18nInfo: "Firefish est traduit dans différentes langues par des bénévoles. Vous pouvez\ - \ contribuer à {link}." +i18nInfo: "Calckey est traduit dans différentes langues par des bénévoles. Vous pouvez + contribuer à {link}." manageAccessTokens: "Gérer les jetons d'accès" accountInfo: " Informations du compte " notesCount: "Nombre de notes" @@ -727,16 +727,16 @@ no: "Non" driveFilesCount: "Nombre de fichiers dans le Drive" driveUsage: "Utilisation du Drive" noCrawle: "Refuser l'indexation par les robots" -noCrawleDescription: "Demandez aux moteurs de recherche de ne pas indexer votre page\ - \ de profil, vos notes, vos pages, etc." -lockedAccountInfo: "À moins que vous ne définissiez la visibilité de votre note sur\ - \ \"Abonné-e-s\", vos notes sont visibles par tous, même si vous exigez que les\ - \ demandes d'abonnement soient approuvées manuellement." +noCrawleDescription: "Demandez aux moteurs de recherche de ne pas indexer votre page + de profil, vos notes, vos pages, etc." +lockedAccountInfo: "À moins que vous ne définissiez la visibilité de votre note sur + \"Abonné-e-s\", vos notes sont visibles par tous, même si vous exigez que les demandes + d'abonnement soient approuvées manuellement." alwaysMarkSensitive: "Marquer les médias comme contenu sensible par défaut" loadRawImages: "Affichage complet des images jointes au lieu des vignettes" disableShowingAnimatedImages: "Désactiver l'animation des images" -verificationEmailSent: "Un e-mail de vérification a été envoyé. Veuillez accéder au\ - \ lien pour compléter la vérification." +verificationEmailSent: "Un e-mail de vérification a été envoyé. Veuillez accéder au + lien pour compléter la vérification." notSet: "Non défini" emailVerified: "Votre adresse e-mail a été vérifiée." noteFavoritesCount: "Nombre de notes dans les favoris" @@ -748,16 +748,16 @@ clips: "Clips" experimentalFeatures: "Fonctionnalités expérimentales" developer: "Développeur" makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"." -makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra\ - \ pas sur la page \"Découvrir\"." +makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra + pas sur la page \"Découvrir\"." showGapBetweenNotesInTimeline: "Afficher un écart entre les notes sur la Timeline" duplicate: "Duliquer" left: "Gauche" center: "Centrer" wide: "Large" narrow: "Condensé" -reloadToApplySetting: "Vos paramètres seront appliqués lorsque vous rechargerez la\ - \ page. Souhaitez-vous recharger ?" +reloadToApplySetting: "Vos paramètres seront appliqués lorsque vous rechargerez la + page. Souhaitez-vous recharger ?" needReloadToApply: "Ce paramètre s'appliquera après un rechargement." showTitlebar: "Afficher la barre de titre" clearCache: "Vider le cache" @@ -765,11 +765,11 @@ onlineUsersCount: "{n} utilisateur(s) en ligne" nUsers: "{n} utilisateur·rice·s" nNotes: "{n} Notes" sendErrorReports: "Envoyer les rapports d’erreur" -sendErrorReportsDescription: "Si vous activez l'envoi des rapports d'erreur, vous\ - \ contribuerez à améliorer la qualité de Firefish grâce au partage d'informations\ - \ détaillées sur les erreurs lorsqu'un problème survient.\nCela inclut des informations\ - \ telles que la version de votre système d'exploitation, le type de navigateur que\ - \ vous utilisez, votre historique d'activité, etc." +sendErrorReportsDescription: "Si vous activez l'envoi des rapports d'erreur, vous + contribuerez à améliorer la qualité de Calckey grâce au partage d'informations détaillées + sur les erreurs lorsqu'un problème survient.\nCela inclut des informations telles + que la version de votre système d'exploitation, le type de navigateur que vous utilisez, + votre historique d'activité, etc." myTheme: "Mes thèmes" backgroundColor: "Arrière-plan" accentColor: "Accentuation" @@ -808,17 +808,17 @@ unlikeConfirm: "Êtes-vous sûr·e de ne plus vouloir aimer cette publication ?" fullView: "Plein écran" quitFullView: "Quitter le plein écran" addDescription: "Ajouter une description" -userPagePinTip: "Vous pouvez afficher des notes ici en sélectionnant l'option « Épingler\ - \ au profil » dans le menu de chaque note." -notSpecifiedMentionWarning: "Vous avez mentionné des utilisateur·rice·s qui ne font\ - \ pas partie de la liste des destinataires" +userPagePinTip: "Vous pouvez afficher des notes ici en sélectionnant l'option « Épingler + au profil » dans le menu de chaque note." +notSpecifiedMentionWarning: "Vous avez mentionné des utilisateur·rice·s qui ne font + pas partie de la liste des destinataires" info: "Informations" userInfo: "Informations sur l'utilisateur" unknown: "Inconnu" onlineStatus: "Statut" hideOnlineStatus: "Se rendre invisible" -hideOnlineStatusDescription: "Rendre votre statut invisible peut diminuer les performances\ - \ de certaines fonctionnalités, telles que la Recherche." +hideOnlineStatusDescription: "Rendre votre statut invisible peut diminuer les performances + de certaines fonctionnalités, telles que la Recherche." online: "En ligne" active: "Actif·ve" offline: "Hors ligne" @@ -853,9 +853,9 @@ emailNotConfiguredWarning: "Vous n'avez pas configuré d'adresse e-mail." ratio: "Ratio" previewNoteText: "Voir l'aperçu" customCss: "CSS personnalisé" -customCssWarn: "Utilisez cette fonctionnalité uniquement si vous savez exactement\ - \ ce que vous faites. Une configuration inadaptée peut empêcher le client de s'exécuter\ - \ normalement." +customCssWarn: "Utilisez cette fonctionnalité uniquement si vous savez exactement + ce que vous faites. Une configuration inadaptée peut empêcher le client de s'exécuter + normalement." global: "Global" squareAvatars: "Avatars carrés" sent: "Envoyer" @@ -870,10 +870,10 @@ whatIsNew: "Voir les derniers changements" translate: "Traduire" translatedFrom: "Traduit depuis {x}" accountDeletionInProgress: "La suppression de votre compte est en cours" -usernameInfo: "C'est un nom qui identifie votre compte sur l'instance de manière unique.\ - \ Vous pouvez utiliser des lettres de l'alphabet (minuscules et majuscules), des\ - \ chiffres (de 0 à 9), ou bien le tiret « _ ». Vous ne pourrez pas modifier votre\ - \ nom d'utilisateur·rice par la suite." +usernameInfo: "C'est un nom qui identifie votre compte sur l'instance de manière unique. + Vous pouvez utiliser des lettres de l'alphabet (minuscules et majuscules), des chiffres + (de 0 à 9), ou bien le tiret « _ ». Vous ne pourrez pas modifier votre nom d'utilisateur·rice + par la suite." aiChanMode: "Mode Ai" keepCw: "Garder le CW" pubSub: "Comptes Pub/Sub" @@ -889,14 +889,14 @@ filter: "Filtre" controlPanel: "Panneau de contrôle" manageAccounts: "Gérer les comptes" makeReactionsPublic: "Rendre les réactions publiques" -makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données\ - \ publique." +makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données + publique." classic: "Classique" muteThread: "Masquer cette discussion" unmuteThread: "Ne plus masquer le fil" ffVisibility: "Visibilité des abonnés/abonnements" -ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu\ - \ suis et les personnes qui te suivent." +ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu + suis et les personnes qui te suivent." continueThread: "Afficher la suite du fil" deleteAccountConfirm: "Votre compte sera supprimé. Êtes vous certain ?" incorrectPassword: "Le mot de passe est incorrect." @@ -904,11 +904,11 @@ voteConfirm: "Confirmez-vous votre vote pour « {choice} » ?" hide: "Masquer" leaveGroup: "Quitter le groupe" leaveGroupConfirm: "Êtes vous sûr de vouloir quitter \"{name}\" ?" -useDrawerReactionPickerForMobile: "Afficher le sélecteur de réactions en tant que\ - \ panneau sur mobile" +useDrawerReactionPickerForMobile: "Afficher le sélecteur de réactions en tant que + panneau sur mobile" welcomeBackWithName: "Heureux de vous revoir, {name}" -clickToFinishEmailVerification: "Veuillez cliquer sur [{ok}] afin de compléter la\ - \ vérification par courriel." +clickToFinishEmailVerification: "Veuillez cliquer sur [{ok}] afin de compléter la + vérification par courriel." overridedDeviceKind: "Type d’appareil" smartphone: "Smartphone" tablet: "Tablette" @@ -948,16 +948,16 @@ _ffVisibility: _signup: almostThere: "Bientôt fini" emailAddressInfo: "Insérez votre adresse e-mail." - emailSent: "Un courriel de confirmation vient d'être envoyé à l'adresse que vous\ - \ avez renseignée ({email}). Cliquez sur le lien contenu dans le message pour\ - \ terminer la création de votre compte." + emailSent: "Un courriel de confirmation vient d'être envoyé à l'adresse que vous + avez renseignée ({email}). Cliquez sur le lien contenu dans le message pour terminer + la création de votre compte." _accountDelete: accountDelete: "Supprimer le compte" - mayTakeTime: "La suppression de compte nécessitant beaucoup de ressources, l'exécution\ - \ du processus peut prendre du temps, en fonction de la quantité de contenus que\ - \ vous avez créés et du nombre de fichiers que vous avez téléversés." - sendEmail: "Une fois la suppression de votre compte effectuée, un courriel sera\ - \ envoyé à l'adresse que vous aviez enregistrée." + mayTakeTime: "La suppression de compte nécessitant beaucoup de ressources, l'exécution + du processus peut prendre du temps, en fonction de la quantité de contenus que + vous avez créés et du nombre de fichiers que vous avez téléversés." + sendEmail: "Une fois la suppression de votre compte effectuée, un courriel sera + envoyé à l'adresse que vous aviez enregistrée." requestAccountDelete: "Demander la suppression de votre compte" started: "La procédure de suppression a commencé." inProgress: "Suppression en cours" @@ -965,14 +965,14 @@ _ad: back: "Retour" reduceFrequencyOfThisAd: "Voir cette publicité moins souvent" _forgotPassword: - enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte.\ - \ Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette\ - \ adresse." - ifNoEmail: "Si vous n'avez pas enregistré d'adresse e-mail, merci de contacter l'administrateur·rice\ - \ de votre instance." - contactAdmin: "Cette instance ne permettant pas l'utilisation d'adresses e-mail,\ - \ prenez contact avec l'administrateur·rice pour procéder à la réinitialisation\ - \ de votre mot de passe." + enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte. + Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette + adresse." + ifNoEmail: "Si vous n'avez pas enregistré d'adresse e-mail, merci de contacter l'administrateur·rice + de votre instance." + contactAdmin: "Cette instance ne permettant pas l'utilisation d'adresses e-mail, + prenez contact avec l'administrateur·rice pour procéder à la réinitialisation + de votre mot de passe." _gallery: my: "Mes publications" liked: " Publications que j'ai aimées" @@ -998,10 +998,10 @@ _aboutMisskey: contributors: "Principaux contributeurs" allContributors: "Tous les contributeurs" source: "Code source" - translation: "Traduire Firefish" - donate: "Soutenir Firefish" - morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes\ - \ non mentionnées ici. Merci à toutes et à tous ! \U0001F970" + translation: "Traduire Calckey" + donate: "Soutenir Calckey" + morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes + non mentionnées ici. Merci à toutes et à tous ! 🥰" patrons: "Contributeurs" _nsfw: respect: "Cacher les médias marqués comme contenu sensible" @@ -1009,22 +1009,22 @@ _nsfw: force: "Cacher tous les médias" _mfm: cheatSheet: "Antisèche MFM" - intro: "MFM est un langage Markdown spécifique utilisable ici et là dans Firefish.\ - \ Vous pouvez vérifier ici les structures utilisables avec MFM." - dummy: "La Fédiverse s'agrandit avec Firefish" + intro: "MFM est un langage Markdown spécifique utilisable ici et là dans Calckey. + Vous pouvez vérifier ici les structures utilisables avec MFM." + dummy: "La Fédiverse s'agrandit avec Calckey" mention: "Mentionner" - mentionDescription: "Vous pouvez afficher un utilisateur spécifique en indiquant\ - \ une arobase suivie d'un nom d'utilisateur" + mentionDescription: "Vous pouvez afficher un utilisateur spécifique en indiquant + une arobase suivie d'un nom d'utilisateur" hashtag: "Hashtags" - hashtagDescription: "Vous pouvez afficher un mot-dièse en utilisant un croisillon\ - \ et du texte" + hashtagDescription: "Vous pouvez afficher un mot-dièse en utilisant un croisillon + et du texte" url: "URL" urlDescription: "L'adresse web peut être affichée." link: "Lien" linkDescription: "Une partie précise d'une phrase peut être liée à l'adresse web." bold: "Gras" - boldDescription: "Il est possible de mettre le texte en exergue en le mettant en\ - \ gras." + boldDescription: "Il est possible de mettre le texte en exergue en le mettant en + gras." small: "Diminuer l'emphase" smallDescription: "Le contenu peut être affiché en petit et fin." center: "Centrer" @@ -1036,8 +1036,8 @@ _mfm: inlineMath: "Formule mathématique (inline)" inlineMathDescription: "Afficher les formules mathématiques (KaTeX)." blockMath: "Formule mathématique (bloc)" - blockMathDescription: "Afficher les formules mathématiques (KaTeX) multi-lignes\ - \ dans un bloc." + blockMathDescription: "Afficher les formules mathématiques (KaTeX) multi-lignes + dans un bloc." quote: "Citer" quoteDescription: "Affiche le contenu sous forme de citation." emoji: "Émojis personnalisés" @@ -1067,8 +1067,8 @@ _mfm: x4: "Plus grand" x4Description: "Afficher le contenu en plus grand." blur: "Flou" - blurDescription: "Le contenu peut être flouté ; il sera visible en le survolant\ - \ avec le curseur." + blurDescription: "Le contenu peut être flouté ; il sera visible en le survolant + avec le curseur." font: "Police de caractères" fontDescription: "Il est possible de choisir la police." rainbow: "Arc-en-ciel" @@ -1109,14 +1109,14 @@ _menuDisplay: hide: "Masquer" _wordMute: muteWords: "Mots à filtrer" - muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec\ - \ un saut de ligne pour une condition OR." - muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez\ - \ les mots-clés entre barres obliques." + muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec + un saut de ligne pour une condition OR." + muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez + les mots-clés entre barres obliques." softDescription: "Masquez les notes de votre fil selon les paramètres que vous définissez." - hardDescription: "Empêchez votre fil de charger les notes selon les paramètres que\ - \ vous définissez. Cette action est irréversible : si vous modifiez ces paramètres\ - \ plus tard, les notes précédemment filtrées ne seront pas récupérées." + hardDescription: "Empêchez votre fil de charger les notes selon les paramètres que + vous définissez. Cette action est irréversible : si vous modifiez ces paramètres + plus tard, les notes précédemment filtrées ne seront pas récupérées." soft: "Doux" hard: "Strict" mutedNotes: "Notes filtrées" @@ -1155,10 +1155,10 @@ _theme: darken: "Sombre" lighten: "Clair" inputConstantName: "Insérez un nom de constante" - importInfo: "Vous pouvez importer un thème vers l’éditeur de thèmes en saisissant\ - \ son code ici." - deleteConstantConfirm: "Êtes-vous sûr·e de vouloir supprimer la constante {const}\ - \ ?" + importInfo: "Vous pouvez importer un thème vers l’éditeur de thèmes en saisissant + son code ici." + deleteConstantConfirm: "Êtes-vous sûr·e de vouloir supprimer la constante {const} + ?" keys: accent: "Accentuation" bg: "Arrière-plan" @@ -1231,51 +1231,51 @@ _tutorial: step1_1: "Bienvenue!" step1_2: "On va vous installer. Vous serez opérationnel en un rien de temps" step2_1: "Tout d'abord, remplissez votre profil" - step2_2: "En fournissant quelques informations sur qui vous êtes, il sera plus facile\ - \ pour les autres de savoir s'ils veulent voir vos notes ou vous suivre." + step2_2: "En fournissant quelques informations sur qui vous êtes, il sera plus facile + pour les autres de savoir s'ils veulent voir vos notes ou vous suivre." step3_1: "Maintenant il est temps de suivre des gens !" - step3_2: "Votre page d'accueil et vos timelines sociales sont basées sur les personnes\ - \ que vous suivez, alors essayez de suivre quelques comptes pour commencer.\n\ - Cliquez sur le cercle plus en haut à droite d'un profil pour le suivre." + step3_2: "Votre page d'accueil et vos timelines sociales sont basées sur les personnes + que vous suivez, alors essayez de suivre quelques comptes pour commencer.\nCliquez + sur le cercle plus en haut à droite d'un profil pour le suivre." step4_1: "On y va." - step4_2: "Pour votre premier post, certaines personnes aiment faire un post {introduction}\ - \ ou un simple post 'Hello world'." + step4_2: "Pour votre premier post, certaines personnes aiment faire un post {introduction} + ou un simple post 'Hello world'." step5_1: "Lignes de temps, lignes de temps partout !" step5_2: "Votre instance a {timelines} différentes chronologies activées !" - step5_3: "La timeline Home {icon} est l'endroit où vous pouvez voir les publications\ - \ de vos followers." - step5_4: "La timeline locale {icon} est l'endroit où vous pouvez voir les messages\ - \ de tout le monde sur cette instance." - step5_5: "La chronologie {icon} sociale est l'endroit où vous pouvez voir uniquement\ - \ les publications des comptes que vous suivez." - step5_6: "La chronologie {icon} recommandée est l'endroit où vous pouvez voir les\ - \ publications des instances recommandées par les administrateurs." - step5_7: "La timeline globale {icon} est l'endroit où vous pouvez voir les messages\ - \ de toutes les autres instances connectées." + step5_3: "La timeline Home {icon} est l'endroit où vous pouvez voir les publications + de vos followers." + step5_4: "La timeline locale {icon} est l'endroit où vous pouvez voir les messages + de tout le monde sur cette instance." + step5_5: "La chronologie {icon} sociale est l'endroit où vous pouvez voir uniquement + les publications des comptes que vous suivez." + step5_6: "La chronologie {icon} recommandée est l'endroit où vous pouvez voir les + publications des instances recommandées par les administrateurs." + step5_7: "La timeline globale {icon} est l'endroit où vous pouvez voir les messages + de toutes les autres instances connectées." step6_1: "Alors quel est cet endroit ?" - step6_2: "Eh bien, vous ne venez pas de rejoindre Firefish. Vous avez rejoint un\ - \ portail vers le Fediverse, un réseau interconnecté de milliers de serveurs,\ - \ appelés \"instances\"." - step6_3: "Chaque serveur fonctionne différemment, et tous les serveurs n'utilisent\ - \ pas Firefish. Cependant, celui-ci le fait ! C'est un peu délicat, mais vous aurez\ - \ le coup de main en un rien de temps." + step6_2: "Eh bien, vous ne venez pas de rejoindre Calckey. Vous avez rejoint un + portail vers le Fediverse, un réseau interconnecté de milliers de serveurs, appelés + \"instances\"." + step6_3: "Chaque serveur fonctionne différemment, et tous les serveurs n'utilisent + pas Calckey. Cependant, celui-ci le fait ! C'est un peu délicat, mais vous aurez + le coup de main en un rien de temps." step6_4: "Maintenant, allez-y, explorez et amusez-vous !" _2fa: alreadyRegistered: "Configuration déjà achevée." registerTOTP: "Ajouter un nouvel appareil" registerSecurityKey: "Enregistrer une clef" - step1: "Tout d'abord, installez une application d'authentification, telle que {a}\ - \ ou {b}, sur votre appareil." + step1: "Tout d'abord, installez une application d'authentification, telle que {a} + ou {b}, sur votre appareil." step2: "Ensuite, scannez le code QR affiché sur l’écran." - step2Url: "Vous pouvez également saisir cette URL si vous utilisez un programme\ - \ de bureau :" + step2Url: "Vous pouvez également saisir cette URL si vous utilisez un programme + de bureau :" step3: "Entrez le jeton affiché sur votre application pour compléter la configuration." - step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos\ - \ connexions." - securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser\ - \ davantage le processus de connexion grâce à une clé de sécurité matérielle qui\ - \ prend en charge FIDO2, ou bien en configurant l'authentification par empreinte\ - \ digitale ou par code PIN sur votre appareil." + step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos + connexions." + securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser + davantage le processus de connexion grâce à une clé de sécurité matérielle qui + prend en charge FIDO2, ou bien en configurant l'authentification par empreinte + digitale ou par code PIN sur votre appareil." _permissions: "read:account": "Afficher les informations du compte" "write:account": "Mettre à jour les informations de votre compte" @@ -1311,8 +1311,8 @@ _permissions: "write:gallery-likes": "Gérer les mentions « J'aime » dans la galerie" _auth: shareAccess: "Autoriser \"{name}\" à accéder à votre compte ?" - shareAccessAsk: "Voulez-vous vraiment autoriser cette application à accéder à votre\ - \ compte?" + shareAccessAsk: "Voulez-vous vraiment autoriser cette application à accéder à votre + compte?" permissionAsk: "Cette application nécessite les autorisations suivantes :" pleaseGoBack: "Veuillez retourner à l’application" callback: "Retour vers l’application" @@ -1412,8 +1412,8 @@ _profile: youCanIncludeHashtags: "Vous pouvez également inclure des hashtags." metadata: "Informations supplémentaires" metadataEdit: "Éditer les informations supplémentaires" - metadataDescription: "Vous pouvez afficher jusqu'à quatre informations supplémentaires\ - \ dans votre profil." + metadataDescription: "Vous pouvez afficher jusqu'à quatre informations supplémentaires + dans votre profil." metadataLabel: "Étiquette" metadataContent: "Contenu" changeAvatar: "Changer l'image de profil" @@ -1487,8 +1487,8 @@ _pages: url: "URL de la page" summary: "Résumé de page" alignCenter: "Centrée" - hideTitleWhenPinned: "Masquer le titre de la page lorsque celle-ci est épinglée\ - \ au profil" + hideTitleWhenPinned: "Masquer le titre de la page lorsque celle-ci est épinglée + au profil" font: "Police de caractères" fontSerif: "Serif" fontSansSerif: "Sans Serif" @@ -1538,8 +1538,8 @@ _pages: note: "Note intégrée" _note: id: "Identifiant de la note" - idDescription: "Pour configurer la note, vous pouvez aussi coller ici l'URL\ - \ correspondante." + idDescription: "Pour configurer la note, vous pouvez aussi coller ici l'URL + correspondante." detailed: "Afficher les détails" switch: "Interrupteur" _switch: @@ -1692,8 +1692,8 @@ _pages: _dailyRannum: arg1: "Minimum" arg2: "Maximum" - dailyRandomPick: "Sélectionné au hasard dans la liste (Quotidien pour chaque\ - \ utilisateur)" + dailyRandomPick: "Sélectionné au hasard dans la liste (Quotidien pour chaque + utilisateur)" _dailyRandomPick: arg1: "Listes" seedRandom: "Aléatoire (graine)" @@ -1709,8 +1709,8 @@ _pages: _seedRandomPick: arg1: "Graine" arg2: "Listes" - DRPWPM: "Sélectionné au hasard dans une liste de probabilités (Quotidien pour\ - \ chaque utilisateur)" + DRPWPM: "Sélectionné au hasard dans une liste de probabilités (Quotidien pour + chaque utilisateur)" _DRPWPM: arg1: "Liste de texte" pick: "Sélectionner dans la liste" @@ -1889,10 +1889,10 @@ adminCustomCssWarn: Ce paramètre ne devrait être utilisé que si vous savez ce dans vos paramètres utilisateur. swipeOnDesktop: Permettre le style de glissement de fenêtre de mobile sur PC moveFromLabel: 'Compte depuis lequel vous migrez :' -migrationConfirm: "Êtes-vous absolument certain⋅e que vous voulez migrer votre compte\ - \ vers {account} ? Une fois fait, vous ne pourrez pas revenir en arrière, et vous\ - \ ne pourrez plus utiliser le compte actuel normalement à nouveau.\nAussi, assurez-vous\ - \ d'avoir configuré le compte actuel comme le compte depuis lequel vous migrez." +migrationConfirm: "Êtes-vous absolument certain⋅e que vous voulez migrer votre compte + vers {account} ? Une fois fait, vous ne pourrez pas revenir en arrière, et vous + ne pourrez plus utiliser le compte actuel normalement à nouveau.\nAussi, assurez-vous + d'avoir configuré le compte actuel comme le compte depuis lequel vous migrez." _preferencesBackups: updatedAt: 'Mis à jour le : {date} {time}' cannotLoad: Le chargement a échoué @@ -1934,8 +1934,8 @@ enterSendsMessage: Appuyer sur Entrée pendant la rédaction pour envoyer le mes allowedInstancesDescription: Hôtes des instances autorisées pour la fédération, chacun séparé par une nouvelle ligne (s'applique uniquement en mode privé). enableAutoSensitive: Marquage automatique du contenu sensible (NSFW) -regexpErrorDescription: "Il y a eu une erreur dans l'expression régulière à la ligne\ - \ {line} de votre {tab} des mots masqués :" +regexpErrorDescription: "Il y a eu une erreur dans l'expression régulière à la ligne + {line} de votre {tab} des mots masqués :" forwardReportIsAnonymous: À la place de votre compte, un compte système anonyme sera affiché comme rapporteur à l'instance distante. noThankYou: Non merci @@ -1944,16 +1944,15 @@ renoteMute: Mettre en silence les renotes flagSpeakAsCat: Parler comme un chat flagSpeakAsCatDescription: Vos messages seront nyanifiés en mode chat hiddenTags: Hashtags cachés -hiddenTagsDescription: "Lister les hashtags (sans le #) que vous souhaitez cacher\ - \ de tendances et explorer. Les hashtags cachés sont toujours découvrables par d'autres\ - \ moyens. Les instances bloqués ne sont pas ne sont pas affectés, même si ils sont\ - \ présent dans cette liste." +hiddenTagsDescription: "Lister les hashtags (sans le #) que vous souhaitez cacher + de tendances et explorer. Les hashtags cachés sont toujours découvrables par d'autres + moyens. Les instances bloqués ne sont pas ne sont pas affectés, même si ils sont + présent dans cette liste." antennaInstancesDescription: Lister un hôte d'instance par ligne userSaysSomethingReason: '{name} a dit {reason}' breakFollowConfirm: Êtes vous sur de vouloir retirer l'abonné ? recommendedInstancesDescription: Instances recommandées séparées par une nouvelle - ligne pour apparaître dans la timeline recommandée. Ne PAS ajouter `https://`, SEULEMENT - le domaine. + ligne pour apparaître dans la timeline recommandée. sendPushNotificationReadMessage: Supprimer les notifications push une fois que les notifications ou messages concernés ont été lus sendPushNotificationReadMessageCaption: Une notification contenant le texte "{emptyPushNotificationMessage}" @@ -2005,17 +2004,16 @@ indexNotice: Indexation en cours. Cela prendra certainement du temps, veuillez n customKaTeXMacro: Macros KaTeX personnalisées enableCustomKaTeXMacro: Activer les macros KaTeX personnalisées noteId: ID de note -customKaTeXMacroDescription: "Définissez des macros pour écrire des expressions mathématiques\ - \ simplement ! La notation se conforme aux définitions de commandes LaTeX et s'écrit\ - \ \\newcommand{\\name}{content} ou \\newcommand{\\name}[number of arguments]{content}.\ - \ Par exemple, \\newcommand{\\add}[2]{#1 + #2} étendra \\add{3}{foo} en 3 + foo.\ - \ Les accolades entourant le nom de la macro peuvent être changés pour des parenthèses\ - \ ou des crochets. Cela affectera les types de parenthèses utilisées pour les arguments.\ - \ Une (et une seule) macro peut être définie par ligne, et vous ne pouvez pas couper\ - \ la ligne au milieu d'une définition. Les lignes invalides sont simplement ignorées.\ - \ Seulement de simples fonctions de substitution de chaines sont supportées; la\ - \ syntaxe avancée, telle que la ramification conditionnelle, ne peut pas être utilisée\ - \ ici." +customKaTeXMacroDescription: "Définissez des macros pour écrire des expressions mathématiques + simplement ! La notation se conforme aux définitions de commandes LaTeX et s'écrit + \\newcommand{\\name}{content} ou \\newcommand{\\name}[number of arguments]{content}. + Par exemple, \\newcommand{\\add}[2]{#1 + #2} étendra \\add{3}{foo} en 3 + foo. Les + accolades entourant le nom de la macro peuvent être changés pour des parenthèses + ou des crochets. Cela affectera les types de parenthèses utilisées pour les arguments. + Une (et une seule) macro peut être définie par ligne, et vous ne pouvez pas couper + la ligne au milieu d'une définition. Les lignes invalides sont simplement ignorées. + Seulement de simples fonctions de substitution de chaines sont supportées; la syntaxe + avancée, telle que la ramification conditionnelle, ne peut pas être utilisée ici." enableRecommendedTimeline: Activer la chronologie recommandée silenceThisInstance: Ne plus montrer cet instance silencedInstances: Instances silencieuses @@ -2038,3 +2036,4 @@ signupsDisabled: Les inscriptions sur ce serveur sont actuellement désactivés, apps: Applications userSaysSomethingReasonReply: '{noms} a répondu à une note contenant {raison}' defaultValueIs: 'défaut : {valeur}' +searchPlaceholder: Recherchez sur Calckey diff --git a/locales/gl.yml b/locales/gl.yml new file mode 100644 index 000000000..891a81927 --- /dev/null +++ b/locales/gl.yml @@ -0,0 +1,17 @@ +_lang_: Inglés +introMisskey: Benvida! Calckey é unha plataforma de medios sociais de código aberto, + descentralizada e gratuíta para sempre!🚀 +monthAndDay: '{day}/{month}' +notifications: Notificacións +password: Contrasinal +forgotPassword: Esquecín o contrasinal +gotIt: Vale! +cancel: Cancelar +noThankYou: Non, grazas +headlineMisskey: Plataforma de medios sociais de código aberto e descentralizada, + gratuíta para sempre!🚀 +search: Buscar +searchPlaceholder: Buscar en Calckey +username: Identificador +fetchingAsApObject: Descargando desde o Fediverso +ok: OK diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 20a10f007..7f805200b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -946,7 +946,7 @@ customSplashIconsDescription: "ユーザがページをロード/リロードす URL。画像は静的なURLで、できればすべて192x192にリサイズしてください。" showUpdates: "Firefishの更新時にポップアップを表示する" recommendedInstances: "おすすめサーバー" -recommendedInstancesDescription: "おすすめタイムラインに表示するサーバーを改行区切りで入力してください。`https://`は書かず、ドメインのみを入力してください。" +recommendedInstancesDescription: "おすすめタイムラインに表示するサーバーを改行区切りで入力してください。" caption: "自動キャプション" splash: "スプラッシュスクリーン" updateAvailable: "アップデートがありますよ!" @@ -977,7 +977,14 @@ customKaTeXMacroDescription: "数式入力を楽にするためのマクロを enableCustomKaTeXMacro: "カスタムKaTeXマクロを有効にする" preventAiLearning: "AIによる学習を防止" preventAiLearningDescription: "投稿したノート、添付した画像などのコンテンツを学習の対象にしないようAIに要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されます。" -noGraze: "ブラウザの拡張機能「Graze for Mastodon」は、Firefishの動作を妨げるため、無効にしてください。" +noGraze: "ブラウザの拡張機能「Graze for Mastodon」は、Calckeyの動作を妨げるため、無効にしてください。" +enableServerMachineStats: "サーバーのマシン情報を公開する" +enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" +showPopup: "ポップアップを表示してユーザーに知らせる" +showWithSparkles: "タイトルをキラキラさせる" +youHaveUnreadAnnouncements: "未読のお知らせがあります" +neverShow: "今後表示しない" +remindMeLater: "また後で" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。" @@ -1062,6 +1069,11 @@ _aboutMisskey: donate: "Firefishに寄付" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰" patrons: "支援者" + patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう! + pleaseDonateToCalckey: Calckey開発への寄付をご検討ください。 + pleaseDonateToHost: また、このサーバー {host} の運営者への寄付もご検討ください。 + donateHost: '{host} に寄付する' + donateTitle: Calckeyを気に入りましたか? _nsfw: respect: "閲覧注意のメディアは隠す" ignore: "閲覧注意のメディアを隠さない" @@ -1373,11 +1385,12 @@ _permissions: _auth: shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?" shareAccessAsk: "アカウントへのアクセスを許可しますか?" - permissionAsk: "このアプリケーションは次の権限を要求しています" + permissionAsk: "このアプリケーションは次の権限を要求しています:" pleaseGoBack: "アプリケーションに戻り続行してください" callback: "アプリケーションに戻っています" denied: "アクセスを拒否しました" - copyAsk: "以下の認証コードをアプリケーションにコピーしてください" + copyAsk: "以下の認証コードをアプリケーションにコピーしてください:" + allPermissions: 全てのアクセス権 _antennaSources: all: "全ての投稿" homeTimeline: "フォローしているユーザーの投稿" @@ -1451,11 +1464,11 @@ _poll: remainingSeconds: "終了まであと{s}秒" _visibility: public: "公開" - publicDescription: "全てのユーザーに公開" + publicDescription: "全ての公開タイムラインに配信されます" home: "未収載" homeDescription: "ホームタイムラインのみに公開" followers: "フォロワー" - followersDescription: "自分のフォロワーのみに公開" + followersDescription: "フォロワーと会話相手のみに公開" specified: "ダイレクト" specifiedDescription: "指定したユーザーのみに公開" localOnly: "ローカルのみ" @@ -1888,14 +1901,14 @@ hiddenTags: 非表示にするハッシュタグ apps: "アプリ" _experiments: title: 試験的な機能 - postImportsCaption: - ユーザーが過去の投稿をFirefish・Misskey・Mastodon・Akkoma・Pleromaからインポートすることを許可します。キューが溜まっているときにインポートするとサーバーに負荷がかかる可能性があります。 + postImportsCaption: + ユーザーが過去の投稿をCalckey・Misskey・Mastodon・Akkoma・Pleromaからインポートすることを許可します。キューが溜まっているときにインポートするとサーバーに負荷がかかる可能性があります。 enablePostImports: 投稿のインポートを有効にする sendModMail: モデレーション通知を送る deleted: 削除済み editNote: 投稿を編集 edited: '編集済み: {date} {time}' -signupsDisabled: +signupsDisabled: 現在、このサーバーでは新規登録が一般開放されていません。招待コードをお持ちの場合には、以下の欄に入力してください。招待コードをお持ちでない場合にも、新規登録を開放している他のサーバーには入れますよ! findOtherInstance: 他のサーバーを探す newer: 新しい投稿 @@ -1929,4 +1942,20 @@ video: 動画 isBot: このアカウントはBotです isLocked: このアカウントのフォローは承認制です isAdmin: 管理者 -isPatron: Firefish 後援者 +isPatron: Calckey 後援者 +_skinTones: + light: ペールオレンジ + mediumLight: ミディアムライト + medium: ミディアム + mediumDark: ミディアムダーク + yellow: 黄色 + dark: 茶色 +removeReaction: リアクションを取り消す +alt: 代替テキスト +swipeOnMobile: ページ間のスワイプを有効にする +reactionPickerSkinTone: 優先する絵文字のスキン色 +xl: 特大 +donationLink: 寄付ページへのリンク +removeMember: メンバーを削除 +removeQuote: 引用を削除 +removeRecipient: 宛先を削除 diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 83e189b9c..5f02d5f19 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -1,2 +1,83 @@ ---- _lang_: "Norsk Bokmål" +search: Søk +monthAndDay: '{day}/{month}' +fetchingAsApObject: Henter fra fediverset +ok: OK +gotIt: Jeg forstår! +profile: Profil +timeline: Tidslinje +save: Lagre +addToList: Legg til liste +searchPlaceholder: Søk Calckey +username: Brukernavn +password: Passord +notifications: Meldinger +forgotPassword: Glemt passord +cancel: Avbryt +noNotes: Ingen poster +instance: Server +settings: Innstillinger +noAccountDescription: Denne brukeren har ikke fylt ut bio'en sin ennå. +login: Logg inn +loggingIn: Logger inn +signup: Oppretter bruker +uploading: Laster opp.. +enterUsername: Skriv inn brukernavn +noNotifications: Ingen meldinger +users: Brukere +addUser: Legg til en bruker +favorite: Legg til i bokmerker +cantFavorite: Kunne ikke legges til i bokmerker. +pin: Fest til profilen +copyContent: Kopier innhold +deleteAndEdit: Slett og rediger +sendMessage: Send en melding +copyUsername: Kopier brukernavn +reply: Svar +loadMore: Last mer +showLess: Lukk +receiveFollowRequest: Følgeforespørsel mottatt +directNotes: Direktemelding +importAndExport: Importer/eksporter data +importRequested: Du har bedt om en importering. Dette vil ta litt tid. +lists: Lister +listsDesc: Lister lar deg lage tidslinjer med utvalgte brukere. De kan hentes frem + fra tidslinje-siden. +deleted: Slettet +editNote: Rediger notat +followsYou: Følger deg +createList: Lag liste +newer: nyere +older: eldre +download: Last ned +unfollowConfirm: Er du sikker på at du ikke lenger vil følge {name}? +noLists: Du har ingen lister +following: Følger +files: Filer +note: Post +notes: Poster +followers: Følgere +otherSettings: Andre innstillinger +addInstance: Legg til en server +alreadyFavorited: Allerede lagt til i bokmerker. +delete: Slett +openInWindow: Åpne i vindu +basicSettings: Grunnleggende innstillinger +headlineMisskey: En desentralisert sosialt media-plattform, basert på åpen kildekode, + som alltid vil være gratis! 🚀 +introMisskey: Velkommen! Calckey er en desentralisert sosialt media-plattform, basert + på åpen kildekode, som alltid vil være gratis! 🚀 +exportRequested: Du har bedt om en eksportering. Dette vil ta litt tid. Den vil bli + lagt til på disken din når den er ferdig. +noThankYou: Nei takk +favorites: Bokmerker +unfavorite: Fjern fra bokmerker +favorited: Lagt til i bokmerker. +copyLink: Kopier lenke +searchUser: Søk etter en bruker +jumpToPrevious: Gå til foregående +showMore: Vis mer +followRequestAccepted: Følgeforespørsel godtatt +import: Importer +export: Eksporter +logout: Logger ut diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index cdfbca146..c26045e02 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -85,3 +85,28 @@ noLists: Você não possui nenhuma lista following: Seguindo followers: Seguidores followsYou: Segue você +fetchingAsApObject: Buscando do Fediverse +timeline: Linha do tempo +favorite: Adicionar aos marcadores +favorites: Marcadores +unfavorite: Remover dos marcadores +favorited: Adicionado aos marcadores. +alreadyFavorited: Já foi adicionado aos marcadores. +download: Download +pageLoadError: Ocorreu um erro ao carregar a página. +pageLoadErrorDescription: Isso normalmente é causado por erros de rede ou pelo cache + do navegador. Tente limpar o cache e, depois de esperar um pouquinho, tente novamente. +serverIsDead: Esse servidos não está respondendo. Por favor espere um pouco e tente + novamente. +youShouldUpgradeClient: Para visualizar essa página, favor reiniciar para atualizar + seu cliente. +enterListName: Insira um nome para a lista +privacy: Privacidade +defaultNoteVisibility: Visibilidade padrão +makeFollowManuallyApprove: Pedidos de seguimento precisam de aprovação +follow: Seguir +followRequest: Seguir +followRequests: Pedidos de seguimento +unfollow: Parar de seguir +followRequestPending: Pedido de seguimento pendente +enterEmoji: Insira um emoji diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index b4274fbbe..3d37b951c 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1847,7 +1847,7 @@ customMOTDDescription: Пользовательские сообщения дл разрывами строк, будут отображаться случайным образом каждый раз, когда пользователь загружает / перезагружает страницу. recommendedInstancesDescription: Рекомендуемые инстансы, разделенные разрывами строк, - должны отображаться на рекомендуемой ленте. НЕ добавляйте `https://`, ТОЛЬКО домен. + должны отображаться на рекомендуемой ленте. caption: Автоматическая подпись splash: Заставка updateAvailable: Возможно, доступно обновление! diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index ef44f61f8..82da6f649 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1,9 +1,12 @@ ---- _lang_: "Türkçe" -introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Firefish'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." +introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Calckey'e hoş geldiniz.\n + Misskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\"\ + \ oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\n + Herkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini + de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." monthAndDay: "{month}Ay {day}Gün" search: "Arama" -notifications: "Bildirim" +notifications: "Bildirimler" username: "Kullanıcı Adı" password: "Şifre" forgotPassword: "şifremi unuttum" @@ -11,7 +14,7 @@ ok: "TAMAM" gotIt: "Anladım" cancel: "İptal" enterUsername: "Kullanıcı adınızı giriniz" -noNotes: "Notlar mevcut değil." +noNotes: "Gönderiler mevcut değil." noNotifications: "Bildirim bulunmuyor" settings: "Ayarlar" basicSettings: "Temel Ayarlar" @@ -37,7 +40,8 @@ copyContent: "İçeriği kopyala" copyLink: "Bağlantıyı Kopyala" delete: "Sil" deleteAndEdit: "Sil ve yeniden düzenle" -deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." +deleteAndEditConfirm: "Bu gönderiyi silip yeniden düzenlemek istiyor musunuz? Bu gönderiye + ilişkin tüm tepkiler, destekler ve yanıtlar silinecektir." addToList: "Listeye ekle" sendMessage: "Mesaj Gönder" copyUsername: "Kullanıcı Adını Kopyala" @@ -61,3 +65,1108 @@ _deck: _columns: notifications: "Bildirim" tl: "Zaman çizelgesi" +searchPlaceholder: Calckey'de Ara +reply: Yanıtla +jumpToPrevious: Öncekini görüntüle +deleted: Silindi +editNote: Notu düzenle +noThankYou: Hayır, teşekkürler +addInstance: Bir sunucu ekle +cantFavorite: Favorilere eklenemedi. +edited: '{date} tarihinde ve {time} vaktinde düzenlendi' +loggingIn: Giriş Yapılıyor +save: Kaydet +headlineMisskey: Sonsuza kadar ücretsiz, açık kaynak kodlu, merkeziyetsiz sosyal medya + platformu! 🚀 +loadMore: Daha fazla yükle +instance: Sunucu +fetchingAsApObject: Fedevren'den çekiliyor +removeReaction: Tepkini sil +rememberNoteVisibility: Gönderi görünürlüğü ayarlarını hatırla +attachCancel: Eklentiyi kaldır +suspend: Askıya Al +unsuspend: Askıya Almayı Kaldır +unmute: Susturmayı Kaldır +blockConfirm: Bu hesabı engellemek istediğinize emin misiniz? +unblockConfirm: Bu hesabın engelini kaldırmak istediğinize emin misiniz? +settingGuide: Tavsiye edilen ayarlar +cacheRemoteFilesDescription: Bu ayar devre dışı bırakıldığında, uzak dosyalar doğrudan + uzak sunucudan yüklenir. Bunun devre dışı bırakılması depolama kullanımını azaltacak, + ancak küçük resimler oluşturulmayacağından trafiği artıracaktır. +flagAsCatDescription: Kedi kulaklarına sahip olacak ve bir kedi gibi konuşacaksın! +flagSpeakAsCat: Kedi gibi konuş +setWallpaper: Arkaplan ayarla +removeWallpaper: Arkaplanı sil +operations: Operasyonlar +clearCachedFiles: Ön belleği temizle +clearCachedFilesConfirm: Önbelleğe alınan tüm uzak dosyaları silmek istediğinizden + emin misiniz? +blockedInstancesDescription: Engellemek istediğiniz sunucuların ana bilgisayar adlarını + listeleyin. Listelenen sunucular artık bu sunucularla iletişim kuramayacak. +blockedUsers: Engellenmiş kullanıcılar +editProfile: Profilini düzenle +intro: Calckey'in indirilmesi tamamlandı! Lütfen yönetici hesap oluşturun. +instanceUsers: Sunucunun kullanıcıları +changePassword: Şifreyi değiştir +security: Güvenlik +newPasswordRetype: Yeni şifreyi tekrarla +uploadFromUrlRequested: Yükleme istendi +syncDeviceDarkMode: Karanlık modu cihazının ayarları ile senkronize et +renameFolder: Bu klasörü yeniden adlandır +emptyFolder: Bu klasör boş +unableToDelete: Silinemiyor +inputNewDescription: Yeni başlık gir +hasChildFilesOrFolders: Bu klasör boş olduğundan silinemez. +disconnectedFromServer: Sunucuyla bağlantı kesildi +reload: Yenile +disablingTimelinesInfo: Yöneticiler ve Moderatörler, etkinleştirilmemiş olsalar bile + tüm zaman çizelgelerine her zaman erişebilir. +pinnedUsersDescription: '"Keşfet" sekmesinde sabitlenecek kullanıcı adlarını satır + sonlarıyla ayırarak listeleyin.' +pinnedPages: Sabitlenmiş Sayfalar +pinnedPagesDescription: Bu sunucunun üst sayfasına sabitlemek istediğiniz Sayfaların + yollarını satır sonları ile ayırarak girin. +enableHcaptcha: hCaptcha'yı Aktif Et +notifyAntenna: Yeni gönderileribildir +recentlyUpdatedUsers: En son aktif kullanıcılar +about: Hakkında +twoStepAuthentication: İki-adımlı doğrulama +securityKeyName: Key name +help: Yardım +inputMessageHere: Mesajını buraya gir +ownedGroups: Gruplarım +joinedGroups: Katılınmış gruplar +invites: Davetler +members: Kullanıcılar +transfer: Transfer +messagingWithGroup: Grup sohbeti +next: Sonraki +retype: Tekrar gir +dashboard: Panel +objectStorageBucket: Bucket +objectStorageBucketDesc: Sağlayıcınız tarafından kullanınan bucket ismini yazın. +showFixedPostForm: Gönderim formunu zaman çizelgesinin en üstünde görüntüleyin +newNoteRecived: Yeni gönderiler mevcut +none: Hiçbiri +details: Detaylar +recentUsed: Son kullanılan +installedApps: Yetkilendirilmiş Uygulamalar +removeAllFollowing: Takip edilen herkesi çıkar +yourAccountSuspendedDescription: Bu hesap, sunucunun hizmet şartlarını veya benzerlerini + ihlal ettiği için askıya alındı. Daha ayrıntılı bir neden öğrenmek istiyorsanız + yöneticiyle iletişime geçin. Lütfen yeni bir hesap oluşturmayın. +addedRelays: Eklenen Röleler +serviceworkerInfo: Push bildirimleri için aktif olması gerekiyor. +author: Sahip +tokenRequested: Hesaba erişim ver +useFullReactionPicker: Tam boyutunda tepki seçici kullan +small: Küçük +enableAll: Hepsine izin ver +disableAll: Hepsini kapat +regexpError: Regex hatası +emailConfigInfo: Kayıt sırasında veya şifrenizi unutursanız e-postanızı onaylamak + için kullanılır +smtpSecure: SMTP bağlantıları için SSL/TSL kullan +regexpErrorDescription: '{tab} kelimenizin {line} satırındaki normal ifadede bir hata + oluştu:' +instanceMute: Sunucu Susturmaları +reporter: Rapor eden +userSaysSomethingReason: '{name}, {reason} söyledi' +userSaysSomethingReasonRenote: '{name}, {reason} içeren bir gönderiyi öne çıkardı' +userSaysSomethingReasonQuote: '{name}, {reason} içeren bir gönderiden alıntı yaptı' +notificationSettingDesc: Görünecek bildirimleri seç. +other: Diğer +sample: Örnek +notSet: Ayarlanmadı +emailVerified: Mail doğrulandı +showGapBetweenNotesInTimeline: Zaman tünelinde gönderiler arasındaki boşluğu göster +sendErrorReports: Hata raporları gönder +followingCount: Takip edilen hesap sayısı +no: Hayır +myTheme: Temam +backgroundColor: Arkaplan rengi +accentColor: Vurgu rengi +textColor: Yazı rengi +createdAt: Oluşturuldu +updatedAt: Güncellendi +saveConfirm: Kaydet? +registry: Kayıt +currentVersion: Şuanki Sürüm +accountDeletionInProgress: Hesap silme şu anda devam ediyor +unresolved: Çözülmedi +newVersionOfClientAvailable: Yeni istemci sürümü mevcut. +shareWithNote: Gönderi ile paylaş +whatIsNew: Değişiklikleri göster +translate: Çevir +breakFollow: Takipçiyi sil +breakFollowConfirm: Takipçiyi kaldırmak istediğinizden emin misiniz? +unfollowConfirm: "{name}'i takibi bırakmak istediğinizden emin misiniz?" +importRequested: Bir içe aktarma isteğinde bulundunuz. Bu biraz zaman alabilir. +somethingHappened: Bir hata ile karşılaşıldı +retry: Tekrar Dene +youShouldUpgradeClient: Bu sayfayı görüntülemek için, lütfen istemcinizi güncelleyin. +reactionSetting: Tepki seçicide gösterilecek tepkiler +unmarkAsSensitive: NSFW işaretini kaldır +enterFileName: Dosya adı gir +noJobs: Hiçbir iş yok +instanceFollowing: Sunucuda takip ediliyor +instanceFollowers: Sunucunun takipçileri +currentPassword: Şuanki şifre +newPassword: Yeni şifre +saved: Kaydedildi +uploadFromUrlDescription: Yüklemek istediğiniz dosyanın URL'si +noMoreHistory: Başka geçmiş yok +startMessaging: Yeni sohbet oluştur +manageGroups: Grupları düzenle +nUsersRead: '{n} tarafından okundu' +images: Görseller +birthday: Doğumgünü +light: Aydınlık +dark: Karanlık +lightThemes: Aydınlık temalar +selectFiles: Dosyalar seç +selectFolders: Klasörler seç +renameFile: Dosyayı yeniden adlandır +folderName: Klasör adı +createFolder: Klasör oluştur +copyUrl: URL'yi Kopyala +maintainerName: Sahip +maintainerEmail: Sahibin e-postası +tosUrl: Kullanım Koşulları URL'si +monthX: '{month}' +basicInfo: Basit bilgi +pinnedUsers: Sabitlenmiş kullanıcılar +manageAntennas: Antenleri Düzenle +name: İsim +silence: Sustur +unsilence: Susturmayı geri al +exploreUsersCount: '{count} Kullanıcı var' +exploreFediverse: Fediversi keşfet +popularTags: Popüler etiketler +close: Kapat +group: Grup +text: Yazı +checking: Doğrulanıyor... +tooLong: Çok uzun +weakPassword: Zayıf şifre +normalPassword: Ortalama şifre +disableDrawer: Çekmece tarzı menüler kullanmayın +youHaveNoGroups: Grupların yok +joinOrCreateGroup: Bir gruba davet edil veya kendininkini oluştur. +regenerate: Yeniden Oluştur +fontSize: Yazı boyutu +noFollowRequests: Bekleyen takip isteğiniz yok +openImageInNewTab: Resmi yeni sekmede aç +useObjectStorage: Object Storage kullan +objectStorageUseProxy: Proxy üzerinden bağlan +installedDate: Yetkilendirilme tarihi +scratchpad: Karalama Defteri +deleteAllFiles: Tüm dosyaları isl +useCw: İçeriği gizle +plugins: Eklentiler +manage: Yönetmek +preferencesBackups: Tercih yedekleri +generateAccessToken: Erişim tokeni oluştur +enableEmail: E-posta dağıtımını etkinleştir +regenerateLoginToken: Giriş tokenini yeniden oluştur +regenerateLoginTokenDescription: Oturum açma sırasında dahili olarak kullanılan belirteci + yeniden oluşturur. Normalde bu eylem gerekli değildir. Yeniden oluşturulursa, tüm + cihazların oturumu kapatılacaktır. +followersCount: Takipçi sayısı +yes: Evet +lockedAccountInfo: Gönderi görünürlüğünüzü "Yalnızca takipçiler" olarak ayarlamazsanız, + takipçilerin manuel olarak onaylanmasını isteseniz bile gönderileriniz herkes tarafından + görülebilir. +unlikeConfirm: Beğeniyi kaldırmak istiyor musunuz? +notSpecifiedMentionWarning: Bu gönderi, alıcı olarak dahil edilmeyen kullanıcılardan + bahsetmektedir. +hideOnlineStatus: Çevrimiçi bilgisini gizle +hideOnlineStatusDescription: Çevrimiçi durumunuzu gizlemek, arama gibi bazı özelliklerin + rahatlığını azaltır. +botProtection: Bot Koruması +selectAccount: Hesap seç +recentPosts: En son sayfalar +high: Yüksek +middle: Orta +secureModeInfo: Diğer sunuculardan talepte bulunurken kanıtlamadan geri göndermeyiniz. +previewNoteText: Önizlemeyi göster +customCss: Özel CSS +global: Global +makeReactionsPublic: Tepki geçmişini herkese açık olarak ayarla +clickToFinishEmailVerification: Mail doğrulamasını tamamlamak için lütfen [{ok}]'a + tıklayın. +overridedDeviceKind: Cihaz tipi +smartphone: Akıllı telefon +tablet: Tablet +auto: Otomatik +tenMinutes: 10 dakika +recentNDays: Son {n} gün +noEmailServerWarning: Mail sunucusu ayarlanmadı. +thereIsUnresolvedAbuseReportWarning: Çözülmemiş raporlar var. +statusbar: Durum çubuğu +pleaseSelect: Bir seçenek seçin +lastActiveDate: Son kullanılan +reverse: Tersi +logoutConfirm: Gerçekten oturum kapatılsın mı? +type: Tip +speed: Hız +slow: Yavaş +activeEmailValidationDescription: Tek kullanımlık adreslerin kontrol edilmesi ve gerçekten + iletişim kurup kurulamayacağına göre e-posta adreslerinin daha sıkı doğrulanmasını + sağlar. İşaretlenmediğinde, yalnızca e-postanın biçimi doğrulanır. +move: Taşı +defaultReaction: Giden ve gelen gönderiler için varsayılan emoji tepkisi +indexPosts: Dizin Gönderileri +youGotNewFollower: takip etti +receiveFollowRequest: Takip isteği alındı +followRequestAccepted: Takip isteği onaylandı +mention: Bahset +download: İndir +lists: Listeler +noLists: Hiç listen yok +cantRenote: Bu gönderi yükseltilemez. +cantReRenote: Yükseltme yükseltilemez. +mute: Sustur +block: Engelle +editWidgetsExit: Tamamlandı +customEmojis: Özel Tepki +cpuAndMemory: İşlemci ve Bellek +selectInstance: Sunucu seç +instances: Sunucular +silencedInstancesDescription: Susturmak istediğiniz sunucuların ana bilgisayar adlarını + listeleyin. Listelenen sunuculardaki hesaplar "Sessiz" olarak değerlendirilir, yalnızca + takip istekleri yapabilir ve takip edilmediği takdirde yerel hesaplardan bahsedemez. + Bu, engellenen sunucuları etkilemeyecektir. +muteAndBlock: Susturmalar ve Engeller +noteDeleteConfirm: Bu gönderiyi silmek istediğine emin misin? +resetAreYouSure: Gerçekten sıfırla? +remoteUserCaution: Uzak kullanıcılardan gelen bilgiler eksik olabilir. +yearsOld: '{age} yaşında' +removed: Başarıyla silindi +reject: Reddet +unwatch: İzlemeyi bırak +accept: Kabul et +normal: Normal +thisMonth: Ay +enableRecaptcha: reCAPTCHA'yı Aktif Et +antennas: Antenler +recaptchaSiteKey: Site key +withFileAntenna: Sadece dosyalı gönderiler +antennaInstancesDescription: Sunucu başı bir satır kullanın +moderator: Moderatör +moderation: Moderasyon +lastUsed: En son kullanılan +unregister: Kaydı sil +passwordLessLogin: Şifresiz giriş +uploadFolder: Yüklemeler için varsayılan klasör +markAsReadAllUnreadNotes: Tüm gönderileri okundu olarak işaretle +notFound: Bulunamadı +groups: Gruplar +quoteQuestion: Alıntı olarak eklensin mi? +signinRequired: Lütfen devam etmeden önce kayıt olun +noMessagesYet: Şuana kadar mesaj yok +newMessageExists: Yeni mesaj yok +invitations: Davetler +invitationCode: Davet kodu +signinWith: '{x} ile giriş yap' +strongPassword: Güçlü şifre +passwordNotMatched: Uyuşmuyor +signinFailed: Giriş yapılamadı. Şifre ve ya kullanıcı adı yanlış. +tapSecurityKey: Güvenlik anahtarınıza dokunun +or: veya +noHistory: Geçmiş bulunamadı +language: Dil +clientSettings: İstemci Ayarları +accountSettings: Hesap Ayarları +listen: Dinle +chooseEmoji: Emoji seç +promotion: Terfi Edildi +nothing: Burada görüntülenecek bir şey yok +lastUsedDate: Son kullanılma tarihi +updateRemoteUser: Uzak kullanıcı bilgilerini güncelle +width: Genişlik +height: Uzunluk +permission: İzinler +email: Mail +smtpSecureInfo: STARTTLS kullanırken bunu kapatın +alwaysMarkSensitive: Varsayılan olarak NSFW olarak işaretle +noteFavoritesCount: İşaretlenen gönderilerin sayısı +pageLikesCount: Beğenilen Sayfaların sayısı +duplicate: Kopyasını Oluştur +clearCache: Önbelleği Temizle +onlineUsersCount: '{n} kullanıcı aktif' +nUsers: '{n} Kullanıcı' +nNotes: '{n} Gönderi' +useReactionPickerForContextMenu: Sağ tık ile tepki seçiciyi aç +typingUsers: '{users} yazıyor' +jumpToSpecifiedDate: Spesifik tarihe atla +showingPastTimeline: Şuan eski bir zaman çizelgesini görüntülüyorsunuz +clear: Temizle +fullView: Tam görünüm +emailNotConfiguredWarning: Mail adresi seçilmedi. +privateMode: Özel Mod +fast: Hızlı +learnMore: Daha fazla bilgi edin +localOnly: Sadece yerel +delayed: Ertelenmiş +useGlobalSetting: Global ayaralrı kullan +switchAccount: Hesap değiştir +notRecommended: Tavsiye edilmiyor +onlineStatus: Çevrimiçi bilgisi +active: Aktif +instanceBlocking: Federasyon Yönetmek +enabled: Aktif +disabled: Deaktif +quickAction: Hızlı işlemler +configure: Yapılandır +blockedInstances: Engellenmiş Sunucular +silencedInstances: Susturulmuş Sunucular +lookup: Görüntüle +inputNewFolderName: Yeni klasör ismi gir +noteOf: Gönderi {user} tarafından +onlyOneFileCanBeAttached: Bir mesaja sadece 1 dosya ekleyebilirsin +install: İndir +uninstall: kALDIR +send: Gönder +noCrawleDescription: Arama motorlarından profil sayfanızı, gönderilerinizi, Sayfalarınızı + vb. indekslememesini isteyin. +emailNotification: Mail bildirimleri +goBack: Geri +online: Çevrimiçi +translatedFrom: "{x}'den çevrildi" +cropImage: Resmi kırp +deleteAccount: Hesabı Sil +navbar: Gezinti çubuğu +account: Hesap +instanceDefaultThemeDescription: Tema kodunu nesne biçiminde girin. +alt: ALT +mutePeriod: Sessiz süresi +indefinitely: Kalıcı olarak +oneHour: Bir saat +oneWeek: Bir hafta +colored: Renkli +sensitiveMediaDetection: Resim NSFW Belirleme +subscribePushNotification: Push bildirimlerini aktif et +pushNotificationAlreadySubscribed: Push bildirimler zaten açık +sendPushNotificationReadMessage: İlgili bildirimler veya mesajlar okunduktan sonra + push bildirimlerini silin +sendPushNotificationReadMessageCaption: Kısa bir süre için "{emptyPushNotificationMessage}" + metnini içeren bir bildirim görüntülenecektir. Bu, mümkünse cihazınızın pil kullanımını + artırabilir. +enterSendsMessage: Mesaj göndermek için Mesajlaşma'da Geri Dön'e basın (Ctrl + Return) +customMOTDDescription: Bir kullanıcı sayfayı her yüklediğinde/yeniden yüklediğinde + rastgele gösterilecek satır sonlarıyla ayrılmış MOTD (açılış ekranı) için özel mesajlar. +customSplashIconsDescription: Bir kullanıcı sayfayı her yüklediğinde/yeniden yüklediğinde + rastgele gösterilecek satır sonlarıyla ayrılmış özel açılış ekranı simgeleri için + URL'ler. Lütfen resimlerin statik bir URL'de olduğundan ve tercihen tümü 192x192 + olarak yeniden boyutlandırıldığından emin olun. +updateAvailable: Bir güncelleme mevcut olabilir! +splash: Açılış Ekranı +moveTo: Şimdiki hesabını yeni bir hesaba taşı +swipeOnMobile: Sayfalar arasında kaydırmaya izin ver +swipeOnDesktop: Masaüstünde mobil stil kaydırmaya izin ver +migration: Taşıma +moveAccount: Hesabını taşı! +moveFrom: Daha eski bir hesaptan bu hesaba taşıyın +moveFromLabel: 'Taşındığınız hesap:' +importAndExport: İçeri/Dışarı Aktar +manageLists: Listeleri düzenle +error: Hata +pageLoadError: Sayfayı yüklerken bir hata ile karşılaşıldı. +serverIsDead: Sunucu yanıt vermiyor. Biraz bekleyip tekrar deneyin. +defaultNoteVisibility: Varsayılan görünürlük +follow: Takip et +reactionSettingDescription2: Yeniden sıralamak için sürükleyin, silmek için tıklayın, + eklemek için "+"ya basın. +you: Sen +clickToShow: Görmek için tıkla +sensitive: NSFW +add: Ekle +reaction: Tepkiler +markAsSensitive: NSFW olarak işaretle +unblock: Engeli Kaldır +addAccount: Hesap ekle +network: İnternet +disk: Depolama +instanceInfo: Sunucu Bilgisi +statistics: İstatistikler +hiddenTagsDescription: Trendlerden gizlemek ve keşfetmek istediğiniz etiketlerin (# + olmadan)etiketlerini listeleyin. Gizli etiketler başka yollarla keşfedilebilir. +mutedUsers: Susturulmuş kullanıcılar +uploadFromUrlMayTakeTime: Yüklemenin tamamlanması zaman alabilir. +activity: Aktivite +theme: Temalar +themeForLightMode: Aydınlık modda kullanmak için temalar +reloadConfirm: Zaman çizelgesini yenilemek ister misiniz? +instanceName: Sunucu adı +circularReferenceFolder: Hedef klasör, taşımak istediğiniz klasörün bir alt klasörüdür. +instanceDescription: Sunucu açıklaması +driveCapacityPerLocalAccount: Kullanıcı başı Driver kapasitesi +driveCapacityPerRemoteAccount: Uzak kullanıcı başı Driver kapasitesi +inMb: Megabayt cinsinden +pinnedClipId: Sabitlenecek atacın ID'si +withFiles: Dosyaları içer +recentlyRegisteredUsers: Yeni katılmış kullanıcılar +recentlyDiscoveredUsers: Yeni keşfedilmiş kullanıcılar +nUsersMentioned: '{n} kullanıcı tarafından bahsedildi' +securityKey: Security key +title: Başlık +total: Toplam +sounds: Sesler +objectStorageRegionDesc: "'xx-east-1' gibi bir bölge belirtin. Hizmetiniz bölgeler + arasında ayrım yapmıyorsa, bunu boş bırakın veya 'us-east-1' girin." +objectStorageUseSSL: SSL Kullan +popout: Açılır Pencere +volume: Ses +showInPage: Sayfada göster +masterVolume: Ana ses +undeck: Desteden çık +useBlurEffectForModal: Modallar için bulanıklık efekti uygula +leaveConfirm: Kaydedilmemiş değişiklikler var. Devam etmek istiyor musunuz? +testEmail: Email dağıtımını test et +wordMute: Kelime susturması +userSaysSomething: '{name} bir şey söyledi' +channel: Kanallar +create: Oluştur +useGlobalSettingDesc: Açıksa, hesap bildirim ayarlarınız kullanılacaktır. Kapatılırsa, + bireysel yapılandırmalar yapılabilir. +setMultipleBySeparatingWithSpace: Birden çok girişi boşluklarla ayırın. +fileIdOrUrl: Dosya ID veya URL'si +behavior: Davranış +abuseReported: Raporunuz gönderildi. Teşekkürler. +reporteeOrigin: Ana Raporcu +reporterOrigin: Ana Rapor Eden +defaultNavigationBehaviour: Varsayılan gezinme davranışı +editTheseSettingsMayBreakAccount: Bunları düzenlemek hesabınıza zarar verebilir. +renotedCount: Alınan yükseltme sayısı +driveFilesCount: Drive dosya sayısı +deleteConfirm: Sil? +invalidValue: Geçersiz değer. +instanceSecurity: Sunucu Güvenliği +searchResult: Arama sonuçları +useBlurEffect: Kullanıcı arayüzünde bulanıklaştırma efektleri kullanın +misskeyUpdated: Calckey güncellendi! +lastCommunication: Son iletişim +itsOn: Etkinleştirilmiş +emailRequiredForSignup: Kayıt olmak için mail gerekiyor +leaveGroup: Gruptan ayrıl +useDrawerReactionPickerForMobile: Reaksiyon seçiciyi mobil cihazda çekmece olarak + göster +leaveGroupConfirm: '"{name}"den ayrılmak istediğinizden emin misiniz?' +instanceDefaultLightTheme: Sunucu genelinde varsayılan aydınlık tema +document: Dökümanlar +numberOfPageCacheDescription: Bu sayının arttırılması, kullanıcılar için kolaylık + sağlayacaktır ancak daha fazla sunucu yükünün yanı sıra daha fazla bellek kullanılmasına + neden olacaktır. +refreshInterval: 'Güncelleme aralığı ' +label: Etiket +replayTutorial: Eğiticiyi tekrar oynat +moveAccountDescription: Bu süreç geri döndürülemez. Taşımadan önce yeni hesabınızda + bu hesap için bir takma ad ayarladığınızdan emin olun. Lütfen @person@server.com + şeklinde biçimlendirilmiş hesabın etiketini girin +emojis: Emoji +flagAsCat: Kedi misin? +selectChannel: Kanal seç +emojiName: Emoji adı +showOnRemote: Uzak sunucuda görüntüle +flagSpeakAsCatDescription: Gönderileriniz kedi modundayken nyanifiye edilecek +flagShowTimelineReplies: Yanıtları zaman çizelgesinde göster +silenceThisInstance: Bu sunucuyu sustur +proxyAccountDescription: Vekil hesabı, belirli koşullar altında kullanıcılar için + uzaktan takipçi işlevi gören bir hesaptır. Örneğin, bir kullanıcı listeye bir uzak + kullanıcı eklediğinde, o kullanıcıyı takip eden yerel bir kullanıcı yoksa uzak kullanıcının + etkinliği sunucuya teslim edilmeyecektir, bu nedenle onun yerine vekil hesabı takip + edilecektir. +clearQueueConfirmTitle: Bu sırayı temizlemek istediğine emin misin? +software: Yazılım +version: Sürüm +federating: Federasyon +preview: Ön izleme +retypedNotMatch: Girişler uyuşmuyor. +attachFile: Dosya ekle +noSuchUser: Kullanıcı bulunamadı +removeAreYouSure: '"{x}" kaldırmak istediğinize emin misiniz?' +keepOriginalUploading: Orjinal resmi sakla +messageRead: Oku +deleteAreYouSure: '"{x}" silmek istediğinize emin misiniz?' +messaging: Sohbet +upload: Yükle +fromUrl: URL'den +agreeTo: '{0} kabul ediyorum' +tos: Kullanım Koşulları +drive: Drive +selectFolder: Klasör seç +inputNewFileName: Yeni dosya ismi gir +whenServerDisconnected: Sunucuyla bağlantı kesildiğinde +avatar: Avatar +rename: Yeniden Adlandır +banner: Afiş +nsfw: NSFW +doNothing: Görmezden Gel +watch: İzle +connectService: Bağlan +registration: Kayıt +hcaptcha: hCaptcha +pinnedNotes: Sabitlenmiş gönderiler +hcaptchaSiteKey: Site key +hcaptchaSecretKey: Secret key +antennaSource: Anten kaynağı +antennaKeywords: Dinlenecek anahtar kelimeler +antennaExcludeKeywords: Hariç tutulacak anahtar kelimeler +antennaKeywordsDescription: AND koşulu için boşluklarla veya OR koşulu için satır + sonlarıyla ayırın. +caseSensitive: Büyük harf duyarlı +enableServiceworker: Tarayıcınız için Push-Bildirimleri Etkinleştirin +unsilenceConfirm: Bu kullanıcının susturma işlemini geri almak istediğinizden emin + misiniz? +userList: Listeler +antennaUsersDescription: Kullanıcı başı bir satır kullanın +administrator: Yönetici +token: Token +cacheClear: Önbelleği temizle +createGroup: Grup oluştur +newPasswordIs: Yeni şifren "{password}" +share: Paylaş +enable: Etkinleştir +groupName: Grup adı +available: Mevcut +unavailable: Mevcut değil +weekOverWeekChanges: Geçen haftadan beri değişiklikler +usernameInvalidFormat: Büyük ve küçük harfleri, sayıları ve alt çizgileri kullanabilirsiniz. +tooShort: Çok kısa +passwordMatched: Uyuşuyor +dayOverDayChanges: Dünden beri değişiklikler +appearance: Görünüm +objectStorageBaseUrl: Ana URL +objectStoragePrefix: Prefix +unableToProcess: Operasyon tamamlanamadı +deleteAllFilesConfirm: Tüm dosyaları silmek istediğine emin misin? +disablePagesScript: Sayfalardan AiScript'i deaktive et +expandOnNoteClick: Gönderileri basarak aç +expandOnNoteClickDesc: Kapatılırsa, gönderileri hala menüden veya sağtıklayarak açabilirsin. +removeAllFollowingDescription: Bunu gerçekleştirmek, {host} üzerindeki tüm hesapları + takip etmeyi bırakır. +deck: Deste +pluginTokenRequestedDescription: Bu eklenti, burada ayarlanan izinleri kullanabilecektir. +notificationType: Bildirim tipi +channelFederationWarn: Kanallar başka sunuculara federe edilmiyor +forwardReport: Raporu uzak sunucuya ilet +openInNewTab: Yeni sekmede aç +clip: Ataç +optional: Opsiyonel +manageAccessTokens: Erişim tokenlerini düzenle +clipsDesc: Ataçlar, paylaşılabilen kategorize yer imleri gibidir. Tek tek gönderiler + menüsünden ataçlar oluşturabilirsiniz. +makeExplorable: Hesabını "Keşfet" 'te göster +accountInfo: Hesap Bilgisi +makeExplorableDescription: Bunu kapatırsanız, hesabınız "Keşfet" bölümünde görünmez. +saveAs: Olarak kaydet... +advanced: Gelişmiş +value: Değer +youAreRunningUpToDateClient: En son istemci sürümünü kullanıyorsunuz. +accounts: Hesaplar +switch: Değiştir +popularPosts: Popüler sayfalar +inChannelSearch: Kanalda ara +administration: Yönetim +ads: Reklamlar +low: Düşük +seperateRenoteQuote: Ayrı destek ve fiyat teklifi düğmeleri +sent: Gönderildi +customMOTD: Özel MOTD +showUpdates: Calckey güncellendiğinde bir açılır pencere göster +logoImageUrl: Logo resim URL'si +showAdminUpdates: Yeni bir Calckey sürümünün mevcut olduğunu belirtin (yalnızca yönetici) +newer: asla +older: daha eski +exportRequested: Bir dışarı aktarma talebinde bulundunuz. Bu biraz zaman alabilir. + Tamamlandığında Drive'ınıza eklenecektir. +notes: Gönderiler +following: Takip Ediyor +followers: Takipçiler +followsYou: Seni takip ediyor +pageLoadErrorDescription: Buna normalde ağ hataları veya tarayıcının önbelleği neden + olur. Önbelleği temizlemeyi deneyin ve biraz bekledikten sonra tekrar deneyin. +quote: Alıntıla +pinnedNote: Sabitlenmiş gönderi +renote: Yükselt +unrenote: Yükseltmeyi geri al +emojiUrl: Emoji URL +suspendConfirm: Bu hesabı askıya almak istediğinize emin misiniz? +addEmoji: Ekle +autoAcceptFollowed: Takip ettiğiniz kullanıcıların takip isteklerini otomatik olarak + onaylayın +general: Genel +accountMoved: 'Bu kullanıcı yeni bir hesapa taşındı:' +wallpaper: Arkaplan +searchWith: 'Arat: {q}' +youHaveNoLists: Hiçbir listen yok +followConfirm: '{name} kullanıcısını takip etmek istediğine emin misin?' +metadata: Metadata +monitor: İzlengeç +jobQueue: İş Sırası +noUsers: Kullanıcı bulunamadı +noInstances: Sunucu bulunamadı +pinLimitExceeded: Daha fazla gönderi sabitleyemezsin +defaultValueIs: 'Varsayılan: {value}' +noCustomEmojis: Emoji yok +blocked: Engellenmiş +default: Varsayılan +all: Tümü +subscribing: Abone Olunuyor +publishing: Yayınlanmak +notResponding: Cevap vermiyor +more: Daha fazla! +featured: Önerilen +usernameOrUserId: Kullanıcı adı veya kullanıcı id'si +fromDrive: Drive'dan +uploadFromUrl: URL'den yükle +announcements: Duyurular +explore: Keşfet +imageUrl: Resim URL'si +thisYear: Yıl +deleteFolder: Bu klasörü sil +addFile: Dosya ekle +dayX: '{day}' +enableLocalTimeline: Yerel zaman çizgisini aktif et +disconnectService: Bağlantıyı kes +enableGlobalTimeline: Global zaman çizgisini aktif et +enableRegistration: Yeni kullanıcı kaydını aktif et +invite: Davet et +bannerUrl: Afiş resmi URL +backgroundImageUrl: Arkaplan URL'si +recaptcha: reCAPTCHA +iconUrl: Ikon URL +recaptchaSecretKey: Secret key +avoidMultiCaptchaConfirm: Birden fazla Captcha sistemi kullanmak aralarında etkileşime + neden olabilir. Şu anda etkin olan diğer Captcha sistemlerini devre dışı bırakmak + ister misiniz? Etkin kalmalarını istiyorsanız, iptal düğmesine basın. +aboutMisskey: Calckey Hakkında +popularUsers: Popüler kullanıcılar +notFoundDescription: Bu URL'ye karşılık gelen sayfa bulunamadı. +reduceUiAnimation: Arayüz animasyonlarını azalt +markAsReadAllNotifications: Tüm bildirimleri okundu olarak işaretle +markAsReadAllTalkMessages: Tüm mesajları okundu olarak işaretle +inviteToGroup: Gruba davet et +quoteAttached: Alıntıla +useOsNativeEmojis: Sistem Emojilerini Kullan +signinHistory: Giriş geçmişleri +disableAnimatedMfm: Animasyonlu MFM'yi devre dışı bırak +uiLanguage: Arayüz dili +groupInvited: Bir gruba davet edildin +createAccount: Hesap Oluştur +existingAccount: Var olan hesap +aboutX: '{x} Hakkında' +doing: İşleniyor... +category: Kategori +deleteAll: Hepsini sil +objectStorageEndpoint: Endpoint +output: Çıkış +userSuspended: Bu kullanıcı askıya alındı. +userSilenced: Bu kullanıcı susturuldu. +yourAccountSuspendedTitle: Bu hesap askıya alındı +relays: Röleler +inboxUrl: Gelen URL +menu: Menü +divider: Ayraç +addItem: Öğe Ekle +enableInfiniteScroll: Otomatik olarak daha fazla yükle +enablePlayer: Video oynatıcıyı aç +disablePlayer: Video oynatıcıyı kapat +expandTweet: Tweeti Büyüt +large: Büyük +medium: Orta +smtpConfig: SMTP Sunucusu Ayarları +smtpHost: Adres +emailServer: Mail sunucusu +edit: Düzenle +emailAddress: Mail adresi +smtpPort: Port +emptyToDisableSmtpAuth: SMTP doğrulamasını kapatmak için kullanıcı adı ve şifreyi + boş bırakın +makeActive: Aktif +display: Gösterim +copy: Kopyala +metrics: Metrikler +pollVotesCount: Gönderilen oylama sayısı +loadRawImages: Küçük resimleri göstermek yerine orijinal resimleri yükleyin +switchUi: Düzen +sentReactionsCount: Gönderilen tepki sayısı +receivedReactionsCount: Alınan tepki sayısı +pollVotedCount: Alınan oylama sayısı +pageLikedCount: Beğeni alan Sayfa sayısı +contact: Bağlantı +useSystemFont: Sistemin varsayılan yazı tipini kullan +usageAmount: Kullanım +inUse: Kullanılan +userInfo: Kullanıcı bilgisi +unknown: Bilinmiyor +customCssWarn: Bu ayar yalnızca ne işe yaradığını biliyorsanız kullanılmalıdır. Uygun + olmayan değerlerin girilmesi, istemcinin normal şekilde çalışmamasına neden olabilir. +memo: Not +allowedInstancesDescription: Her biri yeni bir satırla ayrılmış, federasyon için beyaz + listeye eklenecek sunucu ana bilgisayarları (yalnızca özel modda geçerlidir). +expiration: Bitiş +troubleshooting: Sorun giderme +usernameInfo: Hesabınızı bu sunucudaki diğerlerinden ayıran bir ad. Alfabeyi (a~z, + A~Z), rakamları (0~9) veya alt çizgileri (_) kullanabilirsiniz. Kullanıcı adları + daha sonra değiştirilemez. +size: Boyut +numberOfColumn: Sütun Sayısı +driveCapOverrideCaption: 0 veya daha düşük bir değer girerek kapasiteyi varsayılana + sıfırlayın. +requireAdminForView: Bunu görüntülemek için bir yönetici hesabıyla oturum açmalısınız. +userSaysSomethingReasonReply: '{name}, {reason} içeren bir gönderiye cevap verdi' +overview: Genel Bakış +logs: Günlükler +database: Veri Tabanı +reportAbuseOf: '{name} kullanıcısını raporla' +openInSideView: Yan görünümde aç +createNew: Yeni oluştur +createNewClip: Yeni ataç oluştur +unclip: Atacı Kaldır +notesCount: Gönderi sayısı +repliesCount: Gönderilen yanıt sayısı +renotesCount: Göndeirlen yükseltme sayısı +repliedCount: Alınan yanıt sayısı +driveUsage: Drive kullanımı +noCrawle: Tarayıcı dizine eklemeyi reddet +needReloadToApply: Bunun yansıtılması için bir yeniden yükleme gereklidir. +showTitlebar: Başlık çubuğunu göster +latestVersion: En Son Sürüm +capacity: Kapasite +userPagePinTip: Tek tek gönderiler menüsünden "Profile sabitle"yi seçerek gönderileri + burada görüntüleyebilirsiniz. +offline: Çevrimdışı +priority: Öncelik +ratio: Oran +secureMode: Güvenli Mod (Yetkili Getirme) +aiChanMode: Klasik kullanıcı arayüzünde Ai-chan +recommended: Önerilen +received: Alındı +classic: Ortalanmış +muteThread: Konuyu sessize al +deleteAccountConfirm: Bu, hesabınızı geri alınamaz bir şekilde silecektir. İlerle? +hide: Gizle +pubSub: Pub/Sub Hesapları +filter: Filtre +controlPanel: Kontrol Paneli +continueThread: Konuya devam et +incorrectPassword: Yanlış şifre. +voteConfirm: '"{choice}" için oyunuzu onaylıyor musunuz?' +failedToFetchAccountInformation: Hesap bilgileri getirilemedi +rateLimitExceeded: Hız limiti aşıldı +renotedBy: '{user} Yükseltti' +host: Host +objectStorage: Object Storage +objectStorageUseSSLDesc: API bağlantıları için HTTPS kullanmayacaksanız bunu kapatın +objectStorageUseProxyDesc: API bağlantıları için Proxy kullanmayacaksanız bunu kapatın +objectStorageSetPublicRead: Yüklendiğinde "public-read" kullan +serverLogs: Sunucu günlüğü +abuseReports: Raporlar +reportAbuse: Rapor +verificationEmailSent: Bir doğrulama maili gönderildi. Doğrulamayı tamamlamak için + lütfen verilen bağlantıyı takip edin. +hashtags: Etiketler +resolved: Çözüldü +flagShowTimelineRepliesDescription: Açıksa, kullanıcıların zaman çizelgesindeki diğer + kullanıcıların gönderilerine verdiği yanıtları gösterir. +clearQueueConfirmText: Kuyrukta kalan teslim edilmemiş gönderiler birleştirilmeyecektir. + Genellikle bu işleme gerek yoktur. +image: Resim +video: Video +showMore: Daha Fazla +showLess: Kapat +selectAntenna: Anten seç +selectWidget: Araç seç +unsuspendConfirm: Bu hesabın askıya almasını kaldırmak istediğinize emin misiniz? +selectList: Liste seç +editWidgets: Araçları düzenle +showEmojisInReactionNotifications: Tepki bildirimlerinde emojileri göster +renoteMute: Yükseltmeleri sustur +renoteUnmute: Yükseltmeleri susturmayı kaldır +loginFailed: Giriş yapılamadı +proxyAccount: Vekil Hesap +selectUser: Kullanıcı seç +recipient: Alıcı(lar) +annotation: Yorumlar +federation: Federasyon +registeredAt: Kayıtlı +latestRequestSentAt: Gönderilen son istek +latestRequestReceivedAt: Alınan son istek +latestStatus: Son durum +storageUsage: Depolama kullanımı +charts: Grafikler +perHour: Saat Başı +perDay: Gün Başı +stopActivityDelivery: Etkinlik göndermeyi durdur +blockThisInstance: Bu sunucuyu engelle +themeForDarkMode: Karanlık modda kullanmak için temalar +fileName: Dosya adı +selectFile: Dosya seç +emptyDrive: Drive'n boş +promote: Terfi +numberOfDays: Gün sayısı +hideThisNote: Bu gönderiyi gizle +file: Dosya +enableEmojiReactions: Emoji tepkilerini aç +cw: İçerik uyarısı +makeFollowManuallyApprove: Takip istekleri onay gerektirir +today: Bugün +enableRecommendedTimeline: Tavsiye edilen zaman çizgisini aktive et +state: Durum +sort: Sırala +script: Skript +keepCw: İçerik uyarılarını sakla +manageAccounts: Hesapları Düzenle +makeReactionsPublicDescription: Bu, tüm geçmiş tepkilerinizin listesini herkesin görebileceği + bir hale getirecektir. +unmuteThread: İleti dizisinin sesini aç +ffVisibility: Takipçiler/Takipçiler Görünürlüğü +reflectMayTakeTime: Bunun yansıması biraz zaman alabilir. +cropImageAsk: Bu resmi kırpmak istediğinize emin misiniz? +check: Kontrol Et +driveCapOverrideLabel: Bu kullanıcı için drive kapasitesini değiştirin +numberOfPageCache: Önbelleğe alınan sayfa sayısı +license: Lisans +indexFrom: Post ID'den itibaren dizin +xl: XL +notificationSetting: Bildirim ayarları +fillAbuseReportDescription: Lütfen bu raporla ilgili ayrıntıları doldurun. Belirli + bir gönderiyle ilgiliyse, lütfen URL'sini ekleyin. +forwardReportIsAnonymous: Uzak sunucuda, hesabınız yerine raportör olarak anonim bir + sistem hesabı görüntülenecektir. +abuseMarkAsResolved: Raporu çözüldü olarak işaretle +instanceTicker: Göndeirlerdeki sunucu bilgisi +waitingFor: '{x} bekleniyor' +random: Rastgele +public: Herkese açık +i18nInfo: Calckey, gönüllüler tarafından çeşitli dillere çevriliyor. {link} adresinden + yardımcı olabilirsiniz. +disableShowingAnimatedImages: Animasyonlu görüntüleri oynatma +clips: Ataçlar +experimentalFeatures: Deneysel özellikler +developer: Geliştirici +left: Sol +center: Orta +wide: Geniş +narrow: Dar +reloadToApplySetting: Bu ayar yalnızca bir sayfa yeniden yüklendikten sonra geçerli + olacaktır. Şimdi yeniden yüklensin mi? +editCode: Kodu düzenle +apply: Uygula +receiveAnnouncementFromInstance: Bu sunucudan bildirimleri al +publish: Paylaş +quitFullView: Tam görünümden çık +addDescription: Açıklama ekle +info: Hakkında +noMaintainerInformationWarning: Yönetici bilgileri yapılandırılmadı. +noBotProtectionWarning: Bot koruması yapılandırılmamış. +postToGallery: Yeni galeri gönderisi oluştur +gallery: Galeri +privateModeInfo: Etkinleştirildiğinde, yalnızca beyaz listedeki sunucular sunucunuzla + birleşebilir. Tüm gönderiler halktan gizlenecektir. +itsOff: Etkinsizleştirilmiş +ffVisibilityDescription: Kimleri takip ettiğinizi ve kimlerin sizi takip ettiğini + kimlerin görebileceğini yapılandırmanıza izin verir. +welcomeBackWithName: Tekrar hoş geldin {name} +themeColor: Sunucu Kayan Yazı Rengi +audio: Ses +recentNHours: Son {n} saat +isSystemAccount: Bu hesap sistem tarafından oluşturulur ve otomatik olarak işletilir. + Lütfen bu hesabı denetlemeyin, düzenlemeyin, silmeyin veya başka bir şekilde kurcalamayın, + aksi takdirde sunucunuz bozulabilir. +typeToConfirm: Lütfen onaylamak için {x} girin +remoteOnly: Sadece uzak +failedToUpload: Yükleme başarısız +cannotUploadBecauseInappropriate: Bu dosya, bazı bölümleri potansiyel olarak NSFW + olarak algılandığından yüklenemedi. +cannotUploadBecauseNoFreeSpace: Drive kapasitesi yetersiz olduğundan yükleme başarısız + oldu. +cannotUploadBecauseExceedsFileSizeLimit: Bu dosya, izin verilen maksimum boyutu aştığı + için yüklenemedi. +beta: Beta +enableAutoSensitive: Otomatik NSFW İşaretleme +enableAutoSensitiveDescription: Mümkün olduğunda Makine Öğrenimi yoluyla NSFW ortamının + otomatik olarak algılanmasına ve işaretlenmesine olanak tanır. Bu seçenek devre + dışı bırakılsa bile, sunucu çapında etkinleştirilebilir. +shuffle: Karıştır +pushNotification: Push bildirimleri +unsubscribePushNotification: Push bildirimlerini kapat +pushNotificationNotSupported: Tarayıcınız veya sunucunuz push bildirimleri desteklemiyor +caption: Otomatik Başlık +moveToLabel: 'Taşıyacağın hesap:' +moveFromDescription: Bu, eski hesabınızın bir takma adını belirleyecek ve böylece + o hesaptan bu mevcut hesaba geçebileceksiniz. Bunu eski hesabınızdan taşınmadan + ÖNCE yapın. Lütfen @person@server.com şeklinde biçimlendirilmiş hesabın etiketini + girin +migrationConfirm: "Hesabınızı {account} hesabına taşımak istediğinizden kesinlikle + emin misiniz? Bunu yaptığınızda, geri alamazsınız ve hesabınızı bir daha normal + şekilde kullanamazsınız.\nAyrıca, lütfen bu cari hesabı, taşındığınız hesap olarak + ayarladığınızdan emin olun." +indexFromDescription: Her gönderiyi dizine eklemek için boş bırakın +indexNotice: Şimdi indeksleniyor. Bu muhtemelen biraz zaman alacaktır, lütfen sunucunuzu + en az bir saat yeniden başlatmayın. +customKaTeXMacro: Özel KaTeX makroları +directNotes: Direkt Mesajlar +import: İçeri Aktar +export: Dışarı Aktar +mentions: Bahsetmeler +files: Dosyalar +driveFileDeleteConfirm: '"{name}" dosyasını silmek istediğinizden emin misiniz? Ek + olarak içeren tüm gönderilerden kaldırılacaktır.' +createList: Liste oluştur +listsDesc: Listeler, belirtilen kullanıcılarla zaman çizelgesi oluşturmanıza olanak + tanır. Zaman Çizelgesi sayfasından erişilebilirler. +note: Gönder +enterListName: Liste için isim gir +unfollow: Takipten Çık +privacy: Gizlilik +followRequestPending: Takip isteği bekleniyor +enterEmoji: Bir emoji gir +followRequest: Takip İsteği +followRequests: Takip istekleri +renoted: Yükseldi. +emoji: Emoji +cacheRemoteFiles: Uzak dosyaları önbellekle +flagAsBot: Bu hesabı robot olarak işaretle +flagAsBotDescription: Bu hesap bir program tarafından kontrol ediliyorsa bu seçeneği + etkinleştirin. Etkinleştirilirse, diğer geliştiricilerin diğer botlarla sonsuz etkileşim + zincirlerini önlemesi ve Calckey'nin dahili sistemlerini bu hesabı bir bot olarak + ele alacak şekilde ayarlaması için bir bayrak görevi görür. +clearQueue: Sırayı Temizle +hiddenTags: Gizlenmiş Etiketler +done: Tamamlandı +processing: İşleniyor +silenced: Susturulmuş +darkThemes: Karanlık temalar +suspended: Askıya Alınmış +keepOriginalUploadingDescription: Orijinal olarak yüklenen görüntüyü olduğu gibi kaydeder. + Kapatılırsa, yükleme sırasında web'de görüntülenecek bir sürüm oluşturulur. +start: Başla +home: Ev +location: Konum +registeredDate: Katılım tarihi +yearX: '{year}' +pages: Sayfalar +integration: Entegrasyonlar +antennasDesc: "Antenler, belirlediğiniz kriterlere uyan yeni gönderiler görüntüler!\n + Zaman çizelgeleri sayfasından erişilebilirler." +notesAndReplies: Gönderiler ve yanıtlar +withReplies: Yanıtları da içer +connectedTo: Aşağıdaki hesap(lar) bağlı +silenceConfirm: Bu kullanıcıyı susturmak istediğinize emin misiniz? +messagingWithUser: Özel sohbet +resetPassword: Şifreyi sıfırla +registerSecurityKey: Yeni security key tanımla +docSource: Bu dökümanın kaynağı +ascendingOrder: Artan +tags: Etiketler +descendingOrder: Azalan +scratchpadDescription: Karalama defteri, AiScript deneyleri için bir ortam sağlar. + İçinde Calckey ile etkileşime girerek sonuçlarını yazabilir, çalıştırabilir ve kontrol + edebilirsiniz. +local: Yerel +remote: Uzak +addRelay: Röle Ekle +accessibility: Erişilebilirlik +showFeaturedNotesInTimeline: Önerilen gönderileri zaman çizelgesinde göster +objectStorageBaseUrlDesc: "Referans olarak kullanılan URL. İkisinden birini kullanıyorsanız, + CDN veya Proxy'nizin URL'sini belirtin.\nS3 için 'https://.s3.amazonaws.com' + kullanın ve GCS veya eşdeğer hizmetler için 'https://storage.googleapis.com/' + vb. kullanın." +objectStoragePrefixDesc: Dosyalar bu prefix ile dizinler altında saklanacaktır. +objectStorageEndpointDesc: AWS S3 kullanıyorsanız bunu boş bırakın, aksi halde kullandığınız + hizmete bağlı olarak uç noktayı "" veya ":" olarak belirtin. +objectStorageRegion: Region +invisibleNote: Gizli Gönderi +deletedNote: Silinmiş Gönderi +visibility: Görünürlük +poll: Oylama +themeEditor: Tema düzenleyicisi +enterFileDescription: Başlık gir +description: Açıklama +describeFile: Başlık ekle +system: Sistem +desktop: Masaüstü +confirmToUnclipAlreadyClippedNote: Bu gönderi zaten "{name}" atacının bir parçası. + Bunun yerine onu bu ataçtan kaldırmak istiyor musunuz? +sendErrorReportsDescription: "Açıldığında, bir sorun oluştuğunda ayrıntılı hata bilgileri + Calckey ile paylaşılarak Calckey kalitesinin artırılmasına yardımcı olur.\nBu, işletim + sisteminizin sürümü, kullandığınız tarayıcı, Calckey'deki etkinliğiniz vb. bilgileri + içerecektir." +closeAccount: Hesabı kapat +markAllAsRead: Okunmuş olarak işaretle +allowedInstances: Beyaz Listedeki Sunucular +squareAvatars: Kare avatarları göster +unread: Okunmaımş +instanceDefaultDarkTheme: Sunucu genelinde varsayılan karanlık tema +oneDay: Bir gün +showAds: Reklamları göster +adminCustomCssWarn: Bu ayar yalnızca ne işe yaradığını biliyorsanız kullanılmalıdır. + Yanlış değerler girilmesi, HERKESİN istemcilerinin normal şekilde çalışmamasına + neden olabilir. Lütfen CSS'nizi kullanıcı ayarlarınızda test ederek düzgün çalıştığından + emin olun. +customSplashIcons: Özel açılış ekranı simgeleri (url'ler) +recommendedInstancesDescription: Önerilen zaman çizelgesinde görünmesi için satır + sonlarıyla ayrılmış önerilen sunucular. `https://` EKLEMEYİN, YALNIZCA etki alanı. +recommendedInstances: Önerilen sunucular +enableServerMachineStats: Sunucu donanımı istatistiklerini etkinleştir +_sensitiveMediaDetection: + sensitivityDescription: Hassasiyetin düşürülmesi daha az yanlış tespite yol açarken, + hassasiyeti artırmak daha az tespitin gözden kaçmasına yol açacaktır. + setSensitiveFlagAutomaticallyDescription: Bu seçenek kapatılsa bile dahili algılamanın + sonuçları korunacaktır. + description: Makine Öğrenimi yoluyla NSFW ortamını otomatik olarak tanıyarak sunucu + denetleme çabasını azaltır. Bu, sunucudaki yükü biraz artıracaktır. + sensitivity: Algılama hassasiyeti + analyzeVideos: Videoların analizini etkinleştir + setSensitiveFlagAutomatically: NSFW olarak işaretle + analyzeVideosDescription: Görüntülere ek olarak videoları da analiz eder. Bu, sunucudaki + yükü biraz artıracaktır. +enableIdenticonGeneration: Kimlik oluşturmayı etkinleştir +reactionPickerSkinTone: Tercih edilen emoji cilt tonu +noteId: Gönderi ID +preventAiLearning: AI bot öğrenmesini önleyin +preventAiLearningDescription: Gönderiler ve resimler gibi yüklediğiniz içeriği incelememek + için üçüncü taraf yapay zeka dil modellerini isteyin. +isAdmin: Yönetici +_emailUnavailable: + disposable: Tek kullanımlık mail adresleri kullanılamaz + smtp: Bu mail sunucusu cevap vermiyor + mx: Bu mail sunucusu hatalı + used: Bu mail zaten kullanılıyor + format: Bu mail adresi yanlış +apps: Uygulamalar +findOtherInstance: Başka bir sunucu bul +showWithSparkles: Parıltılarla göster +showPopup: Kullanıcıları pop-up ile bilgilendirin +silencedWarning: Bu sayfa, bu kullanıcılar yöneticinizin susturduğu sunuculardan olduğu + için gösteriliyor, bu nedenle potansiyel olarak spam olabilirler. +isPatron: Calckey Patronu +youHaveUnreadAnnouncements: Okunmamış duyurularınız var +donationLink: Bağış sayfası linki +neverShow: Birdaha gösterme +remindMeLater: Belki sonra +removeQuote: Alıntıyı sil +removeRecipient: Alıcıyı sil +removeMember: Kullanıcıyı sil +customKaTeXMacroDescription: 'Kolayca matematiksel ifadeler yazmak için makrolar kurun! + Gösterim, LaTeX komut tanımlarına uygundur ve \newcommand{\ name}{content} veya + \newcommand{\name}[argüman sayısı]{content} şeklinde yazılır. Örneğin, \newcommand{\add}[2]{#1 + + #2}, \add{3}{foo} öğesini 3 + foo olarak genişletir. Makro adını çevreleyen süslü + parantezler, yuvarlak veya köşeli parantezler olarak değiştirilebilir. Bu, bağımsız + değişkenler için kullanılan parantezleri etkiler. Satır başına bir (ve yalnızca + bir) makro tanımlanabilir ve satırı tanımın ortasından ayıramazsınız. Geçersiz satırlar + basitçe yoksayılır. Yalnızca basit dizi değiştirme işlevleri desteklenir; koşullu + dallanma gibi gelişmiş söz dizimi burada kullanılamaz.' +enableCustomKaTeXMacro: Özel KaTeX makrolarını aktif et +isLocked: Bu hesabın takip onayları var +isModerator: Moderatör +signupsDisabled: Bu sunucudaki kayıtlar şu anda devre dışı, ancak istediğiniz zaman + başka bir sunucuya kaydolabilirsiniz! Bu sunucu için bir davet kodunuz varsa, lütfen + aşağıya girin. +sendModMail: Moderasyon Bildirimi Gönder +noGraze: Calckey ile çakıştığı için lütfen "Graze for Mastodon" tarayıcı uzantısını + devre dışı bırakın. +isBot: Bu hesap bir bottur diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index b2d32cb23..da5397993 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1,7 +1,9 @@ ---- _lang_: "Українська" headlineMisskey: "Мережа об'єднана записами" -introMisskey: "Ласкаво просимо! Firefish - децентралізована служба мікроблогів з відкритим кодом.\nСтворюйте \"нотатки\", щоб поділитися тим, що відбувається, і розповісти всім про себе 📡\nЗа допомогою \"реакцій\" ви також можете швидко висловити свої почуття щодо нотаток інших 👍\nДосліджуймо новий світ! 🚀" +introMisskey: "Ласкаво просимо! Calckey - децентралізована служба мікроблогів з відкритим + кодом.\nСтворюйте \"нотатки\", щоб поділитися тим, що відбувається, і розповісти + всім про себе 📡\nЗа допомогою \"реакцій\" ви також можете швидко висловити свої + почуття щодо нотаток інших 👍\nДосліджуймо новий світ! 🚀" monthAndDay: "{month}/{day}" search: "Пошук" notifications: "Сповіщення" @@ -14,16 +16,16 @@ gotIt: "Зрозуміло!" cancel: "Скасувати" enterUsername: "Введіть ім'я користувача" renotedBy: "Поширено {user}" -noNotes: "Немає нотаток" +noNotes: "Немає записів" noNotifications: "Немає сповіщень" -instance: "Інстанс" +instance: "Сервер" settings: "Налаштування" basicSettings: "Основні налаштування" otherSettings: "Інші налаштування" openInWindow: "Відкрити у вікні" profile: "Профіль" timeline: "Стрічка" -noAccountDescription: "Цей користувач ще нічого не написав про себе" +noAccountDescription: "Цей користувач ще нічого не написав про себе." login: "Увійти" loggingIn: "Здійснюємо вхід..." logout: "Вийти" @@ -44,7 +46,8 @@ copyContent: "Скопіювати контент" copyLink: "Скопіювати посилання" delete: "Видалити" deleteAndEdit: "Видалити й редагувати" -deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї." +deleteAndEditConfirm: "Ви впевнені, що хочете видалити цей запис та відредагувати + його? Ви втратите всі реакції, поширення та відповіді на нього." addToList: "Додати до списку" sendMessage: "Надіслати повідомлення" copyUsername: "Скопіювати ім’я користувача" @@ -64,9 +67,11 @@ import: "Імпорт" export: "Експорт" files: "Файли" download: "Завантажити" -driveFileDeleteConfirm: "Ви впевнені, що хочете видалити файл {name}? Нотатки із цим файлом також буде видалено." +driveFileDeleteConfirm: "Ви впевнені, що хочете видалити файл {name}? Його буде видалено + з усіх записів які містили його." unfollowConfirm: "Ви впевнені, що хочете відписатися від {name}?" -exportRequested: "Експортування розпочато. Це може зайняти деякий час. Після завершення експорту отриманий файл буде додано на диск." +exportRequested: "Експортування розпочато. Це може зайняти деякий час. Після завершення + експорту отриманий файл буде додано на диск." importRequested: "Імпортування розпочато. Це може зайняти деякий час." lists: "Списки" noLists: "Немає списків" @@ -80,10 +85,12 @@ manageLists: "Управління списками" error: "Помилка" somethingHappened: "Щось пішло не так" retry: "Спробувати знову" -pageLoadError: "Помилка при завантаженні сторінки" -pageLoadErrorDescription: "Зазвичай це пов’язано з помилками мережі або кешем браузера. Очистіть кеш або почекайте трохи й спробуйте ще раз." +pageLoadError: "Помилка при завантаженні сторінки." +pageLoadErrorDescription: "Зазвичай це пов’язано з помилками мережі або кешем браузера. + Очистіть кеш або почекайте трохи й спробуйте ще раз." serverIsDead: "Відповіді від сервера немає. Зачекайте деякий час і повторіть спробу." -youShouldUpgradeClient: "Перезавантажте та використовуйте нову версію клієнта, щоб переглянути цю сторінку." +youShouldUpgradeClient: "Перезавантажте та використовуйте нову версію клієнта, щоб + переглянути цю сторінку." enterListName: "Введіть назву списку" privacy: "Конфіденційність" makeFollowManuallyApprove: "Підтверджувати підписників уручну" @@ -95,10 +102,10 @@ unfollow: "Відписатись" followRequestPending: "Очікуючі запити на підписку" enterEmoji: "Введіть емодзі" renote: "Поширити" -unrenote: "Відміна поширення" -renoted: "Поширити запис." -cantRenote: "Неможливо поширити." -cantReRenote: "Поширення не можливо поширити." +unrenote: "скасувати поширення" +renoted: "Поширено." +cantRenote: "Цей запис неможливо поширити." +cantReRenote: "Поширення неможливо поширити." quote: "Цитата" pinnedNote: "Закріплений запис" pinned: "Закріпити" @@ -108,7 +115,8 @@ sensitive: "NSFW" add: "Додати" reaction: "Реакції" reactionSetting: "Налаштування реакцій" -reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати." +reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб + видалити, Натиснути \"+\" щоб додати." rememberNoteVisibility: "Пам’ятати параметри видимісті" attachCancel: "Видалити вкладення" markAsSensitive: "Позначити як NSFW" @@ -137,14 +145,20 @@ emojiUrl: "URL емодзі" addEmoji: "Додати емодзі" settingGuide: "Рекомендована конфігурація" cacheRemoteFiles: "Кешувати дані з інших інстансів" -cacheRemoteFilesDescription: "Якщо кешування вимкнено, віддалені файли завантажуються безпосередньо з віддаленого інстансу. Це зменшує використання сховища, але збільшує трафік, оскільки не генеруются ескізи." +cacheRemoteFilesDescription: "Якщо кешування вимкнено, віддалені файли завантажуються + безпосередньо з віддаленого серверу. Це зменшує використання сховища, але збільшує + трафік, оскільки не генеруются ескізи." flagAsBot: "Акаунт бота" -flagAsBotDescription: "Ввімкніть якщо цей обліковий запис використовується ботом. Ця опція позначить обліковий запис як бота. Це потрібно щоб виключити безкінечну інтеракцію між ботами а також відповідного підлаштування Firefish." +flagAsBotDescription: "Ввімкніть якщо цей обліковий запис використовується ботом. + Ця опція позначить обліковий запис як бота. Це потрібно щоб виключити безкінечну + інтеракцію між ботами а також відповідного підлаштування Calckey." flagAsCat: "Акаунт кота" flagAsCatDescription: "Ввімкніть, щоб позначити, що обліковий запис є котиком." flagShowTimelineReplies: "Показувати відповіді на нотатки на часовій шкалі" -flagShowTimelineRepliesDescription: "Показує відповіді користувачів на нотатки інших користувачів на часовій шкалі." -autoAcceptFollowed: "Автоматично приймати запити на підписку від користувачів, на яких ви підписані" +flagShowTimelineRepliesDescription: "Показує відповіді користувачів на нотатки інших + користувачів на часовій шкалі." +autoAcceptFollowed: "Автоматично приймати запити на підписку від користувачів, на + яких ви підписані" addAccount: "Додати акаунт" loginFailed: "Не вдалося увійти" showOnRemote: "Переглянути в оригіналі" @@ -156,13 +170,17 @@ searchWith: "Пошук: {q}" youHaveNoLists: "У вас немає списків" followConfirm: "Підписатися на {name}?" proxyAccount: "Проксі-акаунт" -proxyAccountDescription: "Обліковий запис проксі – це обліковий запис, який діє як віддалений підписник для користувачів за певних умов. Наприклад, коли користувач додає віддаленого користувача до списку, активність віддаленого користувача не буде доставлена на сервер, якщо жоден локальний користувач не стежить за цим користувачем, то замість нього буде використовуватися обліковий запис проксі-сервера." +proxyAccountDescription: "Обліковий запис проксі – це обліковий запис, який діє як + віддалений підписник для користувачів за певних умов. Наприклад, коли користувач + додає віддаленого користувача до списку, активність віддаленого користувача не буде + доставлена на сервер, якщо жоден локальний користувач не стежить за цим користувачем, + то замість нього буде використовуватися обліковий запис проксі-сервера." host: "Хост" selectUser: "Виберіть користувача" recipient: "Отримувач" annotation: "Коментарі" federation: "Федіверс" -instances: "Інстанс" +instances: "Сервери" registeredAt: "Приєднався(лась)" latestRequestSentAt: "Останній запит надіслано" latestRequestReceivedAt: "Останній запит прийнято" @@ -172,7 +190,7 @@ charts: "Графіки" perHour: "Щогодинно" perDay: "Щоденно" stopActivityDelivery: "Припинити розсилання активності" -blockThisInstance: "Заблокувати цей інстанс" +blockThisInstance: "Заблокувати цей сервер" operations: "Операції" software: "Програмне забезпечення" version: "Версія" @@ -182,15 +200,17 @@ jobQueue: "Черга завдань" cpuAndMemory: "ЦП та пам'ять" network: "Мережа" disk: "Диск" -instanceInfo: "Про цей інстанс" +instanceInfo: "Про цей сервер" statistics: "Статистика" clearQueue: "Очистити чергу" clearQueueConfirmTitle: "Ви впевнені, що хочете очистити чергу?" -clearQueueConfirmText: "Будь-які невідправлені нотатки, що залишилися в черзі, не будуть передані. Зазвичай ця операція НЕ потрібна." +clearQueueConfirmText: "Будь-які невідправлені записи, що залишилися в черзі, не будуть + передані. Зазвичай ця операція НЕ потрібна." clearCachedFiles: "Очистити кеш" clearCachedFilesConfirm: "Ви впевнені, що хочете видалити всі кешовані файли?" -blockedInstances: "Заблоковані інстанси" -blockedInstancesDescription: "Вкажіть інстанси, які потрібно заблокувати. Перелічені інстанси більше не зможуть спілкуватися з цим інстансом." +blockedInstances: "Заблоковані сервери" +blockedInstancesDescription: "Вкажіть сервери, які потрібно заблокувати. Перелічені + сервери більше не зможуть спілкуватися з цим сервером." muteAndBlock: "Заглушення і блокування" mutedUsers: "Заглушені користувачі" blockedUsers: "Заблоковані користувачі" @@ -213,8 +233,8 @@ subscribing: "Підписка" publishing: "Публікація" notResponding: "Не відповідає" instanceFollowing: "Підписка на інстанс" -instanceFollowers: "Підписники інстансу" -instanceUsers: "Користувачі цього інстансу" +instanceFollowers: "Підписники серверу" +instanceUsers: "Користувачі цього серверу" changePassword: "Змінити пароль" security: "Безпека" retypedNotMatch: "Введені дані не збігаються." @@ -238,7 +258,8 @@ saved: "Збережено" messaging: "Чати" upload: "Завантажити" keepOriginalUploading: "Зберегти оригінальне зображення" -keepOriginalUploadingDescription: "Зберігає початково завантажене зображення як є. Якщо вимкнено, версія для відображення в Інтернеті буде створена під час завантаження." +keepOriginalUploadingDescription: "Зберігає початково завантажене зображення як є. + Якщо вимкнено, версія для відображення в Інтернеті буде створена під час завантаження." fromDrive: "З диска" fromUrl: "З посилання" uploadFromUrl: "Завантажити з посилання" @@ -288,7 +309,7 @@ inputNewFileName: "Введіть ім'я нового файлу" inputNewDescription: "Введіть новий заголовок" inputNewFolderName: "Введіть ім'я нової теки" circularReferenceFolder: "Ви намагаєтесь перемістити папку в її підпапку." -hasChildFilesOrFolders: "Ця тека не порожня і не може бути видалена" +hasChildFilesOrFolders: "Ця тека не порожня і не може бути видалена." copyUrl: "Копіювати URL" rename: "Перейменувати" avatar: "Аватар" @@ -304,8 +325,8 @@ unwatch: "Не стежити" accept: "Прийняти" reject: "Відхилити" normal: "Нормальний" -instanceName: "Назва інстансу" -instanceDescription: "Описання інстансу" +instanceName: "Назва серверу" +instanceDescription: "Опис серверу" maintainerName: "Ім'я адміністратора" maintainerEmail: "Email адміністратора" tosUrl: "URL умов використання" @@ -316,12 +337,13 @@ dayX: "{day}" monthX: "{month}" yearX: "{year}" pages: "Сторінки" -integration: "Інтеграція" +integration: "Інтеграції" connectService: "Під’єднати" disconnectService: "Відключитися" enableLocalTimeline: "Увімкнути локальну стрічку" enableGlobalTimeline: "Увімкнути глобальну стрічку" -disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті." +disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх + стрічок, навіть якщо вони вимкнуті." registration: "Реєстрація" enableRegistration: "Дозволити реєстрацію" invite: "Запросити" @@ -333,11 +355,13 @@ bannerUrl: "URL банера" backgroundImageUrl: "URL-адреса фонового зображення" basicInfo: "Основна інформація" pinnedUsers: "Закріплені користувачі" -pinnedUsersDescription: "Впишіть в список користувачів, яких хочете закріпити на сторінці \"Знайти\", ім'я в стовпчик." +pinnedUsersDescription: "Впишіть в список користувачів, яких хочете закріпити на сторінці + \"Знайти\", ім'я в стовпчик." pinnedPages: "Закріплені сторінки" -pinnedPagesDescription: "Введіть шляхи сторінок, які ви бажаєте закріпити на головній сторінці цього інстанса, розділені новими рядками." -pinnedClipId: "Ідентифікатор закріпленої замітки." -pinnedNotes: "Закріплена нотатка" +pinnedPagesDescription: "Введіть шляхи сторінок, які ви бажаєте закріпити на головній + сторінці цього інстанса, розділені новими рядками." +pinnedClipId: "Ідентифікатор закріпленої замітки" +pinnedNotes: "Закріплений запис" hcaptcha: "hCaptcha" enableHcaptcha: "Увімкнути hCaptcha" hcaptchaSiteKey: "Ключ сайту" @@ -346,22 +370,25 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "Увімкнути reCAPTCHA" recaptchaSiteKey: "Ключ сайту" recaptchaSecretKey: "Секретний ключ" -avoidMultiCaptchaConfirm: "Використання кількох систем Captcha може спричинити перешкоди між ними. Бажаєте вимкнути інші активні системи Captcha? Якщо ви хочете, щоб вони залишалися ввімкненими, натисніть «Скасувати»." +avoidMultiCaptchaConfirm: "Використання кількох систем Captcha може спричинити перешкоди + між ними. Бажаєте вимкнути інші активні системи Captcha? Якщо ви хочете, щоб вони + залишалися ввімкненими, натисніть «Скасувати»." antennas: "Антени" manageAntennas: "Налаштування антен" name: "Ім'я" antennaSource: "Джерело антени" antennaKeywords: "Ключові слова антени" antennaExcludeKeywords: "Винятки" -antennaKeywordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\"" -notifyAntenna: "Сповіщати про нові нотатки" -withFileAntenna: "Тільки нотатки з вкладеними файлами" +antennaKeywordsDescription: "Відокремте пробілами для умови \"І\" або перенесенням + до нового рядка для умови \"АБО\"." +notifyAntenna: "Сповіщати про нові записи" +withFileAntenna: "Тільки записи з вкладеними файлами" enableServiceworker: "Ввімкнути ServiceWorker" antennaUsersDescription: "Список імя користувачів в стопчик" caseSensitive: "З урахуванням регістру" withReplies: "Включаючи відповіді" connectedTo: "Наступні акаунти під'єднані" -notesAndReplies: "Нотатки та відповіді" +notesAndReplies: "Записи та відповіді" withFiles: "Файли" silence: "Заглушити" silenceConfirm: "Ви впевнені, що хочете заглушити цього користувача?" @@ -397,7 +424,7 @@ notFoundDescription: "Сторінка за вказаною адресою не uploadFolder: "Місце для завантаження за замовчуванням" cacheClear: "Очистити кеш" markAsReadAllNotifications: "Позначити всі сповіщення як прочитані" -markAsReadAllUnreadNotes: "Позначити всі нотатки як прочитані" +markAsReadAllUnreadNotes: "Позначити всі записи як прочитані" markAsReadAllTalkMessages: "Позначити всі повідомлення як прочитані" help: "Допомога" inputMessageHere: "Введіть повідомлення тут" @@ -418,7 +445,7 @@ text: "Текст" enable: "Увімкнути" next: "Далі" retype: "Введіть ще раз" -noteOf: "Нотатка {user}" +noteOf: "Запис {user}" inviteToGroup: "Запрошення до групи" quoteAttached: "Цитата" quoteQuestion: "Ви хочете додати цитату?" @@ -431,7 +458,8 @@ invitationCode: "Код запрошення" checking: "Перевірка…" available: "Доступно" unavailable: "Недоступно" -usernameInvalidFormat: "літери, цифри та _ є прийнятними" +usernameInvalidFormat: "Ви можете використовувати великі та малі літери, цифри та + підкреслення." tooShort: "Занадто короткий" tooLong: "Занадто довгий" weakPassword: "Слабкий пароль" @@ -454,7 +482,7 @@ joinOrCreateGroup: "Отримуйте запрошення до груп або noHistory: "Історія порожня" signinHistory: "Історія входів" disableAnimatedMfm: "Відключити анімації MFM" -doing: "Виконується" +doing: "Виконується..." category: "Категорія" tags: "Теги" docSource: "Джерело цього документа" @@ -476,29 +504,34 @@ accountSettings: "Налаштування акаунта" promotion: "Виділене" promote: "Виділити" numberOfDays: "Кількість днів" -hideThisNote: "Сховати цю нотатку" -showFeaturedNotesInTimeline: "Показувати популярні нотатки у стрічці" +hideThisNote: "Сховати цей запис" +showFeaturedNotesInTimeline: "Показувати популярні записи у стрічці" objectStorage: "Object Storage" useObjectStorage: "Використовувати object storage" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "Це початкова частина адреси, що використовується CDN або проксі, наприклад для S3: https://.s3.amazonaws.com, або GCS: 'https://storage.googleapis.com/'" +objectStorageBaseUrlDesc: "Це початкова частина адреси, що використовується CDN або + проксі, наприклад для S3: https://.s3.amazonaws.com, або GCS: 'https://storage.googleapis.com/'" objectStorageBucket: "Bucket" objectStorageBucketDesc: "Будь ласка вкажіть назву відра в налаштованому сервісі." objectStoragePrefix: "Prefix" objectStoragePrefixDesc: "Файли будуть зберігатись у розташуванні з цим префіксом." -objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "Залиште пустим при використанні AWS S3. Інакше введіть кінцевий пункт як '' або ':' слідуючи інструкціям сервісу, який використовується." +objectStorageEndpoint: "Кінцевий пункт" +objectStorageEndpointDesc: "Залиште пустим при використанні AWS S3. Інакше введіть + кінцевий пункт як '' або ':' слідуючи інструкціям сервісу, який + використовується." objectStorageRegion: "Region" -objectStorageRegionDesc: "Введіть регіон у формі 'xx-east-1'. Залиште пустим, якщо ваш сервіс не різниться відповідно до регіонів, або введіть 'us-east-1'." +objectStorageRegionDesc: "Введіть регіон у формі 'xx-east-1'. Залиште пустим, якщо + ваш сервіс не різниться відповідно до регіонів, або введіть 'us-east-1'." objectStorageUseSSL: "Використовувати SSL" objectStorageUseSSLDesc: "Вимкніть коли не використовується HTTPS для з'єднання API" objectStorageUseProxy: "Використовувати Proxy" -objectStorageUseProxyDesc: "Вимкніть коли проксі не використовується для з'єднання ObjectStorage" +objectStorageUseProxyDesc: "Вимкніть коли проксі не використовується для з'єднання + ObjectStorage" objectStorageSetPublicRead: "Встановіть 'публічне читання' при завантаженні" serverLogs: "Журнал сервера" deleteAll: "Видалити все" -showFixedPostForm: "Показати форму запису над стрічкою новин." -newNoteRecived: "Є нові нотатки" +showFixedPostForm: "Показати форму запису над стрічкою новин" +newNoteRecived: "Є нові записи" sounds: "Звуки" listen: "Слухати" none: "Відсутній" @@ -521,7 +554,8 @@ sort: "Сортування" ascendingOrder: "За зростанням" descendingOrder: "За спаданням" scratchpad: "Чернетка" -scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Firefish." +scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. + Ви можете писати, виконувати його і тестувати взаємодію з Calckey." output: "Вихід" script: "Скрипт" disablePagesScript: "Вимкнути AiScript на Сторінках" @@ -529,11 +563,14 @@ updateRemoteUser: "Оновити інформацію про віддалено deleteAllFiles: "Видалити всі файли" deleteAllFilesConfirm: "Ви дійсно хочете видалити всі файли?" removeAllFollowing: "Скасувати всі підписки" -removeAllFollowingDescription: "Скасувати підписку на всі акаунти з {host}. Будь ласка, робіть це, якщо інстанс більше не існує." +removeAllFollowingDescription: "Скасувати підписку на всі акаунти з {host}. Будь ласка, + робіть це, якщо сервер більше не існує." userSuspended: "Обліковий запис заблокований." userSilenced: "Обліковий запис приглушений." yourAccountSuspendedTitle: "Цей обліковий запис заблоковано" -yourAccountSuspendedDescription: "Цей обліковий запис було заблоковано через порушення умов надання послуг сервера. Зв'яжіться з адміністратором, якщо ви хочете дізнатися докладнішу причину. Будь ласка, не створюйте новий обліковий запис." +yourAccountSuspendedDescription: "Цей обліковий запис було заблоковано через порушення + умов надання послуг сервера. Зв'яжіться з адміністратором, якщо ви хочете дізнатися + докладнішу причину. Будь ласка, не створюйте новий обліковий запис." menu: "Меню" divider: "Розділювач" addItem: "Додати елемент" @@ -542,8 +579,8 @@ addRelay: "Додати ретранслятор" inboxUrl: "Inbox URL" addedRelays: "Додані ретранслятори" serviceworkerInfo: "Повинен бути ввімкнений для push-сповіщень." -deletedNote: "Видалена нотатка" -invisibleNote: "Приховані записи" +deletedNote: "Видалений запис" +invisibleNote: "Прихований запис" enableInfiniteScroll: "Увімкнути нескінченну прокрутку" visibility: "Видимість" poll: "Опитування" @@ -573,12 +610,14 @@ permission: "Права" enableAll: "Увімкнути все" disableAll: "Вимкнути все" tokenRequested: "Надати доступ до акаунту" -pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані." +pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут + вказані." notificationType: "Тип сповіщення" edit: "Редагувати" emailServer: "Сервер електронної пошти" enableEmail: "Увімкнути функцію доставки пошти" -emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." +emailConfigInfo: "Використовується для підтвердження електронної пошти під час реєстрації, + а також для відновлення паролю" email: "E-mail" emailAddress: "E-mail адреса" smtpConfig: "Налаштування сервера SMTP" @@ -586,14 +625,16 @@ smtpHost: "Хост" smtpPort: "Порт" smtpUser: "Ім'я користувача" smtpPass: "Пароль" -emptyToDisableSmtpAuth: "Залиште назву користувача і пароль пустими для вимкнення підтвердження SMTP" +emptyToDisableSmtpAuth: "Залиште назву користувача і пароль пустими для вимкнення + підтвердження SMTP" smtpSecure: "Використовувати безумовне шифрування SSL/TLS для з'єднань SMTP" -smtpSecureInfo: "Вимкніть при використанні STARTTLS " +smtpSecureInfo: "Вимкніть при використанні STARTTLS" testEmail: "Тестовий email" wordMute: "Блокування слів" regexpError: "Помилка регулярного виразу" -regexpErrorDescription: "Сталася помилка в регулярному виразі в рядку {line} вашого слова {tab} слова що ігноруються:" -instanceMute: "Приглушення інстансів" +regexpErrorDescription: "Сталася помилка в регулярному виразі в рядку {line} вашого + слова {tab} слова що ігноруються:" +instanceMute: "Приглушення серверів" userSaysSomething: "{name} щось сказав(ла)" makeActive: "Активувати" display: "Відображення" @@ -606,12 +647,15 @@ database: "База даних" channel: "Канали" create: "Створити" notificationSetting: "Параметри сповіщень" -notificationSettingDesc: "Виберіть типи сповіщень для відображення" +notificationSettingDesc: "Оберіть типи сповіщень для відображення." useGlobalSetting: "Застосувати глобальнi параметри" -useGlobalSettingDesc: "Якщо увімкнено, то будуть використовуватись налаштування повідомлень облікового запису, інакше можливо налаштувати індивідуально." +useGlobalSettingDesc: "Якщо увімкнено, то будуть використовуватись налаштування повідомлень + облікового запису, інакше можливо налаштувати індивідуально." other: "Інше" regenerateLoginToken: "Оновити Login Token" -regenerateLoginTokenDescription: "Регенерувати внутрішній ключ використовуваний під час входу. Зазвичай цього не потрібно робити. При регенерації всі пристрої вийдуть з системи." +regenerateLoginTokenDescription: "Регенерувати внутрішній ключ використовуваний під + час входу. Зазвичай цього не потрібно робити. При регенерації всі пристрої вийдуть + з системи." setMultipleBySeparatingWithSpace: "Можна вказати кілька значень, відділивши їх пробілом." fileIdOrUrl: "Ідентифікатор файлу або посилання" behavior: "Поведінка" @@ -619,19 +663,22 @@ sample: "Приклад" abuseReports: "Скарги" reportAbuse: "Поскаржитись" reportAbuseOf: "Поскаржитись на {name}" -fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги. Якщо скарга стосується запису, вкажіть посилання на нього." +fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги. Якщо скарга стосується + запису, вкажіть посилання на нього." abuseReported: "Дякуємо, вашу скаргу було відправлено. " reporter: "Репортер" reporteeOrigin: "Про кого повідомлено" reporterOrigin: "Хто повідомив" forwardReport: "Переслати звіт на віддалений інстанс" -forwardReportIsAnonymous: "Замість вашого облікового запису анонімний системний обліковий запис буде відображатися як доповідач на віддаленому інстансі" +forwardReportIsAnonymous: "Замість вашого облікового запису, анонімний системний обліковий + запис буде відображатися як доповідач на віддаленому сервері." send: "Відправити" abuseMarkAsResolved: "Позначити скаргу як вирішену" openInNewTab: "Відкрити в новій вкладці" openInSideView: "Відкрити збоку" defaultNavigationBehaviour: "Поведінка навігації за замовчуванням" -editTheseSettingsMayBreakAccount: "Зміна цих параметрів може призвести до пошкодження вашого акаунта." +editTheseSettingsMayBreakAccount: "Зміна цих параметрів може призвести до пошкодження + вашого акаунта." instanceTicker: "Мітка з назвою інстанса в нотатках" waitingFor: "Чекаємо на {x}" random: "Випадковий" @@ -643,10 +690,11 @@ createNew: "Створити новий" optional: "Необов'язково" createNewClip: "Створити нотатку" public: "Публічний" -i18nInfo: "Firefish перекладається на різні мови волонтерами. Ви можете допомогти: {link}" +i18nInfo: "Calckey перекладається на різні мови волонтерами. Ви можете допомогти за + посиланням: {link}." manageAccessTokens: "Керування токенами доступу" accountInfo: "Інформація про акаунт" -notesCount: "Кількість нотаток" +notesCount: "Кількість записів" repliesCount: "Кількість надісланих відповідей" renotesCount: "Кількість поширень" repliedCount: "Кількість отриманих відповідей" @@ -662,15 +710,19 @@ no: "Ні" driveFilesCount: "Кількість файлів на диску" driveUsage: "Використання місця на диску" noCrawle: "Заборонити індексацію" -noCrawleDescription: "Просити пошукові системи не індексувати ваш профіль, нотатки, сторінки тощо." -lockedAccountInfo: "Якщо видимість вашого запису не встановлена як \"Тільки підписники\", то кожен зможе побачити ваш запис, навіть якщо ви вимагаєте підтвердження підписок вручну." -alwaysMarkSensitive: "Позначати NSFW за замовчуванням" +noCrawleDescription: "Просити пошукові системи не індексувати ваш профіль, записи, + сторінки тощо." +lockedAccountInfo: "Якщо видимість вашого запису не встановлена як \"Тільки підписники\"\ + , то кожен зможе побачити ваш запис, навіть якщо ви вимагаєте підтвердження підписок + вручну." +alwaysMarkSensitive: "Позначати як NSFW за замовчуванням" loadRawImages: "Відображати вкладені зображення повністю замість ескізів" disableShowingAnimatedImages: "Не програвати анімовані зображення" -verificationEmailSent: "Електронний лист з підтвердженням відісланий. Будь ласка перейдіть по посиланню в листі для підтвердження." +verificationEmailSent: "Електронний лист з підтвердженням відісланий. Будь ласка перейдіть + по посиланню в листі для підтвердження." notSet: "Не налаштовано" -emailVerified: "Електронну пошту підтверджено." -noteFavoritesCount: "Кількість улюблених нотаток" +emailVerified: "Електронну пошту підтверджено" +noteFavoritesCount: "Кількість улюблених записів" pageLikesCount: "Кількість отриманих вподобань сторінки" pageLikedCount: "Кількість вподобаних сторінок" contact: "Контакт" @@ -679,7 +731,8 @@ clips: "Добірка" experimentalFeatures: "Експериментальні функції" developer: "Розробник" makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\"" -makeExplorableDescription: "Вимкніть, щоб обліковий запис не показувався у розділі \"Огляд\"." +makeExplorableDescription: "Вимкніть, щоб обліковий запис не показувався у розділі + \"Огляд\"." showGapBetweenNotesInTimeline: "Показувати розрив між записами у стрічці новин" duplicate: "Дублікат" left: "Лівий" @@ -694,7 +747,10 @@ onlineUsersCount: "{n} користувачів онлайн" nUsers: "{n} Користувачів" nNotes: "{n} Записів" sendErrorReports: "Надіслати звіт про помилки" -sendErrorReportsDescription: "При увімкненні детальна інформація про помилки буде надана Firefish у разі виникнення проблем, що дасть можливість покращити Firefish." +sendErrorReportsDescription: "Якщо увімкнено, детальна інформація про помилки буде + передаватися до Calckey, коли виникає проблема, це допоможе покращити якість роботи + Calckey.\nЦе буде включати інформацію таку як: версія вашої ОС, який браузер ви + використовуєте, ваша активність в Calckey тощо." myTheme: "Моя тема" backgroundColor: "Фон" accentColor: "Акцент" @@ -718,7 +774,7 @@ capacity: "Ємність" inUse: "Зайнято" editCode: "Редагувати вихідний текст" apply: "Застосувати" -receiveAnnouncementFromInstance: "Отримувати оповіщення з інстансу" +receiveAnnouncementFromInstance: "Отримувати сповіщення з серверу" emailNotification: "Сповіщення електронною поштою" publish: "Опублікувати" inChannelSearch: "Пошук за каналом" @@ -726,12 +782,12 @@ useReactionPickerForContextMenu: "Відкривати палітру реакц typingUsers: "Стук клавіш. Це {users}…" goBack: "Назад" info: "Інформація" -user: "Користувачі" +user: "Користувач" administration: "Управління" expiration: "Опитування закінчується" middle: "Середній" global: "Глобальна" -sent: "Відправити" +sent: "Відправлене" hashtags: "Хештеґ" hide: "Сховати" searchByGoogle: "Пошук" @@ -756,13 +812,15 @@ _registry: domain: "Домен" createKey: "Створити ключ" _aboutMisskey: - about: "Misskey - це програмне забезпечення з відкритим кодом, яке розробляє syuilo з 2014 року." + about: "Misskey - це програмне забезпечення з відкритим кодом, яке розробляє syuilo + з 2014 року." contributors: "Головні помічники" allContributors: "Всі помічники" source: "Вихідний код" - translation: "Перекладати Firefish" - donate: "Пожертвувати Firefish" - morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених тут. Дякуємо! 🥰" + translation: "Перекладати Calckey" + donate: "Пожертвувати Calckey" + morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених + тут. Дякуємо! 🥰" patrons: "Підтримали" _nsfw: respect: "Приховувати NSFW медіа" @@ -770,10 +828,12 @@ _nsfw: force: "Приховувати всі медіа файли" _mfm: cheatSheet: " Довідка MFM" - intro: "MFM це ексклюзивна мова розмітки тексту в Firefish, яку можна використовувати в багатьох місцях. Тут ви можете переглянути приклади її синтаксису." - dummy: "Firefish розширює світ Федіверсу" + intro: "MFM це ексклюзивна мова розмітки тексту в Calckey, яку можна використовувати + в багатьох місцях. Тут ви можете переглянути приклади її синтаксису." + dummy: "Calckey розширює світ Федіверсу" mention: "Згадка" - mentionDescription: "За допомогою знака \"@\" перед ім'ям можна згадати конкретного користувача." + mentionDescription: "За допомогою знака \"@\" перед ім'ям можна згадати конкретного + користувача." hashtag: "Хештеґ" hashtagDescription: "За допомогою знака \"решітка\" перед словом задається хештег." url: "URL" @@ -819,7 +879,8 @@ _mfm: x4: "Надзвичайно великий" x4Description: "Показує контент надзвичайно великим." blur: "Розмиття" - blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші." + blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, + якщо навести на нього вказівник миші." font: "Шрифт" fontDescription: "Встановлює шрифт для контенту." rotate: "Обертати" @@ -844,10 +905,14 @@ _menuDisplay: hide: "Сховати" _wordMute: muteWords: "Заглушені слова" - muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\"" - muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"." + muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової + лінійки для \"АБО\"" + muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж + слешів \"/\"." softDescription: "Приховати записи які відповідають критеріям зі стрічки подій." - hardDescription: "Приховати записи які відповідають критеріям зі стрічки подій. Також приховані записи не будуть додані до стрічки подій навіть якщо критерії буде змінено." + hardDescription: "Приховати записи які відповідають критеріям зі стрічки подій. + Також приховані записи не будуть додані до стрічки подій навіть якщо критерії + буде змінено." soft: "М'яко" hard: "Жорстко" mutedNotes: "Заблоковані нотатки" @@ -942,21 +1007,32 @@ _tutorial: step1_1: "Ласкаво просимо!" step1_2: "Давайте налаштуємо вас. Ви будете працювати в найкоротші терміни!" step2_1: "Спочатку, будь ласка, заповніть свій профіль" - step2_2: "Надавши деяку інформацію про себе, іншим людям буде легше зрозуміти, чи хочуть вони бачити ваші записи або стежити за вами." + step2_2: "Надавши деяку інформацію про себе, іншим людям буде легше зрозуміти, чи + хочуть вони бачити ваші записи або стежити за вами." step3_1: "Тепер настав час стежити за деякими людьми!" - step3_2: "Ваша домашня і соціальна стрічки ґрунтуються на тому, за ким ви стежите, тому для початку спробуйте стежити за кількома акаунтами.\nНатисніть на гурток із плюсом у правому верхньому кутку профілю, щоб стежити за ним." + step3_2: "Ваша домашня і соціальна стрічки ґрунтуються на тому, за ким ви стежите, + тому для початку спробуйте стежити за кількома акаунтами.\nНатисніть на гурток + із плюсом у правому верхньому кутку профілю, щоб стежити за ним." step4_1: "Давайте вийдемо на вас" - step4_2: "Для свого першого повідомлення деякі люди люблять робити {introduction} повідомлення або просте \"Hello world!\"" + step4_2: "Для свого першого повідомлення деякі люди люблять робити {introduction} + повідомлення або просте \"Hello world!\"" step5_1: "Тимчасові рамки, скрізь тимчасові рамки!" step5_2: "У вашому екземплярі включені {timelines} різних часових ліній." - step5_3: "Головна {icon} часова шкала - це шкала, де ви можете бачити повідомлення ваших підписників." - step5_4: "Місцева {icon} тимчасова шкала - це шкала, де ви можете бачити повідомлення всіх інших користувачів даного екземпляра" - step5_5: "Тимчасова шкала Рекомендовані {icon} - це шкала, де ви можете бачити повідомлення від інстанцій, рекомендованих адміністраторами." - step5_6: "На часовій шкалі Social {icon} відображаються повідомлення від друзів ваших підписників" - step5_7: "Глобальна {icon} часова шкала - це місце, де ви можете бачити повідомлення від усіх інших підключених екземплярів" + step5_3: "Головна {icon} часова шкала - це шкала, де ви можете бачити повідомлення + ваших підписників." + step5_4: "Місцева {icon} тимчасова шкала - це шкала, де ви можете бачити повідомлення + всіх інших користувачів даного екземпляра" + step5_5: "Тимчасова шкала Рекомендовані {icon} - це шкала, де ви можете бачити повідомлення + від інстанцій, рекомендованих адміністраторами." + step5_6: "На часовій шкалі Social {icon} відображаються повідомлення від друзів + ваших підписників" + step5_7: "Глобальна {icon} часова шкала - це місце, де ви можете бачити повідомлення + від усіх інших підключених екземплярів" step6_1: "Отже, що це за місце?" - step6_2: "Ну, ви не просто приєдналися до Кальки. Ви приєдналися до порталу в Fediverse, взаємопов'язаної мережі з тисяч серверів, званих \"інстансами\"." - step6_3: "Кожен сервер працює по-своєму, і не на всіх серверах працює Firefish. Але цей працює! Це трохи складно, але ви швидко розберетеся" + step6_2: "Ну, ви не просто приєдналися до Кальки. Ви приєдналися до порталу в Fediverse, + взаємопов'язаної мережі з тисяч серверів, званих \"інстансами\"." + step6_3: "Кожен сервер працює по-своєму, і не на всіх серверах працює Calckey. Але + цей працює! Це трохи складно, але ви швидко розберетеся" step6_4: "Тепер ідіть, вивчайте і розважайтеся!" _2fa: registerSecurityKey: "Зареєструвати новий ключ безпеки" @@ -1078,7 +1154,8 @@ _profile: youCanIncludeHashtags: "Ви також можете включити хештеги у свій опис." metadata: "Додаткова інформація" metadataEdit: "Редагувати додаткову інформацію" - metadataDescription: "Ви можете вказати до чотирьох пунктів додаткової інформації у своєму профілі." + metadataDescription: "Ви можете вказати до чотирьох пунктів додаткової інформації + у своєму профілі." metadataLabel: "Назва" metadataContent: "Вміст" changeAvatar: "Змінити аватар" @@ -1388,7 +1465,8 @@ _pages: _for: arg1: "Кількість повторень" arg2: "Дія" - typeError: "Паз {slot} приймає \"{expect}\" тип, але надана змінна має тип \"{actual}\"!" + typeError: "Паз {slot} приймає \"{expect}\" тип, але надана змінна має тип \"\ + {actual}\"!" thereIsEmptySlot: "Паз {slot} пустий!" types: string: "Текст" @@ -1453,3 +1531,95 @@ _deck: list: "Списки" mentions: "Згадки" direct: "Особисте" +removeReaction: Видалити вашу реакцію +renoteMute: Ігнорувати поширення +renoteUnmute: Показувати поширення +flagSpeakAsCat: Говорити як кішка +accessibility: Доступність +priority: Пріорітет +high: Високий +customCss: Користувацькі CSS +itsOn: Увімкнено +showingPastTimeline: Наразі відображається стара стрічка +enabled: Увімкнено +noMaintainerInformationWarning: Інформація про супровідника не налаштована. +recommended: Рекомендоване +resolved: Вирішено +itsOff: Вимкнено +emailRequiredForSignup: Вимагати адресу електронної пошти для реєстрації +moderation: Модерація +selectInstance: Оберіть сервер +instanceSecurity: Безпека сервера +searchPlaceholder: Шукати в Calckey +editNote: Відредагувати запис +enableEmojiReactions: Ввімкнути реакції емодзі +low: Низький +emailNotConfiguredWarning: Адрес електронної пошти не встановлено. +unresolved: Не вирішено +offline: Не в мережі +disabled: Вимкнено +configure: Налаштувати +popularPosts: Популярні сторінки +silenced: Ігнорується +manageGroups: Керування групами +active: Активний +whatIsNew: Показати зміни +deleted: Видалено +selectChannel: Виберіть канал +flagSpeakAsCatDescription: Ваші записи будуть няніфіковані у режимі кота +userSaysSomethingReason: '{name} сказав(ла) {reason}' +clear: Очистити +userInfo: Інформація про користувача +selectAccount: Оберіть обліковий запис +switchAccount: Змінити обліковий запис +accounts: Облікові записи +switch: Змінити +noBotProtectionWarning: Захист від ботів не налаштовано. +gallery: Галерея +recentPosts: Недавні сторінки +privateModeInfo: Якщо увімкнено, лише сервери з білого списку можуть федеруватися + з вашим сервером. Всі повідомлення будуть приховані від публіки. +troubleshooting: Вирішення проблем +customCssWarn: Цей параметр слід використовувати лише тоді, коли ви знаєте, що він + робить. Введення неправильних значень може призвести до того, що клієнт перестане + нормально функціонувати. +newer: новіші +older: старіші +addDescription: Додати опис +notSpecifiedMentionWarning: У цьому записі згадуються користувачі, яких не було включено + до списку одержувачів +markAllAsRead: Позначити все як прочитане +userPagePinTip: Ви можете відображати записи тут, вибравши "Прикріпити до профілю" + в меню окремих записів. +unknown: Невідомо +onlineStatus: Онлайн-статус +hideOnlineStatus: Приховати онлайн-статус +online: В мережі +breakFollow: Видалити підписника +translate: Перекласти +translatedFrom: Перекладено з {x} +userSaysSomethingReasonQuote: '{name} цитував запис з {reason}' +userSaysSomethingReasonRenote: '{name} поширив запис з {reason}' +notRecommended: Не рекомендується +botProtection: Захист від ботів +instanceBlocking: Керування Федерацією +privateMode: Приватний режим +allowedInstances: Сервери у білому списку +previewNoteText: Показати прев'ю +antennaInstancesDescription: Введіть по одному хосту сервера на рядок +breakFollowConfirm: Ви дійсно бажаєте видалити підписника? +ads: Реклама +cw: Попередження про вміст +hiddenTags: Приховані хештеги +noInstances: Немає серверів +misskeyUpdated: Calckey оновлено! +received: Отримане +xl: Надвеликий +searchResult: Результати пошуку +useBlurEffect: Використовувати ефекти розмиття в інтерфейсі +learnMore: Дізнатися більше +usernameInfo: Ім'я, яке ідентифікує ваш обліковий запис серед інших на цьому сервері. Ви + можете використовувати алфавіт (a~z, A~Z), цифри (0~9) або знаки підкреслення (_). + Ім'я користувача не може бути змінено пізніше. +noThankYou: Ні, дякую +keepCw: Зберігати попередження про вміст diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 17dc7c8a1..36453551d 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1,71 +1,70 @@ _lang_: "简体中文" -headlineMisskey: "通过帖子连接在一起的网络" -introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n\ - 来探索新的世界吧!🚀" -monthAndDay: "{month}月 {day}日" +headlineMisskey: "一个开源、去中心化的社交媒体平台,永远免费!🚀" +introMisskey: "欢迎! Calckey 是一个开源、去中心化的社交媒体平台,永久免费!🚀" +monthAndDay: "{month} 月 {day} 日" search: "搜索" notifications: "通知" username: "用户名" password: "密码" forgotPassword: "忘记密码" -fetchingAsApObject: "正在联邦宇宙查询中" -ok: "OK" -gotIt: "我明白了" +fetchingAsApObject: "正在从联邦宇宙查询" +ok: "好" +gotIt: "知道了!" cancel: "取消" enterUsername: "输入用户名" -renotedBy: "由 {user} 转贴" +renotedBy: "转发自 {user}" noNotes: "没有帖子" -noNotifications: "无通知" +noNotifications: "没有通知" instance: "服务器" settings: "设置" basicSettings: "基本设置" -otherSettings: "其他设置" +otherSettings: "其它设置" openInWindow: "在新窗口中打开" profile: "个人资料" timeline: "时间线" -noAccountDescription: "这个人很懒,没有写自我介绍" +noAccountDescription: "这个人很懒,没有写自我介绍。" login: "登录" -loggingIn: "正在登录..." +loggingIn: "正在登录" logout: "登出" signup: "新用户注册" -uploading: "正在上传" +uploading: "正在上传..." save: "保存" users: "用户" addUser: "添加用户" -favorite: "收藏" -favorites: "收藏" +favorite: "添加到书签" +favorites: "书签" unfavorite: "取消收藏" -favorited: "已加入收藏夹。" -alreadyFavorited: "收藏夹中已存在。" -cantFavorite: "无法添加到收藏夹。" +favorited: "已添加到书签。" +alreadyFavorited: "书签中已存在。" +cantFavorite: "无法添加到书签。" pin: "置顶" unpin: "取消置顶" copyContent: "复制内容" copyLink: "复制链接" delete: "删除" deleteAndEdit: "删除并编辑" -deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。" +deleteAndEditConfirm: "要删除此帖子并再次编辑吗?对此帖子的所有回应、转发和回复也将被删除。" addToList: "添加至列表" sendMessage: "发送" copyUsername: "复制用户名" searchUser: "搜索用户" reply: "回复" -loadMore: "查看更多" +loadMore: "加载更多" showMore: "查看更多" showLess: "关闭" -youGotNewFollower: "你有新的关注者" -receiveFollowRequest: "您收到了关注请求" -followRequestAccepted: "您的关注请求被通过了" +youGotNewFollower: "关注了您" +receiveFollowRequest: "收到了关注请求" +followRequestAccepted: "关注请求已通过" mention: "提及" mentions: "提及" directNotes: "私信" -importAndExport: "导入和导出" +importAndExport: "导入/导出数据" import: "导入" export: "导出" files: "文件" download: "下载" -driveFileDeleteConfirm: "要删除文件「{name}」吗?它将被所有作为附件包含它的帖子中删除。" -unfollowConfirm: "要取消对{name}的关注吗?" +driveFileDeleteConfirm: "要删除文件「{name}」吗?它将从所有作为附件包含它的帖子中删除。" +unfollowConfirm: "要取消关注 {name} 吗?" exportRequested: "导出请求已提交,这可能需要花一些时间,导出的文件将保存到网盘中。" importRequested: "导入请求已提交,这可能需要花一点时间。" lists: "列表" @@ -74,14 +73,14 @@ note: "帖子" notes: "帖子" following: "关注中" followers: "关注者" -followsYou: "正在关注你" +followsYou: "关注了您" createList: "创建列表" manageLists: "管理列表" error: "错误" -somethingHappened: "出现了一些问题!" +somethingHappened: "发生了一个错误" retry: "重试" -pageLoadError: "页面加载失败。" -pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。" +pageLoadError: "页面加载时发生错误。" +pageLoadErrorDescription: "这通常是由于网络错误或浏览器缓存的原因。请清除缓存或等待片刻后重试。" serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。" youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。" enterListName: "输入列表名称" @@ -89,15 +88,15 @@ privacy: "隐私" makeFollowManuallyApprove: "关注请求需要批准" defaultNoteVisibility: "默认可见性" follow: "关注" -followRequest: "关注申请" -followRequests: "关注申请" +followRequest: "关注请求" +followRequests: "关注请求" unfollow: "取消关注" -followRequestPending: "发送关注请求" +followRequestPending: "关注请求待批准" enterEmoji: "输入表情符号" renote: "转发" unrenote: "取消转发" renoted: "已转发。" -cantRenote: "该帖无法转发。" +cantRenote: "此帖子无法被转发。" cantReRenote: "转发无法被再次转发。" quote: "引用" pinnedNote: "已置顶的帖子" @@ -109,65 +108,66 @@ add: "添加" reaction: "回应" enableEmojiReaction: "启用表情符号回应" showEmojisInReactionNotifications: "在回应通知中显示表情符号" -reactionSetting: "在选择器中显示的回应" +reactionSetting: "在回应选择器中显示的回应" reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。" -rememberNoteVisibility: "保存上次设置的可见性" +rememberNoteVisibility: "保存帖子可见性设置" attachCancel: "删除附件" markAsSensitive: "标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容" enterFileName: "请输入文件名" -mute: "屏蔽" -unmute: "解除屏蔽" -renoteMute: "屏蔽转帖" -renoteUnmute: "解除屏蔽转帖" -block: "拉黑" -unblock: "取消拉黑" +mute: "静音" +unmute: "取消静音" +renoteMute: "静音转发" +renoteUnmute: "取消静音转发" +block: "屏蔽" +unblock: "取消屏蔽" suspend: "冻结" unsuspend: "解除冻结" -blockConfirm: "确定要拉黑吗?" -unblockConfirm: "确定要解除拉黑吗?" -suspendConfirm: "要冻结吗?" -unsuspendConfirm: "要解除冻结吗?" +blockConfirm: "确定要屏蔽吗?" +unblockConfirm: "确定要取消屏蔽吗?" +suspendConfirm: "确定要冻结吗?" +unsuspendConfirm: "确定要解除冻结吗?" selectList: "选择列表" selectAntenna: "选择天线" -selectWidget: "选择小工具" -editWidgets: "编辑部件" +selectWidget: "选择小部件" +editWidgets: "编辑小部件" editWidgetsExit: "完成编辑" customEmojis: "自定义表情符号" emoji: "表情符号" emojis: "表情符号" emojiName: "表情符号名称" -emojiUrl: "表情符号地址" +emojiUrl: "表情符号 URL" addEmoji: "添加表情符号" settingGuide: "推荐配置" -cacheRemoteFiles: "远程文件缓存" -cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程实例载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" +cacheRemoteFiles: "缓存远程文件" +cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" flagAsBot: "这是一个机器人账号" -flagAsBotDescription: "如果此帐户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此帐户识别为机器人。" -flagAsCat: "将这个账户设定为一只猫" -flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后,会在您的头像上出现猫耳朵,并将你的帖子中的「na」替换为「nya」,日文同理。" +flagAsBotDescription: "如果此账号由程序控制,请启用此项。启用后,此标志可以帮助其它开发人员防止机器人之间产生无限互动的行为,并让 Calckey + 的内部系统将此账号识别为机器人。" +flagAsCat: "将这个账号设定为一只猫😺" +flagAsCatDescription: "您会长出猫耳朵并像猫一样说话!" flagShowTimelineReplies: "在时间线上显示帖子的回复" -flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" -autoAcceptFollowed: "自动允许关注者的关注" -addAccount: "添加账户" +flagShowTimelineRepliesDescription: "启用后,时间线除了显示用户的帖子外,还会显示其它用户对帖子的回复。" +autoAcceptFollowed: "自动批准来自关注者的关注请求" +addAccount: "添加账号" loginFailed: "登录失败" -showOnRemote: "转到所在实例显示" +showOnRemote: "转到所在服务器显示" general: "常规设置" wallpaper: "壁纸" setWallpaper: "设置壁纸" removeWallpaper: "移除壁纸" -searchWith: "搜索:{q}" +searchWith: "搜索:{q}" youHaveNoLists: "列表为空" -followConfirm: "你确定要关注{name}吗?" -proxyAccount: "代理账户" -proxyAccountDescription: "代理账户是在某些情况下充当用户的远程关注者的账户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该实例,因此将代之以代理账户。" +followConfirm: "您确定要关注 {name} 吗?" +proxyAccount: "代理账号" +proxyAccountDescription: "代理账号是在某些情况下充当用户的远程关注者的账号。 例如,当一个用户添加一个远程用户为代理账号时,如果没有本地用户关注该用户,远程用户的活动将不会被传递到服务器,因此代理账号将被关注。" host: "主机名" selectUser: "选择用户" -recipient: "收件人" +recipient: "接收者" annotation: "注解" federation: "联合" instances: "服务器" -registeredAt: "初次观测" +registeredAt: "初次观测于" latestRequestSentAt: "上次发送的请求" latestRequestReceivedAt: "上次收到的请求" latestStatus: "最后状态" @@ -176,94 +176,94 @@ charts: "图表" perHour: "每小时" perDay: "每天" stopActivityDelivery: "停止发送活动" -blockThisInstance: "阻止此实例向本实例推流" +blockThisInstance: "屏蔽此服务器" operations: "操作" software: "软件" version: "版本" metadata: "元数据" -monitor: "服务器状态" +monitor: "监测" jobQueue: "作业队列" -cpuAndMemory: "CPU和内存" +cpuAndMemory: "CPU 和内存" network: "网络" -disk: "存储" +disk: "磁盘" instanceInfo: "服务器信息" statistics: "统计" clearQueue: "清除队列" clearQueueConfirmTitle: "确定清除队列?" -clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。" +clearQueueConfirmText: "队列中任何未送达的帖子将不会发送。 通常,您不需要这样做。" clearCachedFiles: "清除缓存" -clearCachedFilesConfirm: "确定要清除缓存文件?" +clearCachedFilesConfirm: "确定要删除所有缓存的远程文件?" blockedInstances: "已屏蔽的服务器" -blockedInstancesDescription: "设定要阻拦的实例,以换行来进行分割。被阻拦的实例将无法与本实例进行交换通讯。" -muteAndBlock: "屏蔽/拉黑" -mutedUsers: "已屏蔽用户" -blockedUsers: "被拉黑的用户" +blockedInstancesDescription: "设定要屏蔽的服务器,一行一个。被屏蔽的服务器将无法与本服务器进行交换通讯。" +muteAndBlock: "静音与屏蔽" +mutedUsers: "已静音的用户" +blockedUsers: "已屏蔽的用户" noUsers: "无用户" -editProfile: "编辑资料" -noteDeleteConfirm: "要删除该帖子吗?" -pinLimitExceeded: "无法置顶更多了" -intro: "Misskey的部署结束啦!填写管理员账号吧!" +editProfile: "编辑个人资料" +noteDeleteConfirm: "确定要删除此帖子吗?" +pinLimitExceeded: "无法置顶更多帖子了" +intro: "Calckey 安装完成!请创建一个管理员用户。" done: "完成" processing: "正在处理" preview: "预览" default: "默认" -defaultValueIs: "默认值: {value}" +defaultValueIs: "默认值:{value}" noCustomEmojis: "没有自定义表情符号" noJobs: "没有任务" federating: "联合中" -blocked: "已拉黑" +blocked: "已屏蔽" suspended: "停止推流" all: "全部" -subscribing: "已订阅" +subscribing: "订阅中" publishing: "直播中" notResponding: "没有响应" -instanceFollowing: "关注实例" -instanceFollowers: "关注实例" -instanceUsers: "实例用户" +instanceFollowing: "关注服务器" +instanceFollowers: "服务器的关注者" +instanceUsers: "此服务器的用户" changePassword: "修改密码" security: "安全" -retypedNotMatch: "两次输入不一致!" +retypedNotMatch: "两次输入不匹配。" currentPassword: "现在的密码" newPassword: "新密码" -newPasswordRetype: "重新输入密码:" +newPasswordRetype: "重新输入新密码" attachFile: "插入附件" more: "更多!" featured: "热门" -usernameOrUserId: "用户名或用户ID" +usernameOrUserId: "用户名或用户 ID" noSuchUser: "用户不存在" lookup: "查询" announcements: "公告" -imageUrl: "图片URL" +imageUrl: "图片 URL" remove: "删除" removed: "已删除" -removeAreYouSure: "要删掉「{x}」吗?" -deleteAreYouSure: "要删掉「{x}」吗?" -resetAreYouSure: "恢复默认设置?" +removeAreYouSure: "确定要删除「{x}」吗?" +deleteAreYouSure: "确定要删除「{x}」吗?" +resetAreYouSure: "确定重置为默认设置?" saved: "已保存" messaging: "聊天" upload: "本地上传" keepOriginalUploading: "保留原图" -keepOriginalUploadingDescription: "上传图片时保留原始图片。关闭时,浏览器会在上传时生成一张用于web发布的图片。" +keepOriginalUploadingDescription: "上传图片时保留原始图片。如果关闭,会在上传时生成一张用于 web 发布的图片。" fromDrive: "从网盘中" fromUrl: "从 URL" -uploadFromUrl: "从网址上传" -uploadFromUrlDescription: "输入文件的URL" -uploadFromUrlRequested: "请求上传" +uploadFromUrl: "从 URL 上传" +uploadFromUrlDescription: "输入文件的 URL" +uploadFromUrlRequested: "已请求上传" uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。" explore: "发现" messageRead: "已读" noMoreHistory: "没有更多的历史记录" -startMessaging: "添加聊天" -nUsersRead: "{n}人已读" -agreeTo: "勾选则表示已阅读并同意{0}" +startMessaging: "开始聊天" +nUsersRead: "{n} 人已读" +agreeTo: "我同意 {0}" tos: "服务条款" start: "开始" home: "首页" -remoteUserCaution: "由于此用户来自其它实例,显示的信息可能不完整。" +remoteUserCaution: "由于此用户来自其它服务器,显示的信息可能不完整。" activity: "活动" images: "图片" birthday: "生日" -yearsOld: "{age}岁" +yearsOld: "{age} 岁" registeredDate: "注册于" location: "位置" theme: "主题" @@ -273,9 +273,9 @@ light: "浅色" dark: "深色" lightThemes: "浅色主题" darkThemes: "深色主题" -syncDeviceDarkMode: "将深色模式与设备设置同步" +syncDeviceDarkMode: "将深色模式设置与设备同步" drive: "网盘" -fileName: "文件名称" +fileName: "文件名" selectFile: "选择文件" selectFiles: "选择文件" selectFolder: "选择文件夹" @@ -302,72 +302,72 @@ nsfw: "敏感内容" whenServerDisconnected: "与服务器连接中断时" disconnectedFromServer: "已和服务器断开连接" reload: "重新加载" -doNothing: "关闭弹窗" +doNothing: "忽略" reloadConfirm: "确定要重新加载吗?" watch: "关注" unwatch: "取消关注" -accept: "允许" +accept: "接受" reject: "拒绝" normal: "正常" instanceName: "服务器名称" instanceDescription: "服务器简介" maintainerName: "管理员名称" maintainerEmail: "管理员电子邮箱" -tosUrl: "服务条款URL" +tosUrl: "服务条款 URL" thisYear: "今年" thisMonth: "本月" today: "今天" -dayX: "{day}日" -monthX: "{month}月" -yearX: "{year}年" +dayX: "{day} 日" +monthX: "{month} 月" +yearX: "{year} 年" pages: "页面" integration: "整合" connectService: "连接" disconnectService: "断开连接" enableLocalTimeline: "启用本地时间线功能" enableGlobalTimeline: "启用全局时间线" -disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。" +disablingTimelinesInfo: "管理员和监察员将始终拥有对所有时间线的访问权,即使它们没有被启用。" registration: "注册" enableRegistration: "允许新用户注册" invite: "邀请" -driveCapacityPerLocalAccount: "每个用户的网盘空间" +driveCapacityPerLocalAccount: "每个本地用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" -inMb: "以兆字节(MegaByte)为单位" -iconUrl: "图标URL" -bannerUrl: "横幅URL" -backgroundImageUrl: "背景图URL" +inMb: "以兆字节 (MegaByte) 为单位" +iconUrl: "图标 URL" +bannerUrl: "横幅图 URL" +backgroundImageUrl: "背景图 URL" basicInfo: "基本信息" pinnedUsers: "置顶用户" -pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。" +pinnedUsersDescription: "列出要在「发现」页面中置顶的用户,一行一个。" pinnedPages: "固定页面" -pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。" -pinnedClipId: "置顶的便签ID" +pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,一行一个。" +pinnedClipId: "置顶的便签 ID" pinnedNotes: "已置顶的帖子" hcaptcha: "hCaptcha" enableHcaptcha: "启用 hCaptcha" -hcaptchaSiteKey: "网站密钥" -hcaptchaSecretKey: "密钥" +hcaptchaSiteKey: "网站密钥 (Site key)" +hcaptchaSecretKey: "密钥 (Secret key)" recaptcha: "reCAPTCHA" -enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" -recaptchaSiteKey: "网站密钥" -recaptchaSecretKey: "reCAPTCHA 密钥" -avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其他验证方式吗?您可以按“取消”按钮,仍然保持启用多种验证方式。" +enableRecaptcha: "启用 reCAPTCHA\n(请注意,reCAPTCHA 在中国大陆无法访问,如果启用,可能导致无法正常使用登录或注册等功能)" +recaptchaSiteKey: "网站密钥 (Site key)" +recaptchaSecretKey: "密钥 (Secret key)" +avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其它现已激活的验证方式吗?如果您希望它们继续被启用,请点击 「取消」。" antennas: "天线" -manageAntennas: "天线管理" +manageAntennas: "管理天线" name: "名称" antennaSource: "接收来源" antennaKeywords: "包含关键字" antennaExcludeKeywords: "排除关键字" -antennaKeywordsDescription: "使用空格分隔会产生AND规范,并且使用换行符分隔会产生OR规范" -notifyAntenna: "开启通知" -withFileAntenna: "仅带有附件的帖子" -enableServiceworker: "启用ServiceWorker" -antennaUsersDescription: "指定用户名,用换行符分隔" +antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" +notifyAntenna: "新帖子通知" +withFileAntenna: "仅显示带有附件的帖子" +enableServiceworker: "为浏览器启用推送通知 (ServiceWorker)" +antennaUsersDescription: "指定用户名,一行一个" caseSensitive: "区分大小写" withReplies: "包括回复" connectedTo: "您的账号已连到接以下第三方账号" notesAndReplies: "帖子与回复" -withFiles: "附件" +withFiles: "包含文件" silence: "禁言" silenceConfirm: "确认要禁言吗?" unsilence: "解除禁言" @@ -376,37 +376,37 @@ popularUsers: "热门用户" recentlyUpdatedUsers: "最近投稿的用户" recentlyRegisteredUsers: "最近登录的用户" recentlyDiscoveredUsers: "最近发现的用户" -exploreUsersCount: "有{count}个用户" +exploreUsersCount: "有 {count} 个用户" exploreFediverse: "探索联邦宇宙" popularTags: "热门标签" userList: "列表" about: "关于" -aboutMisskey: "关于 Misskey" +aboutMisskey: "关于 Calckey" administrator: "管理员" -token: "Token (令牌)" +token: "令牌" twoStepAuthentication: "两步验证" moderator: "监察员" moderation: "管理" -nUsersMentioned: "{n} 被提到" +nUsersMentioned: "被 {n} 人提到" securityKey: "安全密钥" securityKeyName: "密钥名称" -registerSecurityKey: "注册硬件安全密钥" -lastUsed: "最后使用:" -unregister: "删除账户" +registerSecurityKey: "注册安全密钥" +lastUsed: "最近使用" +unregister: "删除账号" passwordLessLogin: "无密码登录" resetPassword: "重置密码" -newPasswordIs: "新的密码是「{password}」" -reduceUiAnimation: "减少UI动画" +newPasswordIs: "新的密码是 {password}" +reduceUiAnimation: "减少 UI 动画" share: "分享" notFound: "未找到" -notFoundDescription: "没有与指定URL对应的页面。" +notFoundDescription: "没有与指定 URL 对应的页面。" uploadFolder: "默认上传文件夹" cacheClear: "清空缓存" markAsReadAllNotifications: "将所有通知标为已读" markAsReadAllUnreadNotes: "将所有帖子标记为已读" markAsReadAllTalkMessages: "将所有聊天标记为已读" help: "帮助" -inputMessageHere: "在此键入信息" +inputMessageHere: "在此输入信息" close: "关闭" group: "群组" groups: "群组" @@ -417,37 +417,37 @@ invites: "邀请" groupName: "群组名" members: "成员" transfer: "转让" -messagingWithUser: "与用户聊天" -messagingWithGroup: "与群组聊天" +messagingWithUser: "私聊" +messagingWithGroup: "群聊" title: "标题" text: "文本" enable: "启用" next: "下一个" retype: "重新输入" -noteOf: "{user}的帖子" +noteOf: "{user} 的帖子" inviteToGroup: "群组邀请" quoteAttached: "已引用" -quoteQuestion: "是否引用此链接内容?" -noMessagesYet: "现在没有新的聊天" +quoteQuestion: "是否引用?" +noMessagesYet: "暂无消息" newMessageExists: "新信息" onlyOneFileCanBeAttached: "只能添加一个附件" signinRequired: "请先登录" invitations: "邀请" invitationCode: "邀请码" -checking: "正在确认" +checking: "正在确认..." available: "可用" unavailable: "不可用" usernameInvalidFormat: "可使用大小写英文字母、数字和下划线。" -tooShort: "过短" -tooLong: "过长" +tooShort: "太短" +tooLong: "太长" weakPassword: "密码强度:弱" normalPassword: "密码强度:中等" strongPassword: "密码强度:强" passwordMatched: "密码一致" passwordNotMatched: "密码不一致" -signinWith: "以{x}登录" +signinWith: "以 {x} 登录" signinFailed: "无法登录,请检查您的用户名和密码是否正确。" -tapSecurityKey: "轻触硬件安全密钥" +tapSecurityKey: "轻触您的安全密钥" or: "或者" language: "语言" uiLanguage: "显示语言" @@ -459,26 +459,26 @@ youHaveNoGroups: "没有群组" joinOrCreateGroup: "请加入一个现有的群组,或者创建新群组。" noHistory: "没有历史记录" signinHistory: "登录历史" -disableAnimatedMfm: "禁用MFM动画" -doing: "正在进行" +disableAnimatedMfm: "禁用 MFM 动画" +doing: "正在处理…" category: "类别" tags: "标签" docSource: "文件来源" -createAccount: "注册账户" -existingAccount: "现有的账户" +createAccount: "注册账号" +existingAccount: "现有的账号" regenerate: "重新生成" fontSize: "字体大小" -noFollowRequests: "没有关注申请" +noFollowRequests: "没有待批准的关注申请" openImageInNewTab: "在新标签页中打开图片" dashboard: "管理面板" local: "本地" remote: "远程" total: "总计" weekOverWeekChanges: "与前一周相比" -dayOverDayChanges: "与前一日相比" +dayOverDayChanges: "与昨日相比" appearance: "外观" clientSettings: "客户端设置" -accountSettings: "账户设置" +accountSettings: "账号设置" promotion: "推广" promote: "推广" numberOfDays: "天数" @@ -487,24 +487,26 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐" objectStorage: "对象存储" useObjectStorage: "使用对象存储" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "用于引用的URL。如果您正在使用CDN或反向代理,请指定其URL,例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" +objectStorageBaseUrlDesc: "用于引用的 URL。如果您正在使用 CDN 或反向代理,请指定其 URL。\n例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”,其它同理。" objectStorageBucket: "存储桶" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStoragePrefix: "前缀" objectStoragePrefixDesc: "文件将存储在此前缀的目录下。" -objectStorageEndpoint: "端点" -objectStorageEndpointDesc: "如果你使用AWS S3请留空。否则请根据你使用的服务商的说明来进行设置,指定端点形式为“”或“:”。" +objectStorageEndpoint: "Endpoint" +objectStorageEndpointDesc: "如果您使用 AWS S3 请留空。否则请根据您使用的服务商的说明来进行设置,指定 Endpoint 形式为 + \"\" 或 \":\"。" objectStorageRegion: "可用区" -objectStorageRegionDesc: "指定一个可用区,例如“xx-east-1”。 如果您的对象存储服务没有可用区概念,请将其留空或填写“us-east-1”。" -objectStorageUseSSL: "使用SSL" -objectStorageUseSSLDesc: "如果不使用https进行API连接,请关闭。" +objectStorageRegionDesc: "指定一个可用区,例如 \"xx-east-1\"。 如果您的对象存储服务没有可用区概念,请将其留空或填写 \"\ + us-east-1\"。\n对于 Cloudflare R2,可以填为 \"auto\"。" +objectStorageUseSSL: "使用 SSL" +objectStorageUseSSLDesc: "如果不使用 HTTPS 进行 API 连接,请关闭" objectStorageUseProxy: "使用代理" -objectStorageUseProxyDesc: "如果您不使用代理进行API连接,请将其关闭。" -objectStorageSetPublicRead: "上传时设置为public-read" +objectStorageUseProxyDesc: "如果您不使用代理进行 API 连接,请将其关闭" +objectStorageSetPublicRead: "上传时设置为 public-read" serverLogs: "服务器日志" deleteAll: "全部删除" showFixedPostForm: "在时间线顶部显示发帖框" -newNoteRecived: "有新的帖子" +newNoteRecived: "新帖子" sounds: "提示音" listen: "试听" none: "无" @@ -521,25 +523,25 @@ uninstall: "卸载" installedApps: "已授权的应用" nothing: "没有" installedDate: "授权日期" -lastUsedDate: "最近使用" +lastUsedDate: "最近使用时间" state: "状态" sort: "排序" ascendingOrder: "升序" descendingOrder: "降序" -scratchpad: "AiScript控制台" -scratchpadDescription: "AiScript控制台为AiScript提供了实验环境。您可以编写代码以与Misskey交互,运行它并查看结果。" +scratchpad: "AiScript 控制台" +scratchpadDescription: "AiScript 控制台为 AiScript 提供了实验环境。您可以编写代码以与 Calckey 交互,运行它并查看结果。" output: "输出" script: "脚本" -disablePagesScript: "禁用页面脚本" +disablePagesScript: "在页面中禁用 AiScript" updateRemoteUser: "更新远程用户信息" deleteAllFiles: "删除所有文件" -deleteAllFilesConfirm: "要删除所有文件吗?" +deleteAllFilesConfirm: "确定要删除所有文件吗?" removeAllFollowing: "取消所有关注" -removeAllFollowingDescription: "取消{host}的所有关注者。当实例不存在时执行。" +removeAllFollowingDescription: "取消 {host} 的所有关注者。如果服务器已不存在,请执行它。" userSuspended: "该用户已被冻结。" userSilenced: "该用户已被禁言。" -yourAccountSuspendedTitle: "账户已被冻结" -yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的账户。" +yourAccountSuspendedTitle: "账号已被冻结" +yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其它原因,该账号已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的账号。" menu: "菜单" divider: "分割线" addItem: "添加项目" @@ -547,10 +549,10 @@ relays: "中继" addRelay: "添加中继" inboxUrl: "Inbox URL" addedRelays: "已添加的中继" -serviceworkerInfo: "您需要启用推送通知" +serviceworkerInfo: "需要启用推送通知。" deletedNote: "已删除的帖子" invisibleNote: "隐藏的帖子" -enableInfiniteScroll: "启用自动滚动页面模式" +enableInfiniteScroll: "滚动页面以载入更多内容" visibility: "可见性" poll: "调查问卷" useCw: "隐藏内容" @@ -567,9 +569,9 @@ manage: "管理" plugins: "插件" preferencesBackups: "备份设置" deck: "Deck" -undeck: "取消Deck" +undeck: "取消 Deck" useBlurEffectForModal: "对话框使用模糊效果" -useFullReactionPicker: "使用全功能的回应工具栏" +useFullReactionPicker: "使用全尺寸的回应选择栏" width: "宽度" height: "高度" large: "大" @@ -579,8 +581,8 @@ generateAccessToken: "生成访问令牌" permission: "权限" enableAll: "启用全部" disableAll: "禁用全部" -tokenRequested: "允许访问账户" -pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" +tokenRequested: "允许访问账号" +pluginTokenRequestedDescription: "此插件将能够拥有这里设置的权限。" notificationType: "通知类型" edit: "编辑" emailServer: "邮件服务器" @@ -588,24 +590,24 @@ enableEmail: "启用发送邮件功能" emailConfigInfo: "用于确认电子邮件和密码重置" email: "邮箱" emailAddress: "电子邮件地址" -smtpConfig: "SMTP服务器设置" +smtpConfig: "SMTP 服务器设置" smtpHost: "主机名" smtpPort: "端口" smtpUser: "用户名" smtpPass: "密码" -emptyToDisableSmtpAuth: "用户名和密码留空可以禁用SMTP验证" +emptyToDisableSmtpAuth: "留空用户名和密码以禁用 SMTP 验证" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" -smtpSecureInfo: "使用STARTTLS时关闭。" +smtpSecureInfo: "使用 STARTTLS 时关闭" testEmail: "邮件发送测试" -wordMute: "文字屏蔽" +wordMute: "文字过滤" regexpError: "正则表达式错误" -regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:" +regexpErrorDescription: "{tab} 文字过滤的第 {line} 行的正则表达式有错误:" instanceMute: "服务器静音" -userSaysSomething: "{name}说了什么" +userSaysSomething: "{name} 说了什么" makeActive: "启用" display: "显示" copy: "复制" -metrics: "服务器监控" +metrics: "指标" overview: "服务器概况" logs: "日志" delayed: "滞后" @@ -615,32 +617,32 @@ create: "创建" notificationSetting: "通知设置" notificationSettingDesc: "选择要显示的通知类型。" useGlobalSetting: "使用全局设置" -useGlobalSettingDesc: "启用时,将使用账户通知设置。关闭时,则可以单独设置。" -other: "其他" +useGlobalSettingDesc: "启用时,将使用账号通知设置。关闭时,则可以单独设置。" +other: "其它" regenerateLoginToken: "重新生成登录令牌" regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多个项目。" -fileIdOrUrl: "文件ID或者URL" +fileIdOrUrl: "文件 ID 或者 URL" behavior: "行为" sample: "示例" abuseReports: "举报" reportAbuse: "举报" -reportAbuseOf: "举报{name}" -fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写URL地址。" -abuseReported: "内容已发送。感谢您提交信息。" +reportAbuseOf: "举报 {name}" +fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写 URL 地址。" +abuseReported: "您的举报已发送。非常感谢您。" reporter: "举报者" reporteeOrigin: "举报来源" reporterOrigin: "举报者来源" -forwardReport: "将该举报信息转发给远程实例" -forwardReportIsAnonymous: "勾选则在远程实例上显示的举报者是匿名的系统账号,而不是您的账号。" +forwardReport: "将该举报信息转发给远程服务器" +forwardReportIsAnonymous: "勾选则在远程服务器上显示的举报者是匿名的系统账号,而不是您的账号。" send: "发送" -abuseMarkAsResolved: "处理完毕" +abuseMarkAsResolved: "标记举报为已解决" openInNewTab: "在新标签页中打开" openInSideView: "在侧边栏中打开" defaultNavigationBehaviour: "默认导航" -editTheseSettingsMayBreakAccount: "编辑这些设置可以会损坏您的账号" -instanceTicker: "帖子的实例信息" -waitingFor: "等待{x}" +editTheseSettingsMayBreakAccount: "编辑这些设置可能会损坏您的账号。" +instanceTicker: "帖子所在的服务器信息" +waitingFor: "等待 {x}" random: "随机" system: "系统" switchUi: "界面" @@ -650,17 +652,17 @@ createNew: "新建" optional: "可选" createNewClip: "新建便签" unclip: "移除便签" -confirmToUnclipAlreadyClippedNote: "本帖已包含在便签\"{name}\"里。您想要将本帖从该便签中移除吗?" +confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?" public: "公开" -i18nInfo: "Firefish已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过{link}帮助翻译。" -manageAccessTokens: "管理 Access Tokens" -accountInfo: "账户信息" +i18nInfo: "Calckey 已经被志愿者们翻译成了各种语言。如果您也有兴趣,可以通过 {link} 帮助翻译。" +manageAccessTokens: "管理访问令牌" +accountInfo: "账号信息" notesCount: "帖子数量" repliesCount: "回复数量" -renotesCount: "转帖数量" +renotesCount: "转发数量" repliedCount: "回复数" -renotedCount: "转发数" -followingCount: "正在关注数量" +renotedCount: "转发数量" +followingCount: "关注中数量" followersCount: "关注者数量" sentReactionsCount: "发送回应数" receivedReactionsCount: "收到回应数" @@ -674,12 +676,12 @@ noCrawle: "要求搜索引擎不索引该用户" noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。" lockedAccountInfo: "即使通过了关注请求,只要您不将帖子可见范围设置成“关注者”,任何人都可以看到您的帖子。" alwaysMarkSensitive: "默认将媒体文件标记为敏感内容" -loadRawImages: "添加附件图像的缩略图时使用原始图像质量" +loadRawImages: "加载原始图像而不是显示缩略图" disableShowingAnimatedImages: "不播放动画" -verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。" +verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成验证。" notSet: "未设置" emailVerified: "电子邮件地址已验证" -noteFavoritesCount: "收藏的帖子数" +noteFavoritesCount: "加入书签的帖子数" pageLikesCount: "页面点赞次数" pageLikedCount: "页面被点赞次数" contact: "联系人" @@ -687,28 +689,29 @@ useSystemFont: "使用系统默认字体" clips: "便签" experimentalFeatures: "实验性功能" developer: "开发者" -makeExplorable: "使账号可见。" +makeExplorable: "使账号在“发现”中可见" makeExplorableDescription: "关闭时,账号不会显示在\"发现\"中。" -showGapBetweenNotesInTimeline: "时间线上的帖子分开显示。" +showGapBetweenNotesInTimeline: "时间线上的帖子分开显示" duplicate: "复制" left: "左" center: "中央" wide: "宽" narrow: "窄" reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?" -needReloadToApply: "重启后应用才会生效。" +needReloadToApply: "需要重新加载才能生效。" showTitlebar: "显示标题栏" clearCache: "清除缓存" -onlineUsersCount: "{n}人在线" -nUsers: "{n}用户" +onlineUsersCount: "{n} 人在线" +nUsers: "{n} 用户" nNotes: "{n} 帖子" sendErrorReports: "发送错误报告" -sendErrorReportsDescription: "启用后,如果出现问题,可以与Misskey共享详细的错误信息,从而帮助提高软件的质量。" +sendErrorReportsDescription: "启用后,如果出现问题,可以与 Calckey 共享详细的错误信息,从而帮助提高软件的质量。\n这将包括您的操作系统版本、您使用的浏览器、您在 + Calckey 中的活动等信息。" myTheme: "我的主题" -backgroundColor: "背景" +backgroundColor: "背景色" accentColor: "强调色" -textColor: "文本" -saveAs: "另存为" +textColor: "文本颜色" +saveAs: "另存为..." advanced: "高级" value: "值" createdAt: "创建日期" @@ -717,7 +720,7 @@ saveConfirm: "确定保存?" deleteConfirm: "确定删除?" invalidValue: "无效值。" registry: "注册表" -closeAccount: "永久注销账户" +closeAccount: "永久注销账号" currentVersion: "当前版本" latestVersion: "最新版本" youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。" @@ -727,16 +730,16 @@ capacity: "容量" inUse: "已使用" editCode: "编辑代码" apply: "应用" -receiveAnnouncementFromInstance: "从实例接收通知" +receiveAnnouncementFromInstance: "从服务器接收通知" emailNotification: "邮件通知" publish: "发布" inChannelSearch: "频道内搜索" useReactionPickerForContextMenu: "单击右键打开回应工具栏" -typingUsers: "{users}正在输入" +typingUsers: "{users} 正在输入" jumpToSpecifiedDate: "跳转到特定日期" showingPastTimeline: "显示过去的时间线" clear: "清除" -markAllAsRead: "全部标记为已读" +markAllAsRead: "将全部标记为已读" goBack: "返回" unlikeConfirm: "取消赞?" fullView: "全屏" @@ -751,22 +754,22 @@ onlineStatus: "在线状态" hideOnlineStatus: "隐藏在线状态" hideOnlineStatusDescription: "隐藏在线状态后,可能会降低搜索等功能的便利性。" online: "在线" -active: "活动" +active: "活跃" offline: "离线" notRecommended: "不推荐" -botProtection: "Bot防御" -instanceBlocking: "联邦管理" -selectAccount: "选择账户" -switchAccount: "切换账户" +botProtection: "Bot 防护" +instanceBlocking: "联合管理" +selectAccount: "选择账号" +switchAccount: "切换账号" enabled: "已启用" -disabled: "已禁用 " +disabled: "已禁用" quickAction: "快捷操作" user: "用户" administration: "管理" -accounts: "账户" +accounts: "账号" switch: "切换" -noMaintainerInformationWarning: "管理人员信息未设置。" -noBotProtectionWarning: "Bot保护未设置。" +noMaintainerInformationWarning: "管理员信息未设置。" +noBotProtectionWarning: "Bot 防御未设置。" configure: "设置" postToGallery: "发送到图库" gallery: "图库" @@ -784,7 +787,7 @@ emailNotConfiguredWarning: "电子邮件地址未设置。" ratio: "比率" previewNoteText: "预览文本" customCss: "自定义 CSS" -customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用!" +customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用。" global: "全局" squareAvatars: "显示方形头像图标" sent: "发送" @@ -792,39 +795,39 @@ received: "收取" searchResult: "搜索结果" hashtags: "话题标签" troubleshooting: "故障排除" -useBlurEffect: "在UI上使用模糊效果" +useBlurEffect: "在 UI 上使用模糊效果" learnMore: "更多信息" -misskeyUpdated: "Misskey更新完成!" +misskeyUpdated: "Calckey 更新完成!" whatIsNew: "显示更新信息" translate: "翻译" translatedFrom: "从 {x} 翻译" -accountDeletionInProgress: "正在删除账户" -usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" +accountDeletionInProgress: "正在删除账号" +usernameInfo: "在服务器上唯一标识您的账号的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" aiChanMode: "小蓝模式" -keepCw: "保留CW" -pubSub: "Pub/Sub账户" +keepCw: "保留内容警告" +pubSub: "推送 (Pub)/订阅 (Sub) 账号" lastCommunication: "最近通信" resolved: "已解决" unresolved: "未解决" breakFollow: "移除关注者" itsOn: "已开启" itsOff: "已关闭" -emailRequiredForSignup: "注册账户需要电子邮件地址" +emailRequiredForSignup: "注册账号需要电子邮件地址" unread: "未读" filter: "筛选" controlPanel: "控制面板" -manageAccounts: "管理账户" +manageAccounts: "管理账号" makeReactionsPublic: "将回应设置为公开" makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。" classic: "居中" -muteThread: "屏蔽帖子列表" -unmuteThread: "取消屏蔽帖子列表" -ffVisibility: "连接的可见范围" -ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围" +muteThread: "静音帖子串" +unmuteThread: "取消静音帖子串" +ffVisibility: "关注/关注者 可见性" +ffVisibilityDescription: "您可以设置谁可以看到您的关注/关注者信息。" continueThread: "查看更多帖子" -deleteAccountConfirm: "将要删除账户。是否确认?" -incorrectPassword: "密码错误" -voteConfirm: "确定投给“{choice}” ?" +deleteAccountConfirm: "将要删除账号。是否继续?" +incorrectPassword: "密码错误。" +voteConfirm: "确定投给 “{choice}” ?" hide: "隐藏" leaveGroup: "离开群组" leaveGroupConfirm: "确定离开「{name}」?" @@ -835,68 +838,68 @@ overridedDeviceKind: "设备类型" smartphone: "智能手机" tablet: "平板" auto: "自动" -themeColor: "主题颜色" +themeColor: "服务器滚动条颜色" size: "大小" numberOfColumn: "列数" searchByGoogle: "Google" -instanceDefaultLightTheme: "实例默认浅色主题" -instanceDefaultDarkTheme: "实例默认深色主题" -instanceDefaultThemeDescription: "以对象格式键入主题代码" -mutePeriod: "屏蔽期限" +instanceDefaultLightTheme: "服务器默认浅色主题" +instanceDefaultDarkTheme: "服务器默认深色主题" +instanceDefaultThemeDescription: "以对象格式键入主题代码。" +mutePeriod: "静音时间" indefinitely: "永久" tenMinutes: "10分钟" -oneHour: "1小时" -oneDay: "1天" -oneWeek: "1周" +oneHour: "1 小时" +oneDay: "1 天" +oneWeek: "1 周" reflectMayTakeTime: "可能需要一些时间才能体现出效果。" -failedToFetchAccountInformation: "获取账户信息失败" -rateLimitExceeded: "已超過速率限制" +failedToFetchAccountInformation: "获取账号信息失败" +rateLimitExceeded: "已超过速率限制" cropImage: "剪裁图像" -cropImageAsk: "是否要裁剪图像?" +cropImageAsk: "您想要裁剪图像吗?" file: "文件" -recentNHours: "最近{n}小时" -recentNDays: "最近{n}天" +recentNHours: "最近 {n} 小时" +recentNDays: "最近 {n} 天" noEmailServerWarning: "电子邮件服务器未设置。" -thereIsUnresolvedAbuseReportWarning: "有未解决的报告" +thereIsUnresolvedAbuseReportWarning: "有未处理的举报。" recommended: "推荐" check: "检查" -driveCapOverrideLabel: "變更此用戶的雲端硬碟容量上限" -driveCapOverrideCaption: "设定为 0 以下则会解除此限制。" -requireAdminForView: "需要使用管理员账户登录才能查看。" -isSystemAccount: "该账号由系统自动创建和管理。" -typeToConfirm: "输入 {x} 以确认操作。" -deleteAccount: "删除账户" +driveCapOverrideLabel: "修改此用户的网盘容量" +driveCapOverrideCaption: "输入 0 或以下的值将容量重置为默认值。" +requireAdminForView: "需要使用管理员账号登录才能查看。" +isSystemAccount: "该账号由系统自动创建和管理。请不要修改、编辑、删除或以其它方式篡改这个账号,否则可能会破坏您的服务器。" +typeToConfirm: "输入 {x} 以确认操作" +deleteAccount: "删除账号" document: "文档" numberOfPageCache: "缓存页数" numberOfPageCacheDescription: "设置较高的值会更方便用户,但设备的负载和内存使用量会增加。" logoutConfirm: "是否确认登出?" -lastActiveDate: "最后活跃时间" +lastActiveDate: "最近使用时间" statusbar: "状态栏" pleaseSelect: "请选择" reverse: "翻转" colored: "彩色" -refreshInterval: "刷新间隔" +refreshInterval: "更新间隔 " label: "标签" type: "类型" speed: "速度" slow: "慢" fast: "快" -sensitiveMediaDetection: "检测到敏感媒体" +sensitiveMediaDetection: "检测到敏感媒体内容" localOnly: "仅限本地" remoteOnly: "仅远程" failedToUpload: "上传失败" -cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法上传。" -cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。" +cannotUploadBecauseInappropriate: "无法上传此文件,因为它可能包含不适宜的内容。" +cannotUploadBecauseNoFreeSpace: "由于已无可用网盘空间,无法上传。" beta: "测试" -enableAutoSensitive: "自动 NSFW 识别" -enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据实例自动设置。" -activeEmailValidationDescription: "积极地验证用户的电子邮件地址,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。" +enableAutoSensitive: "自动 NSFW 标记" +enableAutoSensitiveDescription: "允许通过机器学习对媒体文件自动设置 NSFW 标志。即使关闭此功能,也可能会根据服务器自动设置。" +activeEmailValidationDescription: "启用更严格的电子邮件地址验证,包括判断它是一次性的电子邮件地址还是可以实际通信的地址。关闭时,则只检查字符串是否正确。" navbar: "导航栏" shuffle: "随机" -account: "账户" -move: "移动" +account: "账号" +move: "迁移" customKaTeXMacro: "自定义 KaTeX 宏" -customKaTeXMacroDescription: "使用宏来轻松的输入数学表达式吧!宏的用法与 LaTeX 中的命令定义相同。你可以使用 \\newcommand{\\ +customKaTeXMacroDescription: "使用宏来轻松的输入数学表达式吧!宏的用法与 LaTeX 中的命令定义相同。您可以使用 \\newcommand{\\ name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 来输入数学表达式。举个例子,\\ newcommand{\\add}[2]{#1 + #2} 会将 \\add{3}{foo} 展开为 3 + foo。此外,宏名称外的花括号 {} 可以被替换为圆括号 () 和方括号 [],这会影响用于参数的括号。每行只能够定义一个宏,无法在中间换行,且无效的行将被忽略。只支持简单字符串替换功能,不支持高级语法,如条件分支等。" @@ -910,33 +913,33 @@ _sensitiveMediaDetection: analyzeVideos: "启用对视频的检测" analyzeVideosDescription: "除了静止图像之外,还对视频进行分析。服务器负载会略微增加。" _emailUnavailable: - used: "已经被使用过" + used: "这个电子邮件地址已经被使用过" format: "无效的格式" - disposable: "不是永久可用的地址" + disposable: "不得使用一次性电子邮件地址" mx: "邮件服务器不正确" smtp: "邮件服务器没有响应" _ffVisibility: - public: "发布" - followers: "只有关注你的用户能看到" - private: "私密" + public: "公开" + followers: "仅对关注者可见" + private: "私信" _signup: almostThere: "即将完成" - emailAddressInfo: "请输入您所使用的电子邮件地址" - emailSent: "已将确认邮件发送至您输入的电子邮件地址 ({email})。请访问电子邮件中的链接以完成帐户创建。" + emailAddressInfo: "请输入您所使用的电子邮件地址,它不会公开显示。" + emailSent: "已将确认邮件发送至您输入的电子邮件地址 ({email})。请访问电子邮件中的链接以完成账号创建。" _accountDelete: - accountDelete: "删除帐户" - mayTakeTime: "删除账号是一个性能损耗较大的处理,如果账号持有的内容数量和上传的文件数量较多的话,完成需要花费一段时间。" - sendEmail: "账户删除完成后,将向注册的电子邮件地址发送通知。" - requestAccountDelete: "请求删除账户" - started: "账户删除过程已开始。" + accountDelete: "删除账号" + mayTakeTime: "删除账号是一个性能损耗较大的过程,如果账号持有的内容数量和上传的文件数量较多的话,完成需要花费一段时间。" + sendEmail: "账号删除完成后,将向注册的电子邮件地址发送通知。" + requestAccountDelete: "请求删除账号" + started: "账号删除过程已开始。" inProgress: "正在删除" _ad: back: "返回" reduceFrequencyOfThisAd: "减少此广告的频率" _forgotPassword: - enterEmail: "请输入您验证账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。" - ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系管理员。" - contactAdmin: "该实例不支持发送电子邮件。如果您想重设密码,请联系管理员。" + enterEmail: "请输入您注册账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。" + ifNoEmail: "如果您在注册时没有输入电子邮件地址,请联系服务器管理员。" + contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。" _gallery: my: "我的图库" liked: "喜欢的图片" @@ -944,98 +947,98 @@ _gallery: unlike: "取消喜欢" _email: _follow: - title: "你有新的关注者" + title: "您有新的关注者" _receiveFollowRequest: - title: "收到了关注请求" + title: "您收到了关注请求" _plugin: install: "安装插件" installWarn: "请不要安装不可信的插件。" - manage: "管理插件..." + manage: "管理插件" _preferencesBackups: list: "已创建的备份" saveNew: "另存为" - loadFile: "导入文件" + loadFile: "从文件导入" apply: "应用于本设备" save: "覆盖存档" inputName: "请输入备份的名称" - cannotSave: "无法保存" - nameAlreadyExists: "备份名称\"{name}\"已经存在,请指定其他名称。" - applyConfirm: "您是否要将备份\"{name}\"应用到当前设备上?当前设备现有配置将被丢弃。" + cannotSave: "保存失败" + nameAlreadyExists: "备份名称 \"{name}\" 已经存在,请指定其它名称。" + applyConfirm: "您是否要将备份 \"{name}\" 应用到当前设备上?当前设备现有配置将被丢弃。" saveConfirm: "您确定要覆盖保存 {name} 吗?" deleteConfirm: "您确定要删除 {name} 吗?" - renameConfirm: "您确定要把“{old}”改为“{new}”吗?" - noBackups: "当前没有备份,“另存为”允许您在服务器上保存当前客户端的配置。" + renameConfirm: "您确定要把 \"{old}\" 改为 \"{new}\" 吗?" + noBackups: "没有备份。您可以使用“创建新的备份”来备份您在该服务器上的客户设置。" createdAt: "创建日期:{date} {time}" updatedAt: "更新日期:{date} {time}" cannotLoad: "无法加载" - invalidFile: "无效的的文件格式。" + invalidFile: "无效的的文件格式" _registry: scope: "范围" - key: "主要" - keys: "主要" + key: "键" + keys: "键" domain: "域" createKey: "创建键" _aboutMisskey: - about: "Misskey是由syuilo于2014年开发的开源软件。" + about: "Calckey 是由 ThatOneCalculator 创建的 Misskey 的一个分支,自 2022 年开始开发。" contributors: "主要贡献者" allContributors: "全体贡献者" source: "源代码" - translation: "翻译Misskey" - donate: "赞助Misskey" - morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰" - patrons: "支持者" + translation: "翻译 Calckey" + donate: "赞助 Calckey" + morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰" + patrons: "Calckey 赞助者" patronsList: 按时间顺序而不是捐赠金额排列。通过上面的链接捐款,让您的名字出现在这里! _nsfw: respect: "隐藏敏感内容" ignore: "不隐藏敏感内容" force: "总是隐藏内容" _mfm: - cheatSheet: "MFM代码速查表" - intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。" - dummy: "通过Misskey扩展联邦宇宙的世界" + cheatSheet: "MFM 代码速查表" + intro: "MFM 是一种在 Misskey、Calckey、Akkoma 中使用的标记语言,可以在很多地方使用。您可以在此处查看所有可用的 MFM 语法的列表。" + dummy: "通过 Calckey 扩展联邦宇宙的世界" mention: "提及" - mentionDescription: "可以使用 @+用户名 来指示特定用户" + mentionDescription: "可以使用 @+用户名 来指示特定用户。" hashtag: "话题标签" hashtagDescription: "可以使用井号+文字来表示话题标签。" url: "URL" - urlDescription: "可以表示URL地址。" + urlDescription: "可以表示 URL 地址。" link: "链接" - linkDescription: "可以将部分文字和URL关联起来。" + linkDescription: "可以将部分文字和 URL 关联起来。" bold: "粗体" boldDescription: "可以将文字显示为粗体来表示强调。" small: "缩小" smallDescription: "可以使内容文字变小、变淡。" center: "居中" centerDescription: "可以将内容居中显示。" - inlineCode: "代码(内嵌)" + inlineCode: "代码(内嵌)" inlineCodeDescription: "将文字中的程序代码语法高亮显示。" - blockCode: "代码(块)" + blockCode: "代码(块)" blockCodeDescription: "语法高亮显示整块程序代码。" - inlineMath: "数学公式(内嵌)" - inlineMathDescription: "显示内嵌的KaTeX公式。" - blockMath: "数学公式(块)" - blockMathDescription: "显示整块的KaTeX数学公式。" + inlineMath: "数学公式(内嵌)" + inlineMathDescription: "显示内嵌的 KaTeX 公式" + blockMath: "数学公式(块)" + blockMathDescription: "显示整块的 KaTeX 数学公式" quote: "引用" - quoteDescription: "可以用来表示引用的内容。" + quoteDescription: "将内容显示为引用。" emoji: "自定义表情符号" emojiDescription: "可以将自定义表情符号使用冒号括起来,就可以显示自定义表情符号了。" search: "搜索" searchDescription: "显示含有搜索内容示例的搜索框。" flip: "翻转" flipDescription: "将内容上下或左右翻转。" - jelly: "动画(果冻)" + jelly: "动画(果冻)" jellyDescription: "显示果冻一样的动画效果。" - tada: "动画(锵锵)" + tada: "动画(锵锵)" tadaDescription: "显示\"锵锵!\"的动画效果。" - jump: "动画(跳动)" + jump: "动画(跳动)" jumpDescription: "显示跳动的动画效果。" - bounce: "动画(弹性)" + bounce: "动画(弹性)" bounceDescription: "显示弹性一样的动画效果。" - shake: "动画(摇晃)" + shake: "动画(摇晃)" shakeDescription: "显示摇晃的动画效果。" - twitch: "动画(颤抖)" + twitch: "动画(颤抖)" twitchDescription: "显示强烈颤抖的动画效果。" - spin: "动画(回转)" + spin: "动画(回转)" spinDescription: "显示回转的动画效果。" x2: "大" x2Description: "以大尺寸显示内容。" @@ -1089,30 +1092,30 @@ _channel: removeBanner: "删除横幅" featured: "热点" owned: "管理中" - following: "正在关注" - usersCount: "有{n}人参与" + following: "关注中" + usersCount: "有 {n} 人参与" notesCount: "{n} 帖子" nameAndDescription: "名称与描述" nameOnly: "仅名称" _menuDisplay: sideFull: "横向" - sideIcon: "横向(图标)" + sideIcon: "横向(图标)" top: "顶部" hide: "隐藏" _wordMute: - muteWords: "禁用词" - muteWordsDescription: "使用空格分隔表示AND逻辑,使用换行符分隔表示OR逻辑。" + muteWords: "过滤词" + muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。" softDescription: "隐藏时间线中指定条件的帖子。" - hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。" - soft: "软屏蔽" - hard: "硬屏蔽" - mutedNotes: "已静音的帖子" + hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,原先未添加的帖文也会被排除在外。" + soft: "软过滤" + hard: "硬过滤" + mutedNotes: "已过滤的帖子" _instanceMute: - instanceMuteDescription: "屏蔽配置实例中的所有帖子和转帖,包括实例的用户回复。" + instanceMuteDescription: "静音列出服务器中的所有帖子和转帖,包括服务器的用户回复。" instanceMuteDescription2: "设置时用换行符来分隔" - title: "隐藏实例已设置的帖子。" - heading: "屏蔽实例" + title: "隐藏列出的服务器中的帖子。" + heading: "要静音的服务器列表" _theme: explore: "寻找主题" install: "安装主题" @@ -1142,7 +1145,7 @@ _theme: lighten: "浅色" inputConstantName: "请输入常量名称" importInfo: "您可以在此处粘贴主题代码,将其导入到编辑器中" - deleteConstantConfirm: "确定要删除常量{const}吗?" + deleteConstantConfirm: "确定要删除常量 {const} 吗?" keys: accent: "强调色" bg: "背景" @@ -1160,7 +1163,7 @@ _theme: link: "链接" hashtag: "话题标签" mention: "提及" - mentionMe: "提及" + mentionMe: "提及(自己)" renote: "转发" modalBg: "对话框背景" divider: "分割线" @@ -1174,8 +1177,8 @@ _theme: cwBg: "CW 按钮背景" cwFg: "CW 按钮文本" cwHoverBg: "CW 按钮背景(悬停)" - toastBg: "Toast通知背景" - toastFg: "Toast通知文本" + toastBg: "Toast 通知背景" + toastFg: "Toast 通知文本" buttonBg: "按钮背景" buttonHoverBg: "按钮背景(悬停)" inputBorder: "输入框边框" @@ -1184,8 +1187,8 @@ _theme: wallpaperOverlay: "壁纸叠加层" badge: "徽章" messageBg: "聊天背景" - accentDarken: "强调色(深)" - accentLighten: "强调色(浅)" + accentDarken: "强调色(深)" + accentLighten: "强调色(浅)" fgHighlighted: "高亮显示文本" _sfx: note: "新的帖子" @@ -1196,108 +1199,108 @@ _sfx: antenna: "天线接收" channel: "频道通知" _ago: - future: "未来" - justNow: "最近" - secondsAgo: "{n}秒前" - minutesAgo: "{n}分前" - hoursAgo: "{n}小时前" - daysAgo: "{n}天前" - weeksAgo: "{n}周前" - monthsAgo: "{n}月前" - yearsAgo: "{n}年前" + future: "将来" + justNow: "刚刚" + secondsAgo: "{n} 秒前" + minutesAgo: "{n} 分前" + hoursAgo: "{n} 时前" + daysAgo: "{n} 天前" + weeksAgo: "{n} 周前" + monthsAgo: "{n} 月前" + yearsAgo: "{n} 年前" _time: second: "秒" minute: "分" hour: "小时" day: "日" _tutorial: - title: "如何使用Firefish" + title: "如何使用 Calckey" step1_1: "欢迎!" - step1_2: "让我们把你安排好。你很快就会启动并运行!" + step1_2: "让我们帮您设置一下。您很快就能开始畅游联邦宇宙!" step2_1: "首先,请完成您的个人资料。" - step2_2: "通过提供一些关于你自己的信息,其他人会更容易了解他们是否想看到你的帖子或关注你。" - step3_1: "现在是时候跟随一些人了!" - step3_2: "你的主页和社交馈送是基于你所关注的人,所以试着先关注几个账户。{n点击个人资料右上角的加号圈就可以关注它。" - step4_1: "让我们出去找你。" - step4_2: "对于他们的第一条信息,有些人喜欢做{introduction}或一个简单的 \"hello world!\"" - step5_1: "时间限制,到处是时间限制!" - step5_2: "您的实例已启用各种时间线的{timelines}。" - step5_3: "主{icon}时间线是你可以看到你的订阅者的帖子的时间线。" - step5_4: "本地{icon}时间线是你可以看到实例中所有其他用户的信息的时间线。" - step5_5: "推荐的{icon}时间线 - 是时间轴,你可以看到管理员推荐的实例的信息" - step5_6: "社交{icon}时间线显示来自你的订阅者朋友的信息。" - step5_7: "全球{icon}时间线是你可以看到来自所有其他连接的实例的消息。" + step2_2: "提供一些关于您的信息,让其它人更容易知道他们是否想看您的帖子或关注您。" + step3_1: "现在是时候关注一些人了!" + step3_2: "您的主页和社交馈送是基于您所关注的人,所以试着先关注几个账号。\n点击个人资料右上角的加号圈就可以关注它。" + step4_1: "让我们出发把。" + step4_2: "对于第一条帖子,可以做一个 {introduction} 或一个简单的 \"hello world!\"" + step5_1: "时间线,无处不在的时间线!" + step5_2: "您的服务器已启用 {timelines} 种不同的时间线。" + step5_3: "主页 {icon} 时间线是您可以看到您关注账号的帖子的时间线。" + step5_4: "本地 {icon} 时间线是您可以看到此服务器上其它用户的帖子的时间线。" + step5_5: "社交 {icon} 时间线是主页和本地时间线的结合。" + step5_6: "推荐 {icon} 时间线是您可以看到管理员推荐服务器的帖子的时间线。" + step5_7: "全球 {icon} 时间线是您可以看到来自其它所有互联服务器的帖子的时间线。" step6_1: "那么,这里是什么地方?" - step6_2: "好吧,你不只是加入卡尔基。你已经加入了Fediverse的一个门户,这是一个由成千上万台服务器组成的互联网络,被称为 \"实例\"" - step6_3: "每个服务器的工作方式不同,并不是所有的服务器都运行Firefish。但这个人确实如此! 这有点复杂,但你很快就会明白的。" - step6_4: "现在去学习并享受乐趣!" + step6_2: "好吧,您不只是加入 Calckey。您已经加入了 Fediverse 的一个门户,这是一个由成千上万台服务器组成的互联网络。" + step6_3: "每个服务器的工作方式不同,并不是所有的服务器都运行 Calckey。但这个服务器是的! 这有点复杂,但您很快就会明白的。" + step6_4: "现在,去吧,去探索,去享受乐趣吧!" _2fa: - alreadyRegistered: "此设备已被注册" - registerTOTP: "注册设备" - registerSecurityKey: "注册密钥" - step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。" + alreadyRegistered: "您已经注册了两步验证设备。" + registerTOTP: "注册身份验证器应用" + registerSecurityKey: "注册安全或通行密钥" + step1: "首先,在您的设备上安装身份验证器应用,例如 {a} 或 {b}。" step2: "然后,扫描屏幕上显示的二维码。" - step2Url: "在桌面应用程序中输入以下URL:" - step3: "输入您的应用提供的动态口令以完成设置。" - step4: "从现在开始,任何登录操作都将要求您提供动态口令。" - securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。" + step2Url: "如果您使用的是桌面程序,您也可以输入这个URL:" + step3: "输入您的应用提供的令牌以完成设置。" + step4: "从现在开始,任何登录操作都将要求您提供这样一个登录令牌。" + securityKeyInfo: "除了指纹或 PIN 身份验证外,您还可以通过支持 FIDO2 的硬件安全密钥设置身份验证,以进一步保护您的账号。" renewTOTPOk: 重新配置 renewTOTPCancel: 取消 token: 2FA 令牌 - renewTOTP: 重新配置身份验证器应用程序 - registerTOTPBeforeKey: 请设置一个认证器应用来注册一个安全或通行密钥。 - renewTOTPConfirm: 这将导致您之前的应用程序中的验证码停止工作 + renewTOTP: 重新配置身份验证器应用 + registerTOTPBeforeKey: 请先设置认证器应用以注册安全或通行密钥。 + renewTOTPConfirm: 这将导致您之前应用中的验证码失效 step3Title: 输入验证码 - step2Click: 点击此二维码将允许您在安全密钥或手机验证器应用中注册 2FA。 + step2Click: 点击此二维码将允许您在安全密钥或手机身份验证器应用中注册 2FA。 securityKeyNotSupported: 您的浏览器不支持安全密钥。 securityKeyName: 输入密钥名称 - chromePasskeyNotSupported: 目前不支持 Chrome passkeys。 + chromePasskeyNotSupported: 暂不支持 Chrome 通行密钥。 tapSecurityKey: 请按照您的浏览器的指示注册安全或通行密钥 removeKey: 移除安全密钥 removeKeyConfirm: 真的要删除 {name} 密钥吗? - whyTOTPOnlyRenew: 只要注册了安全密钥,就无法删除身份验证器应用程序。 + whyTOTPOnlyRenew: 只要注册了安全密钥,就无法删除身份验证器应用。 _permissions: - "read:account": "查看账户信息" - "write:account": "更改帐户信息" - "read:blocks": "查看黑名单" - "write:blocks": "编辑黑名单" + "read:account": "查看账号信息" + "write:account": "更改账号信息" + "read:blocks": "查看屏蔽名单" + "write:blocks": "编辑屏蔽名单" "read:drive": "查看网盘" "write:drive": "管理网盘文件" "read:favorites": "查看收藏夹" "write:favorites": "编辑收藏夹" "read:following": "查看关注信息" - "write:following": "关注/取消关注" - "read:messaging": "查看消息" - "write:messaging": "撰写或删除消息" - "read:mutes": "查看屏蔽列表" - "write:mutes": "编辑屏蔽列表" + "write:following": "关注/取消关注其它账号" + "read:messaging": "查看聊天消息" + "write:messaging": "撰写或删除聊天消息" + "read:mutes": "查看静音用户列表" + "write:mutes": "编辑静音用户列表" "write:notes": "撰写或删除帖子" "read:notifications": "查看通知" "write:notifications": "管理通知" "read:reactions": "查看回应" - "write:reactions": "回应操作" + "write:reactions": "编辑回应" "write:votes": "投票" "read:pages": "查看页面" - "write:pages": "操作页面" - "read:page-likes": "查看喜欢的页面" - "write:page-likes": "操作喜欢的页面" + "write:pages": "编辑或删除页面" + "read:page-likes": "查看页面上的喜欢" + "write:page-likes": "编辑页面上的喜欢" "read:user-groups": "查看用户组" "write:user-groups": "操作用户组" "read:channels": "查看频道" "write:channels": "管理频道" "read:gallery": "浏览图库" - "write:gallery": "操作图库" + "write:gallery": "编辑图库" "read:gallery-likes": "读取喜欢的图片" - "write:gallery-likes": "操作喜欢的图片" + "write:gallery-likes": "编辑喜欢的图片" _auth: - shareAccess: "您要授权允许“{name}”访问您的帐户吗?" - shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?" - permissionAsk: "这个应用程序需要以下权限" - pleaseGoBack: "请返回到应用程序" - callback: "回到应用程序" + shareAccess: "您要授权允许 \"{name}\" 访问您的账号吗?" + shareAccessAsk: "您确定要授权此应用访问您的账号吗?" + permissionAsk: "此应用请求以下权限:" + pleaseGoBack: "请返回至应用" + callback: "正在返回至应用" denied: "拒绝访问" - allPermissions: 完全的账户访问权限 - copyAsk: 请将以下授权码粘贴到应用程序中: + allPermissions: 完全的账号访问权限 + copyAsk: 请将以下授权码粘贴到应用中: _antennaSources: all: "所有帖子" homeTimeline: "已关注用户的帖子" @@ -1320,12 +1323,12 @@ _widgets: calendar: "日历" trends: "趋势" clock: "时钟" - rss: "RSS阅读器" - rssTicker: "RSS滚动条" + rss: "RSS 阅读器" + rssTicker: "RSS 滚动条" activity: "活动" photos: "照片" digitalClock: "数字时钟" - unixClock: "UNIX时钟" + unixClock: "UNIX 时钟" federation: "联邦宇宙" instanceCloud: "服务器云端" postForm: "发布窗口" @@ -1334,21 +1337,23 @@ _widgets: onlineUsers: "在线用户" jobQueue: "作业队列" serverMetric: "服务器指标" - aiscript: "AiScript控制台" + aiscript: "AiScript 控制台" aichan: "小蓝" userList: 用户列表 meiliStatus: 服务器状态 meiliIndexCount: 已索引的帖子 meiliSize: 索引大小 serverInfo: 服务器信息 + _userList: + chooseList: 选择列表 _cw: hide: "隐藏" show: "查看更多" - chars: "{count}个字符" + chars: "{count} 个字符" files: "{count} 个文件" _poll: noOnlyOneChoice: "需要至少两个选项" - choiceN: "选择{n}" + choiceN: "选择 {n}" noMore: "无法再添加更多了" canMultipleVote: "允许多个投票" expiration: "截止时间" @@ -1358,23 +1363,23 @@ _poll: deadlineDate: "截止日期" deadlineTime: "小时" duration: "时长" - votesCount: "{n}票" - totalVotes: "总票数{n}" + votesCount: "{n} 票" + totalVotes: "总票数 {n}" vote: "投票" showResult: "显示结果" voted: "已投票" closed: "已截止" - remainingDays: "{d}天{h}小时后截止" - remainingHours: "{h}小时{m}分后截止" - remainingMinutes: "{m}分{s}秒后截止" - remainingSeconds: "{s}秒后截止" + remainingDays: "{d} 天 {h} 小时后截止" + remainingHours: "{h} 小时 {m} 分后截止" + remainingMinutes: "{m} 分 {s} 秒后截止" + remainingSeconds: "{s} 秒后截止" _visibility: public: "公开" - publicDescription: "您的帖子将出现在全局时间线上" + publicDescription: "您的帖子将出现在公共时间线上" home: "不公开" - homeDescription: "仅发送至首页的时间线" + homeDescription: "仅发送至首页时间线" followers: "仅关注者" - followersDescription: "仅发送至关注者" + followersDescription: "仅对您的关注者和提及的用户可见" specified: "指定用户" specifiedDescription: "仅发送至指定用户" localOnly: "仅限本地" @@ -1386,29 +1391,30 @@ _postForm: _placeholders: a: "现在如何?" b: "发生了什么?" - c: "你有什么想法?" - d: "你想要发布些什么吗?" + c: "您有什么想法?" + d: "您想要发布些什么吗?" e: "请写下来吧" f: "等待您的发布..." _profile: name: "昵称" username: "用户名" description: "个人简介" - youCanIncludeHashtags: "您可以包含一个哈希标签。" + youCanIncludeHashtags: "您可以包含一个话题标签。" metadata: "附加信息" metadataEdit: "附加信息编辑" - metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。" + metadataDescription: "使用这些,您可以在您的个人资料中显示其它信息字段。" metadataLabel: "标签" metadataContent: "内容" changeAvatar: "修改头像" changeBanner: "修改横幅" + locationDescription: 如果您先输入您的城市,它将向其它用户显示您的当地时间。 _exportOrImport: allNotes: "所有帖子" - followingList: "关注中" - muteList: "屏蔽" - blockingList: "拉黑" + followingList: "已关注用户" + muteList: "已静音用户" + blockingList: "已屏蔽用户" userLists: "列表" - excludeMutingUsers: "排除屏蔽用户" + excludeMutingUsers: "排除已静音用户" excludeInactiveUsers: "排除不活跃用户" _charts: federation: "联合" @@ -1430,7 +1436,7 @@ _instanceCharts: usersTotal: "用户总计" notes: "帖子:增加/减少" notesTotal: "帖子总计" - ff: "关注/被关注:数量变化" + ff: "被关注用户/关注者的数量差异 " ffTotal: "关注/被关注者总计" cacheSize: "缓存大小:增加/减少" cacheSizeTotal: "缓存大小总计" @@ -1450,8 +1456,8 @@ _pages: updated: "页面已更新" deleted: "该页面已被删除" pageSetting: "页面设置" - nameAlreadyExists: "该页面URL已存在" - invalidNameTitle: "无效的页面URL" + nameAlreadyExists: "该页面 URL 已存在" + invalidNameTitle: "无效的页面 URL" invalidNameText: "请确认该项不为空" editThisPage: "编辑此页面" viewSource: "查看源代码" @@ -1466,7 +1472,7 @@ _pages: content: "页面内容" variables: "变量" title: "标题" - url: "页面URL" + url: "页面 URL" summary: "页面摘要" alignCenter: "居中" hideTitleWhenPinned: "置顶时隐藏标题" @@ -1495,7 +1501,7 @@ _pages: _post: text: "内容" attachCanvasImage: "附加画布图像" - canvasId: "画布ID" + canvasId: "画布 ID" textInput: "文本输入" _textInput: name: "变量名" @@ -1513,13 +1519,13 @@ _pages: default: "默认值" canvas: "画布" _canvas: - id: "画布ID" + id: "画布 ID" width: "宽度" height: "高度" note: "嵌入的帖子" _note: - id: "帖子ID" - idDescription: "您也可以通过粘贴帖子的URL来进行设置。" + id: "帖子 ID" + idDescription: "您也可以将帖子 URL 粘贴到此处。" detailed: "显示详细信息" switch: "开关" _switch: @@ -1546,7 +1552,7 @@ _pages: message: "按下时显示的消息" variable: "发送的变量" no-variable: "空" - callAiScript: "调用AiScript" + callAiScript: "调用 AiScript" _callAiScript: functionName: "函数名" radioButton: "选择项" @@ -1569,7 +1575,7 @@ _pages: list: "列表" blocks: text: "文本" - multiLineText: "文本 (多行)" + multiLineText: "文本(多行)" textList: "文本列表" _textList: info: "请使用换行符分隔每行" @@ -1608,42 +1614,42 @@ _pages: _divide: arg1: "A" arg2: "B" - mod: "取模(MOD)" + mod: "取模 (MOD)" _mod: arg1: "A" arg2: "B" round: "四舍五入" _round: arg1: "数值" - eq: "A和B相等" + eq: "A 和 B 相等" _eq: arg1: "A" arg2: "B" - notEq: "A和B不等" + notEq: "A 和 B 不等" _notEq: arg1: "A" arg2: "B" - and: "A和B" + and: "A 和 B" _and: arg1: "A" arg2: "B" - or: "A或B" + or: "A 或 B" _or: arg1: "A" arg2: "B" - lt: "< A小于B" + lt: "< A 小于 B" _lt: arg1: "A" arg2: "B" - gt: "> A大于B" + gt: "> A 大于 B" _gt: arg1: "A" arg2: "B" - ltEq: "<= A小于等于B" + ltEq: "<= A 小于等于 B" _ltEq: arg1: "A" arg2: "B" - gtEq: ">= A大于等于B" + gtEq: ">= A 大于等于 B" _gtEq: arg1: "A" arg2: "B" @@ -1665,30 +1671,30 @@ _pages: randomPick: "从列表中随机选择" _randomPick: arg1: "列表" - dailyRandom: "随机(每个用户每日)" + dailyRandom: "随机(每个用户每日)" _dailyRandom: arg1: "概率" - dailyRannum: "随机数(每个用户每日)" + dailyRannum: "随机数(每个用户每日)" _dailyRannum: arg1: "最小值" arg2: "最大值" - dailyRandomPick: "从列表中随机选择(每个用户每日)" + dailyRandomPick: "从列表中随机选择(每个用户每日)" _dailyRandomPick: arg1: "列表" - seedRandom: "随机 (种子)" + seedRandom: "随机(种子)" _seedRandom: arg1: "种子" arg2: "概率" - seedRannum: "随机数(种子)" + seedRannum: "随机数(种子)" _seedRannum: arg1: "种子" arg2: "最小值" arg3: "最大值" - seedRandomPick: "从列表中随机选择 (种子)" + seedRandomPick: "从列表中随机选择(种子)" _seedRandomPick: arg1: "种子" arg2: "列表" - DRPWPM: "从概率列表中随机选择(每用户每天)" + DRPWPM: "从概率列表中随机选择(每个用户每日)" _DRPWPM: arg1: "文本列表" pick: "从列表中选择" @@ -1709,7 +1715,7 @@ _pages: _splitStrByLine: arg1: "文本" ref: "变量" - aiScriptVar: "AiScript变量" + aiScriptVar: "AiScript 变量" fn: "函数" _fn: slots: "槽函数" @@ -1719,8 +1725,8 @@ _pages: _for: arg1: "次数" arg2: "处理" - typeError: "槽函数{slot}需要传入“{expect}”,但是实际传入为“{actual}”!" - thereIsEmptySlot: "槽函数{slot}为空!" + typeError: "槽函数 {slot} 需要传入 \"{expect}\",但是实际传入为 \"{actual}\"!" + thereIsEmptySlot: "槽函数 {slot} 为空!" types: string: "文字" number: "数值" @@ -1737,37 +1743,40 @@ _relayStatus: rejected: "已拒绝" _notification: fileUploaded: "文件已上传" - youGotMention: "来自{name}的提及" - youGotReply: "来自{name}的回复" - youGotQuote: "来自{name}的引用" - youRenoted: "来自{name}的转发" - youGotPoll: "来自{name}的投票" - youGotMessagingMessageFromUser: "来自{name}的聊天" - youGotMessagingMessageFromGroup: "来自{name}的群聊" - youWereFollowed: "关注了你。" + youGotMention: "来自 {name} 的提及" + youGotReply: "来自 {name} 的回复" + youGotQuote: "来自 {name} 的引用" + youRenoted: "来自 {name} 的转发" + youGotPoll: "来自 {name} 的投票" + youGotMessagingMessageFromUser: "来自 {name} 的聊天" + youGotMessagingMessageFromGroup: "来自 {name} 的群聊" + youWereFollowed: "关注了您" youReceivedFollowRequest: "您有新的关注请求" yourFollowRequestAccepted: "您的关注请求已通过" - youWereInvitedToGroup: "您有新的群组邀请" - pollEnded: "问卷调查结果已生成。" + youWereInvitedToGroup: "{userName} 邀请您加入一个群组" + pollEnded: "问卷调查结果已生成" emptyPushNotificationMessage: "推送通知已更新" _types: all: "全部" - follow: "关注中" + follow: "新关注者" mention: "提及" reply: "回复" renote: "转发" quote: "引用" reaction: "回应" - pollVote: "问卷调查被投票" + pollVote: "问卷调查投票" pollEnded: "问卷调查结束" - receiveFollowRequest: "收到关注请求" - followRequestAccepted: "关注请求已通过" - groupInvited: "加入群组邀请" + receiveFollowRequest: "收到的关注请求" + followRequestAccepted: "已通过的关注请求" + groupInvited: "群组加入邀请" app: "关联应用的通知" _actions: followBack: "回关" reply: "回复" renote: "转发" + reacted: 回应了您的帖子 + voted: 在您的问卷调查中投了票 + renoted: 转发了您的帖子 _deck: alwaysShowMainColumn: "总是显示主列" columnAlign: "列对齐" @@ -1779,17 +1788,17 @@ _deck: swapDown: "向下移动" stackLeft: "向左折叠" popRight: "向右弹出" - profile: "配置文件" + profile: "工作区" newProfile: "新建工作区" - renameProfile: "重命名配置文件" + renameProfile: "重命名工作区" deleteProfile: "删除工作区" - nameAlreadyExists: "该配置文件名已存在。" + nameAlreadyExists: "该工作区名已存在。" introduction: "将各列进行组合以创建您自己的界面!" - introduction2: "您可以随时通过屏幕右侧的 + 来添加列" - widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具" + introduction2: "您可以随时通过屏幕右侧的 + 来添加列。" + widgetsIntroduction: "从列菜单中,选择“编辑小部件”以添加小部件。" _columns: main: "主列" - widgets: "小工具" + widgets: "小部件" notifications: "通知" tl: "时间线" antenna: "天线" @@ -1804,44 +1813,46 @@ _messaging: migration: 迁移 _experiments: title: 实验性功能 + postImportsCaption: 允许用户从过去的 Calckey、Misskey、Mastodon、Akkoma 和 Pleroma 账号导入帖子。如果您的队列出现拥堵,则可能会导致加载速度减慢。 + enablePostImports: 启用帖子导入 license: 许可证 -flagSpeakAsCatDescription: 在猫模式下你的帖子会喵化 +flagSpeakAsCatDescription: 在猫模式下您的帖子会喵化 allowedInstances: 白名单服务器 -listsDesc: 列表可以让你创建含有指定用户的时间线,它们可以从时间线页面访问。 +listsDesc: 列表可以让您创建含有指定用户的时间线,它们可以从时间线页面访问。 flagSpeakAsCat: 像猫一样说话 -removeReaction: 移除你的回应 +removeReaction: 移除您的回应 expandOnNoteClick: 点击打开帖子 -expandOnNoteClickDesc: 如果禁用,你仍然可以在右键菜单中或通过点击时间戳打开帖子。 -sendPushNotificationReadMessage: 删除已阅读的推送通知 -customMOTD: 自定义 MOTD(闪屏消息) -sendPushNotificationReadMessageCaption: 短暂显示 "{emptyPushNotificationMessage}" 的通知,如果启用,可能会增加你的设备的耗电量。 -adminCustomCssWarn: 仅当你知道此设置的作用时才应使用它。输入不正确的值可能会导致每个人的客户端停止正常运行。请在用户设置中进行测试来确保您的 CSS +expandOnNoteClickDesc: 如果禁用,您仍然可以在右键菜单中或通过点击时间戳打开帖子。 +sendPushNotificationReadMessage: 已读后删除推送通知 +customMOTD: 自定义 MOTD(启动屏幕消息) +sendPushNotificationReadMessageCaption: 会短暂显示 "{emptyPushNotificationMessage}" 的通知,如果启用,可能会增加您的设备的耗电量。 +adminCustomCssWarn: 仅当您知道此设置的作用时才应使用它。输入不正确的值可能会导致每个人的客户端停止正常运行。请在用户设置中进行测试来确保您的 CSS 正常工作。 -customMOTDDescription: 自定义MOTD(闪屏)消息,一行一个,每次用户加载/刷新页面时都会随机显示。 -customSplashIconsDescription: 用换行符隔开的自定义闪屏图标的URL,在用户每次加载/重新加载页面时随机显示。请确保图片是在一个静态的 +customMOTDDescription: 自定义 MOTD(启动屏幕)消息,一行一个,每次用户加载/刷新页面时都会随机显示。 +customSplashIconsDescription: 用换行符隔开的自定义启动屏幕图标的 URL,在用户每次加载/重新载入页面时随机显示。请确保图片是在一个静态的 URL 上,最好全部调整为 192x192 的大小。 -recommendedInstancesDescription: 推荐的服务器以换行符分隔,它们将出现在推荐的时间线中。不要添加 "https://",仅添加域名。 +recommendedInstancesDescription: 推荐的服务器一行一个,它们将出现在推荐的时间线中。不要添加 "https://",仅添加域名。 splash: 启动画面 -showUpdates: Firefish 更新后显示弹出窗口 -selectInstance: 选择一个服务器 -silencedInstances: 静默的服务器 -antennaInstancesDescription: 每行列出一个服务器主机 +showUpdates: Calckey 更新后显示弹出窗口 +selectInstance: 选择服务器 +silencedInstances: 禁言的服务器 +antennaInstancesDescription: 列出服务器主机名,一行一个 pushNotification: 推送通知 subscribePushNotification: 启用推送通知 -showAdminUpdates: 提示新的 Firefish 版本可用(仅对于管理员) -searchPlaceholder: 搜索 Firefish -addInstance: 添加一个服务器 -jumpToPrevious: 跳转至上一个 -silenceThisInstance: 使此服务器静音 +showAdminUpdates: 提示新的 Calckey 版本可用(仅对于管理员) +searchPlaceholder: 搜索 Calckey +addInstance: 添加服务器 +jumpToPrevious: 跳转到上一个 +silenceThisInstance: 禁言此服务器 manageGroups: 管理群组 antennasDesc: "天线会显示符合您设置条件的新帖子!\n可以从时间线页面访问它们。" -channelFederationWarn: 频道还没有与其他服务器联合 -seperateRenoteQuote: 单独的推荐和引用按钮 -customSplashIcons: 自定义闪屏图标(urls) +channelFederationWarn: 频道还没有与其它服务器联合 +seperateRenoteQuote: 单独的转发和引用按钮 +customSplashIcons: 自定义启动屏幕图标(urls) alt: 替代文字 -pushNotificationNotSupported: 你的浏览器或者服务器不支持推送通知 +pushNotificationNotSupported: 您的浏览器或者服务器不支持推送通知 showAds: 显示广告 -enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Retun) +enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Retun 发送) recommendedInstances: 推荐服务器 updateAvailable: 可能有可用更新! swipeOnMobile: 允许在页面之间滑动 @@ -1851,27 +1862,27 @@ deleted: 已删除 editNote: 编辑帖子 edited: 于 {date} {time} 编辑 selectChannel: 选择一个频道 -accountMoved: 用户已迁移至新账户: -silencedInstancesDescription: 列出你想静默的服务器的主机名。列出的服务器中的账户被视为 "静默",只能发出跟随请求,如果不被跟随,就不能提及本地账户。这不会影响被封锁的服务器。 -hiddenTags: 隐藏的哈希标签 -userSaysSomethingReason: '{name} 说 {reason}' +accountMoved: 用户已迁移至新账号: +silencedInstancesDescription: 列出您想禁言的服务器的主机名。列出的服务器中的账号被视为 "禁言",只能发出关注请求,如果不被关注,就不能提及本地账号。这不会影响被屏蔽的服务器。 +hiddenTags: 隐藏的话题标签 +userSaysSomethingReason: '{name} 说了 {reason}' clipsDesc: 便签就像可共享的分类书签。您可以从各个帖子的菜单中创建便签。 -privateModeInfo: 当启用时,只有白名单上的服务器可以与你的服务器联合,所有的帖子都会对公共时间线隐藏。 +privateModeInfo: 当启用时,只有白名单上的服务器可以与您的服务器联合,所有的帖子都会对公共时间线隐藏。 allowedInstancesDescription: 要列入联合白名单的服务器的主机名,一行一个(仅适用于私密模式)。 -breakFollowConfirm: 你确定要移除关注者吗? +breakFollowConfirm: 确定要移除关注者吗? caption: 自动显示说明文字 newer: 更新的 -older: 更老的 +older: 更旧的 noInstances: 没有服务器 -silenced: 静默的 +silenced: 禁言的 accessibility: 无障碍 secureMode: 安全模式(仅允许授权的拉取) replayTutorial: 重播教程 userSaysSomethingReasonReply: '{name} 回复了包含 {reason} 的帖子' userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的帖子' -userSaysSomethingReasonRenote: '{name} 推荐了一个包含 {reason} 的帖子' +userSaysSomethingReasonRenote: '{name} 转发了一个包含 {reason} 的帖子' noThankYou: 不,谢谢 -secureModeInfo: 当向其他服务器请求时,不要在没有验证的情况下发回。 +secureModeInfo: 当向其它服务器请求时,不要在没有验证的情况下发回。 privateMode: 私密模式 instanceSecurity: 服务器安全 image: 图像 @@ -1880,49 +1891,56 @@ audio: 音频 cannotUploadBecauseExceedsFileSizeLimit: 无法上传此文件,因为它超出了允许的最大大小。 unsubscribePushNotification: 禁用推送通知 pushNotificationAlreadySubscribed: 推送通知已启用 -enableEmojiReactions: 启用 emoji 回应 +enableEmojiReactions: 启用表情符号回应 cw: 内容警告 -hiddenTagsDescription: 列出你想隐藏的话题标签(不带#)以避免在趋势和探索中显示。隐藏的标签仍然可以通过其他方式被发现。 +hiddenTagsDescription: 列出您想隐藏的话题标签(不带#)以避免在趋势和探索中显示。隐藏的标签仍然可以通过其它方式被发现。 enableRecommendedTimeline: 启用推荐时间线 _skinTones: medium: 中等 light: 浅色 yellow: 黄色 dark: 深色 -isModerator: 协作者 + mediumLight: 中等偏淡 + mediumDark: 中等偏深 +isModerator: 监察员 isAdmin: 管理员 findOtherInstance: 寻找其它服务器 -moveFromDescription: 这将为您的旧帐户设置一个别名,以便您可以从该旧帐户转移到当前帐户。在从旧帐户转移之前执行此操作。请输入格式如@person@server.com - 的帐户标签 +moveFromDescription: 这将为您的旧账号设置一个别名,以便您可以从该旧账号迁移到当前账号。在从旧账号迁移之前执行此操作。请输入格式如@person@server.com + 的账号标签 indexPosts: 索引帖子 signupsDisabled: 该服务器目前关闭注册,但您随时可以在另一台服务器上注册!如果您有该服务器的邀请码,请在下面输入。 -silencedWarning: 显示这个页面是因为这些用户来自你的管理员设置的静默服务器,所以他们有可能是垃圾信息。 -isBot: 这个账户是一个机器人 -moveAccountDescription: 这个过程是不可逆的。在移动之前,请确保您已在新帐户上为当前帐户设置了别名。请输入格式如 @person@server.com - 帐户标签 -moveFromLabel: 您要移出的旧帐户: +silencedWarning: 显示这个页面是因为这些用户来自您的管理员设置的禁言服务器,所以他们有可能是垃圾信息。 +isBot: 这个账号是一个机器人 +moveAccountDescription: 这个过程是不可逆的。在迁移之前,请确保您已在新账号上为当前账号设置了别名。请输入格式如 @person@server.com + 账号标签 +moveFromLabel: 您要迁移出的旧账号: preventAiLearning: 阻止 AI 机器人抓取 preventAiLearningDescription: 请求第三方人工智能语言模型不要研究您上传的内容,例如帖子和图像。 -noGraze: 请禁用 "Graze for Mastodon" 浏览器扩展,因为它会干扰 Firefish。 -moveTo: 将当前帐户移至新帐户 -moveToLabel: 你要迁移到的目标帐户: -moveAccount: 移动账户! -migrationConfirm: "你确实确定要将帐户迁移到 {account} 吗?此操作无法撤消,并且你将无法再次正常使用旧账户。\n另外,请确保你已将此当前帐户设置为要移出的帐户。" +noGraze: 请禁用 "Graze for Mastodon" 浏览器扩展,因为它会干扰 Calckey。 +moveTo: 将当前账号迁移至新账号 +moveToLabel: 您要迁移到的目标账号: +moveAccount: 迁移账号! +migrationConfirm: "您确实确定要将账号迁移到 {account} 吗?此操作无法撤消,并且您将无法再次正常使用旧账号。\n另外,请确保您已将此当前账号设置为要移出的账号。" indexFromDescription: 留空以索引每个帖子 noteId: 帖子 ID -moveFrom: 从旧帐户移至此帐户 -defaultReaction: 发出和收到的帖子的默认表情符号反应 +moveFrom: 从旧账号迁移至此账号 +defaultReaction: 发出和收到帖子的默认表情符号反应 indexNotice: 现在开始索引。这可能需要一段时间,请至少一个小时内不要重新启动服务器。 indexFrom: 从帖子 ID 开始的索引 sendModMail: 发送审核通知 -isLocked: 该帐户设置了关注请求 +isLocked: 该账号设置了关注请求 _filters: - notesBefore: 在之前的帖子 + notesBefore: 帖子早于 followingOnly: 仅关注中 - notesAfter: 在之后的帖子 + notesAfter: 帖子晚于 fromDomain: 来自域名 withFile: 带有文件 fromUser: 来自用户 followersOnly: 仅关注者 reactionPickerSkinTone: 首选的表情符号肤色 -isPatron: Firefish 赞助 +isPatron: Calckey 赞助 +_dialog: + charactersExceeded: 超出了最大字符数!当前:{current} / 限制:{max} + charactersBelow: 没有足够的字符!当前:{current} / 限制:{min} +enableIdenticonGeneration: 启用 Identicon 生成 +enableServerMachineStats: 启用服务器硬件统计 diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index ec5e8b8c0..3e21a9746 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1,6 +1,6 @@ _lang_: "繁體中文" headlineMisskey: "貼文連繫網路" -introMisskey: "歡迎! Firefish是一個免費,開放原碼,去中心化的社群網路🚀" +introMisskey: "歡迎! Calckey是一個開源、去中心化且永遠免費的社群網路平台!🚀" monthAndDay: "{month}月 {day}日" search: "搜尋" notifications: "通知" @@ -21,7 +21,7 @@ basicSettings: "基本設定" otherSettings: "其他設定" openInWindow: "在新視窗開啟" profile: "個人檔案" -timeline: "時間軸" +timeline: "時間線" noAccountDescription: "此用戶還沒有自我介紹。" login: "登入" loggingIn: "登入中" @@ -31,7 +31,7 @@ uploading: "上傳中..." save: "儲存" users: "使用者" addUser: "新增使用者" -favorite: "我的最愛" +favorite: "添加至我的最愛" favorites: "我的最愛" unfavorite: "從我的最愛中移除" favorited: "已添加至我的最愛。" @@ -43,7 +43,7 @@ copyContent: "複製內容" copyLink: "複製連結" delete: "刪除" deleteAndEdit: "刪除並編輯" -deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有情感、轉發和回覆也將會消失。" +deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也會消失。" addToList: "加入至清單" sendMessage: "發送訊息" copyUsername: "複製使用者名稱" @@ -64,7 +64,7 @@ export: "匯出" files: "檔案" download: "下載" driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。" -unfollowConfirm: "確定要取消追隨{name}嗎?" +unfollowConfirm: "確定要取消追隨 「{name}」 嗎?" exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。" importRequested: "已請求匯入。這可能會花一點時間。" lists: "清單" @@ -95,9 +95,9 @@ followRequestPending: "追隨許可批准中" enterEmoji: "輸入表情符號" renote: "轉發" unrenote: "取消轉發" -renoted: "已轉傳。" +renoted: "已轉發。" cantRenote: "無法轉發此貼文。" -cantReRenote: "無法轉傳之前已經轉傳過的內容。" +cantReRenote: "無法轉發之前已經轉發過的內容。" quote: "引用" pinnedNote: "已置頂的貼文" pinned: "置頂" @@ -105,7 +105,7 @@ you: "您" clickToShow: "按一下以顯示" sensitive: "敏感內容" add: "新增" -reaction: "情感" +reaction: "反應" enableEmojiReaction: "啟用表情符號反應" showEmojisInReactionNotifications: "在反應通知中顯示表情符號" reactionSetting: "在選擇器中顯示反應" @@ -140,14 +140,14 @@ emojiUrl: "表情符號URL" addEmoji: "加入表情符號" settingGuide: "推薦設定" cacheRemoteFiles: "快取遠端檔案" -cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。" -flagAsBot: "此使用者是機器人" -flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Firefish內部系統將本帳戶識別為機器人。" -flagAsCat: "此使用者是貓" +cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外數據花費。" +flagAsBot: "標記此帳號是機器人" +flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Calckey內部系統將本帳戶識別為機器人。" +flagAsCat: "你是喵咪嗎?w😺" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示!" -flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" +flagShowTimelineReplies: "在時間線上顯示貼文的回覆" flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。" -autoAcceptFollowed: "自動追隨中使用者的追隨請求" +autoAcceptFollowed: "自動准予追隨中使用者的追隨請求" addAccount: "添加帳戶" loginFailed: "登入失敗" showOnRemote: "轉到所在伺服器顯示" @@ -157,7 +157,7 @@ setWallpaper: "設定桌布" removeWallpaper: "移除桌布" searchWith: "搜尋: {q}" youHaveNoLists: "你沒有任何清單" -followConfirm: "你真的要追隨{name}嗎?" +followConfirm: "你真的要追隨 「{name}」 嗎?" proxyAccount: "代理帳戶" proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者追蹤該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶追蹤。" host: "主機" @@ -166,7 +166,7 @@ recipient: "收件人" annotation: "註解" federation: "站台聯邦" instances: "伺服器" -registeredAt: "初次觀測" +registeredAt: "初次註冊" latestRequestSentAt: "上次發送的請求" latestRequestReceivedAt: "上次收到的請求" latestStatus: "最後狀態" @@ -234,19 +234,19 @@ lookup: "查詢" announcements: "公告" imageUrl: "圖片URL" remove: "刪除" -removed: "已刪除" +removed: "已成功刪除" removeAreYouSure: "確定要刪掉「{x}」嗎?" deleteAreYouSure: "確定要刪掉「{x}」嗎?" resetAreYouSure: "確定要重設嗎?" saved: "已儲存" -messaging: "傳送訊息" +messaging: "訊息" upload: "上傳" keepOriginalUploading: "保留原圖" -keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。" +keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時自動產生用於貼文發布的圖片。" fromDrive: "從雲端空間" -fromUrl: "從URL" +fromUrl: "從網址" uploadFromUrl: "從網址上傳" -uploadFromUrlDescription: "您要上傳的文件的URL" +uploadFromUrlDescription: "您要上傳的文件的網址" uploadFromUrlRequested: "已請求上傳" uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。" explore: "探索" @@ -258,7 +258,7 @@ agreeTo: "我同意{0}" tos: "使用條款" start: "開始" home: "首頁" -remoteUserCaution: "由於該使用者來自遠端實例,因此資訊可能非即時的。" +remoteUserCaution: "由於該使用者來自遠端實例,因此資料可能是非即時的。" activity: "動態" images: "圖片" birthday: "生日" @@ -267,12 +267,12 @@ registeredDate: "註冊日期" location: "位置" theme: "外觀主題" themeForLightMode: "在淺色模式下使用的主題" -themeForDarkMode: "在黑暗模式下使用的主題" +themeForDarkMode: "在闇黑模式下使用的主題" light: "淺色" -dark: "黑暗" +dark: "闇黑" lightThemes: "明亮主題" -darkThemes: "黑暗主題" -syncDeviceDarkMode: "將黑暗模式與設備設置同步" +darkThemes: "闇黑主題" +syncDeviceDarkMode: "闇黑模式使用裝置設定" drive: "雲端硬碟" fileName: "檔案名稱" selectFile: "選擇檔案" @@ -281,19 +281,19 @@ selectFolder: "選擇資料夾" selectFolders: "選擇資料夾" renameFile: "重新命名檔案" folderName: "資料夾名稱" -createFolder: "新增資料夾" +createFolder: "創建資料夾" renameFolder: "重新命名資料夾" deleteFolder: "刪除資料夾" addFile: "加入附件" -emptyDrive: "雲端硬碟為空" -emptyFolder: "資料夾為空" +emptyDrive: "你的雲端硬碟沒有任何東西( ̄▽ ̄)\"" +emptyFolder: "資料夾裡面沒有東西(⊙_⊙;)" unableToDelete: "無法刪除" inputNewFileName: "輸入檔案名稱" inputNewDescription: "請輸入新標題" inputNewFolderName: "輸入新資料夾的名稱" circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。" hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。" -copyUrl: "複製URL" +copyUrl: "複製網址" rename: "重新命名" avatar: "大頭貼" banner: "橫幅" @@ -304,7 +304,7 @@ reload: "重新整理" doNothing: "無視" reloadConfirm: "確定要重新整理嗎?" watch: "關注" -unwatch: "取消追隨" +unwatch: "取消關注" accept: "接受" reject: "拒絕" normal: "正常" @@ -312,7 +312,7 @@ instanceName: "伺服器名稱" instanceDescription: "伺服器說明" maintainerName: "管理員名稱" maintainerEmail: "管理員郵箱" -tosUrl: "服務條款URL" +tosUrl: "服務條款網址" thisYear: "本年" thisMonth: "本月" today: "本日" @@ -323,23 +323,23 @@ pages: "頁面" integration: "整合" connectService: "己連結" disconnectService: "己斷開" -enableLocalTimeline: "開啟本地時間軸" -enableGlobalTimeline: "啟用公開時間軸" -disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。" +enableLocalTimeline: "開啟本地時間線" +enableGlobalTimeline: "啟用公開時間線" +disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和版主始終可以訪問所有的時間線。" registration: "註冊" enableRegistration: "開啟新使用者註冊" invite: "邀請" driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小" driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量" -inMb: "以Mbps為單位" -iconUrl: "圖像URL" -bannerUrl: "橫幅圖像URL" +inMb: "以MB為單位" +iconUrl: "圖標網址" +bannerUrl: "橫幅圖像網址" backgroundImageUrl: "背景圖片的來源網址" basicInfo: "基本資訊" pinnedUsers: "置頂用戶" -pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。" -pinnedPages: "釘選頁面" -pinnedPagesDescription: "輸入要固定至伺服器首頁的頁面路徑,以換行符分隔。" +pinnedUsersDescription: "在「探索」頁面中使用換行標記想要置頂的使用者。" +pinnedPages: "已釘選的頁面" +pinnedPagesDescription: "輸入要固定至伺服器首頁的頁面路徑,一行一個。" pinnedClipId: "置頂的摘錄ID" pinnedNotes: "已置頂的貼文" hcaptcha: "hCaptcha" @@ -482,7 +482,7 @@ promotion: "推廣" promote: "推廣" numberOfDays: "有效天數" hideThisNote: "隱藏此貼文" -showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦" +showFeaturedNotesInTimeline: "在時間線上顯示熱門推薦" objectStorage: "Object Storage (物件儲存)" useObjectStorage: "使用Object Storage" objectStorageBaseUrl: "根URL" @@ -502,7 +502,7 @@ objectStorageUseProxyDesc: "如果不使用代理進行API連接,請關閉" objectStorageSetPublicRead: "上傳時設定為\"public-read\"" serverLogs: "伺服器日誌" deleteAll: "刪除所有記錄" -showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" +showFixedPostForm: "於時間線頁頂顯示「發送貼文」方框" newNoteRecived: "發現新的貼文" sounds: "音效" listen: "聆聽" @@ -661,8 +661,8 @@ repliedCount: "回覆數量" renotedCount: "轉發次數" followingCount: "正在跟隨的用戶數量" followersCount: "跟隨者數量" -sentReactionsCount: "情感發送次數" -receivedReactionsCount: "情感收到次數" +sentReactionsCount: "反應發送次數" +receivedReactionsCount: "反應收到次數" pollVotesCount: "已統計的投票數" pollVotedCount: "已投票數" yes: "確定" @@ -688,7 +688,7 @@ experimentalFeatures: "實驗中的功能" developer: "開發者" makeExplorable: "使自己的帳戶能夠在“探索”頁面中顯示" makeExplorableDescription: "如果關閉,帳戶將不會被顯示在\"探索\"頁面中。" -showGapBetweenNotesInTimeline: "分開顯示時間軸上的貼文" +showGapBetweenNotesInTimeline: "分開顯示時間線上的貼文" duplicate: "複製" left: "左" center: "置中" @@ -702,7 +702,8 @@ onlineUsersCount: "{n}人正在線上" nUsers: "{n}用戶" nNotes: "{n}貼文" sendErrorReports: "傳送錯誤報告" -sendErrorReportsDescription: "啟用後,問題報告將傳送至Firefish開發者以提升軟體品質。\n問題報告可能包括OS版本,瀏覽器類型,行為歷史記錄等。" +sendErrorReportsDescription: "開啟後,錯誤出現時將會與 Calckey 分享詳細紀錄,對於 Calckey 的開發會有非常大的幫助。\n + 這將包括您的操作系統版本、使用的瀏覽器、您在 Calckey 中的活動等資料。" myTheme: "我的佈景主題" backgroundColor: "背景" accentColor: "重點色彩" @@ -862,7 +863,7 @@ check: "檢查" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" driveCapOverrideCaption: "如果指定0以下的值,就會被取消。" requireAdminForView: "必須以管理者帳號登入才可以檢視。" -isSystemAccount: "由系統自動建立與管理的帳號。" +isSystemAccount: "該帳號由系統自動創建並運行。 千千萬萬不要審核、編輯、刪除或以其他方式修改此帳戶,否則可能會破壞您的伺服器。" typeToConfirm: "要執行這項操作,請輸入 {x}" deleteAccount: "刪除帳號" document: "文件" @@ -1089,8 +1090,8 @@ _wordMute: muteWords: "加入靜音文字" muteWordsDescription: "用空格分隔指定AND,用換行分隔指定OR。" muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。" - softDescription: "隱藏時間軸中指定條件的貼文。" - hardDescription: "具有指定條件的貼文將不添加到時間軸。 即使您更改條件,未被添加的貼文也會被排除在外。" + softDescription: "隱藏時間線中指定條件的貼文。" + hardDescription: "具有指定條件的貼文將不添加到時間線。 即使您更改條件,未被添加的貼文也會被排除在外。" soft: "軟性靜音" hard: "硬性靜音" mutedNotes: "已靜音的貼文" @@ -1203,16 +1204,16 @@ _tutorial: step2_1: "首先,請完成你的個人資料。" step2_2: "通過提供一些關於你自己的資料,其他人會更容易了解他們是否想看到你的帖子或關注你。" step3_1: "現在是時候追隨一些人了!" - step3_2: "你的主頁和社交時間軸是基於你所追蹤的人,所以試著先追蹤幾個賬戶。\n點擊個人資料右上角的加號圈就可以關注它。" + step3_2: "你的主頁和社交時間線是基於你所追蹤的人,所以試著先追蹤幾個帳戶。\n點擊個人資料右上角的加號圈就可以關注它。" step4_1: "讓我們出去找你。" step4_2: "對於他們的第一條信息,有些人喜歡做 {introduction} 或一個簡單的 \"hello world!\"" - step5_1: "時間軸,到處都是時間軸!" - step5_2: "您的伺服器已啟用了{timelines}個時間軸。" - step5_3: "主 {icon} 時間軸是顯示你追蹤的帳號的帖子。" - step5_4: "本地 {icon} 時間軸是你可以看到伺服器中所有其他用戶的信息的時間軸。" - step5_5: "社交 {icon} 時間軸是顯示你的主時間軸 + 本地時間軸。" - step5_6: "推薦 {icon} 時間軸是顯示你的伺服器管理員推薦的帖文。" - step5_7: "全球 {icon} 時間軸是顯示來自所有其他連接的伺服器的帖文。" + step5_1: "時間線,到處都是時間線!" + step5_2: "您的伺服器已啟用了{timelines}個時間線。" + step5_3: "首頁 {icon} 時間線是顯示你追蹤的帳號的帖子。" + step5_4: "本地 {icon} 時間線是你可以看到伺服器中所有其他用戶的貼文的時間線。" + step5_5: "社交 {icon} 時間線是你的 首頁時間線 和 本地時間線 的結合體。" + step5_6: "推薦 {icon} 時間線是顯示你的伺服器管理員推薦的貼文。" + step5_7: "全球 {icon} 時間線是顯示來自所有其他連接的伺服器的貼文。" step6_1: "那麼,這裡是什麼地方?" step6_2: "你不只是加入Firefish。你已經加入了Fediverse的一個門戶,這是一個由成千上萬台服務器組成的互聯網絡。" step6_3: "每個服務器也有不同,而並不是所有的服務器都運行Firefish。但這個服務器確實是運行Firefish的! 你可能會覺得有點複雜,但你很快就會明白的。" @@ -1245,8 +1246,8 @@ _permissions: "write:notes": "撰寫或刪除貼文" "read:notifications": "查看通知" "write:notifications": "編輯通知" - "read:reactions": "查看情感" - "write:reactions": "編輯情感" + "read:reactions": "查看反應" + "write:reactions": "編輯反應" "write:votes": "投票" "read:pages": "顯示頁面" "write:pages": "編輯頁面" @@ -1284,7 +1285,7 @@ _weekday: _widgets: memo: "備忘錄" notifications: "通知" - timeline: "時間軸" + timeline: "時間線" calendar: "行事曆" trends: "發燒貼文" clock: "時鐘" @@ -1335,7 +1336,7 @@ _visibility: public: "公開" publicDescription: "發布給所有用戶" home: "不在主頁顯示" - homeDescription: "僅發送至首頁的時間軸" + homeDescription: "僅發送至首頁的時間線" followers: "追隨者" followersDescription: "僅發送至關注者" specified: "指定使用者" @@ -1403,7 +1404,7 @@ _instanceCharts: _timelines: home: "首頁" local: "本地" - social: "社群" + social: "社交" global: "公開" recommended: 推薦 _pages: @@ -1726,7 +1727,7 @@ _notification: pollEnded: "問卷調查結束" receiveFollowRequest: "已收到追隨請求" followRequestAccepted: "追隨請求已接受" - groupInvited: "加入社群邀請" + groupInvited: "群組加入邀請" app: "應用程式通知" _actions: followBack: "回關" @@ -1755,7 +1756,7 @@ _deck: main: "主列" widgets: "小工具" notifications: "通知" - tl: "時間軸" + tl: "時間線" antenna: "天線" list: "清單" mentions: "提及" @@ -1782,11 +1783,11 @@ enterSendsMessage: 在 Messaging 中按 Return 發送消息 (如關閉則是 Ctr migrationConfirm: "您確定要將你的帳戶遷移到 {account} 嗎? 一旦這樣做,你將無法復原,而你將無法再次正常使用您的帳戶。\n另外,請確保你已將此當前帳戶設置為您要遷移的帳戶。" customSplashIconsDescription: 每次用戶加載/重新加載頁面時,以換行符號分隔的自定啟動畫面圖標的網址將隨機顯示。請確保圖片位於靜態網址上,最好所有圖片解析度調整為 192x192。 -accountMoved: '該使用者已移至新帳戶:' +accountMoved: '該使用者已遷移至新帳戶:' showAds: 顯示廣告 noThankYou: 不用了,謝謝 selectInstance: 選擇伺服器 -enableRecommendedTimeline: 啟用推薦時間軸 +enableRecommendedTimeline: 啟用推薦時間線 antennaInstancesDescription: 分行列出一個伺服器 moveTo: 遷移此帳戶到新帳戶 moveToLabel: '請輸入你將會遷移到的帳戶:' @@ -1838,10 +1839,31 @@ pushNotification: 推送通知 subscribePushNotification: 啟用推送通知 unsubscribePushNotification: 禁用推送通知 pushNotificationAlreadySubscribed: 推送通知已經啟用 -recommendedInstancesDescription: 以每行分隔的推薦服務器出現在推薦的時間軸中。 不要添加 `https://`,只添加域名。 -searchPlaceholder: 搜尋 Firefish +recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中。 +searchPlaceholder: 在聯邦網路上搜尋 cw: 內容警告 selectChannel: 選擇一個頻道 newer: 較新 older: 較舊 jumpToPrevious: 跳到上一個 +removeReaction: 移除你的反應 +listsDesc: 清單可以創建一個只有您指定用戶的時間線。 可以從時間線頁面訪問它們。 +flagSpeakAsCatDescription: 在喵咪模式下你的貼文會被喵化ヾ(•ω•`)o +antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間線訪問它們。" +expandOnNoteClick: 點擊以打開貼文 +expandOnNoteClickDesc: 如果禁用,您仍然可以通過右鍵單擊菜單或單擊時間戳來打開貼文。 +hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(不帶 #)。 隱藏的主題標籤仍然可以通過其他方式發現。' +userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的貼文' +silencedInstancesDescription: 列出您想要靜音的伺服器的網址。 您列出的伺服器內的帳戶將被視為“沉默”,只能發出追隨請求,如果不追隨則不能提及本地帳戶。 + 這不會影響被阻止的伺服器。 +video: 影片 +audio: 音訊 +sendPushNotificationReadMessageCaption: 包含文本 “{emptyPushNotificationMessage}” 的通知將顯示一小段時間。 + 這可能會增加您設備的電池使用量(如果適用)。 +channelFederationWarn: 頻道功能尚未與聯邦宇宙連動 +swipeOnMobile: 允許在頁面之間滑動 +sendPushNotificationReadMessage: 閱讀相關通知或消息後刪除推送通知 +image: 圖片 +seperateRenoteQuote: 分別獨立的轉傳及引用按鈕 +clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。 +noteId: 貼文 ID diff --git a/package.json b/package.json index 8437fd223..88fad2383 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "firefish", - "version": "14.0.0-rc3", + "name": "calckey", + "version": "14.0.0-dev77", "codename": "aqua", "repository": { "type": "git", "url": "https://codeberg.org/firefish/firefish.git" }, - "packageManager": "pnpm@8.6.3", + "packageManager": "pnpm@8.6.7", "private": true, "scripts": { - "rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp", - "build": "pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp", + "rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp", + "build": "pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp", "start": "pnpm --filter backend run start", "start:test": "pnpm --filter backend run start:test", "init": "pnpm run migrate", @@ -21,13 +21,13 @@ "watch": "pnpm run dev", "dev": "pnpm node ./scripts/dev.js", "dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start", - "lint": "pnpm -r run lint", + "lint": "pnpm -r --parallel run lint", "cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "cypress run", "e2e": "start-server-and-test start:test http://localhost:61812 cy:run", "mocha": "pnpm --filter backend run mocha", "test": "pnpm run mocha", - "format": "pnpm -r run format", + "format": "pnpm -r --parallel run format", "clean": "pnpm node ./scripts/clean.js", "clean-all": "pnpm node ./scripts/clean-all.js", "cleanall": "pnpm run clean-all" @@ -36,16 +36,17 @@ "chokidar": "^3.3.1" }, "dependencies": { - "@bull-board/api": "5.2.0", - "@bull-board/ui": "5.2.0", + "@bull-board/api": "5.6.0", + "@bull-board/ui": "5.6.0", "@napi-rs/cli": "^2.16.1", "@tensorflow/tfjs": "^3.21.0", "js-yaml": "4.1.0", "seedrandom": "^3.0.5" }, "devDependencies": { - "@types/gulp": "4.0.10", - "@types/gulp-rename": "2.0.1", + "@types/gulp": "4.0.13", + "@types/gulp-rename": "2.0.2", + "@types/node": "20.4.1", "chalk": "4.1.2", "cross-env": "7.0.3", "cypress": "10.11.0", @@ -58,6 +59,6 @@ "install-peers": "^1.0.4", "rome": "^12.1.3", "start-server-and-test": "1.15.2", - "typescript": "4.9.4" + "typescript": "5.1.6" } } diff --git a/packages/README.md b/packages/README.md index 75e38a494..7c1ecbe67 100644 --- a/packages/README.md +++ b/packages/README.md @@ -6,4 +6,5 @@ This directory contains all of the packages Firefish uses. - `backend/native-utils`: Backend code written in Rust, bound to NodeJS by [NAPI-RS](https://napi.rs/) - `client`: Web interface written in Vue3 and TypeScript - `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript -- `firefish-js`: TypeScript SDK for both backend and client, also published on [NPM](https://www.npmjs.com/package/firefish-js) for public use +- `calckey-js`: TypeScript SDK for both backend and client, also published on [NPM](https://www.npmjs.com/package/calckey-js) for public use +- `megalodon`: TypeScript library used for partial Mastodon API compatibility diff --git a/packages/backend/assets/avatar.png b/packages/backend/assets/avatar.png new file mode 100644 index 000000000..ee22bdb3c Binary files /dev/null and b/packages/backend/assets/avatar.png differ diff --git a/packages/backend/assets/transparent.png b/packages/backend/assets/transparent.png new file mode 100644 index 000000000..240ca4f8d Binary files /dev/null and b/packages/backend/assets/transparent.png differ diff --git a/packages/backend/migration/1678426061773-tweak-varchar-length.js b/packages/backend/migration/1678426061773-tweak-varchar-length.js new file mode 100644 index 000000000..883374599 --- /dev/null +++ b/packages/backend/migration/1678426061773-tweak-varchar-length.js @@ -0,0 +1,10 @@ +export class tweakVarcharLength1678426061773 { + name = 'tweakVarcharLength1678426061773' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpUser" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpPass" TYPE character varying(1024)`, undefined); + } + + async down(queryRunner) {} +} diff --git a/packages/backend/migration/1688280713783-add-meta-options.js b/packages/backend/migration/1688280713783-add-meta-options.js new file mode 100644 index 000000000..e97a95c42 --- /dev/null +++ b/packages/backend/migration/1688280713783-add-meta-options.js @@ -0,0 +1,21 @@ +export class AddMetaOptions1688280713783 { + name = "AddMetaOptions1688280713783"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`, + ); + } + + async down(queryRunner) { + await queryRunner.query( + `ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`, + ); + await queryRunner.query( + `ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`, + ); + } +} diff --git a/packages/backend/migration/1688845537045-announcement-popup.js b/packages/backend/migration/1688845537045-announcement-popup.js new file mode 100644 index 000000000..196590d3e --- /dev/null +++ b/packages/backend/migration/1688845537045-announcement-popup.js @@ -0,0 +1,21 @@ +export class AnnouncementPopup1688845537045 { + name = "AnnouncementPopup1688845537045"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "announcement" ADD "showPopup" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "announcement" ADD "isGoodNews" boolean NOT NULL DEFAULT false`, + ); + } + + async down(queryRunner) { + await queryRunner.query( + `ALTER TABLE "announcement" DROP COLUMN "isGoodNews"`, + ); + await queryRunner.query( + `ALTER TABLE "announcement" DROP COLUMN "showPopup"`, + ); + } +} diff --git a/packages/backend/migration/1689136347561-donation-link.js b/packages/backend/migration/1689136347561-donation-link.js new file mode 100644 index 000000000..dbe0ed7c8 --- /dev/null +++ b/packages/backend/migration/1689136347561-donation-link.js @@ -0,0 +1,15 @@ +export class DonationLink1689136347561 { + name = "DonationLink1689136347561"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "meta" ADD "donationLink" character varying(256)`, + ); + } + + async down(queryRunner) { + await queryRunner.query( + `ALTER TABLE "meta" DROP COLUMN "DonationLink1689136347561"`, + ); + } +} diff --git a/packages/backend/native-utils/src/mastodon_api.rs b/packages/backend/native-utils/src/mastodon_api.rs index c7720e1f7..150a76245 100644 --- a/packages/backend/native-utils/src/mastodon_api.rs +++ b/packages/backend/native-utils/src/mastodon_api.rs @@ -13,7 +13,6 @@ pub enum IdConvertType { #[napi] pub fn convert_id(in_id: String, id_convert_type: IdConvertType) -> napi::Result { - println!("converting id: {}", in_id); use IdConvertType::*; match id_convert_type { MastodonId => { diff --git a/packages/backend/package.json b/packages/backend/package.json index 4be4ad51a..8e4042845 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -25,17 +25,16 @@ "@tensorflow/tfjs-node": "3.21.1" }, "dependencies": { - "@bull-board/api": "5.2.0", - "@bull-board/koa": "5.2.0", - "@bull-board/ui": "5.2.0", - "@firefish/megalodon": "5.2.0", + "@bull-board/api": "5.6.0", + "@bull-board/koa": "5.6.0", + "@bull-board/ui": "5.6.0", "@discordapp/twemoji": "14.1.2", "@elastic/elasticsearch": "7.17.0", "@koa/cors": "3.4.3", "@koa/multer": "3.0.2", "@koa/router": "9.0.1", "@peertube/http-signature": "1.7.0", - "@redocly/openapi-core": "1.0.0-beta.120", + "@redocly/openapi-core": "1.0.0-beta.131", "@sinonjs/fake-timers": "9.1.2", "@syuilo/aiscript": "0.11.1", "@tensorflow/tfjs": "^4.2.0", @@ -43,21 +42,19 @@ "ajv": "8.12.0", "archiver": "5.3.1", "argon2": "^0.30.3", - "async-mutex": "^0.4.0", - "autobind-decorator": "2.4.0", "autolinker": "4.0.0", "autwh": "0.1.0", - "aws-sdk": "2.1277.0", + "aws-sdk": "2.1413.0", "axios": "^1.4.0", "bcryptjs": "2.4.3", - "blurhash": "1.1.5", + "blurhash": "2.0.5", "bull": "4.10.4", "cacheable-lookup": "7.0.0", "firefish-js": "workspace:*", "cbor": "8.1.0", - "chalk": "5.2.0", + "chalk": "5.3.0", "chalk-template": "0.4.0", - "chokidar": "3.5.3", + "chokidar": "^3.5.3", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", @@ -70,15 +67,15 @@ "got": "12.5.3", "hpagent": "0.1.2", "ioredis": "5.3.2", - "ip-cidr": "3.0.11", + "ip-cidr": "3.1.0", "is-svg": "4.3.2", "js-yaml": "4.1.0", "jsdom": "20.0.3", "jsonld": "8.2.0", - "jsrsasign": "10.6.1", - "koa": "2.13.4", + "jsrsasign": "10.8.6", + "koa": "2.14.2", "koa-body": "^6.0.1", - "koa-bodyparser": "4.3.0", + "koa-bodyparser": "4.4.1", "koa-favicon": "2.1.0", "koa-json-body": "5.3.0", "koa-logger": "3.2.1", @@ -87,9 +84,11 @@ "koa-send": "5.0.1", "koa-slow": "2.1.0", "koa-views": "7.0.2", + "megalodon": "workspace:*", "meilisearch": "0.33.0", "mfm-js": "0.23.3", "mime-types": "2.1.35", + "msgpackr": "1.9.5", "multer": "1.4.4-lts.1", "native-utils": "link:native-utils", "nested-property": "4.0.0", @@ -98,9 +97,9 @@ "nsfwjs": "2.4.2", "oauth": "^0.10.0", "os-utils": "0.0.14", - "otpauth": "^9.1.2", + "otpauth": "^9.1.3", "parse5": "7.1.2", - "pg": "8.11.0", + "pg": "8.11.1", "private-ip": "2.3.4", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -110,7 +109,7 @@ "qs": "6.11.2", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.19.0", + "re2": "1.19.1", "redis-lock": "0.1.4", "redis-semaphore": "5.3.1", "reflect-metadata": "0.1.13", @@ -119,7 +118,7 @@ "rss-parser": "3.13.0", "sanitize-html": "2.10.0", "seedrandom": "^3.0.5", - "semver": "7.5.1", + "semver": "7.5.4", "sharp": "0.32.1", "sonic-channel": "^1.3.1", "stringz": "2.1.0", @@ -130,27 +129,26 @@ "tinycolor2": "1.5.2", "tmp": "0.2.1", "twemoji-parser": "14.0.0", - "typeorm": "0.3.11", + "typeorm": "0.3.17", "ulid": "2.3.0", "uuid": "9.0.0", - "web-push": "3.6.1", + "web-push": "3.6.3", "websocket": "1.0.34", "xev": "3.0.2" }, "devDependencies": { "@swc/cli": "^0.1.62", - "@swc/core": "^1.3.62", + "@swc/core": "^1.3.68", "@types/adm-zip": "^0.5.0", "@types/bcryptjs": "2.4.2", - "@types/bull": "3.15.9", "@types/cbor": "6.0.0", "@types/escape-regexp": "0.0.1", - "@types/fluent-ffmpeg": "2.1.20", + "@types/fluent-ffmpeg": "2.1.21", "@types/js-yaml": "4.0.5", - "@types/jsdom": "20.0.1", - "@types/jsonld": "1.5.8", - "@types/jsrsasign": "10.5.4", - "@types/koa": "2.13.5", + "@types/jsdom": "21.1.1", + "@types/jsonld": "1.5.9", + "@types/jsrsasign": "10.5.8", + "@types/koa": "2.13.6", "@types/koa-bodyparser": "4.3.10", "@types/koa-cors": "0.0.2", "@types/koa-favicon": "2.0.21", @@ -169,7 +167,7 @@ "@types/probe-image-size": "^7.2.0", "@types/pug": "2.0.6", "@types/punycode": "2.1.0", - "@types/qrcode": "1.5.0", + "@types/qrcode": "1.5.1", "@types/qs": "6.9.7", "@types/random-seed": "0.3.3", "@types/ratelimiter": "3.4.4", @@ -177,17 +175,15 @@ "@types/rename": "1.0.4", "@types/sanitize-html": "2.9.0", "@types/semver": "7.5.0", - "@types/sharp": "0.31.1", "@types/sinonjs__fake-timers": "8.1.2", "@types/tinycolor2": "1.4.3", "@types/tmp": "0.2.3", - "@types/uuid": "8.3.4", + "@types/uuid": "9.0.2", "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", - "@types/ws": "8.5.4", - "autobind-decorator": "2.4.0", + "@types/ws": "8.5.5", "cross-env": "7.0.3", - "eslint": "^8.42.0", + "eslint": "^8.44.0", "execa": "6.1.0", "json5": "2.2.3", "json5-loader": "4.0.1", @@ -195,11 +191,11 @@ "pug": "3.0.2", "strict-event-emitter-types": "2.0.0", "swc-loader": "^0.2.3", - "ts-loader": "9.4.3", + "ts-loader": "9.4.4", "ts-node": "10.9.1", "tsconfig-paths": "4.2.0", - "typescript": "5.1.3", - "webpack": "^5.85.1", + "typescript": "5.1.6", + "webpack": "^5.88.1", "ws": "8.13.0" } } diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index 2185cda11..7c8c2c35d 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -55,6 +55,8 @@ export default function load() { mixin.clientEntry = clientManifest["src/init.ts"]; if (!config.redis.prefix) config.redis.prefix = mixin.host; + if (config.cacheServer && !config.cacheServer.prefix) + config.cacheServer.prefix = mixin.host; return Object.assign(config, mixin); } diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 84808413c..7789c26e0 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -26,6 +26,16 @@ export type Source = { user?: string; tls?: { [y: string]: string }; }; + cacheServer?: { + host: string; + port: number; + family?: number; + pass?: string; + db?: number; + prefix?: string; + user?: string; + tls?: { [z: string]: string }; + }; elasticsearch: { host: string; port: number; diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 7e8f96444..1f1e63292 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -1,8 +1,7 @@ import config from "@/config/index.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; -export const MAX_NOTE_TEXT_LENGTH = - config.maxNoteLength != null ? config.maxNoteLength : 3000; // <- should we increase this? +export const MAX_NOTE_TEXT_LENGTH = config.maxNoteLength ?? 3000; export const MAX_CAPTION_TEXT_LENGTH = Math.min( config.maxCaptionLength ?? 1500, DB_MAX_IMAGE_COMMENT_LENGTH, diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts index c936d619a..7d4fcd25e 100644 --- a/packages/backend/src/daemons/server-stats.ts +++ b/packages/backend/src/daemons/server-stats.ts @@ -1,6 +1,7 @@ import si from "systeminformation"; import Xev from "xev"; import * as osUtils from "os-utils"; +import { fetchMeta } from "@/misc/fetch-meta.js"; import meilisearch from "../db/meilisearch.js"; const ev = new Xev(); @@ -20,6 +21,10 @@ export default function () { ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); }); + fetchMeta().then((meta) => { + if (!meta.enableServerMachineStats) return; + }); + async function tick() { const cpu = await cpuUsage(); const memStats = await mem(); diff --git a/packages/backend/src/db/redis.ts b/packages/backend/src/db/redis.ts index a1f3279f3..215effd8e 100644 --- a/packages/backend/src/db/redis.ts +++ b/packages/backend/src/db/redis.ts @@ -2,15 +2,19 @@ import Redis from "ioredis"; import config from "@/config/index.js"; export function createConnection() { + let source = config.redis; + if (config.cacheServer) { + source = config.cacheServer; + } return new Redis({ - port: config.redis.port, - host: config.redis.host, - family: config.redis.family ?? 0, - password: config.redis.pass, - username: config.redis.user ?? "default", - keyPrefix: `${config.redis.prefix}:`, - db: config.redis.db || 0, - tls: config.redis.tls, + port: source.port, + host: source.host, + family: source.family ?? 0, + password: source.pass, + username: source.user ?? "default", + keyPrefix: `${source.prefix}:`, + db: source.db || 0, + tls: source.tls, }); } diff --git a/packages/backend/src/misc/acct.ts b/packages/backend/src/misc/acct.ts index 5b7767a10..cb6808b4b 100644 --- a/packages/backend/src/misc/acct.ts +++ b/packages/backend/src/misc/acct.ts @@ -4,7 +4,7 @@ export type Acct = { }; export function parse(acct: string): Acct { - if (acct.startsWith("@")) acct = acct.substr(1); + if (acct.startsWith("@")) acct = acct.slice(1); const split = acct.split("@", 2); return { username: split[0], host: split[1] || null }; } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 9abebc91c..913258f05 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,43 +1,85 @@ +import { redisClient } from "@/db/redis.js"; +import { encode, decode } from "msgpackr"; +import { ChainableCommander } from "ioredis"; + export class Cache { - public cache: Map; - private lifetime: number; + private ttl: number; + private prefix: string; - constructor(lifetime: Cache["lifetime"]) { - this.cache = new Map(); - this.lifetime = lifetime; + constructor(name: string, ttlSeconds: number) { + this.ttl = ttlSeconds; + this.prefix = `cache:${name}`; } - public set(key: string | null, value: T): void { - this.cache.set(key, { - date: Date.now(), - value, - }); + private prefixedKey(key: string | null): string { + return key ? `${this.prefix}:${key}` : this.prefix; } - public get(key: string | null): T | undefined { - const cached = this.cache.get(key); - if (cached == null) return undefined; - if (Date.now() - cached.date > this.lifetime) { - this.cache.delete(key); - return undefined; + public async set( + key: string | null, + value: T, + transaction?: ChainableCommander, + ): Promise { + const _key = this.prefixedKey(key); + const _value = Buffer.from(encode(value)); + const commander = transaction ?? redisClient; + await commander.set(_key, _value, "EX", this.ttl); + } + + public async get(key: string | null, renew = false): Promise { + const _key = this.prefixedKey(key); + const cached = await redisClient.getBuffer(_key); + if (cached === null) return undefined; + + if (renew) await redisClient.expire(_key, this.ttl); + + return decode(cached) as T; + } + + public async getAll(renew = false): Promise> { + const keys = await redisClient.keys(`${this.prefix}*`); + const map = new Map(); + if (keys.length === 0) { + return map; } - return cached.value; + const values = await redisClient.mgetBuffer(keys); + + for (const [i, key] of keys.entries()) { + const val = values[i]; + if (val !== null) { + map.set(key, decode(val) as T); + } + } + + if (renew) { + const trans = redisClient.multi(); + for (const key of map.keys()) { + trans.expire(key, this.ttl); + } + await trans.exec(); + } + + return map; } - public delete(key: string | null) { - this.cache.delete(key); + public async delete(...keys: (string | null)[]): Promise { + if (keys.length > 0) { + const _keys = keys.map(this.prefixedKey); + await redisClient.del(_keys); + } } /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * Returns if cached value exists. Otherwise, calls fetcher and caches. + * Overwrites cached value if invalidated by the optional validator. */ public async fetch( key: string | null, fetcher: () => Promise, + renew = false, validator?: (cachedValue: T) => boolean, ): Promise { - const cachedValue = this.get(key); + const cachedValue = await this.get(key, renew); if (cachedValue !== undefined) { if (validator) { if (validator(cachedValue)) { @@ -52,20 +94,21 @@ export class Cache { // Cache MISS const value = await fetcher(); - this.set(key, value); + await this.set(key, value); return value; } /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * Returns if cached value exists. Otherwise, calls fetcher and caches if the fetcher returns a value. + * Overwrites cached value if invalidated by the optional validator. */ public async fetchMaybe( key: string | null, fetcher: () => Promise, + renew = false, validator?: (cachedValue: T) => boolean, ): Promise { - const cachedValue = this.get(key); + const cachedValue = await this.get(key, renew); if (cachedValue !== undefined) { if (validator) { if (validator(cachedValue)) { @@ -81,7 +124,7 @@ export class Cache { // Cache MISS const value = await fetcher(); if (value !== undefined) { - this.set(key, value); + await this.set(key, value); } return value; } diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index 358fba0f3..1ff09d629 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -11,7 +11,7 @@ import * as Acct from "@/misc/acct.js"; import type { Packed } from "./schema.js"; import { Cache } from "./cache.js"; -const blockingCache = new Cache(1000 * 60 * 5); +const blockingCache = new Cache("blocking", 60 * 5); // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている diff --git a/packages/backend/src/misc/emoji-meta.ts b/packages/backend/src/misc/emoji-meta.ts index fd9d9baa5..2b9365b82 100644 --- a/packages/backend/src/misc/emoji-meta.ts +++ b/packages/backend/src/misc/emoji-meta.ts @@ -1,33 +1,41 @@ import probeImageSize from "probe-image-size"; -import { Mutex, withTimeout } from "async-mutex"; +import { Mutex } from "redis-semaphore"; import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; import Logger from "@/services/logger.js"; import { Cache } from "./cache.js"; +import { redisClient } from "@/db/redis.js"; export type Size = { width: number; height: number; }; -const cache = new Cache(1000 * 60 * 10); // once every 10 minutes for the same url -const mutex = withTimeout(new Mutex(), 1000); +const cache = new Cache("emojiMeta", 60 * 10); // once every 10 minutes for the same url +const logger = new Logger("emoji"); export async function getEmojiSize(url: string): Promise { - const logger = new Logger("emoji"); + let attempted = true; - await mutex.runExclusive(() => { - const attempted = cache.get(url); - if (!attempted) { - cache.set(url, true); - } else { - logger.warn(`Attempt limit exceeded: ${url}`); - throw new Error("Too many attempts"); - } - }); + const lock = new Mutex(redisClient, "getEmojiSize"); + await lock.acquire(); try { - logger.info(`Retrieving emoji size from ${url}`); + attempted = (await cache.get(url)) === true; + if (!attempted) { + await cache.set(url, true); + } + } finally { + await lock.release(); + } + + if (attempted) { + logger.warn(`Attempt limit exceeded: ${url}`); + throw new Error("Too many attempts"); + } + + try { + logger.debug(`Retrieving emoji size from ${url}`); const { width, height, mime } = await probeImageSize(url, { timeout: 5000, }); diff --git a/packages/backend/src/misc/hard-limits.ts b/packages/backend/src/misc/hard-limits.ts index 51d2c0f5d..f1a58a425 100644 --- a/packages/backend/src/misc/hard-limits.ts +++ b/packages/backend/src/misc/hard-limits.ts @@ -3,8 +3,9 @@ /** * Maximum note text length that can be stored in DB. * Surrogate pairs count as one + * DEPRECARTED: use const/MAX_NOTE_TEXT_LENGTH instead */ -export const DB_MAX_NOTE_TEXT_LENGTH = 8192; +// export const DB_MAX_NOTE_TEXT_LENGTH = 8192; /** * Maximum image description length that can be stored in DB. diff --git a/packages/backend/src/misc/keypair-store.ts b/packages/backend/src/misc/keypair-store.ts index 4551bfd98..625577359 100644 --- a/packages/backend/src/misc/keypair-store.ts +++ b/packages/backend/src/misc/keypair-store.ts @@ -3,10 +3,12 @@ import type { User } from "@/models/entities/user.js"; import type { UserKeypair } from "@/models/entities/user-keypair.js"; import { Cache } from "./cache.js"; -const cache = new Cache(Infinity); +const cache = new Cache("keypairStore", 60 * 30); export async function getUserKeypair(userId: User["id"]): Promise { - return await cache.fetch(userId, () => - UserKeypairs.findOneByOrFail({ userId: userId }), + return await cache.fetch( + userId, + () => UserKeypairs.findOneByOrFail({ userId: userId }), + true, ); } diff --git a/packages/backend/src/misc/nyaize.ts b/packages/backend/src/misc/nyaize.ts index dd0c5bbdf..13a112ce5 100644 --- a/packages/backend/src/misc/nyaize.ts +++ b/packages/backend/src/misc/nyaize.ts @@ -20,5 +20,9 @@ export function nyaize(text: string): string { ) .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, "다냥") .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, "냥") + // el-GR + .replaceAll("να", "νια") + .replaceAll("ΝΑ", "ΝΙΑ") + .replaceAll("Να", "Νια") ); } diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index 7aee4ec25..795a267f9 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -7,8 +7,9 @@ import { isSelfHost, toPunyNullable } from "./convert-host.js"; import { decodeReaction } from "./reaction-lib.js"; import config from "@/config/index.js"; import { query } from "@/prelude/url.js"; +import { redisClient } from "@/db/redis.js"; -const cache = new Cache(1000 * 60 * 60 * 12); +const cache = new Cache("populateEmojis", 60 * 60 * 12); /** * 添付用絵文字情報 @@ -75,7 +76,7 @@ export async function populateEmoji( if (emoji && !(emoji.width && emoji.height)) { emoji = await queryOrNull(); - cache.set(cacheKey, emoji); + await cache.set(cacheKey, emoji); } if (emoji == null) return null; @@ -150,7 +151,7 @@ export async function prefetchEmojis( emojis: { name: string; host: string | null }[], ): Promise { const notCachedEmojis = emojis.filter( - (emoji) => cache.get(`${emoji.name} ${emoji.host}`) == null, + async (emoji) => !(await cache.get(`${emoji.name} ${emoji.host}`)), ); const emojisQuery: any[] = []; const hosts = new Set(notCachedEmojis.map((e) => e.host)); @@ -169,7 +170,9 @@ export async function prefetchEmojis( select: ["name", "host", "originalUrl", "publicUrl"], }) : []; + const trans = redisClient.multi(); for (const emoji of _emojis) { - cache.set(`${emoji.name} ${emoji.host}`, emoji); + cache.set(`${emoji.name} ${emoji.host}`, emoji, trans); } + await trans.exec(); } diff --git a/packages/backend/src/models/entities/announcement.ts b/packages/backend/src/models/entities/announcement.ts index 1939604b9..7872c0fe1 100644 --- a/packages/backend/src/models/entities/announcement.ts +++ b/packages/backend/src/models/entities/announcement.ts @@ -36,6 +36,16 @@ export class Announcement { }) public imageUrl: string | null; + @Column("boolean", { + default: false, + }) + public showPopup: boolean; + + @Column("boolean", { + default: false, + }) + public isGoodNews: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 6c7482406..817d4c30b 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -326,13 +326,13 @@ export class Meta { public smtpPort: number | null; @Column("varchar", { - length: 128, + length: 1024, nullable: true, }) public smtpUser: string | null; @Column("varchar", { - length: 128, + length: 1024, nullable: true, }) public smtpPass: string | null; @@ -546,4 +546,20 @@ export class Meta { default: {}, }) public experimentalFeatures: Record; + + @Column("boolean", { + default: false, + }) + public enableServerMachineStats: boolean; + + @Column("boolean", { + default: true, + }) + public enableIdenticonGeneration: boolean; + + @Column("varchar", { + length: 256, + nullable: true, + }) + public donationLink: string | null; } diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 48c8d75b3..e4aab896d 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -1,4 +1,3 @@ -import { URL } from "url"; import { In, Not } from "typeorm"; import Ajv from "ajv"; import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js"; @@ -40,7 +39,10 @@ import { } from "../index.js"; import type { Instance } from "../entities/instance.js"; -const userInstanceCache = new Cache(1000 * 60 * 60 * 3); +const userInstanceCache = new Cache( + "userInstance", + 60 * 60 * 3, +); type IsUserDetailed = Detailed extends true ? Packed<"UserDetailed"> @@ -451,6 +453,7 @@ export const UserRepository = db.getRepository(User).extend({ isAdmin: user.isAdmin || falsy, isModerator: user.isModerator || falsy, isBot: user.isBot || falsy, + isLocked: user.isLocked, isCat: user.isCat || falsy, speakAsCat: user.speakAsCat || falsy, instance: user.host @@ -495,7 +498,6 @@ export const UserRepository = db.getRepository(User).extend({ : null, bannerBlurhash: user.banner?.blurhash || null, bannerColor: null, // 後方互換性のため - isLocked: user.isLocked, isSilenced: user.isSilenced || falsy, isSuspended: user.isSuspended || falsy, description: profile!.description, diff --git a/packages/backend/src/queue/processors/background/index-all-notes.ts b/packages/backend/src/queue/processors/background/index-all-notes.ts index 1dce4406a..c0275b420 100644 --- a/packages/backend/src/queue/processors/background/index-all-notes.ts +++ b/packages/backend/src/queue/processors/background/index-all-notes.ts @@ -1,4 +1,5 @@ import type Bull from "bull"; +import type { DoneCallback } from "bull"; import { queueLogger } from "../../logger.js"; import { Notes } from "@/models/index.js"; @@ -11,7 +12,7 @@ const logger = queueLogger.createSubLogger("index-all-notes"); export default async function indexAllNotes( job: Bull.Job>, - done: () => void, + done: DoneCallback, ): Promise { logger.info("Indexing all notes..."); @@ -20,7 +21,7 @@ export default async function indexAllNotes( let total: number = (job.data.total as number) ?? 0; let running = true; - const take = 100000; + const take = 10000; const batch = 100; while (running) { logger.info( @@ -41,13 +42,14 @@ export default async function indexAllNotes( }, relations: ["user"], }); - } catch (e) { + } catch (e: any) { logger.error(`Failed to query notes ${e}`); - continue; + done(e); + break; } if (notes.length === 0) { - job.progress(100); + await job.progress(100); running = false; break; } @@ -55,7 +57,7 @@ export default async function indexAllNotes( try { const count = await Notes.count(); total = count; - job.update({ indexedCount, cursor, total }); + await job.update({ indexedCount, cursor, total }); } catch (e) {} for (let i = 0; i < notes.length; i += batch) { @@ -69,12 +71,12 @@ export default async function indexAllNotes( indexedCount += chunk.length; const pct = (indexedCount / total) * 100; - job.update({ indexedCount, cursor, total }); - job.progress(+pct.toFixed(1)); + await job.update({ indexedCount, cursor, total }); + await job.progress(+pct.toFixed(1)); logger.info(`Indexed notes ${indexedCount}/${total ? total : "?"}`); } cursor = notes[notes.length - 1].id; - job.update({ indexedCount, cursor, total }); + await job.update({ indexedCount, cursor, total }); if (notes.length < take) { running = false; diff --git a/packages/backend/src/queue/processors/db/import-masto-post.ts b/packages/backend/src/queue/processors/db/import-masto-post.ts index 317ba7b1b..efa4adf3f 100644 --- a/packages/backend/src/queue/processors/db/import-masto-post.ts +++ b/packages/backend/src/queue/processors/db/import-masto-post.ts @@ -50,7 +50,7 @@ export async function importMastoPost( text: text || undefined, reply, renote: null, - cw: post.sensitive, + cw: post.object.sensitive ? post.object.summary : undefined, localOnly: false, visibility: "hidden", visibleUsers: [], diff --git a/packages/backend/src/queue/processors/ended-poll-notification.ts b/packages/backend/src/queue/processors/ended-poll-notification.ts index e3d860ac8..3bf010a1b 100644 --- a/packages/backend/src/queue/processors/ended-poll-notification.ts +++ b/packages/backend/src/queue/processors/ended-poll-notification.ts @@ -33,7 +33,7 @@ export async function endedPollNotification( } // Broadcast the poll result once it ends - await deliverQuestionUpdate(note.id); + if (!note.localOnly) await deliverQuestionUpdate(note.id); done(); } diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index b319fbd38..0e500b89e 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -35,8 +35,11 @@ export default async (job: Bull.Job): Promise => { info["@context"] = undefined; logger.debug(JSON.stringify(info, null, 2)); - if (!signature?.keyId) return `Invalid signature: ${signature}`; - + if (!signature?.keyId) { + const err = `Invalid signature: ${signature}`; + job.moveToFailed({ message: err }); + return err; + } //#endregion const host = toPuny(new URL(signature.keyId).hostname); diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 6e448d4b1..a710b9f11 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -5,7 +5,6 @@ import type { CacheableRemoteUser, CacheableUser, } from "@/models/entities/user.js"; -import { User, IRemoteUser } from "@/models/entities/user.js"; import type { UserPublickey } from "@/models/entities/user-publickey.js"; import type { MessagingMessage } from "@/models/entities/messaging-message.js"; import { @@ -20,8 +19,11 @@ import type { IObject } from "./type.js"; import { getApId } from "./type.js"; import { resolvePerson } from "./models/person.js"; -const publicKeyCache = new Cache(Infinity); -const publicKeyByUserIdCache = new Cache(Infinity); +const publicKeyCache = new Cache("publicKey", 60 * 30); +const publicKeyByUserIdCache = new Cache( + "publicKeyByUserId", + 60 * 30, +); export type UriParseResult = | { @@ -123,17 +125,23 @@ export default class DbResolver { if (parsed.type !== "users") return null; return ( - (await userByIdCache.fetchMaybe(parsed.id, () => - Users.findOneBy({ - id: parsed.id, - }).then((x) => x ?? undefined), + (await userByIdCache.fetchMaybe( + parsed.id, + () => + Users.findOneBy({ + id: parsed.id, + }).then((x) => x ?? undefined), + true, )) ?? null ); } else { - return await uriPersonCache.fetch(parsed.uri, () => - Users.findOneBy({ - uri: parsed.uri, - }), + return await uriPersonCache.fetch( + parsed.uri, + () => + Users.findOneBy({ + uri: parsed.uri, + }), + true, ); } } @@ -156,14 +164,17 @@ export default class DbResolver { return key; }, + true, (key) => key != null, ); if (key == null) return null; return { - user: (await userByIdCache.fetch(key.userId, () => - Users.findOneByOrFail({ id: key.userId }), + user: (await userByIdCache.fetch( + key.userId, + () => Users.findOneByOrFail({ id: key.userId }), + true, )) as CacheableRemoteUser, key, }; @@ -183,6 +194,7 @@ export default class DbResolver { const key = await publicKeyByUserIdCache.fetch( user.id, () => UserPublickeys.findOneBy({ userId: user.id }), + true, (v) => v != null, ); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index c04b1c0c9..8f17cfae8 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -135,23 +135,23 @@ export async function fetchPerson( ): Promise { if (typeof uri !== "string") throw new Error("uri is not string"); - const cached = uriPersonCache.get(uri); + const cached = await uriPersonCache.get(uri, true); if (cached) return cached; // Fetch from the database if the URI points to this server if (uri.startsWith(`${config.url}/`)) { const id = uri.split("/").pop(); const u = await Users.findOneBy({ id }); - if (u) uriPersonCache.set(uri, u); + if (u) await uriPersonCache.set(uri, u); return u; } //#region Returns if already registered with this server - const exist = await Users.findOneBy({ uri }); + const user = await Users.findOneBy({ uri }); - if (exist) { - uriPersonCache.set(uri, exist); - return exist; + if (user != null) { + await uriPersonCache.set(uri, user); + return user; } //#endregion @@ -396,9 +396,9 @@ export async function updatePerson( } //#region Already registered on this server? - const exist = (await Users.findOneBy({ uri })) as IRemoteUser; + const user = (await Users.findOneBy({ uri })) as IRemoteUser; - if (exist == null) { + if (user == null) { return; } //#endregion @@ -416,17 +416,15 @@ export async function updatePerson( [person.icon, person.image].map((img) => img == null ? Promise.resolve(null) - : resolveImage(exist, img).catch(() => null), + : resolveImage(user, img).catch(() => null), ), ); // Custom pictogram acquisition - const emojis = await extractEmojis(person.tag || [], exist.host).catch( - (e) => { - logger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; - }, - ); + const emojis = await extractEmojis(person.tag || [], user.host).catch((e) => { + logger.info(`extractEmojis: ${e}`); + return [] as Emoji[]; + }); const emojiNames = emojis.map((emoji) => emoji.name); @@ -518,11 +516,11 @@ export async function updatePerson( } // Update user - await Users.update(exist.id, updates); + await Users.update(user.id, updates); if (person.publicKey) { await UserPublickeys.update( - { userId: exist.id }, + { userId: user.id }, { keyId: person.publicKey.id, keyPem: person.publicKey.publicKeyPem, @@ -531,7 +529,7 @@ export async function updatePerson( } await UserProfiles.update( - { userId: exist.id }, + { userId: user.id }, { url: url, fields, @@ -543,15 +541,15 @@ export async function updatePerson( }, ); - publishInternalEvent("remoteUserUpdated", { id: exist.id }); + publishInternalEvent("remoteUserUpdated", { id: user.id }); // Hashtag Update - updateUsertags(exist, tags); + updateUsertags(user, tags); // If the user in question is a follower, followers will also be updated. await Followings.update( { - followerId: exist.id, + followerId: user.id, }, { followerSharedInbox: @@ -560,7 +558,7 @@ export async function updatePerson( }, ); - await updateFeatured(exist.id, resolver).catch((err) => logger.error(err)); + await updateFeatured(user.id, resolver).catch((err) => logger.error(err)); } /** @@ -576,10 +574,10 @@ export async function resolvePerson( if (typeof uri !== "string") throw new Error("uri is not string"); //#region If already registered on this server, return it. - const exist = await fetchPerson(uri); + const user = await fetchPerson(uri); - if (exist) { - return exist; + if (user != null) { + return user; } //#endregion diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts index 42274ad2a..460a0ce84 100644 --- a/packages/backend/src/server/api/authenticate.ts +++ b/packages/backend/src/server/api/authenticate.ts @@ -9,7 +9,7 @@ import { localUserByNativeTokenCache, } from "@/services/user-cache.js"; -const appCache = new Cache(Infinity); +const appCache = new Cache("app", 60 * 30); export class AuthenticationError extends Error { constructor(message: string) { @@ -49,6 +49,7 @@ export default async ( const user = await localUserByNativeTokenCache.fetch( token, () => Users.findOneBy({ token }) as Promise, + true, ); if (user == null) { @@ -82,11 +83,14 @@ export default async ( Users.findOneBy({ id: accessToken.userId, }) as Promise, + true, ); if (accessToken.appId) { - const app = await appCache.fetch(accessToken.appId, () => - Apps.findOneByOrFail({ id: accessToken.appId! }), + const app = await appCache.fetch( + accessToken.appId, + () => Apps.findOneByOrFail({ id: accessToken.appId! }), + true, ); return [ diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index a532b6677..754cc6c89 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -47,6 +47,16 @@ export const meta = { optional: false, nullable: true, }, + showPopup: { + type: "boolean", + optional: true, + nullable: false, + }, + isGoodNews: { + type: "boolean", + optional: true, + nullable: false, + }, }, }, } as const; @@ -57,6 +67,8 @@ export const paramDef = { title: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 }, imageUrl: { type: "string", nullable: true, minLength: 1 }, + showPopup: { type: "boolean" }, + isGoodNews: { type: "boolean" }, }, required: ["title", "text", "imageUrl"], } as const; @@ -69,6 +81,8 @@ export default define(meta, paramDef, async (ps) => { title: ps.title, text: ps.text, imageUrl: ps.imageUrl, + showPopup: ps.showPopup ?? false, + isGoodNews: ps.isGoodNews ?? false, }).then((x) => Announcements.findOneByOrFail(x.identifiers[0])); return Object.assign({}, announcement, { diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index fc5b02070..e96517c68 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -57,6 +57,16 @@ export const meta = { optional: false, nullable: false, }, + showPopup: { + type: "boolean", + optional: true, + nullable: false, + }, + isGoodNews: { + type: "boolean", + optional: true, + nullable: false, + }, }, }, }, @@ -100,5 +110,7 @@ export default define(meta, paramDef, async (ps) => { text: announcement.text, imageUrl: announcement.imageUrl, reads: reads.get(announcement)!, + showPopup: announcement.showPopup, + isGoodNews: announcement.isGoodNews, })); }); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 35e64f281..616b94d69 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -24,6 +24,8 @@ export const paramDef = { title: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 }, imageUrl: { type: "string", nullable: true, minLength: 1 }, + showPopup: { type: "boolean" }, + isGoodNews: { type: "boolean" }, }, required: ["id", "title", "text", "imageUrl"], } as const; @@ -38,5 +40,7 @@ export default define(meta, paramDef, async (ps, me) => { title: ps.title, text: ps.text, imageUrl: ps.imageUrl, + showPopup: ps.showPopup ?? false, + isGoodNews: ps.isGoodNews ?? false, }); }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 7d4081613..4366406ec 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -6,7 +6,7 @@ import { ApiError } from "../../../error.js"; import rndstr from "rndstr"; import { publishBroadcastStream } from "@/services/stream.js"; import { db } from "@/db/postgre.js"; -import { type Size, getEmojiSize } from "@/misc/emoji-meta.js"; +import { getEmojiSize } from "@/misc/emoji-meta.js"; export const meta = { tags: ["admin"], @@ -40,12 +40,7 @@ export default define(meta, paramDef, async (ps, me) => { ? file.name.split(".")[0] : `_${rndstr("a-z0-9", 8)}_`; - let size: Size = { width: 0, height: 0 }; - try { - size = await getEmojiSize(file.url); - } catch { - /* skip if any error happens */ - } + const size = await getEmojiSize(file.url); const emoji = await Emojis.insert({ id: genId(), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 45cb9464d..c90e60633 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -6,7 +6,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js"; import { uploadFromUrl } from "@/services/drive/upload-from-url.js"; import { publishBroadcastStream } from "@/services/stream.js"; import { db } from "@/db/postgre.js"; -import { type Size, getEmojiSize } from "@/misc/emoji-meta.js"; +import { getEmojiSize } from "@/misc/emoji-meta.js"; export const meta = { tags: ["admin"], @@ -65,12 +65,7 @@ export default define(meta, paramDef, async (ps, me) => { throw new ApiError(); } - let size: Size = { width: 0, height: 0 }; - try { - size = await getEmojiSize(driveFile.url); - } catch { - /* skip if any error happens */ - } + const size = await getEmojiSize(driveFile.url); const copied = await Emojis.insert({ id: genId(), diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 319330127..9abb57b1b 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -481,6 +481,21 @@ export const meta = { }, }, }, + enableServerMachineStats: { + type: "boolean", + optional: false, + nullable: false, + }, + enableIdenticonGeneration: { + type: "boolean", + optional: false, + nullable: false, + }, + donationLink: { + type: "string", + optional: true, + nullable: true, + }, }, }, } as const; @@ -592,5 +607,8 @@ export default define(meta, paramDef, async (ps, me) => { enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, experimentalFeatures: instance.experimentalFeatures, + enableServerMachineStats: instance.enableServerMachineStats, + enableIdenticonGeneration: instance.enableIdenticonGeneration, + donationLink: instance.donationLink, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index a6d1f3519..00244a777 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -40,9 +40,9 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const exist = await PromoNotes.findOneBy({ noteId: note.id }); + const exist = await PromoNotes.exist({ where: { noteId: note.id } }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyPromoted); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index cf22c6c48..2142c7df7 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,6 +1,5 @@ import { Meta } from "@/models/entities/meta.js"; import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { DB_MAX_NOTE_TEXT_LENGTH } from "@/misc/hard-limits.js"; import { db } from "@/db/postgre.js"; import define from "../../define.js"; @@ -177,6 +176,9 @@ export const paramDef = { postImports: { type: "boolean" }, }, }, + enableServerMachineStats: { type: "boolean" }, + enableIdenticonGeneration: { type: "boolean" }, + donationLink: { type: "string", nullable: true }, }, required: [], } as const; @@ -218,6 +220,15 @@ export default define(meta, paramDef, async (ps, me) => { if (Array.isArray(ps.recommendedInstances)) { set.recommendedInstances = ps.recommendedInstances.filter(Boolean); + if (set.recommendedInstances?.length > 0) { + set.recommendedInstances.forEach((instance, index) => { + if (/^https?:\/\//i.test(instance)) { + set.recommendedInstances![index] = instance + .replace(/^https?:\/\//i, "") + .replace(/\/$/, ""); + } + }); + } } if (Array.isArray(ps.hiddenTags)) { @@ -568,6 +579,21 @@ export default define(meta, paramDef, async (ps, me) => { set.experimentalFeatures = ps.experimentalFeatures || undefined; } + if (ps.enableServerMachineStats !== undefined) { + set.enableServerMachineStats = ps.enableServerMachineStats; + } + + if (ps.enableIdenticonGeneration !== undefined) { + set.enableIdenticonGeneration = ps.enableIdenticonGeneration; + } + + if (ps.donationLink !== undefined) { + set.donationLink = ps.donationLink; + if (set.donationLink && !/^https?:\/\//i.test(set.donationLink)) { + set.donationLink = `https://${set.donationLink}`; + } + } + await db.transaction(async (transactionalEntityManager) => { const metas = await transactionalEntityManager.find(Meta, { order: { diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index 00634cc42..1bab61ba2 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -56,6 +56,16 @@ export const meta = { optional: true, nullable: false, }, + showPopup: { + type: "boolean", + optional: false, + nullable: false, + }, + isGoodNews: { + type: "boolean", + optional: false, + nullable: false, + }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index f69501ae2..c2117d51a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -29,6 +29,11 @@ export const meta = { code: "TOO_MANY_ANTENNAS", id: "c3a5a51e-04d4-11ee-be56-0242ac120002", }, + noKeywords: { + message: "No keywords", + code: "NO_KEYWORDS", + id: "aa975b74-1ddb-11ee-be56-0242ac120002", + }, }, res: { @@ -100,6 +105,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { if (user.movedToUri != null) throw new ApiError(meta.errors.noSuchUserGroup); + if (ps.keywords.length === 0) throw new ApiError(meta.errors.noKeywords); let userList; let userGroupJoining; diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 3dd168d71..c25f0a801 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -10,7 +10,7 @@ import type { Note } from "@/models/entities/note.js"; import type { CacheableLocalUser, User } from "@/models/entities/user.js"; import { isActor, isPost, getApId } from "@/remote/activitypub/type.js"; import type { SchemaType } from "@/misc/schema.js"; -import { HOUR } from "@/const.js"; +import { MINUTE } from "@/const.js"; import { shouldBlockInstance } from "@/misc/should-block-instance.js"; import { updateQuestion } from "@/remote/activitypub/models/question.js"; import { populatePoll } from "@/models/repositories/note.js"; @@ -22,8 +22,8 @@ export const meta = { requireCredential: true, limit: { - duration: HOUR, - max: 30, + duration: MINUTE, + max: 10, }, errors: { diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 35565e256..8e6ad6527 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -41,12 +41,14 @@ export default define(meta, paramDef, async (ps, user) => { const accessToken = secureRndstr(32, true); // Fetch exist access token - const exist = await AccessTokens.findOneBy({ - appId: session.appId, - userId: user.id, + const exist = await AccessTokens.exist({ + where: { + appId: session.appId, + userId: user.id, + }, }); - if (exist == null) { + if (!exist) { // Lookup app const app = await Apps.findOneByOrFail({ id: session.appId }); diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 4bd58d5ef..f00a2923d 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already blocking - const exist = await Blockings.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, + const exist = await Blockings.exist({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyBlocking); } diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index 6c4ca2775..037d5af22 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not blocking - const exist = await Blockings.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, + const exist = await Blockings.exist({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + }, }); - if (exist == null) { + if (!exist) { throw new ApiError(meta.errors.notBlocking); } diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index b9d6b54c9..416af6faa 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -57,12 +57,14 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const exist = await ClipNotes.findOneBy({ - noteId: note.id, - clipId: clip.id, + const exist = await ClipNotes.exist({ + where: { + noteId: note.id, + clipId: clip.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyClipped); } diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index df8968520..e26da30eb 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -26,10 +26,12 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ - md5: ps.md5, - userId: user.id, + const exist = await DriveFiles.exist({ + where: { + md5: ps.md5, + userId: user.id, + }, }); - return file != null; + return exist; }); diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index e617c1ffb..48ae6ae7a 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -82,12 +82,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already following - const exist = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const exist = await Followings.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyFollowing); } diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 2eebe8a90..cbc6097f4 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not following - const exist = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const exist = await Followings.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, }); - if (exist == null) { + if (!exist) { throw new ApiError(meta.errors.notFollowing); } diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 979d298f7..01ccc2761 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -69,12 +69,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not following - const exist = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const exist = await Followings.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, }); - if (exist == null) { + if (!exist) { throw new ApiError(meta.errors.notFollowing); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index fd46406bd..2506e40aa 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -40,12 +40,14 @@ export default define(meta, paramDef, async (ps, user) => { } // if already liked - const exist = await GalleryLikes.findOneBy({ - postId: post.id, - userId: user.id, + const exist = await GalleryLikes.exist({ + where: { + postId: post.id, + userId: user.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyLiked); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index 772dc9202..03bc299b9 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -38,17 +38,17 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchPost); } - const exist = await GalleryLikes.findOneBy({ + const like = await GalleryLikes.findOneBy({ postId: post.id, userId: user.id, }); - if (exist == null) { + if (like == null) { throw new ApiError(meta.errors.notLiked); } // Delete like - await GalleryLikes.delete(exist.id); + await GalleryLikes.delete(like.id); GalleryPosts.decrement({ id: post.id }, "likedCount", 1); }); diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 5218dba87..d0dfa6657 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -30,19 +30,23 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // Check if announcement exists - const announcement = await Announcements.findOneBy({ id: ps.announcementId }); + const exist = await Announcements.exist({ + where: { id: ps.announcementId }, + }); - if (announcement == null) { + if (!exist) { throw new ApiError(meta.errors.noSuchAnnouncement); } // Check if already read - const read = await AnnouncementReads.findOneBy({ - announcementId: ps.announcementId, - userId: user.id, + const read = await AnnouncementReads.exist({ + where: { + announcementId: ps.announcementId, + userId: user.id, + }, }); - if (read != null) { + if (read) { return; } diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts index f98c6c929..a9bcf6935 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts @@ -33,7 +33,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - if (ps.key !== "reactions") return; + if (ps.key !== "reactions" && ps.key !== "defaultNoteVisibility") return; const query = RegistryItems.createQueryBuilder("item") .where("item.domain IS NULL") .andWhere("item.userId = :userId", { userId: user.id }) diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index 308442bf7..3a410fa0e 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -17,9 +17,9 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const token = await AccessTokens.findOneBy({ id: ps.tokenId }); + const exist = await AccessTokens.exist({ where: { id: ps.tokenId } }); - if (token) { + if (exist) { await AccessTokens.delete({ id: ps.tokenId, userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 552ff0195..84a698801 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -389,6 +389,11 @@ export const meta = { nullable: false, default: "⭐", }, + donationLink: { + type: "string", + optional: "true", + nullable: true, + }, }, }, } as const; @@ -491,6 +496,7 @@ export default define(meta, paramDef, async (ps, me) => { translatorAvailable: instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null, defaultReaction: instance.defaultReaction, + donationLink: instance.donationLink, ...(ps.detail ? { diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index bacab9b45..7b2f10905 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -64,12 +64,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already muting - const exist = await Mutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, + const exist = await Mutings.exist({ + where: { + muterId: muter.id, + muteeId: mutee.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyMuting); } diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index cc67a44c2..cd00c1a8a 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -56,18 +56,18 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not muting - const exist = await Mutings.findOneBy({ + const muting = await Mutings.findOneBy({ muterId: muter.id, muteeId: mutee.id, }); - if (exist == null) { + if (muting == null) { throw new ApiError(meta.errors.notMuting); } // Delete mute await Mutings.delete({ - id: exist.id, + id: muting.id, }); publishUserEvent(user.id, "unmute", mutee); diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 9047fcce1..a35b17a02 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,4 +1,3 @@ -import { Brackets } from "typeorm"; import { Notes } from "@/models/index.js"; import define from "../../define.js"; import { makePaginationQuery } from "../../common/make-pagination-query.js"; @@ -11,6 +10,7 @@ export const meta = { requireCredential: false, requireCredentialPrivateMode: true, + description: "Get threaded/chained replies to a note", res: { type: "array", @@ -23,13 +23,14 @@ export const meta = { ref: "Note", }, }, -}; +} as const; export const paramDef = { type: "object", properties: { noteId: { type: "string", format: "misskey:id" }, limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, + depth: { type: "integer", minimum: 1, maximum: 100, default: 12 }, sinceId: { type: "string", format: "misskey:id" }, untilId: { type: "string", format: "misskey:id" }, }, diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 2e8f5ef73..c74da2ec7 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -9,6 +9,7 @@ export const meta = { requireCredential: false, requireCredentialPrivateMode: true, + description: "Get conversation of a note thread/chain by a reply", res: { type: "array", @@ -34,7 +35,11 @@ export const meta = { export const paramDef = { type: "object", properties: { - noteId: { type: "string", format: "misskey:id" }, + noteId: { + type: "string", + format: "misskey:id", + description: "Should be a reply", + }, limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, offset: { type: "integer", default: 0 }, }, @@ -51,7 +56,7 @@ export default define(meta, paramDef, async (ps, user) => { const conversation: Note[] = []; let i = 0; - async function get(id: any) { + async function get(id: string) { i++; const p = await getNote(id, user).catch((e) => { if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") return null; @@ -60,7 +65,7 @@ export default define(meta, paramDef, async (ps, user) => { if (p == null) return; - if (i > ps.offset!) { + if (i > ps.offset) { conversation.push(p); } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 41b8ab979..e15221046 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -224,11 +224,13 @@ export default define(meta, paramDef, async (ps, user) => { // Check blocking if (renote.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: renote.userId, - blockeeId: user.id, + const isBlocked = await Blockings.exist({ + where: { + blockerId: renote.userId, + blockeeId: user.id, + }, }); - if (block) { + if (isBlocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } @@ -249,11 +251,13 @@ export default define(meta, paramDef, async (ps, user) => { // Check blocking if (reply.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: reply.userId, - blockeeId: user.id, + const isBlocked = await Blockings.exist({ + where: { + blockerId: reply.userId, + blockeeId: user.id, + }, }); - if (block) { + if (isBlocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 835594f03..64862a373 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -43,12 +43,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, + const exist = await NoteFavorites.exist({ + where: { + noteId: note.id, + userId: user.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyFavorited); } diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 9a0976748..e05d04a96 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -42,15 +42,15 @@ export default define(meta, paramDef, async (ps, user) => { }); // if already favorited - const exist = await NoteFavorites.findOneBy({ + const favorite = await NoteFavorites.findOneBy({ noteId: note.id, userId: user.id, }); - if (exist == null) { + if (favorite == null) { throw new ApiError(meta.errors.notFavorited); } // Delete favorite - await NoteFavorites.delete(exist.id); + await NoteFavorites.delete(favorite.id); }); diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 39d128134..8c5f91c5c 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -21,6 +21,7 @@ export const meta = { message: "No such note.", code: "NO_SUCH_NOTE", id: "24fcbfc6-2e37-42b6-8388-c29b3861a08d", + httpStatusCode: 404, }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index f14ed39eb..03482c961 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -40,12 +40,14 @@ export default define(meta, paramDef, async (ps, user) => { } // if already liked - const exist = await PageLikes.findOneBy({ - pageId: page.id, - userId: user.id, + const exist = await PageLikes.exist({ + where: { + pageId: page.id, + userId: user.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyLiked); } diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 07bf3fbf4..e607d7a54 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -38,17 +38,17 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchPage); } - const exist = await PageLikes.findOneBy({ + const like = await PageLikes.findOneBy({ pageId: page.id, userId: user.id, }); - if (exist == null) { + if (like == null) { throw new ApiError(meta.errors.notLiked); } // Delete like - await PageLikes.delete(exist.id); + await PageLikes.delete(like.id); Pages.decrement({ id: page.id }, "likedCount", 1); }); diff --git a/packages/backend/src/server/api/endpoints/patrons.ts b/packages/backend/src/server/api/endpoints/patrons.ts index 63c4b78a2..8ca1f8902 100644 --- a/packages/backend/src/server/api/endpoints/patrons.ts +++ b/packages/backend/src/server/api/endpoints/patrons.ts @@ -1,9 +1,15 @@ import define from "../define.js"; import { redisClient } from "@/db/redis.js"; +import * as fs from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); export const meta = { tags: ["meta"], - description: "Get list of Firefish patrons from Codeberg", + description: "Get Calckey patrons", requireCredential: false, requireCredentialPrivateMode: false, @@ -24,21 +30,29 @@ export default define(meta, paramDef, async (ps) => { patrons = JSON.parse(cachedPatrons); } else { AbortSignal.timeout ??= function timeout(ms) { - const ctrl = new AbortController() - setTimeout(() => ctrl.abort(), ms) - return ctrl.signal - } + const ctrl = new AbortController(); + setTimeout(() => ctrl.abort(), ms); + return ctrl.signal; + }; patrons = await fetch( - "https://codeberg.org/firefish/firefish/raw/branch/develop/patrons.json", - { signal: AbortSignal.timeout(2000) } + "https://codeberg.org/calckey/calckey/raw/branch/develop/patrons.json", + { signal: AbortSignal.timeout(2000) }, ) .then((response) => response.json()) .catch(() => { - patrons = cachedPatrons ? JSON.parse(cachedPatrons) : []; + const staticPatrons = JSON.parse( + fs.readFileSync( + `${_dirname}/../../../../../../patrons.json`, + "utf-8", + ), + ); + patrons = cachedPatrons ? JSON.parse(cachedPatrons) : staticPatrons; }); await redisClient.set("patrons", JSON.stringify(patrons), "EX", 3600); } - - return patrons["patrons"]; + return { + patrons: patrons["patrons"], + sponsors: patrons["sponsors"], + }; }); diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 09c8cb6fa..5310382a4 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -33,12 +33,14 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const exist = await PromoReads.findOneBy({ - noteId: note.id, - userId: user.id, + const exist = await PromoReads.exist({ + where: { + noteId: note.id, + userId: user.id, + }, }); - if (exist != null) { + if (exist) { return; } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 857cbd975..f09f197c0 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -47,12 +47,14 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already muting - const exist = await RenoteMutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, + const exist = await RenoteMutings.exist({ + where: { + muterId: muter.id, + muteeId: mutee.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyMuting); } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index fb4c972af..7a898141c 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -45,18 +45,18 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not muting - const exist = await RenoteMutings.findOneBy({ + const muting = await RenoteMutings.findOneBy({ muterId: muter.id, muteeId: mutee.id, }); - if (exist == null) { + if (muting == null) { throw new ApiError(meta.errors.notMuting); } // Delete mute await RenoteMutings.delete({ - id: exist.id, + id: muting.id, }); // publishUserEvent(user.id, "unmute", mutee); diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 81bb053db..87132758f 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -2,11 +2,13 @@ import * as os from "node:os"; import si from "systeminformation"; import define from "../define.js"; import meilisearch from "@/db/meilisearch.js"; +import { fetchMeta } from "@/misc/fetch-meta.js"; export const meta = { requireCredential: false, requireCredentialPrivateMode: true, - + allowGet: true, + cacheSec: 30, tags: ["meta"], } as const; @@ -29,6 +31,23 @@ export default define(meta, paramDef, async () => { } } + const instanceMeta = await fetchMeta(); + if (!instanceMeta.enableServerMachineStats) { + return { + machine: "Not specified", + cpu: { + model: "Not specified", + cores: 0, + }, + mem: { + total: 0, + }, + fs: { + total: 0, + used: 0, + }, + }; + } return { machine: os.hostname(), cpu: { diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 41be4bf74..6268ae26d 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -57,8 +57,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - // if already subscribed - const exist = await SwSubscriptions.findOneBy({ + const subscription = await SwSubscriptions.findOneBy({ userId: me.id, endpoint: ps.endpoint, auth: ps.auth, @@ -67,13 +66,14 @@ export default define(meta, paramDef, async (ps, me) => { const instance = await fetchMeta(true); - if (exist != null) { + // if already subscribed + if (subscription != null) { return { state: "already-subscribed" as const, key: instance.swPublicKey, userId: me.id, - endpoint: exist.endpoint, - sendReadMessage: exist.sendReadMessage, + endpoint: subscription.endpoint, + sendReadMessage: subscription.sendReadMessage, }; } diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts index c7a9609cf..3ccb7de94 100644 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -42,16 +42,16 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, me) => { - const exist = await SwSubscriptions.findOneBy({ + const subscription = await SwSubscriptions.findOneBy({ userId: me.id, endpoint: ps.endpoint, }); - if (exist != null) { + if (subscription != null) { return { - userId: exist.userId, - endpoint: exist.endpoint, - sendReadMessage: exist.sendReadMessage, + userId: subscription.userId, + endpoint: subscription.endpoint, + sendReadMessage: subscription.sendReadMessage, }; } diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 855ab3573..31719bad3 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -98,11 +98,13 @@ export default define(meta, paramDef, async (ps, me) => { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const following = await Followings.findOneBy({ - followeeId: user.id, - followerId: me.id, + const isFollowed = await Followings.exist({ + where: { + followeeId: user.id, + followerId: me.id, + }, }); - if (following == null) { + if (!isFollowed) { throw new ApiError(meta.errors.nullFollowers); } } diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 37d69b048..1c1da0e11 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -97,11 +97,13 @@ export default define(meta, paramDef, async (ps, me) => { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const following = await Followings.findOneBy({ - followeeId: user.id, - followerId: me.id, + const isFollowing = await Followings.exist({ + where: { + followeeId: user.id, + followerId: me.id, + }, }); - if (following == null) { + if (!isFollowing) { throw new ApiError(meta.errors.cannot_find); } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index a14195bbc..6f3e09fcb 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -52,12 +52,14 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: me.id, + const listExists = await UserLists.exist({ + where: { + id: ps.listId, + userId: me.id, + }, }); - if (userList == null) { + if (!listExists) { throw new ApiError(meta.errors.noSuchList); } @@ -70,18 +72,22 @@ export default define(meta, paramDef, async (ps, me) => { // Check blocking if (user.id !== me.id) { - const block = await Blockings.findOneBy({ - blockerId: user.id, - blockeeId: me.id, + const isBlocked = await Blockings.exist({ + where: { + blockerId: user.id, + blockeeId: me.id, + }, }); - if (block) { + if (isBlocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } - const exist = await UserListJoinings.findOneBy({ - userListId: userList.id, - userId: user.id, + const exist = await UserListJoinings.exist({ + where: { + userListId: userList.id, + userId: user.id, + }, }); if (exist) { diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 716fd405d..8479a4e03 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -37,12 +37,14 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: me.id, + const exist = await UserLists.exist({ + where: { + id: ps.listId, + userId: me.id, + }, }); - if (userList == null) { + if (!exist) { throw new ApiError(meta.errors.noSuchList); } diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 17b7a04a0..6b6d32e8a 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -49,7 +49,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId }); - if (me == null || (me.id !== ps.userId && !profile.publicReactions)) { + if (me.id !== ps.userId && !profile.publicReactions) { throw new ApiError(meta.errors.reactionsNotPublic); } diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index 29cfbf93c..9e8c45886 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -112,7 +112,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => { ctx.status = 401; return; } - const data = await client.uploadMedia(multipartData); + const data = await client.uploadMedia(multipartData, ctx.request.body); ctx.body = convertAttachment(data.data as Entity.Attachment); } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts index 6b74c57ea..f3fbd4bad 100644 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts @@ -1,5 +1,5 @@ import Router from "@koa/router"; -import megalodon, { MegalodonInterface } from "@firefish/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import { apiAuthMastodon } from "./endpoints/auth.js"; import { apiAccountMastodon } from "./endpoints/account.js"; import { apiStatusMastodon } from "./endpoints/status.js"; @@ -18,11 +18,7 @@ export function getClient( const accessTokenArr = authorization?.split(" ") ?? [null]; const accessToken = accessTokenArr[accessTokenArr.length - 1]; const generator = (megalodon as any).default; - const client = generator( - "misskey", - BASE_URL, - accessToken, - ) as MegalodonInterface; + const client = generator(BASE_URL, accessToken) as MegalodonInterface; return client; } diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 7c56aed7c..cbaf5287f 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -1,9 +1,11 @@ -import { Entity } from "@firefish/megalodon"; +import { Entity } from "megalodon"; import { convertId, IdType } from "../index.js"; function simpleConvert(data: any) { - data.id = convertId(data.id, IdType.MastodonId); - return data; + // copy the object to bypass weird pass by reference bugs + const result = Object.assign({}, data); + result.id = convertId(data.id, IdType.MastodonId); + return result; } export function convertAccount(account: Entity.Account) { @@ -21,6 +23,9 @@ export function convertFilter(filter: Entity.Filter) { export function convertList(list: Entity.List) { return simpleConvert(list); } +export function convertFeaturedTag(tag: Entity.FeaturedTag) { + return simpleConvert(tag); +} export function convertNotification(notification: Entity.Notification) { notification.account = convertAccount(notification.account); diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 208662318..a4fc74dab 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -7,6 +7,7 @@ import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js"; import { convertId, IdType } from "../../index.js"; import { convertAccount, + convertFeaturedTag, convertList, convertRelationship, convertStatus, @@ -42,12 +43,12 @@ export function apiAccountMastodon(router: Router): void { acct.url = `${BASE_URL}/@${acct.url}`; acct.note = acct.note || ""; acct.avatar_static = acct.avatar; - acct.header = acct.header || "https://http.cat/404"; - acct.header_static = acct.header || "https://http.cat/404"; + acct.header = acct.header || "/static-assets/transparent.png"; + acct.header_static = acct.header || "/static-assets/transparent.png"; acct.source = { note: acct.note, fields: acct.fields, - privacy: "public", + privacy: await client.getDefaultPostPrivacy(), sensitive: false, language: "", }; @@ -164,6 +165,25 @@ export function apiAccountMastodon(router: Router): void { } }, ); + router.get<{ Params: { id: string } }>( + "/v1/accounts/:id/featured_tags", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getAccountFeaturedTags( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = data.data.map((tag) => convertFeaturedTag(tag)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); router.get<{ Params: { id: string } }>( "/v1/accounts/:id/followers", async (ctx) => { @@ -342,6 +362,34 @@ export function apiAccountMastodon(router: Router): void { } }, ); + router.get("/v1/featured_tags", async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getFeaturedTags(); + ctx.body = data.data.map((tag) => convertFeaturedTag(tag)); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); + router.get("/v1/followed_tags", async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getFollowedTags(); + ctx.body = data.data; + } catch (e: any) { + console.error(e); + console.error(e.response.data); + ctx.status = 401; + ctx.body = e.response.data; + } + }); router.get("/v1/bookmarks", async (ctx) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts index 70c374292..b55cb6388 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts @@ -1,4 +1,4 @@ -import megalodon, { MegalodonInterface } from "@firefish/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { koaBody } from "koa-body"; import { getClient } from "../ApiMastodonCompatibleService.js"; diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts index d4ae2c328..e27b7e22a 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -1,4 +1,4 @@ -import megalodon, { MegalodonInterface } from "@firefish/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; import { IdType, convertId } from "../../index.js"; diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index 02a37bf6c..27cc14afd 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -1,4 +1,5 @@ -import { Entity } from "@firefish/megalodon"; +import { Entity } from "megalodon"; +import config from "@/config/index.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import { Users, Notes } from "@/models/index.js"; import { IsNull, MoreThan } from "typeorm"; @@ -17,14 +18,14 @@ export async function getInstance(response: Entity.Instance) { response.description || "This is a vanilla Firefish Instance. It doesn't seem to have a description.", email: response.email || "", - version: "3.0.0 compatible (3.5+ Firefish)", //I hope this version string is correct, we will need to test it. + version: `3.0.0 (compatible; Calckey ${config.version})`, urls: response.urls, stats: { user_count: await totalUsers, status_count: await totalStatuses, domain_count: response.stats.domain_count, }, - thumbnail: response.thumbnail || "https://http.cat/404", + thumbnail: response.thumbnail || "/static-assets/transparent.png", languages: meta.langs, registrations: !meta.disableRegistration || response.registrations, approval_required: !response.registrations, @@ -96,8 +97,8 @@ export async function getInstance(response: Entity.Instance) { url: `${response.uri}/`, avatar: `${response.uri}/static-assets/badges/info.png`, avatar_static: `${response.uri}/static-assets/badges/info.png`, - header: "https://http.cat/404", - header_static: "https://http.cat/404", + header: "/static-assets/transparent.png", + header_static: "/static-assets/transparent.png", followers_count: -1, following_count: 0, statuses_count: 0, diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 62371227b..f0a0bab98 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -1,9 +1,9 @@ -import megalodon, { MegalodonInterface } from "@firefish/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { koaBody } from "koa-body"; import { convertId, IdType } from "../../index.js"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js"; +import { convertTimelinesArgsId } from "./timeline.js"; import { convertNotification } from "../converters.js"; function toLimitToInt(q: any) { if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10); @@ -25,10 +25,6 @@ export function apiNotificationsMastodon(router: Router): void { n = convertNotification(n); if (n.type !== "follow" && n.type !== "follow_request") { if (n.type === "reaction") n.type = "favourite"; - n.status = toTextWithReaction( - n.status ? [n.status] : [], - ctx.hostname, - )[0]; return n; } else { return n; @@ -52,11 +48,13 @@ export function apiNotificationsMastodon(router: Router): void { convertId(ctx.params.id, IdType.FirefishId), ); const data = convertNotification(dataRaw.data); - if (data.type !== "follow" && data.type !== "follow_request") { - if (data.type === "reaction") data.type = "favourite"; - ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0]; - } else { - ctx.body = data; + ctx.body = data; + if ( + data.type !== "follow" && + data.type !== "follow_request" && + data.type === "reaction" + ) { + data.type = "favourite"; } } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index dca367195..8a4817557 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -1,8 +1,8 @@ -import megalodon, { MegalodonInterface } from "@firefish/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; import axios from "axios"; -import { Converter } from "@firefish/megalodon"; +import { Converter } from "megalodon"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; import { convertAccount, convertStatus } from "../converters.js"; @@ -30,21 +30,26 @@ export function apiSearchMastodon(router: Router): void { try { const query: any = convertTimelinesArgsId(limitToInt(ctx.query)); const type = query.type; - if (type) { - const data = await client.search(query.q, type, query); - ctx.body = data.data.accounts.map((account) => convertAccount(account)); - } else { - const acct = await client.search(query.q, "accounts", query); - const stat = await client.search(query.q, "statuses", query); - const tags = await client.search(query.q, "hashtags", query); - ctx.body = { - accounts: acct.data.accounts.map((account) => - convertAccount(account), - ), - statuses: stat.data.statuses.map((status) => convertStatus(status)), - hashtags: tags.data.hashtags, - }; - } + const acct = + !type || type === "accounts" + ? await client.search(query.q, "accounts", query) + : null; + const stat = + !type || type === "statuses" + ? await client.search(query.q, "statuses", query) + : null; + const tags = + !type || type === "hashtags" + ? await client.search(query.q, "hashtags", query) + : null; + + ctx.body = { + accounts: + acct?.data?.accounts.map((account) => convertAccount(account)) ?? [], + statuses: + stat?.data?.statuses.map((status) => convertStatus(status)) ?? [], + hashtags: tags?.data?.hashtags ?? [], + }; } catch (e: any) { console.error(e); ctx.status = 401; @@ -103,7 +108,7 @@ async function getHighlight( i: accessToken, }); const data: MisskeyEntity.Note[] = api.data; - return data.map((note) => Converter.note(note, domain)); + return data.map((note) => new Converter(BASE_URL).note(note, domain)); } catch (e: any) { console.log(e); console.log(e.response.data); @@ -131,7 +136,7 @@ async function getFeaturedUser( return data.map((u) => { return { source: "past_interactions", - account: Converter.userDetail(u, host), + account: new Converter(BASE_URL).userDetail(u, host), }; }); } catch (e: any) { diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 52841f689..caff75c55 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -59,9 +59,33 @@ export function apiStatusMastodon(router: Router): void { } if (!body.media_ids) body.media_ids = undefined; if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; + if (body.media_ids) { + body.media_ids = (body.media_ids as string[]).map((p) => + convertId(p, IdType.CalckeyId), + ); + } const { sensitive } = body; body.sensitive = typeof sensitive === "string" ? sensitive === "true" : sensitive; + + if (body.poll) { + if ( + body.poll.expires_in != null && + typeof body.poll.expires_in === "string" + ) + body.poll.expires_in = parseInt(body.poll.expires_in); + if ( + body.poll.multiple != null && + typeof body.poll.multiple === "string" + ) + body.poll.multiple = body.poll.multiple == "true"; + if ( + body.poll.hide_totals != null && + typeof body.poll.hide_totals === "string" + ) + body.poll.hide_totals = body.poll.hide_totals == "true"; + } + const data = await client.postStatus(text, body); ctx.body = convertStatus(data.data); } catch (e: any) { @@ -81,7 +105,7 @@ export function apiStatusMastodon(router: Router): void { ctx.body = convertStatus(data.data); } catch (e: any) { console.error(e); - ctx.status = 401; + ctx.status = ctx.status == 404 ? 404 : 401; ctx.body = e.response.data; } }); @@ -118,27 +142,7 @@ export function apiStatusMastodon(router: Router): void { id, convertTimelinesArgsId(limitToInt(ctx.query as any)), ); - const status = await client.getStatus(id); - let reqInstance = axios.create({ - headers: { - Authorization: ctx.headers.authorization, - }, - }); - const reactionsAxios = await reqInstance.get( - `${BASE_URL}/api/notes/reactions?noteId=${id}`, - ); - const reactions: IReaction[] = reactionsAxios.data; - const text = reactions - .map((r) => `${r.type.replace("@.", "")} ${r.user.username}`) - .join("
"); - data.data.descendants.unshift( - statusModel( - status.data.id, - status.data.account.id, - status.data.emojis, - text, - ), - ); + data.data.ancestors = data.data.ancestors.map((status) => convertStatus(status), ); @@ -153,6 +157,24 @@ export function apiStatusMastodon(router: Router): void { } }, ); + router.get<{ Params: { id: string } }>( + "/v1/statuses/:id/history", + async (ctx) => { + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getStatusHistory( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = data.data.map((account) => convertAccount(account)); + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } + }, + ); router.get<{ Params: { id: string } }>( "/v1/statuses/:id/reblogged_by", async (ctx) => { @@ -174,7 +196,19 @@ export function apiStatusMastodon(router: Router): void { router.get<{ Params: { id: string } }>( "/v1/statuses/:id/favourited_by", async (ctx) => { - ctx.body = []; + const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; + const accessTokens = ctx.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const data = await client.getStatusFavouritedBy( + convertId(ctx.params.id, IdType.CalckeyId), + ); + ctx.body = data.data.map((account) => convertAccount(account)); + } catch (e: any) { + console.error(e); + ctx.status = 401; + ctx.body = e.response.data; + } }, ); router.post<{ Params: { id: string } }>( @@ -421,65 +455,3 @@ async function getFirstReaction( return react; } } - -export function statusModel( - id: string | null, - acctId: string | null, - emojis: MastodonEntity.Emoji[], - content: string, -) { - const now = new Date().toISOString(); - return { - id: "9atm5frjhb", - uri: "https://http.cat/404", // "" - url: "https://http.cat/404", // "", - account: { - id: "9arzuvv0sw", - username: "Reactions", - acct: "Reactions", - display_name: "Reactions to this post", - locked: false, - created_at: now, - followers_count: 0, - following_count: 0, - statuses_count: 0, - note: "", - url: "https://http.cat/404", - avatar: "/static-assets/badges/info.png", - avatar_static: "/static-assets/badges/info.png", - header: "https://http.cat/404", // "" - header_static: "https://http.cat/404", // "" - emojis: [], - fields: [], - moved: null, - bot: false, - }, - in_reply_to_id: id, - in_reply_to_account_id: acctId, - reblog: null, - content: `

${content}

`, - plain_content: null, - created_at: now, - emojis: emojis, - replies_count: 0, - reblogs_count: 0, - favourites_count: 0, - favourited: false, - reblogged: false, - muted: false, - sensitive: false, - spoiler_text: "", - visibility: "public" as const, - media_attachments: [], - mentions: [], - tags: [], - card: null, - poll: null, - application: null, - language: null, - pinned: false, - emoji_reactions: [], - bookmarked: false, - quote: null, - }; -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 04cdadd5b..4b49d8acc 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -1,8 +1,5 @@ import Router from "@koa/router"; -import megalodon, { Entity, MegalodonInterface } from "@firefish/megalodon"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { statusModel } from "./status.js"; -import Autolinker from "autolinker"; import { ParsedUrlQuery } from "querystring"; import { convertAccount, convertList, convertStatus } from "../converters.js"; import { convertId, IdType } from "../../index.js"; @@ -41,66 +38,6 @@ export function convertTimelinesArgsId(q: ParsedUrlQuery) { return q; } -export function toTextWithReaction(status: Entity.Status[], host: string) { - return status.map((t) => { - if (!t) return statusModel(null, null, [], "no content"); - t.quote = null as any; - if (!t.emoji_reactions) return t; - if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0]; - const reactions = t.emoji_reactions.map((r) => { - const emojiNotation = r.url ? `:${r.name.replace("@.", "")}:` : r.name; - return `${emojiNotation} (${r.count}${r.me ? `* ` : ""})`; - }); - const reaction = t.emoji_reactions as Entity.Reaction[]; - const emoji = t.emojis || []; - for (const r of reaction) { - if (!r.url) continue; - emoji.push({ - shortcode: r.name, - url: r.url, - static_url: r.url, - visible_in_picker: true, - category: "", - }); - } - const isMe = reaction.findIndex((r) => r.me) > -1; - const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0); - t.favourited = isMe; - t.favourites_count = total; - t.emojis = emoji; - t.content = `

${autoLinker(t.content, host)}

${reactions.join( - ", ", - )}

`; - return t; - }); -} -export function autoLinker(input: string, host: string) { - return Autolinker.link(input, { - hashtag: "twitter", - mention: "twitter", - email: false, - stripPrefix: false, - replaceFn: function (match) { - switch (match.type) { - case "url": - return true; - case "mention": - console.log("Mention: ", match.getMention()); - console.log("Mention Service Name: ", match.getServiceName()); - return `@${match.getMention()}`; - case "hashtag": - console.log("Hashtag: ", match.getHashtag()); - return `#${match.getHashtag()}`; - } - return false; - }, - }); -} - export function apiTimelineMastodon(router: Router): void { router.get("/v1/timelines/public", async (ctx, reply) => { const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; @@ -108,15 +45,15 @@ export function apiTimelineMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const query: any = ctx.query; - const data = query.local - ? await client.getLocalTimeline( - convertTimelinesArgsId(argsToBools(limitToInt(query))), - ) - : await client.getPublicTimeline( - convertTimelinesArgsId(argsToBools(limitToInt(query))), - ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + const data = + query.local === "true" + ? await client.getLocalTimeline( + convertTimelinesArgsId(argsToBools(limitToInt(query))), + ) + : await client.getPublicTimeline( + convertTimelinesArgsId(argsToBools(limitToInt(query))), + ); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -135,8 +72,7 @@ export function apiTimelineMastodon(router: Router): void { ctx.params.hashtag, convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))), ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -153,8 +89,7 @@ export function apiTimelineMastodon(router: Router): void { const data = await client.getHomeTimeline( convertTimelinesArgsId(limitToInt(ctx.query)), ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -173,8 +108,7 @@ export function apiTimelineMastodon(router: Router): void { convertId(ctx.params.listId, IdType.FirefishId), convertTimelinesArgsId(limitToInt(ctx.query)), ); - let resp = data.data.map((status) => convertStatus(status)); - ctx.body = toTextWithReaction(resp, ctx.hostname); + ctx.body = data.data.map((status) => convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); diff --git a/packages/backend/src/server/api/service/discord.ts b/packages/backend/src/server/api/service/discord.ts index 9906d2f7c..848a70d4a 100644 --- a/packages/backend/src/server/api/service/discord.ts +++ b/packages/backend/src/server/api/service/discord.ts @@ -18,7 +18,7 @@ function getUserToken(ctx: Koa.BaseContext): string | null { function compareOrigin(ctx: Koa.BaseContext): boolean { function normalizeUrl(url?: string): string { - return url ? (url.endsWith("/") ? url.substr(0, url.length - 1) : url) : ""; + return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : ""; } const referer = ctx.headers["referer"]; diff --git a/packages/backend/src/server/api/service/github.ts b/packages/backend/src/server/api/service/github.ts index f77c5f795..fd015fb8a 100644 --- a/packages/backend/src/server/api/service/github.ts +++ b/packages/backend/src/server/api/service/github.ts @@ -18,7 +18,7 @@ function getUserToken(ctx: Koa.BaseContext): string | null { function compareOrigin(ctx: Koa.BaseContext): boolean { function normalizeUrl(url?: string): string { - return url ? (url.endsWith("/") ? url.substr(0, url.length - 1) : url) : ""; + return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : ""; } const referer = ctx.headers["referer"]; diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 0b52f6912..d14031950 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -22,11 +22,13 @@ export default class extends Channel { this.listId = params.listId as string; // Check existence and owner - const list = await UserLists.findOneBy({ - id: this.listId, - userId: this.user!.id, + const exist = await UserLists.exist({ + where: { + id: this.listId, + userId: this.user!.id, + }, }); - if (!list) return; + if (!exist) return; // Subscribe stream this.subscriber.on(`userListStream:${this.listId}`, this.send); diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 09f9b03a9..72dce1951 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -25,9 +25,8 @@ import { readNotification } from "../common/read-notification.js"; import channels from "./channels/index.js"; import type Channel from "./channel.js"; import type { StreamEventEmitter, StreamMessages } from "./types.js"; -import { Converter } from "@firefish/megalodon"; +import { Converter } from "megalodon"; import { getClient } from "../mastodon/ApiMastodonCompatibleService.js"; -import { toTextWithReaction } from "../mastodon/endpoints/timeline.js"; /** * Main stream connection @@ -248,7 +247,7 @@ export default class Connection { for (const obj of objs) { const { type, body } = obj; - console.log(type, body); + // console.log(type, body); switch (type) { case "readNotification": this.onReadNotification(body); @@ -400,12 +399,7 @@ export default class Connection { JSON.stringify({ stream: [payload.id], event: "update", - payload: JSON.stringify( - toTextWithReaction( - [Converter.note(payload.body, this.host)], - this.host, - )[0], - ), + payload: JSON.stringify(Converter.note(payload.body, this.host)), }), ); this.onSubscribeNote({ @@ -415,7 +409,7 @@ export default class Connection { // reaction const client = getClient(this.host, this.accessToken); client.getStatus(payload.id).then((data) => { - const newPost = toTextWithReaction([data.data], this.host); + const newPost = [data.data]; const targetPost = newPost[0]; for (const stream of this.currentSubscribe) { this.wsConnection.send( @@ -442,10 +436,6 @@ export default class Connection { if (payload.id === "user") { const body = Converter.notification(payload.body, this.host); if (body.type === "reaction") body.type = "favourite"; - body.status = toTextWithReaction( - body.status ? [body.status] : [], - "", - )[0]; this.wsConnection.send( JSON.stringify({ stream: ["user"], diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 346cd1890..efff6dd23 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -16,12 +16,13 @@ import { IsNull } from "typeorm"; import config from "@/config/index.js"; import Logger from "@/services/logger.js"; import { UserProfiles, Users } from "@/models/index.js"; +import { fetchMeta } from "@/misc/fetch-meta.js"; import { genIdenticon } from "@/misc/gen-identicon.js"; import { createTemp } from "@/misc/create-temp.js"; import { publishMainStream } from "@/services/stream.js"; import * as Acct from "@/misc/acct.js"; import { envOption } from "@/env.js"; -import megalodon, { MegalodonInterface } from "@firefish/megalodon"; +import megalodon, { MegalodonInterface } from "megalodon"; import activityPub from "./activitypub.js"; import nodeinfo from "./nodeinfo.js"; import wellKnown from "./well-known.js"; @@ -125,10 +126,15 @@ router.get("/avatar/@:acct", async (ctx) => { }); router.get("/identicon/:x", async (ctx) => { - const [temp, cleanup] = await createTemp(); - await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); - ctx.set("Content-Type", "image/png"); - ctx.body = fs.createReadStream(temp).on("close", () => cleanup()); + const meta = await fetchMeta(); + if (meta.enableIdenticonGeneration) { + const [temp, cleanup] = await createTemp(); + await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); + ctx.set("Content-Type", "image/png"); + ctx.body = fs.createReadStream(temp).on("close", () => cleanup()); + } else { + ctx.redirect("/static-assets/avatar.png"); + } }); mastoRouter.get("/oauth/authorize", async (ctx) => { @@ -160,7 +166,7 @@ mastoRouter.post("/oauth/token", async (ctx) => { let client_id: any = body.client_id; const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const generator = (megalodon as any).default; - const client = generator("misskey", BASE_URL, null) as MegalodonInterface; + const client = generator(BASE_URL, null) as MegalodonInterface; let m = null; let token = null; if (body.code) { diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index b4eb166f7..595d0a101 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -100,7 +100,10 @@ const nodeinfo2 = async () => { }; }; -const cache = new Cache>>(1000 * 60 * 10); +const cache = new Cache>>( + "nodeinfo", + 60 * 10, +); router.get(nodeinfo2_1path, async (ctx) => { const base = await cache.fetch(null, () => nodeinfo2()); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 580334945..c8efc4824 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -102,7 +102,11 @@ localStorage.setItem("fontSize", null); fontSize = localStorage.getItem("fontSize"); } - document.documentElement.style.fontSize = fontSize + "px"; + document.documentElement.style.fontSize = `${fontSize}px`; + } + + if (["ja-JP", "ja-KS", "ko-KR", "zh-CN", "zh-TW"].includes(lang)) { + document.documentElement.classList.add("useCJKFont"); } const useSystemFont = localStorage.getItem("useSystemFont"); @@ -123,7 +127,7 @@ } async function addStyle(styleText) { - let css = document.createElement("style"); + const css = document.createElement("style"); css.appendChild(document.createTextNode(styleText)); document.head.appendChild(css); } diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts index a9a2734b5..50e6bfc32 100644 --- a/packages/backend/src/server/web/feed.ts +++ b/packages/backend/src/server/web/feed.ts @@ -4,22 +4,39 @@ import config from "@/config/index.js"; import type { User } from "@/models/entities/user.js"; import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js"; -export default async function (user: User) { +export default async function ( + user: User, + threadDepth = 5, + history = 20, + noteintitle = false, + renotes = true, + replies = true, +) { const author = { link: `${config.url}/@${user.username}`, + email: `${user.username}@${config.host}`, name: user.name || user.username, }; const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + const searchCriteria = { + userId: user.id, + visibility: In(["public", "home"]), + }; + + if (!renotes) { + searchCriteria.renoteId = IsNull(); + } + + if (!replies) { + searchCriteria.replyId = IsNull(); + } + const notes = await Notes.find({ - where: { - userId: user.id, - renoteId: IsNull(), - visibility: In(["public", "home"]), - }, + where: searchCriteria, order: { createdAt: -1 }, - take: 20, + take: history, }); const feed = new Feed({ @@ -43,22 +60,105 @@ export default async function (user: User) { }); for (const note of notes) { + let contentStr = await noteToString(note, true); + let next = note.renoteId ? note.renoteId : note.replyId; + let depth = threadDepth; + while (depth > 0 && next) { + const finding = await findById(next); + contentStr += finding.text; + next = finding.next; + depth -= 1; + } + + let title = `${author.name} `; + if (note.renoteId) { + title += "renotes"; + } else if (note.replyId) { + title += "replies"; + } else { + title += "says"; + } + if (noteintitle) { + const content = note.cw ?? note.text; + if (content) { + title += `: ${content}`; + } else { + title += "something"; + } + } + + feed.addItem({ + title: title + .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + .substring(0, 100), + link: `${config.url}/notes/${note.id}`, + date: note.createdAt, + description: note.cw + ? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + : undefined, + content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""), + }); + } + + async function noteToString(note, isTheNote = false) { + const author = isTheNote + ? null + : await Users.findOneBy({ id: note.userId }); + let outstr = author + ? `${author.name}(@${author.username}@${ + author.host ? author.host : config.host + }) ${ + note.renoteId ? "renotes" : note.replyId ? "replies" : "says" + }:
` + : ""; const files = note.fileIds.length > 0 ? await DriveFiles.findBy({ id: In(note.fileIds), }) : []; - const file = files.find((file) => file.type.startsWith("image/")); + let fileEle = ""; + for (const file of files) { + if (file.type.startsWith("image/")) { + fileEle += `
`; + } else if (file.type.startsWith("audio/")) { + fileEle += `