chore: update .gitignore and enhance backend and mobile app functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s

- Added mobile app build directories and configuration files to .gitignore for cleaner repository management.
- Improved error handling in diaryMemberController by requiring diaryDateId and memberId query parameters.
- Refactored DiaryMemberService to log tag IDs instead of raw values for better debugging.
- Enhanced TournamentParticipantsTab and TournamentTab components with improved touch-action properties for better user experience.
- Updated mobile app's gradle.properties and build.gradle.kts for compatibility with AGP 9.x and Kotlin 2.1.21, including new dependencies for Coil and UCrop.
- Refactored MainApplication to simplify initialization and improved MainActivity to handle dependencies more robustly.
- Updated various UI components in the mobile app to enhance layout and functionality, including MemberDetailScreen and MemberEditScreen.
This commit is contained in:
Torsten Schulz (local)
2026-05-12 23:14:31 +02:00
parent 27f8af559b
commit 48f71b9df1
138 changed files with 54488 additions and 56 deletions

View File

@@ -0,0 +1,222 @@
#!/usr/bin/env node
/**
* Generates i18n maps for the native mobile app from the web source of truth.
*
* Source of truth: `frontend/src/i18n/locales/*.json`
*
* Output:
* - `mobile-app/shared/src/commonMain/kotlin/de/tt_tagebuch/shared/i18n/MobileStrings.kt`
*
* Notes:
* - German (`de`) is the canonical key set.
* - Missing locale keys fall back to German at generation time so every mobile locale is complete.
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const SOURCE_DIR = path.join(ROOT, 'frontend', 'src', 'i18n', 'locales');
const OUT = path.join(
ROOT,
'mobile-app',
'shared',
'src',
'commonMain',
'kotlin',
'de',
'tt_tagebuch',
'shared',
'i18n',
'MobileStrings.kt'
);
const LOCALE_LABELS = {
'de': 'Deutsch',
'de-CH': 'Deutsch (Schweiz)',
'de-extended': 'Deutsch (erweitert)',
'en-US': 'English (US)',
'en-GB': 'English (UK)',
'en-AU': 'English (AU)',
'es': 'Español',
'fr': 'Français',
'it': 'Italiano',
'pl': 'Polski',
'ja': '日本語',
'th': 'ไทย',
'tl': 'Tagalog',
'fil': 'Filipino',
'zh': '中文',
};
const MOBILE_STRINGS = {
de: {
'mobile.add': 'Hinzufügen',
'mobile.appLoading': 'App wird geladen',
'mobile.cancel': 'Abbrechen',
'mobile.clubRequest': 'Zugriff anfragen',
'mobile.clubRequested': 'Angefragt',
'mobile.createDiaryEntry': 'Eintrag erstellen',
'mobile.deleteNote': 'Notiz löschen',
'mobile.deleteTag': 'Tag entfernen',
'mobile.editTimes': 'Zeiten bearbeiten',
'mobile.emailAndPasswordRequired': 'E-Mail und Passwort sind erforderlich',
'mobile.entry': 'Eintrag',
'mobile.language': 'Sprache',
'mobile.lastTraining': 'Letztes Training',
'mobile.loginInProgress': 'Anmelden...',
'mobile.loginFailed': 'Login fehlgeschlagen',
'mobile.more': 'Mehr',
'mobile.new': 'Neu',
'mobile.newNote': 'Neue Notiz',
'mobile.newTag': 'Neuer Tag',
'mobile.noDiaryEntries': 'Noch keine Tagebuch-Einträge',
'mobile.noMembers': 'Keine Mitglieder gefunden',
'mobile.noResults': 'Keine Treffer',
'mobile.noTimes': 'Keine Zeiten',
'mobile.participationTop': 'Top Teilnahmen',
'mobile.refresh': 'Aktualisieren',
'mobile.requestedAccess': 'Angefragt',
'mobile.role': 'Rolle',
'mobile.saveEntry': 'Speichern',
'mobile.search': 'Suche',
'mobile.select': 'Auswählen',
'mobile.sessionCheck': 'Session prüfen',
'mobile.sessionCheckFailed': 'Session check fehlgeschlagen',
'mobile.sessionInvalid': 'Session ungültig',
'mobile.sessionValid': 'Session gültig',
'mobile.status': 'Status',
'mobile.active': 'Aktiv',
'mobile.inactive': 'Inaktiv',
'mobile.user': 'Benutzer',
},
en: {
'mobile.add': 'Add',
'mobile.appLoading': 'Loading app',
'mobile.cancel': 'Cancel',
'mobile.clubRequest': 'Request access',
'mobile.clubRequested': 'Requested',
'mobile.createDiaryEntry': 'Create entry',
'mobile.deleteNote': 'Delete note',
'mobile.deleteTag': 'Remove tag',
'mobile.editTimes': 'Edit times',
'mobile.emailAndPasswordRequired': 'Email and password are required',
'mobile.entry': 'Entry',
'mobile.language': 'Language',
'mobile.lastTraining': 'Last training',
'mobile.loginInProgress': 'Signing in...',
'mobile.loginFailed': 'Login failed',
'mobile.more': 'More',
'mobile.new': 'New',
'mobile.newNote': 'New note',
'mobile.newTag': 'New tag',
'mobile.noDiaryEntries': 'No diary entries yet',
'mobile.noMembers': 'No members found',
'mobile.noResults': 'No results',
'mobile.noTimes': 'No times',
'mobile.participationTop': 'Top attendance',
'mobile.refresh': 'Refresh',
'mobile.requestedAccess': 'Requested',
'mobile.role': 'Role',
'mobile.saveEntry': 'Save',
'mobile.search': 'Search',
'mobile.select': 'Select',
'mobile.sessionCheck': 'Check session',
'mobile.sessionCheckFailed': 'Session check failed',
'mobile.sessionInvalid': 'Session invalid',
'mobile.sessionValid': 'Session valid',
'mobile.status': 'Status',
'mobile.active': 'Active',
'mobile.inactive': 'Inactive',
'mobile.user': 'User',
},
};
function flatten(obj, prefix = '', out = {}) {
for (const [key, value] of Object.entries(obj || {})) {
const nextKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
flatten(value, nextKey, out);
} else if (typeof value === 'string') {
out[nextKey] = value;
}
}
return out;
}
function escapeKotlinString(value) {
return value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n');
}
function kotlinIdentifier(code) {
return code.replace(/[^A-Za-z0-9_]/g, '_');
}
function main() {
const baseJson = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'de.json'), 'utf8'));
const baseFlat = {
...flatten(baseJson),
...MOBILE_STRINGS.de,
};
const localeFiles = fs
.readdirSync(SOURCE_DIR)
.filter((file) => file.endsWith('.json') && !file.endsWith('.backup'))
.sort((a, b) => a.localeCompare(b));
const lines = [];
lines.push('package de.tt_tagebuch.shared.i18n');
lines.push('');
lines.push('data class SupportedLanguage(val code: String, val label: String)');
lines.push('');
lines.push('object MobileStrings {');
lines.push(' const val DEFAULT_LANGUAGE = "de"');
lines.push('');
lines.push(' val supportedLanguages: List<SupportedLanguage> = listOf(');
for (const file of localeFiles) {
const code = path.basename(file, '.json');
lines.push(` SupportedLanguage("${escapeKotlinString(code)}", "${escapeKotlinString(LOCALE_LABELS[code] || code)}"),`);
}
lines.push(' )');
lines.push('');
const baseEntries = Object.entries(baseFlat).sort(([a], [b]) => a.localeCompare(b));
for (const file of localeFiles) {
const code = path.basename(file, '.json');
const localeJson = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, file), 'utf8'));
const specificMobileStrings = MOBILE_STRINGS[code] || (code.startsWith('en-') ? MOBILE_STRINGS.en : {});
const flat = {
...baseFlat,
...flatten(localeJson),
...specificMobileStrings,
};
lines.push(` private val ${kotlinIdentifier(code)}: Map<String, String> by lazy { mapOf(`);
for (const [key, value] of Object.entries(flat).sort(([a], [b]) => a.localeCompare(b))) {
lines.push(` "${escapeKotlinString(key)}" to "${escapeKotlinString(value)}",`);
}
lines.push(' ) }');
lines.push('');
}
lines.push(` private val fallback: Map<String, String> get() = ${kotlinIdentifier('de')}`);
lines.push('');
lines.push(' fun get(languageCode: String, key: String, fallbackValue: String): String =');
lines.push(' mapFor(languageCode)[key] ?: fallback[key] ?: fallbackValue');
lines.push('');
lines.push(' private fun mapFor(languageCode: String): Map<String, String> = when (languageCode) {');
for (const file of localeFiles) {
const code = path.basename(file, '.json');
lines.push(` "${escapeKotlinString(code)}" -> ${kotlinIdentifier(code)}`);
}
lines.push(' else -> fallback');
lines.push(' }');
lines.push('}');
lines.push('');
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, lines.join('\n'), 'utf8');
console.log(`Wrote ${OUT} (${localeFiles.length} locales, ${baseEntries.length} keys each)`);
}
main();