diff --git a/packages/bot-services/package.json b/packages/bot-services/package.json index 36fdd806a..4f09e860f 100644 --- a/packages/bot-services/package.json +++ b/packages/bot-services/package.json @@ -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" diff --git a/packages/bot-services/src/i18n/i18n.module.ts b/packages/bot-services/src/i18n/i18n.module.ts new file mode 100644 index 000000000..c38d7ac2c --- /dev/null +++ b/packages/bot-services/src/i18n/i18n.module.ts @@ -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; + 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], + }; + } +} diff --git a/packages/bot-services/src/i18n/i18n.service.ts b/packages/bot-services/src/i18n/i18n.service.ts new file mode 100644 index 000000000..240c86db8 --- /dev/null +++ b/packages/bot-services/src/i18n/i18n.service.ts @@ -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 = { de, en }; + +/** + * Language display names + */ +export const LANGUAGE_NAMES: Record = { + 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('bot.defaultLanguage') || + 'de'; + + this.logger.log(`Default language: ${this.defaultLanguage}`); + } + + /** + * Get the language for a user + */ + async getLanguage(userId: string): Promise { + if (this.sessionService) { + const lang = await this.sessionService.getSessionData(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 { + 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> { + 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 + > { + 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 + > { + 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> { + 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 { + const lang = await this.getLanguage(userId); + return translations[lang].todo; + } + + async getCalendarTranslations(userId: string): Promise { + const lang = await this.getLanguage(userId); + return translations[lang].calendar; + } + + async getContactsTranslations(userId: string): Promise { + const lang = await this.getLanguage(userId); + return translations[lang].contacts; + } + + async getClockTranslations(userId: string): Promise { + 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 { + 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 { + 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 { + 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', + }); + } +} diff --git a/packages/bot-services/src/i18n/index.ts b/packages/bot-services/src/i18n/index.ts new file mode 100644 index 000000000..d437077a9 --- /dev/null +++ b/packages/bot-services/src/i18n/index.ts @@ -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'; diff --git a/packages/bot-services/src/i18n/locales/de.ts b/packages/bot-services/src/i18n/locales/de.ts new file mode 100644 index 000000000..181c10055 --- /dev/null +++ b/packages/bot-services/src/i18n/locales/de.ts @@ -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.', + }, +}; diff --git a/packages/bot-services/src/i18n/locales/en.ts b/packages/bot-services/src/i18n/locales/en.ts new file mode 100644 index 000000000..8a453e404 --- /dev/null +++ b/packages/bot-services/src/i18n/locales/en.ts @@ -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.', + }, +}; diff --git a/packages/bot-services/src/i18n/types.ts b/packages/bot-services/src/i18n/types.ts new file mode 100644 index 000000000..18775f94d --- /dev/null +++ b/packages/bot-services/src/i18n/types.ts @@ -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; +} diff --git a/packages/bot-services/src/index.ts b/packages/bot-services/src/index.ts index ff1930bc1..deaac67c8 100644 --- a/packages/bot-services/src/index.ts +++ b/packages/bot-services/src/index.ts @@ -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'; diff --git a/services/matrix-calendar-bot/src/bot/bot.module.ts b/services/matrix-calendar-bot/src/bot/bot.module.ts index d548c6247..95db4fedb 100644 --- a/services/matrix-calendar-bot/src/bot/bot.module.ts +++ b/services/matrix-calendar-bot/src/bot/bot.module.ts @@ -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], diff --git a/services/matrix-calendar-bot/src/bot/matrix.service.ts b/services/matrix-calendar-bot/src/bot/matrix.service.ts index 556c9e45d..433380a74 100644 --- a/services/matrix-calendar-bot/src/bot/matrix.service.ts +++ b/services/matrix-calendar-bot/src/bot/matrix.service.ts @@ -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}**`); + } + } } diff --git a/services/matrix-clock-bot/src/bot/bot.module.ts b/services/matrix-clock-bot/src/bot/bot.module.ts index 0dda1fc31..98951d8de 100644 --- a/services/matrix-clock-bot/src/bot/bot.module.ts +++ b/services/matrix-clock-bot/src/bot/bot.module.ts @@ -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], }) diff --git a/services/matrix-clock-bot/src/bot/matrix.service.ts b/services/matrix-clock-bot/src/bot/matrix.service.ts index 9a4c1075c..d1fd1cd77 100644 --- a/services/matrix-clock-bot/src/bot/matrix.service.ts +++ b/services/matrix-clock-bot/src/bot/matrix.service.ts @@ -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 { @@ -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}**`); + } + } } diff --git a/services/matrix-clock-bot/src/clock/clock.service.ts b/services/matrix-clock-bot/src/clock/clock.service.ts index b278c70ab..13a90ebb9 100644 --- a/services/matrix-clock-bot/src/clock/clock.service.ts +++ b/services/matrix-clock-bot/src/clock/clock.service.ts @@ -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; } diff --git a/services/matrix-contacts-bot/src/bot/bot.module.ts b/services/matrix-contacts-bot/src/bot/bot.module.ts index cb8f49692..d4a602d03 100644 --- a/services/matrix-contacts-bot/src/bot/bot.module.ts +++ b/services/matrix-contacts-bot/src/bot/bot.module.ts @@ -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], diff --git a/services/matrix-contacts-bot/src/bot/matrix.service.ts b/services/matrix-contacts-bot/src/bot/matrix.service.ts index 56d0ea8e5..3295286db 100644 --- a/services/matrix-contacts-bot/src/bot/matrix.service.ts +++ b/services/matrix-contacts-bot/src/bot/matrix.service.ts @@ -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}**`); + } + } } diff --git a/services/matrix-todo-bot/src/bot/bot.module.ts b/services/matrix-todo-bot/src/bot/bot.module.ts index b2b0247da..0c1866ebf 100644 --- a/services/matrix-todo-bot/src/bot/bot.module.ts +++ b/services/matrix-todo-bot/src/bot/bot.module.ts @@ -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], diff --git a/services/matrix-todo-bot/src/bot/matrix.service.ts b/services/matrix-todo-bot/src/bot/matrix.service.ts index c2a156d60..0ac117ef8 100644 --- a/services/matrix-todo-bot/src/bot/matrix.service.ts +++ b/services/matrix-todo-bot/src/bot/matrix.service.ts @@ -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,