feat(matrix-bots): add i18n system and direct message fallback

- Add I18nService with per-user language preferences (de/en)
- Add !language/!sprache command to all 4 bots (todo, calendar, contacts, clock)
- Add fallback behavior: messages without commands create tasks/events/contacts/timers
- Improve clock bot duration parsing to accept bare numbers as minutes (e.g. "25" = 25min)
- Add support for more duration formats: "25 minuten", "1 stunde", etc.

Language preferences stored in SessionService, default configurable via BOT_DEFAULT_LANGUAGE env var.
This commit is contained in:
Till-JS 2026-02-02 16:07:27 +01:00
parent 5c688d713e
commit c2c80efc50
17 changed files with 1626 additions and 18 deletions

View file

@ -38,6 +38,10 @@
"types": "./dist/credit/index.d.ts",
"default": "./dist/credit/index.js"
},
"./i18n": {
"types": "./dist/i18n/index.d.ts",
"default": "./dist/i18n/index.js"
},
"./nutrition": {
"types": "./dist/nutrition/index.d.ts",
"default": "./dist/nutrition/index.js"

View file

@ -0,0 +1,68 @@
import { Module, DynamicModule, Global } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { I18nService, I18N_OPTIONS } from './i18n.service';
import { I18nOptions } from './types';
import { SessionModule } from '../session/session.module';
/**
* I18n Module for Matrix Bots
*
* Provides multi-language support with per-user language preferences.
*
* @example
* ```typescript
* // Basic usage (uses SessionModule and ConfigModule)
* @Module({
* imports: [I18nModule.forRoot()],
* })
*
* // With custom default language
* @Module({
* imports: [I18nModule.forRoot({ defaultLanguage: 'en' })],
* })
* ```
*/
@Global()
@Module({})
export class I18nModule {
/**
* Register the I18n module
*/
static forRoot(options?: I18nOptions): DynamicModule {
return {
module: I18nModule,
imports: [ConfigModule, SessionModule.forRoot()],
providers: [
{
provide: I18N_OPTIONS,
useValue: options || {},
},
I18nService,
],
exports: [I18nService],
};
}
/**
* Register the I18n module with async configuration
*/
static forRootAsync(options: {
imports?: any[];
useFactory: (...args: any[]) => I18nOptions | Promise<I18nOptions>;
inject?: any[];
}): DynamicModule {
return {
module: I18nModule,
imports: [...(options.imports || []), ConfigModule, SessionModule.forRoot()],
providers: [
{
provide: I18N_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
},
I18nService,
],
exports: [I18nService],
};
}
}

View file

@ -0,0 +1,242 @@
import { Injectable, Inject, Optional, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
Language,
BotTranslations,
TodoTranslations,
CalendarTranslations,
ContactsTranslations,
ClockTranslations,
I18nOptions,
} from './types';
import { de } from './locales/de';
import { en } from './locales/en';
import { SessionService } from '../session/session.service';
/**
* Injection token for I18n options
*/
export const I18N_OPTIONS = 'I18N_OPTIONS';
/**
* Session data key for language preference
*/
const LANGUAGE_KEY = 'language';
/**
* All available translations
*/
const translations: Record<Language, BotTranslations> = { de, en };
/**
* Language display names
*/
export const LANGUAGE_NAMES: Record<Language, string> = {
de: 'Deutsch',
en: 'English',
};
/**
* I18n Service for Matrix Bots
*
* Provides multi-language support with:
* - Per-user language preference (stored in session)
* - Default language from environment variable
* - Placeholder substitution in translations
*
* @example
* ```typescript
* // Get translator for a user
* const t = await i18n.getTranslator(userId, 'todo');
*
* // Use translations
* const msg = t('taskCreated', { title: 'Buy milk' });
* // → "Aufgabe erstellt: **Buy milk**" (if user language is German)
* ```
*/
@Injectable()
export class I18nService {
private readonly logger = new Logger(I18nService.name);
private readonly defaultLanguage: Language;
constructor(
@Optional() private sessionService?: SessionService,
@Optional() private configService?: ConfigService,
@Optional() @Inject(I18N_OPTIONS) private options?: I18nOptions
) {
// Priority: options > env > config > 'de'
this.defaultLanguage =
options?.defaultLanguage ||
(process.env.BOT_DEFAULT_LANGUAGE as Language) ||
this.configService?.get<Language>('bot.defaultLanguage') ||
'de';
this.logger.log(`Default language: ${this.defaultLanguage}`);
}
/**
* Get the language for a user
*/
async getLanguage(userId: string): Promise<Language> {
if (this.sessionService) {
const lang = await this.sessionService.getSessionData<Language>(userId, LANGUAGE_KEY);
if (lang && this.isValidLanguage(lang)) {
return lang;
}
}
return this.defaultLanguage;
}
/**
* Set the language for a user
*/
async setLanguage(userId: string, language: Language): Promise<void> {
if (!this.isValidLanguage(language)) {
throw new Error(
`Invalid language: ${language}. Available: ${this.getAvailableLanguages().join(', ')}`
);
}
if (this.sessionService) {
await this.sessionService.setSessionData(userId, LANGUAGE_KEY, language);
this.logger.log(`Language set for ${userId}: ${language}`);
}
}
/**
* Check if a language code is valid
*/
isValidLanguage(lang: string): lang is Language {
return lang === 'de' || lang === 'en';
}
/**
* Get list of available languages
*/
getAvailableLanguages(): Language[] {
return ['de', 'en'];
}
/**
* Get language display name
*/
getLanguageName(lang: Language): string {
return LANGUAGE_NAMES[lang];
}
/**
* Get all translations for a language
*/
getTranslations(language: Language): BotTranslations {
return translations[language] || translations[this.defaultLanguage];
}
/**
* Get a translator function for todo bot
*/
async getTodoTranslator(
userId: string
): Promise<(key: keyof TodoTranslations, params?: Record<string, string | number>) => string> {
const lang = await this.getLanguage(userId);
const t = translations[lang].todo;
return (key, params) => this.interpolate(t[key], params);
}
/**
* Get a translator function for calendar bot
*/
async getCalendarTranslator(
userId: string
): Promise<
(key: keyof CalendarTranslations, params?: Record<string, string | number>) => string
> {
const lang = await this.getLanguage(userId);
const t = translations[lang].calendar;
return (key, params) => this.interpolate(t[key], params);
}
/**
* Get a translator function for contacts bot
*/
async getContactsTranslator(
userId: string
): Promise<
(key: keyof ContactsTranslations, params?: Record<string, string | number>) => string
> {
const lang = await this.getLanguage(userId);
const t = translations[lang].contacts;
return (key, params) => this.interpolate(t[key], params);
}
/**
* Get a translator function for clock bot
*/
async getClockTranslator(
userId: string
): Promise<(key: keyof ClockTranslations, params?: Record<string, string | number>) => string> {
const lang = await this.getLanguage(userId);
const t = translations[lang].clock;
return (key, params) => this.interpolate(t[key], params);
}
/**
* Get translations directly for a bot type
*/
async getTodoTranslations(userId: string): Promise<TodoTranslations> {
const lang = await this.getLanguage(userId);
return translations[lang].todo;
}
async getCalendarTranslations(userId: string): Promise<CalendarTranslations> {
const lang = await this.getLanguage(userId);
return translations[lang].calendar;
}
async getContactsTranslations(userId: string): Promise<ContactsTranslations> {
const lang = await this.getLanguage(userId);
return translations[lang].contacts;
}
async getClockTranslations(userId: string): Promise<ClockTranslations> {
const lang = await this.getLanguage(userId);
return translations[lang].clock;
}
/**
* Interpolate placeholders in a string
*
* @example
* interpolate('Hello {name}!', { name: 'World' })
* // → 'Hello World!'
*/
interpolate(template: string, params?: Record<string, string | number>): string {
if (!params) return template;
return template.replace(/\{(\w+)\}/g, (_, key) => {
return params[key]?.toString() ?? `{${key}}`;
});
}
/**
* Format a date according to user's language
*/
async formatDate(userId: string, date: Date | string): Promise<string> {
const lang = await this.getLanguage(userId);
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
/**
* Format a time according to user's language
*/
async formatTime(userId: string, date: Date | string): Promise<string> {
const lang = await this.getLanguage(userId);
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleTimeString(lang === 'de' ? 'de-DE' : 'en-US', {
hour: '2-digit',
minute: '2-digit',
});
}
}

View file

@ -0,0 +1,5 @@
export * from './types';
export * from './i18n.service';
export * from './i18n.module';
export { de } from './locales/de';
export { en } from './locales/en';

View file

@ -0,0 +1,390 @@
import { type BotTranslations } from '../types';
export const de: BotTranslations = {
common: {
// General
error: 'Fehler',
errorOccurred: 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.',
notLoggedIn: 'Du bist nicht angemeldet.',
loginRequired: 'Bitte melde dich zuerst an mit `!login email passwort`',
loginSuccess: 'Erfolgreich angemeldet als **{email}**',
loginFailed: 'Anmeldung fehlgeschlagen: {error}',
logoutSuccess: 'Erfolgreich abgemeldet.',
invalidCommand: 'Unbekannter Befehl: {command}',
helpHint: 'Sag "hilfe" für alle Befehle.',
// Credits
credits: 'Credits',
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
// Sync
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
// Status
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Angemeldet als: {email}',
notLoggedInStatus: 'Nicht angemeldet',
// Language
languageChanged: 'Sprache geändert zu: **{language}**',
currentLanguage: 'Aktuelle Sprache: **{language}**',
availableLanguages: 'Verfügbare Sprachen: {languages}',
// Dates
today: 'Heute',
tomorrow: 'Morgen',
dayAfterTomorrow: 'Übermorgen',
// Actions
created: 'Erstellt',
deleted: 'Gelöscht',
updated: 'Aktualisiert',
completed: 'Erledigt',
},
todo: {
// Inherit common
error: 'Fehler',
errorOccurred: 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.',
notLoggedIn: 'Du bist nicht angemeldet.',
loginRequired: 'Bitte melde dich zuerst an mit `!login email passwort`',
loginSuccess: 'Erfolgreich angemeldet als **{email}**',
loginFailed: 'Anmeldung fehlgeschlagen: {error}',
logoutSuccess: 'Erfolgreich abgemeldet.',
invalidCommand: 'Unbekannter Befehl: {command}',
helpHint: 'Sag "hilfe" für alle Befehle.',
credits: 'Credits',
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Angemeldet als: {email}',
notLoggedInStatus: 'Nicht angemeldet',
languageChanged: 'Sprache geändert zu: **{language}**',
currentLanguage: 'Aktuelle Sprache: **{language}**',
availableLanguages: 'Verfügbare Sprachen: {languages}',
today: 'Heute',
tomorrow: 'Morgen',
dayAfterTomorrow: 'Übermorgen',
created: 'Erstellt',
deleted: 'Gelöscht',
updated: 'Aktualisiert',
completed: 'Erledigt',
// Tasks
task: 'Aufgabe',
tasks: 'Aufgaben',
taskCreated: 'Aufgabe erstellt: **{title}**',
taskCompleted: 'Erledigt: ~~{title}~~',
taskDeleted: 'Gelöscht: {title}',
noTasks: 'Keine offenen Aufgaben.',
noTasksToday: 'Keine Aufgaben für heute.',
inboxEmpty: 'Inbox ist leer.',
allTasks: 'Alle offenen Aufgaben',
todayTasks: 'Aufgaben für heute',
inbox: 'Inbox (ohne Datum)',
// Projects
project: 'Projekt',
projects: 'Projekte',
noProjects: 'Keine Projekte.',
projectTasks: 'Projekt: {name}',
// Priorities
priority: 'Priorität',
date: 'Datum',
// Help
helpTitle: 'Todo Bot - Hilfe',
helpCommands: `**Befehle:**
\`!add [Aufgabe]\` - Neue Aufgabe erstellen
\`!list\` - Alle offenen Aufgaben
\`!today\` - Heutige Aufgaben
\`!inbox\` - Aufgaben ohne Datum
\`!done [Nr]\` - Aufgabe als erledigt markieren
\`!delete [Nr]\` - Aufgabe löschen
\`!projects\` - Alle Projekte
\`!project [Name]\` - Projektaufgaben anzeigen
\`!status\` - Bot-Status
\`!language [de/en]\` - Sprache ändern`,
helpSyntax: `**Syntax:**
\`!add Aufgabe !p1 @morgen #projekt\`
\`!p1-4\` - Priorität (1=höchste)
\`@heute/@morgen/@übermorgen\` - Datum
\`#projektname\` - Projekt`,
helpExamples: `**Beispiele:**
\`Einkaufen gehen\`
\`Meeting vorbereiten !p1 @morgen\`
\`Bericht schreiben #arbeit\``,
// Actions
markDone: 'Erledigen: `!done [Nr]`',
delete: 'Löschen: `!delete [Nr]`',
},
calendar: {
// Inherit common
error: 'Fehler',
errorOccurred: 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.',
notLoggedIn: 'Du bist nicht angemeldet.',
loginRequired: 'Bitte melde dich zuerst an mit `!login email passwort`',
loginSuccess: 'Erfolgreich angemeldet als **{email}**',
loginFailed: 'Anmeldung fehlgeschlagen: {error}',
logoutSuccess: 'Erfolgreich abgemeldet.',
invalidCommand: 'Unbekannter Befehl: {command}',
helpHint: 'Sag "hilfe" für alle Befehle.',
credits: 'Credits',
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Angemeldet als: {email}',
notLoggedInStatus: 'Nicht angemeldet',
languageChanged: 'Sprache geändert zu: **{language}**',
currentLanguage: 'Aktuelle Sprache: **{language}**',
availableLanguages: 'Verfügbare Sprachen: {languages}',
today: 'Heute',
tomorrow: 'Morgen',
dayAfterTomorrow: 'Übermorgen',
created: 'Erstellt',
deleted: 'Gelöscht',
updated: 'Aktualisiert',
completed: 'Erledigt',
// Events
event: 'Termin',
events: 'Termine',
eventCreated: 'Termin erstellt: **{title}**',
eventDeleted: 'Gelöscht: {title}',
noEvents: 'Keine anstehenden Termine.',
noEventsToday: 'Keine Termine für heute.',
noEventsTomorrow: 'Keine Termine für morgen.',
noEventsThisWeek: 'Keine Termine diese Woche.',
upcomingEvents: 'Anstehende Termine',
todayEvents: 'Termine heute',
tomorrowEvents: 'Termine morgen',
weekEvents: 'Termine diese Woche',
// Calendars
calendar: 'Kalender',
calendars: 'Kalender',
yourCalendars: 'Deine Kalender',
// Time
time: 'Zeit',
allDay: 'ganztägig',
location: 'Ort',
// Help
helpTitle: 'Kalender Bot - Hilfe',
helpCommands: `**Befehle:**
\`!add [Termin]\` - Neuen Termin erstellen
\`!today\` - Heutige Termine
\`!tomorrow\` - Morgige Termine
\`!week\` - Termine diese Woche
\`!events\` - Nächste 14 Tage
\`!details [Nr]\` - Termindetails
\`!delete [Nr]\` - Termin löschen
\`!calendars\` - Alle Kalender
\`!status\` - Bot-Status
\`!language [de/en]\` - Sprache ändern`,
helpSyntax: `**Syntax:**
\`Meeting morgen um 14:00\`
\`Zahnarzt am 15.02. um 10:30\`
\`Urlaub am 01.03. ganztägig\``,
helpExamples: `**Beispiele:**
\`Team Meeting morgen um 10:00\`
\`Arzt am 20.02. um 15:30\`
\`Geburtstag am 15.03. ganztägig\``,
// Parsing errors
couldNotParseDateTime: 'Konnte Datum/Uhrzeit nicht erkennen.',
pleaseProvideTitle: 'Bitte gib einen Titel für den Termin an.',
},
contacts: {
// Inherit common
error: 'Fehler',
errorOccurred: 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.',
notLoggedIn: 'Du bist nicht angemeldet.',
loginRequired: 'Bitte melde dich zuerst an mit `!login email passwort`',
loginSuccess: 'Erfolgreich angemeldet als **{email}**',
loginFailed: 'Anmeldung fehlgeschlagen: {error}',
logoutSuccess: 'Erfolgreich abgemeldet.',
invalidCommand: 'Unbekannter Befehl: {command}',
helpHint: 'Sag "hilfe" für alle Befehle.',
credits: 'Credits',
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Angemeldet als: {email}',
notLoggedInStatus: 'Nicht angemeldet',
languageChanged: 'Sprache geändert zu: **{language}**',
currentLanguage: 'Aktuelle Sprache: **{language}**',
availableLanguages: 'Verfügbare Sprachen: {languages}',
today: 'Heute',
tomorrow: 'Morgen',
dayAfterTomorrow: 'Übermorgen',
created: 'Erstellt',
deleted: 'Gelöscht',
updated: 'Aktualisiert',
completed: 'Erledigt',
// Contacts
contact: 'Kontakt',
contacts: 'Kontakte',
contactCreated: 'Kontakt **{name}** erstellt!',
contactDeleted: 'Kontakt **{name}** gelöscht.',
contactUpdated: 'Kontakt **{name}** aktualisiert!',
noContacts: 'Du hast noch keine Kontakte.',
// Favorites
favorite: 'Favorit',
favorites: 'Favoriten',
noFavorites: 'Du hast noch keine Favoriten.',
markedAsFavorite: '**{name}** als Favorit markiert ★',
removedFromFavorites: '**{name}** aus Favoriten entfernt',
// Search
search: 'Suche',
searchResults: 'Suchergebnisse für "{query}"',
noSearchResults: 'Keine Kontakte gefunden für: "{query}"',
// Fields
email: 'E-Mail',
phone: 'Telefon',
mobile: 'Mobil',
company: 'Firma',
jobTitle: 'Beruf',
address: 'Adresse',
website: 'Website',
birthday: 'Geburtstag',
notes: 'Notizen',
// Help
helpTitle: 'Contacts Bot - Hilfe',
helpCommands: `**Befehle:**
\`!contacts\` - Alle Kontakte
\`!search [text]\` - Kontakte suchen
\`!favorites\` - Favoriten anzeigen
\`!contact [Nr]\` - Kontaktdetails
\`!add Vorname Nachname\` - Neuer Kontakt
\`!edit [Nr] [feld] [wert]\` - Bearbeiten
\`!delete [Nr]\` - Kontakt löschen
\`!fav [Nr]\` - Favorit umschalten
\`!status\` - Bot-Status
\`!language [de/en]\` - Sprache ändern`,
helpFields: `**Felder:** email, phone, mobile, company, job, website, street, city, zip, country, notes, birthday`,
helpExamples: `**Beispiele:**
\`Max Mustermann\`
\`!edit 1 email max@example.com\`
\`!edit 1 phone +49 123 456789\``,
},
clock: {
// Inherit common
error: 'Fehler',
errorOccurred: 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.',
notLoggedIn: 'Du bist nicht angemeldet.',
loginRequired: 'Bitte melde dich zuerst an mit `!login email passwort`',
loginSuccess: 'Erfolgreich angemeldet als **{email}**',
loginFailed: 'Anmeldung fehlgeschlagen: {error}',
logoutSuccess: 'Erfolgreich abgemeldet.',
invalidCommand: 'Unbekannter Befehl: {command}',
helpHint: 'Sag "hilfe" für alle Befehle.',
credits: 'Credits',
creditsRemaining: '{amount} verbleibend',
insufficientCredits: 'Nicht genügend Credits. Benötigt: {required}, Verfügbar: {available}',
buyCredits: 'Credits kaufen: https://mana.how/credits',
synced: 'Synchronisiert',
localStorage: 'Lokaler Speicher',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Angemeldet als: {email}',
notLoggedInStatus: 'Nicht angemeldet',
languageChanged: 'Sprache geändert zu: **{language}**',
currentLanguage: 'Aktuelle Sprache: **{language}**',
availableLanguages: 'Verfügbare Sprachen: {languages}',
today: 'Heute',
tomorrow: 'Morgen',
dayAfterTomorrow: 'Übermorgen',
created: 'Erstellt',
deleted: 'Gelöscht',
updated: 'Aktualisiert',
completed: 'Erledigt',
// Timer
timer: 'Timer',
timerStarted: 'Timer gestartet!',
timerPaused: 'Timer pausiert',
timerResumed: 'Timer fortgesetzt',
timerReset: 'Timer zurückgesetzt.',
timerFinished: 'Timer beendet!',
noActiveTimer: 'Kein aktiver Timer.',
noPausedTimer: 'Kein pausierter Timer.',
noTimers: 'Keine Timer.',
remaining: 'Verbleibend',
duration: 'Dauer',
label: 'Label',
// Alarm
alarm: 'Alarm',
alarmSet: 'Alarm gestellt!',
alarmDeleted: 'Alarm gelöscht.',
noAlarms: 'Keine Alarme.',
yourAlarms: 'Deine Alarme',
// World Clock
worldClock: 'Weltuhr',
worldClocks: 'Weltuhren',
worldClockAdded: 'Weltuhr hinzugefügt: {city}',
noWorldClocks: 'Keine Weltuhren.',
yourWorldClocks: 'Deine Weltuhren',
// Time
currentTime: 'Aktuelle Zeit',
// Help
helpTitle: 'Clock Bot - Hilfe',
helpCommands: `**Befehle:**
\`!timer 25m\` - Timer starten
\`!stop\` - Timer pausieren
\`!resume\` - Timer fortsetzen
\`!reset\` - Timer zurücksetzen
\`!timers\` - Alle Timer
\`!alarm 07:30\` - Alarm stellen
\`!alarms\` - Alle Alarme
\`!time\` - Aktuelle Zeit
\`!worldclock Berlin\` - Weltuhr hinzufügen
\`!worldclocks\` - Alle Weltuhren
\`!status\` - Bot-Status
\`!language [de/en]\` - Sprache ändern`,
helpExamples: `**Beispiele:**
\`25\` (25 Minuten Timer)
\`1h30m\` (1,5 Stunden Timer)
\`!alarm 7 Uhr 30\``,
// Parsing errors
couldNotParseDuration: 'Konnte Zeit nicht verstehen.',
couldNotParseTime: 'Konnte Uhrzeit nicht verstehen.',
},
};

View file

@ -0,0 +1,390 @@
import { type BotTranslations } from '../types';
export const en: BotTranslations = {
common: {
// General
error: 'Error',
errorOccurred: 'An error occurred. Please try again.',
notLoggedIn: 'You are not logged in.',
loginRequired: 'Please log in first with `!login email password`',
loginSuccess: 'Successfully logged in as **{email}**',
loginFailed: 'Login failed: {error}',
logoutSuccess: 'Successfully logged out.',
invalidCommand: 'Unknown command: {command}',
helpHint: 'Say "help" for all commands.',
// Credits
credits: 'Credits',
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
// Sync
synced: 'Synced',
localStorage: 'Local storage',
// Status
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Logged in as: {email}',
notLoggedInStatus: 'Not logged in',
// Language
languageChanged: 'Language changed to: **{language}**',
currentLanguage: 'Current language: **{language}**',
availableLanguages: 'Available languages: {languages}',
// Dates
today: 'Today',
tomorrow: 'Tomorrow',
dayAfterTomorrow: 'Day after tomorrow',
// Actions
created: 'Created',
deleted: 'Deleted',
updated: 'Updated',
completed: 'Completed',
},
todo: {
// Inherit common
error: 'Error',
errorOccurred: 'An error occurred. Please try again.',
notLoggedIn: 'You are not logged in.',
loginRequired: 'Please log in first with `!login email password`',
loginSuccess: 'Successfully logged in as **{email}**',
loginFailed: 'Login failed: {error}',
logoutSuccess: 'Successfully logged out.',
invalidCommand: 'Unknown command: {command}',
helpHint: 'Say "help" for all commands.',
credits: 'Credits',
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Logged in as: {email}',
notLoggedInStatus: 'Not logged in',
languageChanged: 'Language changed to: **{language}**',
currentLanguage: 'Current language: **{language}**',
availableLanguages: 'Available languages: {languages}',
today: 'Today',
tomorrow: 'Tomorrow',
dayAfterTomorrow: 'Day after tomorrow',
created: 'Created',
deleted: 'Deleted',
updated: 'Updated',
completed: 'Completed',
// Tasks
task: 'Task',
tasks: 'Tasks',
taskCreated: 'Task created: **{title}**',
taskCompleted: 'Completed: ~~{title}~~',
taskDeleted: 'Deleted: {title}',
noTasks: 'No open tasks.',
noTasksToday: 'No tasks for today.',
inboxEmpty: 'Inbox is empty.',
allTasks: 'All open tasks',
todayTasks: 'Tasks for today',
inbox: 'Inbox (no date)',
// Projects
project: 'Project',
projects: 'Projects',
noProjects: 'No projects.',
projectTasks: 'Project: {name}',
// Priorities
priority: 'Priority',
date: 'Date',
// Help
helpTitle: 'Todo Bot - Help',
helpCommands: `**Commands:**
\`!add [task]\` - Create new task
\`!list\` - All open tasks
\`!today\` - Today's tasks
\`!inbox\` - Tasks without date
\`!done [Nr]\` - Mark task as done
\`!delete [Nr]\` - Delete task
\`!projects\` - All projects
\`!project [name]\` - Show project tasks
\`!status\` - Bot status
\`!language [de/en]\` - Change language`,
helpSyntax: `**Syntax:**
\`!add Task !p1 @tomorrow #project\`
\`!p1-4\` - Priority (1=highest)
\`@today/@tomorrow\` - Due date
\`#projectname\` - Project`,
helpExamples: `**Examples:**
\`Go shopping\`
\`Prepare meeting !p1 @tomorrow\`
\`Write report #work\``,
// Actions
markDone: 'Complete: `!done [Nr]`',
delete: 'Delete: `!delete [Nr]`',
},
calendar: {
// Inherit common
error: 'Error',
errorOccurred: 'An error occurred. Please try again.',
notLoggedIn: 'You are not logged in.',
loginRequired: 'Please log in first with `!login email password`',
loginSuccess: 'Successfully logged in as **{email}**',
loginFailed: 'Login failed: {error}',
logoutSuccess: 'Successfully logged out.',
invalidCommand: 'Unknown command: {command}',
helpHint: 'Say "help" for all commands.',
credits: 'Credits',
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Logged in as: {email}',
notLoggedInStatus: 'Not logged in',
languageChanged: 'Language changed to: **{language}**',
currentLanguage: 'Current language: **{language}**',
availableLanguages: 'Available languages: {languages}',
today: 'Today',
tomorrow: 'Tomorrow',
dayAfterTomorrow: 'Day after tomorrow',
created: 'Created',
deleted: 'Deleted',
updated: 'Updated',
completed: 'Completed',
// Events
event: 'Event',
events: 'Events',
eventCreated: 'Event created: **{title}**',
eventDeleted: 'Deleted: {title}',
noEvents: 'No upcoming events.',
noEventsToday: 'No events for today.',
noEventsTomorrow: 'No events for tomorrow.',
noEventsThisWeek: 'No events this week.',
upcomingEvents: 'Upcoming events',
todayEvents: "Today's events",
tomorrowEvents: "Tomorrow's events",
weekEvents: "This week's events",
// Calendars
calendar: 'Calendar',
calendars: 'Calendars',
yourCalendars: 'Your calendars',
// Time
time: 'Time',
allDay: 'all day',
location: 'Location',
// Help
helpTitle: 'Calendar Bot - Help',
helpCommands: `**Commands:**
\`!add [event]\` - Create new event
\`!today\` - Today's events
\`!tomorrow\` - Tomorrow's events
\`!week\` - This week's events
\`!events\` - Next 14 days
\`!details [Nr]\` - Event details
\`!delete [Nr]\` - Delete event
\`!calendars\` - All calendars
\`!status\` - Bot status
\`!language [de/en]\` - Change language`,
helpSyntax: `**Syntax:**
\`Meeting tomorrow at 2pm\`
\`Dentist on 02/15 at 10:30am\`
\`Vacation on 03/01 all day\``,
helpExamples: `**Examples:**
\`Team meeting tomorrow at 10am\`
\`Doctor on 02/20 at 3:30pm\`
\`Birthday on 03/15 all day\``,
// Parsing errors
couldNotParseDateTime: 'Could not parse date/time.',
pleaseProvideTitle: 'Please provide a title for the event.',
},
contacts: {
// Inherit common
error: 'Error',
errorOccurred: 'An error occurred. Please try again.',
notLoggedIn: 'You are not logged in.',
loginRequired: 'Please log in first with `!login email password`',
loginSuccess: 'Successfully logged in as **{email}**',
loginFailed: 'Login failed: {error}',
logoutSuccess: 'Successfully logged out.',
invalidCommand: 'Unknown command: {command}',
helpHint: 'Say "help" for all commands.',
credits: 'Credits',
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Logged in as: {email}',
notLoggedInStatus: 'Not logged in',
languageChanged: 'Language changed to: **{language}**',
currentLanguage: 'Current language: **{language}**',
availableLanguages: 'Available languages: {languages}',
today: 'Today',
tomorrow: 'Tomorrow',
dayAfterTomorrow: 'Day after tomorrow',
created: 'Created',
deleted: 'Deleted',
updated: 'Updated',
completed: 'Completed',
// Contacts
contact: 'Contact',
contacts: 'Contacts',
contactCreated: 'Contact **{name}** created!',
contactDeleted: 'Contact **{name}** deleted.',
contactUpdated: 'Contact **{name}** updated!',
noContacts: 'You have no contacts yet.',
// Favorites
favorite: 'Favorite',
favorites: 'Favorites',
noFavorites: 'You have no favorites yet.',
markedAsFavorite: '**{name}** marked as favorite ★',
removedFromFavorites: '**{name}** removed from favorites',
// Search
search: 'Search',
searchResults: 'Search results for "{query}"',
noSearchResults: 'No contacts found for: "{query}"',
// Fields
email: 'Email',
phone: 'Phone',
mobile: 'Mobile',
company: 'Company',
jobTitle: 'Job title',
address: 'Address',
website: 'Website',
birthday: 'Birthday',
notes: 'Notes',
// Help
helpTitle: 'Contacts Bot - Help',
helpCommands: `**Commands:**
\`!contacts\` - All contacts
\`!search [text]\` - Search contacts
\`!favorites\` - Show favorites
\`!contact [Nr]\` - Contact details
\`!add FirstName LastName\` - New contact
\`!edit [Nr] [field] [value]\` - Edit
\`!delete [Nr]\` - Delete contact
\`!fav [Nr]\` - Toggle favorite
\`!status\` - Bot status
\`!language [de/en]\` - Change language`,
helpFields: `**Fields:** email, phone, mobile, company, job, website, street, city, zip, country, notes, birthday`,
helpExamples: `**Examples:**
\`John Doe\`
\`!edit 1 email john@example.com\`
\`!edit 1 phone +1 123 456 7890\``,
},
clock: {
// Inherit common
error: 'Error',
errorOccurred: 'An error occurred. Please try again.',
notLoggedIn: 'You are not logged in.',
loginRequired: 'Please log in first with `!login email password`',
loginSuccess: 'Successfully logged in as **{email}**',
loginFailed: 'Login failed: {error}',
logoutSuccess: 'Successfully logged out.',
invalidCommand: 'Unknown command: {command}',
helpHint: 'Say "help" for all commands.',
credits: 'Credits',
creditsRemaining: '{amount} remaining',
insufficientCredits: 'Insufficient credits. Required: {required}, Available: {available}',
buyCredits: 'Buy credits: https://mana.how/credits',
synced: 'Synced',
localStorage: 'Local storage',
status: 'Status',
online: 'Online',
offline: 'Offline',
loggedInAs: 'Logged in as: {email}',
notLoggedInStatus: 'Not logged in',
languageChanged: 'Language changed to: **{language}**',
currentLanguage: 'Current language: **{language}**',
availableLanguages: 'Available languages: {languages}',
today: 'Today',
tomorrow: 'Tomorrow',
dayAfterTomorrow: 'Day after tomorrow',
created: 'Created',
deleted: 'Deleted',
updated: 'Updated',
completed: 'Completed',
// Timer
timer: 'Timer',
timerStarted: 'Timer started!',
timerPaused: 'Timer paused',
timerResumed: 'Timer resumed',
timerReset: 'Timer reset.',
timerFinished: 'Timer finished!',
noActiveTimer: 'No active timer.',
noPausedTimer: 'No paused timer.',
noTimers: 'No timers.',
remaining: 'Remaining',
duration: 'Duration',
label: 'Label',
// Alarm
alarm: 'Alarm',
alarmSet: 'Alarm set!',
alarmDeleted: 'Alarm deleted.',
noAlarms: 'No alarms.',
yourAlarms: 'Your alarms',
// World Clock
worldClock: 'World clock',
worldClocks: 'World clocks',
worldClockAdded: 'World clock added: {city}',
noWorldClocks: 'No world clocks.',
yourWorldClocks: 'Your world clocks',
// Time
currentTime: 'Current time',
// Help
helpTitle: 'Clock Bot - Help',
helpCommands: `**Commands:**
\`!timer 25m\` - Start timer
\`!stop\` - Pause timer
\`!resume\` - Resume timer
\`!reset\` - Reset timer
\`!timers\` - All timers
\`!alarm 07:30\` - Set alarm
\`!alarms\` - All alarms
\`!time\` - Current time
\`!worldclock Berlin\` - Add world clock
\`!worldclocks\` - All world clocks
\`!status\` - Bot status
\`!language [de/en]\` - Change language`,
helpExamples: `**Examples:**
\`25\` (25 minute timer)
\`1h30m\` (1.5 hour timer)
\`!alarm 7:30 am\``,
// Parsing errors
couldNotParseDuration: 'Could not parse duration.',
couldNotParseTime: 'Could not parse time.',
},
};

View file

@ -0,0 +1,235 @@
/**
* Supported languages
*/
export type Language = 'de' | 'en';
/**
* Common translations shared across all bots
*/
export interface CommonTranslations {
// General
error: string;
errorOccurred: string;
notLoggedIn: string;
loginRequired: string;
loginSuccess: string;
loginFailed: string;
logoutSuccess: string;
invalidCommand: string;
helpHint: string;
// Credits
credits: string;
creditsRemaining: string;
insufficientCredits: string;
buyCredits: string;
// Sync
synced: string;
localStorage: string;
// Status
status: string;
online: string;
offline: string;
loggedInAs: string;
notLoggedInStatus: string;
// Language
languageChanged: string;
currentLanguage: string;
availableLanguages: string;
// Dates
today: string;
tomorrow: string;
dayAfterTomorrow: string;
// Actions
created: string;
deleted: string;
updated: string;
completed: string;
}
/**
* Todo bot translations
*/
export interface TodoTranslations extends CommonTranslations {
// Tasks
task: string;
tasks: string;
taskCreated: string;
taskCompleted: string;
taskDeleted: string;
noTasks: string;
noTasksToday: string;
inboxEmpty: string;
allTasks: string;
todayTasks: string;
inbox: string;
// Projects
project: string;
projects: string;
noProjects: string;
projectTasks: string;
// Priorities
priority: string;
date: string;
// Help
helpTitle: string;
helpCommands: string;
helpSyntax: string;
helpExamples: string;
// Actions
markDone: string;
delete: string;
}
/**
* Calendar bot translations
*/
export interface CalendarTranslations extends CommonTranslations {
// Events
event: string;
events: string;
eventCreated: string;
eventDeleted: string;
noEvents: string;
noEventsToday: string;
noEventsTomorrow: string;
noEventsThisWeek: string;
upcomingEvents: string;
todayEvents: string;
tomorrowEvents: string;
weekEvents: string;
// Calendars
calendar: string;
calendars: string;
yourCalendars: string;
// Time
time: string;
allDay: string;
location: string;
// Help
helpTitle: string;
helpCommands: string;
helpSyntax: string;
helpExamples: string;
// Parsing errors
couldNotParseDateTime: string;
pleaseProvideTitle: string;
}
/**
* Contacts bot translations
*/
export interface ContactsTranslations extends CommonTranslations {
// Contacts
contact: string;
contacts: string;
contactCreated: string;
contactDeleted: string;
contactUpdated: string;
noContacts: string;
// Favorites
favorite: string;
favorites: string;
noFavorites: string;
markedAsFavorite: string;
removedFromFavorites: string;
// Search
search: string;
searchResults: string;
noSearchResults: string;
// Fields
email: string;
phone: string;
mobile: string;
company: string;
jobTitle: string;
address: string;
website: string;
birthday: string;
notes: string;
// Help
helpTitle: string;
helpCommands: string;
helpFields: string;
helpExamples: string;
}
/**
* Clock bot translations
*/
export interface ClockTranslations extends CommonTranslations {
// Timer
timer: string;
timerStarted: string;
timerPaused: string;
timerResumed: string;
timerReset: string;
timerFinished: string;
noActiveTimer: string;
noPausedTimer: string;
noTimers: string;
remaining: string;
duration: string;
label: string;
// Alarm
alarm: string;
alarmSet: string;
alarmDeleted: string;
noAlarms: string;
yourAlarms: string;
// World Clock
worldClock: string;
worldClocks: string;
worldClockAdded: string;
noWorldClocks: string;
yourWorldClocks: string;
// Time
currentTime: string;
// Help
helpTitle: string;
helpCommands: string;
helpExamples: string;
// Parsing errors
couldNotParseDuration: string;
couldNotParseTime: string;
}
/**
* All bot translations combined
*/
export interface BotTranslations {
common: CommonTranslations;
todo: TodoTranslations;
calendar: CalendarTranslations;
contacts: ContactsTranslations;
clock: ClockTranslations;
}
/**
* I18n service options
*/
export interface I18nOptions {
defaultLanguage?: Language;
}

View file

@ -136,6 +136,20 @@ export type {
CreditStatusMessage,
} from './credit/index.js';
// I18n (Multi-language support for Matrix bots)
export { I18nModule, I18nService, I18N_OPTIONS, LANGUAGE_NAMES } from './i18n/index.js';
export type {
Language,
I18nOptions,
BotTranslations,
CommonTranslations,
TodoTranslations,
CalendarTranslations,
ContactsTranslations,
ClockTranslations,
} from './i18n/index.js';
export { de as deTranslations, en as enTranslations } from './i18n/index.js';
// ===== Placeholder Services (to be implemented) =====
export { NutritionModule } from './nutrition/index.js';

View file

@ -7,6 +7,7 @@ import {
SessionModule,
CreditModule,
CalendarApiService,
I18nModule,
} from '@manacore/bot-services';
// Factory provider for CalendarApiService
@ -28,6 +29,7 @@ const calendarApiServiceProvider = {
}),
SessionModule.forRoot({ storageMode: 'redis' }),
CreditModule.forRoot(),
I18nModule.forRoot(),
],
providers: [MatrixService, calendarApiServiceProvider],
exports: [MatrixService],

View file

@ -13,6 +13,9 @@ import {
CreditService,
CalendarApiService,
CalendarEvent as ApiCalendarEvent,
I18nService,
Language,
LANGUAGE_NAMES,
} from '@manacore/bot-services';
import { CalendarService, CalendarEvent } from '../calendar/calendar.service';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
@ -43,7 +46,8 @@ export class MatrixService extends BaseMatrixService {
private calendarService: CalendarService,
private calendarApiService: CalendarApiService,
private sessionService: SessionService,
private creditService: CreditService
private creditService: CreditService,
private i18nService: I18nService
) {
super(configService);
}
@ -132,6 +136,9 @@ export class MatrixService extends BaseMatrixService {
await this.executeCommand(roomId, event, sender, keywordCommand, '');
return;
}
// Fallback: treat any message as an event
await this.handleCreateEvent(roomId, event, sender, message);
}
private async executeCommand(
@ -207,6 +214,12 @@ export class MatrixService extends BaseMatrixService {
await this.handleLogout(roomId, event, userId);
break;
case 'language':
case 'sprache':
case 'lang':
await this.handleLanguage(roomId, event, userId, args);
break;
default:
// Unknown command - ignore silently
break;
@ -695,4 +708,47 @@ export class MatrixService extends BaseMatrixService {
this.logger.error(`Failed to send welcome message: ${error}`);
}
}
private async handleLanguage(
roomId: string,
event: MatrixRoomEvent,
userId: string,
args: string
) {
const lang = args.trim().toLowerCase();
if (!lang) {
const currentLang = await this.i18nService.getLanguage(userId);
const langName = LANGUAGE_NAMES[currentLang];
const available = this.i18nService
.getAvailableLanguages()
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
.join(', ');
await this.sendReply(
roomId,
event,
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
);
return;
}
if (!this.i18nService.isValidLanguage(lang)) {
const available = this.i18nService.getAvailableLanguages().join(', ');
await this.sendReply(
roomId,
event,
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
);
return;
}
await this.i18nService.setLanguage(userId, lang as Language);
const langName = LANGUAGE_NAMES[lang as Language];
if (lang === 'de') {
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
} else {
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
}
}
}

View file

@ -1,10 +1,21 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { ClockModule } from '../clock/clock.module';
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
import {
TranscriptionModule,
SessionModule,
CreditModule,
I18nModule,
} from '@manacore/bot-services';
@Module({
imports: [ClockModule, TranscriptionModule.forRoot(), SessionModule.forRoot(), CreditModule.forRoot()],
imports: [
ClockModule,
TranscriptionModule.forRoot(),
SessionModule.forRoot(),
CreditModule.forRoot(),
I18nModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],
})

View file

@ -8,7 +8,14 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ClockService } from '../clock/clock.service';
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
import {
TranscriptionService,
SessionService,
CreditService,
I18nService,
Language,
LANGUAGE_NAMES,
} from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration';
@Injectable()
@ -30,7 +37,8 @@ export class MatrixService extends BaseMatrixService {
private clockService: ClockService,
private transcriptionService: TranscriptionService,
private sessionService: SessionService,
private creditService: CreditService
private creditService: CreditService,
private i18nService: I18nService
) {
super(configService);
}
@ -218,6 +226,12 @@ export class MatrixService extends BaseMatrixService {
await this.handleWorldClocksCommand(roomId, event, userId);
break;
case 'language':
case 'sprache':
case 'lang':
await this.handleLanguage(roomId, event, userId, args);
break;
default:
// Silently ignore unknown commands
break;
@ -676,7 +690,12 @@ export class MatrixService extends BaseMatrixService {
}
}
// No match - don't respond to random messages
// Fallback: try to parse any message as a timer duration
const duration = this.clockService.parseDuration(text);
if (duration) {
await this.handleTimerCommand(roomId, event, userId, text);
return;
}
}
private async getToken(userId: string): Promise<string | null> {
@ -691,4 +710,47 @@ export class MatrixService extends BaseMatrixService {
// Entwicklungs-Fallback
return this.demoToken || null;
}
private async handleLanguage(
roomId: string,
event: MatrixRoomEvent,
userId: string,
args: string
) {
const lang = args.trim().toLowerCase();
if (!lang) {
const currentLang = await this.i18nService.getLanguage(userId);
const langName = LANGUAGE_NAMES[currentLang];
const available = this.i18nService
.getAvailableLanguages()
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
.join(', ');
await this.sendReply(
roomId,
event,
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
);
return;
}
if (!this.i18nService.isValidLanguage(lang)) {
const available = this.i18nService.getAvailableLanguages().join(', ');
await this.sendReply(
roomId,
event,
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
);
return;
}
await this.i18nService.setLanguage(userId, lang as Language);
const langName = LANGUAGE_NAMES[lang as Language];
if (lang === 'de') {
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
} else {
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
}
}
}

View file

@ -186,27 +186,27 @@ export class ClockService {
parseDuration(input: string): number | null {
let totalSeconds = 0;
// Match hours
const hoursMatch = input.match(/(\d+)\s*h/i);
// Match hours: 1h, 1 h, 1 stunde, 1 stunden, 1 hour, 1 hours
const hoursMatch = input.match(/(\d+)\s*(?:h|stunde[n]?|hour[s]?)\b/i);
if (hoursMatch) {
totalSeconds += parseInt(hoursMatch[1], 10) * 3600;
}
// Match minutes
const minutesMatch = input.match(/(\d+)\s*m(?:in)?/i);
// Match minutes: 25m, 25 m, 25min, 25 min, 25 minuten, 25 minute, 25 minutes
const minutesMatch = input.match(/(\d+)\s*(?:m|min|minute[n]?|minutes?)\b/i);
if (minutesMatch) {
totalSeconds += parseInt(minutesMatch[1], 10) * 60;
}
// Match seconds
const secondsMatch = input.match(/(\d+)\s*s(?:ec)?/i);
// Match seconds: 30s, 30 s, 30sec, 30 sec, 30 sekunden, 30 seconds
const secondsMatch = input.match(/(\d+)\s*(?:s|sec|sekunde[n]?|seconds?)\b/i);
if (secondsMatch) {
totalSeconds += parseInt(secondsMatch[1], 10);
}
// If just a number, assume minutes
// If just a number (with optional whitespace), assume minutes
if (totalSeconds === 0) {
const justNumber = input.match(/^(\d+)$/);
const justNumber = input.trim().match(/^(\d+)$/);
if (justNumber) {
totalSeconds = parseInt(justNumber[1], 10) * 60;
}

View file

@ -1,7 +1,12 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { ContactsModule } from '../contacts/contacts.module';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
import {
SessionModule,
TranscriptionModule,
CreditModule,
I18nModule,
} from '@manacore/bot-services';
@Module({
imports: [
@ -11,6 +16,7 @@ import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
I18nModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,7 +9,14 @@ import {
UserListMapper,
} from '@manacore/matrix-bot-common';
import { ContactsService, Contact } from '../contacts/contacts.service';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import {
SessionService,
TranscriptionService,
CreditService,
I18nService,
Language,
LANGUAGE_NAMES,
} from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
const CONTACT_CREATE_CREDITS = 0.02;
@ -32,7 +39,8 @@ export class MatrixService extends BaseMatrixService {
private readonly transcriptionService: TranscriptionService,
private contactsService: ContactsService,
private sessionService: SessionService,
private creditService: CreditService
private creditService: CreditService,
private i18nService: I18nService
) {
super(configService);
}
@ -102,6 +110,10 @@ Sag "hilfe" fur alle Befehle!`;
await this.handleCommand(roomId, event, sender, `!${detectedCommand}`);
return;
}
// Fallback: treat any message as a new contact
const args = message.trim().split(/\s+/);
await this.handleCreateContact(roomId, event, sender, args);
}
private async handleCommand(
@ -188,6 +200,12 @@ Sag "hilfe" fur alle Befehle!`;
await this.pinHelpMessage(roomId, event);
break;
case 'language':
case 'sprache':
case 'lang':
await this.handleLanguage(roomId, event, sender, argString);
break;
default:
await this.sendReply(
roomId,
@ -766,4 +784,47 @@ Sag "hilfe" fur alle Befehle!`;
await this.sendReply(roomId, event, 'Fehler beim Pinnen der Hilfe.');
}
}
private async handleLanguage(
roomId: string,
event: MatrixRoomEvent,
userId: string,
args: string
) {
const lang = args.trim().toLowerCase();
if (!lang) {
const currentLang = await this.i18nService.getLanguage(userId);
const langName = LANGUAGE_NAMES[currentLang];
const available = this.i18nService
.getAvailableLanguages()
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
.join(', ');
await this.sendReply(
roomId,
event,
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
);
return;
}
if (!this.i18nService.isValidLanguage(lang)) {
const available = this.i18nService.getAvailableLanguages().join(', ');
await this.sendReply(
roomId,
event,
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
);
return;
}
await this.i18nService.setLanguage(userId, lang as Language);
const langName = LANGUAGE_NAMES[lang as Language];
if (lang === 'de') {
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
} else {
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
}
}
}

View file

@ -7,6 +7,7 @@ import {
SessionModule,
CreditModule,
TodoApiService,
I18nModule,
} from '@manacore/bot-services';
// Factory provider for TodoApiService
@ -26,6 +27,7 @@ const todoApiServiceProvider = {
TranscriptionModule.forRoot(),
SessionModule.forRoot({ storageMode: 'redis' }),
CreditModule.forRoot(),
I18nModule.forRoot(),
],
providers: [MatrixService, todoApiServiceProvider],
exports: [MatrixService],

View file

@ -14,6 +14,9 @@ import {
CreditService,
TodoApiService,
Task as ApiTask,
I18nService,
Language,
LANGUAGE_NAMES,
} from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
@ -44,7 +47,8 @@ export class MatrixService extends BaseMatrixService {
private todoApiService: TodoApiService,
private transcriptionService: TranscriptionService,
private sessionService: SessionService,
private creditService: CreditService
private creditService: CreditService,
private i18nService: I18nService
) {
super(configService);
}
@ -108,7 +112,11 @@ export class MatrixService extends BaseMatrixService {
if (body.startsWith('!')) {
const [command, ...args] = body.slice(1).split(' ');
await this.executeCommand(roomId, event, userId, command.toLowerCase(), args.join(' '));
return;
}
// Fallback: treat any message as a task
await this.handleAddTask(roomId, event, userId, body);
} catch (error) {
this.logger.error(`Error handling message: ${error}`);
await this.sendReply(roomId, event, 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
@ -299,12 +307,64 @@ export class MatrixService extends BaseMatrixService {
await this.handlePinHelp(roomId, event);
break;
case 'language':
case 'sprache':
case 'lang':
await this.handleLanguage(roomId, event, userId, args);
break;
default:
// Unknown command - ignore silently or send help
break;
}
}
private async handleLanguage(
roomId: string,
event: MatrixRoomEvent,
userId: string,
args: string
) {
const lang = args.trim().toLowerCase();
// Show current language if no argument
if (!lang) {
const currentLang = await this.i18nService.getLanguage(userId);
const langName = LANGUAGE_NAMES[currentLang];
const available = this.i18nService
.getAvailableLanguages()
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
.join(', ');
await this.sendReply(
roomId,
event,
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
);
return;
}
// Validate and set language
if (!this.i18nService.isValidLanguage(lang)) {
const available = this.i18nService.getAvailableLanguages().join(', ');
await this.sendReply(
roomId,
event,
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
);
return;
}
await this.i18nService.setLanguage(userId, lang as Language);
const langName = LANGUAGE_NAMES[lang as Language];
// Respond in the new language
if (lang === 'de') {
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
} else {
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
}
}
private async handleAddTask(
roomId: string,
event: MatrixRoomEvent,