feat: add i18n support for backend API and Telegram bot (#797)

* feat: add i18n support for backend API and Telegram bot

- Add comprehensive i18n support for all backend API error messages (zh/en)
- Add /lang command for Telegram bot to set language preference
- Add bilingual command descriptions for Telegram bot
- Support per-user language preference stored in KV
- Global push uses DEFAULT_LANG, user push uses saved preference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: improve Telegram bot language preference feature

- Add internationalized message for disabled language feature
- Fix hardcoded English message in /lang command
- Optimize getTgMessages calls (reduce from 3 to 1 call)
- Remove verbose comments for better code clarity
- Add TgLangFeatureDisabledMsg to i18n (zh/en)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2025-12-31 01:42:41 +08:00
committed by GitHub
co-authored by Claude Sonnet 4.5
parent 5e227d2b2d
commit 3ebe22115a
25 changed files with 762 additions and 276 deletions
+30 -10
View File
@@ -4,6 +4,11 @@ import { cleanup } from '../common';
import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting } from '../utils';
import { CleanupSettings, CustomSqlCleanup } from '../models';
import i18n from '../i18n';
import { LocaleMessages } from '../i18n/type';
// SQL validation error types
type SqlValidationError = 'empty' | 'too_long' | 'not_delete' | 'multiple_statements' | 'has_comments';
// Normalize SQL: trim and remove trailing semicolon
const normalizeSql = (sql: string): string => {
@@ -14,34 +19,45 @@ const normalizeSql = (sql: string): string => {
return normalized;
};
// Get error message from error type
const getValidationErrorMsg = (errorType: SqlValidationError, msgs: LocaleMessages): string => {
switch (errorType) {
case 'empty': return msgs.SqlEmptyMsg;
case 'too_long': return msgs.SqlTooLongMsg;
case 'not_delete': return msgs.SqlOnlyDeleteMsg;
case 'multiple_statements': return msgs.SqlSingleStatementMsg;
case 'has_comments': return msgs.SqlNoCommentsMsg;
}
};
// Validate custom SQL cleanup statement
export const validateCustomSql = (sql: string): { valid: boolean; error?: string } => {
export const validateCustomSql = (sql: string): { valid: boolean; errorType?: SqlValidationError } => {
if (!sql || !sql.trim()) {
return { valid: false, error: "SQL statement is empty" };
return { valid: false, errorType: 'empty' };
}
const trimmedSql = normalizeSql(sql);
// Check SQL length (max 1000 characters)
if (trimmedSql.length > 1000) {
return { valid: false, error: "SQL statement is too long (max 1000 characters)" };
return { valid: false, errorType: 'too_long' };
}
const sqlUpper = trimmedSql.toUpperCase();
// Only allow DELETE statements
if (!sqlUpper.startsWith('DELETE ')) {
return { valid: false, error: "Only DELETE statements are allowed" };
return { valid: false, errorType: 'not_delete' };
}
// Only allow single statement (no semicolons after trimming)
if (trimmedSql.includes(';')) {
return { valid: false, error: "Only single SQL statement is allowed" };
return { valid: false, errorType: 'multiple_statements' };
}
// Forbid SQL comments
if (/--/.test(trimmedSql) || /\/\*/.test(trimmedSql)) {
return { valid: false, error: "SQL comments are not allowed" };
return { valid: false, errorType: 'has_comments' };
}
return { valid: true };
@@ -52,13 +68,14 @@ export const executeCustomSqlCleanup = async (
c: Context<HonoCustomType>,
customSql: CustomSqlCleanup
): Promise<{ success: boolean; rowsAffected?: number; error?: string }> => {
const msgs = i18n.getMessagesbyContext(c);
if (!customSql || !customSql.sql) {
return { success: false, error: "Invalid custom SQL cleanup config" };
return { success: false, error: msgs.InvalidCleanupConfigMsg };
}
const validation = validateCustomSql(customSql.sql);
if (!validation.valid) {
return { success: false, error: validation.error };
return { success: false, error: getValidationErrorMsg(validation.errorType!, msgs) };
}
const sql = normalizeSql(customSql.sql);
@@ -78,12 +95,13 @@ export const executeCustomSqlCleanup = async (
export default {
cleanup: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { cleanType, cleanDays } = await c.req.json();
try {
await cleanup(c, cleanType, cleanDays);
} catch (error) {
console.error(error);
return c.text(`Failed to cleanup ${(error as Error).message}`, 500)
return c.text(`${msgs.OperationFailedMsg}: ${(error as Error).message}`, 500)
}
return c.json({ success: true })
},
@@ -92,6 +110,7 @@ export default {
return c.json(cleanupSetting)
},
saveCleanup: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const cleanupSetting = await c.req.json<CleanupSettings>();
// Validate custom SQL cleanup list
@@ -100,7 +119,8 @@ export default {
if (customSql.sql) {
const validation = validateCustomSql(customSql.sql);
if (!validation.valid) {
return c.text(`Invalid SQL [${customSql.name || 'unnamed'}]: ${validation.error}`, 400);
const errorMsg = getValidationErrorMsg(validation.errorType!, msgs);
return c.text(`[${customSql.name || 'unnamed'}]: ${errorMsg}`, 400);
}
}
}