add translation using i18n

This commit is contained in:
leca 2024-08-08 20:44:07 +03:00
parent 21ea2cd48f
commit 93f746a766
9 changed files with 227 additions and 49 deletions

24
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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,

View File

@ -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
};

View File

@ -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')
}

View File

@ -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) => {

View File

@ -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 };
export { readConfig, logError, logInfo, uploadMediaFromEvent, convertMsgType };

41
translations/en.json Normal file
View File

@ -0,0 +1,41 @@
{
"general": {
"welcome": "Hello! I am a bot for new meetings in Matrix!\nTo start, please, type \"!start <language>\", 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!"
}
}

42
translations/ru.json Normal file
View File

@ -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": "Вы уже отправляли сообщение этому человеку. Ваше сообщение не было отправлено!"
}
}