diff --git a/CHANGELOG.md b/CHANGELOG.md index 899fb7c5e1..7edf9c156d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 2025.4.0 ### General -- Feat: チャットがリニューアルして復活しました(beta) +- Feat: チャット(ダイレクトメッセージ)がリニューアルして復活しました - 既存のDM機能よりも便利で効率的な実装になっています - チャットを受け付ける相手を制限可能です - 誰でも / フォローユーザーのみ / フォロワーのみ / 相互のみ / 受け付けない から選択できます @@ -11,7 +11,8 @@ - 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です - 参加中のルームをミュートして通知が来ないように設定可能です - メッセージにはリアクションも可能です -- Feat: アカウントのマイグレーション時に古いアカウントからロールをコピーできるようになりました。 + - 現在、リモートユーザーがチャットを受け付ける設定になっているかどうかを取得する術がないため、ローカルユーザー間でのみ利用可能です +- Feat: アカウントの移行時に古いアカウントからあたらしいアカウントにロールをコピーできるようになりました。 - 管理者がロールの設定でマイグレーション時にコピーするかを指定できるようになります。 - Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 - Misskeyネイティブでダッシュボードを実装予定です @@ -41,6 +42,7 @@ - 再度ログインすればサーバーのバックアップから設定データを復元可能です - エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました - 設定情報の移行は自動で行われますが、何らかの理由で失敗した場合、設定→その他→旧設定情報を移行 で再試行可能です + - 過去に作成されたバックアップデータとは現在互換性がありませんのでご注意ください - Feat: 画面を重ねて表示するオプションを実装(実験的) - 設定 → その他 → 実験的機能 → Enable stacking router view - Enhance: プラグインの管理が強化されました @@ -59,11 +61,13 @@ - Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように - Enhance: 全体的なブラッシュアップ - Enhance 全体的なパフォーマンス向上 +- Enhance: ファイルのアップロードでデフォルトで圧縮するかどうかのオプションが廃止され、アップロード時に圧縮するかどうかを選択するようになりました + - 画像データの貼り付け、ドロップ時は圧縮されるようになりました - Fix: 読み込み直後にスクロールしようとすると途中で止まる場合があるのを修正 - Fix: テーマ切り替え時に一部の色が変わらない問題を修正 +- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正 - NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました - デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置し、ナビゲーションバーを上部に表示することである程度クラシックUIを再現できます -- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正 ### Server - Enhance 全体的なパフォーマンス向上 diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 354013283a..8f83a33356 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -424,7 +424,7 @@ antennaExcludeBots: "Exclou els bots" antennaKeywordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR." notifyAntenna: "Notifica'm les publicacions noves" withFileAntenna: "Només les publicacions amb fitxers" -hideNotesInSensitiveChannel: "Amaga les notes a canals sensibles " +excludeNotesInSensitiveChannel: "Excloure notes a canals sensibles" enableServiceworker: "Activar les notificacions al navegador" antennaUsersDescription: "Llistar un nom d'usuari per línia" caseSensitive: "Sensible a majúscules i minúscules " @@ -537,7 +537,7 @@ mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única limitTo: "Limita a {x}" noFollowRequests: "No tens sol·licituds de seguiment" openImageInNewTab: "Obre imatges a una nova pestanya" -dashboard: "Taulell de control" +dashboard: "Tauler de control" local: "Local" remote: "Remot" total: "Total" @@ -652,7 +652,7 @@ manage: "Administració" plugins: "Extensions" preferencesBackups: "Configuracions de les Còpies de seguretat" deck: "Escriptori" -undeck: "Tanca l'escriptori" +undeck: "Tanca el tauler" useBlurEffectForModal: "Utilitzar l'efecte de difuminació a modals" useFullReactionPicker: "Utilitza el cercador de reaccions d'escala sencera" width: "Amplada" @@ -915,7 +915,7 @@ off: "Desactivar" emailRequiredForSignup: "Demanar correu electrònic per registrar-se " unread: "Sense llegir" filter: "Filtrar" -controlPanel: "Taulell de control" +controlPanel: "Tauler de control" manageAccounts: "Gestionar comptes" makeReactionsPublic: "Reaccions públiques " makeReactionsPublicDescription: "Això fa que totes les teves reaccions siguin visibles públicament " @@ -1341,6 +1341,9 @@ right: "Dreta" bottom: "A baix " top: "A dalt " embed: "Incrustar" +settingsMigrating: "Estem fent la migració de la teva configuració. Si us plau espera un moment... (També pots fer la migració més tard i manualment anant a Preferències → Altres configuracions → Migrar configuració antiga)" +readonly: "Només lectura" +goToDeck: "Tornar al tauler" _chat: noMessagesYet: "Encara no tens missatges " newMessage: "Missatge nou" @@ -1370,6 +1373,7 @@ _chat: muteThisRoom: "Silenciar aquesta sala" deleteRoom: "Esborrar la sala" chatNotAvailableForThisAccountOrServer: "El xat no està disponible per aquest servidor o aquest compte." + chatIsReadOnlyForThisAccountOrServer: "El xat és només de lectura en aquest servidor o compte. No es poden escriure nous missatges ni crear o unir-se a sales de xat." chatNotAvailableInOtherAccount: "La funció de xat es troba desactivada al compte de l'altre usuari." cannotChatWithTheUser: "No pots xatejar amb aquest usuari" cannotChatWithTheUser_description: "El xat està desactivat o l'altra part encara no l'ha obert." @@ -1930,7 +1934,7 @@ _role: canImportFollowing: "Autoritza la importació de seguidors" canImportMuting: "Autoritza la importació de silenciats" canImportUserLists: "Autoritza la importació de llistes d'usuaris " - canChat: "Pot xatejar" + chatAvailability: "Es permet xatejar" _condition: roleAssignedTo: "Assignat a rols manuals" isLocal: "Usuari local" @@ -2122,7 +2126,7 @@ _theme: fg: "Text" focus: "Enfocament" indicator: "Indicador" - panel: "Taulell " + panel: "Tauler" shadow: "Ombra" header: "Capçalera" navBg: "Fons de la barra lateral" @@ -2600,7 +2604,7 @@ _deck: columnGap: "Espai entre columnes" deckMenuPosition: "Posició del menú del tauler" navbarPosition: "Posició de la barra de navegació " - addColumn: "Afig una columna" + addColumn: "Afegeix una columna" newNoteNotificationSettings: "Configuració de notificacions per a notes noves" configureColumn: "Configuració de columnes" swapLeft: "Mou a l’esquerra" @@ -2795,7 +2799,7 @@ _hemisphere: _reversi: reversi: "Reversi" gameSettings: "Opcions del joc" - chooseBoard: "Escull un taulell" + chooseBoard: "Escull un tauler" blackOrWhite: "Negres/Blanques" blackIs: "{name} juga amb negres " rules: "Regles" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 973a561a86..b953a5717e 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -424,6 +424,7 @@ antennaExcludeBots: "Bot-Accounts ausschließen" antennaKeywordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen" notifyAntenna: "Über neue Notizen benachrichtigen" withFileAntenna: "Nur Notizen mit Dateien" +excludeNotesInSensitiveChannel: "Schließe Notizen von sensitive Kanäle aus" enableServiceworker: "Push-Benachrichtigungen im Browser aktivieren" antennaUsersDescription: "Benutzernamen getrennt durch Zeilenumbrüche angeben" caseSensitive: "Groß-/Kleinschreibung unterscheiden" @@ -1304,6 +1305,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "Logge dich ein, um weitere Inhal lockdown: "Sperren" pleaseSelectAccount: "Bitte Konto auswählen" availableRoles: "Verfügbare Rollen" +acknowledgeNotesAndEnable: "Schalten Sie dies erst ein, wenn Sie die Vorsichtsmaßnahmen verstanden haben." federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren." federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren." confirmOnReact: "Reagieren bestätigen" @@ -1311,9 +1313,11 @@ reactAreYouSure: "Willst du eine \"{emoji}\"-Reaktion hinzufügen?" markAsSensitiveConfirm: "Möchtest du dieses Medium als sensibel kennzeichnen?" unmarkAsSensitiveConfirm: "Möchtest du die Kennzeichnung dieses Mediums als sensibel aufheben?" preferences: "Einstellungen" +accessibility: "Eingabehilfe" preferencesProfile: "Einstellungsprofil" copyPreferenceId: "Kopiere die Einstellungs-ID" resetToDefaultValue: "Auf Standard zurücksetzen" +overrideByAccount: "Überschreibung durch das Konto" untitled: "Unbenannt" noName: "Kein Name" skip: "Überspringen" @@ -1333,15 +1337,24 @@ chat: "Chat" migrateOldSettings: "Alte Client-Einstellungen migrieren" migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben." compress: "Komprimieren" +right: "Rechts" +bottom: "Unten" +top: "Oben" +embed: "Einbetten" +settingsMigrating: "Ihre Einstellungen werden gerade migriert, Bitte warten Sie einen Moment... (Sie können die Einstellungen später auch manuell migrieren, indem Sie zu Einstellungen → Sonstiges → Alte Einstellungen migrieren gehen)" +readonly: "Nur Lesezugriff" +goToDeck: "Zurück zum Deck" _chat: noMessagesYet: "Noch keine Nachrichten" newMessage: "Neue Nachricht" individualChat: "Privater Chat" individualChat_description: "Führe einen privaten Chat mit einer anderen Person." + roomChat: "Chatraum" roomChat_description: "Ein Chat-Raum, an dem mehrere Personen teilnehmen können.\nDu kannst auch Personen einladen, die keine privaten Chats zulassen, wenn sie die Einladung annehmen." createRoom: "Raum erstellen" inviteUserToChat: "Lade Benutzer ein, um mit dem Chatten zu beginnen" yourRooms: "Erstellte Räume" + joiningRooms: "Raum beitreten" invitations: "Einladen" noInvitations: "Keine Einladungen" history: "Verlauf" @@ -1360,6 +1373,7 @@ _chat: muteThisRoom: "Raum stummschalten" deleteRoom: "Raum löschen" chatNotAvailableForThisAccountOrServer: "Der Chat ist auf diesem Server oder für dieses Konto nicht aktiviert." + chatIsReadOnlyForThisAccountOrServer: "Der Chat ist auf dieser Instanz oder diesem Konto nur zum Lesen freigegeben. Es ist nicht möglich, neue Nachrichten zu schreiben oder Chaträume zu erstellen oder zu betreten." chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert." cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich" cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert." @@ -1384,8 +1398,10 @@ _emojiPalette: _settings: driveBanner: "Du kannst den Drive verwalten und konfigurieren, die Auslastung überprüfen und Einstellungen für das Hochladen von Dateien vornehmen." pluginBanner: "Du kannst die Funktionen des Clients mit Plugins erweitern. Plugins können installiert, individuell konfiguriert und verwaltet werden." + notificationsBanner: "Sie können die Arten und den Umfang der Benachrichtigungen vom Server und der Push- Mitteilungen konfigurieren." api: "API" webhook: "Webhook" + serviceConnection: "Integrierte Dienste" serviceConnectionBanner: "Du kannst Zugriffstoken und Webhooks für die Integration mit externen Anwendungen und Diensten verwalten und konfigurieren." accountData: "Kontodaten" accountDataBanner: "Export/Import und Verwaltung von Kontodatenarchiven." @@ -1393,13 +1409,17 @@ _settings: accessibilityBanner: "Die Clients können personalisiert und für eine optimale Nutzung im Hinblick auf ihre Darstellung und ihr Verhalten eingerichtet werden." privacyBanner: "Du kannst Einstellungen für die Privatsphäre deines Kontos vornehmen, z. B. inwieweit Inhalte veröffentlicht werden, wie leicht sie zu finden sind und ob Follower genehmigt werden müssen." securityBanner: "Du kannst Einstellungen für die Kontosicherheit konfigurieren, z. B. Passwörter, Anmeldemethoden, Authentifizierungs-Apps und Passkeys." + preferencesBanner: "Sie können das Gesamtverhalten des Clients nach Ihren Wünschen konfigurieren." appearanceBanner: "Du kannst das Erscheinungsbild und die Anzeigeeinstellungen für den Client nach deinen Wünschen konfigurieren." soundsBanner: "Du kannst die Einstellungen für die Wiedergabe von Klängen im Client konfigurieren." timelineAndNote: "Chroniken und Notizen" makeEveryTextElementsSelectable: "Alle Textelemente auswählbar machen" makeEveryTextElementsSelectable_description: "Die Aktivierung kann in manchen Situationen die Benutzerfreundlichkeit beeinträchtigen." + useStickyIcons: "Icons beim Scrollen folgen lassen" + showNavbarSubButtons: "Unterschaltflächen in der Navigationsleiste anzeigen" ifOn: "Wenn eingeschaltet" ifOff: "Wenn ausgeschaltet" + enableSyncThemesBetweenDevices: "Synchronisierung von installierten Themen auf verschiedenen Endgeräten" _chat: showSenderName: "Name des Absenders anzeigen" sendOnEnter: "Eingabetaste sendet Nachricht" @@ -1427,6 +1447,7 @@ _accountSettings: makeNotesHiddenBeforeDescription: "" mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden." mayNotEffectSomeSituations: "Diese Einschränkungen sind vereinfacht. Sie gelten möglicherweise nicht in allen Situationen, z. B. bei der Anzeige auf einem fremden Server oder während der Moderation." + notesHavePassedSpecifiedPeriod: "Notizen die nach der folgenden Zeit veröffentlicht worden" notesOlderThanSpecifiedDateAndTime: "Notizen vor einem bestimmtem Datum und Uhrzeit" _abuseUserReport: forward: "Weiterleiten" @@ -1438,6 +1459,7 @@ _abuseUserReport: _delivery: status: "Auslieferungsstatus" stop: "Gesperrt" + resume: "Zustellung wieder fortsetzen" _type: none: "Wird veröffentlicht" manuallySuspended: "Manuell gesperrt" @@ -1519,6 +1541,7 @@ _initialTutorial: description2: "Du kannst jederzeit am oberen Rand des Bildschirms zwischen den jeweiligen Chroniken wechseln." description3: "Darüber hinaus gibt es Listen-Chroniken und Kanal-Chroniken. Weitere Einzelheiten findest du unter {link}." _postNote: + title: "Optionen bei Abschicken einer Notiz" description1: "Wenn du eine Notiz auf Misskey veröffentlichst, stehen dir verschiedene Optionen zur Verfügung. Die Oberfläche sieht folgendermaßen aus." _visibility: description: "Du kannst einschränken, wer deine Notiz sehen kann." @@ -1839,6 +1862,7 @@ _achievements: _bubbleGameDoubleExplodingHead: title: "Doppel🤯" description: "Zwei der größten Objekte im Bubble Game zur gleichen Zeit" + flavor: "Eine Lunchbox kann man auch mit etwas mehr 🤯 🤯 füllen" _role: new: "Rolle erstellen" edit: "Rolle bearbeiten" @@ -1868,6 +1892,8 @@ _role: descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich." displayOrder: "Position" descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position." + preserveAssignmentOnMoveAccount: "Rolle übertragbar machen" + preserveAssignmentOnMoveAccount_description: "Wenn diese Option aktiviert ist, wird diese Rolle bei der Migration mit übertragen." canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." priority: "Priorität" @@ -1908,7 +1934,7 @@ _role: canImportFollowing: "Importieren von Gefolgten zulassen" canImportMuting: "Importieren von Stummgeschalteten zulassen" canImportUserLists: "Importieren von Listen erlauben" - canChat: "Chatten erlauben" + chatAvailability: "Chatten erlauben" _condition: roleAssignedTo: "Manuellen Rollen zugewiesen" isLocal: "Lokaler Benutzer" @@ -2263,12 +2289,14 @@ _permissions: "read:admin:announcements": "Ankündigungen einsehen" "write:admin:avatar-decorations": "Kann Avatar-Dekorationen verwalten" "read:admin:avatar-decorations": "Avatar-Dekorationen ansehen" + "write:admin:federation": "Informationen über Föderationen bearbeiten oder löschen" "write:admin:account": "Benutzerkonten verwalten" "read:admin:account": "Benutzerkonten anzeigen" "write:admin:emoji": "Emojis verwalten" "read:admin:emoji": "Emojis anzeigen" "write:admin:queue": "Job-Warteschlange verwalten" "read:admin:queue": "Job-Warteschlange anzeigen" + "write:admin:promo": "Moderationsnotiz hinzufügen" "write:admin:drive": "Benutzer-Drive verwalten" "read:admin:drive": "Benutzer-Drive ansehen" "read:admin:stream": "Verwendung der Websocket-API für Administratoren" @@ -2276,6 +2304,8 @@ _permissions: "read:admin:ad": "Werbung ansehen" "write:invite-codes": "Einladungscodes erstellen" "read:invite-codes": "Einladungscodes anzeigen" + "write:clip-favorite": "Clip-Likes bearbeiten oder löschen" + "read:clip-favorite": "Clip-Likes ansehen" "read:federation": "Informationen zur Föderation einsehen" "write:report-abuse": "Verstöße melden" "write:chat": "Chats bedienen" @@ -2290,6 +2320,7 @@ _auth: callback: "Es wird zur Anwendung zurückgekehrt" accepted: "Zugriff gewährt" denied: "Zugriff verweigert" + scopeUser: "Als folgender Benutzer agieren" pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren." byClickingYouWillBeRedirectedToThisUrl: "Wenn der Zugang gewährt wird, wirst du automatisch zu folgender URL weitergeleitet" _antennaSources: @@ -2542,6 +2573,7 @@ _notification: exportOfXCompleted: "Der Export von {x} ist abgeschlossen" login: "Neue Anmeldung erfolgt" createToken: "Ein Zugangstoken wurde erstellt" + createTokenDescription: "Wenn Sie keine Ahnung haben, löschen Sie das Zugriffstoken über \"{text}\"" _types: all: "Alle" note: "Neue Notizen" @@ -2569,6 +2601,9 @@ _notification: _deck: alwaysShowMainColumn: "Hauptspalte immer zeigen" columnAlign: "Spaltenausrichtung" + columnGap: "Spaltenabstand" + deckMenuPosition: "Position des Deck-Menüs" + navbarPosition: "Position der Navigationsleiste" addColumn: "Spalte hinzufügen" newNoteNotificationSettings: "Benachrichtigungseinstellungen für neue Notizen" configureColumn: "Spalteneinstellungen" @@ -2791,6 +2826,9 @@ _reversi: allGames: "Alle Runden" ended: "Beendet" playing: "Partie läuft" + isLlotheo: "Der mit weniger Steinen gewinnt (Llotheo)" + loopedMap: "Wiederholendes Spielbrett" + canPutEverywhere: "Steine können überall platziert werden" timeLimitForEachTurn: "Zeitlimit eines Zugs" freeMatch: "Freies Spiel" lookingForPlayer: "Gegner werden gesucht..." @@ -2841,6 +2879,7 @@ _customEmojisManager: copySelectionRows: "Ausgewählte Zeilen kopieren" copySelectionRanges: "Auswahl kopieren" deleteSelectionRows: "Ausgewählte Zeilen löschen" + deleteSelectionRanges: "Zeilen in der Auswahl löschen" searchSettings: "Sucheinstellungen" searchSettingCaption: "Detaillierte Suchkriterien festlegen." searchLimit: "Anzahl der Ergebnisse" @@ -2849,10 +2888,12 @@ _customEmojisManager: registrationLogsCaption: "Protokolle werden beim Aktualisieren oder Löschen von Emojis angezeigt. Sie verschwinden nach dem Aktualisieren oder Löschen, dem Wechsel zu einer neuen Seite oder dem Neuladen." alertEmojisRegisterFailedDescription: "Emoji konnte nicht aktualisiert oder gelöscht werden. Bitte prüfe das Registrierungsprotokoll für Details." _logs: + showSuccessLogSwitch: "Erfolgsprotokoll zeigen" failureLogNothing: "Es gibt kein Fehlerprotokoll." logNothing: "Keine Protokoll-Einträge." _remote: selectionRowDetail: "Details der ausgewählten Zeile" + importSelectionRows: "Ausgewählte Zeilen importieren" importSelectionRangesRows: "Zeilen in der Auswahl importieren" importEmojisButton: "Ausgewählte Emojis importieren" confirmImportEmojisTitle: "Emojis importieren" @@ -2862,15 +2903,23 @@ _customEmojisManager: tabTitleRegister: "Emojis hinzufügen" _list: emojisNothing: "Es wurden keine Emojis hinzugefügt." + markAsDeleteTargetRows: "Ausgewählte Zeilen als zu löschendes Element markieren" + markAsDeleteTargetRanges: "Zeilen in der Auswahl als zu löschendes Element markieren" alertUpdateEmojisNothingDescription: "Es wurden keine Emojis geändert." alertDeleteEmojisNothingDescription: "Es gibt keine zu löschenden Emojis." + confirmMovePage: "Möchten Sie die Seiten verschieben?" + confirmChangeView: "Möchten Sie die Darstellung wechseln?" confirmUpdateEmojisDescription: "Aktualisiere {count} Emoji(s). Willst du fortfahren?" confirmDeleteEmojisDescription: "Lösche {count} ausgewählte Emoji(s). Willst du fortfahren?" + confirmResetDescription: "Alle bisher vorgenommenen Änderungen werden zurückgesetzt." confirmMovePageDesciption: "An den Emojis auf dieser Seite wurden Änderungen vorgenommen.\nWenn du die Seite verlässt, ohne zu speichern, werden alle auf dieser Seite vorgenommenen Änderungen verworfen." + dialogSelectRoleTitle: "Suche nach dem Rollensatz in Emojis" _register: + uploadSettingTitle: "Upload-Einstellungen" uploadSettingDescription: "Hier kannst du das Verhalten beim Hochladen von Emojis konfigurieren." directoryToCategoryLabel: "Gib den Namen des Verzeichnisses in das Feld „Kategorie“ ein" directoryToCategoryCaption: "Wenn du ein Verzeichnis ziehst und ablegst, gib den Verzeichnisnamen in das Feld „Kategorie“ ein." + emojiInputAreaCaption: "Wählen Sie die Emojis aus, die Sie mit einer der folgenden Methoden speichern möchten." emojiInputAreaList1: "Ziehe Bilddateien oder Verzeichnisse per Drag-and-drop in diesen Rahmen" emojiInputAreaList2: "Klicke auf diesen Link, um von deinem PC aus zu wählen" emojiInputAreaList3: "Klicke auf diesen Link, um vom Drive aus zu wählen" diff --git a/locales/en-US.yml b/locales/en-US.yml index 5c82e4176c..06a536bcf6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -424,7 +424,7 @@ antennaExcludeBots: "Exclude bot accounts" antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." notifyAntenna: "Notify about new notes" withFileAntenna: "Only notes with files" -hideNotesInSensitiveChannel: "Hide notes from sensitive channels" +excludeNotesInSensitiveChannel: "Exclude notes from sensitive channels" enableServiceworker: "Enable Push-Notifications for your Browser" antennaUsersDescription: "List one username per line" caseSensitive: "Case sensitive" @@ -1341,6 +1341,9 @@ right: "Right" bottom: "Bottom" top: "Top" embed: "Embed" +settingsMigrating: "Settings are being migrated, please wait a moment... (You can also migrate manually later by going to Settings→Others→Migrate old settings)" +readonly: "Read only" +goToDeck: "Return to Deck" _chat: noMessagesYet: "No messages yet" newMessage: "New message" @@ -1370,6 +1373,7 @@ _chat: muteThisRoom: "Mute room" deleteRoom: "Delete room" chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account." + chatIsReadOnlyForThisAccountOrServer: "Chat is read-only on this instance or this account. You cannot write new messages or create/join chat rooms." chatNotAvailableInOtherAccount: "The chat function is disabled for the other user." cannotChatWithTheUser: "Cannot start a chat with this user" cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat." @@ -1930,7 +1934,7 @@ _role: canImportFollowing: "Allow importing following" canImportMuting: "Allow importing muting" canImportUserLists: "Allow importing lists" - canChat: "Allow Chat" + chatAvailability: "Allow Chat" _condition: roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" diff --git a/locales/index.d.ts b/locales/index.d.ts index fa340119d3..166c44f3b4 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1715,9 +1715,9 @@ export interface Locale extends ILocale { */ "withFileAntenna": string; /** - * センシティブなチャンネルのノートを非表示 + * センシティブなチャンネルのノートを除外 */ - "hideNotesInSensitiveChannel": string; + "excludeNotesInSensitiveChannel": string; /** * ブラウザへのプッシュ通知を有効にする */ @@ -5383,6 +5383,18 @@ export interface Locale extends ILocale { * 埋め込み */ "embed": string; + /** + * 設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます) + */ + "settingsMigrating": string; + /** + * 読み取り専用 + */ + "readonly": string; + /** + * デッキへ戻る + */ + "goToDeck": string; "_chat": { /** * まだメッセージはありません @@ -5497,6 +5509,10 @@ export interface Locale extends ILocale { * このサーバー、またはこのアカウントでチャットは有効化されていません。 */ "chatNotAvailableForThisAccountOrServer": string; + /** + * このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。 + */ + "chatIsReadOnlyForThisAccountOrServer": string; /** * 相手のアカウントでチャット機能が使えない状態になっています。 */ @@ -7551,7 +7567,7 @@ export interface Locale extends ILocale { /** * チャットを許可 */ - "canChat": string; + "chatAvailability": string; /** * Can view the bubble timeline */ diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 9b62141fdc..44b0858fe8 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -424,7 +424,6 @@ antennaExcludeBots: "Escludere i Bot" antennaKeywordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)." notifyAntenna: "Invia notifiche delle nuove note" withFileAntenna: "Solo note con file in allegato" -hideNotesInSensitiveChannel: "Nascondere le Note dai canali espliciti" enableServiceworker: "Abilita ServiceWorker" antennaUsersDescription: "Elenca un nome utente per riga" caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole" @@ -523,7 +522,7 @@ showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mou showReactionsCount: "Visualizza il numero di reazioni su una nota" noHistory: "Nessuna cronologia" signinHistory: "Storico degli accessi al profilo" -enableAdvancedMfm: "Attiva MFM avanzati" +enableAdvancedMfm: "Attivare i Misskey Flavoured Markdown (MFM) avanzati" enableAnimatedMfm: "Attiva MFM animati" doing: "In corso..." category: "Categoria" @@ -606,7 +605,7 @@ uiInspector: "UI Inspector" uiInspectorDescription: "Puoi visualizzare un elenco di elementi UI presenti in memoria. I componenti dell'interfaccia utente vengono generati dalle funzioni Ui:C:." output: "Output" script: "Script" -disablePagesScript: "Disabilita AiScript nelle pagine" +disablePagesScript: "Disabilitare AiScript nelle pagine" updateRemoteUser: "Aggiorna dati dal profilo remoto" unsetUserAvatar: "Rimozione foto profilo" unsetUserAvatarConfirm: "Vuoi davvero rimuovere la foto profilo?" @@ -664,7 +663,7 @@ generateAccessToken: "Genera token di accesso" permission: "Autorizzazioni " adminPermission: "Privilegi amministrativi" enableAll: "Abilita tutto" -disableAll: "Disabilita tutto" +disableAll: "Disabilitare tutto" tokenRequested: "Autorizza accesso al profilo" pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui." notificationType: "Tipo di notifiche" @@ -767,7 +766,7 @@ noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pa lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow." alwaysMarkSensitive: "Segnare automaticamente come espliciti gli allegati" loadRawImages: "Visualizza le intere immagini allegate invece delle miniature." -disableShowingAnimatedImages: "Disabilita le immagini animate" +disableShowingAnimatedImages: "Disabilitare le immagini animate" highlightSensitiveMedia: "Evidenzia i media espliciti" verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica." notSet: "Non impostato" @@ -1193,7 +1192,7 @@ renotes: "Rinota" loadReplies: "Leggi le risposte" loadConversation: "Leggi la conversazione" pinnedList: "Elenco in primo piano" -keepScreenOn: "Mantieni lo schermo acceso" +keepScreenOn: "Mantenere lo schermo acceso" verifiedLink: "Abbiamo confermato la validità di questo collegamento" notifyNotes: "Notifica nuove Note" unnotifyNotes: "Interrompi le notifiche di nuove Note" @@ -1235,7 +1234,7 @@ flip: "Inverti" showAvatarDecorations: "Mostra decorazione della foto profilo" releaseToRefresh: "Rilascia per aggiornare" refreshing: "Aggiornamento..." -pullDownToRefresh: "Trascina per aggiornare" +pullDownToRefresh: "Trascinare per aggiornare" disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo reale" useGroupedNotifications: "Mostra le notifiche raggruppate" signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo." @@ -1263,7 +1262,7 @@ backToTitle: "Torna al titolo" hemisphere: "Geolocalizzazione" withSensitive: "Mostra le Note con allegati espliciti" userSaysSomethingSensitive: "Note da {name} con allegati espliciti" -enableHorizontalSwipe: "Trascina per invertire i tab" +enableHorizontalSwipe: "Trascinare per invertire le colonne" loading: "Caricamento" surrender: "Annulla" gameRetry: "Riprova" @@ -1930,7 +1929,6 @@ _role: canImportFollowing: "Può importare Following" canImportMuting: "Può importare Silenziati" canImportUserLists: "Può importare liste di Profili" - canChat: "Chat consentita" _condition: roleAssignedTo: "Assegnato a ruoli manualmente" isLocal: "Profilo locale" @@ -2916,7 +2914,7 @@ _customEmojisManager: directoryToCategoryLabel: "Inseriscile in una cartella omonima alla categoria" directoryToCategoryCaption: "Crea il campo categoria in base alla cartella." emojiInputAreaCaption: "Seleziona l'emoji da registrare utilizzando uno dei metodi." - emojiInputAreaList1: "Trascina una immagine o una cartella in quest'area" + emojiInputAreaList1: "Trascinare una immagine o una cartella in quest'area" emojiInputAreaList2: "Clicca per scegliere file dal tuo dispositivo" emojiInputAreaList3: "Clicca per selezionare dal Drive" confirmRegisterEmojisDescription: "Registrazione delle emoji elencate come nuove emoji personalizzate. Vuoi davvero procedere? (Per evitare sovraccarichi, puoi registrare al massimo {count} emoji per volta)" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 323dc3a38a..c7a9fc8792 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -424,7 +424,7 @@ antennaExcludeBots: "Botアカウントを除外" antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" notifyAntenna: "新しいノートを通知する" withFileAntenna: "ファイルが添付されたノートのみ" -hideNotesInSensitiveChannel: "センシティブなチャンネルのノートを非表示" +excludeNotesInSensitiveChannel: "センシティブなチャンネルのノートを除外" enableServiceworker: "ブラウザへのプッシュ通知を有効にする" antennaUsersDescription: "ユーザー名を改行で区切って指定します" caseSensitive: "大文字小文字を区別する" @@ -1341,6 +1341,9 @@ right: "右" bottom: "下" top: "上" embed: "埋め込み" +settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)" +readonly: "読み取り専用" +goToDeck: "デッキへ戻る" _chat: noMessagesYet: "まだメッセージはありません" @@ -1371,6 +1374,7 @@ _chat: muteThisRoom: "このルームをミュート" deleteRoom: "ルームを削除" chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。" + chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。" chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。" cannotChatWithTheUser: "このユーザーとのチャットを開始できません" cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" @@ -1949,7 +1953,7 @@ _role: canImportFollowing: "フォローのインポートを許可" canImportMuting: "ミュートのインポートを許可" canImportUserLists: "リストのインポートを許可" - canChat: "チャットを許可" + chatAvailability: "チャットを許可" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index f957eb7da3..fc578758a2 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -64,8 +64,8 @@ copyNoteId: "노트 ID 복사" copyFileId: "파일 ID 복사" copyFolderId: "폴더 ID 복사" copyProfileUrl: "프로필 URL 복사" -searchUser: "사용자 검색" -searchThisUsersNotes: "사용자의 노트 검색" +searchUser: "유저 검색" +searchThisUsersNotes: "유저의 노트를 검색" reply: "답글" loadMore: "더 보기" showMore: "더 보기" @@ -267,7 +267,7 @@ publishing: "배포 중" notResponding: "응답 없음" instanceFollowing: "서버의 팔로잉" instanceFollowers: "서버의 팔로워" -instanceUsers: "서버의 사용자" +instanceUsers: "서버의 유저" changePassword: "비밀번호 변경" security: "보안" retypedNotMatch: "입력이 일치하지 않습니다." @@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 explore: "둘러보기" messageRead: "읽음" noMoreHistory: "이것보다 과거의 기록이 없습니다" +startChat: "채팅을 시작하기" nUsersRead: "{n}명이 읽음" agreeTo: "{0}에 동의" agree: "동의합니다" @@ -384,12 +385,12 @@ disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리 registration: "등록" invite: "초대" driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량" -driveCapacityPerRemoteAccount: "원격 사용자별 드라이브 용량" +driveCapacityPerRemoteAccount: "리모트 유저별 드라이브 용량" inMb: "메가바이트 단위" bannerUrl: "배너 이미지 URL" backgroundImageUrl: "배경 이미지 URL" basicInfo: "기본 정보" -pinnedUsers: "고정한 사용자" +pinnedUsers: "고정한 유저" pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다." pinnedPages: "고정한 페이지" pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다." @@ -417,12 +418,13 @@ antennas: "안테나" manageAntennas: "안테나 관리" name: "이름" antennaSource: "받을 소스" -antennaKeywords: "받을 검색어" -antennaExcludeKeywords: "제외할 검색어" +antennaKeywords: "받을 키워드" +antennaExcludeKeywords: "제외할 키워드" antennaExcludeBots: "봇 계정 제외" antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다" notifyAntenna: "새로운 노트를 알림" withFileAntenna: "파일이 첨부된 노트만" +excludeNotesInSensitiveChannel: "민감한 채널의 노트 제외" enableServiceworker: "ServiceWorker 사용" antennaUsersDescription: "유저명을 한 줄에 한 명씩 적습니다" caseSensitive: "대소문자를 구분" @@ -434,11 +436,11 @@ silence: "사일런스" silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?" unsilence: "사일런스 해제" unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?" -popularUsers: "인기 사용자" -recentlyUpdatedUsers: "최근에 활동한 사용자" -recentlyRegisteredUsers: "최근에 가입한 사용자" -recentlyDiscoveredUsers: "최근에 발견한 사용자" -exploreUsersCount: "{count}명의 사용자가 있습니다" +popularUsers: "인기 유저" +recentlyUpdatedUsers: "최근에 활동한 유저" +recentlyRegisteredUsers: "최근에 가입한 유저" +recentlyDiscoveredUsers: "최근에 발견한 유저" +exploreUsersCount: "{count}명의 유저가 있습니다" exploreFediverse: "연합우주를 탐색" popularTags: "인기 태그" userList: "리스트" @@ -506,7 +508,7 @@ strongPassword: "강한 비밀번호" passwordMatched: "일치합니다" passwordNotMatched: "일치하지 않습니다" signinWith: "{x}로 로그인" -signinFailed: "로그인할 수 없습니다. 사용자 이름과 비밀번호를 확인해 주십시오." +signinFailed: "로그인할 수 없습니다. 유저 이름과 비밀번호를 확인해 주십시오." or: "혹은" language: "언어" uiLanguage: "UI 표시 언어" @@ -518,7 +520,7 @@ style: "스타일" drawer: "서랍" popup: "팝업" showNoteActionsOnlyHover: "마우스가 올라간 때에만 노트 동작 버튼을 표시하기" -showReactionsCount: "노트의 반응 수를 표시하기" +showReactionsCount: "노트의 리액션 수를 표시하기" noHistory: "기록이 없습니다" signinHistory: "로그인 기록" enableAdvancedMfm: "고급 MFM을 활성화" @@ -605,7 +607,7 @@ uiInspectorDescription: "메모리에 있는 UI 컴포넌트의 인스턴트 목 output: "출력" script: "스크립트" disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음" -updateRemoteUser: "원격 사용자 정보 갱신" +updateRemoteUser: "리모트 유저 정보 갱신" unsetUserAvatar: "아바타 제거" unsetUserAvatarConfirm: "아바타를 제거할까요?" unsetUserBanner: "배너 제거" @@ -614,7 +616,7 @@ deleteAllFiles: "모든 파일 삭제" deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?" removeAllFollowing: "모든 팔로잉 해제" removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않는 경우 등에 실행해 주세요." -userSuspended: "이 사용자는 정지되었습니다." +userSuspended: "이 유저는 정지되었습니다." userSilenced: "이 계정은 사일런스된 상태입니다." yourAccountSuspendedTitle: "계정이 정지되었습니다" yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오." @@ -675,7 +677,7 @@ emailAddress: "메일 주소" smtpConfig: "SMTP 서버 설정" smtpHost: "호스트" smtpPort: "포트" -smtpUser: "사용자 이름" +smtpUser: "유저 이름" smtpPass: "비밀번호" emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다." smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용" @@ -752,8 +754,8 @@ repliedCount: "받은 답글 수" renotedCount: "받은 리노트 수" followingCount: "팔로우 수" followersCount: "팔로워 수" -sentReactionsCount: "반응 수" -receivedReactionsCount: "받은 반응 수" +sentReactionsCount: "리액션 수" +receivedReactionsCount: "받은 리액션 수" pollVotesCount: "투표 수" pollVotedCount: "받은 투표 수" yes: "예" @@ -761,7 +763,7 @@ no: "아니오" driveFilesCount: "드라이브에 있는 파일 수" driveUsage: "드라이브 사용량" noCrawle: "검색엔진의 인덱싱 거부" -noCrawleDescription: "검색엔진에 사용자 페이지, 노트, 페이지 등의 콘텐츠를 인덱싱되지 않게 합니다." +noCrawleDescription: "검색엔진에 유저 페이지, 노트, 페이지 등의 콘텐츠를 인덱싱되지 않게 합니다." lockedAccountInfo: "팔로우를 승인으로 승인받더라도 노트의 공개 범위를 '팔로워'로 하지 않는 한 누구나 당신의 노트를 볼 수 있습니다." alwaysMarkSensitive: "미디어를 항상 열람 주의로 설정" loadRawImages: "첨부한 이미지의 썸네일을 원본화질로 표시" @@ -793,7 +795,7 @@ needReloadToApply: "변경 사항은 새로고침하면 적용됩니다." showTitlebar: "타이틀 바를 표시하기" clearCache: "캐시 비우기" onlineUsersCount: "{n}명이 접속 중" -nUsers: "{n} 사용자" +nUsers: "{n} 유저" nNotes: "{n} 노트" sendErrorReports: "오류 보고서 보내기" sendErrorReportsDescription: "이 설정을 활성화하면, 문제가 발생했을 때 오류에 대한 상세 정보를 Misskey에 보내어 더 나은 소프트웨어를 만드는 데에 도움을 줄 수 있습니다." @@ -839,7 +841,7 @@ addDescription: "설명 추가" userPagePinTip: "각 노트의 메뉴에서 「프로필에 고정」을 선택하는 것으로, 여기에 노트를 표시해 둘 수 있어요." notSpecifiedMentionWarning: "수신자가 선택되지 않은 멘션이 있어요" info: "정보" -userInfo: "사용자 정보" +userInfo: "유저 정보" unknown: "알 수 없음" onlineStatus: "온라인 상태" hideOnlineStatus: "온라인 상태 숨기기" @@ -855,7 +857,7 @@ switchAccount: "계정 바꾸기" enabled: "활성화" disabled: "비활성화" quickAction: "빠른 동작" -user: "사용자" +user: "유저" administration: "관리" accounts: "계정" switch: "전환" @@ -866,8 +868,8 @@ configure: "설정하기" postToGallery: "갤러리에 업로드" postToHashtag: "이 해시태그에 게시" gallery: "갤러리" -recentPosts: "최근 포스트" -popularPosts: "인기 포스트" +recentPosts: "최근 게시물" +popularPosts: "인기 게시물" shareWithNote: "노트로 공유" ads: "광고" expiration: "기한" @@ -896,7 +898,7 @@ whatIsNew: "패치 정보 보기" translate: "번역" translatedFrom: "{x}에서 번역" accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다" -usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다." +usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 유저명은 나중에 변경할 수 없습니다." aiChanMode: "아이 모드" devMode: "개발자 모드" keepCw: "CW 유지하기" @@ -1030,7 +1032,7 @@ correspondingSourceIsAvailable: "소스 코드는 {anchor}에서 받아보실 roles: "역할" role: "역할" noRole: "역할이 없습니다" -normalUser: "일반 사용자" +normalUser: "일반 유저" undefined: "정의되지 않음" assign: "할당" unassign: "할당 취소" @@ -1054,7 +1056,7 @@ thisPostMayBeAnnoyingHome: "홈에 게시" thisPostMayBeAnnoyingCancel: "그만두기" thisPostMayBeAnnoyingIgnore: "이대로 게시" collapseRenotes: "이미 본 리노트를 간략화하기" -collapseRenotesDescription: "반응이나 리노트를 한 노트를 접어서 표시합니다." +collapseRenotesDescription: "리액션이나 리노트를 한 노트를 접어서 표시합니다." internalServerError: "내부 서버 오류" internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다." copyErrorInfo: "오류 정보 복사" @@ -1078,8 +1080,8 @@ resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?" sensitiveWords: "민감한 단어" sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다." sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." -prohibitedWords: "금지 워드" -prohibitedWordsDescription: "설정된 워드가 포함되는 노트를 작성하려고 하면, 에러가 발생하도록 합니다. 줄바꿈으로 구분지어 복수 설정할 수 있습니다." +prohibitedWords: "금지 단어" +prohibitedWordsDescription: "설정된 단어가 포함되는 노트를 작성하려고 하면, 오류가 발생하도록 합니다. 줄바꿈으로 구분지어 복수 설정할 수 있습니다." prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." hiddenTags: "숨긴 해시태그" hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다." @@ -1104,7 +1106,7 @@ audio: "소리" audioFiles: "소리" dataSaver: "데이터 절약 모드" accountMigration: "계정 이동" -accountMoved: "이 사용자는 다음 계정으로 이사했습니다:" +accountMoved: "이 유저는 다음 계정으로 이사했습니다:" accountMovedShort: "이사한 계정입니다" operationForbidden: "사용할 수 없습니다" forceShowAds: "광고를 항상 표시" @@ -1125,8 +1127,8 @@ serverRules: "서버 규칙" pleaseConfirmBelowBeforeSignup: "이 서버에 가입하기 전에 아래 사항을 확인하여 주십시오." pleaseAgreeAllToContinue: "계속하시려면 모든 항목에 동의하십시오." continue: "계속" -preservedUsernames: "예약한 사용자 이름" -preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다." +preservedUsernames: "예약한 유저명" +preservedUsernamesDescription: "예약할 유저명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 유저명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다." createNoteFromTheFile: "이 파일로 노트를 작성" archive: "아카이브" archived: "아카이브 됨" @@ -1140,7 +1142,7 @@ youFollowing: "팔로잉" preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부" preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다." options: "옵션" -specifyUser: "사용자 지정" +specifyUser: "유저 지정" lookupConfirm: "조회 할까요?" openTagPageConfirm: "해시태그의 페이지를 열까요?" specifyHost: "호스트 지정" @@ -1295,8 +1297,8 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "입력된 패스키 messageToFollower: "팔로워에게 보낼 메시지" target: "대상" testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. 실제 환경에서는 사용하지 마세요." -prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)" -prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다." +prohibitedWordsForNameOfUser: "금지 단어 (유저명)" +prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 유저명에 있는 경우, 일반 유저는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 유저는 제한 대상에서 제외됩니다." yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다." yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요." thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해야 볼 수 있도록 설정되어 있습니다." @@ -1331,12 +1333,63 @@ emojiPalette: "이모지 팔레트" postForm: "글 입력란" textCount: "문자 수" information: "정보" +chat: "채팅" +migrateOldSettings: "기존 설정 정보를 이전" +migrateOldSettings_description: "보통은 자동으로 이루어지지만, 어떤 이유로 인해 성공적으로 이전이 이루어지지 않는 경우 수동으로 이전을 실행할 수 있습니다. 현재 설정 정보는 덮어쓰게 됩니다." +compress: "압축" +right: "오른쪽" +bottom: "아래" +top: "위" +embed: "임베드" +settingsMigrating: "설정을 이전하는 중입니다. 잠시 기다려주십시오... (나중에 '환경설정 → 기타 → 기존 설정 정보를 이전'에서 수동으로 이전할 수도 있습니다)" +readonly: "읽기 전용" +goToDeck: "덱으로 돌아가기" _chat: + noMessagesYet: "아직 메시지가 없습니다" + newMessage: "새로운 메시지" + individualChat: "개인 대화" + individualChat_description: "특정 유저와 일대일 채팅을 할 수 있습니다." + roomChat: "룸 채팅" + roomChat_description: "여러 명이 함께 채팅할 수 있습니다.\n또한, 개인 채팅을 허용하지 않은 유저와도 상대방이 수락하면 채팅을 할 수 있습니다." + createRoom: "룸을 생성" + inviteUserToChat: "유저를 초대하여 채팅을 시작하세요" + yourRooms: "생성한 룸" + joiningRooms: "참가 중인 룸" invitations: "초대" + noInvitations: "초대장이 없습니다" + history: "이력" noHistory: "기록이 없습니다" + noRooms: "룸이 없습니다" + inviteUser: "유저를 초대" + sentInvitations: "초대를 보내기" + join: "참여" + ignore: "무시" + leave: "룸을 떠나기" members: "멤버" + searchMessages: "메시지 검색" home: "홈" send: "전송" + newline: "줄바꿈" + muteThisRoom: "이 룸을 뮤트" + deleteRoom: "룸을 삭제" + chatNotAvailableForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅이 활성화되어 있지 않습니다." + chatIsReadOnlyForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅은 읽기 전용입니다. 새로 쓰거나 채팅방을 만들거나 참가할 수 없습니다." + chatNotAvailableInOtherAccount: "상대방 계정에서 채팅 기능을 사용할 수 없는 상태입니다." + cannotChatWithTheUser: "이 유저와 채팅을 시작할 수 없습니다" + cannotChatWithTheUser_description: "채팅을 사용할 수 없는 상태이거나 상대방이 채팅을 열지 않은 상태입니다." + chatWithThisUser: "채팅하기" + thisUserAllowsChatOnlyFromFollowers: "이 유저는 팔로워만 채팅을 할 수 있습니다." + thisUserAllowsChatOnlyFromFollowing: "이 유저는 이 유저가 팔로우하는 유저만 채팅을 허용합니다." + thisUserAllowsChatOnlyFromMutualFollowing: "이 유저는 상호 팔로우하는 유저만 채팅을 허용합니다." + thisUserNotAllowedChatAnyone: "이 유저는 다른 사람의 채팅을 받지 않습니다." + chatAllowedUsers: "채팅을 허용한 상대" + chatAllowedUsers_note: "내가 채팅 메시지를 보낸 상대와는 이 설정과 상관없이 채팅이 가능합니다." + _chatAllowedUsers: + everyone: "누구나" + followers: "자신의 팔로워만" + following: "자신이 팔로우한 유저만" + mutual: "상호 팔로우한 유저만" + none: "아무도 허락하지 않기" _emojiPalette: palettes: "팔레트" enableSyncBetweenDevicesForPalettes: "팔레트의 디바이스 간 동기화를 활성화" @@ -1361,7 +1414,15 @@ _settings: soundsBanner: "클라이언트에서 재생할 소리에 대한 설정을 합니다." timelineAndNote: "타임라인과 노트" makeEveryTextElementsSelectable: "모든 텍스트 요소를 선택할 수 있도록 함" - makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 사용자의 접근성이 나빠질 수도 있습니다." + makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 유저의 접근성이 나빠질 수도 있습니다." + useStickyIcons: "아이콘이 스크롤을 따라가도록 하기" + showNavbarSubButtons: "내비게이션 바에 보조 버튼 표시" + ifOn: "켜져 있을 때" + ifOff: "꺼져 있을 때" + enableSyncThemesBetweenDevices: "기기 간 설치한 테마 동기화" + _chat: + showSenderName: "발신자 이름 표시" + sendOnEnter: "엔터로 보내기" _preferencesProfile: profileName: "프로필 이름" profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요." @@ -1379,12 +1440,12 @@ _accountSettings: requireSigninToViewContents: "콘텐츠 열람을 위해 로그인을 필수로 설정하기" requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다." requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다." - requireSigninToViewContentsDescription3: "원격 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다." + requireSigninToViewContentsDescription3: "리모트 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다." makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기" makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다." makeNotesHiddenBefore: "과거 노트 비공개로 전환하기" makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다." - mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다." + mayNotEffectForFederatedNotes: "리모트 서버에 연합된 노트에는 효과가 없을 수도 있습니다." mayNotEffectSomeSituations: "여기서 설정하는 제한은 모더레이션이나 리모트 서버에서 볼 때 등 일부 환경에서는 적용되지 않을 수도 있습니다." notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트" notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트" @@ -1425,11 +1486,11 @@ _announcement: needConfirmationToRead: "읽음으로 표시하기 전에 확인하기" needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다. '모두 읽음'의 대상에서도 제외됩니다." end: "공지에서 내리기" - tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 사용자 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다." + tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 유저 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다." readConfirmTitle: "읽음으로 표시합니까?" readConfirmText: "〈{title}〉의 내용을 읽음으로 표시합니다." shouldNotBeUsedToPresentPermanentInfo: "신규 유저의 이용 경험에 악영향을 끼칠 수 있으므로, 일시적인 알림 수단으로만 사용하고 고정된 정보에는 사용을 지양하는 것을 추천합니다." - dialogAnnouncementUxWarn: "다이얼로그 형태의 알림이 동시에 2개 이상 존재하는 경우, 사용자 경험에 악영향을 끼칠 수 있으므로 신중히 결정하십시오." + dialogAnnouncementUxWarn: "다이얼로그 형태의 알림이 동시에 2개 이상 존재하는 경우, 유저 경험에 악영향을 끼칠 수 있으므로 신중히 결정하십시오." silence: "조용히 알림" silenceDescription: "활성화하면 공지사항에 대한 알림이 가지 않게 되며, 확인 버튼을 누를 필요가 없게 됩니다." _initialAccountSetting: @@ -1555,53 +1616,53 @@ _achievements: _types: _notes1: title: "미스키 계정 만들었어요" - description: "첫 노트를 작성했습니다" + description: "첫 노트를 게시했다" flavor: "Misskey에 어서 오세요!" _notes10: title: "몇 가지 노트" - description: "10개의 노트를 작성했습니다" + description: "10개의 노트를 게시했다" _notes100: title: "많은 노트" - description: "100개의 노트를 작성했습니다" + description: "100개의 노트를 게시했다" _notes500: title: "노트 범벅" - description: "500개의 노트를 작성했습니다" + description: "500개의 노트를 게시했다" _notes1000: title: "노트가 산더미" - description: "1,000개의 노트를 작성했습니다" + description: "1,000개의 노트를 게시했다" _notes5000: title: "솟아나는 노트" - description: "5,000개의 노트를 작성했습니다" + description: "5,000개의 노트를 게시했다" _notes10000: title: "슈퍼 노트" - description: "10,000개의 노트를 작성했습니다" + description: "10,000개의 노트를 게시했다" _notes20000: - title: "노트가 필요해요" - description: "20,000개의 노트를 작성했습니다" + title: "노트가 더 필요해요" + description: "20,000개의 노트를 게시했다" _notes30000: title: "노트노트노트" - description: "30,000개의 노트를 작성했습니다" + description: "30,000개의 노트를 게시했다" _notes40000: title: "노트 공장" - description: "40,000개의 노트를 작성했습니다" + description: "40,000개의 노트를 게시했다" _notes50000: title: "노트 행성" - description: "50,000개의 노트를 작성했습니다" + description: "50,000개의 노트를 게시했다" _notes60000: title: "노트 퀘이사" - description: "60,000개의 노트를 작성했습니다" + description: "60,000개의 노트를 게시했다" _notes70000: title: "노트 블랙홀" - description: "70,000개의 노트를 작성했습니다" + description: "70,000개의 노트를 게시했다" _notes80000: title: "노트 은하" - description: "80,000개의 노트를 작성했습니다" + description: "80,000개의 노트를 게시했다" _notes90000: title: "노트 우주" - description: "90,000개의 노트를 작성했습니다" + description: "90,000개의 노트를 게시했다" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" - description: "100,000개의 노트를 작성했습니다" + description: "100,000개의 노트를 게시했다" flavor: "이렇게나 쓸 게 있어요?" _login3: title: "초보자 I" @@ -1626,181 +1687,181 @@ _achievements: flavor: "그 유저, 미스키스트이다" _login200: title: "단골 I" - description: "총 200일간 로그인했습니다" + description: "총 로그인한 날이 200일" _login300: title: "단골 II" - description: "총 300일간 로그인했습니다" + description: "총 로그인한 날이 300일" _login400: title: "단골 III" - description: "총 400일간 로그인했습니다" + description: "총 로그인한 날이 400일" _login500: title: "베테랑 I" - description: "총 500일간 로그인했습니다" + description: "총 로그인한 날이 500일" flavor: "제군, 나는 노트가 좋다" _login600: title: "베테랑 II" - description: "총 600일간 로그인했습니다" + description: "총 로그인한 날이 600일" _login700: title: "베테랑 III" - description: "총 700일간 로그인했습니다" + description: "총 로그인한 날이 700일" _login800: title: "노트 마스터 I" - description: "총 800일간 로그인했습니다" + description: "총 로그인한 날이 800일" _login900: title: "노트 마스터 II" - description: "총 900일간 로그인했습니다" + description: "총 로그인한 날이 900일" _login1000: title: "노트 마스터 III" - description: "총 1,000일간 로그인했습니다" + description: "총 로그인한 날이 1,000일" flavor: "Misskey를 사용해 주셔서 감사합니다!" _noteClipped1: title: "클립할 수밖에 없었어" - description: "처음으로 노트를 클립했습니다" + description: "처음으로 노트를 클립했다" _noteFavorited1: title: "별을 바라보는 자" - description: "처음으로 노트를 즐겨찾기했습니다" + description: "처음으로 노트를 즐겨찾기했다" _myNoteFavorited1: title: "별을 원하는 자" - description: "다른 사람이 당신의 노트를 즐겨찾기했습니다" + description: "다른 사람이 당신의 노트를 즐겨찾기했다" _profileFilled: title: "준비 완료" - description: "프로필 설정을 완료했습니다" + description: "프로필 설정을 완료했다" _markedAsCat: title: "나는 고양이다냥!" - description: "계정을 고양이로 설정했습니다냥" + description: "계정을 고양이로 설정했다냥" flavor: "냐냐냐냐냐냐아아아아앙!" _following1: title: "첫 팔로우" - description: "사용자를 처음으로 팔로우했습니다" + description: "유저를 처음으로 팔로우했다" _following10: title: "팔로우, 팔로우" - description: "10명의 사용자를 팔로우했습니다" + description: "10명의 유저를 팔로우했다" _following50: title: "친구 잔뜩" - description: "50명의 사용자를 팔로우했습니다" + description: "50명의 유저를 팔로우했다" _following100: title: "주소록 한 권으론 부족해" - description: "100명의 사용자를 팔로우했습니다" + description: "100명의 유저를 팔로우했다" _following300: title: "친구가 넘쳐나" - description: "300명의 사용자를 팔로우했습니다" + description: "300명의 유저를 팔로우했다" _followers1: title: "첫 팔로워" - description: "사용자가 처음으로 팔로잉했습니다" + description: "유저가 처음으로 팔로잉했다" _followers10: title: "팔로우 미!" - description: "10명의 사용자가 팔로우했습니다" + description: "10명의 유저가 팔로우했다" _followers50: title: "이곳저곳" - description: "50명의 사용자가 팔로우했습니다" + description: "50명의 유저가 팔로우했다" _followers100: title: "인기왕" - description: "100명의 사용자가 팔로우했습니다" + description: "100명의 유저가 팔로우했다" _followers300: title: "줄 좀 서봐요" - description: "100명의 사용자가 팔로우했습니다" + description: "100명의 유저가 팔로우했다" _followers500: title: "기지국" - description: "500명의 사용자가 팔로우했습니다" + description: "500명의 유저가 팔로우했다" _followers1000: title: "유명인사" - description: "1,000명의 사용자가 팔로우했습니다" + description: "1,000명의 유저가 팔로우했다" _collectAchievements30: title: "도전 과제 콜렉터" - description: "30개의 도전과제를 획득했습니다" + description: "30개의 도전과제를 획득했다" _viewAchievements3min: title: "저 도전과제 좋아해요" - description: "도전 과제 목록을 3분 이상 쳐다봤습니다" + description: "도전 과제 목록을 3분 이상 쳐다봤다" _iLoveMisskey: title: "I Love Misskey" - description: "\"I ❤ #Misskey\"를 포스트했습니다" + description: "\"I ❤ #Misskey\"를 게시했다" flavor: "Misskey를 이용해 주셔서 감사합니다! ― 개발 팀" _foundTreasure: title: "보물찾기" - description: "숨겨진 보물을 발견했습니다" + description: "숨겨진 보물을 발견했다" _client30min: title: "잠시 쉬어요" - description: "클라이언트를 시작하고 30분이 경과하였습니다" + description: "클라이언트를 시작하고 30분이 경과했다" _client60min: title: "No \"Miss\" in Misskey" - description: "클라이언트를 시작하고 60분이 경과하였습니다" + description: "클라이언트를 시작하고 60분이 경과했다" _noteDeletedWithin1min: title: "있었는데요 없었습니다" - description: "노트를 포스트한 후 1분 이내에 삭제했습니다" + description: "노트를 게시한 후 1분 이내에 삭제했다" _postedAtLateNight: title: "올빼미" - description: "한밤중에 노트를 포스트했습니다" + description: "한밤중에 노트를 게시했다" flavor: "잠 좀 자세요. 걱정돼요." _postedAt0min0sec: title: "정각" - description: "0분 0초 정각에 노트를 작성했습니다" + description: "0분 0초 정각에 노트를 게시했다" flavor: "째깍 째깍 째깍 땡!" _selfQuote: title: "혼잣말" - description: "자기 노트를 인용했습니다" + description: "자기 노트를 인용했다" _htl20npm: title: "타임라인 폭주 중" - description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다" + description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었다" _viewInstanceChart: title: "애널리스트" - description: "서버의 차트를 열었습니다" + description: "서버의 차트를 열었다" _outputHelloWorldOnScratchpad: title: "Hello, world!" - description: "스크래치패드에서 hello world를 출력했습니다" + description: "스크래치패드에서 hello world를 출력했다" _open3windows: title: "멀티 윈도우" - description: "3개 이상의 창을 열었습니다" + description: "3개 이상의 창을 열었다" _driveFolderCircularReference: title: "순환 참조" - description: "드라이브 폴더에 스스로를 넣게 했습니다" + description: "드라이브 폴더에 스스로를 넣게 했다" _reactWithoutRead: title: "읽고 답하긴 하시는 건가요?" - description: "100자가 넘는 노트를 작성한 지 3초 안에 반응했어요" + description: "100자가 넘는 노트를 작성한 지 3초 안에 리액션했다" _clickedClickHere: - title: "여기를 누르세요" - description: "여기를 눌렀습니다" + title: "여길 눌러보세요" + description: "여기를 눌렀다" _justPlainLucky: title: "그냥 운이 좋았어" - description: "매 10초마다 0.01%의 확률로 달성됩니다" + description: "매 10초마다 0.01%의 확률로 달성된다" _setNameToSyuilo: title: "신 콤플렉스" - description: "이름을 syuilo로 설정했습니다" + description: "이름을 syuilo로 설정했다" _passedSinceAccountCreated1: title: "1주년" - description: "계정을 생성하고 1년이 지났습니다" + description: "계정을 생성하고 1년이 지났다" _passedSinceAccountCreated2: title: "2주년" - description: "계정을 생성하고 2년이 지났습니다" + description: "계정을 생성하고 2년이 지났다" _passedSinceAccountCreated3: title: "3주년" - description: "계정을 생성하고 3년이 지났습니다" + description: "계정을 생성하고 3년이 지났다" _loggedInOnBirthday: title: "생일 축하합니다!" - description: "생일에 로그인했습니다" + description: "생일에 로그인했다" _loggedInOnNewYearsDay: title: "새해 복 많이 받으세요" - description: "새해 첫 날에 로그인했습니다" + description: "새해 첫 날에 로그인했다" flavor: "올해에도 저희 서버에 관심을 가져 주셔서 감사합니다" _cookieClicked: title: "쿠키를 클릭하는 게임" - description: "쿠키를 클릭했습니다" - flavor: "소프트웨어 착각하지 않으셨나요?" + description: "쿠키를 클릭했다" + flavor: "소프트웨어 착각하지 않았어?" _brainDiver: title: "Brain Diver" - description: "Brain Diver로의 링크를 첨부했습니다" + description: "Brain Diver로의 링크를 첨부했다" flavor: "Misskey-Misskey La-Tu-Ma" _smashTestNotificationButton: title: "테스트 과잉" - description: "매우 짧은 시간 안에 알림 테스트를 여러 번 수행했습니다" + description: "매우 짧은 시간 안에 알림 테스트를 여러 번 수행했다" _tutorialCompleted: title: "Misskey 입문자 과정 수료증" - description: "튜토리얼을 완료했습니다" + description: "튜토리얼을 완료했다" _bubbleGameExplodingHead: title: "🤯" description: "버블 게임에서 가장 큰 물건을 내놓았다" _bubbleGameDoubleExplodingHead: title: "더블 🤯" - description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았다." + description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았다" flavor: "이 정도만 도시락통에 🤯 🤯 조금만 더" _role: new: "새 역할 생성" @@ -1810,7 +1871,7 @@ _role: permission: "역할 권한" descriptionOfPermission: "조정자는 기본적인 조정 작업을 진행할 수 있습니다.\n관리자는 서버의 모든 설정을 변경할 수 있습니다." assignTarget: "할당 대상" - descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." + descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 유저를 자동으로 포함되게 할 수 있습니다." manual: "수동" manualRoles: "수동 역할" conditional: "조건부" @@ -1818,7 +1879,7 @@ _role: condition: "조건" isConditionalRole: "조건부 역할입니다." isPublic: "역할 공개" - descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다." + descriptionOfIsPublic: "역할에 할당된 유저를 누구나 볼 수 있습니다. 또한 유저 프로필에 이 역할이 표시됩니다." options: "옵션" policies: "정책" baseRole: "기본 역할" @@ -1831,8 +1892,10 @@ _role: descriptionOfIsExplorable: "활성화하면 역할 타임라인을 공개합니다. 비활성화 시 타임라인이 공개되지 않습니다." displayOrder: "표시 순서" descriptionOfDisplayOrder: "값이 클 수록 UI에서 먼저 표시됩니다." + preserveAssignmentOnMoveAccount: "이전 대상 계정에도 할당 상태 전달" + preserveAssignmentOnMoveAccount_description: "켜면 이 역할이 부여된 계정이 이전될 때 마이그레이션 대상 계정에도 이 역할이 승계됩니다." canEditMembersByModerator: "모더레이터의 역할 수정 허용" - descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다." + descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 유저를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다." priority: "우선순위" _priority: low: "낮음" @@ -1858,8 +1921,8 @@ _role: webhookMax: "만들 수 있는 Webhook 수" clipMax: "만들 수 있는 클립 수" noteEachClipsMax: "클립에 넣을 수 있는 노트 수" - userListMax: "만들 수 있는 사용자 리스트 수" - userEachUserListsMax: "사용자 리스트에 넣을 수 있는 사용자 수" + userListMax: "만들 수 있는 유저 리스트 수" + userEachUserListsMax: "유저 리스트에 넣을 수 있는 유저 수" rateLimitFactor: "요청 빈도 제한" descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." canHideAds: "광고 숨기기" @@ -1871,23 +1934,24 @@ _role: canImportFollowing: "팔로우 가져오기 허용" canImportMuting: "뮤트 목록 가져오기 허용" canImportUserLists: "리스트 목록 가져오기 허용" + chatAvailability: "채팅을 허락" _condition: roleAssignedTo: "수동 역할에 이미 할당됨" - isLocal: "로컬 사용자" - isRemote: "원격 사용자" - isCat: "고양이 사용자" - isBot: "봇 사용자" - isSuspended: "정지된 사용자" - isLocked: "잠금 계정 사용자" - isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 사용자" + isLocal: "로컬 유저" + isRemote: "리모트 유저" + isCat: "고양이 유저" + isBot: "봇 유저" + isSuspended: "정지된 유저" + isLocked: "잠금 계정 유저" + isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 유저" createdLessThan: "가입한 지 다음 일수 이내인 유저" createdMoreThan: "가입한 지 다음 일수 이상인 유저" followersLessThanOrEq: "팔로워 수가 다음 이하인 유저" - followersMoreThanOrEq: "팔로워 수가 다음보다 많은 사용자" + followersMoreThanOrEq: "팔로워 수가 다음보다 많은 유저" followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저" - followingMoreThanOrEq: "팔로잉 수가 다음보다 많은 사용자" + followingMoreThanOrEq: "팔로잉 수가 다음보다 많은 유저" notesLessThanOrEq: "노트 수가 다음 이하인 유저" - notesMoreThanOrEq: "노트 수가 다음보다 많은 사용자" + notesMoreThanOrEq: "노트 수가 다음보다 많은 유저" and: "다음을 모두 만족" or: "다음을 하나라도 만족" not: "다음을 만족하지 않음" @@ -1929,7 +1993,7 @@ _ad: adsSettings: "광고 표시 설정" notesPerOneAd: "실시간으로 갱신되는 타임라인에서 광고를 노출시키는 간격 (노트 당)" setZeroToDisable: "0으로 지정하면 실시간 타임라인에서의 광고를 비활성화합니다" - adsTooClose: "광고의 표시 간격이 매우 작아, 사용자 경험에 부정적인 영향을 미칠 수 있습니다." + adsTooClose: "광고의 표시 간격이 매우 작아, 유저 경험에 부정적인 영향을 미칠 수 있습니다." _forgotPassword: enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다." ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오." @@ -2097,6 +2161,7 @@ _sfx: noteMy: "내 노트" notification: "알림" reaction: "리액션 선택" + chatMessage: "채팅 메시지" _soundSettings: driveFile: "드라이브에 있는 오디오를 사용" driveFileWarn: "드라이브에 있는 파일을 선택하세요." @@ -2183,7 +2248,7 @@ _permissions: "write:pages": "페이지를 수정합니다" "read:page-likes": "페이지의 좋아요를 확인합니다" "write:page-likes": "페이지에 좋아요를 추가하거나 취소합니다" - "read:user-groups": "사용자 그룹 보기" + "read:user-groups": "유저 그룹 보기" "write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다" "read:channels": "채널을 보기" "write:channels": "채널을 추가하거나 삭제합니다" @@ -2195,23 +2260,23 @@ _permissions: "write:flash": "Play를 조작합니다" "read:flash-likes": "Play의 좋아요를 봅니다" "write:flash-likes": "Play의 좋아요를 조작합니다" - "read:admin:abuse-user-reports": "사용자 신고 보기" - "write:admin:delete-account": "사용자 계정 삭제하기" - "write:admin:delete-all-files-of-a-user": "모든 사용자 파일 삭제하기" + "read:admin:abuse-user-reports": "유저 신고 보기" + "write:admin:delete-account": "유저 계정 삭제하기" + "write:admin:delete-all-files-of-a-user": "모든 유저 파일 삭제하기" "read:admin:index-stats": "데이터베이스 색인 정보 보기" "read:admin:table-stats": "데이터베이스 테이블 정보 보기" - "read:admin:user-ips": "사용자 IP 주소 보기" + "read:admin:user-ips": "유저 IP 주소 보기" "read:admin:meta": "인스턴스 메타데이터 보기" - "write:admin:reset-password": "사용자 비밀번호 재설정하기" - "write:admin:resolve-abuse-user-report": "사용자 신고 처리하기" + "write:admin:reset-password": "유저 비밀번호 재설정하기" + "write:admin:resolve-abuse-user-report": "유저 신고 처리하기" "write:admin:send-email": "이메일 보내기" "read:admin:server-info": "서버 정보 보기" "read:admin:show-moderation-log": "조정 기록 보기" - "read:admin:show-user": "사용자 개인정보 보기" - "write:admin:suspend-user": "사용자 정지하기" - "write:admin:unset-user-avatar": "사용자 아바타 삭제하기" - "write:admin:unset-user-banner": "사용자 배너 삭제하기" - "write:admin:unsuspend-user": "사용자 정지 해제하기" + "read:admin:show-user": "유저 개인정보 보기" + "write:admin:suspend-user": "유저 정지하기" + "write:admin:unset-user-avatar": "유저 아바타 삭제하기" + "write:admin:unset-user-banner": "유저 배너 삭제하기" + "write:admin:unsuspend-user": "유저 정지 해제하기" "write:admin:meta": "인스턴스 메타데이터 수정하기" "write:admin:user-note": "조정 기록 수정하기" "write:admin:roles": "역할 수정하기" @@ -2225,15 +2290,15 @@ _permissions: "write:admin:avatar-decorations": "아바타 꾸미기 수정하기" "read:admin:avatar-decorations": "아바타 꾸미기 보기" "write:admin:federation": "연합 정보 수정하기" - "write:admin:account": "사용자 계정 수정하기" - "read:admin:account": "사용자 정보 보기" + "write:admin:account": "유저 계정 수정하기" + "read:admin:account": "유저 정보 보기" "write:admin:emoji": "이모지 수정하기" "read:admin:emoji": "이모지 보기" "write:admin:queue": "작업 대기열 수정하기" "read:admin:queue": "작업 대기열 정보 보기" "write:admin:promo": "홍보 기록 수정하기" - "write:admin:drive": "사용자 드라이브 수정하기" - "read:admin:drive": "사용자 드라이브 정보 보기" + "write:admin:drive": "유저 드라이브 수정하기" + "read:admin:drive": "유저 드라이브 정보 보기" "read:admin:stream": "관리자용 Websocket API 사용하기" "write:admin:ad": "광고 수정하기" "read:admin:ad": "광고 보기" @@ -2244,6 +2309,7 @@ _permissions: "read:federation": "연합 정보 불러오기" "write:report-abuse": "위반 내용 신고하기" "write:chat": "대화를 시작하거나 메시지를 보냅니다" + "read:chat": "채팅 열람하기" _auth: shareAccessTitle: "어플리케이션의 접근 허가" shareAccess: "‘{name}’에서 계정에 접근하는 것을 허용하시겠습니까?" @@ -2254,7 +2320,7 @@ _auth: callback: "앱으로 돌아갑니다" accepted: "접근 권한이 부여되었습니다." denied: "접근이 거부되었습니다" - scopeUser: "다음 사용자로 활동하고 있습니다." + scopeUser: "다음 유저로 활동하고 있습니다." pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오." byClickingYouWillBeRedirectedToThisUrl: "접근을 허용하면 자동으로 다음 URL로 이동합니다." _antennaSources: @@ -2291,7 +2357,7 @@ _widgets: postForm: "글 입력란" slideshow: "슬라이드 쇼" button: "버튼" - onlineUsers: "온라인 사용자" + onlineUsers: "온라인 유저" jobQueue: "작업 대기열" serverMetric: "서버 통계" aiscript: "AiScript 콘솔" @@ -2301,7 +2367,7 @@ _widgets: _userList: chooseList: "리스트 선택" clicker: "클리커" - birthdayFollowings: "오늘이 생일인 사용자" + birthdayFollowings: "오늘이 생일인 유저" _cw: hide: "숨기기" show: "더 보기" @@ -2353,7 +2419,7 @@ _postForm: f: "작성해주시길 기다리고 있어요..." _profile: name: "이름" - username: "사용자 이름" + username: "유저명" description: "자기소개" youCanIncludeHashtags: "해시 태그를 포함할 수 있습니다." metadata: "추가 정보" @@ -2384,7 +2450,7 @@ _charts: apRequest: "요청" usersIncDec: "유저 수 증감" usersTotal: "유저 수 합계" - activeUsers: "활동 사용자 수" + activeUsers: "활동 유저 수" notesIncDec: "노트 수 증감" localNotesIncDec: "로컬 노트 수 증감" remoteNotesIncDec: "리모트 노트 수 증감" @@ -2395,8 +2461,8 @@ _charts: storageUsageTotal: "스토리지 사용량 합계" _instanceCharts: requests: "요청" - users: "사용자 수 차이" - usersTotal: "누적 사용자 수" + users: "유저 수 차이" + usersTotal: "누적 유저 수" notes: "노트 수 증감" notesTotal: "누적 노트 수" ff: "팔로잉/팔로워 증감" @@ -2492,13 +2558,14 @@ _notification: newNote: "새 게시물" unreadAntennaNote: "안테나 {name}" roleAssigned: "역할이 부여 되었습니다." + chatRoomInvitationReceived: "채팅 룸에 초대받았습니다" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" achievementEarned: "도전 과제를 달성했습니다" testNotification: "알림 테스트" checkNotificationBehavior: "알림 표시를 체크하기" sendTestNotification: "테스트 알림 보내기" notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시됩니다" - reactedBySomeUsers: "{n}명이 반응했습니다" + reactedBySomeUsers: "{n}명이 리액션했습니다" likedBySomeUsers: "{n}명이 좋아요를 했습니다" renotedBySomeUsers: "{n}명이 리노트했습니다" followedBySomeUsers: "{n}명에게 팔로우됨" @@ -2509,17 +2576,18 @@ _notification: createTokenDescription: "만약 기억이 나지 않는다면 '{text}'를 통해 액세스 토큰을 삭제해 주세요." _types: all: "전부" - note: "사용자의 새 글" + note: "유저의 새 글" follow: "팔로잉" mention: "멘션" reply: "답글" renote: "리노트" quote: "인용" - reaction: "반응" + reaction: "리액션" pollEnded: "투표가 종료됨" receiveFollowRequest: "팔로우 요청을 받았을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때" - roleAssigned: "역할이 부여 됨" + roleAssigned: "역할이 부여됨" + chatRoomInvitationReceived: "채팅 룸에 초대받음" achievementEarned: "도전 과제 획득" exportCompleted: "추출을 성공함" login: "로그인" @@ -2533,6 +2601,9 @@ _notification: _deck: alwaysShowMainColumn: "메인 칼럼 항상 표시" columnAlign: "칼럼 정렬" + columnGap: "칼럼 간 여백" + deckMenuPosition: "덱 메뉴 위치" + navbarPosition: "내비게이션 바 위치" addColumn: "칼럼 추가" newNoteNotificationSettings: "새 노트 알림 설정" configureColumn: "칼럼 설정" @@ -2604,10 +2675,10 @@ _abuseReport: mail: "이메일" webhook: "Webhook" _captions: - mail: "모더레이터 권한을 가진 사용자의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)" + mail: "모더레이터 권한을 가진 유저의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)" webhook: "지정한 SystemWebhook에 알림을 보냅니다 (신고를 받은 때와 해결했을 때에 송신)" keywords: "키워드" - notifiedUser: "알릴 사용자" + notifiedUser: "알릴 유저" notifiedWebhook: "사용할 Webhook" deleteConfirm: "수신자를 삭제하시겠습니까?" _moderationLogTypes: @@ -2626,11 +2697,11 @@ _moderationLogTypes: deleteDriveFile: "파일 삭제" deleteNote: "노트 삭제" createGlobalAnnouncement: "전역 공지사항 생성" - createUserAnnouncement: "사용자 공지사항 만들기" + createUserAnnouncement: "유저에게 공지사항 만들기" updateGlobalAnnouncement: "모든 공지사항 수정" - updateUserAnnouncement: "사용자 공지사항 수정" + updateUserAnnouncement: "유저의 공지사항 수정" deleteGlobalAnnouncement: "모든 공지사항 삭제" - deleteUserAnnouncement: "사용자 공지사항 삭제" + deleteUserAnnouncement: "유저의 공지사항 삭제" resetPassword: "비밀번호 재설정" suspendRemoteInstance: "리모트 서버를 정지" unsuspendRemoteInstance: "리모트 서버의 정지를 해제" @@ -2658,7 +2729,8 @@ _moderationLogTypes: deleteAccount: "계정을 삭제" deletePage: "페이지를 삭제" deleteFlash: "Play를 삭제" - deleteGalleryPost: "갤러리 포스트를 삭제" + deleteGalleryPost: "갤러리 게시물을 삭제" + deleteChatRoom: "채팅 룸 삭제" updateProxyAccountDescription: "프록시 계정의 설명 업데이트" _fileViewer: title: "파일 상세" @@ -2871,7 +2943,7 @@ _embedCodeGen: _selfXssPrevention: warning: "경고" title: "“이 화면에 뭔가를 붙여넣어라\"는 것은 모두 사기입니다." - description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다." + description1: "여기에 무언가를 붙여넣으면 악의적인 유저에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다." description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오." description3: "자세한 내용은 여기를 확인해 주세요. {link}" _followRequest: @@ -2904,8 +2976,8 @@ _captcha: title: "CAPTCHA 검증을 실패했습니다." text: "설정이 올바른지 다시 한 번 확인해보세요." _unknown: - title: "CAPTCHA 에러" - text: "알 수 없는 에러가 발생했습니다." + title: "CAPTCHA 오류" + text: "알 수 없는 오류가 발생했습니다." _bootErrors: title: "로딩이 실패함" serverError: "잠시 기다렸다가 다시 로드해도 여전히 문제가 해결되지 않으면 아래 Error ID와 함께 서버 관리자에게 연락해 주세요." @@ -2922,7 +2994,7 @@ _search: searchScopeAll: "전체" searchScopeLocal: "로컬" searchScopeServer: "서버 지정" - searchScopeUser: "사용자 지정" + searchScopeUser: "유저 지정" pleaseEnterServerHost: "서버의 호스트를 입력해 주세요." pleaseSelectUser: "유저를 선택해주세요" serverHostPlaceholder: "예: misskey.example.com" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 38e4814373..bc8c86f2c3 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -5,6 +5,7 @@ introMisskey: "Welkom! Misskey is een open source, gedecentraliseerde microblogd poweredByMisskeyDescription: "{name} is één van de services die door het open source platform Misskey wordt geleverd (het wordt ook wel een \"Misskey server genmoemd\")." monthAndDay: "{day} {month}" search: "Zoeken" +reset: "Herstellen" notifications: "Meldingen" username: "Gebruikersnaam" password: "Wachtwoord" @@ -48,6 +49,7 @@ pin: "Vastmaken aan profielpagina" unpin: "Losmaken van profielpagina" copyContent: "Kopiëren inhoud" copyLink: "Kopiëren link" +copyRemoteLink: "Remote-link kopiëren" copyLinkRenote: "" delete: "Verwijderen" deleteAndEdit: "Verwijderen en bewerken" @@ -63,6 +65,7 @@ copyFileId: "Kopieer veld ID" copyFolderId: "Kopieer folder ID" copyProfileUrl: "Kopieer profiel URL" searchUser: "Zoeken een gebruiker" +searchThisUsersNotes: "Notities van deze gebruiker doorzoeken" reply: "Antwoord" loadMore: "Laad meer" showMore: "Toon meer" @@ -129,9 +132,12 @@ emojiPicker: "Emoji kiezer" pinnedEmojisForReactionSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren" pinnedEmojisSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren" emojiPickerDisplay: "Emoji kiezer weergave" +overwriteFromPinnedEmojisForReaction: "Overschrijven met reactieinstellingen" +overwriteFromPinnedEmojis: "Overschrijven met algemene instellingen" reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen" rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen" attachCancel: "Verwijder bijlage" +deleteFile: "Bestand verwijderen" markAsSensitive: "Markeren als NSFW" unmarkAsSensitive: "Geen NSFW" enterFileName: "Invoeren bestandsnaam" @@ -147,6 +153,7 @@ suspendConfirm: "Ben je zeker dat je deze account wil suspenderen?" unsuspendConfirm: "Ben je zeker dat je deze account wil opnieuw aanstellen?" selectList: "Kies een lijst." selectAntenna: "Kies een antenne" +createAntenna: "Antenne aanmaken" selectWidget: "Kies een widget" editWidgets: "Bewerk widgets" editWidgetsExit: "Klaar" @@ -158,6 +165,7 @@ emojiUrl: "URL emoji" addEmoji: "Toevoegen emoji" settingGuide: "Aanbevolen instellingen" cacheRemoteFiles: "Externe bestanden cachen" +cacheRemoteFilesDescription: "Als deze instelling uitgeschakeld is worden bestanden altijd direct van remote servers geladen. Hiermee wordt opslagruimte bespaard, maar doordat er geen thumbnails worden gegenereerd, zal netwerkverkeer toenemen." flagAsBot: "Markeer dit account als een robot." flagAsBotDescription: "Als dit account van een programma wordt beheerd, zet deze vlag aan. Het aanzetten helpt andere ontwikkelaars om bijvoorbeeld onbedoelde feedback loops te doorbreken of om Misskey meer geschikt te maken." flagAsCat: "Markeer dit account als een kat." @@ -168,6 +176,10 @@ autoAcceptFollowed: "Accepteer verzoeken om jezelf te volgen vanzelf als je de v addAccount: "Account toevoegen" loginFailed: "Aanmelding mislukt." showOnRemote: "Toon op de externe instantie." +continueOnRemote: "Verder op remote server" +chooseServerOnMisskeyHub: "Kies een server van de Misskey Hub" +specifyServerHost: "Serverhost uitkiezen" +inputHostName: "Domein invullen" general: "Algemeen" wallpaper: "Achtergrond" setWallpaper: "Achtergrond instellen" @@ -178,6 +190,7 @@ followConfirm: "Weet je zeker dat je {name} wilt volgen?" proxyAccount: "Proxy account" proxyAccountDescription: "Een proxy-account is een account dat onder bepaalde voorwaarden fungeert als externe volger voor gebruikers. Als een gebruiker bijvoorbeeld een externe gebruiker aan de lijst toevoegt, wordt de activiteit van de externe gebruiker niet aan de server geleverd als geen lokale gebruiker die gebruiker volgt, dus het proxy-account volgt in plaats daarvan." host: "Server" +selectSelf: "Mezelf kiezen" selectUser: "Kies een gebruiker" recipient: "Ontvanger" annotation: "Reacties" @@ -192,6 +205,7 @@ perHour: "Per uur" perDay: "Per dag" stopActivityDelivery: "Stop met versturen activiteiten" blockThisInstance: "Blokkeer deze server" +mediaSilenceThisInstance: "Media van deze server dempen" operations: "Verwerkingen" software: "Software" version: "Versie" @@ -211,6 +225,11 @@ clearCachedFiles: "Cache opschonen" clearCachedFilesConfirm: "Weet je zeker dat je alle externe bestanden in de cache wilt verwijderen?" blockedInstances: "Geblokkeerde servers" blockedInstancesDescription: "Maak een lijst van de servers die moeten worden geblokkeerd, gescheiden door regeleinden. Geblokkeerde servers kunnen niet meer communiceren met deze server." +silencedInstancesDescription: "Geef de hostnamen van de servers die je wil dempen op, elk op hun eigen regel. Alle accounts die bij de opgegeven servers horen worden als gedempt behandeld, kunnen alleen maar volgverzoeken maken, en kunnen lokale accounts niet vermelden als ze niet gevolgd worden. Geblokkeerde servers worden hier niet door beïnvloed." +mediaSilencedInstances: "Media-gedempte servers" +mediaSilencedInstancesDescription: "Geef de hostnamen van de servers die je wil media-dempen op, elk op hun eigen regel. Alle accounts die bij de opgegeven servers horen worden als gedempt behandeld, en kunnen geen eigen emojis gebruiken. Geblokkeerde servers worden hier niet door beïnvloed." +federationAllowedHosts: "Servers die mogen federeren " +federationAllowedHostsDescription: "Geef de hostnamen van de servers die mogen federeren op, elk op hun eigen regel." muteAndBlock: "Gedempt en geblokkeerd" mutedUsers: "Gedempte gebruikers" blockedUsers: "Geblokkeerde gebruikers" @@ -255,6 +274,7 @@ removed: "Succesvol verwijderd" removeAreYouSure: "Weet je zeker dat je \"{x}\" wil verwijderen?" deleteAreYouSure: "Weet je zeker dat je \"{x}\" wil verwijderen?" resetAreYouSure: "Resetten?" +areYouSure: "Weet je het zeker?" saved: "Opgeslagen" upload: "Uploaden" keepOriginalUploading: "Origineel beeld behouden." @@ -268,6 +288,7 @@ uploadFromUrlMayTakeTime: "Het kan even duren voordat het uploaden voltooid is." explore: "Verkennen" messageRead: "Lezen" noMoreHistory: "Er is geen verdere geschiedenis" +startChat: "Chat starten" nUsersRead: "gelezen door {n}" agreeTo: "Ik stem in met {0}" start: "Aan de slag" @@ -294,12 +315,15 @@ selectFile: "Kies een bestand" selectFiles: "Selecteer bestanden" selectFolder: "Kies een map" selectFolders: "Kies mappen" +fileNotSelected: "Geen bestand geselecteerd" renameFile: "Wijzig bestandsnaam" folderName: "Mapnaam" createFolder: "Map aanmaken" renameFolder: "Map hernoemen" deleteFolder: "Map verwijderen" +folder: "Map" addFile: "Bestand toevoegen" +showFile: "Bestanden weergeven" emptyDrive: "Jouw Drive is leeg." emptyFolder: "Deze map is leeg" unableToDelete: "Kan niet worden verwijderd" @@ -355,8 +379,11 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Inschakelen hCaptcha" hcaptchaSiteKey: "Site sleutel" hcaptchaSecretKey: "Geheime sleutel" +mcaptcha: "mCaptcha" +enableMcaptcha: "mCaptcha activeren" mcaptchaSiteKey: "Site sleutel" mcaptchaSecretKey: "Geheime sleutel" +mcaptchaInstanceUrl: "mCaptcha server-URL" recaptcha: "reCAPTCHA" enableRecaptcha: "Inschakelen reCAPTCHA" recaptchaSiteKey: "Site sleutel" @@ -371,6 +398,7 @@ name: "Naam" antennaSource: "Bron antenne" antennaKeywords: "Sleutelwoorden" antennaExcludeKeywords: "Blokkeerwoorden" +antennaExcludeBots: "Bot-accounts uitsluiten" withReplies: "Antwoorden toevoegen" connectedTo: "De volgende accounts zijn verbonden" notesAndReplies: "Berichten en reacties" @@ -421,7 +449,13 @@ retype: "Opnieuw invoeren" noteOf: "Notitie van {user}" quoteAttached: "Citaat" quoteQuestion: "Toevoegen als citaat?" +signinOrContinueOnRemote: "Ga naar je eigen instantie of registreer je/log in op deze server om door te gaan." invitations: "Uitnodigen" +menuStyle: "Menustijl" +style: "Stijl" +drawer: "Lade" +popup: "Pop-up" +showReactionsCount: "Zie het aantal reacties op notities" dashboard: "Overzicht" local: "Lokaal" remote: "Remote" @@ -437,16 +471,41 @@ numberOfDays: "Aantal dagen" hideThisNote: "Verberg deze notitie" showFeaturedNotesInTimeline: "Laat featured notities in tijdlijn zien" sound: "Geluid" +notUseSound: "Geluid uitschakelen" +useSoundOnlyWhenActive: "Geluid alleen inschakelen wanneer Misskey actief is" +uiInspector: "UI-inspecteur" +unsetUserAvatar: "Avatar verwijderen" +unsetUserAvatarConfirm: "Weet je zeker dat je je avatar wil verwijderen?" +unsetUserBanner: "Banner verwijderen" +unsetUserBannerConfirm: "Weet je zeker dat je je banner wil verwijderen?" +expandTweet: "Notitie uitklappen" +adminPermission: "Administratorrechten" smtpHost: "Server" smtpUser: "Gebruikersnaam" smtpPass: "Wachtwoord" +wordMuteDescription: "Minimaliseert notities die het gespecificeerde woord of zin bevatten. Geminimaliseerde notities kunnen worden weergegeven door er op te klikken." +hardWordMute: "Harde woorddemping" +showMutedWord: "Gedempte woorden weergeven" +hardWordMuteDescription: "Verbert notities die het gespecificeerde woord of zin bevatten. In tegenstelling tot woorddemping wordt de notitie volledig verborgen." +userSaysSomethingAbout: "{name} zei iets over '{word}'" +copiedToClipboard: "Naar het klembord gekopieerd" +theKeywordWhenSearchingForCustomEmoji: "Dit is het keyword dat gebruikt wordt bij het zoeken naar eigen emojis." +fillAbuseReportDescription: "Vul s.v.p. de details in over deze melding. Geef, als het over een specifieke notitie gaat, ook de URL op." +reloadToApplySetting: "Deze instelling gaat pas in nadat de pagina herladen is. Nu herladen?" clearCache: "Cache opschonen" info: "Over" user: "Gebruikers" +noInquiryUrlWarning: "Contact-URL niet opgegeven" muteThread: "Discussies dempen " unmuteThread: "Dempen van discussie ongedaan maken" +followingVisibility: "Zichtbaarheid van gevolgden" +followersVisibility: "Zichtbaarheid van volgers" +incorrectTotp: "Het eenmalige wachtwoord is incorrect of verlopen" hide: "Verbergen" searchByGoogle: "Zoeken" +threeMonths: "3 maanden" +oneYear: "1 jaar" +threeDays: "3 dagen" cropImage: "Afbeelding bijsnijden" cropImageAsk: "Bijsnijdengevraagd" file: "Bestanden" @@ -457,9 +516,28 @@ pushNotificationAlreadySubscribed: "Pushberichtrn al ingeschakeld" windowMaximize: "Maximaliseren" windowRestore: "Herstellen" loggedInAsBot: "Momenteel als bot ingelogd" +correspondingSourceIsAvailable: "De bijbehorende broncode is beschikbaar bij {anchor}" +invalidParamErrorDescription: "De aanvraagparameters zijn ongeldig. Dit komt meestal door een bug, maar kan ook omdat de invoer te lang is of iets dergelijks." +collapseRenotes: "Renotes die je al gezien hebt, inklappen" +collapseRenotesDescription: "Klapt notities in waar je al op gereageerd hebt of die je al gerenotet hebt." +prohibitedWords: "Verboden woorden" +prohibitedWordsDescription: "Activeert een foutmelding als er geprobeerd wordt een notitie met de ingestelde woorden te plaatsen. Meerdere woorden kunnen worden ingesteld, elk op hun eigen regel." +hiddenTags: "Verborgen hashtags" +hiddenTagsDescription: "Selecteer tags die niet worden weergegeven in de trends. Meerdere tags kunnen worden geregistreerd, elk op hun eigen regel." +enableStatsForFederatedInstances: "Statistieken van remote servers ontvangen" +limitWidthOfReaction: "Limiteert de maximale breedte van reacties en geef ze verkleind weer" +audio: "Audio" +audioFiles: "Audio" +archived: "Gearchiveerd" +unarchive: "Dearchiveren" +lookupConfirm: "Weet je zeker dat je dit wil opzoeken?" +openTagPageConfirm: "Wil je deze hashtagpagina openen?" +specifyHost: "Specificeer host" icon: "Avatar" replies: "Antwoord" renotes: "Herdelen" +followingOrFollower: "Gevolgd of volger" +confirmShowRepliesAll: "Dit is een onomkeerbare operatie. Weet je zeker dat reacties op anderen van iedereen die je volgt, wil weergeven in je tijdlijn?" information: "Over" _chat: invitations: "Uitnodigen" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 57a78471db..b313c4ce1f 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -424,7 +424,7 @@ antennaExcludeBots: "排除机器人账户" antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" notifyAntenna: "开启通知" withFileAntenna: "仅带有附件的帖子" -hideNotesInSensitiveChannel: "隐藏敏感频道内的帖子" +excludeNotesInSensitiveChannel: "排除敏感频道内的帖子" enableServiceworker: "启用 ServiceWorker" antennaUsersDescription: "指定用户名,一行一个" caseSensitive: "区分大小写" @@ -1341,6 +1341,9 @@ right: "右" bottom: "下" top: "上" embed: "嵌入" +settingsMigrating: "正在迁移设置,请稍候。(之后也可以在设置 → 其它 → 迁移旧设置来手动迁移)" +readonly: "只读" +goToDeck: "返回至 Deck" _chat: noMessagesYet: "还没有消息" newMessage: "新消息" @@ -1370,6 +1373,7 @@ _chat: muteThisRoom: "静音此房间" deleteRoom: "删除房间" chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。" + chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。" chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。" cannotChatWithTheUser: "无法与此用户聊天" cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。" @@ -1929,7 +1933,7 @@ _role: canImportFollowing: "允许导入关注列表" canImportMuting: "允许导入隐藏列表" canImportUserLists: "允许导入用户列表" - canChat: "允许聊天" + chatAvailability: "允许聊天" _condition: roleAssignedTo: "已分配给手动角色" isLocal: "是本地用户" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 8f707c8824..57b7e84b8a 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -424,7 +424,6 @@ antennaExcludeBots: "排除機器人帳戶" antennaKeywordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)" notifyAntenna: "通知有新貼文" withFileAntenna: "僅帶有附件的貼文" -hideNotesInSensitiveChannel: "隱藏敏感頻道的貼文" enableServiceworker: "啟用瀏覽器的推播通知" antennaUsersDescription: "填寫使用者名稱,以換行分隔" caseSensitive: "區分大小寫" @@ -1341,6 +1340,8 @@ right: "右" bottom: "下" top: "上" embed: "嵌入" +settingsMigrating: "正在移轉設定。請稍候……(之後也可以到「設定 → 其他 → 舊設定資訊移轉」中手動進行移轉)" +readonly: "唯讀" _chat: noMessagesYet: "尚無訊息" newMessage: "新訊息" @@ -1370,6 +1371,7 @@ _chat: muteThisRoom: "此聊天室已靜音" deleteRoom: "刪除聊天室" chatNotAvailableForThisAccountOrServer: "這個伺服器或這個帳號的聊天功能尚未啟用。" + chatIsReadOnlyForThisAccountOrServer: "在此伺服器或此帳戶上的聊天是唯讀的。您無法發布新訊息、建立或加入聊天室。" chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。" cannotChatWithTheUser: "無法與此使用者聊天" cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。" @@ -1435,7 +1437,7 @@ _preferencesBackup: _accountSettings: requireSigninToViewContents: "須登入以顯示內容" requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。" - requireSigninToViewContentsDescription2: "來自不支援 URL 預覽 (OGP)、 網頁嵌入和引用貼文的伺服器,也將停止顯示。" + requireSigninToViewContentsDescription2: "針對您貼文的 URL 預覽 (OGP) 與網頁嵌入功能將會無法使用。而不支援引用貼文的伺服器,也將停止顯示。" requireSigninToViewContentsDescription3: "這些限制可能不適用於被聯邦發送至遠端伺服器的內容。" makeNotesFollowersOnlyBefore: "讓過去的貼文僅對追隨者顯示" makeNotesFollowersOnlyBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對追隨者顯示。 如果您再次停用它,貼文的公開狀態也會恢復原狀。" @@ -1930,7 +1932,7 @@ _role: canImportFollowing: "允許匯入追隨名單" canImportMuting: "允許匯入靜音名單" canImportUserLists: "允許匯入清單" - canChat: "允許聊天" + chatAvailability: "允許聊天" _condition: roleAssignedTo: "手動指派角色完成" isLocal: "本地使用者" diff --git a/package.json b/package.json index 0cbe009f5d..a5ae3374c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2025.4.0-rc.1", + "version": "2025.4.0", "codename": "shonk", "repository": { "type": "git", diff --git a/packages/backend/migration/1744075766000-excludeNotesInSensitiveChannel.js b/packages/backend/migration/1744075766000-excludeNotesInSensitiveChannel.js new file mode 100644 index 0000000000..1e8faafbc4 --- /dev/null +++ b/packages/backend/migration/1744075766000-excludeNotesInSensitiveChannel.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ExcludeNotesInSensitiveChannel1744075766000 { + name = 'ExcludeNotesInSensitiveChannel1744075766000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" RENAME COLUMN "hideNotesInSensitiveChannel" TO "excludeNotesInSensitiveChannel"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" RENAME COLUMN "excludeNotesInSensitiveChannel" TO "hideNotesInSensitiveChannel"`); + } +} diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index bc7b3d2417..13e3dcdbd8 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -114,7 +114,7 @@ export class AntennaService implements OnApplicationShutdown { if (note.visibility === 'specified') return false; if (note.visibility === 'followers') return false; - if (antenna.hideNotesInSensitiveChannel && note.channel?.isSensitive) return false; + if (antenna.excludeNotesInSensitiveChannel && note.channel?.isSensitive) return false; if (antenna.excludeBots && noteUser.isBot) return false; diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 3984cefc80..b0e8cfb61c 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -94,6 +94,40 @@ export class ChatService { ) { } + @bindThis + public async getChatAvailability(userId: MiUser['id']): Promise<{ read: boolean; write: boolean; }> { + const policies = await this.roleService.getUserPolicies(userId); + + switch (policies.chatAvailability) { + case 'available': + return { + read: true, + write: true, + }; + case 'readonly': + return { + read: true, + write: false, + }; + case 'unavailable': + return { + read: false, + write: false, + }; + default: + throw new Error('invalid chat availability (unreachable)'); + } + } + + /** getChatAvailabilityの糖衣。主にAPI呼び出し時に走らせて、権限的に問題ない場合はそのまま続行する */ + @bindThis + public async checkChatAvailability(userId: MiUser['id'], permission: 'read' | 'write') { + const policy = await this.getChatAvailability(userId); + if (policy[permission] === false) { + throw new Error('ROLE_PERMISSION_DENIED'); + } + } + @bindThis public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: { text?: string | null; @@ -140,7 +174,7 @@ export class ChatService { } } - if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) { + if (!(await this.getChatAvailability(toUser.id)).write) { throw new Error('recipient is cannot chat (policy)'); } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index d4b5b07b38..8b98680f4c 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -66,7 +66,7 @@ export type RolePolicies = { canImportFollowing: boolean; canImportMuting: boolean; canImportUserLists: boolean; - canChat: boolean; + chatAvailability: 'available' | 'readonly' | 'unavailable'; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -104,7 +104,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canImportFollowing: true, canImportMuting: true, canImportUserLists: true, - canChat: true, + chatAvailability: 'available', }; @Injectable() @@ -376,6 +376,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value)); } + function aggregateChatAvailability(vs: RolePolicies['chatAvailability'][]) { + if (vs.some(v => v === 'available')) return 'available'; + if (vs.some(v => v === 'readonly')) return 'readonly'; + return 'unavailable'; + } + return { gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), btlAvailable: calc('btlAvailable', vs => vs.some(v => v === true)), @@ -411,7 +417,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)), canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), - canChat: calc('canChat', vs => vs.some(v => v === true)), + chatAvailability: calc('chatAvailability', aggregateChatAvailability), }; } diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index e81c1e8db4..1f8c8ae3e8 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -41,7 +41,7 @@ export class AntennaEntityService { excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, - hideNotesInSensitiveChannel: antenna.hideNotesInSensitiveChannel, + excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel, isActive: antenna.isActive, hasUnreadNote: false, // TODO notify: false, // 後方互換性のため diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index a46e2e2983..d11042d73b 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -665,7 +665,7 @@ export class UserEntityService implements OnModuleInit { followersVisibility: profile!.followersVisibility, followingVisibility: profile!.followingVisibility, chatScope: user.chatScope, - canChat: this.roleService.getUserPolicies(user.id).then(r => r.canChat), + canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'), roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ id: role.id, name: role.name, diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts index 0d92c5cade..17ec0c0f79 100644 --- a/packages/backend/src/models/Antenna.ts +++ b/packages/backend/src/models/Antenna.ts @@ -104,5 +104,5 @@ export class MiAntenna { @Column('boolean', { default: false, }) - public hideNotesInSensitiveChannel: boolean; + public excludeNotesInSensitiveChannel: boolean; } diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index 2bdaca80d0..eca7563066 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -100,7 +100,7 @@ export const packedAntennaSchema = { optional: false, nullable: false, default: false, }, - hideNotesInSensitiveChannel: { + excludeNotesInSensitiveChannel: { type: 'boolean', optional: false, nullable: false, default: false, diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 285c10c4bd..9e95684f67 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -300,9 +300,10 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, - canChat: { - type: 'boolean', + chatAvailability: { + type: 'string', optional: false, nullable: false, + enum: ['available', 'readonly', 'unavailable'], }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index a5c1bd7835..55d686e390 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -79,7 +79,7 @@ export const paramDef = { excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - hideNotesInSensitiveChannel: { type: 'boolean' }, + excludeNotesInSensitiveChannel: { type: 'boolean' }, }, required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], } as const; @@ -140,7 +140,7 @@ export default class extends Endpoint { // eslint- excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel, + excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel, }); this.globalEventService.publishInternalEvent('antennaCreated', antenna); diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index a8e0ae39c5..3f2513bf75 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -78,7 +78,7 @@ export const paramDef = { excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - hideNotesInSensitiveChannel: { type: 'boolean' }, + excludeNotesInSensitiveChannel: { type: 'boolean' }, }, required: ['antennaId'], } as const; @@ -136,7 +136,7 @@ export default class extends Endpoint { // eslint- excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel, + excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel, isActive: true, lastUsedAt: new Date(), }); diff --git a/packages/backend/src/server/api/endpoints/chat/history.ts b/packages/backend/src/server/api/endpoints/chat/history.ts index 7553a751e0..fdd9055106 100644 --- a/packages/backend/src/server/api/endpoints/chat/history.ts +++ b/packages/backend/src/server/api/endpoints/chat/history.ts @@ -46,6 +46,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit); const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts index a988dc60b9..ad2b82e219 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts @@ -16,7 +16,6 @@ export const meta = { tags: ['chat'], requireCredential: true, - requiredRolePolicy: 'canChat', prohibitMoved: true, @@ -74,6 +73,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + const room = await this.chatService.findRoomById(ps.toRoomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts index bbaab8a6c3..fa34a7d558 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts @@ -16,7 +16,6 @@ export const meta = { tags: ['chat'], requireCredential: true, - requiredRolePolicy: 'canChat', prohibitMoved: true, @@ -86,6 +85,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + let file = null; if (ps.fileId != null) { file = await this.driveFilesRepository.findOneBy({ diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts index 25fc774d4f..63b75fb6a7 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts @@ -13,7 +13,6 @@ export const meta = { tags: ['chat'], requireCredential: true, - requiredRolePolicy: 'canChat', kind: 'write:chat', @@ -43,6 +42,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + const message = await this.chatService.findMyMessageById(me.id, ps.messageId); if (message == null) { throw new ApiError(meta.errors.noSuchMessage); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts index 0145e380be..5f61e7e992 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/react.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/react.ts @@ -13,7 +13,6 @@ export const meta = { tags: ['chat'], requireCredential: true, - requiredRolePolicy: 'canChat', kind: 'write:chat', @@ -44,6 +43,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + await this.chatService.react(ps.messageId, me.id, ps.reaction); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts index b6d3356196..c0e344b889 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts @@ -54,6 +54,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const room = await this.chatService.findRoomById(ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/search.ts b/packages/backend/src/server/api/endpoints/chat/messages/search.ts index 4c989e5ca9..682597f76d 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/search.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/search.ts @@ -54,6 +54,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + if (ps.roomId != null) { const room = await this.chatService.findRoomById(ps.roomId); if (room == null) { diff --git a/packages/backend/src/server/api/endpoints/chat/messages/show.ts b/packages/backend/src/server/api/endpoints/chat/messages/show.ts index 371f7a7071..9a2bbb8742 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/show.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/show.ts @@ -50,6 +50,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const message = await this.chatService.findMessageById(ps.messageId); if (message == null) { throw new ApiError(meta.errors.noSuchMessage); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts index b97bad8a9c..6784bb6ecf 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts @@ -13,7 +13,6 @@ export const meta = { tags: ['chat'], requireCredential: true, - requiredRolePolicy: 'canChat', kind: 'write:chat', @@ -44,6 +43,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + await this.chatService.unreact(ps.messageId, me.id, ps.reaction); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts index a35f121bb1..a057e2e088 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts @@ -56,6 +56,8 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const other = await this.getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts index fa4cc8ceb4..68a53f0886 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts @@ -15,7 +15,6 @@ export const meta = { tags: ['chat'], requireCredential: true, - requiredRolePolicy: 'canChat', prohibitMoved: true, @@ -52,6 +51,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + const room = await this.chatService.createRoom(me, { name: ps.name, description: ps.description ?? '', diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts index 1d77a06dd8..82a8e1f30d 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts @@ -42,6 +42,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + const room = await this.chatService.findRoomById(ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts index 5da4a1a772..b1f049f2b9 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts @@ -15,7 +15,6 @@ export const meta = { tags: ['chat'], requireCredential: true, - requiredRolePolicy: 'canChat', prohibitMoved: true, @@ -57,6 +56,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + const room = await this.chatService.findMyRoomById(me.id, ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts index 8c017f7d01..b8a228089b 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts @@ -42,6 +42,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + await this.chatService.ignoreRoomInvitation(me.id, ps.roomId); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts index 07337480fc..8a02d1c704 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts @@ -47,6 +47,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); return this.chatEntityService.packRoomInvitations(invitations, me); }); diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts index 12d496e94b..0702ba086c 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/outbox.ts @@ -55,6 +55,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const room = await this.chatService.findMyRoomById(me.id, ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts index dbd4d1ea5a..d561f9e03f 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts @@ -42,6 +42,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + await this.chatService.joinToRoom(me.id, ps.roomId); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts index c4c6253236..ba9242c762 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts @@ -47,6 +47,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId); return this.chatEntityService.packRoomMemberships(memberships, me, { diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts index 724ad61f7e..a3ad0c2d6f 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts @@ -42,6 +42,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + await this.chatService.leaveRoom(me.id, ps.roomId); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts index 407bfe74f1..f5ffa21d32 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts @@ -54,6 +54,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const room = await this.chatService.findRoomById(ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts index 5208b8a253..11cbe7b8b9 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts @@ -43,6 +43,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + await this.chatService.muteRoom(me.id, ps.roomId, ps.mute); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts index 6516120bca..accf7e1bee 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts @@ -47,6 +47,8 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); return this.chatEntityService.packRooms(rooms, me); }); diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts index 547618ee7d..50da210d81 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts @@ -47,6 +47,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'read'); + const room = await this.chatService.findRoomById(ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts index 6f2a9c10b5..0cd62cb040 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts @@ -49,6 +49,8 @@ export default class extends Endpoint { // eslint- private chatEntityService: ChatEntityService, ) { super(meta, paramDef, async (ps, me) => { + await this.chatService.checkChatAvailability(me.id, 'write'); + const room = await this.chatService.findMyRoomById(me.id, ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index c90e99d930..c899ad9490 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -54,6 +54,8 @@ class GlobalTimelineChannel extends Channel { if (note.visibility !== 'public') return; if (note.channelId != null) return; if (note.user.requireSigninToViewContents && this.user == null) return; + if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; + if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 630bcce8cf..82b128eae0 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -57,6 +57,8 @@ class LocalTimelineChannel extends Channel { if (note.visibility !== 'public') return; if (note.channelId != null) return; if (note.user.requireSigninToViewContents && this.user == null) return; + if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; + if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; // 関係ない返信は除外 if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) { diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index fd3a6706f0..cbb18fe4ed 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -146,7 +146,7 @@ describe('アンテナ', () => { caseSensitive: false, createdAt: new Date(response.createdAt).toISOString(), excludeKeywords: [['']], - hideNotesInSensitiveChannel: false, + excludeNotesInSensitiveChannel: false, hasUnreadNote: false, isActive: true, keywords: [['keyword']], @@ -218,8 +218,8 @@ describe('アンテナ', () => { { parameters: () => ({ withReplies: true }) }, { parameters: () => ({ withFile: false }) }, { parameters: () => ({ withFile: true }) }, - { parameters: () => ({ hideNotesInSensitiveChannel: false }) }, - { parameters: () => ({ hideNotesInSensitiveChannel: true }) }, + { parameters: () => ({ excludeNotesInSensitiveChannel: false }) }, + { parameters: () => ({ excludeNotesInSensitiveChannel: true }) }, ]; test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => { const response = await successfulApiCall({ @@ -633,7 +633,7 @@ describe('アンテナ', () => { const keyword = 'キーワード'; const antenna = await successfulApiCall({ endpoint: 'antennas/create', - parameters: { ...defaultParam, keywords: [[keyword]], hideNotesInSensitiveChannel: true }, + parameters: { ...defaultParam, keywords: [[keyword]], excludeNotesInSensitiveChannel: true }, user: alice, }); const nonSensitiveChannel = await successfulApiCall({ diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 8341fed41b..22e4e36292 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -174,7 +174,7 @@ export const ROLE_POLICIES = [ 'canImportFollowing', 'canImportMuting', 'canImportUserLists', - 'canChat', + 'chatAvailability', ] as const; export const DEFAULT_SERVER_ERROR_IMAGE_URL = '/client-assets/status/error.jpg'; diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts index 99af81fb70..97f4e589a3 100644 --- a/packages/frontend/lib/vite-plugin-create-search-index.ts +++ b/packages/frontend/lib/vite-plugin-create-search-index.ts @@ -3,13 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +/// + import { parse as vueSfcParse } from 'vue/compiler-sfc'; import { createLogger, EnvironmentModuleGraph, - normalizePath, type LogErrorOptions, type LogOptions, + normalizePath, type Plugin, type PluginOption } from 'vite'; @@ -20,29 +22,25 @@ import MagicString, { SourceMap } from 'magic-string'; import path from 'node:path' import { hash, toBase62 } from '../vite.config'; import { minimatch } from 'minimatch'; -import type { - AttributeNode, CompoundExpressionNode, DirectiveNode, - ElementNode, - RootNode, SimpleExpressionNode, - TemplateChildNode, +import { + type AttributeNode, + type DirectiveNode, + type ElementNode, + ElementTypes, + NodeTypes, + type RootNode, + type SimpleExpressionNode, + type TemplateChildNode, } from '@vue/compiler-core'; -import { NodeTypes } from '@vue/compiler-core'; -export type AnalysisResult = { - filePath: string; - usage: T[]; -} - -export type SearchIndexItem = SearchIndexItemLink; -export type SearchIndexStringItem = SearchIndexItemLink; -export interface SearchIndexItemLink { +export interface SearchIndexItem { id: string; + parentId?: string; path?: string; label: string; - keywords: string | string[]; + keywords: string[]; icon?: string; inlining?: string[]; - children?: T[]; } export type Options = { @@ -65,7 +63,7 @@ interface MarkerRelation { let logger = { info: (msg: string, options?: LogOptions) => { }, warn: (msg: string, options?: LogOptions) => { }, - error: (msg: string, options?: LogErrorOptions) => { }, + error: (msg: string, options?: LogErrorOptions | unknown) => { }, }; let loggerInitialized = false; @@ -90,252 +88,64 @@ function initLogger(options: Options) { } } -function collectSearchItemIndexes(analysisResults: AnalysisResult[]): SearchIndexItem[] { - logger.info(`Processing ${analysisResults.length} files for output`); +//region AST Utility - // 新しいツリー構造を構築 - const allMarkers = new Map(); +type WalkVueNode = RootNode | TemplateChildNode | SimpleExpressionNode; - // 1. すべてのマーカーを一旦フラットに収集 - for (const file of analysisResults) { - logger.info(`Processing file: ${file.filePath} with ${file.usage.length} markers`); - - for (const marker of file.usage) { - if (marker.id) { - // キーワードとchildren処理を共通化 - const processedMarker: SearchIndexStringItem = { - ...marker, - keywords: processMarkerProperty(marker.keywords, 'keywords'), - }; - - allMarkers.set(marker.id, processedMarker); - } +/** + * Walks the Vue AST. + * @param nodes + * @param context The context value passed to callback. you can update context for children by returning value in callback + * @param callback Returns false if you don't want to walk inner tree + */ +function walkVueElements(nodes: WalkVueNode[], context: C, callback: (node: ElementNode, context: C) => C | undefined | void | false): void { + for (const node of nodes) { + let currentContext = context; + if (node.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION"); + if (node.type === NodeTypes.ELEMENT) { + const result = callback(node, context); + if (result === false) return; + if (result !== undefined) currentContext = result; + } + if ('children' in node) { + walkVueElements(node.children, currentContext, callback); } } - - logger.info(`Collected total ${allMarkers.size} unique markers`); - - // 2. 子マーカーIDの収集 - const childIds = collectChildIds(allMarkers); - logger.info(`Found ${childIds.size} child markers`); - - // 3. ルートマーカーの特定(他の誰かの子でないマーカー) - const rootMarkers = identifyRootMarkers(allMarkers, childIds); - logger.info(`Found ${rootMarkers.length} root markers`); - - // 4. 子マーカーの参照を解決 - const resolvedRootMarkers = resolveChildReferences(rootMarkers, allMarkers); - - // 5. デバッグ情報を生成 - const { totalMarkers, totalChildren } = countMarkers(resolvedRootMarkers); - logger.info(`Total markers in tree: ${totalMarkers} (${resolvedRootMarkers.length} roots + ${totalChildren} nested children)`); - - return resolvedRootMarkers; } -/** - * マーカーのプロパティ(keywordsやchildren)を処理する - */ -function processMarkerProperty(propValue: string | string[], propType: 'keywords' | 'children'): string | string[] { - // 文字列の配列表現を解析 - if (typeof propValue === 'string' && propValue.startsWith('[') && propValue.endsWith(']')) { - try { - // JSON5解析を試みる - return JSON5.parse(propValue.replace(/'/g, '"')); - } catch (e) { - // 解析に失敗した場合 - logger.warn(`Could not parse ${propType}: ${propValue}, using ${propType === 'children' ? 'empty array' : 'as is'}`); - return propType === 'children' ? [] : propValue; +function findAttribute(props: Array, name: string): AttributeNode | DirectiveNode | null { + for (const prop of props) { + switch (prop.type) { + case NodeTypes.ATTRIBUTE: + if (prop.name === name) { + return prop; + } + break; + case NodeTypes.DIRECTIVE: + if (prop.name === 'bind' && prop.arg && 'content' in prop.arg && prop.arg.content === name) { + return prop; + } + break; } } - - return propValue; + return null; } -/** - * 全マーカーから子IDを収集する - */ -function collectChildIds(allMarkers: Map): Set { - const childIds = new Set(); - - allMarkers.forEach((marker, id) => { - // 通常のchildren処理 - const children = marker.children; - if (Array.isArray(children)) { - children.forEach(childId => { - if (typeof childId === 'string') { - if (!allMarkers.has(childId)) { - logger.warn(`Warning: Child marker ID ${childId} referenced but not found`); - } else { - childIds.add(childId); - } - } - }); - } - - // inlining処理を追加 - if (marker.inlining) { - let inliningIds: string[] = []; - - // 文字列の場合は配列に変換 - if (typeof marker.inlining === 'string') { - try { - const inliningStr = (marker.inlining as string).trim(); - if (inliningStr.startsWith('[') && inliningStr.endsWith(']')) { - inliningIds = JSON5.parse(inliningStr.replace(/'/g, '"')); - logger.info(`Parsed inlining string to array: ${inliningStr} -> ${JSON.stringify(inliningIds)}`); - } else { - inliningIds = [inliningStr]; - } - } catch (e) { - logger.error(`Failed to parse inlining string: ${marker.inlining}`, e); - } - } - // 既に配列の場合 - else if (Array.isArray(marker.inlining)) { - inliningIds = marker.inlining; - } - - // inliningで指定されたIDを子セットに追加 - for (const inlineId of inliningIds) { - if (typeof inlineId === 'string') { - if (!allMarkers.has(inlineId)) { - logger.warn(`Warning: Inlining marker ID ${inlineId} referenced but not found`); - } else { - // inliningで参照されているマーカーも子として扱う - childIds.add(inlineId); - logger.info(`Added inlined marker ${inlineId} as child in collectChildIds`); - } - } - } - } - }); - - return childIds; -} - -/** - * ルートマーカー(他の子でないマーカー)を特定する - */ -function identifyRootMarkers( - allMarkers: Map, - childIds: Set -): SearchIndexStringItem[] { - const rootMarkers: SearchIndexStringItem[] = []; - - allMarkers.forEach((marker, id) => { - if (!childIds.has(id)) { - rootMarkers.push(marker); - logger.info(`Added root marker to output: ${id} with label ${marker.label}`); - } - }); - - return rootMarkers; -} - -/** - * 子マーカーの参照をIDから実際のオブジェクトに解決する - */ -function resolveChildReferences( - rootMarkers: SearchIndexStringItem[], - allMarkers: Map -): SearchIndexItem[] { - function resolveChildrenForMarker(marker: SearchIndexStringItem): SearchIndexItem { - // マーカーのディープコピーを作成 - const resolvedMarker: SearchIndexItem = { ...marker, children: [] }; - // 明示的に子マーカー配列を作成 - const resolvedChildren: SearchIndexItem[] = []; - - // 通常のchildren処理 - if (Array.isArray(marker.children)) { - for (const childId of marker.children) { - if (typeof childId === 'string') { - const childMarker = allMarkers.get(childId); - if (childMarker) { - // 子マーカーの子も再帰的に解決 - const resolvedChild = resolveChildrenForMarker(childMarker); - resolvedChildren.push(resolvedChild); - logger.info(`Resolved regular child ${childId} for parent ${marker.id}`); - } - } - } - } - - // inlining属性の処理 - let inliningIds: string[] = []; - - // 文字列の場合は配列に変換。例: "['2fa']" -> ['2fa'] - if (typeof marker.inlining === 'string') { - try { - // 文字列形式の配列を実際の配列に変換 - const inliningStr = (marker.inlining as string).trim(); - if (inliningStr.startsWith('[') && inliningStr.endsWith(']')) { - inliningIds = JSON5.parse(inliningStr.replace(/'/g, '"')); - logger.info(`Converted string inlining to array: ${inliningStr} -> ${JSON.stringify(inliningIds)}`); - } else { - // 単一値の場合は配列に - inliningIds = [inliningStr]; - logger.info(`Converted single string inlining to array: ${inliningStr}`); - } - } catch (e) { - logger.error(`Failed to parse inlining string: ${marker.inlining}`, e); - } - } - // 既に配列の場合はそのまま使用 - else if (Array.isArray(marker.inlining)) { - inliningIds = marker.inlining; - } - - // インライン指定されたマーカーを子として追加 - for (const inlineId of inliningIds) { - if (typeof inlineId === 'string') { - const inlineMarker = allMarkers.get(inlineId); - if (inlineMarker) { - // インライン指定されたマーカーを再帰的に解決 - const resolvedInline = resolveChildrenForMarker(inlineMarker); - delete resolvedInline.path - resolvedChildren.push(resolvedInline); - logger.info(`Added inlined marker ${inlineId} as child to ${marker.id}`); - } else { - logger.warn(`Inlining target not found: ${inlineId} referenced by ${marker.id}`); - } - } - } - - // 解決した子が存在する場合のみchildrenプロパティを設定 - if (resolvedChildren.length > 0) { - resolvedMarker.children = resolvedChildren; - } else { - delete resolvedMarker.children; - } - - return resolvedMarker; +function findEndOfStartTagAttributes(node: ElementNode): number { + if (node.children.length > 0) { + // 子要素がある場合、最初の子要素の開始位置を基準にする + const nodeStart = node.loc.start.offset; + const firstChildStart = node.children[0].loc.start.offset; + const endOfStartTag = node.loc.source.lastIndexOf('>', firstChildStart - nodeStart); + if (endOfStartTag === -1) throw new Error("Bug: Failed to find end of start tag"); + return nodeStart + endOfStartTag; + } else { + // 子要素がない場合、自身の終了位置から逆算 + return node.isSelfClosing ? node.loc.end.offset - 1 : node.loc.end.offset; } - - // すべてのルートマーカーの子を解決 - return rootMarkers.map(marker => resolveChildrenForMarker(marker)); } -/** - * マーカー数を数える(デバッグ用) - */ -function countMarkers(markers: SearchIndexItem[]): { totalMarkers: number, totalChildren: number } { - let totalMarkers = markers.length; - let totalChildren = 0; - - function countNested(items: SearchIndexItem[]): void { - for (const marker of items) { - if (marker.children && Array.isArray(marker.children)) { - totalChildren += marker.children.length; - totalMarkers += marker.children.length; - countNested(marker.children as SearchIndexItem[]); - } - } - } - - countNested(markers); - return { totalMarkers, totalChildren }; -} +//endregion /** * TypeScriptコード生成 @@ -349,787 +159,325 @@ function generateJavaScriptCode(resolvedRootMarkers: SearchIndexItem[]): string * オブジェクトを特殊な形式の文字列に変換する * i18n参照を保持しつつ適切な形式に変換 */ -function customStringify(obj: unknown, depth = 0): string { - const INDENT_STR = '\t'; - - // 配列の処理 - if (Array.isArray(obj)) { - if (obj.length === 0) return '[]'; - const indent = INDENT_STR.repeat(depth); - const childIndent = INDENT_STR.repeat(depth + 1); - - // 配列要素の処理 - const items = obj.map(item => { - // オブジェクト要素 - if (typeof item === 'object' && item !== null) { - return `${childIndent}${customStringify(item, depth + 1)}`; - } - - // i18n参照を含む文字列要素 - if (typeof item === 'string' && item.includes('i18n.ts.')) { - return `${childIndent}${item}`; // クォートなしでそのまま出力 - } - - // その他の要素 - return `${childIndent}${JSON5.stringify(item)}`; - }).join(',\n'); - - return `[\n${items},\n${indent}]`; - } - - // null または非オブジェクト - if (obj === null || typeof obj !== 'object') { - return JSON5.stringify(obj); - } - - // オブジェクトの処理 - const indent = INDENT_STR.repeat(depth); - const childIndent = INDENT_STR.repeat(depth + 1); - - const entries = Object.entries(obj) - // 不要なプロパティを除去 - .filter(([key, value]) => { - if (value === undefined) return false; - if (key === 'children' && Array.isArray(value) && value.length === 0) return false; - return true; - }) - // 各プロパティを変換 - .map(([key, value]) => { - // 子要素配列の特殊処理 - if (key === 'children' && Array.isArray(value) && value.length > 0) { - return `${childIndent}${key}: ${customStringify(value, depth + 1)}`; - } - - // ラベルやその他プロパティを処理 - return `${childIndent}${key}: ${formatSpecialProperty(key, value)}`; - }); - - if (entries.length === 0) return '{}'; - return `{\n${entries.join(',\n')},\n${indent}}`; +function customStringify(obj: unknown): string { + return JSON.stringify(obj).replaceAll(/"(.*?)"/g, (all, group) => { + // propertyAccessProxy が i18n 参照を "${i18n.xxx}"のような形に変換してるので、これをそのまま`${i18n.xxx}` + // のような形にすると、実行時にi18nのプロパティにアクセスするようになる。 + // objectのkeyでは``が使えないので、${ が使われている場合にのみ``に置き換えるようにする + return group.includes('${') ? '`' + group + '`' : all; + }); } +// region extractElementText + /** - * 特殊プロパティの書式設定 + * 要素のノードの中身のテキストを抽出する */ -function formatSpecialProperty(key: string, value: unknown): string { - // 値がundefinedの場合は空文字列を返す - if (value === undefined) { - return '""'; - } - - // childrenが配列の場合は特別に処理 - if (key === 'children' && Array.isArray(value)) { - return customStringify(value); - } - - // keywordsが配列の場合、特別に処理 - if (key === 'keywords' && Array.isArray(value)) { - return `[${formatArrayForOutput(value)}]`; - } - - // 文字列値の場合の特別処理 - if (typeof value === 'string') { - // i18n.ts 参照を含む場合 - クォートなしでそのまま出力 - if (isI18nReference(value)) { - logger.info(`Preserving i18n reference in output: ${value}`); - return value; - } - - // keywords が配列リテラルの形式の場合 - if (key === 'keywords' && value.startsWith('[') && value.endsWith(']')) { - return value; - } - } - - // 上記以外は通常の JSON5 文字列として返す - return JSON5.stringify(value); +function extractElementText(node: ElementNode, id: string): string | null { + return extractElementTextChecked(node, node.tag, id); } -/** - * 配列式の文字列表現を生成 - */ -function formatArrayForOutput(items: unknown[]): string { - return items.map(item => { - // i18n.ts. 参照の文字列はそのままJavaScript式として出力 - if (typeof item === 'string' && isI18nReference(item)) { - logger.info(`Preserving i18n reference in array: ${item}`); - return item; // クォートなしでそのまま - } - - // その他の値はJSON5形式で文字列化 - return JSON5.stringify(item); - }).join(', '); +function extractElementTextChecked(node: ElementNode, processingNodeName: string, id: string): string | null { + const result: string[] = []; + for (const child of node.children) { + const text = extractElementText2Inner(child, processingNodeName, id); + if (text == null) return null; + result.push(text); + } + return result.join(''); } -/** - * 要素ノードからテキスト内容を抽出する - * 各抽出方法を分離して可読性を向上 - */ -function extractElementText(node: TemplateChildNode): string | null { - if (!node) return null; +function extractElementText2Inner(node: TemplateChildNode, processingNodeName: string, id: string): string | null { if (node.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION"); - logger.info(`Extracting text from node type=${node.type}, tag=${'tag' in node ? node.tag : 'unknown'}`); - - // 1. 直接コンテンツの抽出を試行 - const directContent = extractDirectContent(node); - if (directContent) return directContent; - - // 子要素がない場合は終了 - if (!('children' in node) || !Array.isArray(node.children)) { - return null; - } - - // 2. インターポレーションノードを検索 - const interpolationContent = extractInterpolationContent(node.children); - if (interpolationContent) return interpolationContent; - - // 3. 式ノードを検索 - const expressionContent = extractExpressionContent(node.children); - if (expressionContent) return expressionContent; - - // 4. テキストノードを検索 - const textContent = extractTextContent(node.children); - if (textContent) return textContent; - - // 5. 再帰的に子ノードを探索 - return extractNestedContent(node.children); -} -/** - * ノードから直接コンテンツを抽出 - */ -function extractDirectContent(node: TemplateChildNode): string | null { - if (!('content' in node)) return null; - if (typeof node.content == 'object' && node.content.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION"); - - const content = typeof node.content === 'string' ? node.content.trim() - : node.content.type !== NodeTypes.INTERPOLATION ? node.content.content.trim() - : null; - - if (!content) return null; - - logger.info(`Direct node content found: ${content}`); - - // Mustache構文のチェック - const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; - const mustacheMatch = content.match(mustachePattern); - - if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { - const extractedContent = mustacheMatch[1].trim(); - logger.info(`Extracted i18n reference from mustache: ${extractedContent}`); - return extractedContent; - } - - // 直接i18n参照を含む場合 - if (isI18nReference(content)) { - logger.info(`Direct i18n reference found: ${content}`); - return content; - } - - // その他のコンテンツ - return content; -} - -/** - * インターポレーションノード(Mustache)からコンテンツを抽出 - */ -function extractInterpolationContent(children: TemplateChildNode[]): string | null { - for (const child of children) { - if (child.type === NodeTypes.INTERPOLATION) { - logger.info(`Found interpolation node (Mustache): ${JSON.stringify(child.content).substring(0, 100)}...`); - - if (child.content && child.content.type === 4 && child.content.content) { - const content = child.content.content.trim(); - logger.info(`Interpolation content: ${content}`); - - if (isI18nReference(content)) { - return content; - } - } else if (child.content && typeof child.content === 'object') { - if (child.content.type == NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION"); - // オブジェクト形式のcontentを探索 - logger.info(`Complex interpolation node: ${JSON.stringify(child.content).substring(0, 100)}...`); - - if (child.content.content) { - const content = child.content.content.trim(); - - if (isI18nReference(content)) { - logger.info(`Found i18n reference in complex interpolation: ${content}`); - return content; - } - } + switch (node.type) { + case NodeTypes.INTERPOLATION: { + const expr = node.content; + if (expr.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error(`Unexpected COMPOUND_EXPRESSION`); + const exprResult = evalExpression(expr.content); + if (typeof exprResult !== 'string') { + logger.error(`Result of interpolation node is not string at line ${id}:${node.loc.start.line}`); + return null; } + return exprResult; } + case NodeTypes.ELEMENT: + if (node.tagType === ElementTypes.ELEMENT) { + return extractElementTextChecked(node, processingNodeName, id); + } else { + logger.error(`Unexpected ${node.tag} extracting text of ${processingNodeName} ${id}:${node.loc.start.line}`); + return null; + } + case NodeTypes.TEXT: + return node.content; + case NodeTypes.COMMENT: + // We skip comments + return ''; + case NodeTypes.IF: + case NodeTypes.IF_BRANCH: + case NodeTypes.FOR: + case NodeTypes.TEXT_CALL: + logger.error(`Unexpected controlflow element extracting text of ${processingNodeName} ${id}:${node.loc.start.line}`); + return null; } - - return null; } -/** - * 式ノードからコンテンツを抽出 - */ -function extractExpressionContent(children: TemplateChildNode[]): string | null { - // i18n.ts. 参照パターンを持つものを優先 - for (const child of children) { - if (child.type === NodeTypes.TEXT && child.content) { - const expr = child.content.trim(); +// endregion - if (isI18nReference(expr)) { - logger.info(`Found i18n reference in expression node: ${expr}`); - return expr; - } - } - } - - // その他の式 - for (const child of children) { - if (child.type === NodeTypes.TEXT && child.content) { - const expr = child.content.trim(); - logger.info(`Found expression: ${expr}`); - return expr; - } - } - - return null; -} +// region extractUsageInfoFromTemplateAst /** - * テキストノードからコンテンツを抽出 + * SearchLabel/SearchKeyword/SearchIconを探して抽出する関数 */ -function extractTextContent(children: TemplateChildNode[]): string | null { - for (const child of children) { - if (child.type === NodeTypes.COMMENT && child.content) { - const text = child.content.trim(); - - if (text) { - logger.info(`Found text node: ${text}`); - - // Mustache構文のチェック - const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; - const mustacheMatch = text.match(mustachePattern); - - if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { - logger.info(`Extracted i18n ref from text mustache: ${mustacheMatch[1]}`); - return mustacheMatch[1].trim(); - } - - return text; - } - } - } - - return null; -} - -/** - * 子ノードを再帰的に探索してコンテンツを抽出 - */ -function extractNestedContent(children: TemplateChildNode[]): string | null { - for (const child of children) { - if ('children' in child && Array.isArray(child.children) && child.children.length > 0) { - const nestedContent = extractElementText(child); - - if (nestedContent) { - logger.info(`Found nested content: ${nestedContent}`); - return nestedContent; - } - } else if (child.type === NodeTypes.ELEMENT) { - // childrenがなくても内部を調査 - const nestedContent = extractElementText(child); - - if (nestedContent) { - logger.info(`Found content in childless element: ${nestedContent}`); - return nestedContent; - } - } - } - - return null; -} - - -/** - * SearchLabelとSearchKeywordを探して抽出する関数 - */ -function extractLabelsAndKeywords(nodes: TemplateChildNode[]): { label: string | null, keywords: string[] } { - let label: string | null = null; +function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null, keywords: string[], icon: string | null } { + let label: string | null | undefined = undefined; + let icon: string | null | undefined = undefined; const keywords: string[] = []; logger.info(`Extracting labels and keywords from ${nodes.length} nodes`); - // 再帰的にSearchLabelとSearchKeywordを探索(ネストされたSearchMarkerは処理しない) - function findComponents(nodes: TemplateChildNode[]) { - for (const node of nodes) { - if (node.type === NodeTypes.ELEMENT) { - logger.info(`Checking element: ${node.tag}`); - - // SearchMarkerの場合は、その子要素は別スコープなのでスキップ - if (node.tag === 'SearchMarker') { - logger.info(`Found nested SearchMarker - skipping its content to maintain scope isolation`); - continue; // このSearchMarkerの中身は処理しない (スコープ分離) + walkVueElements(nodes, null, (node) => { + switch (node.tag) { + case 'SearchMarker': + return false; // SearchMarkerはスキップ + case 'SearchLabel': + if (label !== undefined) { + logger.warn(`Duplicate SearchLabel found, ignoring the second one at ${id}:${node.loc.start.line}`); + break; // 2つ目のSearchLabelは無視 } - // SearchLabelの処理 - if (node.tag === 'SearchLabel') { - logger.info(`Found SearchLabel node, structure: ${JSON.stringify(node).substring(0, 200)}...`); - - // まず完全なノード内容の抽出を試みる - const content = extractElementText(node); - if (content) { - label = content; - logger.info(`SearchLabel content extracted: ${content}`); - } else { - logger.info(`SearchLabel found but extraction failed, trying direct children inspection`); - - // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 - { - for (const child of node.children) { - // Mustacheインターポレーション - if (child.type === NodeTypes.INTERPOLATION && child.content) { - // content内の式を取り出す - if (child.content.type == NodeTypes.COMPOUND_EXPRESSION) throw new Error("unexpected COMPOUND_EXPRESSION"); - const expression = child.content.content || - (child.content.type === 4 ? child.content.content : null) || - JSON.stringify(child.content); - - logger.info(`Interpolation expression: ${expression}`); - if (typeof expression === 'string' && isI18nReference(expression)) { - label = expression.trim(); - logger.info(`Found i18n in interpolation: ${label}`); - break; - } - } - // 式ノード - else if (child.type === NodeTypes.TEXT && child.content && isI18nReference(child.content)) { - label = child.content.trim(); - logger.info(`Found i18n in expression: ${label}`); - break; - } - // テキストノードでもMustache構文を探す - else if (child.type === NodeTypes.COMMENT && child.content) { - const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); - if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { - label = mustacheMatch[1].trim(); - logger.info(`Found i18n in text mustache: ${label}`); - break; - } - } - } - } - } + label = extractElementText(node, id); + return; + case 'SearchKeyword': + const content = extractElementText(node, id); + if (content) { + keywords.push(content); } - // SearchKeywordの処理 - else if (node.tag === 'SearchKeyword') { - logger.info(`Found SearchKeyword node`); - - // まず完全なノード内容の抽出を試みる - const content = extractElementText(node); - if (content) { - keywords.push(content); - logger.info(`SearchKeyword content extracted: ${content}`); - } else { - logger.info(`SearchKeyword found but extraction failed, trying direct children inspection`); - - // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 - { - for (const child of node.children) { - // Mustacheインターポレーション - if (child.type === NodeTypes.INTERPOLATION && child.content) { - // content内の式を取り出す - if (child.content.type == NodeTypes.COMPOUND_EXPRESSION) throw new Error("unexpected COMPOUND_EXPRESSION"); - const expression = child.content.content || - (child.content.type === 4 ? child.content.content : null) || - JSON.stringify(child.content); - - logger.info(`Keyword interpolation: ${expression}`); - if (typeof expression === 'string' && isI18nReference(expression)) { - const keyword = expression.trim(); - keywords.push(keyword); - logger.info(`Found i18n keyword in interpolation: ${keyword}`); - break; - } - } - // 式ノード - else if (child.type === NodeTypes.TEXT && child.content && isI18nReference(child.content)) { - const keyword = child.content.trim(); - keywords.push(keyword); - logger.info(`Found i18n keyword in expression: ${keyword}`); - break; - } - // テキストノードでもMustache構文を探す - else if (child.type === NodeTypes.COMMENT && child.content) { - const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); - if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { - const keyword = mustacheMatch[1].trim(); - keywords.push(keyword); - logger.info(`Found i18n keyword in text mustache: ${keyword}`); - break; - } - } - } - } - } + return; + case 'SearchIcon': + if (icon !== undefined) { + logger.warn(`Duplicate SearchIcon found, ignoring the second one at ${id}:${node.loc.start.line}`); + break; // 2つ目のSearchIconは無視 } - // 子要素を再帰的に調査(ただしSearchMarkerは除外) - if (node.children && Array.isArray(node.children)) { - findComponents(node.children); + if (node.children.length !== 1) { + logger.error(`SearchIcon must have exactly one child at ${id}:${node.loc.start.line}`); + return; } - } + + const iconNode = node.children[0]; + if (iconNode.type !== NodeTypes.ELEMENT) { + logger.error(`SearchIcon must have a child element at ${id}:${node.loc.start.line}`); + return; + } + icon = getStringProp(findAttribute(iconNode.props, 'class'), id); + return; } - } - findComponents(nodes); + return; + }); // デバッグ情報 - logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}]`); - return { label, keywords }; + logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}, icon=${icon}]`); + return { label: label ?? null, keywords, icon: icon ?? null }; } +function getStringProp(attr: AttributeNode | DirectiveNode | null, id: string): string | null { + switch (attr?.type) { + case null: + case undefined: + return null; + case NodeTypes.ATTRIBUTE: + return attr.value?.content ?? null; + case NodeTypes.DIRECTIVE: + if (attr.exp == null) return null; + if (attr.exp.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error('Unexpected COMPOUND_EXPRESSION'); + const value = evalExpression(attr.exp.content ?? ''); + if (typeof value !== 'string') { + logger.error(`Expected string value, got ${typeof value} at ${id}:${attr.loc.start.line}`); + return null; + } + return value; + } +} + +function getStringArrayProp(attr: AttributeNode | DirectiveNode | null, id: string): string[] | null { + switch (attr?.type) { + case null: + case undefined: + return null; + case NodeTypes.ATTRIBUTE: + logger.error(`Expected directive, got attribute at ${id}:${attr.loc.start.line}`); + return null; + case NodeTypes.DIRECTIVE: + if (attr.exp == null) return null; + if (attr.exp.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error('Unexpected COMPOUND_EXPRESSION'); + const value = evalExpression(attr.exp.content ?? ''); + if (!Array.isArray(value) || !value.every(x => typeof x === 'string')) { + logger.error(`Expected string array value, got ${typeof value} at ${id}:${attr.loc.start.line}`); + return null; + } + return value; + } +} function extractUsageInfoFromTemplateAst( templateAst: RootNode | undefined, id: string, -): SearchIndexStringItem[] { - const allMarkers: SearchIndexStringItem[] = []; - const markerMap = new Map>(); - const childrenIds = new Set(); - const normalizedId = id.replace(/\\/g, '/'); +): SearchIndexItem[] { + const allMarkers: SearchIndexItem[] = []; + const markerMap = new Map(); if (!templateAst) return allMarkers; - // マーカーの基本情報を収集 - function collectMarkers(node: TemplateChildNode | RootNode, parentId: string | null = null) { - if (node.type === NodeTypes.ELEMENT && node.tag === 'SearchMarker') { - // マーカーID取得 - const markerIdProp = node.props?.find(p => p.name === 'markerId'); - const markerId = markerIdProp?.type == NodeTypes.ATTRIBUTE ? markerIdProp.value?.content : null; - - // SearchMarkerにマーカーIDがない場合はエラー - if (markerId == null) { - logger.error(`Marker ID not found for node: ${JSON.stringify(node)}`); - throw new Error(`Marker ID not found in file ${id}`); - } - - // マーカー基本情報 - const markerInfo: SearchIndexStringItem = { - id: markerId, - children: [], - label: '', // デフォルト値 - keywords: [], - }; - - // 静的プロパティを取得 - if (node.props && Array.isArray(node.props)) { - for (const prop of node.props) { - if (prop.type === 6 && prop.name && prop.name !== 'markerId') { - if (prop.name === 'path') markerInfo.path = prop.value?.content || ''; - else if (prop.name === 'icon') markerInfo.icon = prop.value?.content || ''; - else if (prop.name === 'label') markerInfo.label = prop.value?.content || ''; - } - } - } - - // バインドプロパティを取得 - const bindings = extractNodeBindings(node); - if (bindings.path) markerInfo.path = bindings.path; - if (bindings.icon) markerInfo.icon = bindings.icon; - if (bindings.label) markerInfo.label = bindings.label; - if (bindings.children) markerInfo.children = processMarkerProperty(bindings.children, 'children') as string[]; - if (bindings.inlining) { - markerInfo.inlining = bindings.inlining; - logger.info(`Added inlining ${JSON.stringify(bindings.inlining)} to marker ${markerId}`); - } - if (bindings.keywords) { - if (Array.isArray(bindings.keywords)) { - markerInfo.keywords = bindings.keywords; - } else { - markerInfo.keywords = bindings.keywords || []; - } - } - - //pathがない場合はファイルパスを設定 - if (markerInfo.path == null && parentId == null) { - markerInfo.path = normalizedId.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1]; - } - - // SearchLabelとSearchKeywordを抽出 (AST全体を探索) - if (node.children && Array.isArray(node.children)) { - logger.info(`Processing marker ${markerId} for labels and keywords`); - const extracted = extractLabelsAndKeywords(node.children); - - // SearchLabelからのラベル取得は最優先で適用 - if (extracted.label) { - markerInfo.label = extracted.label; - logger.info(`Using extracted label for ${markerId}: ${extracted.label}`); - } else if (markerInfo.label) { - logger.info(`Using existing label for ${markerId}: ${markerInfo.label}`); - } else { - markerInfo.label = 'Unnamed marker'; - logger.info(`No label found for ${markerId}, using default`); - } - - // SearchKeywordからのキーワード取得を追加 - if (extracted.keywords.length > 0) { - const existingKeywords = Array.isArray(markerInfo.keywords) ? - [...markerInfo.keywords] : - (markerInfo.keywords ? [markerInfo.keywords] : []); - - // i18n参照のキーワードは最優先で追加 - const combinedKeywords = [...existingKeywords]; - for (const kw of extracted.keywords) { - combinedKeywords.push(kw); - logger.info(`Added extracted keyword to ${markerId}: ${kw}`); - } - - markerInfo.keywords = combinedKeywords; - } - } - - // マーカーを登録 - markerMap.set(markerId, markerInfo); - allMarkers.push(markerInfo); - - // 親子関係を記録 - if (parentId) { - const parent = markerMap.get(parentId); - if (parent) { - childrenIds.add(markerId); - } - } - - // 子ノードを処理 - for (const child of node.children) { - collectMarkers(child, markerId); - } - - return markerId; - } - // SearchMarkerでない場合は再帰的に子ノードを処理 - else if ('children' in node && Array.isArray(node.children)) { - for (const child of node.children) { - if (typeof child == 'object' && child.type !== NodeTypes.SIMPLE_EXPRESSION) { - collectMarkers(child, parentId); - } - } + walkVueElements([templateAst], null, (node, parentId) => { + if (node.tag !== 'SearchMarker') { + return; } - return null; - } + // マーカーID取得 + const markerIdProp = node.props?.find(p => p.name === 'markerId'); + const markerId = markerIdProp?.type == NodeTypes.ATTRIBUTE ? markerIdProp.value?.content : null; + + // SearchMarkerにマーカーIDがない場合はエラー + if (markerId == null) { + logger.error(`Marker ID not found for node: ${JSON.stringify(node)}`); + throw new Error(`Marker ID not found in file ${id}`); + } + + // マーカー基本情報 + const markerInfo: SearchIndexItem = { + id: markerId, + parentId: parentId ?? undefined, + label: '', // デフォルト値 + keywords: [], + }; + + // バインドプロパティを取得 + const path = getStringProp(findAttribute(node.props, 'path'), id) + const icon = getStringProp(findAttribute(node.props, 'icon'), id) + const label = getStringProp(findAttribute(node.props, 'label'), id) + const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id) + const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id) + + if (path) markerInfo.path = path; + if (icon) markerInfo.icon = icon; + if (label) markerInfo.label = label; + if (inlining) markerInfo.inlining = inlining; + if (keywords) markerInfo.keywords = keywords; + + //pathがない場合はファイルパスを設定 + if (markerInfo.path == null && parentId == null) { + markerInfo.path = id.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1]; + } + + // SearchLabelとSearchKeywordを抽出 (AST全体を探索) + { + const extracted = extractSugarTags(node.children, id); + if (extracted.label && markerInfo.label) logger.warn(`Duplicate label found for ${markerId} at ${id}:${node.loc.start.line}`); + if (extracted.icon && markerInfo.icon) logger.warn(`Duplicate icon found for ${markerId} at ${id}:${node.loc.start.line}`); + markerInfo.label = extracted.label ?? markerInfo.label ?? ''; + markerInfo.keywords = [...extracted.keywords, ...markerInfo.keywords]; + markerInfo.icon = extracted.icon ?? markerInfo.icon ?? undefined; + } + + if (!markerInfo.label) { + logger.warn(`No label found for ${markerId} at ${id}:${node.loc.start.line}`); + } + + // マーカーを登録 + markerMap.set(markerId, markerInfo); + allMarkers.push(markerInfo); + return markerId; + }); - // AST解析開始 - collectMarkers(templateAst); return allMarkers; } -type SpecialBindings = { - inlining: string[]; - keywords: string[] | string; -}; -type Bindings = Partial, keyof SpecialBindings> & SpecialBindings>; -// バインドプロパティの処理を修正する関数 -function extractNodeBindings(node: TemplateChildNode | RootNode): Bindings { - const bindings: Bindings = {}; +//endregion - if (node.type !== NodeTypes.ELEMENT) return bindings; +//region evalExpression - // バインド式を収集 - for (const prop of node.props) { - if (prop.type === NodeTypes.DIRECTIVE && prop.name === 'bind' && prop.arg && 'content' in prop.arg) { - const propName = prop.arg.content; - if (prop.exp?.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error('unexpected COMPOUND_EXPRESSION'); - const propContent = prop.exp?.content || ''; - - logger.info(`Processing bind prop ${propName}: ${propContent}`); - - // inliningプロパティの処理を追加 - if (propName === 'inlining') { - try { - const content = propContent.trim(); - - // 配列式の場合 - if (content.startsWith('[') && content.endsWith(']')) { - // 配列要素を解析 - const elements = parseArrayExpression(content); - if (elements.length > 0) { - bindings.inlining = elements; - logger.info(`Parsed inlining array: ${JSON5.stringify(elements)}`); - } else { - bindings.inlining = []; - } - } - // 文字列の場合は配列に変換 - else if (content) { - bindings.inlining = [content]; // 単一の値を配列に - logger.info(`Converting inlining to array: [${content}]`); - } - } catch (e) { - logger.error(`Failed to parse inlining binding: ${propContent}`, e); - } - } - // keywordsの特殊処理 - if (propName === 'keywords') { - try { - const content = propContent.trim(); - - // 配列式の場合 - if (content.startsWith('[') && content.endsWith(']')) { - // i18n参照や特殊な式を保持するため、各要素を個別に解析 - const elements = parseArrayExpression(content); - if (elements.length > 0) { - bindings.keywords = elements; - logger.info(`Parsed keywords array: ${JSON5.stringify(elements)}`); - } else { - bindings.keywords = []; - logger.info('Empty keywords array'); - } - } - // その他の式(非配列) - else if (content) { - bindings.keywords = content; // 式をそのまま保持 - logger.info(`Keeping keywords as expression: ${content}`); - } else { - bindings.keywords = []; - logger.info('No keywords provided'); - } - } catch (e) { - logger.error(`Failed to parse keywords binding: ${propContent}`, e); - // エラーが起きても何らかの値を設定 - bindings.keywords = propContent || []; - } - } - // その他のプロパティ - else if (propName === 'label') { - // ラベルの場合も式として保持 - bindings[propName] = propContent; - logger.info(`Set label from bind expression: ${propContent}`); - } - else { - bindings[propName] = propContent; - } - } - } - - return bindings; +/** + * expr を実行します。 + * i18n はそのアクセスを保持するために propertyAccessProxy を使用しています。 + */ +function evalExpression(expr: string): unknown { + const rarResult = Function('i18n', `return ${expr}`)(i18nProxy); + // JSON.stringify を一回通すことで、 AccessProxy を文字列に変換する + // Walk してもいいんだけど横着してJSON.stringifyしてる。ビルド時にしか通らないのであんまりパフォーマンス気にする必要ないんで + return JSON.parse(JSON.stringify(rarResult)); } -// 配列式をパースする補助関数(文字列リテラル処理を改善) -function parseArrayExpression(expr: string): string[] { - try { - // 単純なケースはJSON5でパースを試みる - return JSON5.parse(expr.replace(/'/g, '"')); - } catch (e) { - // 複雑なケース(i18n.ts.xxx などの式を含む場合)は手動パース - logger.info(`Complex array expression, trying manual parsing: ${expr}`); +const propertyAccessProxySymbol = Symbol('propertyAccessProxySymbol'); - // "["と"]"を取り除く - const content = expr.substring(1, expr.length - 1).trim(); - if (!content) return []; +type AccessProxy = { + [propertyAccessProxySymbol]: string[], + [k: string]: AccessProxy, +} - const result: string[] = []; - let currentItem = ''; - let depth = 0; - let inString = false; - let stringChar = ''; - - // カンマで区切る(ただし文字列内や入れ子の配列内のカンマは無視) - for (let i = 0; i < content.length; i++) { - const char = content[i]; - - if (inString) { - if (char === stringChar && content[i - 1] !== '\\') { - inString = false; - } - currentItem += char; - } else if (char === '"' || char === "'") { - inString = true; - stringChar = char; - currentItem += char; - } else if (char === '[') { - depth++; - currentItem += char; - } else if (char === ']') { - depth--; - currentItem += char; - } else if (char === ',' && depth === 0) { - // 項目の区切りを検出 - const trimmed = currentItem.trim(); - - // 純粋な文字列リテラルの場合、実際の値に変換 - if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || - (trimmed.startsWith('"') && trimmed.endsWith('"'))) { - try { - result.push(JSON5.parse(trimmed)); - } catch (err) { - result.push(trimmed); - } - } else { - // それ以外の式はそのまま(i18n.ts.xxx など) - result.push(trimmed); - } - - currentItem = ''; - } else { - currentItem += char; - } +const propertyAccessProxyHandler: ProxyHandler = { + get(target: AccessProxy, p: string | symbol): any { + if (p in target) { + return (target as any)[p]; } - - // 最後の項目を処理 - if (currentItem.trim()) { - const trimmed = currentItem.trim(); - - // 純粋な文字列リテラルの場合、実際の値に変換 - if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || - (trimmed.startsWith('"') && trimmed.endsWith('"'))) { - try { - result.push(JSON5.parse(trimmed)); - } catch (err) { - result.push(trimmed); - } - } else { - // それ以外の式はそのまま(i18n.ts.xxx など) - result.push(trimmed); - } + if (p == "toJSON" || p == Symbol.toPrimitive) { + return propertyAccessProxyToJSON; } - - logger.info(`Parsed complex array expression: ${expr} -> ${JSON.stringify(result)}`); - return result; + if (typeof p == 'string') { + return target[p] = propertyAccessProxy([...target[propertyAccessProxySymbol], p]); + } + return undefined; } } -export function collectFileMarkers(files: [id: string, code: string][]): AnalysisResult { - const allMarkers: SearchIndexStringItem[] = []; - for (const [id, code] of files) { - try { - const { descriptor, errors } = vueSfcParse(code, { - filename: id, - }); - - if (errors.length > 0) { - logger.error(`Compile Error: ${id}, ${errors}`); - continue; // エラーが発生したファイルはスキップ - } - - const fileMarkers = extractUsageInfoFromTemplateAst(descriptor.template?.ast, id); - - if (fileMarkers && fileMarkers.length > 0) { - allMarkers.push(...fileMarkers); // すべてのマーカーを収集 - logger.info(`Successfully extracted ${fileMarkers.length} markers from ${id}`); - } else { - logger.info(`No markers found in ${id}`); - } - } catch (error) { - logger.error(`Error analyzing file ${id}:`, error); +function propertyAccessProxyToJSON(this: AccessProxy, hint: string) { + const expression = this[propertyAccessProxySymbol].reduce((prev, current) => { + if (current.match(/^[a-z][0-9a-z]*$/i)) { + return `${prev}.${current}`; + } else { + return `${prev}['${current}']`; } - } + }); + return '$\{' + expression + '}'; +} - // 収集したすべてのマーカー情報を使用 - return { - filePath: "combined-markers", // すべてのファイルのマーカーを1つのエントリとして扱う - usage: allMarkers, +/** + * プロパティのアクセスを保持するための Proxy オブジェクトを作成します。 + * + * この関数で生成した proxy は JSON でシリアライズするか、`${}`のように string にすると、 ${property.path} のような形になる。 + * @param path + */ +function propertyAccessProxy(path: string[]): AccessProxy { + const target: AccessProxy = { + [propertyAccessProxySymbol]: path, }; + return new Proxy(target, propertyAccessProxyHandler); } +const i18nProxy = propertyAccessProxy(['i18n']); + +export function collectFileMarkers(id: string, code: string): SearchIndexItem[] { + try { + const { descriptor, errors } = vueSfcParse(code, { + filename: id, + }); + + if (errors.length > 0) { + logger.error(`Compile Error: ${id}, ${errors}`); + return []; // エラーが発生したファイルはスキップ + } + + return extractUsageInfoFromTemplateAst(descriptor.template?.ast, id); + } catch (error) { + logger.error(`Error analyzing file ${id}:`, error); + } + + return []; +} + +// endregion + type TransformedCode = { code: string, map: SourceMap, @@ -1177,84 +525,37 @@ export class MarkerIdAssigner { }; } - type SearchMarkerElementNode = ElementNode & { - __markerId?: string, - __children?: string[], - }; + walkVueElements([ast], null, (node, parentId) => { + if (node.tag !== 'SearchMarker') return; - function traverse(node: RootNode | TemplateChildNode | SimpleExpressionNode | CompoundExpressionNode, currentParent?: SearchMarkerElementNode) { - if (node.type === NodeTypes.ELEMENT && node.tag === 'SearchMarker') { - // 行番号はコード先頭からの改行数で取得 - const lineNumber = code.slice(0, node.loc.start.offset).split('\n').length; + const markerIdProp = findAttribute(node.props, 'markerId'); + + let nodeMarkerId: string; + if (markerIdProp != null) { + if (markerIdProp.type !== NodeTypes.ATTRIBUTE) return logger.error(`markerId must be a attribute at ${id}:${markerIdProp.loc.start.line}`); + if (markerIdProp.value == null) return logger.error(`markerId must have a value at ${id}:${markerIdProp.loc.start.line}`); + nodeMarkerId = markerIdProp.value.content; + } else { // ファイルパスと行番号からハッシュ値を生成 // この際実行環境で差が出ないようにファイルパスを正規化 const idKey = id.replace(/\\/g, '/').split('packages/frontend/')[1] - const generatedMarkerId = toBase62(hash(`${idKey}:${lineNumber}`)); + const generatedMarkerId = toBase62(hash(`${idKey}:${node.loc.start.line}`)); - const props = node.props || []; - const hasMarkerIdProp = props.some((prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === 'markerId'); - const nodeMarkerId = hasMarkerIdProp - ? props.find((prop): prop is AttributeNode => prop.type === NodeTypes.ATTRIBUTE && prop.name === 'markerId')?.value?.content as string - : generatedMarkerId; - (node as SearchMarkerElementNode).__markerId = nodeMarkerId; + // markerId attribute を追加 + const endOfStartTag = findEndOfStartTagAttributes(node); + s.appendRight(endOfStartTag, ` markerId="${generatedMarkerId}" data-in-app-search-marker-id="${generatedMarkerId}"`); - // 子マーカーの場合、親ノードに __children を設定しておく - if (currentParent) { - currentParent.__children = currentParent.__children || []; - currentParent.__children.push(nodeMarkerId); - } - - const parentMarkerId = currentParent && currentParent.__markerId; - markerRelations.push({ - parentId: parentMarkerId, - markerId: nodeMarkerId, - node: node, - }); - - if (!hasMarkerIdProp) { - const nodeStart = node.loc.start.offset; - let endOfStartTag; - - if (node.children && node.children.length > 0) { - // 子要素がある場合、最初の子要素の開始位置を基準にする - endOfStartTag = code.lastIndexOf('>', node.children[0].loc.start.offset); - } else if (node.loc.end.offset > nodeStart) { - // 子要素がない場合、自身の終了位置から逆算 - const nodeSource = code.substring(nodeStart, node.loc.end.offset); - // 自己終了タグか通常の終了タグかを判断 - if (nodeSource.includes('/>')) { - endOfStartTag = code.indexOf('/>', nodeStart) - 1; - } else { - endOfStartTag = code.indexOf('>', nodeStart); - } - } - - if (endOfStartTag !== undefined && endOfStartTag !== -1) { - // markerId が既に存在しないことを確認 - const tagText = code.substring(nodeStart, endOfStartTag + 1); - const markerIdRegex = /\s+markerId\s*=\s*["'][^"']*["']/; - - if (!markerIdRegex.test(tagText)) { - s.appendRight(endOfStartTag, ` markerId="${generatedMarkerId}" data-in-app-search-marker-id="${generatedMarkerId}"`); - logger.info(`Adding markerId="${generatedMarkerId}" to ${id}:${lineNumber}`); - } else { - logger.info(`markerId already exists in ${id}:${lineNumber}`); - } - } - } + nodeMarkerId = generatedMarkerId; } - const newParent: SearchMarkerElementNode | undefined = node.type === NodeTypes.ELEMENT && node.tag === 'SearchMarker' ? node : currentParent; - if ('children' in node) { - for (const child of node.children) { - if (typeof child == 'object') { - traverse(child, newParent); - } - } - } - } + markerRelations.push({ + parentId: parentId ?? undefined, + markerId: nodeMarkerId, + node: node, + }); - traverse(ast); // AST を traverse (1段階目: ID 生成と親子関係記録) + return nodeMarkerId; + }) // 2段階目: :children 属性の追加 // 最初に親マーカーごとに子マーカーIDを集約する処理を追加 @@ -1266,93 +567,42 @@ export class MarkerIdAssigner { if (!parentChildrenMap.has(relation.parentId)) { parentChildrenMap.set(relation.parentId, []); } - parentChildrenMap.get(relation.parentId)?.push(relation.markerId); + parentChildrenMap.get(relation.parentId)!.push(relation.markerId); } }); // 2. 親ごとにまとめて :children 属性を処理 for (const [parentId, childIds] of parentChildrenMap.entries()) { const parentRelation = markerRelations.find(r => r.markerId === parentId); - if (!parentRelation || !parentRelation.node) continue; + if (!parentRelation) continue; const parentNode = parentRelation.node; - const childrenProp = parentNode.props?.find((prop): prop is DirectiveNode => - prop.type === NodeTypes.DIRECTIVE && - prop.name === 'bind' && - prop.arg?.type === NodeTypes.SIMPLE_EXPRESSION && - prop.arg.content === 'children'); + const childrenProp = findAttribute(parentNode.props, 'children'); + if (childrenProp != null) { + if (childrenProp.type !== NodeTypes.DIRECTIVE) { + console.error(`children prop should be directive (:children) at ${id}:${childrenProp.loc.start.line}`); + continue; + } - // 親ノードの開始位置を特定 - const parentNodeStart = parentNode.loc!.start.offset; - const endOfParentStartTag = parentNode.children && parentNode.children.length > 0 - ? code.lastIndexOf('>', parentNode.children[0].loc!.start.offset) - : code.indexOf('>', parentNodeStart); - - if (endOfParentStartTag === -1) continue; - - // 親タグのテキストを取得 - const parentTagText = code.substring(parentNodeStart, endOfParentStartTag + 1); - - if (childrenProp) { // AST で :children 属性が検出された場合、それを更新 - try { - const childrenStart = code.indexOf('[', childrenProp.exp!.loc.start.offset); - const childrenEnd = code.indexOf(']', childrenProp.exp!.loc.start.offset); - if (childrenStart !== -1 && childrenEnd !== -1) { - const childrenArrayStr = code.slice(childrenStart, childrenEnd + 1); - let childrenArray = JSON5.parse(childrenArrayStr.replace(/'/g, '"')); + const childrenValue = getStringArrayProp(childrenProp, id); + if (childrenValue == null) continue; - // 新しいIDを追加(重複は除外) - const newIds = childIds.filter(id => !childrenArray.includes(id)); - if (newIds.length > 0) { - childrenArray = [...childrenArray, ...newIds]; - const updatedChildrenArrayStr = JSON5.stringify(childrenArray).replace(/"/g, "'"); - s.overwrite(childrenStart, childrenEnd + 1, updatedChildrenArrayStr); - logger.info(`Added ${newIds.length} child markerIds to existing :children in ${id}`); - } + const newValue: string[] = [...childrenValue]; + for (const childId of [...childIds]) { + if (!newValue.includes(childId)) { + newValue.push(childId); } - } catch (e) { - logger.error('Error updating :children attribute:', e); } + + const expression = JSON.stringify(newValue).replaceAll(/"/g, "'"); + s.overwrite(childrenProp.exp!.loc.start.offset, childrenProp.exp!.loc.end.offset, expression); + logger.info(`Added ${childIds.length} child markerIds to existing :children in ${id}`); } else { - // AST では検出されなかった場合、タグテキストを調べる - const childrenRegex = /:children\s*=\s*["']\[(.*?)\]["']/; - const childrenMatch = parentTagText.match(childrenRegex); - - if (childrenMatch) { - // テキストから :children 属性値を解析して更新 - try { - const childrenContent = childrenMatch[1]; - const childrenArrayStr = `[${childrenContent}]`; - const childrenArray = JSON5.parse(childrenArrayStr.replace(/'/g, '"')); - - // 新しいIDを追加(重複は除外) - const newIds = childIds.filter(id => !childrenArray.includes(id)); - if (newIds.length > 0) { - childrenArray.push(...newIds); - - // :children="[...]" の位置を特定して上書き - const attrStart = parentTagText.indexOf(':children='); - if (attrStart > -1) { - const attrValueStart = parentTagText.indexOf('[', attrStart); - const attrValueEnd = parentTagText.indexOf(']', attrValueStart) + 1; - if (attrValueStart > -1 && attrValueEnd > -1) { - const absoluteStart = parentNodeStart + attrValueStart; - const absoluteEnd = parentNodeStart + attrValueEnd; - const updatedArrayStr = JSON5.stringify(childrenArray).replace(/"/g, "'"); - s.overwrite(absoluteStart, absoluteEnd, updatedArrayStr); - logger.info(`Updated existing :children in tag text for ${id}`); - } - } - } - } catch (e) { - logger.error('Error updating :children in tag text:', e); - } - } else { - // :children 属性がまだない場合、新規作成 - s.appendRight(endOfParentStartTag, ` :children="${JSON5.stringify(childIds).replace(/"/g, "'")}"`); - logger.info(`Created new :children attribute with ${childIds.length} markerIds in ${id}`); - } + // :children 属性がまだない場合、新規作成 + const endOfParentStartTag = findEndOfStartTagAttributes(parentNode); + s.appendRight(endOfParentStartTag, ` :children="${JSON5.stringify(childIds).replace(/"/g, "'")}"`); + logger.info(`Created new :children attribute with ${childIds.length} markerIds in ${id}`); } } @@ -1490,7 +740,7 @@ export function pluginCreateSearchIndexVirtualModule(options: Options, asigner: this.addWatchFile(searchIndexFilePath); const code = await asigner.getOrLoad(searchIndexFilePath); - return generateJavaScriptCode(collectSearchItemIndexes([collectFileMarkers([[id, code]])])); + return generateJavaScriptCode(collectFileMarkers(searchIndexFilePath, code)); } return null; }, @@ -1504,13 +754,3 @@ export function pluginCreateSearchIndexVirtualModule(options: Options, asigner: } }; } - -// i18n参照を検出するためのヘルパー関数を追加 -function isI18nReference(text: string | null | undefined): boolean { - if (!text) return false; - // ドット記法(i18n.ts.something) - const dotPattern = /i18n\.ts\.\w+/; - // ブラケット記法(i18n.ts['something']) - const bracketPattern = /i18n\.ts\[['"][^'"]+['"]\]/; - return dotPattern.test(text) || bracketPattern.test(text); -} diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue index e1c8200b73..59099d54bd 100644 --- a/packages/frontend/src/components/MkAntennaEditor.vue +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.localOnly }} {{ i18n.ts.caseSensitive }} {{ i18n.ts.withFileAntenna }} - {{ i18n.ts.hideNotesInSensitiveChannel }} + {{ i18n.ts.excludeNotesInSensitiveChannel }}
@@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue index 061ce3f47d..ded1f9a28b 100644 --- a/packages/frontend/src/components/global/SearchMarker.vue +++ b/packages/frontend/src/components/global/SearchMarker.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -21,7 +21,7 @@ import { useTemplateRef, inject, } from 'vue'; -import type { Ref } from 'vue'; +import { DI } from '@/di.js'; const props = defineProps<{ markerId?: string; @@ -36,12 +36,13 @@ const rootEl = useTemplateRef('root'); const rootElMutationObserver = new MutationObserver(() => { checkChildren(); }); -const injectedSearchMarkerId = inject | null>('inAppSearchMarkerId', null); +const injectedSearchMarkerId = inject(DI.inAppSearchMarkerId, null); const searchMarkerId = computed(() => injectedSearchMarkerId?.value ?? window.location.hash.slice(1)); const highlighted = ref(props.markerId === searchMarkerId.value); +const isParentOfTarget = computed(() => props.children?.includes(searchMarkerId.value)); function checkChildren() { - if (props.children?.includes(searchMarkerId.value)) { + if (isParentOfTarget.value) { const el = window.document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`); highlighted.value = el == null; } @@ -105,8 +106,8 @@ onBeforeUnmount(dispose); @keyframes blink { 0%, 100% { - background: color(from var(--MI_THEME-accent) srgb r g b / 0.05); - border: 1px solid color(from var(--MI_THEME-accent) srgb r g b / 0.7); + background: color(from var(--MI_THEME-accent) srgb r g b / 0.1); + border: 1px solid color(from var(--MI_THEME-accent) srgb r g b / 0.75); } 50% { background: transparent; diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 6c6903c3a4..34cf598b84 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -30,6 +30,7 @@ import PageWithAnimBg from './global/PageWithAnimBg.vue'; import SearchMarker from './global/SearchMarker.vue'; import SearchLabel from './global/SearchLabel.vue'; import SearchKeyword from './global/SearchKeyword.vue'; +import SearchIcon from './global/SearchIcon.vue'; import type { App } from 'vue'; @@ -67,6 +68,7 @@ export const components = { SearchMarker: SearchMarker, SearchLabel: SearchLabel, SearchKeyword: SearchKeyword, + SearchIcon: SearchIcon, }; declare module '@vue/runtime-core' { @@ -98,5 +100,6 @@ declare module '@vue/runtime-core' { SearchMarker: typeof SearchMarker; SearchLabel: typeof SearchLabel; SearchKeyword: typeof SearchKeyword; + SearchIcon: typeof SearchIcon; } } diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts index 2dfe242cf4..58a2cce207 100644 --- a/packages/frontend/src/di.ts +++ b/packages/frontend/src/di.ts @@ -16,4 +16,6 @@ export const DI = { currentStickyBottom: Symbol() as InjectionKey>, mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>, inModal: Symbol() as InjectionKey, + inAppSearchMarkerId: Symbol() as InjectionKey>, + forceSpacerMin: Symbol() as InjectionKey, }; diff --git a/packages/frontend/src/lib/nirax.ts b/packages/frontend/src/lib/nirax.ts index a97803e879..a166df9eb0 100644 --- a/packages/frontend/src/lib/nirax.ts +++ b/packages/frontend/src/lib/nirax.ts @@ -266,18 +266,20 @@ export class Nirax extends EventEmitter { throw new Error('no route found for: ' + fullPath); } - if ('redirect' in res.route) { - let redirectPath: string; - if (typeof res.route.redirect === 'function') { - redirectPath = res.route.redirect(res.props); - } else { - redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : ''); + for (let current: PathResolvedResult | undefined = res; current; current = current.child) { + if ('redirect' in current.route) { + let redirectPath: string; + if (typeof current.route.redirect === 'function') { + redirectPath = current.route.redirect(current.props); + } else { + redirectPath = current.route.redirect + (current._parsedRoute.queryString ? '?' + current._parsedRoute.queryString : '') + (current._parsedRoute.hash ? '#' + current._parsedRoute.hash : ''); + } + if (_DEV_) console.log('Redirecting to: ', redirectPath); + if (_redirected && this.redirectCount++ > 10) { + throw new Error('redirect loop detected'); + } + return this.navigate(redirectPath, emitChange, true); } - if (_DEV_) console.log('Redirecting to: ', redirectPath); - if (_redirected && this.redirectCount++ > 10) { - throw new Error('redirect loop detected'); - } - return this.navigate(redirectPath, emitChange, true); } if (res.route.loginRequired && !this.isLoggedIn) { diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 3ebff5a3f6..8e40cb284a 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -134,6 +134,7 @@ export const navbarItemDef = reactive({ title: i18n.ts.chat, icon: 'ti ti-messages', to: '/chat', + show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'), indicated: computed(() => $i != null && $i.hasUnreadChatMessages), }, achievements: { diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 0023d07be8..5a12e3ae6d 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -562,12 +562,13 @@ export function success(): Promise { }); } -export function waiting(): Promise { +export function waiting(text?: string | null): Promise { return new Promise(resolve => { const showing = ref(true); const { dispose } = popup(MkWaitingDialog, { success: false, showing: showing, + text, }, { done: () => resolve(), closed: () => dispose(), diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index b1bd45baab..85c8ba15ba 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -363,7 +363,7 @@ defineExpose({ box-sizing: border-box; border-right: solid 0.5px var(--MI_THEME-divider); overflow: auto; - height: 100dvh; + height: 100cqh; } > .main { diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index d04996dac0..c49a1bf286 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -224,21 +224,24 @@ SPDX-License-Identifier: AGPL-3.0-only
- - + +
- + - + - - + + + + +
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index b8527a309d..63e89a47f2 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -78,12 +78,15 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - - + + + + - + + + + @@ -322,6 +325,7 @@ import MkInput from '@/components/MkInput.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkButton from '@/components/MkButton.vue'; +import MkSelect from '@/components/MkSelect.vue'; import MkRange from '@/components/MkRange.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkRolePreview from '@/components/MkRolePreview.vue'; diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue index 7524283641..78c1c66f52 100644 --- a/packages/frontend/src/pages/chat/XMessage.vue +++ b/packages/frontend/src/pages/chat/XMessage.vue @@ -85,7 +85,7 @@ const isMe = computed(() => props.message.fromUserId === $i.id); const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []); provide(DI.mfmEmojiReactCallback, (reaction) => { - if (!$i.policies.canChat) return; + if ($i.policies.chatAvailability !== 'available') return; sound.playMisskeySfx('reaction'); misskeyApi('chat/messages/react', { @@ -95,7 +95,7 @@ provide(DI.mfmEmojiReactCallback, (reaction) => { }); function react(ev: MouseEvent) { - if (!$i.policies.canChat) return; + if ($i.policies.chatAvailability !== 'available') return; const targetEl = getHTMLElementOrNull(ev.currentTarget ?? ev.target); if (!targetEl) return; @@ -110,7 +110,7 @@ function react(ev: MouseEvent) { } function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) { - if (!$i.policies.canChat) return; + if ($i.policies.chatAvailability !== 'available') return; if (record.user.id === $i.id) { misskeyApi('chat/messages/unreact', { @@ -138,7 +138,7 @@ function onContextmenu(ev: MouseEvent) { function showMenu(ev: MouseEvent, contextmenu = false) { const menu: MenuItem[] = []; - if (!isMe.value && $i.policies.canChat) { + if (!isMe.value && $i.policies.chatAvailability === 'available') { menu.push({ text: i18n.ts.reaction, icon: 'ti ti-mood-plus', @@ -164,7 +164,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) { type: 'divider', }); - if (isMe.value && $i.policies.canChat) { + if (isMe.value && $i.policies.chatAvailability === 'available') { menu.push({ text: i18n.ts.delete, icon: 'ti ti-trash', diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue index 17f0e0fbcd..a8ed891de0 100644 --- a/packages/frontend/src/pages/chat/home.home.vue +++ b/packages/frontend/src/pages/chat/home.home.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only