chore: update .gitignore and enhance backend and mobile app functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
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:
222
scripts/generate-mobile-i18n.js
Normal file
222
scripts/generate-mobile-i18n.js
Normal 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();
|
||||
Reference in New Issue
Block a user