From 652d74c1e802b03b90aa7fc6815d17f473059f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Sat, 22 Jun 2024 18:18:19 +0100 Subject: [PATCH 1/4] feat: Add source notices for translations. --- assets/localisations/commands/eng-US.json | 1443 +++++++++-------- source/constants/contexts.ts | 3 + source/constants/licences.ts | 24 +- .../library/adapters/dictionaries/adapter.ts | 4 +- .../library/adapters/translators/adapter.ts | 4 + source/library/adapters/translators/deepl.ts | 2 +- .../adapters/translators/google-translate.ts | 2 +- .../library/adapters/translators/lingvanex.ts | 2 +- .../source-notices/source-notice.ts | 6 +- .../translation-source-notice.ts | 19 + .../commands/handlers/licence/dictionary.ts | 6 +- source/library/commands/handlers/translate.ts | 25 +- 12 files changed, 795 insertions(+), 745 deletions(-) create mode 100644 source/library/commands/components/source-notices/translation-source-notice.ts diff --git a/assets/localisations/commands/eng-US.json b/assets/localisations/commands/eng-US.json index 2709cebb5..29e713139 100644 --- a/assets/localisations/commands/eng-US.json +++ b/assets/localisations/commands/eng-US.json @@ -1,722 +1,723 @@ { - "noDescription": "No description.", - "information.name": "information", - "information.description": "Used to display various information.", - "information.options.bot.name": "bot", - "information.options.bot.description": "Displays information about the bot.", - "information.options.bot.strings.concept.title": "What is Logos?", - "information.options.bot.strings.concept.description": "Logos is the go-to Discord bot for language communities. Continuously growing, the bot now serves 8 language communities and over 30,000 users.", - "information.options.bot.strings.function.title": "What does Logos do?", - "information.options.bot.strings.function.description": "Logos aids learners by providing them with useful learning tools, some of which include:", - "information.options.bot.strings.function.features.definitions": "Searching for information about words, including definitions, translations, pronunciation, history, relations, usage notes, and more.", - "information.options.bot.strings.function.features.translations": "Recognition of almost 200 languages, translation in over 140 languages.", - "information.options.bot.strings.function.features.games": "Language games such as \"Pick the correct word\", similar to Clozemaster or Memrise.", - "information.options.bot.strings.function.features.messages": "The ability to propose corrections and submit answers to messages.", - "information.options.bot.strings.function.features.guides": "Guides to linguistic concepts such to the Common European Framework of Reference for Languages (CEFR).", - "information.options.bot.strings.languages.title": "What languages does Logos support?", - "information.options.bot.strings.languages.description": "On top of the plethora of features Logos provides, thanks to a team of amazing volunteers, the bot also has excellent localisation support for 🇫🇷 French, 🇩🇪 German, 🇵🇱 Polish, 🇹🇷 Turkish, 🇳🇴 Norwegian, and 🇷🇴 Romanian, with more localisations on the way for 🇫🇮 Finnish, 🇸🇪 Swedish, 🇭🇺 Hungarian, 🇦🇲 Armenian (Western + Eastern) and 🇳🇱 Dutch.", - "information.options.server.name": "server", - "information.options.server.description": "Displays information about the server.", - "information.options.server.strings.information.title": "Information about {server_name}", - "information.options.server.strings.information.description.description": "Description", - "information.options.server.strings.information.description.noDescription": "No description specified.", - "information.options.server.strings.information.description.members": "Members", - "information.options.server.strings.information.description.created": "Created", - "information.options.server.strings.information.description.channels": "Channels", - "information.options.server.strings.information.description.languages": "Languages", - "information.options.server.strings.information.description.owner": "Owner", - "information.options.server.strings.information.description.moderators": "Moderators", - "information.options.server.strings.information.description.overseenByModerators": "This server is overseen by a collective of guides.", - "information.options.server.strings.information.description.distribution": "Distribution of members' language proficiency", - "information.options.server.strings.information.description.withoutProficiency": "without a specified language proficiency.", - "information.options.server.strings.channelTypes.text": "Text", - "information.options.server.strings.channelTypes.voice": "Voice", - "information.options.server.strings.languageTypes.home": "Home", - "information.options.server.strings.languageTypes.target": "Target", - "answer.message.name": "Answer", - "answer.message.description": "Allows the user to submit an answer to a question.", - "answer.strings.cannotAnswer.title": "Cannot answer message", - "answer.strings.cannotAnswer.description": "That message cannot be answered.", - "answer.strings.cannotAnswerOwn.title": "Cannot answer own message", - "answer.strings.cannotAnswerOwn.description": "You cannot submit an answer for your own message.", - "answer.strings.sureToCancel.title": "Sure to exit?", - "answer.strings.sureToCancel.description": "Are you sure you want to stop submitting your answer?", - "answer.strings.submittedBy": "Answer submitted by {username}.", - "cefr.name": "cefr", - "cefr.description": "Displays a guide for the CEFR (Common European Framework of Reference for Languages).", - "cefr.strings.brackets.a": "Bracket A", - "cefr.strings.brackets.b": "Bracket B", - "cefr.strings.brackets.c": "Bracket C", - "cefr.strings.tabs.guide": "Guide", - "cefr.strings.tabs.examples": "Examples", - "cefr.strings.levels.a0.title": "A0 (Starter)", - "cefr.strings.levels.a0.description": "I have just started, I have very limited command of the language.", - "cefr.strings.levels.a1.title": "A1 (Elementary)", - "cefr.strings.levels.a1.description": "I've been learning for a short while, I understand the language as spoken in day-to-day contexts and if the language is spoken slowly and clearly.", - "cefr.strings.levels.a2.title": "A2 (Pre-intermediate)", - "cefr.strings.levels.a2.description": "I started a longer while ago, I can communicate in a range of day-to-day situations, and on topics I have studied vocabulary in previously.", - "cefr.strings.levels.b1.title": "B1 (Intermediate)", - "cefr.strings.levels.b1.description": "I've been studying for a longer while, and, although I'm unable to speak fluently, I have some confidence in my command of the language. I'm starting to find alternate ways to express myself when I don't know certain vocabulary.", - "cefr.strings.levels.b2.title": "B2 (Upper Intermediate)", - "cefr.strings.levels.b2.description": "I have been learning for quite a long time. I'm able to use the language effectively and I speak with good confidence. My inadequacies in vocabulary are becoming near indistinguishable as I quickly find ways to express myself using vocabulary I already know.", - "cefr.strings.levels.c1.title": "C1 (Advanced)", - "cefr.strings.levels.c1.description": "I've been learning for a very long time. Not only am I able to express myself accurately and with confidence, I can use language in a situational, culturally-aware way. Sometimes, I still come across vocabulary, phrasing or features of the language I didn't know.", - "cefr.strings.levels.c2.title": "C2 (Mastery)", - "cefr.strings.levels.c2.description": "I have been studying the language for years now. I speak the language with ease, never finding myself unable to talk about what I want in detail and with nuance. When needed, I can be creative with my language, recognising the different ways to use it. I have very good grasp of what sounds good and what doesn't, and I can easily pick up on mistakes, providing alternatives, and even correcting myself.", - "cefr.strings.levels.c3.title": "C2+/C3 (Expert)", - "cefr.strings.levels.c3.description": "On top of the command over language a C2 user would be expected to display, I have extensive knowledge of creative, poetic, technical or unusual uses of vocabulary generally expected of people with a very high command of language.", - "correction.options.partial.message.name": "Quick Correction", - "correction.options.partial.message.description": "Allows the user to submit a quick correction to a message.", - "correction.options.full.message.name": "Full-text Correction", - "correction.options.full.message.description": "Allows the user to submit a thorough correction to a message.", - "correction.strings.textsNotDifferent.title": "Original and corrected versions are the same", - "correction.strings.textsNotDifferent.description": "The corrected version may not be the same as the original.", - "correction.strings.tooLong.title": "Message too long", - "correction.strings.tooLong.description.tooLong": "The selected message is too long to be corrected.", - "correction.strings.tooLong.description.maximumLength": "The number of characters in a message cannot exceed {character_limit}.", - "correction.strings.cannotCorrect.title": "Cannot correct message", - "correction.strings.cannotCorrect.description": "That message cannot be corrected.", - "correction.strings.cannotCorrectOwn.title": "Cannot correct own message", - "correction.strings.cannotCorrectOwn.description": "You cannot submit a correction for your own message.", - "correction.strings.userDoesNotWantCorrections.title": "User does not want corrections", - "correction.strings.userDoesNotWantCorrections.description": "This user has specifically stated that they do not wish to be corrected.", - "correction.strings.failed.title": "Unable to correct", - "correction.strings.failed.description": "Failed to correct the given message.", - "correction.strings.sureToCancel.title": "Sure to exit?", - "correction.strings.sureToCancel.description": "Are you sure you want to stop making your correction?", - "correction.strings.suggestedBy": "Correction suggested by {username}.", - "recognise.name": "recognise", - "recognise.description": "Given a text, recognises its source language, or mentions likely candidates.", - "recognise.options.text.name": "text", - "recognise.options.text.description": "The text to use for language recognition.", - "recognise.message.name": "Recognise Language", - "recognise.message.description": "Given a message, recognises its source language, or mentions likely candidates.", - "recognise.strings.cannotUse.title": "Cannot use message", - "recognise.strings.cannotUse.description": "That message cannot be used for language recognition.", - "recognise.strings.unknown.title": "Unknown language", - "recognise.strings.unknown.description.text": "Could not recognise the language of the given text.", - "recognise.strings.unknown.description.message": "Could not recognise the language of the given message.", - "recognise.strings.textEmpty.title": "Text empty", - "recognise.strings.textEmpty.description": "The text must not be empty or consist solely of whitespace.", - "recognise.strings.fields.likelyMatches.title": "Likely matches", - "recognise.strings.fields.likelyMatches.description.single": "The language is likely to be {language}.", - "recognise.strings.fields.likelyMatches.description.multiple": "The language is likely to be one of:", - "recognise.strings.fields.possibleMatches.title": "Other options", - "recognise.strings.fields.possibleMatches.description.single": "The language is less likely to be {language}.", - "recognise.strings.fields.possibleMatches.description.multiple": "These choices are less likely, but the language could also be one of:", - "game.name": "game", - "game.description": "Pick the correct word out of four to fit in the blank.", - "game.strings.sentence": "Sentence", - "game.strings.translation": "Translation", - "game.strings.noSentencesAvailable.title": "No sentences available", - "game.strings.noSentencesAvailable.description": "There are no sentences available in the requested language.", - "game.strings.skip": "Skip", - "game.strings.sourcedFrom": "Sentences sourced from {source}.", - "game.strings.correctGuesses": "Correct guesses: {number}", - "game.strings.allTime": "All time: {number}", - "game.strings.next": "Next", - "resources.name": "resources", - "resources.description": "Redirects to the repository of resources to learn the language.", - "resources.strings.redirect": "Click for resources to learn {language}", - "translate.name": "translate", - "translate.description": "Translates a text from the source language to the target language.", - "translate.options.text.name": "text", - "translate.options.text.description": "The text to translate.", - "translate.options.from.name": "from", - "translate.options.from.description": "The source language.", - "translate.options.to.name": "to", - "translate.options.to.description": "The target language.", - "translate.message.name": "Translate", - "translate.message.description": "Translates the contents of a message from the source language to the target language.", - "translate.strings.cannotUse.title": "Cannot translate message", - "translate.strings.cannotUse.description": "That message cannot be translated.", - "translate.strings.textEmpty.title": "Text empty", - "translate.strings.textEmpty.description": "The source text may not be empty.", - "translate.strings.invalid.source.title": "Invalid source language", - "translate.strings.invalid.source.description": "The source language is invalid.", - "translate.strings.invalid.target.title": "Invalid target language", - "translate.strings.invalid.target.description": "The target language is invalid.", - "translate.strings.invalid.both.title": "Invalid languages", - "translate.strings.invalid.both.description": "Both the source and target languages are invalid.", - "translate.strings.languagesNotDifferent.title": "Source and target languages are the same", - "translate.strings.languagesNotDifferent.description": "The target language may not be the same as the source language.", - "translate.strings.cannotDetermine.source.title": "Cannot determine source language", - "translate.strings.cannotDetermine.source.description.cannotDetermine": "The source language could not be determined.", - "translate.strings.cannotDetermine.source.description.tryAgain": "Please set it yourself, or try again with a different input.", - "translate.strings.cannotDetermine.target.title": "Cannot determine target language", - "translate.strings.cannotDetermine.target.description.cannotDetermine": "The target language could not be determined.", - "translate.strings.cannotDetermine.target.description.tryAgain": "Please set it manually to the language you'd like to translate to.", - "translate.strings.languageNotSupported.title": "Language not supported", - "translate.strings.languageNotSupported.description": "The {language} language is not supported.", - "translate.strings.failed.title": "Unable to translate text", - "translate.strings.failed.description": "Failed to translate the given text.", - "translate.strings.noTranslationAdapters.title": "Language pair not supported", - "translate.strings.noTranslationAdapters.description": "The selected language pair is not supported.", - "translate.strings.sourceText": "Source Text", - "translate.strings.translation": "Translation", - "word.name": "word", - "word.description": "Displays information about a given word.", - "word.options.word.name": "word", - "word.options.word.description": "The word to display information about.", - "word.options.language.name": "language", - "word.options.language.description": "The language of the word.", - "word.options.verbose.name": "verbose", - "word.options.verbose.description": "If set to true, more (possibly unnecessary) information will be shown.", - "word.strings.invalid.language.title": "Invalid language", - "word.strings.invalid.language.description": "The language you specified is invalid.", - "word.strings.noDictionaryAdapters.title": "No dictionaries available for language", - "word.strings.noDictionaryAdapters.description": "There are no dictionaries available in the requested language.", - "word.strings.noResults.title": "No results", - "word.strings.noResults.description": "There are no results for the word '{word}'.", - "word.strings.fields.translations": "Translations", - "word.strings.fields.pronunciation": "Pronunciation", - "word.strings.fields.nativeDefinitions": "Native definitions", - "word.strings.fields.definitions": "Definitions", - "word.strings.fields.etymology": "Etymology", - "word.strings.fields.expressions": "Expressions", - "word.strings.relations.synonyms": "Synonyms", - "word.strings.relations.antonyms": "Antonyms", - "word.strings.relations.diminutives": "Diminutives", - "word.strings.relations.augmentatives": "Augmentatives", - "word.strings.definitionsOmitted": "Omitted {definitions}. To display more results, enable the `{flag}` flag.", - "word.strings.definitionsOmitted.definitions.one": "{one} definition", - "word.strings.definitionsOmitted.definitions.two": "{two} definitions", - "word.strings.definitionsOmitted.definitions.many": "{many} definitions", - "word.strings.page": "Page", - "word.strings.definitions": "Definitions", - "word.strings.nativeDefinitionsForWord": "Native definitions for '{word}'", - "word.strings.definitionsForWord": "Definitions for '{word}'", - "word.strings.inflection": "Inflection", - "word.strings.verbs.moodsAndParticiples": "Moods and participles", - "word.strings.verbs.moods.conditional": "Conditional", - "word.strings.verbs.moods.imperative": "Imperative", - "word.strings.verbs.moods.indicative": "Indicative", - "word.strings.verbs.moods.infinitive": "Infinitive", - "word.strings.verbs.moods.longInfinitive": "Long infinitive", - "word.strings.verbs.moods.optative": "Optative", - "word.strings.verbs.moods.presumptive": "Presumptive", - "word.strings.verbs.moods.subjunctive": "Subjunctive", - "word.strings.verbs.moods.supine": "Supine", - "word.strings.verbs.participles.present": "Present participle", - "word.strings.verbs.participles.past": "Past participle", - "word.strings.verbs.popular": "popular", - "word.strings.verbs.tenses.tenses": "Tenses", - "word.strings.verbs.tenses.present": "Present", - "word.strings.verbs.tenses.presentContinuous": "Present continuous", - "word.strings.verbs.tenses.imperfect": "Imperfect", - "word.strings.verbs.tenses.preterite": "Preterite", - "word.strings.verbs.tenses.pluperfect": "Pluperfect", - "word.strings.verbs.tenses.perfect": "Perfect", - "word.strings.verbs.tenses.compoundPerfect": "Compound perfect", - "word.strings.verbs.tenses.future": "Future", - "word.strings.verbs.tenses.futureCertain": "Certain future", - "word.strings.verbs.tenses.futurePlanned": "Planned future", - "word.strings.verbs.tenses.futureDecided": "Decided future", - "word.strings.verbs.tenses.futureIntended": "Intended future", - "word.strings.verbs.tenses.futureInThePast": "Future-in-the-past", - "word.strings.verbs.tenses.futurePerfect": "Future perfect", - "word.strings.nouns.cases.cases": "Cases", - "word.strings.nouns.cases.nominativeAccusative": "Nominative-accusative", - "word.strings.nouns.cases.genitiveDative": "Genitive-dative", - "word.strings.nouns.cases.vocative": "Vocative", - "word.strings.nouns.singular": "Singular", - "word.strings.nouns.plural": "Plural", - "word.strings.sourcedResponsibly": "Information sourced with gratitude from {dictionaries}.", - "word.strings.sourcedResponsibly.dictionaries.one": "the dictionary above", - "word.strings.sourcedResponsibly.dictionaries.two": "the dictionaries above", - "word.strings.sourcedResponsibly.dictionaries.many": "the dictionaries above", - "list.name": "list", - "list.description": "Allows the viewing of various information about users.", - "list.options.warnings.name": "warnings", - "list.options.warnings.description": "Lists the warnings issued to a user.", - "list.options.warnings.strings.failed.title": "Unable to show warnings", - "list.options.warnings.strings.failed.description": "Failed to show warnings for the given user.", - "list.options.warnings.strings.noActiveWarnings.title": "No active warnings", - "list.options.warnings.strings.noActiveWarnings.description.self": "You have no active warnings.", - "list.options.warnings.strings.noActiveWarnings.description.other": "This user does not have any active warnings.", - "list.options.warnings.strings.warnings.title": "Warnings", - "list.options.warnings.strings.warnings.description.warning": "Warning #{index}, given {relative_timestamp}", - "list.options.praises.name": "praises", - "list.options.praises.description": "Allows the viewing of user praises.", - "list.options.praises.options.author.name": "author", - "list.options.praises.options.author.description": "Lists the praises given out by a user.", - "list.options.praises.options.target.name": "target", - "list.options.praises.options.target.description": "Lists the praises given out to a user.", - "list.options.praises.strings.failed.title": "Unable to show praises", - "list.options.praises.strings.failed.description": "Failed to show praises for the given user.", - "list.options.praises.strings.noPraises.title": "No praises", - "list.options.praises.strings.noPraises.description.self.author": "You have not given out any praises.", - "list.options.praises.strings.noPraises.description.self.target": "You have not been given any praises.", - "list.options.praises.strings.noPraises.description.other.author": "This user has not given out any praises.", - "list.options.praises.strings.noPraises.description.other.target": "This user has not been given any praises.", - "list.options.praises.strings.praises.title": "Praises", - "list.options.praises.strings.praises.noComment": "No comment attached.", - "pardon.name": "pardon", - "pardon.description": "Removes one of the warnings previously given to a user.", - "pardon.options.warning.name": "warning", - "pardon.options.warning.description": "The warning to remove.", - "pardon.strings.failed.title": "Failed to remove warning", - "pardon.strings.failed.description": "Due to unknown reasons, removing the specified warning failed.", - "pardon.strings.invalidWarning.title": "Invalid warning", - "pardon.strings.invalidWarning.description": "The warning you specified is invalid.", - "pardon.strings.pardoned.title": "User pardoned", - "pardon.strings.pardoned.description": "User {user_mention} has been pardoned from their warning for: {reason}", - "policy.name": "policy", - "policy.description": "Displays the server moderation policy.", - "purge.name": "purge", - "purge.description": "Deletes a number of messages from a channel.", - "purge.options.start.name": "start", - "purge.options.start.description": "ID of the message at the beginning of the range.", - "purge.options.end.name": "end", - "purge.options.end.description": "ID of the message at the end of the range.", - "purge.options.author.name": "author", - "purge.options.author.description": "The user whose messages to delete.", - "purge.strings.invalid.start.title": "Invalid start message ID", - "purge.strings.invalid.start.description": "The start message ID you provided is invalid.", - "purge.strings.invalid.end.title": "Invalid end message ID", - "purge.strings.invalid.end.description": "The end message ID you provided is invalid.", - "purge.strings.invalid.both.title": "Invalid IDs", - "purge.strings.invalid.both.description": "The start and end message IDs you provided are invalid.", - "purge.strings.idsNotDifferent.title": "Start and end messages are the same", - "purge.strings.idsNotDifferent.description": "The end message may not be the same as the start message.", - "purge.strings.failed.title": "Failed to purge", - "purge.strings.failed.description": "Due to unknown reasons, purging messages from this channel has failed.", - "purge.strings.indexing.title": "Indexing messages...", - "purge.strings.indexing.description": "The bot is indexing messages falling within the range you specified.", - "purge.strings.indexed.title": "Finished indexing", - "purge.strings.indexed.description.some": "The bot has indexed messages falling within the range you specified, and found **{messages}**.", - "purge.strings.indexed.description.some.messages.one": "{one} deletable message", - "purge.strings.indexed.description.some.messages.two": "{two} deletable messages", - "purge.strings.indexed.description.some.messages.many": "{many} deletable messages", - "purge.strings.indexed.description.none": "The bot has indexed messages falling within the range you specified, and found no deletable messages.", - "purge.strings.indexed.description.tryDifferentQuery": "Try a different query.", - "purge.strings.indexed.description.tooMany": "The bot has indexed messages falling within the range you specified, and found **{messages}**, which is greater than the **maximum number of messages deletable in a given purge action ({maximum_deletable})**.", - "purge.strings.indexed.description.tooMany.messages.one": "{one} deletable message", - "purge.strings.indexed.description.tooMany.messages.two": "{two} deletable messages", - "purge.strings.indexed.description.tooMany.messages.many": "{many} deletable messages", - "purge.strings.indexed.description.limited": "If you choose to continue, **{messages}** will be deleted instead.", - "purge.strings.indexed.description.limited.messages.one": "{one} message", - "purge.strings.indexed.description.limited.messages.two": "{two} messages", - "purge.strings.indexed.description.limited.messages.many": "{many} messages", - "purge.strings.sureToPurge.title": "Sure to purge?", - "purge.strings.sureToPurge.description": "During purging, **{messages}** will be irrevocably deleted from {channel_mention}.", - "purge.strings.sureToPurge.description.messages.one": "{one} message", - "purge.strings.sureToPurge.description.messages.two": "{two} messages", - "purge.strings.sureToPurge.description.messages.many": "{many} messages", - "purge.strings.continue.title": "Continue?", - "purge.strings.continue.description": "During purging, **{messages}** will be irrevocably deleted from {channel_mention}, which is only a fraction of the {all_messages} found.", - "purge.strings.continue.description.messages.one": "{one} message", - "purge.strings.continue.description.messages.two": "{two} messages", - "purge.strings.continue.description.messages.many": "{many} messages", - "purge.strings.continue.description.allMessages.one": "{one} message", - "purge.strings.continue.description.allMessages.two": "{two} messages", - "purge.strings.continue.description.allMessages.many": "{many} messages", - "purge.strings.purging.title": "Purging...", - "purge.strings.purging.description.purging": "{messages} from {channel_mention}.", - "purge.strings.purging.description.purging.messages.one": "{one} message is being purged", - "purge.strings.purging.description.purging.messages.two": "{two} messages are being purged", - "purge.strings.purging.description.purging.messages.many": "{many} messages are being purged", - "purge.strings.purging.description.mayTakeTime": "This may take a while.", - "purge.strings.purging.description.onceComplete": "Once complete, you will be able to find a success message in the log channel.", - "purge.strings.purged.title": "Purged successfully", - "purge.strings.purged.description": "{messages} from {channel_mention}.", - "purge.strings.purged.description.messages.one": "{one} message has been purged", - "purge.strings.purged.description.messages.two": "{two} messages have been purged", - "purge.strings.purged.description.messages.many": "{many} messages have been purged", - "purge.strings.rangeTooBig.title": "Range too big", - "purge.strings.rangeTooBig.description.rangeTooBig": "While indexing the selected range of messages, the bot found more than **{messages}**, which is too big of a range.", - "purge.strings.rangeTooBig.description.rangeTooBig.messages.one": "{one} message", - "purge.strings.rangeTooBig.description.rangeTooBig.messages.two": "{two} messages", - "purge.strings.rangeTooBig.description.rangeTooBig.messages.many": "{many} messages", - "purge.strings.rangeTooBig.description.trySmaller": "Try selecting a smaller range of messages to purge.", - "purge.strings.start": "Start message", - "purge.strings.end": "End message", - "purge.strings.noContent": "No content", - "purge.strings.embedPosted": "Embed posted {relative_timestamp} by {user_mention}", - "purge.strings.posted": "Posted {relative_timestamp} by {user_mention}", - "purge.strings.messagesFound": "Messages found", - "purge.strings.yes": "Yes", - "purge.strings.no": "No", - "report.name": "report", - "report.description": "Allows the user to create a user report.", - "report.strings.submitted.title": "Report submitted!", - "report.strings.submitted.description": "Your report has been submitted. The report will be reviewed by the server staff, but you will not be notified directly about the outcome of a particular report.", - "report.strings.failed.title": "Failed to submit report", - "report.strings.failed.description": "Due to unknown reasons, submitting the report failed.", - "report.strings.sureToCancel.title": "Sure to exit?", - "report.strings.sureToCancel.description": "Are you sure you want to stop submitting your report?", - "report.strings.tooMany.title": "Too many reports", - "report.strings.tooMany.description": "You have already made a few reports recently. Before submitting another report, you should wait some time.", - "rule.name": "rule", - "rule.description": "Cites a server rule.", - "rule.options.rule.name": "rule", - "rule.options.rule.description": "The rule to cite.", - "rule.strings.invalid.title": "Invalid rule", - "rule.strings.invalid.description": "The rule you specified is invalid.", - "slowmode.name": "slowmode", - "slowmode.description": "Toggles slowmode for the channel this command is run in.", - "slowmode.options.level.name": "level", - "slowmode.options.level.description": "The level of slowmode to apply.", - "slowmode.strings.levels.lowest": "No biggie. (Lowest, 5 seconds)", - "slowmode.strings.levels.low": "It's minor. (Low, 10 seconds)", - "slowmode.strings.levels.medium": "It's getting problematic. (Medium, 30 seconds)", - "slowmode.strings.levels.high": "It's major. (High, 1 minute).", - "slowmode.strings.levels.highest": "Sh*t just hit the fan. (Highest, 5 minutes)", - "slowmode.strings.levels.emergency": "It's an emergency. (Emergency, 20 minutes)", - "slowmode.strings.levels.lockdown": "We need to lock the place down. (Lockdown, 1 hour)", - "slowmode.strings.levels.beyond": "It's reached the next level. (Beyond, 1 day)", - "slowmode.strings.tooSoon.title": "Cannot disable too soon", - "slowmode.strings.tooSoon.description.justEnabled": "Slowmode has been enabled just a moment ago.", - "slowmode.strings.tooSoon.description.canDisableIn": "You will be able to disable it {relative_timestamp}.", - "slowmode.strings.invalid.title": "Invalid level", - "slowmode.strings.invalid.description": "The level you specified is invalid.", - "slowmode.strings.enabled.title": "Slowmode active", - "slowmode.strings.enabled.description": "Slowmode has been enabled for this channel.", - "slowmode.strings.disabled.title": "Slowmode no longer active", - "slowmode.strings.disabled.description": "Slowmode has been disabled for this channel.", - "slowmode.strings.upgraded.title": "Slowmode level upgraded", - "slowmode.strings.upgraded.description": "The slowmode level has been upgraded to a higher severity.", - "slowmode.strings.downgraded.title": "Slowmode level downgraded", - "slowmode.strings.downgraded.description": "The slowmode level has been downgraded to a lesser severity.", - "slowmode.strings.theSame.title": "Slowmode level is the same", - "slowmode.strings.theSame.description.theSame": "The current slowmode level is the same as the requested one.", - "slowmode.strings.theSame.description.chooseDifferent": "Choose a different level.", - "timeout.name": "timeout", - "timeout.description": "Used to manage user timeouts.", - "timeout.options.set.name": "set", - "timeout.options.set.description": "Times a user out, making them unable to interact on the server.", - "timeout.options.clear.name": "clear", - "timeout.options.clear.description": "Clears a user's timeout.", - "timeout.strings.durationInvalid.title": "Invalid duration", - "timeout.strings.durationInvalid.description": "The duration you specified is invalid.", - "timeout.strings.tooShort.title": "Timeout too short", - "timeout.strings.tooShort.description": "The timeout must last at least a minute.", - "timeout.strings.tooLong.title": "Timeout too long", - "timeout.strings.tooLong.description": "The duration must not be longer than a week.", - "timeout.strings.timedOut.title": "User timed out", - "timeout.strings.timedOut.description": "User {user_mention} has been timed out. The timeout will expire {relative_timestamp}.", - "timeout.strings.notTimedOut.title": "User not timed out", - "timeout.strings.notTimedOut.description": "User {user_mention} is not currently timed out.", - "timeout.strings.timeoutCleared.title": "Timeout cleared", - "timeout.strings.timeoutCleared.description": "User {user_mention} is no longer timed out.", - "warn.name": "warn", - "warn.description": "Warns a user.", - "warn.options.rule.name": "rule", - "warn.options.rule.description": "The rule to cite as having been broken.", - "warn.options.rule.strings.other": "Other (justified in reason)", - "warn.strings.invalidRule.title": "Invalid rule", - "warn.strings.invalidRule.description": "The rule you specified is invalid.", - "warn.strings.failed.title": "Failed to warn user", - "warn.strings.failed.description": "Due to unknown reasons, warning the specified user failed.", - "warn.strings.warned.title": "User warned", - "warn.strings.warned.description": "User {user_mention} has been warned. They now have {warnings}.", - "warn.strings.warned.description.warnings.one": "{one} warning", - "warn.strings.warned.description.warnings.two": "{two} warnings", - "warn.strings.warned.description.warnings.many": "{many} warnings", - "warn.strings.limitReached.title": "Warning limit reached", - "warn.strings.limitReached.description": "{user_mention} has reached the warning limit ({limit}).", - "warn.strings.limitSurpassed.title": "Warning limit surpassed", - "warn.strings.limitSurpassed.description": "{user_mention} has *surpassed* the warning limit ({limit}), having just received warning no. {number}.", - "warn.strings.limitSurpassedTimedOut.title": "Warning limit surpassed", - "warn.strings.limitSurpassedTimedOut.description": "{user_mention} has *surpassed* the warning limit ({limit}), having just received warning no. {number}, and has subsequently been timed out for {period}.", - "music.name": "music", - "music.description": "Allows the user to manage music playback in a voice channel.", - "music.options.history.name": "history", - "music.options.history.description": "Displays a list of previously played songs.", - "music.options.history.strings.playbackHistory": "Playback history", - "music.options.loop.name": "loop", - "music.options.loop.description": "Loops the currently playing song.", - "music.options.loop.strings.noSong.title": "Not playing song", - "music.options.loop.strings.noSong.description": "There is no song to loop.", - "music.options.loop.strings.noSongCollection.title": "Not playing song collection", - "music.options.loop.strings.noSongCollection.description.noSongCollection": "There is no song collection to loop.", - "music.options.loop.strings.noSongCollection.description.trySongInstead": "Try looping the current song instead.", - "music.options.loop.strings.enabled.title": "Loop enabled", - "music.options.loop.strings.enabled.description.song": "The current song will be looped.", - "music.options.loop.strings.enabled.description.songCollection": "The current song collection will be looped.", - "music.options.loop.strings.disabled.title": "Loop disabled", - "music.options.loop.strings.disabled.description.song": "The current song will no longer be looped.", - "music.options.loop.strings.disabled.description.songCollection": "The current song collection will no longer be looped.", - "music.options.now.name": "now", - "music.options.now.description": "Displays the currently playing song.", - "music.options.now.strings.noSong.title": "Not playing song", - "music.options.now.strings.noSong.description": "There is no song to show information about.", - "music.options.now.strings.noSongCollection.title": "Not playing song collection", - "music.options.now.strings.noSongCollection.description.noSongCollection": "There is no song collection to show information about.", - "music.options.now.strings.noSongCollection.description.trySongInstead": "Try requesting information about the current song instead.", - "music.options.now.strings.nowPlaying": "Now playing", - "music.options.now.strings.songs": "Songs", - "music.options.now.strings.collection": "Collection", - "music.options.now.strings.track": "Track", - "music.options.now.strings.title": "Title", - "music.options.now.strings.requestedBy": "Requested by", - "music.options.now.strings.runningTime": "Running time", - "music.options.now.strings.playingSince": "Since {relative_timestamp}.", - "music.options.now.strings.sourcedFrom": "This listing was sourced from {source}.", - "music.options.now.strings.theInternet": "the internet", - "music.options.pause.name": "pause", - "music.options.pause.description": "Pauses the currently playing song or song collection.", - "music.options.pause.strings.paused.title": "Paused", - "music.options.pause.strings.paused.description": "Paused the playback of music.", - "music.options.play.name": "play", - "music.options.play.description": "Allows the user to play music in a voice channel.", - "music.options.play.options.stream.name": "stream", - "music.options.play.options.stream.description": "Plays an audio stream.", - "music.options.play.options.stream.options.url.name": "url", - "music.options.play.options.stream.options.url.description": "Link to the audio stream.", - "music.options.play.options.youtube.name": "youtube", - "music.options.play.options.youtube.description": "Plays a song from YouTube.", - "music.options.play.strings.selectSong.title": "Select an entry", - "music.options.play.strings.selectSong.description": "Select a song or song collection from the choices below.", - "music.options.play.strings.stream": "Audio stream", - "music.options.play.strings.notFound.title": "Song not found", - "music.options.play.strings.notFound.description.notFound": "Couldn't find the requested song.", - "music.options.play.strings.notFound.description.tryDifferentQuery": "You could try an alternative search, or request a different song.", - "music.options.play.strings.inDifferentVc.title": "Bot is in another channel", - "music.options.play.strings.inDifferentVc.description": "The bot is already playing music in another voice channel.", - "music.options.play.strings.queueFull.title": "The queue is full", - "music.options.play.strings.queueFull.description": "The queue is full. Try removing a song from the song queue, skip the current song to advance the queue immediately, or wait until the current song finishes playing.", - "music.options.play.strings.queued.title": "Listing queued", - "music.options.play.strings.queued.description.private": "Your listing, **{title}**, has been added to the queue.", - "music.options.play.strings.queued.description.public": "**{title}** added to the queue as requested by {user_mention}.", - "music.options.play.strings.failedToLoad.title": "Failed to load track", - "music.options.play.strings.failedToLoad.description": "The track, **{title}**, could not be loaded.", - "music.options.play.strings.failedToPlay.title": "Failed to play track", - "music.options.play.strings.failedToPlay.description": "The track, **{title}**, could not be played.", - "music.options.play.strings.nowPlaying.title.nowPlaying": "Now playing {listing_type}", - "music.options.play.strings.nowPlaying.title.type.song": "song", - "music.options.play.strings.nowPlaying.title.type.songCollection": "song collection", - "music.options.play.strings.nowPlaying.title.type.stream": "audio stream", - "music.options.play.strings.nowPlaying.description.nowPlaying": "Now playing {song_information} [**{title}**]({url}) as requested by {user_mention}.", - "music.options.play.strings.nowPlaying.description.track": "track **{index}/{number}** of **{title}**: ", - "music.options.queue.name": "queue", - "music.options.queue.description": "Displays a list of queued song listings.", - "music.options.queue.strings.queue": "Queue", - "music.options.remove.name": "remove", - "music.options.remove.description": "Removes a song listing from the queue.", - "music.options.remove.strings.queueEmpty.title": "The queue is empty", - "music.options.remove.strings.queueEmpty.description": "There are no songs in the queue.", - "music.options.remove.strings.selectSong.title": "Select an entry", - "music.options.remove.strings.selectSong.description": "Select a song or song collection to remove from the choices below.", - "music.options.remove.strings.failed.title": "Failed to remove song", - "music.options.remove.strings.failed.description": "Due to unknown reasons, removing the selected song from the queue failed.", - "music.options.remove.strings.removed.title": "Removed", - "music.options.remove.strings.removed.description": "The song **{title}** has been removed by {user_mention}.", - "music.options.replay.name": "replay", - "music.options.replay.description": "Begins playing the currently playing song from the start.", - "music.options.replay.strings.noSong.title": "No song to replay", - "music.options.replay.strings.noSong.description": "There is no song to replay.", - "music.options.replay.strings.noSongCollection.title": "No song collection to replay", - "music.options.replay.strings.noSongCollection.description.noSongCollection": "There is no song collection to replay.", - "music.options.replay.strings.noSongCollection.description.trySongInstead": "Try replaying the current song instead.", - "music.options.replay.strings.replaying.title": "Replaying", - "music.options.replay.strings.replaying.description": "The current song will be played again.", - "music.options.resume.name": "resume", - "music.options.resume.description": "Unpauses the currently playing song if it is paused.", - "music.options.resume.strings.noSong.title": "No song to resume", - "music.options.resume.strings.noSong.description": "There is no song to resume playing.", - "music.options.resume.strings.notPaused.title": "Song not paused", - "music.options.resume.strings.notPaused.description": "The current song is not paused.", - "music.options.resume.strings.resumed.title": "Resumed", - "music.options.resume.strings.resumed.description": "Music playback has been resumed.", - "music.options.skip-to.name": "skip-to", - "music.options.skip-to.description": "Skips to a given point in the currently playing song.", - "music.options.skip-to.strings.noSong.title": "No song to seek timestamp", - "music.options.skip-to.strings.noSong.description": "There is no song to seek a timestamp in.", - "music.options.skip-to.strings.invalidTimestamp.title": "Invalid timestamp", - "music.options.skip-to.strings.invalidTimestamp.description": "The timestamp you specified is invalid.", - "music.options.skip-to.strings.skippedTo.title": "Skipped to timestamp", - "music.options.skip-to.strings.skippedTo.description": "Playback has skipped to the specified timestamp.", - "music.options.fast-forward.name": "fast-forward", - "music.options.fast-forward.description": "Fast-forwards the song by a given amount of time.", - "music.options.fast-forward.strings.noSong.title": "No song to fast-forward", - "music.options.fast-forward.strings.noSong.description": "There is no song to fast-forward.", - "music.options.fast-forward.strings.invalidTimestamp.title": "Invalid timestamp", - "music.options.fast-forward.strings.invalidTimestamp.description": "The timestamp you specified is invalid.", - "music.options.fast-forward.strings.fastForwarded.title": "Fast-forwarded", - "music.options.fast-forward.strings.fastForwarded.description": "Playback has skipped by the given amount of time.", - "music.options.rewind.name": "rewind", - "music.options.rewind.description": "Rewinds the song by a given amount of time.", - "music.options.rewind.strings.noSong.title": "No song to rewind", - "music.options.rewind.strings.noSong.description": "There is no song to rewind.", - "music.options.rewind.strings.invalidTimestamp.title": "Invalid timestamp", - "music.options.rewind.strings.invalidTimestamp.description": "The timestamp you specified is invalid.", - "music.options.rewind.strings.rewound.title": "Rewound", - "music.options.rewind.strings.rewound.description": "Playback has been rewound by the given amount of time.", - "music.options.skip.name": "skip", - "music.options.skip.description": "Skips the currently playing song.", - "music.options.skip.strings.noSong.title": "No song to skip", - "music.options.skip.strings.noSong.description": "There is no song to skip.", - "music.options.skip.strings.noSongCollection.title": "No song collection to skip", - "music.options.skip.strings.noSongCollection.description.noSongCollection": "There is no song collection to skip.", - "music.options.skip.strings.noSongCollection.description.trySongInstead": "Try skipping the current song instead.", - "music.options.skip.strings.skippedSong.title": "Skipped", - "music.options.skip.strings.skippedSong.description": "The song has been skipped.", - "music.options.skip.strings.skippedSongCollection.title": "Skipped", - "music.options.skip.strings.skippedSongCollection.description": "The song collection has been skipped.", - "music.options.stop.name": "stop", - "music.options.stop.description": "Stops the current listening session, clearing the queue and song history.", - "music.options.stop.strings.stopped.title": "Stopped", - "music.options.stop.strings.stopped.description": "The listening session has been stopped, and the song queue and history have been cleared.", - "music.options.unskip.name": "unskip", - "music.options.unskip.description": "Brings back the last played song.", - "music.options.unskip.strings.historyEmpty.title": "The history is empty", - "music.options.unskip.strings.historyEmpty.description": "There are no song listings to bring back.", - "music.options.unskip.strings.noSongCollection.title": "No song collection to skip", - "music.options.unskip.strings.noSongCollection.description.noSongCollection": "There is no song collection to skip.", - "music.options.unskip.strings.noSongCollection.description.trySongInstead": "Try skipping the current song instead.", - "music.options.unskip.strings.queueFull.title": "The queue is full", - "music.options.unskip.strings.queueFull.description": "The last played song listing cannot be brought back because the song queue is already full.", - "music.options.unskip.strings.unskipped.title": "Unskipped", - "music.options.unskip.strings.unskipped.description": "The last played song listing has been brought back.", - "music.options.volume.name": "volume", - "music.options.volume.description": "Allows the user to manage the volume of music playback.", - "music.options.volume.options.display.name": "display", - "music.options.volume.options.display.description": "Displays the volume of playback.", - "music.options.volume.options.display.strings.volume.title": "Volume", - "music.options.volume.options.display.strings.volume.description": "The current volume is {volume}%.", - "music.options.volume.options.set.name": "set", - "music.options.volume.options.set.description": "Sets the volume of playback.", - "music.options.volume.options.set.options.volume.name": "volume", - "music.options.volume.options.set.options.volume.description": "The volume to set.", - "music.options.volume.options.set.strings.invalid.title": "Invalid volume", - "music.options.volume.options.set.strings.invalid.description": "Song volume may not be negative, and it may not be higher than {volume}%.", - "music.options.volume.options.set.strings.set.title": "Volume set", - "music.options.volume.options.set.strings.set.description": "The volume has been set to {volume}%.", - "music.strings.notPlaying.title": "Not playing music", - "music.strings.notPlaying.description.toManage": "To manage playback, first request some music.", - "music.strings.notPlaying.description.toCheck": "To check playback-related information, first request some music.", - "music.strings.notInVc.title": "Not in voice channel", - "music.strings.notInVc.description.toManage": "To manage music, you must be in a voice channel.", - "music.strings.notInVc.description.toCheck": "To check playback-related information, you must be in a voice channel.", - "music.strings.listings": "Listings", - "music.strings.listEmpty": "This list is empty.", - "music.strings.skips.tooManyArguments.title": "Too many skip arguments", - "music.strings.skips.tooManyArguments.description": "You may not skip __by__ a number of songs and skip __to__ a certain song in the same query.", - "music.strings.skips.invalid.title": "Invalid number", - "music.strings.skips.invalid.description": "The number you have entered is invalid.", - "music.strings.outage.halted.title": "Audio playback halted", - "music.strings.outage.halted.description.outage": "Logos is experiencing some momentary problems with audio connectivity.", - "music.strings.outage.halted.description.noLoss": "Don't worry: If you were listening, your session is still active, and it will be back up as soon as the connection is re-established.", - "music.strings.outage.cannotManage.title": "Audio playback is currently halted", - "music.strings.outage.cannotManage.description.outage": "Audio playback was halted due to a temporary audio interruption.", - "music.strings.outage.cannotManage.description.backUpSoon": "You will be able to manage music as soon as the audio connection is re-established.", - "music.strings.outage.restored.title": "Audio playback resumed", - "music.strings.outage.restored.description": "Audio connectivity has been restored.", - "suggestion.name": "suggestion", - "suggestion.description": "Allows the user to make a suggestion for the community.", - "suggestion.strings.sent.title": "Suggestion sent!", - "suggestion.strings.sent.description": "Your suggestion has been passed over to the server staff.", - "suggestion.strings.failed.title": "Failed to send suggestion", - "suggestion.strings.failed.description": "Due to unknown reasons, sending your suggestion failed.", - "suggestion.strings.sureToCancel.title": "Sure to exit?", - "suggestion.strings.sureToCancel.description": "Are you sure you want to stop submitting your suggestion?", - "suggestion.strings.tooMany.title": "Too many suggestions", - "suggestion.strings.tooMany.description": "You have already made a few suggestions recently. You should wait a little before making another suggestion.", - "resource.name": "resource", - "resource.description": "Allows the user to submit a resource.", - "resource.strings.sent.title": "Resource sent!", - "resource.strings.sent.description": "Your resource has been submitted.", - "resource.strings.failed.title": "Failed to send resource", - "resource.strings.failed.description": "Due to unknown reasons, submitting your resource failed.", - "resource.strings.sureToCancel.title": "Sure to exit?", - "resource.strings.sureToCancel.description": "Are you sure you want to stop submitting your resource?", - "resource.strings.tooMany.title": "Too many resource submissions", - "resource.strings.tooMany.description": "You have already submitted a few resources recently. You should wait a little before submitting another one.", - "ticket.name": "ticket", - "ticket.description": "Allows the user to manage tickets.", - "ticket.options.open.name": "open", - "ticket.options.open.description": "Opens a ticket.", - "ticket.strings.sent.title": "Ticket opened!", - "ticket.strings.sent.description": "A ticket has been opened.", - "ticket.strings.failed.title": "Failed to open ticket", - "ticket.strings.failed.description": "Due to unknown reasons, a ticket could not be opened.", - "ticket.strings.sureToCancel.title": "Sure to exit?", - "ticket.strings.sureToCancel.description": "Are you sure you no longer want to open a ticket?", - "ticket.strings.tooMany.title": "Too many tickets", - "ticket.strings.tooMany.description": "You have already opened a tickets recently. You should wait a little before opening another one.", - "praise.name": "praise", - "praise.description": "Praises a user for their contribution.", - "praise.options.comment.name": "comment", - "praise.options.comment.description": "A comment to attach to the praise.", - "praise.strings.praised.title": "User praised!", - "praise.strings.praised.description": "{user_mention} has just been notified that you have praised them.", - "praise.strings.cannotPraiseSelf.title": "Cannot praise self", - "praise.strings.cannotPraiseSelf.description": "You cannot praise yourself.", - "praise.strings.failed.title": "Failed to praise user", - "praise.strings.failed.description": "Due to unknown reasons, praising the specified user failed.", - "praise.strings.tooMany.title": "Too many praises", - "praise.strings.tooMany.description": "You have already praised a user recently. You must wait before praising somebody again.", - "profile.name": "profile", - "profile.description": "Allows the user to view information about themselves or another user.", - "profile.options.roles.name": "roles", - "profile.options.roles.description": "Opens the role selection menu.", - "profile.options.roles.strings.limitReached.title": "Role limit reached", - "profile.options.roles.strings.limitReached.description.limitReached": "You have reached the limit of roles you can assign in this category.", - "profile.options.roles.strings.limitReached.description.toChooseNew": "To choose a new role, unassign one of your existing roles.", - "profile.options.roles.strings.roleMandatory.title": "This role is mandatory", - "profile.options.roles.strings.roleMandatory.description": "You cannot unassign this role. You can only swap it for another role from the same category.", - "profile.options.roles.strings.chooseCategory": "Choose a category.", - "profile.options.roles.strings.chooseRole": "Choose a role.", - "profile.options.roles.strings.back": "Back", - "profile.options.roles.strings.assigned": "Assigned", - "profile.options.view.name": "view", - "profile.options.view.description": "Displays a user's profile.", - "profile.options.view.strings.failed.title": "Failed to show information", - "profile.options.view.strings.failed.description": "Due to unknown reasons, showing information about the specified user failed.", - "profile.options.view.strings.information.title": "Information about {username}", - "profile.options.view.strings.information.description.roles": "Roles", - "profile.options.view.strings.information.description.dates": "Dates", - "profile.options.view.strings.information.description.statistics": "Statistics", - "profile.options.view.strings.information.description.received": "Received", - "profile.options.view.strings.information.description.sent": "Sent", - "profile.options.view.strings.information.description.praises": "Praises", - "profile.options.view.strings.information.description.warnings": "Warnings", - "settings.name": "settings", - "settings.description": "Allows the user to manage their settings.", - "settings.options.view.name": "view", - "settings.options.view.description": "Displays the user's settings.", - "settings.options.language.name": "language", - "settings.options.language.description": "Allows the user to change their display language.", - "settings.options.language.options.clear.name": "clear", - "settings.options.language.options.clear.description": "Clears the display language, setting it back to the default one.", - "settings.options.language.options.set.name": "set", - "settings.options.language.options.set.description": "Sets the display language.", - "settings.options.language.options.set.options.language.name": "language", - "settings.options.language.options.set.options.language.description": "The language to set.", - "settings.strings.settings.title": "User Settings", - "settings.strings.settings.fields.language.title": "Display Language", - "settings.strings.settings.fields.language.description.noLanguageSet.noLanguageSet": "You have not set a display language.", - "settings.strings.settings.fields.language.description.noLanguageSet.defaultShown": "Logos will adaptively display messages based on your app language.", - "settings.strings.cannotClear.title": "No display language has been set", - "settings.strings.cannotClear.description": "You cannot reset your display language because you have not set one yet.", - "settings.strings.cleared.title": "Display language reset", - "settings.strings.cleared.description": "Logos will now show messages in the language you have set in the Discord app.", - "settings.strings.invalid.title": "Invalid language", - "settings.strings.invalid.description": "The language you specified is invalid.", - "settings.strings.alreadySet.title": "Already set as display language", - "settings.strings.alreadySet.description": "You already have {language} set as your display language.", - "settings.strings.languageUpdated.title": "Updated display language", - "settings.strings.languageUpdated.description": "Your display language has been set to {language}.", - "licence.name": "licences", - "licence.description": "Allows the user to view notices for data and software used by the Logos project.", - "licence.options.dictionary.name": "dictionary", - "licence.options.dictionary.description": "View licence notices for dictionaries used by Logos.", - "licence.options.dictionary.options.dictionary.name": "dictionary", - "licence.options.dictionary.options.dictionary.description": "The dictionary to view the licence for.", - "licence.options.software.name": "software", - "licence.options.software.description": "View licence notices for software used by Logos.", - "licence.options.software.options.package.name": "package", - "licence.options.software.options.package.description": "The package to view the licence for.", - "licence.strings.invalid.title": "Invalid licence", - "licence.strings.invalid.description": "The licence you tried to view is invalid.", - "licence.strings.license": "Licence – {entity}", - "licence.strings.source": "Source", - "licence.strings.copyright": "Copyright Notice", - "credits.name": "credits", - "credits.description": "Shows the credits menu.", - "credits.strings.translation": "Translation", - "acknowledgements.name": "acknowledgements", - "acknowledgements.description": "Shows the acknowledgements menu.", - "acknowledgements.strings.acknowledgements": "Acknowledgements" -} \ No newline at end of file + "noDescription": "No description.", + "information.name": "information", + "information.description": "Used to display various information.", + "information.options.bot.name": "bot", + "information.options.bot.description": "Displays information about the bot.", + "information.options.bot.strings.concept.title": "What is Logos?", + "information.options.bot.strings.concept.description": "Logos is the go-to Discord bot for language communities. Continuously growing, the bot now serves 8 language communities and over 30,000 users.", + "information.options.bot.strings.function.title": "What does Logos do?", + "information.options.bot.strings.function.description": "Logos aids learners by providing them with useful learning tools, some of which include:", + "information.options.bot.strings.function.features.definitions": "Searching for information about words, including definitions, translations, pronunciation, history, relations, usage notes, and more.", + "information.options.bot.strings.function.features.translations": "Recognition of almost 200 languages, translation in over 140 languages.", + "information.options.bot.strings.function.features.games": "Language games such as \"Pick the correct word\", similar to Clozemaster or Memrise.", + "information.options.bot.strings.function.features.messages": "The ability to propose corrections and submit answers to messages.", + "information.options.bot.strings.function.features.guides": "Guides to linguistic concepts such to the Common European Framework of Reference for Languages (CEFR).", + "information.options.bot.strings.languages.title": "What languages does Logos support?", + "information.options.bot.strings.languages.description": "On top of the plethora of features Logos provides, thanks to a team of amazing volunteers, the bot also has excellent localisation support for 🇫🇷 French, 🇩🇪 German, 🇵🇱 Polish, 🇹🇷 Turkish, 🇳🇴 Norwegian, and 🇷🇴 Romanian, with more localisations on the way for 🇫🇮 Finnish, 🇸🇪 Swedish, 🇭🇺 Hungarian, 🇦🇲 Armenian (Western + Eastern) and 🇳🇱 Dutch.", + "information.options.server.name": "server", + "information.options.server.description": "Displays information about the server.", + "information.options.server.strings.information.title": "Information about {server_name}", + "information.options.server.strings.information.description.description": "Description", + "information.options.server.strings.information.description.noDescription": "No description specified.", + "information.options.server.strings.information.description.members": "Members", + "information.options.server.strings.information.description.created": "Created", + "information.options.server.strings.information.description.channels": "Channels", + "information.options.server.strings.information.description.languages": "Languages", + "information.options.server.strings.information.description.owner": "Owner", + "information.options.server.strings.information.description.moderators": "Moderators", + "information.options.server.strings.information.description.overseenByModerators": "This server is overseen by a collective of guides.", + "information.options.server.strings.information.description.distribution": "Distribution of members' language proficiency", + "information.options.server.strings.information.description.withoutProficiency": "without a specified language proficiency.", + "information.options.server.strings.channelTypes.text": "Text", + "information.options.server.strings.channelTypes.voice": "Voice", + "information.options.server.strings.languageTypes.home": "Home", + "information.options.server.strings.languageTypes.target": "Target", + "answer.message.name": "Answer", + "answer.message.description": "Allows the user to submit an answer to a question.", + "answer.strings.cannotAnswer.title": "Cannot answer message", + "answer.strings.cannotAnswer.description": "That message cannot be answered.", + "answer.strings.cannotAnswerOwn.title": "Cannot answer own message", + "answer.strings.cannotAnswerOwn.description": "You cannot submit an answer for your own message.", + "answer.strings.sureToCancel.title": "Sure to exit?", + "answer.strings.sureToCancel.description": "Are you sure you want to stop submitting your answer?", + "answer.strings.submittedBy": "Answer submitted by {username}.", + "cefr.name": "cefr", + "cefr.description": "Displays a guide for the CEFR (Common European Framework of Reference for Languages).", + "cefr.strings.brackets.a": "Bracket A", + "cefr.strings.brackets.b": "Bracket B", + "cefr.strings.brackets.c": "Bracket C", + "cefr.strings.tabs.guide": "Guide", + "cefr.strings.tabs.examples": "Examples", + "cefr.strings.levels.a0.title": "A0 (Starter)", + "cefr.strings.levels.a0.description": "I have just started, I have very limited command of the language.", + "cefr.strings.levels.a1.title": "A1 (Elementary)", + "cefr.strings.levels.a1.description": "I've been learning for a short while, I understand the language as spoken in day-to-day contexts and if the language is spoken slowly and clearly.", + "cefr.strings.levels.a2.title": "A2 (Pre-intermediate)", + "cefr.strings.levels.a2.description": "I started a longer while ago, I can communicate in a range of day-to-day situations, and on topics I have studied vocabulary in previously.", + "cefr.strings.levels.b1.title": "B1 (Intermediate)", + "cefr.strings.levels.b1.description": "I've been studying for a longer while, and, although I'm unable to speak fluently, I have some confidence in my command of the language. I'm starting to find alternate ways to express myself when I don't know certain vocabulary.", + "cefr.strings.levels.b2.title": "B2 (Upper Intermediate)", + "cefr.strings.levels.b2.description": "I have been learning for quite a long time. I'm able to use the language effectively and I speak with good confidence. My inadequacies in vocabulary are becoming near indistinguishable as I quickly find ways to express myself using vocabulary I already know.", + "cefr.strings.levels.c1.title": "C1 (Advanced)", + "cefr.strings.levels.c1.description": "I've been learning for a very long time. Not only am I able to express myself accurately and with confidence, I can use language in a situational, culturally-aware way. Sometimes, I still come across vocabulary, phrasing or features of the language I didn't know.", + "cefr.strings.levels.c2.title": "C2 (Mastery)", + "cefr.strings.levels.c2.description": "I have been studying the language for years now. I speak the language with ease, never finding myself unable to talk about what I want in detail and with nuance. When needed, I can be creative with my language, recognising the different ways to use it. I have very good grasp of what sounds good and what doesn't, and I can easily pick up on mistakes, providing alternatives, and even correcting myself.", + "cefr.strings.levels.c3.title": "C2+/C3 (Expert)", + "cefr.strings.levels.c3.description": "On top of the command over language a C2 user would be expected to display, I have extensive knowledge of creative, poetic, technical or unusual uses of vocabulary generally expected of people with a very high command of language.", + "correction.options.partial.message.name": "Quick Correction", + "correction.options.partial.message.description": "Allows the user to submit a quick correction to a message.", + "correction.options.full.message.name": "Full-text Correction", + "correction.options.full.message.description": "Allows the user to submit a thorough correction to a message.", + "correction.strings.textsNotDifferent.title": "Original and corrected versions are the same", + "correction.strings.textsNotDifferent.description": "The corrected version may not be the same as the original.", + "correction.strings.tooLong.title": "Message too long", + "correction.strings.tooLong.description.tooLong": "The selected message is too long to be corrected.", + "correction.strings.tooLong.description.maximumLength": "The number of characters in a message cannot exceed {character_limit}.", + "correction.strings.cannotCorrect.title": "Cannot correct message", + "correction.strings.cannotCorrect.description": "That message cannot be corrected.", + "correction.strings.cannotCorrectOwn.title": "Cannot correct own message", + "correction.strings.cannotCorrectOwn.description": "You cannot submit a correction for your own message.", + "correction.strings.userDoesNotWantCorrections.title": "User does not want corrections", + "correction.strings.userDoesNotWantCorrections.description": "This user has specifically stated that they do not wish to be corrected.", + "correction.strings.failed.title": "Unable to correct", + "correction.strings.failed.description": "Failed to correct the given message.", + "correction.strings.sureToCancel.title": "Sure to exit?", + "correction.strings.sureToCancel.description": "Are you sure you want to stop making your correction?", + "correction.strings.suggestedBy": "Correction suggested by {username}.", + "recognise.name": "recognise", + "recognise.description": "Given a text, recognises its source language, or mentions likely candidates.", + "recognise.options.text.name": "text", + "recognise.options.text.description": "The text to use for language recognition.", + "recognise.message.name": "Recognise Language", + "recognise.message.description": "Given a message, recognises its source language, or mentions likely candidates.", + "recognise.strings.cannotUse.title": "Cannot use message", + "recognise.strings.cannotUse.description": "That message cannot be used for language recognition.", + "recognise.strings.unknown.title": "Unknown language", + "recognise.strings.unknown.description.text": "Could not recognise the language of the given text.", + "recognise.strings.unknown.description.message": "Could not recognise the language of the given message.", + "recognise.strings.textEmpty.title": "Text empty", + "recognise.strings.textEmpty.description": "The text must not be empty or consist solely of whitespace.", + "recognise.strings.fields.likelyMatches.title": "Likely matches", + "recognise.strings.fields.likelyMatches.description.single": "The language is likely to be {language}.", + "recognise.strings.fields.likelyMatches.description.multiple": "The language is likely to be one of:", + "recognise.strings.fields.possibleMatches.title": "Other options", + "recognise.strings.fields.possibleMatches.description.single": "The language is less likely to be {language}.", + "recognise.strings.fields.possibleMatches.description.multiple": "These choices are less likely, but the language could also be one of:", + "game.name": "game", + "game.description": "Pick the correct word out of four to fit in the blank.", + "game.strings.sentence": "Sentence", + "game.strings.translation": "Translation", + "game.strings.noSentencesAvailable.title": "No sentences available", + "game.strings.noSentencesAvailable.description": "There are no sentences available in the requested language.", + "game.strings.skip": "Skip", + "game.strings.sourcedFrom": "Sentences sourced from {source}.", + "game.strings.correctGuesses": "Correct guesses: {number}", + "game.strings.allTime": "All time: {number}", + "game.strings.next": "Next", + "resources.name": "resources", + "resources.description": "Redirects to the repository of resources to learn the language.", + "resources.strings.redirect": "Click for resources to learn {language}", + "translate.name": "translate", + "translate.description": "Translates a text from the source language to the target language.", + "translate.options.text.name": "text", + "translate.options.text.description": "The text to translate.", + "translate.options.from.name": "from", + "translate.options.from.description": "The source language.", + "translate.options.to.name": "to", + "translate.options.to.description": "The target language.", + "translate.message.name": "Translate", + "translate.message.description": "Translates the contents of a message from the source language to the target language.", + "translate.strings.cannotUse.title": "Cannot translate message", + "translate.strings.cannotUse.description": "That message cannot be translated.", + "translate.strings.textEmpty.title": "Text empty", + "translate.strings.textEmpty.description": "The source text may not be empty.", + "translate.strings.invalid.source.title": "Invalid source language", + "translate.strings.invalid.source.description": "The source language is invalid.", + "translate.strings.invalid.target.title": "Invalid target language", + "translate.strings.invalid.target.description": "The target language is invalid.", + "translate.strings.invalid.both.title": "Invalid languages", + "translate.strings.invalid.both.description": "Both the source and target languages are invalid.", + "translate.strings.languagesNotDifferent.title": "Source and target languages are the same", + "translate.strings.languagesNotDifferent.description": "The target language may not be the same as the source language.", + "translate.strings.cannotDetermine.source.title": "Cannot determine source language", + "translate.strings.cannotDetermine.source.description.cannotDetermine": "The source language could not be determined.", + "translate.strings.cannotDetermine.source.description.tryAgain": "Please set it yourself, or try again with a different input.", + "translate.strings.cannotDetermine.target.title": "Cannot determine target language", + "translate.strings.cannotDetermine.target.description.cannotDetermine": "The target language could not be determined.", + "translate.strings.cannotDetermine.target.description.tryAgain": "Please set it manually to the language you'd like to translate to.", + "translate.strings.languageNotSupported.title": "Language not supported", + "translate.strings.languageNotSupported.description": "The {language} language is not supported.", + "translate.strings.failed.title": "Unable to translate text", + "translate.strings.failed.description": "Failed to translate the given text.", + "translate.strings.noTranslationAdapters.title": "Language pair not supported", + "translate.strings.noTranslationAdapters.description": "The selected language pair is not supported.", + "translate.strings.sourceText": "Source Text", + "translate.strings.translation": "Translation", + "translate.strings.sourcedFrom": "Translation sourced from {source}.", + "word.name": "word", + "word.description": "Displays information about a given word.", + "word.options.word.name": "word", + "word.options.word.description": "The word to display information about.", + "word.options.language.name": "language", + "word.options.language.description": "The language of the word.", + "word.options.verbose.name": "verbose", + "word.options.verbose.description": "If set to true, more (possibly unnecessary) information will be shown.", + "word.strings.invalid.language.title": "Invalid language", + "word.strings.invalid.language.description": "The language you specified is invalid.", + "word.strings.noDictionaryAdapters.title": "No dictionaries available for language", + "word.strings.noDictionaryAdapters.description": "There are no dictionaries available in the requested language.", + "word.strings.noResults.title": "No results", + "word.strings.noResults.description": "There are no results for the word '{word}'.", + "word.strings.fields.translations": "Translations", + "word.strings.fields.pronunciation": "Pronunciation", + "word.strings.fields.nativeDefinitions": "Native definitions", + "word.strings.fields.definitions": "Definitions", + "word.strings.fields.etymology": "Etymology", + "word.strings.fields.expressions": "Expressions", + "word.strings.relations.synonyms": "Synonyms", + "word.strings.relations.antonyms": "Antonyms", + "word.strings.relations.diminutives": "Diminutives", + "word.strings.relations.augmentatives": "Augmentatives", + "word.strings.definitionsOmitted": "Omitted {definitions}. To display more results, enable the `{flag}` flag.", + "word.strings.definitionsOmitted.definitions.one": "{one} definition", + "word.strings.definitionsOmitted.definitions.two": "{two} definitions", + "word.strings.definitionsOmitted.definitions.many": "{many} definitions", + "word.strings.page": "Page", + "word.strings.definitions": "Definitions", + "word.strings.nativeDefinitionsForWord": "Native definitions for '{word}'", + "word.strings.definitionsForWord": "Definitions for '{word}'", + "word.strings.inflection": "Inflection", + "word.strings.verbs.moodsAndParticiples": "Moods and participles", + "word.strings.verbs.moods.conditional": "Conditional", + "word.strings.verbs.moods.imperative": "Imperative", + "word.strings.verbs.moods.indicative": "Indicative", + "word.strings.verbs.moods.infinitive": "Infinitive", + "word.strings.verbs.moods.longInfinitive": "Long infinitive", + "word.strings.verbs.moods.optative": "Optative", + "word.strings.verbs.moods.presumptive": "Presumptive", + "word.strings.verbs.moods.subjunctive": "Subjunctive", + "word.strings.verbs.moods.supine": "Supine", + "word.strings.verbs.participles.present": "Present participle", + "word.strings.verbs.participles.past": "Past participle", + "word.strings.verbs.popular": "popular", + "word.strings.verbs.tenses.tenses": "Tenses", + "word.strings.verbs.tenses.present": "Present", + "word.strings.verbs.tenses.presentContinuous": "Present continuous", + "word.strings.verbs.tenses.imperfect": "Imperfect", + "word.strings.verbs.tenses.preterite": "Preterite", + "word.strings.verbs.tenses.pluperfect": "Pluperfect", + "word.strings.verbs.tenses.perfect": "Perfect", + "word.strings.verbs.tenses.compoundPerfect": "Compound perfect", + "word.strings.verbs.tenses.future": "Future", + "word.strings.verbs.tenses.futureCertain": "Certain future", + "word.strings.verbs.tenses.futurePlanned": "Planned future", + "word.strings.verbs.tenses.futureDecided": "Decided future", + "word.strings.verbs.tenses.futureIntended": "Intended future", + "word.strings.verbs.tenses.futureInThePast": "Future-in-the-past", + "word.strings.verbs.tenses.futurePerfect": "Future perfect", + "word.strings.nouns.cases.cases": "Cases", + "word.strings.nouns.cases.nominativeAccusative": "Nominative-accusative", + "word.strings.nouns.cases.genitiveDative": "Genitive-dative", + "word.strings.nouns.cases.vocative": "Vocative", + "word.strings.nouns.singular": "Singular", + "word.strings.nouns.plural": "Plural", + "word.strings.sourcedResponsibly": "Information sourced with gratitude from {dictionaries}.", + "word.strings.sourcedResponsibly.dictionaries.one": "the dictionary above", + "word.strings.sourcedResponsibly.dictionaries.two": "the dictionaries above", + "word.strings.sourcedResponsibly.dictionaries.many": "the dictionaries above", + "list.name": "list", + "list.description": "Allows the viewing of various information about users.", + "list.options.warnings.name": "warnings", + "list.options.warnings.description": "Lists the warnings issued to a user.", + "list.options.warnings.strings.failed.title": "Unable to show warnings", + "list.options.warnings.strings.failed.description": "Failed to show warnings for the given user.", + "list.options.warnings.strings.noActiveWarnings.title": "No active warnings", + "list.options.warnings.strings.noActiveWarnings.description.self": "You have no active warnings.", + "list.options.warnings.strings.noActiveWarnings.description.other": "This user does not have any active warnings.", + "list.options.warnings.strings.warnings.title": "Warnings", + "list.options.warnings.strings.warnings.description.warning": "Warning #{index}, given {relative_timestamp}", + "list.options.praises.name": "praises", + "list.options.praises.description": "Allows the viewing of user praises.", + "list.options.praises.options.author.name": "author", + "list.options.praises.options.author.description": "Lists the praises given out by a user.", + "list.options.praises.options.target.name": "target", + "list.options.praises.options.target.description": "Lists the praises given out to a user.", + "list.options.praises.strings.failed.title": "Unable to show praises", + "list.options.praises.strings.failed.description": "Failed to show praises for the given user.", + "list.options.praises.strings.noPraises.title": "No praises", + "list.options.praises.strings.noPraises.description.self.author": "You have not given out any praises.", + "list.options.praises.strings.noPraises.description.self.target": "You have not been given any praises.", + "list.options.praises.strings.noPraises.description.other.author": "This user has not given out any praises.", + "list.options.praises.strings.noPraises.description.other.target": "This user has not been given any praises.", + "list.options.praises.strings.praises.title": "Praises", + "list.options.praises.strings.praises.noComment": "No comment attached.", + "pardon.name": "pardon", + "pardon.description": "Removes one of the warnings previously given to a user.", + "pardon.options.warning.name": "warning", + "pardon.options.warning.description": "The warning to remove.", + "pardon.strings.failed.title": "Failed to remove warning", + "pardon.strings.failed.description": "Due to unknown reasons, removing the specified warning failed.", + "pardon.strings.invalidWarning.title": "Invalid warning", + "pardon.strings.invalidWarning.description": "The warning you specified is invalid.", + "pardon.strings.pardoned.title": "User pardoned", + "pardon.strings.pardoned.description": "User {user_mention} has been pardoned from their warning for: {reason}", + "policy.name": "policy", + "policy.description": "Displays the server moderation policy.", + "purge.name": "purge", + "purge.description": "Deletes a number of messages from a channel.", + "purge.options.start.name": "start", + "purge.options.start.description": "ID of the message at the beginning of the range.", + "purge.options.end.name": "end", + "purge.options.end.description": "ID of the message at the end of the range.", + "purge.options.author.name": "author", + "purge.options.author.description": "The user whose messages to delete.", + "purge.strings.invalid.start.title": "Invalid start message ID", + "purge.strings.invalid.start.description": "The start message ID you provided is invalid.", + "purge.strings.invalid.end.title": "Invalid end message ID", + "purge.strings.invalid.end.description": "The end message ID you provided is invalid.", + "purge.strings.invalid.both.title": "Invalid IDs", + "purge.strings.invalid.both.description": "The start and end message IDs you provided are invalid.", + "purge.strings.idsNotDifferent.title": "Start and end messages are the same", + "purge.strings.idsNotDifferent.description": "The end message may not be the same as the start message.", + "purge.strings.failed.title": "Failed to purge", + "purge.strings.failed.description": "Due to unknown reasons, purging messages from this channel has failed.", + "purge.strings.indexing.title": "Indexing messages...", + "purge.strings.indexing.description": "The bot is indexing messages falling within the range you specified.", + "purge.strings.indexed.title": "Finished indexing", + "purge.strings.indexed.description.some": "The bot has indexed messages falling within the range you specified, and found **{messages}**.", + "purge.strings.indexed.description.some.messages.one": "{one} deletable message", + "purge.strings.indexed.description.some.messages.two": "{two} deletable messages", + "purge.strings.indexed.description.some.messages.many": "{many} deletable messages", + "purge.strings.indexed.description.none": "The bot has indexed messages falling within the range you specified, and found no deletable messages.", + "purge.strings.indexed.description.tryDifferentQuery": "Try a different query.", + "purge.strings.indexed.description.tooMany": "The bot has indexed messages falling within the range you specified, and found **{messages}**, which is greater than the **maximum number of messages deletable in a given purge action ({maximum_deletable})**.", + "purge.strings.indexed.description.tooMany.messages.one": "{one} deletable message", + "purge.strings.indexed.description.tooMany.messages.two": "{two} deletable messages", + "purge.strings.indexed.description.tooMany.messages.many": "{many} deletable messages", + "purge.strings.indexed.description.limited": "If you choose to continue, **{messages}** will be deleted instead.", + "purge.strings.indexed.description.limited.messages.one": "{one} message", + "purge.strings.indexed.description.limited.messages.two": "{two} messages", + "purge.strings.indexed.description.limited.messages.many": "{many} messages", + "purge.strings.sureToPurge.title": "Sure to purge?", + "purge.strings.sureToPurge.description": "During purging, **{messages}** will be irrevocably deleted from {channel_mention}.", + "purge.strings.sureToPurge.description.messages.one": "{one} message", + "purge.strings.sureToPurge.description.messages.two": "{two} messages", + "purge.strings.sureToPurge.description.messages.many": "{many} messages", + "purge.strings.continue.title": "Continue?", + "purge.strings.continue.description": "During purging, **{messages}** will be irrevocably deleted from {channel_mention}, which is only a fraction of the {all_messages} found.", + "purge.strings.continue.description.messages.one": "{one} message", + "purge.strings.continue.description.messages.two": "{two} messages", + "purge.strings.continue.description.messages.many": "{many} messages", + "purge.strings.continue.description.allMessages.one": "{one} message", + "purge.strings.continue.description.allMessages.two": "{two} messages", + "purge.strings.continue.description.allMessages.many": "{many} messages", + "purge.strings.purging.title": "Purging...", + "purge.strings.purging.description.purging": "{messages} from {channel_mention}.", + "purge.strings.purging.description.purging.messages.one": "{one} message is being purged", + "purge.strings.purging.description.purging.messages.two": "{two} messages are being purged", + "purge.strings.purging.description.purging.messages.many": "{many} messages are being purged", + "purge.strings.purging.description.mayTakeTime": "This may take a while.", + "purge.strings.purging.description.onceComplete": "Once complete, you will be able to find a success message in the log channel.", + "purge.strings.purged.title": "Purged successfully", + "purge.strings.purged.description": "{messages} from {channel_mention}.", + "purge.strings.purged.description.messages.one": "{one} message has been purged", + "purge.strings.purged.description.messages.two": "{two} messages have been purged", + "purge.strings.purged.description.messages.many": "{many} messages have been purged", + "purge.strings.rangeTooBig.title": "Range too big", + "purge.strings.rangeTooBig.description.rangeTooBig": "While indexing the selected range of messages, the bot found more than **{messages}**, which is too big of a range.", + "purge.strings.rangeTooBig.description.rangeTooBig.messages.one": "{one} message", + "purge.strings.rangeTooBig.description.rangeTooBig.messages.two": "{two} messages", + "purge.strings.rangeTooBig.description.rangeTooBig.messages.many": "{many} messages", + "purge.strings.rangeTooBig.description.trySmaller": "Try selecting a smaller range of messages to purge.", + "purge.strings.start": "Start message", + "purge.strings.end": "End message", + "purge.strings.noContent": "No content", + "purge.strings.embedPosted": "Embed posted {relative_timestamp} by {user_mention}", + "purge.strings.posted": "Posted {relative_timestamp} by {user_mention}", + "purge.strings.messagesFound": "Messages found", + "purge.strings.yes": "Yes", + "purge.strings.no": "No", + "report.name": "report", + "report.description": "Allows the user to create a user report.", + "report.strings.submitted.title": "Report submitted!", + "report.strings.submitted.description": "Your report has been submitted. The report will be reviewed by the server staff, but you will not be notified directly about the outcome of a particular report.", + "report.strings.failed.title": "Failed to submit report", + "report.strings.failed.description": "Due to unknown reasons, submitting the report failed.", + "report.strings.sureToCancel.title": "Sure to exit?", + "report.strings.sureToCancel.description": "Are you sure you want to stop submitting your report?", + "report.strings.tooMany.title": "Too many reports", + "report.strings.tooMany.description": "You have already made a few reports recently. Before submitting another report, you should wait some time.", + "rule.name": "rule", + "rule.description": "Cites a server rule.", + "rule.options.rule.name": "rule", + "rule.options.rule.description": "The rule to cite.", + "rule.strings.invalid.title": "Invalid rule", + "rule.strings.invalid.description": "The rule you specified is invalid.", + "slowmode.name": "slowmode", + "slowmode.description": "Toggles slowmode for the channel this command is run in.", + "slowmode.options.level.name": "level", + "slowmode.options.level.description": "The level of slowmode to apply.", + "slowmode.strings.levels.lowest": "No biggie. (Lowest, 5 seconds)", + "slowmode.strings.levels.low": "It's minor. (Low, 10 seconds)", + "slowmode.strings.levels.medium": "It's getting problematic. (Medium, 30 seconds)", + "slowmode.strings.levels.high": "It's major. (High, 1 minute).", + "slowmode.strings.levels.highest": "Sh*t just hit the fan. (Highest, 5 minutes)", + "slowmode.strings.levels.emergency": "It's an emergency. (Emergency, 20 minutes)", + "slowmode.strings.levels.lockdown": "We need to lock the place down. (Lockdown, 1 hour)", + "slowmode.strings.levels.beyond": "It's reached the next level. (Beyond, 1 day)", + "slowmode.strings.tooSoon.title": "Cannot disable too soon", + "slowmode.strings.tooSoon.description.justEnabled": "Slowmode has been enabled just a moment ago.", + "slowmode.strings.tooSoon.description.canDisableIn": "You will be able to disable it {relative_timestamp}.", + "slowmode.strings.invalid.title": "Invalid level", + "slowmode.strings.invalid.description": "The level you specified is invalid.", + "slowmode.strings.enabled.title": "Slowmode active", + "slowmode.strings.enabled.description": "Slowmode has been enabled for this channel.", + "slowmode.strings.disabled.title": "Slowmode no longer active", + "slowmode.strings.disabled.description": "Slowmode has been disabled for this channel.", + "slowmode.strings.upgraded.title": "Slowmode level upgraded", + "slowmode.strings.upgraded.description": "The slowmode level has been upgraded to a higher severity.", + "slowmode.strings.downgraded.title": "Slowmode level downgraded", + "slowmode.strings.downgraded.description": "The slowmode level has been downgraded to a lesser severity.", + "slowmode.strings.theSame.title": "Slowmode level is the same", + "slowmode.strings.theSame.description.theSame": "The current slowmode level is the same as the requested one.", + "slowmode.strings.theSame.description.chooseDifferent": "Choose a different level.", + "timeout.name": "timeout", + "timeout.description": "Used to manage user timeouts.", + "timeout.options.set.name": "set", + "timeout.options.set.description": "Times a user out, making them unable to interact on the server.", + "timeout.options.clear.name": "clear", + "timeout.options.clear.description": "Clears a user's timeout.", + "timeout.strings.durationInvalid.title": "Invalid duration", + "timeout.strings.durationInvalid.description": "The duration you specified is invalid.", + "timeout.strings.tooShort.title": "Timeout too short", + "timeout.strings.tooShort.description": "The timeout must last at least a minute.", + "timeout.strings.tooLong.title": "Timeout too long", + "timeout.strings.tooLong.description": "The duration must not be longer than a week.", + "timeout.strings.timedOut.title": "User timed out", + "timeout.strings.timedOut.description": "User {user_mention} has been timed out. The timeout will expire {relative_timestamp}.", + "timeout.strings.notTimedOut.title": "User not timed out", + "timeout.strings.notTimedOut.description": "User {user_mention} is not currently timed out.", + "timeout.strings.timeoutCleared.title": "Timeout cleared", + "timeout.strings.timeoutCleared.description": "User {user_mention} is no longer timed out.", + "warn.name": "warn", + "warn.description": "Warns a user.", + "warn.options.rule.name": "rule", + "warn.options.rule.description": "The rule to cite as having been broken.", + "warn.options.rule.strings.other": "Other (justified in reason)", + "warn.strings.invalidRule.title": "Invalid rule", + "warn.strings.invalidRule.description": "The rule you specified is invalid.", + "warn.strings.failed.title": "Failed to warn user", + "warn.strings.failed.description": "Due to unknown reasons, warning the specified user failed.", + "warn.strings.warned.title": "User warned", + "warn.strings.warned.description": "User {user_mention} has been warned. They now have {warnings}.", + "warn.strings.warned.description.warnings.one": "{one} warning", + "warn.strings.warned.description.warnings.two": "{two} warnings", + "warn.strings.warned.description.warnings.many": "{many} warnings", + "warn.strings.limitReached.title": "Warning limit reached", + "warn.strings.limitReached.description": "{user_mention} has reached the warning limit ({limit}).", + "warn.strings.limitSurpassed.title": "Warning limit surpassed", + "warn.strings.limitSurpassed.description": "{user_mention} has *surpassed* the warning limit ({limit}), having just received warning no. {number}.", + "warn.strings.limitSurpassedTimedOut.title": "Warning limit surpassed", + "warn.strings.limitSurpassedTimedOut.description": "{user_mention} has *surpassed* the warning limit ({limit}), having just received warning no. {number}, and has subsequently been timed out for {period}.", + "music.name": "music", + "music.description": "Allows the user to manage music playback in a voice channel.", + "music.options.history.name": "history", + "music.options.history.description": "Displays a list of previously played songs.", + "music.options.history.strings.playbackHistory": "Playback history", + "music.options.loop.name": "loop", + "music.options.loop.description": "Loops the currently playing song.", + "music.options.loop.strings.noSong.title": "Not playing song", + "music.options.loop.strings.noSong.description": "There is no song to loop.", + "music.options.loop.strings.noSongCollection.title": "Not playing song collection", + "music.options.loop.strings.noSongCollection.description.noSongCollection": "There is no song collection to loop.", + "music.options.loop.strings.noSongCollection.description.trySongInstead": "Try looping the current song instead.", + "music.options.loop.strings.enabled.title": "Loop enabled", + "music.options.loop.strings.enabled.description.song": "The current song will be looped.", + "music.options.loop.strings.enabled.description.songCollection": "The current song collection will be looped.", + "music.options.loop.strings.disabled.title": "Loop disabled", + "music.options.loop.strings.disabled.description.song": "The current song will no longer be looped.", + "music.options.loop.strings.disabled.description.songCollection": "The current song collection will no longer be looped.", + "music.options.now.name": "now", + "music.options.now.description": "Displays the currently playing song.", + "music.options.now.strings.noSong.title": "Not playing song", + "music.options.now.strings.noSong.description": "There is no song to show information about.", + "music.options.now.strings.noSongCollection.title": "Not playing song collection", + "music.options.now.strings.noSongCollection.description.noSongCollection": "There is no song collection to show information about.", + "music.options.now.strings.noSongCollection.description.trySongInstead": "Try requesting information about the current song instead.", + "music.options.now.strings.nowPlaying": "Now playing", + "music.options.now.strings.songs": "Songs", + "music.options.now.strings.collection": "Collection", + "music.options.now.strings.track": "Track", + "music.options.now.strings.title": "Title", + "music.options.now.strings.requestedBy": "Requested by", + "music.options.now.strings.runningTime": "Running time", + "music.options.now.strings.playingSince": "Since {relative_timestamp}.", + "music.options.now.strings.sourcedFrom": "This listing was sourced from {source}.", + "music.options.now.strings.theInternet": "the internet", + "music.options.pause.name": "pause", + "music.options.pause.description": "Pauses the currently playing song or song collection.", + "music.options.pause.strings.paused.title": "Paused", + "music.options.pause.strings.paused.description": "Paused the playback of music.", + "music.options.play.name": "play", + "music.options.play.description": "Allows the user to play music in a voice channel.", + "music.options.play.options.stream.name": "stream", + "music.options.play.options.stream.description": "Plays an audio stream.", + "music.options.play.options.stream.options.url.name": "url", + "music.options.play.options.stream.options.url.description": "Link to the audio stream.", + "music.options.play.options.youtube.name": "youtube", + "music.options.play.options.youtube.description": "Plays a song from YouTube.", + "music.options.play.strings.selectSong.title": "Select an entry", + "music.options.play.strings.selectSong.description": "Select a song or song collection from the choices below.", + "music.options.play.strings.stream": "Audio stream", + "music.options.play.strings.notFound.title": "Song not found", + "music.options.play.strings.notFound.description.notFound": "Couldn't find the requested song.", + "music.options.play.strings.notFound.description.tryDifferentQuery": "You could try an alternative search, or request a different song.", + "music.options.play.strings.inDifferentVc.title": "Bot is in another channel", + "music.options.play.strings.inDifferentVc.description": "The bot is already playing music in another voice channel.", + "music.options.play.strings.queueFull.title": "The queue is full", + "music.options.play.strings.queueFull.description": "The queue is full. Try removing a song from the song queue, skip the current song to advance the queue immediately, or wait until the current song finishes playing.", + "music.options.play.strings.queued.title": "Listing queued", + "music.options.play.strings.queued.description.private": "Your listing, **{title}**, has been added to the queue.", + "music.options.play.strings.queued.description.public": "**{title}** added to the queue as requested by {user_mention}.", + "music.options.play.strings.failedToLoad.title": "Failed to load track", + "music.options.play.strings.failedToLoad.description": "The track, **{title}**, could not be loaded.", + "music.options.play.strings.failedToPlay.title": "Failed to play track", + "music.options.play.strings.failedToPlay.description": "The track, **{title}**, could not be played.", + "music.options.play.strings.nowPlaying.title.nowPlaying": "Now playing {listing_type}", + "music.options.play.strings.nowPlaying.title.type.song": "song", + "music.options.play.strings.nowPlaying.title.type.songCollection": "song collection", + "music.options.play.strings.nowPlaying.title.type.stream": "audio stream", + "music.options.play.strings.nowPlaying.description.nowPlaying": "Now playing {song_information} [**{title}**]({url}) as requested by {user_mention}.", + "music.options.play.strings.nowPlaying.description.track": "track **{index}/{number}** of **{title}**: ", + "music.options.queue.name": "queue", + "music.options.queue.description": "Displays a list of queued song listings.", + "music.options.queue.strings.queue": "Queue", + "music.options.remove.name": "remove", + "music.options.remove.description": "Removes a song listing from the queue.", + "music.options.remove.strings.queueEmpty.title": "The queue is empty", + "music.options.remove.strings.queueEmpty.description": "There are no songs in the queue.", + "music.options.remove.strings.selectSong.title": "Select an entry", + "music.options.remove.strings.selectSong.description": "Select a song or song collection to remove from the choices below.", + "music.options.remove.strings.failed.title": "Failed to remove song", + "music.options.remove.strings.failed.description": "Due to unknown reasons, removing the selected song from the queue failed.", + "music.options.remove.strings.removed.title": "Removed", + "music.options.remove.strings.removed.description": "The song **{title}** has been removed by {user_mention}.", + "music.options.replay.name": "replay", + "music.options.replay.description": "Begins playing the currently playing song from the start.", + "music.options.replay.strings.noSong.title": "No song to replay", + "music.options.replay.strings.noSong.description": "There is no song to replay.", + "music.options.replay.strings.noSongCollection.title": "No song collection to replay", + "music.options.replay.strings.noSongCollection.description.noSongCollection": "There is no song collection to replay.", + "music.options.replay.strings.noSongCollection.description.trySongInstead": "Try replaying the current song instead.", + "music.options.replay.strings.replaying.title": "Replaying", + "music.options.replay.strings.replaying.description": "The current song will be played again.", + "music.options.resume.name": "resume", + "music.options.resume.description": "Unpauses the currently playing song if it is paused.", + "music.options.resume.strings.noSong.title": "No song to resume", + "music.options.resume.strings.noSong.description": "There is no song to resume playing.", + "music.options.resume.strings.notPaused.title": "Song not paused", + "music.options.resume.strings.notPaused.description": "The current song is not paused.", + "music.options.resume.strings.resumed.title": "Resumed", + "music.options.resume.strings.resumed.description": "Music playback has been resumed.", + "music.options.skip-to.name": "skip-to", + "music.options.skip-to.description": "Skips to a given point in the currently playing song.", + "music.options.skip-to.strings.noSong.title": "No song to seek timestamp", + "music.options.skip-to.strings.noSong.description": "There is no song to seek a timestamp in.", + "music.options.skip-to.strings.invalidTimestamp.title": "Invalid timestamp", + "music.options.skip-to.strings.invalidTimestamp.description": "The timestamp you specified is invalid.", + "music.options.skip-to.strings.skippedTo.title": "Skipped to timestamp", + "music.options.skip-to.strings.skippedTo.description": "Playback has skipped to the specified timestamp.", + "music.options.fast-forward.name": "fast-forward", + "music.options.fast-forward.description": "Fast-forwards the song by a given amount of time.", + "music.options.fast-forward.strings.noSong.title": "No song to fast-forward", + "music.options.fast-forward.strings.noSong.description": "There is no song to fast-forward.", + "music.options.fast-forward.strings.invalidTimestamp.title": "Invalid timestamp", + "music.options.fast-forward.strings.invalidTimestamp.description": "The timestamp you specified is invalid.", + "music.options.fast-forward.strings.fastForwarded.title": "Fast-forwarded", + "music.options.fast-forward.strings.fastForwarded.description": "Playback has skipped by the given amount of time.", + "music.options.rewind.name": "rewind", + "music.options.rewind.description": "Rewinds the song by a given amount of time.", + "music.options.rewind.strings.noSong.title": "No song to rewind", + "music.options.rewind.strings.noSong.description": "There is no song to rewind.", + "music.options.rewind.strings.invalidTimestamp.title": "Invalid timestamp", + "music.options.rewind.strings.invalidTimestamp.description": "The timestamp you specified is invalid.", + "music.options.rewind.strings.rewound.title": "Rewound", + "music.options.rewind.strings.rewound.description": "Playback has been rewound by the given amount of time.", + "music.options.skip.name": "skip", + "music.options.skip.description": "Skips the currently playing song.", + "music.options.skip.strings.noSong.title": "No song to skip", + "music.options.skip.strings.noSong.description": "There is no song to skip.", + "music.options.skip.strings.noSongCollection.title": "No song collection to skip", + "music.options.skip.strings.noSongCollection.description.noSongCollection": "There is no song collection to skip.", + "music.options.skip.strings.noSongCollection.description.trySongInstead": "Try skipping the current song instead.", + "music.options.skip.strings.skippedSong.title": "Skipped", + "music.options.skip.strings.skippedSong.description": "The song has been skipped.", + "music.options.skip.strings.skippedSongCollection.title": "Skipped", + "music.options.skip.strings.skippedSongCollection.description": "The song collection has been skipped.", + "music.options.stop.name": "stop", + "music.options.stop.description": "Stops the current listening session, clearing the queue and song history.", + "music.options.stop.strings.stopped.title": "Stopped", + "music.options.stop.strings.stopped.description": "The listening session has been stopped, and the song queue and history have been cleared.", + "music.options.unskip.name": "unskip", + "music.options.unskip.description": "Brings back the last played song.", + "music.options.unskip.strings.historyEmpty.title": "The history is empty", + "music.options.unskip.strings.historyEmpty.description": "There are no song listings to bring back.", + "music.options.unskip.strings.noSongCollection.title": "No song collection to skip", + "music.options.unskip.strings.noSongCollection.description.noSongCollection": "There is no song collection to skip.", + "music.options.unskip.strings.noSongCollection.description.trySongInstead": "Try skipping the current song instead.", + "music.options.unskip.strings.queueFull.title": "The queue is full", + "music.options.unskip.strings.queueFull.description": "The last played song listing cannot be brought back because the song queue is already full.", + "music.options.unskip.strings.unskipped.title": "Unskipped", + "music.options.unskip.strings.unskipped.description": "The last played song listing has been brought back.", + "music.options.volume.name": "volume", + "music.options.volume.description": "Allows the user to manage the volume of music playback.", + "music.options.volume.options.display.name": "display", + "music.options.volume.options.display.description": "Displays the volume of playback.", + "music.options.volume.options.display.strings.volume.title": "Volume", + "music.options.volume.options.display.strings.volume.description": "The current volume is {volume}%.", + "music.options.volume.options.set.name": "set", + "music.options.volume.options.set.description": "Sets the volume of playback.", + "music.options.volume.options.set.options.volume.name": "volume", + "music.options.volume.options.set.options.volume.description": "The volume to set.", + "music.options.volume.options.set.strings.invalid.title": "Invalid volume", + "music.options.volume.options.set.strings.invalid.description": "Song volume may not be negative, and it may not be higher than {volume}%.", + "music.options.volume.options.set.strings.set.title": "Volume set", + "music.options.volume.options.set.strings.set.description": "The volume has been set to {volume}%.", + "music.strings.notPlaying.title": "Not playing music", + "music.strings.notPlaying.description.toManage": "To manage playback, first request some music.", + "music.strings.notPlaying.description.toCheck": "To check playback-related information, first request some music.", + "music.strings.notInVc.title": "Not in voice channel", + "music.strings.notInVc.description.toManage": "To manage music, you must be in a voice channel.", + "music.strings.notInVc.description.toCheck": "To check playback-related information, you must be in a voice channel.", + "music.strings.listings": "Listings", + "music.strings.listEmpty": "This list is empty.", + "music.strings.skips.tooManyArguments.title": "Too many skip arguments", + "music.strings.skips.tooManyArguments.description": "You may not skip __by__ a number of songs and skip __to__ a certain song in the same query.", + "music.strings.skips.invalid.title": "Invalid number", + "music.strings.skips.invalid.description": "The number you have entered is invalid.", + "music.strings.outage.halted.title": "Audio playback halted", + "music.strings.outage.halted.description.outage": "Logos is experiencing some momentary problems with audio connectivity.", + "music.strings.outage.halted.description.noLoss": "Don't worry: If you were listening, your session is still active, and it will be back up as soon as the connection is re-established.", + "music.strings.outage.cannotManage.title": "Audio playback is currently halted", + "music.strings.outage.cannotManage.description.outage": "Audio playback was halted due to a temporary audio interruption.", + "music.strings.outage.cannotManage.description.backUpSoon": "You will be able to manage music as soon as the audio connection is re-established.", + "music.strings.outage.restored.title": "Audio playback resumed", + "music.strings.outage.restored.description": "Audio connectivity has been restored.", + "suggestion.name": "suggestion", + "suggestion.description": "Allows the user to make a suggestion for the community.", + "suggestion.strings.sent.title": "Suggestion sent!", + "suggestion.strings.sent.description": "Your suggestion has been passed over to the server staff.", + "suggestion.strings.failed.title": "Failed to send suggestion", + "suggestion.strings.failed.description": "Due to unknown reasons, sending your suggestion failed.", + "suggestion.strings.sureToCancel.title": "Sure to exit?", + "suggestion.strings.sureToCancel.description": "Are you sure you want to stop submitting your suggestion?", + "suggestion.strings.tooMany.title": "Too many suggestions", + "suggestion.strings.tooMany.description": "You have already made a few suggestions recently. You should wait a little before making another suggestion.", + "resource.name": "resource", + "resource.description": "Allows the user to submit a resource.", + "resource.strings.sent.title": "Resource sent!", + "resource.strings.sent.description": "Your resource has been submitted.", + "resource.strings.failed.title": "Failed to send resource", + "resource.strings.failed.description": "Due to unknown reasons, submitting your resource failed.", + "resource.strings.sureToCancel.title": "Sure to exit?", + "resource.strings.sureToCancel.description": "Are you sure you want to stop submitting your resource?", + "resource.strings.tooMany.title": "Too many resource submissions", + "resource.strings.tooMany.description": "You have already submitted a few resources recently. You should wait a little before submitting another one.", + "ticket.name": "ticket", + "ticket.description": "Allows the user to manage tickets.", + "ticket.options.open.name": "open", + "ticket.options.open.description": "Opens a ticket.", + "ticket.strings.sent.title": "Ticket opened!", + "ticket.strings.sent.description": "A ticket has been opened.", + "ticket.strings.failed.title": "Failed to open ticket", + "ticket.strings.failed.description": "Due to unknown reasons, a ticket could not be opened.", + "ticket.strings.sureToCancel.title": "Sure to exit?", + "ticket.strings.sureToCancel.description": "Are you sure you no longer want to open a ticket?", + "ticket.strings.tooMany.title": "Too many tickets", + "ticket.strings.tooMany.description": "You have already opened a tickets recently. You should wait a little before opening another one.", + "praise.name": "praise", + "praise.description": "Praises a user for their contribution.", + "praise.options.comment.name": "comment", + "praise.options.comment.description": "A comment to attach to the praise.", + "praise.strings.praised.title": "User praised!", + "praise.strings.praised.description": "{user_mention} has just been notified that you have praised them.", + "praise.strings.cannotPraiseSelf.title": "Cannot praise self", + "praise.strings.cannotPraiseSelf.description": "You cannot praise yourself.", + "praise.strings.failed.title": "Failed to praise user", + "praise.strings.failed.description": "Due to unknown reasons, praising the specified user failed.", + "praise.strings.tooMany.title": "Too many praises", + "praise.strings.tooMany.description": "You have already praised a user recently. You must wait before praising somebody again.", + "profile.name": "profile", + "profile.description": "Allows the user to view information about themselves or another user.", + "profile.options.roles.name": "roles", + "profile.options.roles.description": "Opens the role selection menu.", + "profile.options.roles.strings.limitReached.title": "Role limit reached", + "profile.options.roles.strings.limitReached.description.limitReached": "You have reached the limit of roles you can assign in this category.", + "profile.options.roles.strings.limitReached.description.toChooseNew": "To choose a new role, unassign one of your existing roles.", + "profile.options.roles.strings.roleMandatory.title": "This role is mandatory", + "profile.options.roles.strings.roleMandatory.description": "You cannot unassign this role. You can only swap it for another role from the same category.", + "profile.options.roles.strings.chooseCategory": "Choose a category.", + "profile.options.roles.strings.chooseRole": "Choose a role.", + "profile.options.roles.strings.back": "Back", + "profile.options.roles.strings.assigned": "Assigned", + "profile.options.view.name": "view", + "profile.options.view.description": "Displays a user's profile.", + "profile.options.view.strings.failed.title": "Failed to show information", + "profile.options.view.strings.failed.description": "Due to unknown reasons, showing information about the specified user failed.", + "profile.options.view.strings.information.title": "Information about {username}", + "profile.options.view.strings.information.description.roles": "Roles", + "profile.options.view.strings.information.description.dates": "Dates", + "profile.options.view.strings.information.description.statistics": "Statistics", + "profile.options.view.strings.information.description.received": "Received", + "profile.options.view.strings.information.description.sent": "Sent", + "profile.options.view.strings.information.description.praises": "Praises", + "profile.options.view.strings.information.description.warnings": "Warnings", + "settings.name": "settings", + "settings.description": "Allows the user to manage their settings.", + "settings.options.view.name": "view", + "settings.options.view.description": "Displays the user's settings.", + "settings.options.language.name": "language", + "settings.options.language.description": "Allows the user to change their display language.", + "settings.options.language.options.clear.name": "clear", + "settings.options.language.options.clear.description": "Clears the display language, setting it back to the default one.", + "settings.options.language.options.set.name": "set", + "settings.options.language.options.set.description": "Sets the display language.", + "settings.options.language.options.set.options.language.name": "language", + "settings.options.language.options.set.options.language.description": "The language to set.", + "settings.strings.settings.title": "User Settings", + "settings.strings.settings.fields.language.title": "Display Language", + "settings.strings.settings.fields.language.description.noLanguageSet.noLanguageSet": "You have not set a display language.", + "settings.strings.settings.fields.language.description.noLanguageSet.defaultShown": "Logos will adaptively display messages based on your app language.", + "settings.strings.cannotClear.title": "No display language has been set", + "settings.strings.cannotClear.description": "You cannot reset your display language because you have not set one yet.", + "settings.strings.cleared.title": "Display language reset", + "settings.strings.cleared.description": "Logos will now show messages in the language you have set in the Discord app.", + "settings.strings.invalid.title": "Invalid language", + "settings.strings.invalid.description": "The language you specified is invalid.", + "settings.strings.alreadySet.title": "Already set as display language", + "settings.strings.alreadySet.description": "You already have {language} set as your display language.", + "settings.strings.languageUpdated.title": "Updated display language", + "settings.strings.languageUpdated.description": "Your display language has been set to {language}.", + "licence.name": "licences", + "licence.description": "Allows the user to view notices for data and software used by the Logos project.", + "licence.options.dictionary.name": "dictionary", + "licence.options.dictionary.description": "View licence notices for dictionaries used by Logos.", + "licence.options.dictionary.options.dictionary.name": "dictionary", + "licence.options.dictionary.options.dictionary.description": "The dictionary to view the licence for.", + "licence.options.software.name": "software", + "licence.options.software.description": "View licence notices for software used by Logos.", + "licence.options.software.options.package.name": "package", + "licence.options.software.options.package.description": "The package to view the licence for.", + "licence.strings.invalid.title": "Invalid licence", + "licence.strings.invalid.description": "The licence you tried to view is invalid.", + "licence.strings.license": "Licence – {entity}", + "licence.strings.source": "Source", + "licence.strings.copyright": "Copyright Notice", + "credits.name": "credits", + "credits.description": "Shows the credits menu.", + "credits.strings.translation": "Translation", + "acknowledgements.name": "acknowledgements", + "acknowledgements.description": "Shows the acknowledgements menu.", + "acknowledgements.strings.acknowledgements": "Acknowledgements" +} diff --git a/source/constants/contexts.ts b/source/constants/contexts.ts index c2412f866..1d1a11553 100644 --- a/source/constants/contexts.ts +++ b/source/constants/contexts.ts @@ -865,6 +865,9 @@ export default Object.freeze({ translation: localise("game.strings.translation", locale)(), sourcedFrom: localise("game.strings.sourcedFrom", locale), }), + translationsSourcedFrom: ({ localise, locale }) => ({ + sourcedFrom: localise("translate.strings.sourcedFrom", locale), + }), pardoned: ({ localise, locale }) => ({ title: localise("pardon.strings.pardoned.title", locale)(), description: localise("pardon.strings.pardoned.description", locale), diff --git a/source/constants/licences.ts b/source/constants/licences.ts index dbe00352c..478160114 100644 --- a/source/constants/licences.ts +++ b/source/constants/licences.ts @@ -2,11 +2,11 @@ import apache from "logos:constants/licences/apache"; import bsd from "logos:constants/licences/bsd"; import mit from "logos:constants/licences/mit"; -interface DictionaryLicence { +interface Licence { readonly name: string; readonly link: string; readonly faviconLink?: string; - readonly notices: { + readonly notices?: { readonly licence: string; readonly copyright?: string; readonly badgeLink?: string; @@ -72,7 +72,21 @@ You must also include, in your app or site, wherever you provide attributions or "You may cache and thus store API Data on your system for up to 24 hours, after which such cached API Data must be purged. Subject to that exception, you will not copy, store, archive, distribute to any third party (other than to End Users as contemplated in this Agreement) any API Data, any metadata or any Link. You agree that any cached API Data will be used by you only for the purpose of populating the Developer Application.", }, }, - } satisfies Record, + } satisfies Record, + translators: { + deepl: { + name: "DeepL", + link: "https://www.deepl.com/translator/", + }, + googleTranslate: { + name: "Google Translate", + link: "https://translate.google.com/", + }, + lingvanex: { + name: "Lingvanex", + link: "https://lingvanex.com/translate/", + }, + } satisfies Record, software: { "@discordeno/bot": apache("Copyright 2021 - 2023 Discordeno"), cldpre: apache("Copyright (c) Authors of cldpre"), @@ -102,10 +116,10 @@ function isValidDictionary(dictionary: string): dictionary is Dictionary { return dictionary in licences.dictionaries; } -function getDictionaryLicenceByDictionary(dictionary: Dictionary): DictionaryLicence { +function getDictionaryLicenceByDictionary(dictionary: Dictionary): Licence { return licences.dictionaries[dictionary]; } export default licences; export { isValidDictionary, getDictionaryLicenceByDictionary }; -export type { DictionaryLicence }; +export type { Licence }; diff --git a/source/library/adapters/dictionaries/adapter.ts b/source/library/adapters/dictionaries/adapter.ts index aa5906229..67a891a53 100644 --- a/source/library/adapters/dictionaries/adapter.ts +++ b/source/library/adapters/dictionaries/adapter.ts @@ -1,5 +1,5 @@ import type { LearningLanguage } from "logos:constants/languages"; -import type { DictionaryLicence } from "logos:constants/licences"; +import type { Licence } from "logos:constants/licences"; import type { PartOfSpeech } from "logos:constants/parts-of-speech"; import type { Client } from "logos/client"; import { Logger } from "logos/logger"; @@ -68,7 +68,7 @@ interface DictionaryEntry { /** The inflection of the lemma. */ inflectionTable?: InflectionTable; - sources: [link: string, licence: DictionaryLicence][]; + sources: [link: string, licence: Licence][]; } abstract class DictionaryAdapter { diff --git a/source/library/adapters/translators/adapter.ts b/source/library/adapters/translators/adapter.ts index af1a5f4d9..208a09a2c 100644 --- a/source/library/adapters/translators/adapter.ts +++ b/source/library/adapters/translators/adapter.ts @@ -1,6 +1,7 @@ import type { Languages, TranslationLanguage } from "logos:constants/languages"; import type { Client } from "logos/client"; import { Logger } from "logos/logger"; +import { Licence } from "logos:constants/licences.ts"; interface TranslationResult { /** The language detected from the text sent to be translated. */ @@ -8,6 +9,9 @@ interface TranslationResult { /** The translation result. */ readonly text: string; + + /** The source of the result. */ + readonly source: Licence; } abstract class TranslatorAdapter { diff --git a/source/library/adapters/translators/deepl.ts b/source/library/adapters/translators/deepl.ts index d7d660560..e1fb3cba7 100644 --- a/source/library/adapters/translators/deepl.ts +++ b/source/library/adapters/translators/deepl.ts @@ -87,7 +87,7 @@ class DeepLAdapter extends TranslatorAdapter { ? getDeepLTranslationLanguageByLocale(detectedSourceLocale) : undefined; - return { detectedSourceLanguage, text: translation.text }; + return { detectedSourceLanguage, text: translation.text, source: constants.licences.translators.deepl }; } } diff --git a/source/library/adapters/translators/google-translate.ts b/source/library/adapters/translators/google-translate.ts index 731445466..9d3884d15 100644 --- a/source/library/adapters/translators/google-translate.ts +++ b/source/library/adapters/translators/google-translate.ts @@ -91,7 +91,7 @@ class GoogleTranslateAdapter extends TranslatorAdapter ? undefined : getGoogleTranslateLanguageByLocale(detectedSourceLocale); - return { detectedSourceLanguage, text: translatedText }; + return { detectedSourceLanguage, text: translatedText, source: constants.licences.translators.googleTranslate }; } } diff --git a/source/library/adapters/translators/lingvanex.ts b/source/library/adapters/translators/lingvanex.ts index ece567f49..28adb821d 100644 --- a/source/library/adapters/translators/lingvanex.ts +++ b/source/library/adapters/translators/lingvanex.ts @@ -74,7 +74,7 @@ class LingvanexAdapter extends TranslatorAdapter { ? undefined : getLingvanexTranslationLanguageByLocale(detectedSourceLocale); - return { detectedSourceLanguage, text: translatedText }; + return { detectedSourceLanguage, text: translatedText, source: constants.licences.translators.lingvanex }; } } diff --git a/source/library/commands/components/source-notices/source-notice.ts b/source/library/commands/components/source-notices/source-notice.ts index ab0f051f7..76133bc67 100644 --- a/source/library/commands/components/source-notices/source-notice.ts +++ b/source/library/commands/components/source-notices/source-notice.ts @@ -7,7 +7,7 @@ abstract class SourceNotice { readonly #buttonPresses: InteractionCollector; readonly #interaction: Logos.Interaction; readonly #sources: string[]; - readonly #notice: string; + readonly #notice?: string; get button(): Discord.ButtonComponent { const strings = constants.contexts.source({ @@ -25,7 +25,7 @@ abstract class SourceNotice { constructor( client: Client, - { interaction, sources, notice }: { interaction: Logos.Interaction; sources: string[]; notice: string }, + { interaction, sources, notice }: { interaction: Logos.Interaction; sources: string[]; notice?: string }, ) { this.client = client; @@ -45,7 +45,7 @@ abstract class SourceNotice { { description: `${constants.emojis.link} ${sourcesFormatted}`, color: constants.colours.blue, - footer: { text: this.#notice }, + footer: this.#notice !== undefined ? { text: this.#notice } : undefined, }, ], }, diff --git a/source/library/commands/components/source-notices/translation-source-notice.ts b/source/library/commands/components/source-notices/translation-source-notice.ts new file mode 100644 index 000000000..b716f9e4e --- /dev/null +++ b/source/library/commands/components/source-notices/translation-source-notice.ts @@ -0,0 +1,19 @@ +import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; +import type { Client } from "logos/client.ts"; +import type { Licence } from "logos:constants/licences.ts"; + +class TranslationSourceNotice extends SourceNotice { + constructor(client: Client, { interaction, source }: { interaction: Logos.Interaction; source: Licence }) { + const strings = constants.contexts.translationsSourcedFrom({ + localise: client.localise.bind(client), + locale: interaction.displayLocale, + }); + + super(client, { + interaction, + sources: [strings.sourcedFrom({ source: `[${source.name}](${source.link})` })], + }); + } +} + +export { TranslationSourceNotice }; diff --git a/source/library/commands/handlers/licence/dictionary.ts b/source/library/commands/handlers/licence/dictionary.ts index 116765bf7..1de575daf 100644 --- a/source/library/commands/handlers/licence/dictionary.ts +++ b/source/library/commands/handlers/licence/dictionary.ts @@ -40,14 +40,14 @@ async function handleDisplayDictionaryLicence( iconUrl: licence.faviconLink, url: licence.link, }, - description: `*${licence.notices.licence}*`, - image: licence.notices.badgeLink !== undefined ? { url: licence.notices.badgeLink } : undefined, + description: licence.notices !== undefined ? `*${licence.notices.licence}*` : undefined, + image: licence.notices?.badgeLink !== undefined ? { url: licence.notices.badgeLink } : undefined, fields: [ { name: strings.fields.source, value: licence.link, }, - ...(licence.notices.copyright !== undefined + ...(licence.notices?.copyright !== undefined ? [ { name: strings.fields.copyright, diff --git a/source/library/commands/handlers/translate.ts b/source/library/commands/handlers/translate.ts index 74d00e2ba..9434f4966 100644 --- a/source/library/commands/handlers/translate.ts +++ b/source/library/commands/handlers/translate.ts @@ -7,6 +7,8 @@ import languages, { import { trim } from "logos:core/formatting"; import type { TranslationResult } from "logos/adapters/translators/adapter"; import type { Client } from "logos/client"; +import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; +import { TranslationSourceNotice } from "logos/commands/components/source-notices/translation-source-notice.ts"; async function handleTranslateChatInputAutocomplete( client: Client, @@ -336,14 +338,21 @@ async function translateText( ]; } - const components: Discord.ActionRow[] | undefined = interaction.parameters.show - ? undefined - : [ - { - type: Discord.MessageComponentTypes.ActionRow, - components: [client.interactionRepetitionService.getShowButton(interaction)], - }, - ]; + const sourceNotice = new TranslationSourceNotice(client, { interaction, source: translation.source }); + + await sourceNotice.register(); + + const components: Discord.ActionRow[] = [ + { + type: Discord.MessageComponentTypes.ActionRow, + components: [ + ...(interaction.parameters.show + ? [] + : [client.interactionRepetitionService.getShowButton(interaction)]), + sourceNotice.button, + ] as [Discord.ButtonComponent], + }, + ]; await client.noticed(interaction, { embeds, components }); } From acdd6d4d77691a3872415477d79d1b2d8ae31772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Sat, 22 Jun 2024 18:18:47 +0100 Subject: [PATCH 2/4] style: Format. --- source/library/adapters/translators/adapter.ts | 2 +- .../components/source-notices/tatoeba-source-notice.ts | 2 +- .../components/source-notices/translation-source-notice.ts | 4 ++-- .../commands/components/source-notices/word-source-notice.ts | 2 +- source/library/commands/handlers/game.ts | 2 +- source/library/commands/handlers/translate.ts | 1 - 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/source/library/adapters/translators/adapter.ts b/source/library/adapters/translators/adapter.ts index 208a09a2c..19301359a 100644 --- a/source/library/adapters/translators/adapter.ts +++ b/source/library/adapters/translators/adapter.ts @@ -1,7 +1,7 @@ import type { Languages, TranslationLanguage } from "logos:constants/languages"; +import type { Licence } from "logos:constants/licences.ts"; import type { Client } from "logos/client"; import { Logger } from "logos/logger"; -import { Licence } from "logos:constants/licences.ts"; interface TranslationResult { /** The language detected from the text sent to be translated. */ diff --git a/source/library/commands/components/source-notices/tatoeba-source-notice.ts b/source/library/commands/components/source-notices/tatoeba-source-notice.ts index 26e51d630..49f6e2025 100644 --- a/source/library/commands/components/source-notices/tatoeba-source-notice.ts +++ b/source/library/commands/components/source-notices/tatoeba-source-notice.ts @@ -1,5 +1,5 @@ -import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; import type { Client } from "logos/client.ts"; +import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; class TatoebaSourceNotice extends SourceNotice { constructor( diff --git a/source/library/commands/components/source-notices/translation-source-notice.ts b/source/library/commands/components/source-notices/translation-source-notice.ts index b716f9e4e..1f9392ae8 100644 --- a/source/library/commands/components/source-notices/translation-source-notice.ts +++ b/source/library/commands/components/source-notices/translation-source-notice.ts @@ -1,6 +1,6 @@ -import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; -import type { Client } from "logos/client.ts"; import type { Licence } from "logos:constants/licences.ts"; +import type { Client } from "logos/client.ts"; +import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; class TranslationSourceNotice extends SourceNotice { constructor(client: Client, { interaction, source }: { interaction: Logos.Interaction; source: Licence }) { diff --git a/source/library/commands/components/source-notices/word-source-notice.ts b/source/library/commands/components/source-notices/word-source-notice.ts index 27cfdb9e7..fdea6f09e 100644 --- a/source/library/commands/components/source-notices/word-source-notice.ts +++ b/source/library/commands/components/source-notices/word-source-notice.ts @@ -1,5 +1,5 @@ -import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; import type { Client } from "logos/client.ts"; +import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; class WordSourceNotice extends SourceNotice { constructor(client: Client, { interaction, sources }: { interaction: Logos.Interaction; sources: string[] }) { diff --git a/source/library/commands/handlers/game.ts b/source/library/commands/handlers/game.ts index c078c4fb3..4172befb5 100644 --- a/source/library/commands/handlers/game.ts +++ b/source/library/commands/handlers/game.ts @@ -3,10 +3,10 @@ import { capitalise } from "logos:core/formatting"; import * as levenshtein from "fastest-levenshtein"; import type { Client } from "logos/client"; import { InteractionCollector } from "logos/collectors"; +import { TatoebaSourceNotice } from "logos/commands/components/source-notices/tatoeba-source-notice.ts"; import { GuildStatistics } from "logos/models/guild-statistics"; import { User } from "logos/models/user"; import type { SentencePair } from "logos/stores/volatile"; -import { TatoebaSourceNotice } from "logos/commands/components/source-notices/tatoeba-source-notice.ts"; function random(max: number): number { return Math.floor(Math.random() * max); diff --git a/source/library/commands/handlers/translate.ts b/source/library/commands/handlers/translate.ts index 9434f4966..5116f9f0b 100644 --- a/source/library/commands/handlers/translate.ts +++ b/source/library/commands/handlers/translate.ts @@ -7,7 +7,6 @@ import languages, { import { trim } from "logos:core/formatting"; import type { TranslationResult } from "logos/adapters/translators/adapter"; import type { Client } from "logos/client"; -import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; import { TranslationSourceNotice } from "logos/commands/components/source-notices/translation-source-notice.ts"; async function handleTranslateChatInputAutocomplete( From bd14523f55145b2f2527e3efc928a069979a5bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Sat, 22 Jun 2024 18:32:02 +0100 Subject: [PATCH 3/4] feat: Add licence notices to detectors. --- source/constants/licences.ts | 18 +++++++++++ source/library/adapters/detectors/adapter.ts | 2 ++ source/library/adapters/detectors/cld.ts | 2 +- source/library/adapters/detectors/eld.ts | 2 +- source/library/adapters/detectors/fasttext.ts | 2 +- source/library/adapters/detectors/tinyld.ts | 2 +- source/library/stores/adapters/detectors.ts | 32 +++++++++++-------- 7 files changed, 43 insertions(+), 17 deletions(-) diff --git a/source/constants/licences.ts b/source/constants/licences.ts index 478160114..27451b716 100644 --- a/source/constants/licences.ts +++ b/source/constants/licences.ts @@ -87,6 +87,24 @@ You must also include, in your app or site, wherever you provide attributions or link: "https://lingvanex.com/translate/", }, } satisfies Record, + detectors: { + cld: { + name: "CLD", + link: "https://npmjs.com/package/cldpre", + }, + eld: { + name: "ELD", + link: "https://github.com/vxern/efficient-language-detector-js", + }, + fasttext: { + name: "fastText", + link: "https://npmjs.com/package/fasttext.wasm.js", + }, + tinyld: { + name: "TinyLD", + link: "https://npmjs.com/package/tinyld", + }, + } satisfies Record, software: { "@discordeno/bot": apache("Copyright 2021 - 2023 Discordeno"), cldpre: apache("Copyright (c) Authors of cldpre"), diff --git a/source/library/adapters/detectors/adapter.ts b/source/library/adapters/detectors/adapter.ts index b2e23c845..73ff3d2e7 100644 --- a/source/library/adapters/detectors/adapter.ts +++ b/source/library/adapters/detectors/adapter.ts @@ -1,9 +1,11 @@ import type { DetectionLanguage } from "logos:constants/languages"; import type { Client } from "logos/client"; import { Logger } from "logos/logger"; +import type { Licence } from "logos:constants/licences.ts"; interface SingleDetectionResult { readonly language: DetectionLanguage; + readonly source: Licence; } abstract class DetectorAdapter { diff --git a/source/library/adapters/detectors/cld.ts b/source/library/adapters/detectors/cld.ts index 34961ae09..a84a36d83 100644 --- a/source/library/adapters/detectors/cld.ts +++ b/source/library/adapters/detectors/cld.ts @@ -17,7 +17,7 @@ class CLDAdapter extends DetectorAdapter { const detectedLanguage = getCLDDetectionLanguageByLocale(detectedLocale); - return { language: detectedLanguage }; + return { language: detectedLanguage, source: constants.licences.detectors.cld }; } } diff --git a/source/library/adapters/detectors/eld.ts b/source/library/adapters/detectors/eld.ts index 700c258ff..f00abf5fa 100644 --- a/source/library/adapters/detectors/eld.ts +++ b/source/library/adapters/detectors/eld.ts @@ -17,7 +17,7 @@ class ELDAdapter extends DetectorAdapter { const detectedLanguage = getELDDetectionLanguageByLocale(result.language); - return { language: detectedLanguage }; + return { language: detectedLanguage, source: constants.licences.detectors.eld }; } } diff --git a/source/library/adapters/detectors/fasttext.ts b/source/library/adapters/detectors/fasttext.ts index 873dfac26..17da7647d 100644 --- a/source/library/adapters/detectors/fasttext.ts +++ b/source/library/adapters/detectors/fasttext.ts @@ -19,7 +19,7 @@ class FastTextAdapter extends DetectorAdapter { const detectedLanguage = getFastTextDetectionLanguageByLocale(result.alpha3); - return { language: detectedLanguage }; + return { language: detectedLanguage, source: constants.licences.detectors.fasttext }; } } diff --git a/source/library/adapters/detectors/tinyld.ts b/source/library/adapters/detectors/tinyld.ts index 875ec4584..01c2237cf 100644 --- a/source/library/adapters/detectors/tinyld.ts +++ b/source/library/adapters/detectors/tinyld.ts @@ -24,7 +24,7 @@ class TinyLDAdapter extends DetectorAdapter { const detectedLanguage = getTinyLDDetectionLanguageByLocale(detectedLocale); - return { language: detectedLanguage }; + return { language: detectedLanguage, source: constants.licences.detectors.tinyld }; } } diff --git a/source/library/stores/adapters/detectors.ts b/source/library/stores/adapters/detectors.ts index 9fcfff278..aa4ec1702 100644 --- a/source/library/stores/adapters/detectors.ts +++ b/source/library/stores/adapters/detectors.ts @@ -5,13 +5,14 @@ import { ELDAdapter } from "logos/adapters/detectors/eld"; import { FastTextAdapter } from "logos/adapters/detectors/fasttext"; import { TinyLDAdapter } from "logos/adapters/detectors/tinyld"; import type { Client } from "logos/client"; +import type { Licence } from "logos:constants/licences.ts"; interface DetectionResult { readonly likely: DetectionLanguage[]; readonly possible: DetectionLanguage[]; + readonly sources: Licence[]; } -type DetectionFrequencies = Partial>; class DetectorStore { readonly adapters: { readonly cld: CLDAdapter; @@ -51,25 +52,30 @@ class DetectorStore { } compileDetections({ detections }: { detections: SingleDetectionResult[] }): DetectionResult { - const sortedByFrequency = detections.reduce((sorted, detection) => { - if (detection.language in sorted) { - sorted[detection.language]! += 1; - } else { - sorted[detection.language] = 1; - } + const sortedByFrequency = detections.reduce>>( + (sorted, detection) => { + if (detection.language in sorted) { + sorted[detection.language]!.push(detection.source); + } else { + sorted[detection.language] = [detection.source]; + } - return sorted; - }, {}); + return sorted; + }, + {}, + ); - const mode = Math.max(...Object.values(sortedByFrequency)); + const mode = Math.max(...Object.values(sortedByFrequency).map((sources) => sources.length)); - const result: DetectionResult = { likely: [], possible: [] }; - for (const [language, frequency] of Object.entries(sortedByFrequency) as [DetectionLanguage, number][]) { - if (frequency === mode) { + const result: DetectionResult = { likely: [], possible: [], sources: [] }; + for (const [language, sources] of Object.entries(sortedByFrequency) as [DetectionLanguage, Licence[]][]) { + if (sources.length === mode) { result.likely.push(language); } else { result.possible.push(language); } + + result.sources.push(...sources); } return result; From 2715e7fbad7a434f72b98f4c790d71081d4dcaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20Oszcz=C4=99da?= Date: Sun, 23 Jun 2024 11:35:34 +0100 Subject: [PATCH 4/4] feat: Add source notices for language recognition. --- assets/localisations/commands/eng-US.json | 1 + source/constants/contexts.ts | 3 ++ source/library/adapters/detectors/adapter.ts | 2 +- .../source-notices/recognition-notice.ts | 20 +++++++++++++ source/library/commands/handlers/recognise.ts | 28 +++++++++++++++++-- source/library/stores/adapters/detectors.ts | 2 +- 6 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 source/library/commands/components/source-notices/recognition-notice.ts diff --git a/assets/localisations/commands/eng-US.json b/assets/localisations/commands/eng-US.json index 29e713139..84d0ebac7 100644 --- a/assets/localisations/commands/eng-US.json +++ b/assets/localisations/commands/eng-US.json @@ -104,6 +104,7 @@ "recognise.strings.fields.possibleMatches.title": "Other options", "recognise.strings.fields.possibleMatches.description.single": "The language is less likely to be {language}.", "recognise.strings.fields.possibleMatches.description.multiple": "These choices are less likely, but the language could also be one of:", + "recognise.strings.recognitionsMadeBy": "Recognitions made by the above libraries.", "game.name": "game", "game.description": "Pick the correct word out of four to fit in the blank.", "game.strings.sentence": "Sentence", diff --git a/source/constants/contexts.ts b/source/constants/contexts.ts index 1d1a11553..21584e195 100644 --- a/source/constants/contexts.ts +++ b/source/constants/contexts.ts @@ -868,6 +868,9 @@ export default Object.freeze({ translationsSourcedFrom: ({ localise, locale }) => ({ sourcedFrom: localise("translate.strings.sourcedFrom", locale), }), + recognitionsMadeBy: ({ localise, locale }) => ({ + recognitionsMadeBy: localise("recognise.strings.recognitionsMadeBy", locale)(), + }), pardoned: ({ localise, locale }) => ({ title: localise("pardon.strings.pardoned.title", locale)(), description: localise("pardon.strings.pardoned.description", locale), diff --git a/source/library/adapters/detectors/adapter.ts b/source/library/adapters/detectors/adapter.ts index 73ff3d2e7..753a226f9 100644 --- a/source/library/adapters/detectors/adapter.ts +++ b/source/library/adapters/detectors/adapter.ts @@ -1,7 +1,7 @@ import type { DetectionLanguage } from "logos:constants/languages"; +import type { Licence } from "logos:constants/licences.ts"; import type { Client } from "logos/client"; import { Logger } from "logos/logger"; -import type { Licence } from "logos:constants/licences.ts"; interface SingleDetectionResult { readonly language: DetectionLanguage; diff --git a/source/library/commands/components/source-notices/recognition-notice.ts b/source/library/commands/components/source-notices/recognition-notice.ts new file mode 100644 index 000000000..02b6f2162 --- /dev/null +++ b/source/library/commands/components/source-notices/recognition-notice.ts @@ -0,0 +1,20 @@ +import type { Licence } from "logos:constants/licences.ts"; +import type { Client } from "logos/client.ts"; +import { SourceNotice } from "logos/commands/components/source-notices/source-notice.ts"; + +class RecognitionSourceNotice extends SourceNotice { + constructor(client: Client, { interaction, sources }: { interaction: Logos.Interaction; sources: Licence[] }) { + const strings = constants.contexts.recognitionsMadeBy({ + localise: client.localise.bind(client), + locale: interaction.displayLocale, + }); + + super(client, { + interaction, + sources: sources.map((source) => `[${source.name}](${source.link})`), + notice: strings.recognitionsMadeBy, + }); + } +} + +export { RecognitionSourceNotice }; diff --git a/source/library/commands/handlers/recognise.ts b/source/library/commands/handlers/recognise.ts index aaf9555d8..916597edb 100644 --- a/source/library/commands/handlers/recognise.ts +++ b/source/library/commands/handlers/recognise.ts @@ -1,6 +1,7 @@ import type { DetectionLanguage } from "logos:constants/languages"; import { list } from "logos:core/formatting"; import type { Client } from "logos/client"; +import { RecognitionSourceNotice } from "logos/commands/components/source-notices/recognition-notice.ts"; async function handleRecogniseLanguageChatInput( client: Client, @@ -69,6 +70,10 @@ async function handleRecogniseLanguage( return; } + const sourceNotice = new RecognitionSourceNotice(client, { interaction, sources: detectedLanguages.sources }); + + await sourceNotice.register(); + if (detectedLanguages.likely.length === 1 && detectedLanguages.possible.length === 0) { const language = detectedLanguages.likely.at(0) as DetectionLanguage | undefined; if (language === undefined) { @@ -79,8 +84,19 @@ async function handleRecogniseLanguage( ...constants.contexts.likelyMatch({ localise: client.localise.bind(client), locale: interaction.locale }), ...constants.contexts.language({ localise: client.localise.bind(client), locale: interaction.locale }), }; + await client.noticed(interaction, { - description: strings.description({ language: strings.language(language) }), + embeds: [ + { + description: strings.description({ language: strings.language(language) }), + }, + ], + components: [ + { + type: Discord.MessageComponentTypes.ActionRow, + components: [sourceNotice.button], + }, + ], }); return; } @@ -160,7 +176,15 @@ async function handleRecogniseLanguage( }); } - await client.noticed(interaction, { fields }); + await client.noticed(interaction, { + embeds: [{ fields }], + components: [ + { + type: Discord.MessageComponentTypes.ActionRow, + components: [sourceNotice.button], + }, + ], + }); } } diff --git a/source/library/stores/adapters/detectors.ts b/source/library/stores/adapters/detectors.ts index aa4ec1702..73fe5c33a 100644 --- a/source/library/stores/adapters/detectors.ts +++ b/source/library/stores/adapters/detectors.ts @@ -1,11 +1,11 @@ import type { DetectionLanguage, Detector } from "logos:constants/languages"; +import type { Licence } from "logos:constants/licences.ts"; import type { DetectorAdapter, SingleDetectionResult } from "logos/adapters/detectors/adapter"; import { CLDAdapter } from "logos/adapters/detectors/cld"; import { ELDAdapter } from "logos/adapters/detectors/eld"; import { FastTextAdapter } from "logos/adapters/detectors/fasttext"; import { TinyLDAdapter } from "logos/adapters/detectors/tinyld"; import type { Client } from "logos/client"; -import type { Licence } from "logos:constants/licences.ts"; interface DetectionResult { readonly likely: DetectionLanguage[];