Add i18n support for localization in Telegram bot; remove hardcoded error messages and integrate translation functionality.

This commit is contained in:
vchikalkin 2025-08-14 13:41:32 +03:00
parent e92d1418dd
commit bdaff801e4
6 changed files with 78 additions and 11 deletions

3
locales/en.ftl Normal file
View File

@ -0,0 +1,3 @@
invalid_url = Invalid url
invalid_download_urls = download urls not found
generic = Something went wrong

3
locales/ru.ftl Normal file
View File

@ -0,0 +1,3 @@
invalid_url = Неверная ссылка
invalid_download_urls = Не найдены ссылки для скачивания
generic = Что-то пошло не так

View File

@ -13,6 +13,7 @@
"license": "ISC",
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531",
"dependencies": {
"@grammyjs/i18n": "^1.1.2",
"@tobyg74/tiktok-api-dl": "^1.3.4",
"@types/node": "^24.2.1",
"grammy": "^1.37.0",

56
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@grammyjs/i18n':
specifier: ^1.1.2
version: 1.1.2(grammy@1.37.0)
'@tobyg74/tiktok-api-dl':
specifier: ^1.3.4
version: 1.3.4(@types/node@24.2.1)(canvas@3.1.2)(typescript@5.9.2)
@ -164,6 +167,12 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@deno/shim-deno-test@0.5.0':
resolution: {integrity: sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==}
'@deno/shim-deno@0.18.2':
resolution: {integrity: sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA==}
'@emnapi/core@1.4.5':
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
@ -386,6 +395,20 @@ packages:
'@fastify/busboy@3.1.1':
resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==}
'@fluent/bundle@0.17.1':
resolution: {integrity: sha512-CRFNT9QcSFAeFDneTF59eyv3JXFGhIIN4boUO2y22YmsuuKLyDk+N1I/NQUYz9Ab63e6V7T6vItoZIG/2oOOuw==}
engines: {node: '>=12.0.0', npm: '>=7.0.0'}
'@fluent/langneg@0.6.2':
resolution: {integrity: sha512-YF4gZ4sLYRQfctpUR2uhb5UyPUYY5n/bi3OaED/Q4awKjPjlaF8tInO3uja7pnLQcmLTURkZL7L9zxv2Z5NDwg==}
engines: {node: '>=12.0.0', npm: '>=7.0.0'}
'@grammyjs/i18n@1.1.2':
resolution: {integrity: sha512-PcK06mxuDDZjxdZ5HywBhr+erEITsR816KP4DNIDDds1jpA45pfz/nS9FdZmzF8H6lMyPix3mV5WL1rT4q+BuA==}
engines: {node: '>=12'}
peerDependencies:
grammy: ^1.10.0
'@grammyjs/types@3.21.0':
resolution: {integrity: sha512-IMj0EpmglPCICuyfGRx4ENKPSuzS2xMSoPgSPzHC6FtnWKDEmJLBP/GbPv/h3TAeb27txqxm/BUld+gbJk6ccQ==}
@ -2288,6 +2311,10 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isexe@3.1.1:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
isomorphic-ws@5.0.0:
resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
peerDependencies:
@ -3405,6 +3432,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
which@4.0.0:
resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
engines: {node: ^16.13.0 || >=18.0.0}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@ -3608,6 +3640,13 @@ snapshots:
'@csstools/css-tokenizer@3.0.4': {}
'@deno/shim-deno-test@0.5.0': {}
'@deno/shim-deno@0.18.2':
dependencies:
'@deno/shim-deno-test': 0.5.0
which: 4.0.0
'@emnapi/core@1.4.5':
dependencies:
'@emnapi/wasi-threads': 1.0.4
@ -3773,6 +3812,17 @@ snapshots:
'@fastify/busboy@3.1.1': {}
'@fluent/bundle@0.17.1': {}
'@fluent/langneg@0.6.2': {}
'@grammyjs/i18n@1.1.2(grammy@1.37.0)':
dependencies:
'@deno/shim-deno': 0.18.2
'@fluent/bundle': 0.17.1
'@fluent/langneg': 0.6.2
grammy: 1.37.0
'@grammyjs/types@3.21.0': {}
'@graphql-eslint/eslint-plugin@4.4.0(@types/node@24.2.1)(eslint@9.33.0(jiti@2.5.1))(graphql@16.11.0)(typescript@5.9.2)':
@ -6053,6 +6103,8 @@ snapshots:
isexe@2.0.0: {}
isexe@3.1.1: {}
isomorphic-ws@5.0.0(ws@8.18.3):
dependencies:
ws: 8.18.3
@ -7307,6 +7359,10 @@ snapshots:
dependencies:
isexe: 2.0.0
which@4.0.0:
dependencies:
isexe: 3.1.1
word-wrap@1.2.5: {}
wrap-ansi@7.0.0:

View File

@ -1,5 +0,0 @@
export const ERROR_MESSAGES = {
INVALID_URL: 'Invalid url',
INVALID_DOWNLOAD_URLS: 'Invalid download urls found',
GENERIC: 'Something went wrong',
};

View File

@ -1,21 +1,30 @@
import { env } from './config/env';
import { Downloader } from '@tobyg74/tiktok-api-dl';
import { Bot, InputFile } from 'grammy';
import { Bot, Context, InputFile } from 'grammy';
import { logger } from './utils/logger';
import { ERROR_MESSAGES } from './constants/messages';
import { validateTikTokUrl } from './utils/urls';
import { I18n, I18nFlavor } from '@grammyjs/i18n';
const bot = new Bot(env.BOT_TOKEN, {
type MyContext = Context & I18nFlavor;
const bot = new Bot<MyContext>(env.BOT_TOKEN, {
client: {
apiRoot: env.TELEGRAM_API_ROOT,
},
});
const i18n = new I18n<MyContext>({
defaultLocale: 'en', // see below for more information
directory: 'locales', // Load all translation files from locales/.
});
bot.use(i18n);
bot.on('message:text', async (ctx) => {
try {
const url = ctx.message.text;
if (!validateTikTokUrl(url)) return ctx.reply(ERROR_MESSAGES.INVALID_URL);
if (!validateTikTokUrl(url)) return ctx.reply(ctx.t('invalid_url'));
const { result, message } = await Downloader(url, { version: 'v3' });
@ -25,7 +34,7 @@ bot.on('message:text', async (ctx) => {
const imagesUrls = result?.images;
if (!videoUrl && !imagesUrls?.length) {
return ctx.reply(ERROR_MESSAGES.INVALID_DOWNLOAD_URLS);
return ctx.reply(ctx.t('invalid_download_urls'));
}
if (result?.type === 'video' && videoUrl) {
@ -38,7 +47,7 @@ bot.on('message:text', async (ctx) => {
} catch (error) {
logger.error(error);
return ctx.reply(ERROR_MESSAGES.GENERIC);
return ctx.reply(ctx.t('generic'));
}
});