diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 208ccb1598..a269a0d008 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1601,6 +1601,8 @@ _aboutMisskey: morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí. Gràcies! 🥰 patrons: Mecenes de Calckey + patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació + amb l'enllaç de dalt per veure el teu nom aquí! unknown: Desconegut pageLikesCount: Nombre de pàgines amb M'agrada youAreRunningUpToDateClient: Estás fent servir la versió del client més nova. @@ -2082,11 +2084,7 @@ _experiments: alpha: Alfa beta: Beta release: Publicà - enablePostEditing: Activà l'edició de publicacions title: Experiments - postEditingCaption: Mostra l'opció perquè els usuaris editin les seves publicacions - mitjançant el menú d'opcions de publicació, i permet rebre publicacions editades - d'altres servidors. enablePostImports: Activar l'importació de publicacions postImportsCaption: Permet els usuaris importar publicacions desde comptes a Calckey, Misskey, Mastodon, Akkoma i Pleroma. Pot fer que el servidor vagi més lent durant diff --git a/locales/de-DE.yml b/locales/de-DE.yml index d37869064d..9e3a07654f 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -2095,10 +2095,7 @@ jumpToPrevious: Zum Vorherigen springen silencedWarning: Diese Meldung wird angezeigt, weil diese Nutzer von Servern stammen, die Ihr Administrator abgeschaltet hat, so dass es sich möglicherweise um Spam handelt. _experiments: - enablePostEditing: Beitragsbearbeitung ermöglichen title: Funktionstests - postEditingCaption: Zeigt die Option für Nutzer an, ihre bestehenden Beiträge über - das Menü "Beitragsoptionen" zu bearbeiten enablePostImports: Beitragsimporte aktivieren postImportsCaption: Erlaubt es Nutzer:innen ihre Posts von alten Calckey, Misskey, Mastodon, Akkoma und Pleroma Accounts zu importieren. Bei Engpässen in der Warteschlange @@ -2152,3 +2149,4 @@ clipsDesc: Clips sind wie teilbare, kategorisierte Lesezeichen. Du kannst Clips Menü individueller Posts aus erstellen. channelFederationWarn: Kanäle föderieren noch nicht zu anderen Servern reactionPickerSkinTone: Bevorzugte Emoji-Hautfarbe +swipeOnMobile: Wischen zwischen den Seiten erlauben diff --git a/locales/en-US.yml b/locales/en-US.yml index cc327c5aa5..d493f332f5 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1213,6 +1213,7 @@ _aboutMisskey: morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" 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" ignore: "Don't hide NSFW media" @@ -2069,9 +2070,6 @@ _deck: direct: "Direct messages" _experiments: title: "Experiments" - enablePostEditing: "Enable post editing" - postEditingCaption: "Shows the option for users to edit their existing posts via\ - \ the post options menu, and allows post edits from other instances to be recieved." enablePostImports: "Enable post imports" postImportsCaption: "Allows users to import their posts from past Calckey,\ \ Misskey, Mastodon, Akkoma, and Pleroma accounts. It may cause slowdowns during\ diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 5a61870c4e..5291d16932 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2028,7 +2028,7 @@ flagShowTimelineRepliesDescription: Si activé, affiche dans le fil les réponse _experiments: alpha: Alpha beta: Beta - enablePostEditing: Autoriser l'édition de note + enablePostImports: Autoriser l'importation de messages title: Expérimentations findOtherInstance: Trouver un autre serveur userSaysSomethingReasonQuote: '{name} a cité une note contenant {reason}' diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 21f616808b..fa3c3f1cc1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1887,9 +1887,7 @@ hiddenTagsDescription: 'トレンドと「みつける」から除外したい hiddenTags: 非表示にするハッシュタグ apps: "アプリ" _experiments: - enablePostEditing: 投稿の編集機能を有効にする title: 試験的な機能 - postEditingCaption: 投稿のメニューに既存の投稿を編集するボタンを表示し、他サーバーの編集も受信できるようにします。 postImportsCaption: ユーザーが過去の投稿をCalckey・Misskey・Mastodon・Akkoma・Pleromaからインポートすることを許可します。キューが溜まっているときにインポートするとサーバーに負荷がかかる可能性があります。 enablePostImports: 投稿のインポートを有効にする diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index a6c9ec5a1e..6870571be8 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1,7 +1,7 @@ ---- _lang_: "简体中文" headlineMisskey: "通过帖子连接在一起的网络" -introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧!🚀" +introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n\ + 来探索新的世界吧!🚀" monthAndDay: "{month}月 {day}日" search: "搜索" notifications: "通知" @@ -14,9 +14,9 @@ gotIt: "我明白了" cancel: "取消" enterUsername: "输入用户名" renotedBy: "由 {user} 转贴" -noNotes: "没有帖文" +noNotes: "没有帖子" noNotifications: "无通知" -instance: "实例" +instance: "服务器" settings: "设置" basicSettings: "基本设置" otherSettings: "其他设置" @@ -64,7 +64,7 @@ import: "导入" export: "导出" files: "文件" download: "下载" -driveFileDeleteConfirm: "要删除「{name}」文件吗?附加此文件的帖子也会被删除。" +driveFileDeleteConfirm: "要删除文件「{name}」吗?它将被所有作为附件包含它的帖子中删除。" unfollowConfirm: "要取消对{name}的关注吗?" exportRequested: "导出请求已提交,这可能需要花一些时间,导出的文件将保存到网盘中。" importRequested: "导入请求已提交,这可能需要花一点时间。" @@ -166,7 +166,7 @@ selectUser: "选择用户" recipient: "收件人" annotation: "注解" federation: "联合" -instances: "实例" +instances: "服务器" registeredAt: "初次观测" latestRequestSentAt: "上次发送的请求" latestRequestReceivedAt: "上次收到的请求" @@ -186,14 +186,14 @@ jobQueue: "作业队列" cpuAndMemory: "CPU和内存" network: "网络" disk: "存储" -instanceInfo: "实例信息" +instanceInfo: "服务器信息" statistics: "统计" clearQueue: "清除队列" clearQueueConfirmTitle: "确定清除队列?" clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。" clearCachedFiles: "清除缓存" clearCachedFilesConfirm: "确定要清除缓存文件?" -blockedInstances: "被阻拦的实例" +blockedInstances: "已屏蔽的服务器" blockedInstancesDescription: "设定要阻拦的实例,以换行来进行分割。被阻拦的实例将无法与本实例进行交换通讯。" muteAndBlock: "屏蔽/拉黑" mutedUsers: "已屏蔽用户" @@ -309,8 +309,8 @@ unwatch: "取消关注" accept: "允许" reject: "拒绝" normal: "正常" -instanceName: "实例名称" -instanceDescription: "实例介绍" +instanceName: "服务器名称" +instanceDescription: "服务器简介" maintainerName: "管理员名称" maintainerEmail: "管理员电子邮箱" tosUrl: "服务条款URL" @@ -321,7 +321,7 @@ dayX: "{day}日" monthX: "{month}月" yearX: "{year}年" pages: "页面" -integration: "关联" +integration: "整合" connectService: "连接" disconnectService: "断开连接" enableLocalTimeline: "启用本地时间线功能" @@ -600,7 +600,7 @@ testEmail: "邮件发送测试" wordMute: "文字屏蔽" regexpError: "正则表达式错误" regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:" -instanceMute: "实例的屏蔽" +instanceMute: "服务器静音" userSaysSomething: "{name}说了什么" makeActive: "启用" display: "显示" @@ -643,7 +643,7 @@ instanceTicker: "帖子的实例信息" waitingFor: "等待{x}" random: "随机" system: "系统" -switchUi: "切换界面" +switchUi: "界面" desktop: "桌面" clip: "便签" createNew: "新建" @@ -701,7 +701,7 @@ showTitlebar: "显示标题栏" clearCache: "清除缓存" onlineUsersCount: "{n}人在线" nUsers: "{n}用户" -nNotes: "{n}帖子" +nNotes: "{n} 帖子" sendErrorReports: "发送错误报告" sendErrorReportsDescription: "启用后,如果出现问题,可以与Misskey共享详细的错误信息,从而帮助提高软件的质量。" myTheme: "我的主题" @@ -755,7 +755,7 @@ active: "活动" offline: "离线" notRecommended: "不推荐" botProtection: "Bot防御" -instanceBlocking: "被阻拦的实例" +instanceBlocking: "联邦管理" selectAccount: "选择账户" switchAccount: "切换账户" enabled: "已启用" @@ -816,7 +816,7 @@ controlPanel: "控制面板" manageAccounts: "管理账户" makeReactionsPublic: "将回应设置为公开" makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。" -classic: "经典" +classic: "居中" muteThread: "屏蔽帖子列表" unmuteThread: "取消屏蔽帖子列表" ffVisibility: "连接的可见范围" @@ -896,7 +896,10 @@ shuffle: "随机" account: "账户" move: "移动" customKaTeXMacro: "自定义 KaTeX 宏" -customKaTeXMacroDescription: "使用宏来轻松的输入数学表达式吧!宏的用法与 LaTeX 中的命令定义相同。你可以使用 \\newcommand{\\name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 来输入数学表达式。举个例子,\\newcommand{\\add}[2]{#1 + #2} 会将 \\add{3}{foo} 展开为 3 + foo。此外,宏名称外的花括号 {} 可以被替换为圆括号 () 和方括号 [],这会影响用于参数的括号。每行只能够定义一个宏,无法在中间换行,且无效的行将被忽略。只支持简单字符串替换功能,不支持高级语法,如条件分支等。" +customKaTeXMacroDescription: "使用宏来轻松的输入数学表达式吧!宏的用法与 LaTeX 中的命令定义相同。你可以使用 \\newcommand{\\ + name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 来输入数学表达式。举个例子,\\ + newcommand{\\add}[2]{#1 + #2} 会将 \\add{3}{foo} 展开为 3 + foo。此外,宏名称外的花括号 {} 可以被替换为圆括号 + () 和方括号 [],这会影响用于参数的括号。每行只能够定义一个宏,无法在中间换行,且无效的行将被忽略。只支持简单字符串替换功能,不支持高级语法,如条件分支等。" enableCustomKaTeXMacro: "启用自定义 KaTeX 宏" _sensitiveMediaDetection: description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" @@ -981,6 +984,7 @@ _aboutMisskey: donate: "赞助Misskey" morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰" patrons: "支持者" + patronsList: 按时间顺序而不是捐赠金额排列。通过上面的链接捐款,让您的名字出现在这里! _nsfw: respect: "隐藏敏感内容" ignore: "不隐藏敏感内容" @@ -1051,6 +1055,24 @@ _mfm: rotateDescription: "旋转指定的角度。" plain: "简洁" plainDescription: "禁用所有内部语法。" + crop: 裁剪 + scale: 缩放 + position: 位置 + fade: 渐淡 + advanced: 高级 MFM + background: 背景色 + fadeDescription: 内容淡入和淡出。 + warn: MFM 可能包含快速移动或华丽的动画 + advancedDescription: 如果禁用,则仅允许基本标记,除非正在播放动态 MFM + foreground: 前景色 + backgroundDescription: 更改文本的背景色。 + play: 播放 MFM + alwaysPlay: 始终自动播放所有动态的 MFM + stop: 停止播放 MFM + positionDescription: 将内容移动指定的量。 + cropDescription: 裁剪内容。 + scaleDescription: 按指定量缩放内容。 + foregroundDescription: 更改文本的前景色。 _instanceTicker: none: "不显示" remote: "仅远程用户" @@ -1059,6 +1081,7 @@ _serverDisconnectedBehavior: reload: "自动重载" dialog: "对话框警告" quiet: "安静警告" + nothing: 什么也不做 _channel: create: "创建频道" edit: "编辑频道" @@ -1068,7 +1091,7 @@ _channel: owned: "管理中" following: "正在关注" usersCount: "有{n}人参与" - notesCount: "有{n}个帖子" + notesCount: "{n} 帖子" nameAndDescription: "名称与描述" nameOnly: "仅名称" _menuDisplay: @@ -1084,7 +1107,7 @@ _wordMute: hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。" soft: "软屏蔽" hard: "硬屏蔽" - mutedNotes: "被屏蔽的帖子" + mutedNotes: "已静音的帖子" _instanceMute: instanceMuteDescription: "屏蔽配置实例中的所有帖子和转帖,包括实例的用户回复。" instanceMuteDescription2: "设置时用换行符来分隔" @@ -1165,7 +1188,7 @@ _theme: accentLighten: "强调色(浅)" fgHighlighted: "高亮显示文本" _sfx: - note: "帖子" + note: "新的帖子" noteMy: "我的帖子" notification: "通知" chat: "聊天" @@ -1178,7 +1201,7 @@ _ago: secondsAgo: "{n}秒前" minutesAgo: "{n}分前" hoursAgo: "{n}小时前" - daysAgo: "{n}日前" + daysAgo: "{n}天前" weeksAgo: "{n}周前" monthsAgo: "{n}月前" yearsAgo: "{n}年前" @@ -1218,6 +1241,21 @@ _2fa: step3: "输入您的应用提供的动态口令以完成设置。" step4: "从现在开始,任何登录操作都将要求您提供动态口令。" securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。" + renewTOTPOk: 重新配置 + renewTOTPCancel: 取消 + token: 2FA 令牌 + renewTOTP: 重新配置身份验证器应用程序 + registerTOTPBeforeKey: 请设置一个认证器应用来注册一个安全或通行密钥。 + renewTOTPConfirm: 这将导致您之前的应用程序中的验证码停止工作 + step3Title: 输入验证码 + step2Click: 点击此二维码将允许您在安全密钥或手机验证器应用中注册 2FA。 + securityKeyNotSupported: 您的浏览器不支持安全密钥。 + securityKeyName: 输入密钥名称 + chromePasskeyNotSupported: 目前不支持 Chrome passkeys。 + tapSecurityKey: 请按照您的浏览器的指示注册安全或通行密钥 + removeKey: 移除安全密钥 + removeKeyConfirm: 真的要删除 {name} 密钥吗? + whyTOTPOnlyRenew: 只要注册了安全密钥,就无法删除身份验证器应用程序。 _permissions: "read:account": "查看账户信息" "write:account": "更改帐户信息" @@ -1258,12 +1296,15 @@ _auth: pleaseGoBack: "请返回到应用程序" callback: "回到应用程序" denied: "拒绝访问" + allPermissions: 完全的账户访问权限 + copyAsk: 请将以下授权码粘贴到应用程序中: _antennaSources: all: "所有帖子" homeTimeline: "已关注用户的帖子" users: "来自指定用户的帖子" userList: "来自指定列表中的帖子" userGroup: "来自指定群组中用户的帖子" + instances: 服务器上所有用户的帖子 _weekday: sunday: "星期日" monday: "星期一" @@ -1280,21 +1321,26 @@ _widgets: trends: "趋势" clock: "时钟" rss: "RSS阅读器" - rssTicker: "RSS Ticker" + rssTicker: "RSS滚动条" activity: "活动" photos: "照片" digitalClock: "数字时钟" unixClock: "UNIX时钟" federation: "联邦宇宙" - instanceCloud: "实例云" - postForm: "投稿窗口" + instanceCloud: "服务器云端" + postForm: "发布窗口" slideshow: "幻灯片展示" button: "按钮" onlineUsers: "在线用户" jobQueue: "作业队列" - serverMetric: "服务器监控" + serverMetric: "服务器指标" aiscript: "AiScript控制台" aichan: "小蓝" + userList: 用户列表 + meiliStatus: 服务器状态 + meiliIndexCount: 已索引的帖子 + meiliSize: 索引大小 + serverInfo: 服务器信息 _cw: hide: "隐藏" show: "查看更多" @@ -1325,7 +1371,7 @@ _poll: _visibility: public: "公开" publicDescription: "您的帖子将出现在全局时间线上" - home: "首页" + home: "不公开" homeDescription: "仅发送至首页的时间线" followers: "仅关注者" followersDescription: "仅发送至关注者" @@ -1395,6 +1441,7 @@ _timelines: local: "本地" social: "社交" global: "全局" + recommended: 推荐 _pages: newPage: "创建页面" editPage: "编辑页面" @@ -1733,9 +1780,9 @@ _deck: stackLeft: "向左折叠" popRight: "向右弹出" profile: "配置文件" - newProfile: "新建配置文件" + newProfile: "新建工作区" renameProfile: "重命名配置文件" - deleteProfile: "删除配置文件" + deleteProfile: "删除工作区" nameAlreadyExists: "该配置文件名已存在。" introduction: "将各列进行组合以创建您自己的界面!" introduction2: "您可以随时通过屏幕右侧的 + 来添加列" @@ -1748,4 +1795,134 @@ _deck: antenna: "天线" list: "列表" mentions: "提及" - direct: "指定用户" + direct: "私信" + channel: 频道 +apps: 应用 +_messaging: + dms: 私信 + groups: 群组 +migration: 迁移 +_experiments: + title: 实验性功能 +license: 许可证 +flagSpeakAsCatDescription: 在猫模式下你的帖子会喵化 +allowedInstances: 白名单服务器 +listsDesc: 列表可以让你创建含有指定用户的时间线,它们可以从时间线页面访问。 +flagSpeakAsCat: 像猫一样说话 +removeReaction: 移除你的回应 +expandOnNoteClick: 点击打开帖子 +expandOnNoteClickDesc: 如果禁用,你仍然可以在右键菜单中或通过点击时间戳打开帖子。 +sendPushNotificationReadMessage: 删除已阅读的推送通知 +customMOTD: 自定义 MOTD(闪屏消息) +sendPushNotificationReadMessageCaption: 短暂显示 "{emptyPushNotificationMessage}" 的通知,如果启用,可能会增加你的设备的耗电量。 +adminCustomCssWarn: 仅当你知道此设置的作用时才应使用它。输入不正确的值可能会导致每个人的客户端停止正常运行。请在用户设置中进行测试来确保您的 CSS + 正常工作。 +customMOTDDescription: 自定义MOTD(闪屏)消息,一行一个,每次用户加载/刷新页面时都会随机显示。 +customSplashIconsDescription: 用换行符隔开的自定义闪屏图标的URL,在用户每次加载/重新加载页面时随机显示。请确保图片是在一个静态的 + URL 上,最好全部调整为 192x192 的大小。 +recommendedInstancesDescription: 推荐的服务器以换行符分隔,它们将出现在推荐的时间线中。不要添加 "https://",仅添加域名。 +splash: 启动画面 +showUpdates: Calckey 更新后显示弹出窗口 +selectInstance: 选择一个服务器 +silencedInstances: 静默的服务器 +antennaInstancesDescription: 每行列出一个服务器主机 +pushNotification: 推送通知 +subscribePushNotification: 启用推送通知 +showAdminUpdates: 提示新的 Calckey 版本可用(仅对于管理员) +searchPlaceholder: 搜索 Calckey +addInstance: 添加一个服务器 +jumpToPrevious: 跳转至上一个 +silenceThisInstance: 使此服务器静音 +manageGroups: 管理群组 +antennasDesc: "天线会显示符合您设置条件的新帖子!\n可以从时间线页面访问它们。" +channelFederationWarn: 频道还没有与其他服务器联合 +seperateRenoteQuote: 单独的推荐和引用按钮 +customSplashIcons: 自定义闪屏图标(urls) +alt: 替代文字 +pushNotificationNotSupported: 你的浏览器或者服务器不支持推送通知 +showAds: 显示广告 +enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Retun) +recommendedInstances: 推荐服务器 +updateAvailable: 可能有可用更新! +swipeOnMobile: 允许在页面之间滑动 +swipeOnDesktop: 允许在桌面端以移动设备方式滑动 +logoImageUrl: Logo 图像 URL +deleted: 已删除 +editNote: 编辑帖子 +edited: 于 {date} {time} 编辑 +selectChannel: 选择一个频道 +accountMoved: 用户已迁移至新账户: +silencedInstancesDescription: 列出你想静默的服务器的主机名。列出的服务器中的账户被视为 "静默",只能发出跟随请求,如果不被跟随,就不能提及本地账户。这不会影响被封锁的服务器。 +hiddenTags: 隐藏的哈希标签 +userSaysSomethingReason: '{name} 说 {reason}' +clipsDesc: 便签就像可共享的分类书签。您可以从各个帖子的菜单中创建便签。 +privateModeInfo: 当启用时,只有白名单上的服务器可以与你的服务器联合,所有的帖子都会对公共时间线隐藏。 +allowedInstancesDescription: 要列入联合白名单的服务器的主机名,一行一个(仅适用于私密模式)。 +breakFollowConfirm: 你确定要移除关注者吗? +caption: 自动显示说明文字 +newer: 更新的 +older: 更老的 +noInstances: 没有服务器 +silenced: 静默的 +accessibility: 无障碍 +secureMode: 安全模式(仅允许授权的拉取) +replayTutorial: 重播教程 +userSaysSomethingReasonReply: '{name} 回复了包含 {reason} 的帖子' +userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的帖子' +userSaysSomethingReasonRenote: '{name} 推荐了一个包含 {reason} 的帖子' +noThankYou: 不,谢谢 +secureModeInfo: 当向其他服务器请求时,不要在没有验证的情况下发回。 +privateMode: 私密模式 +instanceSecurity: 服务器安全 +image: 图像 +video: 视频 +audio: 音频 +cannotUploadBecauseExceedsFileSizeLimit: 无法上传此文件,因为它超出了允许的最大大小。 +unsubscribePushNotification: 禁用推送通知 +pushNotificationAlreadySubscribed: 推送通知已启用 +enableEmojiReactions: 启用 emoji 回应 +cw: 内容警告 +hiddenTagsDescription: 列出你想隐藏的话题标签(不带#)以避免在趋势和探索中显示。隐藏的标签仍然可以通过其他方式被发现。 +enableRecommendedTimeline: 启用推荐时间线 +_skinTones: + medium: 中等 + light: 浅色 + yellow: 黄色 + dark: 深色 +isModerator: 协作者 +isAdmin: 管理员 +findOtherInstance: 寻找其它服务器 +moveFromDescription: 这将为您的旧帐户设置一个别名,以便您可以从该旧帐户转移到当前帐户。在从旧帐户转移之前执行此操作。请输入格式如@person@server.com + 的帐户标签 +indexPosts: 索引帖子 +signupsDisabled: 该服务器目前关闭注册,但您随时可以在另一台服务器上注册!如果您有该服务器的邀请码,请在下面输入。 +silencedWarning: 显示这个页面是因为这些用户来自你的管理员设置的静默服务器,所以他们有可能是垃圾信息。 +isBot: 这个账户是一个机器人 +moveAccountDescription: 这个过程是不可逆的。在移动之前,请确保您已在新帐户上为当前帐户设置了别名。请输入格式如 @person@server.com + 帐户标签 +moveFromLabel: 您要移出的旧帐户: +preventAiLearning: 阻止 AI 机器人抓取 +preventAiLearningDescription: 请求第三方人工智能语言模型不要研究您上传的内容,例如帖子和图像。 +noGraze: 请禁用 "Graze for Mastodon" 浏览器扩展,因为它会干扰 Calckey。 +moveTo: 将当前帐户移至新帐户 +moveToLabel: 你要迁移到的目标帐户: +moveAccount: 移动账户! +migrationConfirm: "你确实确定要将帐户迁移到 {account} 吗?此操作无法撤消,并且你将无法再次正常使用旧账户。\n另外,请确保你已将此当前帐户设置为要移出的帐户。" +indexFromDescription: 留空以索引每个帖子 +noteId: 帖子 ID +moveFrom: 从旧帐户移至此帐户 +defaultReaction: 发出和收到的帖子的默认表情符号反应 +indexNotice: 现在开始索引。这可能需要一段时间,请至少一个小时内不要重新启动服务器。 +indexFrom: 从帖子 ID 开始的索引 +sendModMail: 发送审核通知 +isLocked: 该帐户设置了关注请求 +_filters: + notesBefore: 在之前的帖子 + followingOnly: 仅关注中 + notesAfter: 在之后的帖子 + fromDomain: 来自域名 + withFile: 带有文件 + fromUser: 来自用户 + followersOnly: 仅关注者 +reactionPickerSkinTone: 首选的表情符号肤色 +isPatron: Calckey 赞助 diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index d373daf5d9..c615bcdcc5 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1816,7 +1816,6 @@ silenceThisInstance: 靜音此伺服器 silencedInstances: 已靜音的伺服器 silenced: 已靜音 _experiments: - enablePostEditing: 啟用帖子編輯 title: 試驗功能 findOtherInstance: 找找另一個伺服器 noGraze: 瀏覽器擴展 "Graze for Mastodon" 會與Calckey發生衝突,請停用該擴展。 diff --git a/packages/backend/native-utils/src/mastodon_api.rs b/packages/backend/native-utils/src/mastodon_api.rs index 57119ea73f..3016acd54f 100644 --- a/packages/backend/native-utils/src/mastodon_api.rs +++ b/packages/backend/native-utils/src/mastodon_api.rs @@ -13,18 +13,19 @@ 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 => { - let mut out: i64 = 0; + let mut out: i128 = 0; for (i, c) in in_id.to_lowercase().chars().rev().enumerate() { - out += num_from_char(c)? as i64 * 36_i64.pow(i as u32); + out += num_from_char(c)? as i128 * 36_i128.pow(i as u32); } Ok(out.to_string()) } CalckeyId => { - let mut input: i64 = match in_id.parse() { + let mut input: i128 = match in_id.parse() { Ok(s) => s, Err(_) => { return Err(Error::new( diff --git a/packages/backend/package.json b/packages/backend/package.json index f7d19d85b2..fd5b2c3b3d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -43,7 +43,6 @@ "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", @@ -112,6 +111,7 @@ "ratelimiter": "3.4.1", "re2": "1.19.0", "redis-lock": "0.1.4", + "redis-semaphore": "5.3.1", "reflect-metadata": "0.1.13", "rename": "1.0.4", "rndstr": "1.0.0", diff --git a/packages/backend/src/misc/emoji-meta.ts b/packages/backend/src/misc/emoji-meta.ts index fd9d9baa5c..3bcc4262bc 100644 --- a/packages/backend/src/misc/emoji-meta.ts +++ b/packages/backend/src/misc/emoji-meta.ts @@ -1,42 +1,48 @@ 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 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); + const lock = new Mutex(redisClient, "getEmojiSize"); + await lock.acquire(); + try { + const key = `getEmojiSize:${url}`; + attempted = (await redisClient.get(key)) !== null; if (!attempted) { - cache.set(url, true); - } else { - logger.warn(`Attempt limit exceeded: ${url}`); - throw new Error("Too many attempts"); + await redisClient.set(key, "done", "EX", 60 * 10); } - }); + } finally { + await lock.release(); + } + + if (attempted) { + logger.warn(`Attempt limit exceeded: ${url}`); + throw new Error("attempt limit exceeded"); + } try { - logger.info(`Retrieving emoji size from ${url}`); + logger.debug(`Retrieving emoji size from ${url}`); const { width, height, mime } = await probeImageSize(url, { timeout: 5000, }); if (!(mime.startsWith("image/") && FILE_TYPE_BROWSERSAFE.includes(mime))) { - throw new Error("Unsupported image type"); + throw new Error("unsupported image type"); } return { width, height }; } catch (e) { - throw new Error(`Unable to retrieve metadata: ${e}`); + throw new Error(`unable to retrieve metadata: ${e}`); } } diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index a84a446fe7..d7580a4f62 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -482,7 +482,8 @@ export function createCleanRemoteFilesJob() { export function createIndexAllNotesJob(data = {}) { return backgroundQueue.add("indexAllNotes", data, { removeOnComplete: true, - removeOnFail: true, + removeOnFail: false, + timeout: 1000 * 60 * 60 * 24, }); } 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 10c332aa3b..1dce4406a9 100644 --- a/packages/backend/src/queue/processors/background/index-all-notes.ts +++ b/packages/backend/src/queue/processors/background/index-all-notes.ts @@ -20,7 +20,7 @@ export default async function indexAllNotes( let total: number = (job.data.total as number) ?? 0; let running = true; - const take = 50000; + const take = 100000; const batch = 100; while (running) { logger.info( diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 26aa5bf544..a3141e388a 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -541,10 +541,6 @@ function notEmpty(partial: Partial) { export async function updateNote(value: string | IObject, resolver?: Resolver) { const uri = typeof value === "string" ? value : value.id; if (!uri) throw new Error("Missing note uri"); - const instanceMeta = await fetchMeta(); - if (instanceMeta.experimentalFeatures?.postEdits === false) { - throw new Error("Post edits disabled."); - } // Skip if URI points to this server if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local"); diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index ad70310633..3193301275 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -476,9 +476,6 @@ export const meta = { optional: true, nullable: true, properties: { - postEditing: { - type: "boolean", - }, postImports: { type: "boolean", }, 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 b967112a5c..cf22c6c489 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -174,7 +174,6 @@ export const paramDef = { type: "object", nullable: true, properties: { - postEditing: { type: "boolean" }, postImports: { type: "boolean" }, }, }, diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index f6c978b2de..673a0266c8 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -529,7 +529,7 @@ export default define(meta, paramDef, async (ps, me) => { github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, serviceWorker: instance.enableServiceWorker, - postEditing: instance.experimentalFeatures?.postEditing || false, + postEditing: true, postImports: instance.experimentalFeatures?.postImports || false, miauth: true, }; diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 14ce2faafc..70c5ceffb4 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -140,12 +140,6 @@ export const meta = { code: "NOT_LOCAL_USER", id: "b907f407-2aa0-4283-800b-a2c56290b822", }, - - editsDisabled: { - message: "Post edits are disabled.", - code: "EDITS_DISABLED", - id: "99306f00-fb81-11ed-be56-0242ac120002", - }, }, } as const; @@ -244,11 +238,6 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked); - const instanceMeta = await fetchMeta(); - if (instanceMeta.experimentalFeatures?.postEdits === false) { - throw new ApiError(meta.errors.editsDisabled); - } - if (!Users.isLocalUser(user)) { throw new ApiError(meta.errors.notLocalUser); } diff --git a/packages/backend/src/server/api/endpoints/patrons.ts b/packages/backend/src/server/api/endpoints/patrons.ts index ac914daa12..353cba1dc0 100644 --- a/packages/backend/src/server/api/endpoints/patrons.ts +++ b/packages/backend/src/server/api/endpoints/patrons.ts @@ -23,8 +23,15 @@ export default define(meta, paramDef, async (ps) => { if (!ps.forceUpdate && cachedPatrons) { patrons = JSON.parse(cachedPatrons); } else { + AbortSignal.timeout ??= function timeout(ms) { + const ctrl = new AbortController() + setTimeout(() => ctrl.abort(), ms) + return ctrl.signal + } + patrons = await fetch( "https://codeberg.org/calckey/calckey/raw/branch/develop/patrons.json", + { signal: AbortSignal.timeout(2000) } ) .then((response) => response.json()) .catch(() => { diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index 18e04f4209..dbfb28ff6a 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -83,7 +83,7 @@ const nodeinfo2 = async () => { disableGlobalTimeline: meta.disableGlobalTimeline, emailRequiredForSignup: meta.emailRequiredForSignup, searchFilters: config.meilisearch ? true : false, - postEditing: meta.experimentalFeatures?.postEditing || false, + postEditing: true, postImports: meta.experimentalFeatures?.postImports || false, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index defd9742e2..f00678ce22 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -69,6 +69,7 @@ import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { shouldSilenceInstance } from "@/misc/should-block-instance.js"; import meilisearch from "../../db/meilisearch.js"; import { redisClient } from "@/db/redis.js"; +import { Mutex } from "redis-semaphore"; const mutedWordsCache = new Cache< { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] @@ -461,58 +462,43 @@ export default async ( } if (!dontFederateInitially) { + let publishKey: string; + let noteToPublish: Note; const relays = await getCachedRelays(); + // Some relays (e.g., aode-relay) deliver posts by boosting them as // Announce activities. In that case, user is the relay's actor. const boostedByRelay = !!user.inbox && relays.map((relay) => relay.inbox).includes(user.inbox); - if (!note.uri) { - // Publish if the post is local - publishNotesStream(note); - } else if (boostedByRelay && data.renote?.uri) { - // Use Redis transaction for atomicity - await redisClient.watch(`publishedNote:${data.renote.uri}`); - const exists = await redisClient.exists( - `publishedNote:${data.renote.uri}`, - ); - if (exists === 0) { - // Start the transaction - const transaction = redisClient.multi(); - const key = `publishedNote:${data.renote.uri}`; - transaction.set(key, 1, "EX", 30); - // Execute the transaction - transaction.exec((err, replies) => { - // Publish after setting the key in Redis - if (!err && data.renote) { - publishNotesStream(data.renote); - } - }); - } else { - // Abort the transaction - redisClient.unwatch(); - } - } else if (!boostedByRelay && note.uri) { - // Use Redis transaction for atomicity - await redisClient.watch(`publishedNote:${note.uri}`); - const exists = await redisClient.exists(`publishedNote:${note.uri}`); - if (exists === 0) { - // Start the transaction - const transaction = redisClient.multi(); - const key = `publishedNote:${note.uri}`; - transaction.set(key, 1, "EX", 30); - // Execute the transaction - transaction.exec((err, replies) => { - // Publish after setting the key in Redis - if (!err) { - publishNotesStream(note); - } - }); - } else { - // Abort the transaction - redisClient.unwatch(); + if (boostedByRelay && data.renote && data.renote.userHost) { + publishKey = `publishedNote:${data.renote.id}`; + noteToPublish = data.renote; + } else { + publishKey = `publishedNote:${note.id}`; + noteToPublish = note; + } + + const lock = new Mutex(redisClient, "publishedNote"); + await lock.acquire(); + try { + const published = (await redisClient.get(publishKey)) !== null; + if (!published) { + await redisClient.set(publishKey, "done", "EX", 30); + if (noteToPublish.renoteId) { + // Prevents other threads from publishing the boosting post + await redisClient.set( + `publishedNote:${noteToPublish.renoteId}`, + "done", + "EX", + 30, + ); + } + publishNotesStream(noteToPublish); } + } finally { + await lock.release(); } } if (note.replyId != null) { diff --git a/packages/client/src/components/MkEmojiPickerDialog.vue b/packages/client/src/components/MkEmojiPickerDialog.vue index 8bbb4bb850..204505e89f 100644 --- a/packages/client/src/components/MkEmojiPickerDialog.vue +++ b/packages/client/src/components/MkEmojiPickerDialog.vue @@ -67,9 +67,8 @@ function chosen(emoji: any) { function opening() { try { picker.value?.reset(); - } - catch (e) { - console.error(`Something's wrong with restting the emoji picker: ${e}`) + } catch (e) { + console.error(`Something's wrong with restting the emoji picker: ${e}`); } picker.value?.focus(); } diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue index 1123035849..18961bed42 100644 --- a/packages/client/src/components/MkNote.vue +++ b/packages/client/src/components/MkNote.vue @@ -337,9 +337,7 @@ let appearNote = $computed(() => const isMyRenote = $i && $i.id === note.userId; const showContent = ref(false); const isDeleted = ref(false); -const muted = ref( - getWordSoftMute(appearNote, $i, defaultStore.state.mutedWords) -); +const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords)); const translation = ref(null); const translating = ref(false); const enableEmojiReactions = defaultStore.state.enableEmojiReactions; diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue index 3ddcb6079b..5916621840 100644 --- a/packages/client/src/components/MkNotes.vue +++ b/packages/client/src/components/MkNotes.vue @@ -53,12 +53,12 @@ const props = defineProps<{ const pagingComponent = ref>(); function scrollTop() { - scroll(tlEl.value, { top: 0, behavior: 'smooth' }) + scroll(tlEl.value, { top: 0, behavior: "smooth" }); } defineExpose({ pagingComponent, - scrollTop + scrollTop, }); diff --git a/packages/client/src/components/MkTimeline.vue b/packages/client/src/components/MkTimeline.vue index 81e4f575c6..8c57a29ef5 100644 --- a/packages/client/src/components/MkTimeline.vue +++ b/packages/client/src/components/MkTimeline.vue @@ -23,7 +23,7 @@ ref="tlComponent" :no-gap="!$store.state.showGapBetweenNotesInTimeline" :pagination="pagination" - @queue="(x) => queue = x" + @queue="(x) => (queue = x)" /> diff --git a/packages/client/src/pages/about-calckey.vue b/packages/client/src/pages/about-calckey.vue index 6071a2c2d9..0a22dca4da 100644 --- a/packages/client/src/pages/about-calckey.vue +++ b/packages/client/src/pages/about-calckey.vue @@ -97,13 +97,16 @@ > - + + {{ i18n.ts._aboutMisskey.patrons }} +

+ {{ i18n.ts._aboutMisskey.patronsList }} +

- - - - (null); type MetaExperiments = { experimentalFeatures?: { - postEditing?: boolean; postImports?: boolean; }; }; @@ -64,14 +49,12 @@ async function init() { meta = (await os.api("admin/meta")) as MetaExperiments; if (!meta) return; - enablePostEditing = meta.experimentalFeatures?.postEditing ?? false; enablePostImports = meta.experimentalFeatures?.postImports ?? false; } function save() { const experiments: MetaExperiments = { experimentalFeatures: { - postEditing: enablePostEditing, postImports: enablePostImports, }, }; diff --git a/packages/client/src/pages/admin/overview.stats.vue b/packages/client/src/pages/admin/overview.stats.vue index b6d9e0bf14..172e346e70 100644 --- a/packages/client/src/pages/admin/overview.stats.vue +++ b/packages/client/src/pages/admin/overview.stats.vue @@ -22,7 +22,7 @@ :value="usersComparedToThePrevDay" > -
Users
+
{{ i18n.ts.users }}
@@ -41,7 +41,7 @@ :value="notesComparedToThePrevDay" >
-
Posts
+
{{ i18n.ts.notes }}
@@ -55,7 +55,7 @@ style="margin-right: 0.5em" />
-
Instances
+
{{ i18n.ts.instances }}
@@ -69,10 +69,26 @@ style="margin-right: 0.5em" />
-
Online
+
{{ i18n.ts.online }}
+ + +
+
+ +
+
+
+ +
+
{{ i18n.ts.emojis }}
+ + @@ -90,6 +106,7 @@ let stats: any = $ref(null); let usersComparedToThePrevDay = $ref(); let notesComparedToThePrevDay = $ref(); let onlineUsersCount = $ref(0); +let emojiCount = $ref(0); let fetching = $ref(true); onMounted(async () => { @@ -110,6 +127,10 @@ onMounted(async () => { stats.originalNotesCount - chart.local.total[1]; }); + os.api("meta", { detail: false }).then((meta) => { + emojiCount = meta.emojis.length; + }); + fetching = false; }); @@ -172,6 +193,13 @@ onMounted(async () => { } } + &.drive { + > .icon { + background: #b4637a22; + color: #eb6f92; + } + } + > .body { padding: 2px 0; diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue index fbee993d32..a99ba68724 100644 --- a/packages/client/src/pages/antenna-timeline.vue +++ b/packages/client/src/pages/antenna-timeline.vue @@ -126,7 +126,7 @@ definePageMetadata(