From 93f746a766a4132837f4871c550e82470703dee4 Mon Sep 17 00:00:00 2001 From: leca Date: Thu, 8 Aug 2024 20:44:07 +0300 Subject: [PATCH] add translation using i18n --- package-lock.json | 24 ++++++++++ package.json | 1 + scheme.psql | 1 + src/db.js | 12 ++++- src/index.js | 102 ++++++++++++++++++++++++++++++------------- src/interactions.js | 42 ++++++++++++++---- src/utils.js | 11 +---- translations/en.json | 41 +++++++++++++++++ translations/ru.json | 42 ++++++++++++++++++ 9 files changed, 227 insertions(+), 49 deletions(-) create mode 100644 translations/en.json create mode 100644 translations/ru.json diff --git a/package-lock.json b/package-lock.json index 32d0194..c42967d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "GPL-3.0-or-later", "dependencies": { + "i18n-js": "^4.4.3", "matrix-bot-sdk": "^0.7.1", "nodemon": "^3.1.4", "pg": "^8.12.0" @@ -288,6 +289,14 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1070,6 +1079,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/i18n-js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/i18n-js/-/i18n-js-4.4.3.tgz", + "integrity": "sha512-QIIyvJ+wOKdigL4BlgwiFFrpoXeGdlC8EYgori64YSWm1mnhNYYjIfRu5wETFrmiNP2fyD6xIjVG8dlzaiQr/A==", + "dependencies": { + "bignumber.js": "*", + "lodash": "*", + "make-plural": "*" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1227,6 +1246,11 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "node_modules/make-plural": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", + "integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==" + }, "node_modules/matrix-bot-sdk": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/matrix-bot-sdk/-/matrix-bot-sdk-0.7.1.tgz", diff --git a/package.json b/package.json index d2d0134..80fc3e7 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "author": "leca", "license": "GPL-3.0-or-later", "dependencies": { + "i18n-js": "^4.4.3", "matrix-bot-sdk": "^0.7.1", "nodemon": "^3.1.4", "pg": "^8.12.0" diff --git a/scheme.psql b/scheme.psql index 44e88a0..92ab885 100644 --- a/scheme.psql +++ b/scheme.psql @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS users ( sex CHAR, -- 'm' of 'f' interest CHAR, -- male, female or both ('m', 'f' and 'b') description VARCHAR(512) DEFAULT NULL, + language VARCHAR(8) DEFAULT 'en', country VARCHAR(64) DEFAULT NULL, city VARCHAR(64) DEFAULT NULL, current_action VARCHAR(16) DEFAULT NULL, diff --git a/src/db.js b/src/db.js index e1b2c28..ff2c3fb 100644 --- a/src/db.js +++ b/src/db.js @@ -156,6 +156,14 @@ const markMessageAsRead = async (roomId, recipient) => { return (await db.query("UPDATE messages SET read = TRUE WHERE sender = $1 AND recipient = $2", [roomId, recipient])); } +const setUserLanguage = async (roomId, language) => { + await db.query("UPDATE users SET language = $1 WHERE room_id = $2", [language, roomId]); +} + +const getUserLanguage = async (roomId) => { + return (await db.query("SELECT language FROM users WHERE room_id = $1", [roomId])).rows[0].language; +} + export { eraseUser, appendUserPictures, @@ -176,5 +184,7 @@ export { uploadMediaAsMessage, insertMessageIntoDB, getUnreadMessages, - markMessageAsRead + markMessageAsRead, + setUserLanguage, + getUserLanguage }; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 7542541..2c067cb 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,6 @@ import { logError, logInfo, readConfig, - readMessages, uploadMediaFromEvent } from './utils.js'; @@ -31,16 +30,25 @@ import { insertMessageIntoDB, markMessageAsRead, setUserState, - uploadMediaAsMessage + uploadMediaAsMessage, + setUserLanguage, + getUserLanguage } from './db.js'; import { processRequest, showRandomProfileToUser, showNewLikes } from "./interactions.js"; import { db } from "./db.js"; +import fs from "fs"; + +import { I18n } from "i18n-js"; +const i18n = new I18n({ + en: JSON.parse(fs.readFileSync("./translations/en.json")), + ru: JSON.parse(fs.readFileSync("./translations/ru.json")) +}); +i18n.defaultLocale = "en"; const config = readConfig(); -const messages = readMessages(); const homeserverUrl = config.homeserverURL; const accessToken = config.token; @@ -60,11 +68,19 @@ client.on("room.message", async (roomId, event) => { let answer = event.content.body; let msgtype = event.content.msgtype + let preferredLanguage = await getUserLanguage(roomId); + if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; + i18n.locale = preferredLanguage; + switch (current_action) { case "wait_start": - if (answer !== "!start") return; + let a = answer.split(" ") + if (a[0] !== "!start") return; + if (a[1] !== "ru" && a[1] !== "en") return; + await setUserLanguage(roomId, a[1]); await setUserState(roomId, "country"); - await client.sendText(roomId, messages.setup.country); + await client.sendText(roomId, i18n.t(["setup", "country"])); + break; case "country": await processRequest(client, roomId, current_action, answer, 'city'); @@ -78,12 +94,13 @@ client.on("room.message", async (roomId, event) => { case "age": answer = parseInt(answer) if (!answer) { - await client.sendText(roomId, messages.errors.didntunderstand); + await client.sendText(roomId, i18n.t(["general", "age"])); return; } if (answer < 14) { - await client.sendText(roomId, messages.errors.tooyoung); + await client.sendText(roomId, i18n.t(["errors", "tooyoung"])); + await client.leaveRoom(roomId); await eraseUser(roomId); return; @@ -93,7 +110,8 @@ client.on("room.message", async (roomId, event) => { case "sex": answer = answer.toLowerCase().trim(); if (answer.toLowerCase() != "male" && answer.toLowerCase() != "female") { - await client.sendText(roomId, messages.errors.twosexes); + await client.sendText(roomId, i18n.t(["errors", "twosexes"])); + return; } await processRequest(client, roomId, current_action, answer[0], 'interest'); @@ -101,7 +119,8 @@ client.on("room.message", async (roomId, event) => { case "interest": answer = answer.toLowerCase().trim(); if (answer != "male" && answer != "female" && answer != "both") { - await client.sendText(roomId, messages.errors.didntunderstand); + await client.sendText(roomId, i18n.t(["errors", "didntunderstand"])); + return; } await processRequest(client, roomId, current_action, answer[0], 'description'); @@ -111,13 +130,15 @@ client.on("room.message", async (roomId, event) => { break; case "pictures": if (event.content?.msgtype !== 'm.image' && event.content?.msgtype !== 'm.video') { - await client.sendText(roomId, messages.setup.done); + await client.sendText(roomId, i18n.t(["setup", "done"])); + await setUserState(roomId, 'view_profiles'); await showRandomProfileToUser(client, roomId); } else { let pictures_count = parseInt(await getAmountOfUserPictures(roomId)); if (pictures_count >= maxAmountOfPhotoesPerUser) { - await client.sendText(roomId, messages.errors.toomuch); + await client.sendText(roomId, i18n.t(["errors", "toomuch"])); + await setUserState(roomId, 'view_profiles'); await showRandomProfileToUser(client, roomId); } else { @@ -126,10 +147,13 @@ client.on("room.message", async (roomId, event) => { await appendUserPictures(roomId, mxc, type); let pictures_count = await getAmountOfUserPictures(roomId); if (pictures_count < maxAmountOfPhotoesPerUser) { - await client.sendText(roomId, messages.setup.more + String(maxAmountOfPhotoesPerUser - pictures_count)); + await client.sendText(roomId, i18n.t(["setup", "more"], { amount: String(maxAmountOfPhotoesPerUser - pictures_count) })); + } else { - await client.sendText(roomId, messages.setup.enough); - await client.sendText(roomId, messages.setup.done); + await client.sendText(roomId, i18n.t(["setup", "enough"])); + + await client.sendText(roomId, i18n.t(["setup", "done"])); + await setUserState(roomId, 'view_profiles'); await showRandomProfileToUser(client, roomId); } @@ -142,12 +166,15 @@ client.on("room.message", async (roomId, event) => { await appendUserLikes(roomId, currently_viewing); let value = await checkForMutualLike(roomId, currently_viewing); if (value) { - await client.sendText(roomId, messages.general.newlikes); - await client.sendText(currently_viewing, messages.general.newlikes); + await client.sendText(roomId, i18n.t(["general", "newlikes"])); + + await client.sendText(roomId, i18n.t(["general", "newlikes"])); + } } else if (answer == 'πŸ’Œ' || answer == '3') { await setUserState(roomId, 'send_message'); - await client.sendText(roomId, messages.general.message); + await client.sendText(roomId, i18n.t(["general", "message"])); + return; } else if (answer == '🏠️' || answer == '4') { await client.sendText(roomId, messages.general.menu); @@ -168,10 +195,13 @@ client.on("room.message", async (roomId, event) => { content = answer; } if (await insertMessageIntoDB(roomId, recipient, msgtype, content)) { - await client.sendText(recipient, messages.general.newmessage); - await client.sendText(roomId, messages.general.messagesent); + await client.sendText(roomId, i18n.t(["general", "newmessage"])); + + await client.sendText(roomId, i18n.t(["general", "messagesent"])); + } else { - await client.sendText(roomId, messages.errors.alreadymessaged); + await client.sendText(roomId, i18n.t(["general", "alreadymessaged"])); + } await setUserState(roomId, 'view_profiles'); await showRandomProfileToUser(client, roomId); @@ -184,17 +214,21 @@ client.on("room.message", async (roomId, event) => { break; case '2': await showNewLikes(client, roomId); - await client.sendText(roomId, messages.general.menu); + await client.sendText(roomId, i18n.t(["general", "menu"])); + break; case '3': let unreadMessages = await getUnreadMessages(roomId); if (!unreadMessages || unreadMessages.length == 0) { - await client.sendText(roomId, messages.general.nonewmessages); + await client.sendText(roomId, i18n.t(["general", "nonewmessages"])); + return; } - await client.sendText(roomId, "Messages:"); + await client.sendText(roomId, i18n.t(["general", "msg"])); + for (let message of unreadMessages) { - await client.sendText(roomId, message.mx_id + messages.general.showmessage); + await client.sendText(roomId, i18n.t(["general", "showmessage"], { user: message.mx_id })); + if (message.type == "t") { await client.sendText(roomId, message.content) } else if (message.type == "p" || message.type == "v") { @@ -214,16 +248,19 @@ client.on("room.message", async (roomId, event) => { } await markMessageAsRead(message.sender, roomId); } - await client.sendText(roomId, messages.general.menu); + await client.sendText(roomId, i18n.t(["general", "menu"])); break; default: - await client.sendText(roomId, messages.errors.didntunderstand); - await client.sendText(roomId, messages.general.menu); + await client.sendText(roomId, i18n.t(["general", "didntunderstand"])); + + await client.sendText(roomId, i18n.t(["general", "menu"])); + break; } break; default: - await client.sendText(roomId, messages.errors.didntunderstand); + await client.sendText(roomId, i18n.t(["general", "didntunderstand"])); + return; } } catch (e) { @@ -249,7 +286,8 @@ client.on("room.invite", async (roomId, event) => { let isDM = members.length == 2 ? true : false; if (!isDM) { - await client.sendText(roomId, messages.errors.notadm); + await client.sendText(roomId, i18n.t(["errors", "notadm"])); + await client.leaveRoom(roomId) } @@ -262,7 +300,11 @@ client.on("room.invite", async (roomId, event) => { if (!is_profile_exists) { await db.query("INSERT INTO users(mx_id, room_id, current_action) VALUES ($1, $2, $3)", [mx_id, roomId, "wait_start"]) - await client.sendText(roomId, messages.general.welcome); + i18n.locale = "en"; + await client.sendText(roomId, i18n.t(["general", "welcome"])); + i18n.locale = "ru"; + await client.sendText(roomId, i18n.t(["general", "welcome"])); + } else { setUserState(roomId, 'view_profiles') } diff --git a/src/interactions.js b/src/interactions.js index 05acad4..0273c15 100644 --- a/src/interactions.js +++ b/src/interactions.js @@ -2,7 +2,6 @@ import { logError, logInfo, readConfig, - readMessages } from './utils.js'; import { @@ -14,9 +13,19 @@ import { getAllLikesForUser, checkForMutualLike, markLikeAsRead, + getUserLanguage } from "./db.js"; -const messages = readMessages(); +import fs from 'fs'; + + +import { I18n } from "i18n-js"; +const i18n = new I18n({ + en: JSON.parse(fs.readFileSync("./translations/en.json")), + ru: JSON.parse(fs.readFileSync("./translations/ru.json")) +}); + +i18n.defaultLocale = "en"; const requiresLengths = { "country": 64, @@ -26,20 +35,32 @@ const requiresLengths = { }; const processRequest = async (client, roomId, question, answer, nextQuestion) => { + + let preferredLanguage = await getUserLanguage(roomId); + if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; + i18n.locale = preferredLanguage; + if (answer.length > requiresLengths[question]) { - await client.sendText(roomId, messages.errors.toobig); + await client.sendText(roomId, i18n.t(["errors", "toobig"])); return; } await db.query(`UPDATE users SET ${question} = $1 WHERE room_id = $2`, [answer, roomId]); - await client.sendText(roomId, `Set your ${question} setting to "${answer}".`); - await client.sendText(roomId, messages.setup[nextQuestion]); //next question + await client.sendText(roomId, i18n.t(["general", "setopt"], { opt: answer })); + + await client.sendText(roomId, i18n.t(["setup", nextQuestion])); + setUserState(roomId, nextQuestion); }; const showRandomProfileToUser = async (client, roomId) => { + let preferredLanguage = await getUserLanguage(roomId); + if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; + i18n.locale = preferredLanguage; + let chosenProfile = await selectProfilesForUser(roomId); if (!chosenProfile) { - await client.sendText(roomId, messages.errors.noprofiles); + await client.sendText(roomId, i18n.t(["errors", "noprofiles"])); + return; } let message = @@ -71,13 +92,18 @@ ${chosenProfile.description}`; }; const showProfileToUser = async (client, roomId, profileId) => { + let preferredLanguage = await getUserLanguage(roomId); + if (!preferredLanguage) preferredLanguage = i18n.defaultLocale; + i18n.locale = preferredLanguage; + let profileInfo = await getProfileInfo(profileId); let message = `${profileInfo.country}, ${profileInfo.city}. ${profileInfo.name}, ${profileInfo.sex == 'm' ? 'male' : 'female'}, ${profileInfo.age}. ${profileInfo.description}`; - await client.sendText(roomId, messages.general.showalike); + await client.sendText(roomId, i18n.t(["general", "showalike"])); + await client.sendText(roomId, message); if (profileInfo.media) { @@ -95,8 +121,8 @@ ${profileInfo.description}`; }); } } + await client.sendText(roomId, i18n.t(["general", "mxid"], { mxid: profileInfo.mx_id })); - await client.sendText(roomId, messages.general.mxid + profileInfo.mx_id); }; const showNewLikes = async (client, roomId) => { diff --git a/src/utils.js b/src/utils.js index 5b10489..f3cbc11 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,7 +8,6 @@ import { } from "matrix-bot-sdk"; const configPath = './config.json'; -const messagesPath = './messages.json'; const logError = (message) => { let time = new Date; @@ -40,14 +39,6 @@ const readConfig = () => { return JSON.parse(fs.readFileSync(configPath)); }; -const readMessages = () => { - if (!fs.existsSync(messagesPath)) { - logError("No 'messages.json' file found. Please, ensure that you are up to date by syncing using 'git pull' command."); - process.exit(-1); - } - return JSON.parse(fs.readFileSync(messagesPath)); -}; - const uploadMediaFromEvent = async (client, event) => { const message = new MessageEvent(event); const fileEvent = new MessageEvent(message.raw); @@ -69,4 +60,4 @@ const convertMsgType = (msgtype) => { } }; -export { readMessages, readConfig, logError, logInfo, uploadMediaFromEvent, convertMsgType }; \ No newline at end of file +export { readConfig, logError, logInfo, uploadMediaFromEvent, convertMsgType }; \ No newline at end of file diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..07d1cf9 --- /dev/null +++ b/translations/en.json @@ -0,0 +1,41 @@ +{ + "general": { + "welcome": "Hello! I am a bot for new meetings in Matrix!\nTo start, please, type \"!start \", where language are 'en' or 'ru'.\nI'll ask some questions to fill your profile. You can change that info later, if you will.\nThen, I'll start showing you another people's profiles.\nYou can like other profile, skip or send a message to the person.\nIf you mutually liked each other, you'll be given each other's MXID to start a DM.\nMy source code can be found on gitea: https://git.foxarmy.org/leca/heart2heart.\n\nGood luck!", + "rate": "Decide wether you like (πŸ‘οΈ, ❀️ or 1), dislike (πŸ‘ŽοΈ, πŸ’” or 2) or wanna send a messsage (πŸ’Œ or 3). Write '🏠️' or 4 to quit to menu. Any other response counts as dislike.", + "menu": "Pick an action:\n1) Start watching profiles\n2) Check for mutual likes\n3) Check for messages", + "newlikes": "You have new mutual like(s)! Go into menu (🏠️) to see!", + "showalike": "This person and you mutually liked each other!", + "mxid": "Go drop them a line! Their MXID is: %{mxid}", + "message": "Write one message to this person or upload one picture/video to send them!", + "newmessage": "You've got new message(s)! Go to a menu (🏠️) to see!", + "showmessage": " sent you this: ", + "nonewmessages": "You have no new messages!", + "messagesent": "You message was sent!", + "msg": "Messages:", + "setopt": "Got \"%{opt}\"" + }, + "setup": { + "country": "Write a name of country you are living in. Prefer local language. For example, 'Россия', 'United States' or 'Π£ΠΊΡ€Π°Ρ—Π½Π°'.", + "city": "Write a name of city you are living in. Prefer local language. For example, 'Москва', 'Washington, D.C.', or 'ΠšΠΈΡ—Π²'.", + "name": "Write a name of your profile. Not longer than 32 symbols", + "description": "Write a description of your profile. Not longer than 512 symbols.", + "age": "Write your age. People yonger than 14 are disallowed to use the service.", + "sex": "Write your sex: Male or female.", + "interest": "Write who you are interested to talk to? Write 'both', 'male', or 'female'.", + "pictures": "Send up to five pictures that will be shown in your profile. Write any text message when you are done.", + "done": "Phew! You are done filling your profile! Now I'll start showing you others profiles!", + "more": "Got that cool picture! You still can upload up %{amount} picture(s)", + "enough": "That's enough of pictures! Let's go further!" + }, + "errors": { + "notadm": "This room is not a DM (direct or private messages). You should invite me in a DM! By the way, I support encrypted rooms! Bye and see you in DM!", + "toobig": "Sorry! The length of what you have written here is bigger than I expected! If you beleive that it is an error, please, file an issue on my git: https://git.foxarmy.org/leca/heart2heart. Otherwise, write something a little bit smaller! Thank you!", + "tooyoung": "Sorry! You are too young for this! Please, come back when you'll turn at least 14! Thank you and good bye!", + "toomuch": "You have loaded more that 5 pictures. I'll not upload that. Going to the next step...", + "twosexes": "There are only two sexes! Please, choose your biological one.", + "didntunderstand": "I cannot understand your answer. Please, read my question again and answer!", + "notimplemented": "This feature is not yet implemented! Please, keep track of my updates at https://git.foxarmy.org/leca/heart2heart. Bother your admins to update, if released! ;)", + "noprofiles": "There are no profiles left for you! I'm sorry. Please, come back later!", + "alreadymessaged": "You have already messaged that person. Your message was not sent!" + } +} \ No newline at end of file diff --git a/translations/ru.json b/translations/ru.json new file mode 100644 index 0000000..e4082cc --- /dev/null +++ b/translations/ru.json @@ -0,0 +1,42 @@ +{ + "general": { + "welcome": "ΠŸΡ€ΠΈΠ²Π΅Ρ‚! Π― Π±ΠΎΡ‚, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠΌΠΎΠΆΠ΅Ρ‚ Π²Π°ΠΌ Π½Π°ΠΉΡ‚ΠΈ Π½ΠΎΠ²Ρ‹Π΅ знакомства Π² ΠœΠ°Ρ‚Ρ€ΠΈΠΊΡΠ΅!\nΠ§Ρ‚ΠΎΠ±Ρ‹ Π½Π°Ρ‡Π°Ρ‚ΡŒ, Π½Π°ΠΏΠΈΡˆΠΈΡ‚Π΅ \"!start <язык>\". ВмСсто <язык> Π²ΡΡ‚Π°Π²ΡŒΡ‚Π΅ ru ΠΈΠ»ΠΈ en.\nΠ― ΡΠΏΡ€ΠΎΡˆΡƒ нСсколько вопросов, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π·Π°ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ ваш ΠΏΡ€ΠΎΡ„ΠΈΠ»ΡŒ. Π’Ρ‹ смоТСтС ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΏΠΎΠ·ΠΆΠ΅.\nПослС этого, я Π½Π°Ρ‡Π½Ρƒ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π²Π°ΠΌ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΡ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ..\nΠ’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π»Π°ΠΉΠΊΠ°Ρ‚ΡŒ, ΡΠΊΠΈΠΏΠ°Ρ‚ΡŒ Ρ‡ΡƒΠΆΠΈΠ΅ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ ΠΈΠ»ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ ΠΈΠΌ сообщСния.\nЕсли Π²Ρ‹ Π²Π·Π°ΠΈΠΌΠ½ΠΎ Π»Π°ΠΉΠΊΠ½ΡƒΠ»ΠΈ Π΄Ρ€ΡƒΠ³ Π΄Ρ€ΡƒΠ³, Π²Π°ΠΌ Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΠΎΠΊΠ°Π·Π°Π½Ρ‹ ваши MXID.\nМой исходный ΠΊΠΎΠ΄ ΠΌΠΎΠΆΠ½ΠΎ Π½Π°ΠΉΡ‚ΠΈ Π½Π° gitea: https://git.foxarmy.org/leca/heart2heart.\n\nΠ£Π΄Π°Ρ‡ΠΈ!", + "rate": "Π Π΅ΡˆΠΈΡ‚Π΅, нравится Π»ΠΌ (πŸ‘οΈ, ❀️ or 1), Π½Π΅ нравится (πŸ‘ŽοΈ, πŸ’” or 2) ΠΈΠ»ΠΈ Π²Ρ‹ Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ сообщСниС (πŸ’Œ or 3). ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ '🏠️' ΠΈΠ»ΠΈ 4 Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π²Ρ‹ΠΉΡ‚ΠΈ Π² мСню. Π›ΡŽΠ±ΠΎΠΉ Π΄Ρ€ΡƒΠ³ΠΎΠΉ ΠΎΡ‚Π²Π΅Ρ‚ считаСтся ΠΊΠ°ΠΊ \"Π½Π΅ нравится\".", + "menu": "Π’Ρ‹Π±Π΅Ρ€Π΅Ρ‚Π΅ дСйствиС:\n1) ΠΠ°Ρ‡Π°Ρ‚ΡŒ просмотр ΠΏΡ€ΠΎΡ„ΠΈΠ»Π΅ΠΉ\n2) ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Π²Π·Π°ΠΈΠΌΠ½Ρ‹Π΅ Π»Π°ΠΉΠΊΠΈ\n3) ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ сообщСния", + "newlikes": "Π£ вас Π½ΠΎΠ²Ρ‹ΠΉ(-ΠΈΠ΅) Π²Π·Π°ΠΈΠΌΠ½Ρ‹Π΅ Π»Π°ΠΉΠΊ(ΠΈ)! Π’Ρ‹ΠΉΠ΄ΠΈΡ‚Π΅ Π² мСню (🏠️) Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ!", + "showalike": "Π­Ρ‚ΠΎΡ‚ Ρ‡Π΅Π»ΠΎΠ²Π΅ΠΊ ΠΈ Π²Ρ‹ Π΄Ρ€ΡƒΠ³ Π΄Ρ€ΡƒΠ³Π° Π²Π·Π°ΠΈΠΌΠ½ΠΎ Π»Π°ΠΉΠΊΠ½ΡƒΠ»ΠΈ!", + "mxid": "Π˜Π΄ΠΈΡ‚Π΅ ΠΈ Π½Π°ΠΏΠΈΡˆΠΈΡ‚Π΅ этому Ρ‡Π΅Π»ΠΎΠ²Π΅ΠΊΡƒ! MXID: %{mxid}", + "message": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ ΠΎΠ΄Π½ΠΎ сообщСниС этому Ρ‡Π΅Π»ΠΎΠ²Π΅ΠΊΡƒ ΠΈΠ»ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚Π΅ ΠΎΠ΄Π½ΠΎ Π²ΠΈΠ΄ΠΈΠ΅ΠΎ ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΡƒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ вмСсто тСкстового сообщСния!", + "newmessage": "Π£ вас Π½ΠΎΠ²ΠΎΠ΅(-ΠΈΠ΅) сообщСниС(-ия)! Π’Ρ‹ΠΉΠ΄ΠΈΡ‚Π΅ Π² мСню (🏠️) Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ!", + "showmessage": "%{user} ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΠ»(-Π°) Π²Π°ΠΌ: ", + "nonewmessages": "Π£ вас Π½Π΅Ρ‚ Π½ΠΎΠ²Ρ‹Ρ… сообщСний!", + "messagesent": "Π’Π°ΡˆΠ΅ сообщСниС Π±Ρ‹Π»ΠΎ ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ!", + "msg": "БообщСния:", + "setopt": "Записал \"%{opt}\"" + }, + "setup": { + "country": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ имя страны, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ Π²Ρ‹ Π½Π°Ρ…ΠΎΠ΄ΠΈΡ‚Π΅ΡΡŒ. ΠŸΡ€Π΅Π΄ΠΏΠΎΡ‡Ρ‚ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π½Π° мСстном языкС. НапримСр, 'Россия', 'United States' ΠΈΠ»ΠΈ 'Π£ΠΊΡ€Π°Ρ—Π½Π°'.", + "city": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ имя Π³ΠΎΡ€ΠΎΠ΄Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ Π²Ρ‹ Π½Π°Ρ…ΠΎΠ΄ΠΈΡ‚Π΅ΡΡŒ. ΠŸΡ€Π΅Π΄ΠΏΠΎΡ‡Ρ‚ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π½Π° мСстном языкС. НапримСр, 'Москва', 'Washington, D.C.', ΠΈΠ»ΠΈ 'ΠšΠΈΡ—Π²'.", + "name": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ имя вашСго профиля. НС большС 32 символов", + "description": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ описаниС вашСго профиля. НС большС 512 символов.", + "age": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ ваш возраст. Π›ΡŽΠ΄ΡΠΌ, младшС 14 Π½Π΅ Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ этот сСрвис.", + "sex": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ ваш ΠΏΠΎΠ»: Male or female. (Π½Π° английском)", + "interest": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅, с ΠΊΠ΅ΠΌ Π²Π°ΠΌ интСрСсно Π·Π½Π°ΠΊΠΎΠΌΠΈΡ‚ΡŒΡΡ? ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ 'both', 'male', ΠΈΠ»ΠΈ 'female'. (Ρ‚ΠΎΠΆΠ΅ Π½Π° английском)", + "pictures": "ΠžΡ‚ΠΏΡ€Π°Π²ΡŒΡ‚Π΅ Π΄ΠΎ пяти ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΎΠΊ ΠΈΠ»ΠΈ Π²ΠΈΠ΄Π΅ΠΎ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄ΡƒΡ‚ ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½Ρ‹ Π² вашСм ΠΏΡ€ΠΎΡ„ΠΈΠ»Π΅. ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ любоС тСкстовоС сообщСниС, Ссли Ρ€Π΅ΡˆΠΈΡ‚Π΅ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ мСньшС.", + "done": "Π₯ΡƒΡ…! ΠœΡ‹ Π·Π°ΠΊΠΎΠ½Ρ‡ΠΈΠ»ΠΈ Π·Π°ΠΏΠΎΠ»Π½ΡΡ‚ΡŒ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΡŒ! Π’Π΅ΠΏΠ΅Ρ€ΡŒ я Π½Π°Ρ‡ΠΈΠ½Π°ΡŽ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π²Π°ΠΌ Ρ‡ΡƒΠΆΠΈΠ΅ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ!", + "more": "ΠŸΡ€ΠΈΠ½ΡΠ» эту ΠΊΡ€ΡƒΡ‚ΡƒΡŽ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΡƒ! Π’Ρ‹ всё Π΅Ρ‰Ρ‘ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ %{amount} ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΎΠΊ(ΠΊΡƒ)", + "enough": "Π₯Π²Π°Ρ‚ΠΈΡ‚ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΎΠΊ, Π΄Π°Π²Π°ΠΉΡ‚Π΅ дальшС!" + }, + "errors": { + "notadm": "Π­Ρ‚Π° ΠΊΠΎΠΌΠ½Π°Ρ‚Π° - Π½Π΅ Π›Π‘ (Π»ΠΈΡ‡Π½Ρ‹Π΅ сообщСния). Π’Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΠΏΡ€ΠΈΠ³Π»Π°ΡΠΈΡ‚ΡŒ мСня Π² Π›Π‘! ΠšΡΡ‚Π°Ρ‚ΠΈ, я ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ°Π²ΡŽ Π·Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΠΊΠΎΠΌΠ½Π°Ρ‚Ρ‹! Пока ΠΈ увидимся Π² Π›Π‘!", + "toobig": "Π˜Π·Π²ΠΈΠ½ΠΈΡ‚Π΅! Π”Π»ΠΈΠ½Π° Ρ‚ΠΎΠ³ΠΎ, Ρ‡Ρ‚ΠΎ Π²Ρ‹ написали большС, Ρ‡Π΅ΠΌ я ΠΎΠΆΠΈΠ΄Π°Π»! Если Π²Ρ‹ Π΄ΡƒΠΌΠ°Π΅Ρ‚Π΅, Ρ‡Ρ‚ΠΎ это ошибка, поТалуйста, создайтС issue Π½Π° ΠΌΠΎΡ‘ΠΌ Π³ΠΈΡ‚Π΅(ΠŸΡ€Π΅Π΄ΠΏΠΎΡ‡Ρ‚ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π½Π° английском): https://git.foxarmy.org/leca/heart2heart. Или ΠΏΠΎΠΏΡ‹Ρ‚Π°ΠΉΡ‚Π΅ΡΡŒ Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ Ρ‡Ρ‚ΠΎ-Ρ‚ΠΎ мСньшС! Бпасибо!", + "tooyoung": "Π˜Π·Π²ΠΈΠ½ΠΈΡ‚Π΅! Π’Ρ‹ слишком ΠΌΠΎΠ»ΠΎΠ΄Ρ‹ для этого! ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΠΉΡ‚Π΅ΡΡŒ, ΠΊΠΎΠ³Π΄Π° Π²Π°ΠΌ Π±ΡƒΠ΄Π΅Ρ‚ ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ 14! Бпасибо ΠΈ ΠΏΠΎΠΊΠ°!", + "toomuch": "Π’Ρ‹ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΠ»ΠΈ большС 5 ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΎΠΊ. Π― Π½Π΅ Π±ΡƒΠ΄Ρƒ это Π·Π°Π³Ρ€ΡƒΠΆΠ°Ρ‚ΡŒ. ΠŸΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΈΠΌ ΠΊ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌΡƒ ΡˆΠ°Π³Ρƒ...", + "twosexes": "Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, Π²Ρ‹ ошиблись Π² написании ΠΏΠΎΠ»Π°! ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π½Π°ΠΏΠΈΡˆΠΈΡ‚Π΅ \"male\" ΠΈΠ»ΠΈ \"female\".", + "didntunderstand": "Π― Π½Π΅ понимаю ваш ΠΎΡ‚Π²Π΅Ρ‚. ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΠΏΠ΅Ρ€Π΅Ρ‡ΠΈΡ‚Π°ΠΉΡ‚Π΅ ΠΌΠΎΡ‘ ΠΏΡ€ΠΎΡˆΠ»ΠΎΠ΅ сообщСниС ΠΈ ΠΏΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ снова!", + "notimplemented": "Π­Ρ‚Π° Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π΅Ρ‰Ρ‘ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π°! ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, слСдитС Π·Π° обновлСниями Π½Π° ΠΌΠΎΡ‘ΠΌ git'Π΅: https://git.foxarmy.org/leca/heart2heart. ДоставайтС своих Π°Π΄ΠΌΠΈΠ½ΠΎΠ², Ρ‡Ρ‚ΠΎΠ±Ρ‹ обновлялись, ΠΊΠΎΠ³Π΄Π° новая вСрсия Π²Ρ‹Ρ…ΠΎΠ΄ΠΈΡ‚ ;)", + "noprofiles": "НС ΠΌΠΎΠ³Ρƒ Π½Π°ΠΉΡ‚ΠΈ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ подходят вашим критСриям! Π˜Π·Π²ΠΈΠ½ΠΈΡ‚Π΅. ΠŸΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ ΠΏΠΎΠ·ΠΆΠ΅!", + "alreadymessaged": "Π’Ρ‹ ΡƒΠΆΠ΅ отправляли сообщСниС этому Ρ‡Π΅Π»ΠΎΠ²Π΅ΠΊΡƒ. Π’Π°ΡˆΠ΅ сообщСниС Π½Π΅ Π±Ρ‹Π»ΠΎ ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ!" + + } +} \ No newline at end of file