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';