All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
- Added a new button for quick creation of training days in the DiaryView, improving user experience. - Implemented logic to find the next available training slot across groups and create a training day entry. - Enhanced localization by adding new keys for quick create messages in multiple languages, ensuring better accessibility for users. - Updated the DiaryManager to handle quick create operations and clear errors effectively.
224 lines
7.4 KiB
JavaScript
224 lines
7.4 KiB
JavaScript
#!/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/tsschulz/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',
|
|
'tsschulz',
|
|
'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.tsschulz.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();
|