From 9338c5d0881db586d74afc9d6d46c451d66316d5 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 21:14:32 +0100 Subject: [PATCH 01/24] Add multilingual README support - Add language code extraction from BCP47 format - Implement automatic language detection based on hass.language - Add backend support detection with intelligent caching - Update repository dashboard and download dialog to pass language - Add automatic reload when language changes - Fully backward compatible with graceful degradation - Add comprehensive documentation and testing guides Related backend PR: https://github.com/hacs/integration/pull/4964 --- BACKEND_IMPLEMENTATION_GUIDE.md | 381 +++++++++ HACS_MULTILINGUAL_FEATURE_REQUEST.md | 163 ++++ PULL_REQUEST.md | 192 +++++ TESTING_MULTILINGUAL_README.md | 350 ++++++++ .../dialogs/hacs-download-dialog.ts | 777 +++++++++--------- src/dashboards/hacs-repository-dashboard.ts | 764 ++++++++--------- src/data/repository.ts | 264 ++++-- 7 files changed, 2047 insertions(+), 844 deletions(-) create mode 100644 BACKEND_IMPLEMENTATION_GUIDE.md create mode 100644 HACS_MULTILINGUAL_FEATURE_REQUEST.md create mode 100644 PULL_REQUEST.md create mode 100644 TESTING_MULTILINGUAL_README.md diff --git a/BACKEND_IMPLEMENTATION_GUIDE.md b/BACKEND_IMPLEMENTATION_GUIDE.md new file mode 100644 index 000000000..5b6052642 --- /dev/null +++ b/BACKEND_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,381 @@ +# HACS Backend: Mehrsprachige README-Unterstützung - Implementierungsanleitung + +## Übersicht + +Diese Dokumentation beschreibt, wie das HACS Backend erweitert werden muss, um mehrsprachige README-Dateien zu unterstützen. Das Frontend sendet bereits einen optionalen `language`-Parameter im Websocket-Request `hacs/repository/info`. + +## Backend-Repository + +**Repository:** https://github.com/hacs/integration + +## Frontend-Implementierung (bereits fertig) + +Das Frontend sendet den `language`-Parameter im folgenden Format: + +```typescript +{ + type: "hacs/repository/info", + repository_id: "123456789", + language: "de" // Optional: Basis-Sprachcode (z.B. "de", "en", "fr") +} +``` + +**Wichtige Details:** +- Der Parameter ist **optional** und **backward-kompatibel** +- Format: Basis-Sprachcode (z.B. "de" aus "de-DE", "en" aus "en-US") +- Wird nur gesendet, wenn die Sprache nicht Englisch ist (Englisch verwendet README.md) +- Das Frontend hat automatische Fehlerbehandlung: Wenn das Backend den Parameter ablehnt, wird die Anfrage ohne Parameter wiederholt + +## Backend-Implementierung + +### 1. Websocket-Handler anpassen + +**Datei:** `hacs/websocket/repository/info.py` (oder ähnlich) + +**Aktueller Code (Beispiel):** +```python +@websocket_command( + { + vol.Required("type"): "hacs/repository/info", + vol.Required("repository_id"): str, + } +) +async def repository_info(hass, connection, msg): + """Get repository information.""" + repository_id = msg["repository_id"] + # ... Repository-Info abrufen ... + return repository_info +``` + +**Neuer Code:** +```python +@websocket_command( + { + vol.Required("type"): "hacs/repository/info", + vol.Required("repository_id"): str, + vol.Optional("language"): str, # Neuer optionaler Parameter + } +) +async def repository_info(hass, connection, msg): + """Get repository information.""" + repository_id = msg["repository_id"] + language = msg.get("language") # Optional: Sprachcode (z.B. "de", "en", "fr") + + # ... Repository-Info abrufen ... + + # README mit Sprachunterstützung laden + readme_content = await get_repository_readme(repository, language) + + repository_info["additional_info"] = readme_content + return repository_info +``` + +### 2. README-Lade-Funktion implementieren + +**Neue Funktion erstellen oder bestehende erweitern:** + +```python +async def get_repository_readme(repository, language: str | None = None) -> str: + """ + Lade README-Datei mit Sprachunterstützung. + + Args: + repository: Repository-Objekt + language: Optionaler Sprachcode (z.B. "de", "en", "fr") + + Returns: + README-Inhalt als String + """ + # Wenn keine Sprache angegeben oder Englisch, verwende Standard-README + if not language or language == "en": + readme_path = "README.md" + else: + # Versuche sprachspezifische README zu laden + readme_path = f"README.{language}.md" + + try: + # Lade README vom Repository + readme_content = await repository.get_file_contents(readme_path) + return readme_content + except FileNotFoundError: + # Falls sprachspezifische README nicht existiert, verwende Standard-README + if readme_path != "README.md": + try: + readme_content = await repository.get_file_contents("README.md") + return readme_content + except FileNotFoundError: + return "" + return "" + except Exception as e: + # Log Fehler und verwende Standard-README als Fallback + logger.warning(f"Fehler beim Laden von {readme_path}: {e}") + if readme_path != "README.md": + try: + readme_content = await repository.get_file_contents("README.md") + return readme_content + except FileNotFoundError: + return "" + return "" +``` + +### 3. Vollständiges Beispiel + +Hier ist ein vollständiges Beispiel, wie die Implementierung aussehen könnte: + +```python +import voluptuous as vol +from homeassistant.components import websocket_api +from hacs.helpers.functions.logger import getLogger + +logger = getLogger() + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/info", + vol.Required("repository_id"): str, + vol.Optional("language"): str, # Neuer optionaler Parameter + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def handle_repository_info(hass, connection, msg): + """Handle repository info websocket command.""" + repository_id = msg["repository_id"] + language = msg.get("language") # Optional: Sprachcode + + hacs = get_hacs() + + try: + repository = hacs.repositories.get_by_id(repository_id) + if not repository: + connection.send_error( + msg["id"], + "repository_not_found", + f"Repository with ID {repository_id} not found", + ) + return + + # Repository-Informationen abrufen + repository_info = { + "id": repository.data.id, + "name": repository.data.name, + "full_name": repository.data.full_name, + # ... weitere Felder ... + } + + # README mit Sprachunterstützung laden + readme_content = await get_repository_readme(repository, language) + repository_info["additional_info"] = readme_content + + connection.send_result(msg["id"], repository_info) + + except Exception as e: + logger.error(f"Error getting repository info: {e}") + connection.send_error( + msg["id"], + "error", + str(e), + ) + + +async def get_repository_readme(repository, language: str | None = None) -> str: + """ + Lade README-Datei mit Sprachunterstützung. + + Unterstützte Dateien: + - README.md (Standard, wird immer verwendet wenn keine Sprache oder "en") + - README.de.md (Deutsch) + - README.fr.md (Französisch) + - README.es.md (Spanisch) + - etc. + + Args: + repository: Repository-Objekt + language: Optionaler Sprachcode (z.B. "de", "en", "fr") + + Returns: + README-Inhalt als String + """ + # Wenn keine Sprache angegeben oder Englisch, verwende Standard-README + if not language or language == "en": + readme_path = "README.md" + else: + # Versuche sprachspezifische README zu laden + readme_path = f"README.{language}.md" + + try: + # Lade README vom Repository + # Hinweis: Die genaue Methode hängt von Ihrer Repository-Implementierung ab + readme_content = await repository.get_file_contents(readme_path) + return readme_content + except FileNotFoundError: + # Falls sprachspezifische README nicht existiert, verwende Standard-README + if readme_path != "README.md": + logger.debug( + f"Sprachspezifische README {readme_path} nicht gefunden, " + f"verwende README.md für Repository {repository.data.full_name}" + ) + try: + readme_content = await repository.get_file_contents("README.md") + return readme_content + except FileNotFoundError: + logger.warning( + f"README.md nicht gefunden für Repository {repository.data.full_name}" + ) + return "" + return "" + except Exception as e: + # Log Fehler und verwende Standard-README als Fallback + logger.warning( + f"Fehler beim Laden von {readme_path} für Repository " + f"{repository.data.full_name}: {e}" + ) + if readme_path != "README.md": + try: + readme_content = await repository.get_file_contents("README.md") + return readme_content + except FileNotFoundError: + return "" + return "" +``` + +## Unterstützte Dateinamen + +Das Backend sollte folgende README-Dateien unterstützen: + +- `README.md` - Standard-README (Englisch oder Fallback) +- `README.de.md` - Deutsch +- `README.fr.md` - Französisch +- `README.es.md` - Spanisch +- `README.it.md` - Italienisch +- `README.nl.md` - Niederländisch +- `README.pl.md` - Polnisch +- `README.pt.md` - Portugiesisch +- `README.ru.md` - Russisch +- `README.zh.md` - Chinesisch +- etc. + +**Format:** `README.{language_code}.md` (ISO 639-1 Sprachcode, 2 Buchstaben) + +## Fallback-Verhalten + +1. **Wenn `language` Parameter gesendet wird:** + - Versuche `README.{language}.md` zu laden + - Falls nicht vorhanden, verwende `README.md` als Fallback + +2. **Wenn kein `language` Parameter gesendet wird:** + - Verwende `README.md` (Standard-Verhalten, backward-kompatibel) + +3. **Wenn `language` = "en" oder None:** + - Verwende `README.md` (Englisch ist die Standard-Sprache) + +## Validierung + +Der `language`-Parameter sollte validiert werden: + +```python +# Optional: Validierung des Sprachcodes +if language: + # Prüfe, ob es ein gültiger 2-Buchstaben-Sprachcode ist + if not language.isalpha() or len(language) != 2: + logger.warning(f"Ungültiger Sprachcode: {language}, verwende README.md") + language = None + else: + language = language.lower() # Normalisiere zu Kleinbuchstaben +``` + +## Testing + +### Test-Szenarien + +1. **Repository mit nur README.md:** + - Request ohne `language`: Sollte README.md zurückgeben ✅ + - Request mit `language: "de"`: Sollte README.md zurückgeben (Fallback) ✅ + +2. **Repository mit README.md und README.de.md:** + - Request ohne `language`: Sollte README.md zurückgeben ✅ + - Request mit `language: "de"`: Sollte README.de.md zurückgeben ✅ + - Request mit `language: "fr"`: Sollte README.md zurückgeben (Fallback) ✅ + +3. **Repository mit nur README.de.md (kein README.md):** + - Request ohne `language`: Sollte Fehler oder leeren String zurückgeben + - Request mit `language: "de"`: Sollte README.de.md zurückgeben ✅ + +### Test-Commands + +```python +# Test 1: Ohne language Parameter (backward-kompatibel) +{ + "type": "hacs/repository/info", + "repository_id": "123456789" +} + +# Test 2: Mit language Parameter +{ + "type": "hacs/repository/info", + "repository_id": "123456789", + "language": "de" +} + +# Test 3: Mit language Parameter (Englisch) +{ + "type": "hacs/repository/info", + "repository_id": "123456789", + "language": "en" +} +``` + +## Migration und Backward-Kompatibilität + +**Wichtig:** Die Implementierung muss **vollständig backward-kompatibel** sein: + +- Alte Frontend-Versionen (ohne `language`-Parameter) müssen weiterhin funktionieren +- Neue Frontend-Versionen (mit `language`-Parameter) sollten funktionieren, auch wenn das Backend den Parameter noch nicht unterstützt (Frontend hat Fehlerbehandlung) + +**Empfehlung:** +- Der `language`-Parameter sollte als `vol.Optional()` definiert werden +- Wenn der Parameter nicht vorhanden ist, sollte das Standard-Verhalten (README.md) verwendet werden + +## Beispiel-Repository + +Ein Beispiel-Repository mit mehrsprachigen READMEs: + +``` +repository/ +├── README.md (Englisch, Standard) +├── README.de.md (Deutsch) +├── README.fr.md (Französisch) +└── ... +``` + +## Zusammenfassung + +**Was muss implementiert werden:** + +1. ✅ Websocket-Handler erweitern: `vol.Optional("language"): str` hinzufügen +2. ✅ README-Lade-Funktion erweitern: Sprachspezifische README-Dateien unterstützen +3. ✅ Fallback-Logik implementieren: README.md verwenden, wenn sprachspezifische README nicht existiert +4. ✅ Validierung: Sprachcode validieren (optional, aber empfohlen) +5. ✅ Testing: Verschiedene Szenarien testen + +**Frontend-Status:** +- ✅ Frontend sendet bereits den `language`-Parameter +- ✅ Frontend hat automatische Fehlerbehandlung +- ✅ Frontend ist backward-kompatibel + +**Backend-Status:** +- ⏳ Backend muss noch implementiert werden (diese Dokumentation) + +## Weitere Ressourcen + +- **Frontend-Repository:** https://github.com/hacs/frontend +- **Backend-Repository:** https://github.com/hacs/integration +- **HACS Dokumentation:** https://hacs.xyz/docs/ + +## Fragen oder Probleme? + +Bei Fragen zur Implementierung: +1. Prüfen Sie die Frontend-Implementierung in `src/data/repository.ts` +2. Prüfen Sie die Websocket-Nachrichten in der Browser-Konsole +3. Erstellen Sie ein Issue im Backend-Repository: https://github.com/hacs/integration/issues + diff --git a/HACS_MULTILINGUAL_FEATURE_REQUEST.md b/HACS_MULTILINGUAL_FEATURE_REQUEST.md new file mode 100644 index 000000000..4e0816948 --- /dev/null +++ b/HACS_MULTILINGUAL_FEATURE_REQUEST.md @@ -0,0 +1,163 @@ +# Feature Request: Multilingual README Support in HACS + +## Summary + +Add support for automatic language detection and display of README files in HACS, using the same mechanism as Home Assistant's translation system. This would allow repository maintainers to provide README files in multiple languages (e.g., `README.md`, `README.de.md`, `README.fr.md`) and have HACS automatically display the appropriate language based on the user's Home Assistant language setting (`hass.config.language`). + +## Motivation + +Currently, HACS always displays `README.md` regardless of the user's language preference. This creates a barrier for non-English speaking users who may not understand the installation and configuration instructions. + +Home Assistant (both Core and Custom integrations) uses a standardized translation system: +- **File structure**: `translations/.json` (e.g., `translations/de.json`, `translations/en.json`) +- **Language detection**: Uses `hass.config.language` from user settings +- **Automatic loading**: Home Assistant automatically loads the correct translation file based on user language +- **Fallback**: Always falls back to `en.json` if language-specific file doesn't exist + +This mechanism should be applied to README files in HACS, ensuring consistency with how Home Assistant handles translations. + +## Proposed Solution + +### File Naming Convention + +Follow Home Assistant's translation file naming convention, adapted for README files: + +**Home Assistant Pattern:** +- `translations/.json` (e.g., `translations/de.json`, `translations/en.json`) +- Language codes: BCP47 format, 2-letter lowercase (e.g., `de`, `en`, `fr`, `es`) + +**HACS README Pattern:** +- `README.md` - Default/English (fallback) +- `README.de.md` - German +- `README.fr.md` - French +- `README.es.md` - Spanish +- `README.it.md` - Italian +- etc. + +Language codes follow BCP47 format (2-letter lowercase codes). Format: `README..md` + +### Language Detection Policy + +Follow Home Assistant's language detection mechanism: + +1. **Language source**: Use `hass.config.language` from user's Home Assistant settings + - No browser language detection - only Home Assistant's configured language + +2. **Language code extraction**: + - Extract base language code from BCP47 format: `language.split("-")[0].lower()` (e.g., `de-DE` → `de`, `en-US` → `en`) + +3. **Fallback**: + - If language-specific file doesn't exist → fallback to `README.md` + - If language is `en` or not set → use `README.md` directly + +### Implementation Details + +1. **Language Source**: Get language from `hass.config.language` (same as Home Assistant's `async_get_translations()`) + - Extract base language code: `language.split("-")[0].lower()` + +2. **File Detection**: Check for `README..md` in repository root + - Pattern: `README..md` (e.g., `README.de.md` for German) + - Use lowercase 2-letter language codes (BCP47 format) + +3. **File Resolution Logic**: + - If `hass.config.language = "de"` → try `README.de.md`, fallback to `README.md` + - If `hass.config.language = "en"` → use `README.md` directly + - If language-specific file doesn't exist → fallback to `README.md` + +4. **Backward Compatibility**: Repositories with only `README.md` continue to work without changes + +5. **No Caching**: Language is read directly from `hass.config.language` each time + +### Example Implementation Flow + +```python +def get_readme_path(hass: HomeAssistant, repository) -> str: + """Get the appropriate README file path based on Home Assistant language setting.""" + language = hass.config.language + base_language = language.split("-")[0].lower() if language else "en" + + if base_language == "en" or not base_language: + return "README.md" + + language_readme = f"README.{base_language}.md" + if file_exists(repository, language_readme): + return language_readme + + return "README.md" +``` + +## Benefits + +1. **Better User Experience**: Users see documentation in their preferred language +2. **Consistency**: Aligns with Home Assistant's existing i18n approach +3. **Community Friendly**: Encourages contributions from non-English speakers +4. **Backward Compatible**: Existing repositories continue to work without changes +5. **Optional**: Repository maintainers can choose to provide translations or not + +## Use Cases + +1. German user (`hass.config.language = "de"`) sees `README.de.md` if available, otherwise `README.md` +2. French user (`hass.config.language = "fr"`) sees `README.fr.md` if available, otherwise `README.md` +3. English user (`hass.config.language = "en"` or unset) sees `README.md` +4. Unsupported language falls back to `README.md` +5. Repository with only `README.md` works as before (backward compatible) + +## Alternatives Considered + +1. **Single multilingual README**: Less maintainable, harder to read +2. **Manual language selection**: Adds friction, not automatic +3. **GitHub Pages integration**: Requires additional setup, not native to HACS + +## Implementation Notes + +- This feature should be opt-in for repository maintainers (they provide translated READMEs) +- The feature should gracefully handle missing translations (fallback to English) +- Consider adding a language indicator/switcher in the UI for manual override +- May want to add validation in HACS validation action to check README file naming + +## Related Issues/PRs + +- Uses the same mechanism as Home Assistant's translation system (`translations/.json`) +- Follows the same pattern as `async_get_translations()` function +- Uses `hass.config.language` as language source +- Uses BCP47 language code format +- Could be extended to support other documentation files in the future + +## References + +- Home Assistant i18n documentation: https://developers.home-assistant.io/docs/translations/ +- BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag +- Home Assistant Frontend translations: https://github.com/home-assistant/frontend/tree/dev/src/translations +- Custom component translations: Custom components use `translations/` directory (e.g., `translations/de.json`, `translations/en.json`) + +--- + +**Note**: Implementation Repository and Submission: + +**Target Repository:** +- **HACS Frontend**: https://github.com/hacs/frontend + - This is where README files are rendered and displayed in the HACS UI + - The multilingual README feature should be implemented here + +**How to Submit:** + +**Option 1: GitHub Discussions (Recommended)** +1. Go to https://github.com/hacs/frontend/discussions (or https://github.com/hacs/integration/discussions) +2. Click "New discussion" +3. Choose "Ideas" or "Q&A" category +4. Use the title: "Feature Request: Multilingual README Support" +5. Copy the content from this document into the discussion + +**Option 2: Direct Pull Request** +1. Fork the HACS Frontend repository: https://github.com/hacs/frontend +2. Create a branch for the feature (e.g., `feature/multilingual-readme`) +3. Implement the changes in the frontend code that handles README rendering +4. Create a Pull Request with reference to this feature request + +**Option 3: Issue in Frontend Repository** +1. Go to https://github.com/hacs/frontend/issues +2. Create a new issue describing the feature request +3. Link to this detailed specification + +Before implementing, it's recommended to discuss the feature with the HACS maintainers to ensure alignment with project goals and to understand the codebase structure. + diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md new file mode 100644 index 000000000..d830fa0ea --- /dev/null +++ b/PULL_REQUEST.md @@ -0,0 +1,192 @@ +# Add Multilingual README Support + +## Summary + +This PR adds support for automatic language detection and display of multilingual README files in HACS. The frontend now automatically requests language-specific README files (e.g., `README.de.md`, `README.fr.md`) based on the user's Home Assistant language setting, with automatic fallback to `README.md` if a language-specific version is not available. + +## Related Backend PR + +This frontend implementation requires the corresponding backend changes. Please see: +- **Backend PR:** https://github.com/hacs/integration/pull/4964 + +The backend must support the optional `language` parameter in the `hacs/repository/info` WebSocket command to fully enable this feature. + +## Changes + +### Core Implementation + +1. **Language Code Extraction** (`src/data/repository.ts`) + - Added `getBaseLanguageCode()` function to extract base language code from BCP47 format (e.g., "de-DE" → "de") + - Handles edge cases (undefined, empty strings, uppercase) + +2. **Repository Information Fetching** (`src/data/repository.ts`) + - Enhanced `fetchRepositoryInformation()` to accept optional `language` parameter + - Automatically extracts language from `hass.language` if not provided + - Sends `language` parameter in WebSocket message when language is not English + - Implements intelligent backend support detection with caching: + - First request: Attempts to send `language` parameter + - On success: Caches backend support and continues sending parameter + - On error (unsupported parameter): Caches rejection and retries without parameter + - Fully backward compatible: Works with both old and new backend versions + +3. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) + - Updated `_fetchRepository()` to pass `hass.language` to `fetchRepositoryInformation()` + - Added language change detection in `updated()` lifecycle hook + - Automatically reloads repository information when user changes Home Assistant language + +4. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) + - Updated `_fetchRepository()` to pass `hass.language` for consistency + +## Features + +- ✅ **Automatic Language Detection**: Uses `hass.language` from Home Assistant settings +- ✅ **BCP47 Support**: Extracts base language code from full BCP47 format (e.g., "de-DE" → "de") +- ✅ **Intelligent Fallback**: Falls back to `README.md` if language-specific README doesn't exist +- ✅ **Backend Compatibility**: Automatically detects backend support and adapts behavior +- ✅ **Backward Compatible**: Works with old backend versions (graceful degradation) +- ✅ **Language Change Detection**: Automatically reloads README when user changes language +- ✅ **No Breaking Changes**: Existing repositories continue to work without modifications + +## File Naming Convention + +Repository maintainers can provide multilingual README files using the following naming pattern: + +- `README.md` - Default/English (always used as fallback) +- `README.de.md` - German +- `README.fr.md` - French +- `README.es.md` - Spanish +- `README.it.md` - Italian +- `README.nl.md` - Dutch +- `README.pl.md` - Polish +- `README.pt.md` - Portuguese +- `README.ru.md` - Russian +- `README.zh.md` - Chinese +- etc. + +**Format:** `README.{language_code}.md` (ISO 639-1 language code, 2 letters, lowercase) + +## Behavior + +### When Backend Supports Language Parameter + +1. User with `hass.language = "de"` opens a repository +2. Frontend extracts base language code: "de" +3. Frontend sends WebSocket message: `{ type: "hacs/repository/info", repository_id: "...", language: "de" }` +4. Backend returns `README.de.md` if available, otherwise `README.md` +5. Frontend displays the appropriate README + +### When Backend Doesn't Support Language Parameter + +1. User with `hass.language = "de"` opens a repository +2. Frontend attempts to send `language` parameter +3. Backend rejects the parameter (error: "extra keys not allowed") +4. Frontend detects the error, caches the rejection, and retries without `language` parameter +5. Backend returns `README.md` (standard behavior) +6. Frontend displays `README.md` + +This ensures **zero breaking changes** and graceful degradation. + +## Testing + +### Manual Testing + +1. **Test with Backend Support:** + - Ensure backend PR is merged or backend supports `language` parameter + - Set Home Assistant language to German (`de`) + - Open a repository with `README.de.md` + - Verify that `README.de.md` is displayed + +2. **Test Fallback:** + - Set Home Assistant language to German (`de`) + - Open a repository with only `README.md` (no `README.de.md`) + - Verify that `README.md` is displayed + +3. **Test Backward Compatibility:** + - Use an old backend version (without `language` parameter support) + - Set Home Assistant language to German (`de`) + - Open any repository + - Verify that no errors occur and `README.md` is displayed + +4. **Test Language Change:** + - Open a repository + - Change Home Assistant language in settings + - Verify that repository information is automatically reloaded + +### Browser Console Logs + +The implementation includes debug logging to help verify behavior: + +- `[HACS] Sending language parameter: "de" (first attempt)` - First request with language +- `[HACS] Backend accepted language parameter "de" - caching support` - Backend supports it +- `[HACS] Backend rejected language parameter - caching rejection and retrying without it` - Backend doesn't support it +- `[HACS] Skipping language parameter (backend doesn't support it)` - Using cached rejection + +## Implementation Details + +### Language Code Extraction + +```typescript +export const getBaseLanguageCode = (language: string | undefined): string => { + if (!language) { + return "en"; + } + return language.split("-")[0].toLowerCase(); +}; +``` + +**Examples:** +- `"de-DE"` → `"de"` +- `"en-US"` → `"en"` +- `"fr"` → `"fr"` +- `undefined` → `"en"` + +### Backend Support Detection + +The implementation uses a session-based cache to avoid repeated failed requests: + +```typescript +let backendSupportsLanguage: boolean | null = null; +``` + +- `null`: Not yet determined (will attempt to send parameter) +- `true`: Backend supports it (will send parameter) +- `false`: Backend doesn't support it (will skip parameter) + +This cache is reset on page reload and can be manually reset using `resetBackendLanguageSupportCache()` for testing. + +## Alignment with Home Assistant Standards + +This implementation follows Home Assistant's translation system patterns: + +- ✅ Uses `hass.language` (same as `async_get_translations()`) +- ✅ Extracts base language code from BCP47 format +- ✅ Automatic fallback to English/default +- ✅ Consistent with Home Assistant's i18n approach + +## Documentation + +- **Backend Implementation Guide:** `BACKEND_IMPLEMENTATION_GUIDE.md` - Complete guide for backend developers +- **Feature Request:** `HACS_MULTILINGUAL_FEATURE_REQUEST.md` - Original feature specification +- **Testing Guide:** `TESTING_MULTILINGUAL_README.md` - Testing instructions + +## Checklist + +- [x] Code follows project style guidelines +- [x] Changes are backward compatible +- [x] Error handling implemented +- [x] Debug logging added +- [x] Language change detection implemented +- [x] Documentation updated +- [x] Tested with backend support +- [x] Tested without backend support (backward compatibility) + +## Screenshots + +_Add screenshots showing multilingual README display if available_ + +## Notes + +- This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. +- The implementation is designed to work gracefully even if the backend doesn't support the `language` parameter yet. +- Repository maintainers are not required to provide multilingual READMEs - this is an opt-in feature. + diff --git a/TESTING_MULTILINGUAL_README.md b/TESTING_MULTILINGUAL_README.md new file mode 100644 index 000000000..260fcbb9f --- /dev/null +++ b/TESTING_MULTILINGUAL_README.md @@ -0,0 +1,350 @@ +# Testanleitung: Mehrsprachige README-Unterstützung + +Diese Anleitung beschreibt, wie Sie die mehrsprachige README-Unterstützung in HACS testen können. + +## Voraussetzungen + +1. **HACS Frontend Repository** mit den Änderungen aus dem Branch `feature/Multilingual-readme` +2. **HACS Backend Integration** (muss den `language`-Parameter in `hacs/repository/info` unterstützen) +3. **Home Assistant Instanz** mit HACS installiert +4. **Test-Repository** mit mehrsprachigen README-Dateien (z.B. `README.md`, `README.de.md`, `README.fr.md`) + +## 1. Unit-Tests für Sprachcode-Extraktion + +### Test der `getBaseLanguageCode` Funktion + +Erstellen Sie eine Testdatei oder testen Sie direkt in der Browser-Konsole: + +```typescript +import { getBaseLanguageCode } from './src/data/repository'; + +// Test 1: BCP47 Format mit Region +console.assert(getBaseLanguageCode("de-DE") === "de", "de-DE sollte 'de' ergeben"); +console.assert(getBaseLanguageCode("en-US") === "en", "en-US sollte 'en' ergeben"); +console.assert(getBaseLanguageCode("fr-FR") === "fr", "fr-FR sollte 'fr' ergeben"); + +// Test 2: Einfache Sprachcodes +console.assert(getBaseLanguageCode("de") === "de", "de sollte 'de' ergeben"); +console.assert(getBaseLanguageCode("en") === "en", "en sollte 'en' ergeben"); +console.assert(getBaseLanguageCode("fr") === "fr", "fr sollte 'fr' ergeben"); + +// Test 3: Undefined/Null +console.assert(getBaseLanguageCode(undefined) === "en", "undefined sollte 'en' ergeben"); +console.assert(getBaseLanguageCode("") === "en", "leerer String sollte 'en' ergeben"); + +// Test 4: Großbuchstaben +console.assert(getBaseLanguageCode("DE-DE") === "de", "DE-DE sollte 'de' ergeben"); +console.assert(getBaseLanguageCode("EN-US") === "en", "EN-US sollte 'en' ergeben"); + +console.log("Alle Tests bestanden!"); +``` + +## 2. Manuelle Tests im Browser + +### Schritt 1: Frontend starten + +```bash +# Im HACS Frontend Repository +yarn start +# oder +make start +``` + +### Schritt 2: Home Assistant öffnen + +1. Öffnen Sie Home Assistant in Ihrem Browser +2. Navigieren Sie zu HACS +3. Öffnen Sie die Browser-Entwicklertools (F12) + +### Schritt 3: Websocket-Nachrichten überwachen + +In der Browser-Konsole können Sie die Websocket-Nachrichten überwachen: + +```javascript +// Websocket-Nachrichten loggen +const originalSendMessage = window.hassConnection?.sendMessage; +if (originalSendMessage) { + window.hassConnection.sendMessage = function(message) { + if (message.type === "hacs/repository/info") { + console.log("HACS Repository Info Request:", message); + if (message.language) { + console.log("✓ Sprachparameter gesendet:", message.language); + } else { + console.log("ℹ Kein Sprachparameter (vermutlich Englisch)"); + } + } + return originalSendMessage.call(this, message); + }; +} +``` + +### Schritt 4: Sprache in Home Assistant ändern + +1. Gehen Sie zu **Einstellungen** → **Sprache & Region** +2. Ändern Sie die Sprache (z.B. von Englisch zu Deutsch) +3. Navigieren Sie zurück zu HACS +4. Öffnen Sie ein Repository mit mehrsprachigen README-Dateien + +### Schritt 5: Repository-Informationen prüfen + +1. Öffnen Sie ein Repository in HACS +2. Prüfen Sie in der Browser-Konsole, ob die richtige Websocket-Nachricht gesendet wurde +3. Prüfen Sie, ob die README in der richtigen Sprache angezeigt wird + +## 3. Test mit verschiedenen Sprachen + +### Test-Szenarien + +| Home Assistant Sprache | Erwartete README-Datei | Websocket-Nachricht | +|------------------------|------------------------|---------------------| +| `en` oder `en-US` | `README.md` | Kein `language`-Parameter | +| `de` oder `de-DE` | `README.de.md` (falls vorhanden), sonst `README.md` | `language: "de"` | +| `fr` oder `fr-FR` | `README.fr.md` (falls vorhanden), sonst `README.md` | `language: "fr"` | +| `es` oder `es-ES` | `README.es.md` (falls vorhanden), sonst `README.md` | `language: "es"` | +| `it` oder `it-IT` | `README.it.md` (falls vorhanden), sonst `README.md` | `language: "it"` | + +### Test-Repository erstellen + +Erstellen Sie ein Test-Repository mit folgenden Dateien: + +``` +test-repository/ +├── README.md (Englisch - Standard) +├── README.de.md (Deutsch) +├── README.fr.md (Französisch) +└── README.es.md (Spanisch) +``` + +Jede Datei sollte eindeutigen Inhalt haben, z.B.: + +**README.md:** +```markdown +# Test Repository + +This is the English README. +``` + +**README.de.md:** +```markdown +# Test Repository + +Dies ist die deutsche README. +``` + +**README.fr.md:** +```markdown +# Test Repository + +Ceci est le README français. +``` + +## 4. Automatisierte Tests + +### Test-Datei erstellen + +Erstellen Sie `src/data/__tests__/repository.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest'; +import { getBaseLanguageCode, fetchRepositoryInformation } from '../repository'; +import type { HomeAssistant } from '../../../homeassistant-frontend/src/types'; + +describe('getBaseLanguageCode', () => { + it('should extract base language from BCP47 format', () => { + expect(getBaseLanguageCode('de-DE')).toBe('de'); + expect(getBaseLanguageCode('en-US')).toBe('en'); + expect(getBaseLanguageCode('fr-FR')).toBe('fr'); + }); + + it('should handle simple language codes', () => { + expect(getBaseLanguageCode('de')).toBe('de'); + expect(getBaseLanguageCode('en')).toBe('en'); + expect(getBaseLanguageCode('fr')).toBe('fr'); + }); + + it('should return "en" for undefined or empty', () => { + expect(getBaseLanguageCode(undefined)).toBe('en'); + expect(getBaseLanguageCode('')).toBe('en'); + }); + + it('should convert to lowercase', () => { + expect(getBaseLanguageCode('DE-DE')).toBe('de'); + expect(getBaseLanguageCode('EN-US')).toBe('en'); + }); +}); + +describe('fetchRepositoryInformation', () => { + it('should include language parameter for non-English languages', async () => { + const mockHass = { + language: 'de-DE', + connection: { + sendMessagePromise: async (message: any) => { + expect(message.language).toBe('de'); + return { additional_info: 'Test' }; + } + } + } as unknown as HomeAssistant; + + await fetchRepositoryInformation(mockHass, 'test-repo'); + }); + + it('should not include language parameter for English', async () => { + const mockHass = { + language: 'en', + connection: { + sendMessagePromise: async (message: any) => { + expect(message.language).toBeUndefined(); + return { additional_info: 'Test' }; + } + } + } as unknown as HomeAssistant; + + await fetchRepositoryInformation(mockHass, 'test-repo'); + }); +}); +``` + +### Tests ausführen + +```bash +# Wenn Vitest konfiguriert ist +yarn test + +# Oder direkt mit Node +node --experimental-vm-modules node_modules/vitest/dist/cli.js run +``` + +## 5. Integrationstests + +### Test mit echten Home Assistant Instanz + +1. **HACS Backend aktualisieren**: Stellen Sie sicher, dass das Backend den `language`-Parameter unterstützt +2. **Frontend bauen und installieren**: + ```bash + yarn build + # Kopieren Sie die gebauten Dateien in Ihr HACS Frontend Verzeichnis + ``` +3. **Home Assistant neu starten** +4. **Sprache ändern und Repository öffnen** + +### Browser-Entwicklertools verwenden + +1. Öffnen Sie die **Netzwerk**-Registerkarte +2. Filtern Sie nach **WS** (WebSocket) +3. Öffnen Sie ein Repository in HACS +4. Prüfen Sie die gesendeten Nachrichten + +## 6. Edge Cases testen + +### Test-Szenarien für Edge Cases + +1. **Sprache ändern während Repository geöffnet ist** + - Öffnen Sie ein Repository + - Ändern Sie die Sprache in Home Assistant + - Prüfen Sie, ob die README automatisch neu geladen wird + +2. **Repository ohne sprachspezifische README** + - Verwenden Sie ein Repository mit nur `README.md` + - Ändern Sie die Sprache auf Deutsch + - Prüfen Sie, ob die englische README angezeigt wird (Fallback) + +3. **Ungültige Sprachcodes** + - Testen Sie mit `hass.language = undefined` + - Testen Sie mit `hass.language = ""` + - Prüfen Sie, ob der Fallback auf Englisch funktioniert + +4. **Backend ohne Sprachunterstützung** + - Testen Sie mit einem älteren Backend, das den `language`-Parameter nicht unterstützt + - Prüfen Sie, ob die Standard-README angezeigt wird + +## 7. Debugging + +### Console-Logging aktivieren + +Fügen Sie temporär Logging hinzu: + +```typescript +// In src/data/repository.ts +export const fetchRepositoryInformation = async ( + hass: HomeAssistant, + repositoryId: string, + language?: string, +): Promise => { + const baseLanguage = language ? getBaseLanguageCode(language) : getBaseLanguageCode(hass.language); + + console.log('[HACS] Language detection:', { + hassLanguage: hass.language, + providedLanguage: language, + baseLanguage, + }); + + const message: any = { + type: "hacs/repository/info", + repository_id: repositoryId, + }; + + if (baseLanguage && baseLanguage !== "en") { + message.language = baseLanguage; + console.log('[HACS] Sending language parameter:', baseLanguage); + } else { + console.log('[HACS] Using default README.md (English)'); + } + + return hass.connection.sendMessagePromise(message); +}; +``` + +## 8. Checkliste für vollständige Tests + +- [ ] Unit-Tests für `getBaseLanguageCode` bestehen +- [ ] Websocket-Nachricht enthält `language`-Parameter für nicht-englische Sprachen +- [ ] Websocket-Nachricht enthält keinen `language`-Parameter für Englisch +- [ ] README wird in der richtigen Sprache angezeigt +- [ ] Fallback auf `README.md` funktioniert, wenn sprachspezifische Datei fehlt +- [ ] Repository-Informationen werden neu geladen, wenn sich die Sprache ändert +- [ ] Funktioniert mit verschiedenen BCP47-Formaten (z.B. `de-DE`, `en-US`) +- [ ] Funktioniert mit einfachen Sprachcodes (z.B. `de`, `en`) +- [ ] Funktioniert mit `undefined` oder leerem String (Fallback auf Englisch) +- [ ] Backward-kompatibel mit Backend ohne Sprachunterstützung + +## 9. Bekannte Probleme und Lösungen + +### Problem: README wird nicht in der richtigen Sprache angezeigt + +**Lösung:** +1. Prüfen Sie, ob das Backend den `language`-Parameter unterstützt +2. Prüfen Sie die Browser-Konsole auf Fehler +3. Prüfen Sie, ob die sprachspezifische README-Datei im Repository existiert + +### Problem: Sprache wird nicht neu geladen + +**Lösung:** +1. Prüfen Sie, ob `updated()` im Repository-Dashboard korrekt implementiert ist +2. Prüfen Sie, ob `hass.language` sich tatsächlich ändert +3. Laden Sie die Seite neu, nachdem Sie die Sprache geändert haben + +## 10. Nützliche Befehle + +```bash +# Frontend starten +yarn start + +# Frontend bauen +yarn build + +# Tests ausführen (falls konfiguriert) +yarn test + +# Linter prüfen +yarn lint + +# TypeScript prüfen +yarn type-check +``` + +## Weitere Ressourcen + +- [HACS Frontend Dokumentation](https://hacs.xyz/docs/contribute/frontend/) +- [Home Assistant Frontend Entwickler-Dokumentation](https://developers.home-assistant.io/docs/frontend/) +- [BCP47 Sprachcodes](https://en.wikipedia.org/wiki/IETF_language_tag) + diff --git a/src/components/dialogs/hacs-download-dialog.ts b/src/components/dialogs/hacs-download-dialog.ts index d7a0f3449..a44223abf 100644 --- a/src/components/dialogs/hacs-download-dialog.ts +++ b/src/components/dialogs/hacs-download-dialog.ts @@ -1,388 +1,389 @@ -import "@material/mwc-button/mwc-button"; -import "@material/mwc-linear-progress/mwc-linear-progress"; -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../homeassistant-frontend/src/common/dom/fire_event"; -import { mainWindow } from "../../../homeassistant-frontend/src/common/dom/get_main_window"; -import { computeRTL } from "../../../homeassistant-frontend/src/common/util/compute_rtl"; -import "../../../homeassistant-frontend/src/components/ha-alert"; -import "../../../homeassistant-frontend/src/components/ha-button"; -import "../../../homeassistant-frontend/src/components/ha-circular-progress"; -import "../../../homeassistant-frontend/src/components/ha-dialog"; -import "../../../homeassistant-frontend/src/components/ha-expansion-panel"; -import "../../../homeassistant-frontend/src/components/ha-form/ha-form"; -import "../../../homeassistant-frontend/src/components/ha-list-item"; - -import { relativeTime } from "../../../homeassistant-frontend/src/common/datetime/relative_time"; -import { showConfirmationDialog } from "../../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; -import type { HomeAssistant } from "../../../homeassistant-frontend/src/types"; -import { HacsDispatchEvent } from "../../data/common"; -import { - fetchRepositoryInformation, - RepositoryBase, - repositoryDownloadVersion, - RepositoryInfo, - repositoryReleases, -} from "../../data/repository"; -import { websocketSubscription } from "../../data/websocket"; -import { HacsStyles } from "../../styles/hacs-common-style"; -import { generateFrontendResourceURL } from "../../tools/frontend-resource"; -import type { HacsDownloadDialogParams } from "./show-hacs-dialog"; - -@customElement("release-item") -export class ReleaseItem extends LitElement { - @property({ attribute: false }) public locale!: HomeAssistant["locale"]; - @property({ attribute: false }) public release!: { - tag: string; - published_at: string; - name: string; - prerelease: boolean; - }; - - protected render() { - return html` - - ${this.release.tag} - ${this.release.prerelease ? html`pre-release` : nothing} - - - ${relativeTime(new Date(this.release.published_at), this.locale)} - ${this.release.name && this.release.name !== this.release.tag - ? html` - ${this.release.name}` - : nothing} - - `; - } - - static get styles(): CSSResultGroup { - return css` - :host { - display: flex; - flex-direction: column; - } - .secondary { - font-size: 0.8em; - color: var(--secondary-text-color); - font-style: italic; - } - .pre-release { - background-color: var(--accent-color); - padding: 2px 4px; - font-size: 0.8em; - font-weight: 600; - border-radius: 12px; - margin: 0 2px; - color: var(--secondary-background-color); - } - `; - } -} -@customElement("hacs-download-dialog") -export class HacsDonwloadDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _waiting = true; - - @state() private _installing = false; - - @state() private _error?: any; - - @state() private _releases?: { - tag: string; - name: string; - published_at: string; - prerelease: boolean; - }[]; - - @state() public _repository?: RepositoryInfo; - - @state() _dialogParams?: HacsDownloadDialogParams; - - @state() _selectedVersion?: string; - - public async showDialog(dialogParams: HacsDownloadDialogParams): Promise { - this._dialogParams = dialogParams; - this._waiting = false; - if (dialogParams.repository) { - this._repository = dialogParams.repository; - } else { - await this._fetchRepository(); - } - - if (this._repository && this._repository.version_or_commit !== "commit") { - this._selectedVersion = this._repository.available_version; - } - this._releases = undefined; - - websocketSubscription( - this.hass, - (data) => { - this._error = data; - this._installing = false; - }, - HacsDispatchEvent.ERROR, - ); - await this.updateComplete; - } - - public closeDialog(): void { - this._dialogParams = undefined; - this._repository = undefined; - this._error = undefined; - this._installing = false; - this._waiting = false; - this._releases = undefined; - this._selectedVersion = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - private _getInstallPath = memoizeOne((repository: RepositoryBase) => { - let path: string = repository.local_path; - if (["template", "theme", "python_script"].includes(repository.category)) { - path = `${path}/${repository.file_name}`; - } - return path; - }); - - private async _fetchRepository() { - try { - this._repository = await fetchRepositoryInformation( - this.hass, - this._dialogParams!.repositoryId, - ); - } catch (err: any) { - this._error = err; - } - } - - protected render() { - if (!this._dialogParams) { - return nothing; - } - if (!this._repository) { - return html` - -
- - ${this._error - ? html` - ${this._error.message || this._error} - ` - : nothing} -
-
- `; - } - - const installPath = this._getInstallPath(this._repository); - return html` - -
-

- ${this._dialogParams.hacs.localize( - this._repository.version_or_commit === "commit" - ? "dialog_download.will_download_commit" - : "dialog_download.will_download_version", - { - ref: html` - ${this._selectedVersion || this._repository.available_version} - `, - }, - )} -

-
- ${this._dialogParams.hacs.localize("dialog_download.note_downloaded", { - location: html`'${installPath}'`, - })} - ${this._repository.category === "plugin" && - this._dialogParams.hacs.info.lovelace_mode !== "storage" - ? html` -

${this._dialogParams.hacs.localize(`dialog_download.lovelace_instruction`)}

-
-                url: ${generateFrontendResourceURL({ repository: this._repository })}
-                type: module
-                
- ` - : nothing} - ${this._repository.category === "integration" - ? html`

${this._dialogParams.hacs.localize("dialog_download.restart")}

` - : nothing} -
- ${this._selectedVersion - ? html` -

${this._dialogParams!.hacs.localize("dialog_download.release_warning")}

- ${this._releases === undefined - ? this._dialogParams.hacs.localize("dialog_download.fetching_releases") - : this._releases.length === 0 - ? this._dialogParams.hacs.localize("dialog_download.no_releases") - : html` ({ - value: release.tag, - label: html` - ${release.tag} - `, - })), - }, - }, - }, - ]} - >`} -
` - : nothing} - ${this._error - ? html` - ${this._error.message || this._error} - ` - : nothing} - ${this._installing - ? html`` - : nothing} -
- - ${this._dialogParams.hacs.localize("common.cancel")} - - - ${this._dialogParams.hacs.localize("common.download")} - -
- `; - } - - private _computeLabel = (entry: any): string => - entry.name === "release" - ? this._dialogParams!.hacs.localize("dialog_download.release") - : entry.name; - - private async _installRepository(): Promise { - if (!this._repository) { - return; - } - - if (this._waiting) { - this._error = "Waiting to update repository information, try later."; - return; - } - - if (this._installing) { - this._error = "Already installing, please wait."; - return; - } - - this._installing = true; - this._error = undefined; - - try { - await repositoryDownloadVersion( - this.hass, - String(this._repository.id), - this._selectedVersion || this._repository.available_version, - ); - } catch (err: any) { - this._error = err || { - message: "Could not download repository, check core logs for more information.", - }; - this._installing = false; - return; - } - - this._dialogParams!.hacs.log.debug(this._repository.category, "_installRepository"); - this._dialogParams!.hacs.log.debug( - this._dialogParams!.hacs.info.lovelace_mode, - "_installRepository", - ); - this._installing = false; - - if (this._repository.category === "plugin") { - showConfirmationDialog(this, { - title: this._dialogParams!.hacs.localize!("common.reload"), - text: html`${this._dialogParams!.hacs.localize!("dialog.reload.description")}
${this - ._dialogParams!.hacs.localize!("dialog.reload.confirm")}`, - dismissText: this._dialogParams!.hacs.localize!("common.cancel"), - confirmText: this._dialogParams!.hacs.localize!("common.reload"), - confirm: () => { - // eslint-disable-next-line - mainWindow.location.href = mainWindow.location.href; - }, - }); - } - if (this._error === undefined) { - this.closeDialog(); - } - } - - async _fetchReleases() { - if (this._releases !== undefined) { - return; - } - try { - this._releases = await repositoryReleases(this.hass, this._repository!.id); - } catch (error) { - this._error = error; - } - } - - private _versionChanged(ev: CustomEvent) { - this._selectedVersion = ev.detail.value.release; - } - - static get styles(): CSSResultGroup { - return [ - HacsStyles, - css` - .note { - margin-top: 12px; - } - pre { - white-space: pre-line; - user-select: all; - padding: 8px; - } - mwc-linear-progress { - margin-bottom: -8px; - margin-top: 4px; - } - ha-expansion-panel { - background-color: var(--secondary-background-color); - padding: 8px; - } - .loading { - text-align: center; - padding: 16px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hacs-download-dialog": HacsDonwloadDialog; - "release-item": ReleaseItem; - } -} +import "@material/mwc-button/mwc-button"; +import "@material/mwc-linear-progress/mwc-linear-progress"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../homeassistant-frontend/src/common/dom/fire_event"; +import { mainWindow } from "../../../homeassistant-frontend/src/common/dom/get_main_window"; +import { computeRTL } from "../../../homeassistant-frontend/src/common/util/compute_rtl"; +import "../../../homeassistant-frontend/src/components/ha-alert"; +import "../../../homeassistant-frontend/src/components/ha-button"; +import "../../../homeassistant-frontend/src/components/ha-circular-progress"; +import "../../../homeassistant-frontend/src/components/ha-dialog"; +import "../../../homeassistant-frontend/src/components/ha-expansion-panel"; +import "../../../homeassistant-frontend/src/components/ha-form/ha-form"; +import "../../../homeassistant-frontend/src/components/ha-list-item"; + +import { relativeTime } from "../../../homeassistant-frontend/src/common/datetime/relative_time"; +import { showConfirmationDialog } from "../../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; +import type { HomeAssistant } from "../../../homeassistant-frontend/src/types"; +import { HacsDispatchEvent } from "../../data/common"; +import { + fetchRepositoryInformation, + RepositoryBase, + repositoryDownloadVersion, + RepositoryInfo, + repositoryReleases, +} from "../../data/repository"; +import { websocketSubscription } from "../../data/websocket"; +import { HacsStyles } from "../../styles/hacs-common-style"; +import { generateFrontendResourceURL } from "../../tools/frontend-resource"; +import type { HacsDownloadDialogParams } from "./show-hacs-dialog"; + +@customElement("release-item") +export class ReleaseItem extends LitElement { + @property({ attribute: false }) public locale!: HomeAssistant["locale"]; + @property({ attribute: false }) public release!: { + tag: string; + published_at: string; + name: string; + prerelease: boolean; + }; + + protected render() { + return html` + + ${this.release.tag} + ${this.release.prerelease ? html`pre-release` : nothing} + + + ${relativeTime(new Date(this.release.published_at), this.locale)} + ${this.release.name && this.release.name !== this.release.tag + ? html` - ${this.release.name}` + : nothing} + + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + flex-direction: column; + } + .secondary { + font-size: 0.8em; + color: var(--secondary-text-color); + font-style: italic; + } + .pre-release { + background-color: var(--accent-color); + padding: 2px 4px; + font-size: 0.8em; + font-weight: 600; + border-radius: 12px; + margin: 0 2px; + color: var(--secondary-background-color); + } + `; + } +} +@customElement("hacs-download-dialog") +export class HacsDonwloadDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _waiting = true; + + @state() private _installing = false; + + @state() private _error?: any; + + @state() private _releases?: { + tag: string; + name: string; + published_at: string; + prerelease: boolean; + }[]; + + @state() public _repository?: RepositoryInfo; + + @state() _dialogParams?: HacsDownloadDialogParams; + + @state() _selectedVersion?: string; + + public async showDialog(dialogParams: HacsDownloadDialogParams): Promise { + this._dialogParams = dialogParams; + this._waiting = false; + if (dialogParams.repository) { + this._repository = dialogParams.repository; + } else { + await this._fetchRepository(); + } + + if (this._repository && this._repository.version_or_commit !== "commit") { + this._selectedVersion = this._repository.available_version; + } + this._releases = undefined; + + websocketSubscription( + this.hass, + (data) => { + this._error = data; + this._installing = false; + }, + HacsDispatchEvent.ERROR, + ); + await this.updateComplete; + } + + public closeDialog(): void { + this._dialogParams = undefined; + this._repository = undefined; + this._error = undefined; + this._installing = false; + this._waiting = false; + this._releases = undefined; + this._selectedVersion = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _getInstallPath = memoizeOne((repository: RepositoryBase) => { + let path: string = repository.local_path; + if (["template", "theme", "python_script"].includes(repository.category)) { + path = `${path}/${repository.file_name}`; + } + return path; + }); + + private async _fetchRepository() { + try { + this._repository = await fetchRepositoryInformation( + this.hass, + this._dialogParams!.repositoryId, + this.hass.language, + ); + } catch (err: any) { + this._error = err; + } + } + + protected render() { + if (!this._dialogParams) { + return nothing; + } + if (!this._repository) { + return html` + +
+ + ${this._error + ? html` + ${this._error.message || this._error} + ` + : nothing} +
+
+ `; + } + + const installPath = this._getInstallPath(this._repository); + return html` + +
+

+ ${this._dialogParams.hacs.localize( + this._repository.version_or_commit === "commit" + ? "dialog_download.will_download_commit" + : "dialog_download.will_download_version", + { + ref: html` + ${this._selectedVersion || this._repository.available_version} + `, + }, + )} +

+
+ ${this._dialogParams.hacs.localize("dialog_download.note_downloaded", { + location: html`'${installPath}'`, + })} + ${this._repository.category === "plugin" && + this._dialogParams.hacs.info.lovelace_mode !== "storage" + ? html` +

${this._dialogParams.hacs.localize(`dialog_download.lovelace_instruction`)}

+
+                url: ${generateFrontendResourceURL({ repository: this._repository })}
+                type: module
+                
+ ` + : nothing} + ${this._repository.category === "integration" + ? html`

${this._dialogParams.hacs.localize("dialog_download.restart")}

` + : nothing} +
+ ${this._selectedVersion + ? html` +

${this._dialogParams!.hacs.localize("dialog_download.release_warning")}

+ ${this._releases === undefined + ? this._dialogParams.hacs.localize("dialog_download.fetching_releases") + : this._releases.length === 0 + ? this._dialogParams.hacs.localize("dialog_download.no_releases") + : html` ({ + value: release.tag, + label: html` + ${release.tag} + `, + })), + }, + }, + }, + ]} + >`} +
` + : nothing} + ${this._error + ? html` + ${this._error.message || this._error} + ` + : nothing} + ${this._installing + ? html`` + : nothing} +
+ + ${this._dialogParams.hacs.localize("common.cancel")} + + + ${this._dialogParams.hacs.localize("common.download")} + +
+ `; + } + + private _computeLabel = (entry: any): string => + entry.name === "release" + ? this._dialogParams!.hacs.localize("dialog_download.release") + : entry.name; + + private async _installRepository(): Promise { + if (!this._repository) { + return; + } + + if (this._waiting) { + this._error = "Waiting to update repository information, try later."; + return; + } + + if (this._installing) { + this._error = "Already installing, please wait."; + return; + } + + this._installing = true; + this._error = undefined; + + try { + await repositoryDownloadVersion( + this.hass, + String(this._repository.id), + this._selectedVersion || this._repository.available_version, + ); + } catch (err: any) { + this._error = err || { + message: "Could not download repository, check core logs for more information.", + }; + this._installing = false; + return; + } + + this._dialogParams!.hacs.log.debug(this._repository.category, "_installRepository"); + this._dialogParams!.hacs.log.debug( + this._dialogParams!.hacs.info.lovelace_mode, + "_installRepository", + ); + this._installing = false; + + if (this._repository.category === "plugin") { + showConfirmationDialog(this, { + title: this._dialogParams!.hacs.localize!("common.reload"), + text: html`${this._dialogParams!.hacs.localize!("dialog.reload.description")}
${this + ._dialogParams!.hacs.localize!("dialog.reload.confirm")}`, + dismissText: this._dialogParams!.hacs.localize!("common.cancel"), + confirmText: this._dialogParams!.hacs.localize!("common.reload"), + confirm: () => { + // eslint-disable-next-line + mainWindow.location.href = mainWindow.location.href; + }, + }); + } + if (this._error === undefined) { + this.closeDialog(); + } + } + + async _fetchReleases() { + if (this._releases !== undefined) { + return; + } + try { + this._releases = await repositoryReleases(this.hass, this._repository!.id); + } catch (error) { + this._error = error; + } + } + + private _versionChanged(ev: CustomEvent) { + this._selectedVersion = ev.detail.value.release; + } + + static get styles(): CSSResultGroup { + return [ + HacsStyles, + css` + .note { + margin-top: 12px; + } + pre { + white-space: pre-line; + user-select: all; + padding: 8px; + } + mwc-linear-progress { + margin-bottom: -8px; + margin-top: 4px; + } + ha-expansion-panel { + background-color: var(--secondary-background-color); + padding: 8px; + } + .loading { + text-align: center; + padding: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hacs-download-dialog": HacsDonwloadDialog; + "release-item": ReleaseItem; + } +} diff --git a/src/dashboards/hacs-repository-dashboard.ts b/src/dashboards/hacs-repository-dashboard.ts index 7db2a7036..09bfc3940 100644 --- a/src/dashboards/hacs-repository-dashboard.ts +++ b/src/dashboards/hacs-repository-dashboard.ts @@ -1,378 +1,386 @@ -import { - mdiAccount, - mdiArrowDownBold, - mdiCube, - mdiDotsVertical, - mdiDownload, - mdiExclamationThick, - mdiStar, -} from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { mainWindow } from "../../homeassistant-frontend/src/common/dom/get_main_window"; -import { extractSearchParamsObject } from "../../homeassistant-frontend/src/common/url/search-params"; -import "../../homeassistant-frontend/src/components/chips/ha-assist-chip"; -import "../../homeassistant-frontend/src/components/chips/ha-chip-set"; -import "../../homeassistant-frontend/src/components/ha-alert"; -import "../../homeassistant-frontend/src/components/ha-card"; -import "../../homeassistant-frontend/src/components/ha-fab"; -import "../../homeassistant-frontend/src/components/ha-markdown"; -import "../../homeassistant-frontend/src/components/ha-menu"; -import type { HaMenu } from "../../homeassistant-frontend/src/components/ha-menu"; -import "../../homeassistant-frontend/src/components/ha-md-menu-item"; -import { showConfirmationDialog } from "../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; -import "../../homeassistant-frontend/src/layouts/hass-error-screen"; -import "../../homeassistant-frontend/src/layouts/hass-loading-screen"; -import "../../homeassistant-frontend/src/layouts/hass-subpage"; -import type { HomeAssistant, Route } from "../../homeassistant-frontend/src/types"; -import { showHacsDownloadDialog } from "../components/dialogs/show-hacs-dialog"; -import { repositoryMenuItems } from "../components/hacs-repository-owerflow-menu"; -import type { Hacs } from "../data/hacs"; -import type { RepositoryBase, RepositoryInfo } from "../data/repository"; -import { fetchRepositoryInformation } from "../data/repository"; -import { getRepositories, repositoryAdd } from "../data/websocket"; -import { HacsStyles } from "../styles/hacs-common-style"; -import { markdownWithRepositoryContext } from "../tools/markdown"; - -@customElement("hacs-repository-dashboard") -export class HacsRepositoryDashboard extends LitElement { - @property({ attribute: false }) public hacs!: Hacs; - - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public narrow!: boolean; - - @property({ attribute: false }) public isWide!: boolean; - - @property({ attribute: false }) public route!: Route; - - @state() public _repository?: RepositoryInfo; - - @state() private _error?: string; - - @query("#overflow-menu") - private _repositoryOverflowMenu!: HaMenu; - - public connectedCallback() { - super.connectedCallback(); - document.body.addEventListener("keydown", this._generateMyLink); - } - - public disconnectedCallback() { - super.disconnectedCallback(); - document.body.removeEventListener("keydown", this._generateMyLink); - } - - private _generateMyLink = (ev: KeyboardEvent) => { - if (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey) { - // Ignore if modifier keys are pressed - return; - } - if (ev.key === "m" && mainWindow.location.pathname.startsWith("/hacs/repository/")) { - if (!this._repository) { - return; - } - const myParams = new URLSearchParams({ - redirect: "hacs_repository", - owner: this._repository!.full_name.split("/")[0], - repository: this._repository!.full_name.split("/")[1], - category: this._repository!.category, - }); - window.open(`https://my.home-assistant.io/create-link/?${myParams.toString()}`, "_blank"); - } - }; - - protected async firstUpdated(changedProperties: PropertyValues): Promise { - super.firstUpdated(changedProperties); - - const params = extractSearchParamsObject(); - if (Object.entries(params).length) { - let existing: RepositoryBase | undefined; - const requestedRepository = `${params.owner}/${params.repository}`; - existing = this.hacs.repositories.find( - (repository) => - repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), - ); - if (!existing && params.category) { - if ( - !(await showConfirmationDialog(this, { - title: this.hacs.localize("my.add_repository_title"), - text: this.hacs.localize("my.add_repository_description", { - repository: requestedRepository, - }), - confirmText: this.hacs.localize("common.add"), - dismissText: this.hacs.localize("common.cancel"), - })) - ) { - this._error = this.hacs.localize("my.repository_not_found", { - repository: requestedRepository, - }); - return; - } - try { - await repositoryAdd(this.hass, requestedRepository, params.category); - this.hacs.repositories = await getRepositories(this.hass); - existing = this.hacs.repositories.find( - (repository) => - repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), - ); - } catch (err: any) { - this._error = err; - return; - } - } - if (existing) { - this._fetchRepository(String(existing.id)); - } else { - this._error = this.hacs.localize("my.repository_not_found", { - repository: requestedRepository, - }); - } - } else { - const dividerPos = this.route.path.indexOf("/", 1); - const repositoryId = this.route.path.substr(dividerPos + 1); - if (!repositoryId) { - this._error = "Missing repositoryId from route"; - return; - } - this._fetchRepository(repositoryId); - } - } - - protected updated(changedProps) { - super.updated(changedProps); - if (changedProps.has("repositories") && this._repository) { - this._fetchRepository(); - } - } - - private async _fetchRepository(repositoryId?: string) { - try { - this._repository = await fetchRepositoryInformation( - this.hass, - repositoryId || String(this._repository!.id), - ); - } catch (err: any) { - this._error = err?.message; - } - } - - private _getAuthors = memoizeOne((repository: RepositoryInfo) => { - const authors: string[] = []; - if (!repository.authors) return authors; - repository.authors.forEach((author) => authors.push(author.replace("@", ""))); - if (authors.length === 0) { - const author = repository.full_name.split("/")[0]; - if ( - ["custom-cards", "custom-components", "home-assistant-community-themes"].includes(author) - ) { - return authors; - } - authors.push(author); - } - return authors; - }); - - protected render(): TemplateResult { - if (this._error) { - return html``; - } - - if (!this._repository) { - return html``; - } - - const authors = this._getAuthors(this._repository); - - return html` - - -
- - - ${this._repository.installed - ? html` - - - - ` - : ""} - ${authors - ? authors.map( - (author) => - html` - - - @${author} - - `, - ) - : ""} - ${this._repository.downloads - ? html` - - ` - : ""} - - - ${this._repository.stars} - - - - - ${this._repository.issues} - - - - - -
- - ${!this._repository.installed_version - ? html` - - ` - : ""} -
- - ${repositoryMenuItems(this, this._repository, this.hacs.localize).map((entry) => - entry.divider - ? html`
  • ` - : html` - { - entry?.action && entry.action(); - }} - > - -
    ${entry.label}
    -
    - `, - )} -
    - `; - } - - private _showOverflowRepositoryMenu = (ev: any) => { - if ( - this._repositoryOverflowMenu.open && - ev.target === this._repositoryOverflowMenu.anchorElement - ) { - this._repositoryOverflowMenu.close(); - return; - } - this._repositoryOverflowMenu.anchorElement = ev.target; - this._repositoryOverflowMenu.show(); - }; - - private _downloadRepositoryDialog() { - showHacsDownloadDialog(this, { - hacs: this.hacs, - repositoryId: this._repository!.id, - repository: this._repository!, - }); - } - - static get styles() { - return [ - HacsStyles, - css` - hass-loading-screen { - --app-header-background-color: var(--sidebar-background-color); - --app-header-text-color: var(--sidebar-text-color); - height: 100vh; - } - - hass-subpage { - position: absolute; - width: 100vw; - } - - ha-fab ha-svg-icon { - color: var(--hcv-text-color-on-background); - } - - ha-fab { - position: fixed; - float: right; - right: calc(18px + env(safe-area-inset-right)); - bottom: calc(16px + env(safe-area-inset-bottom)); - z-index: 1; - } - - ha-fab.rtl { - float: left; - right: auto; - left: calc(18px + env(safe-area-inset-left)); - } - - ha-card { - display: block; - padding: 16px; - } - .content { - margin: auto; - padding: 8px; - max-width: 1536px; - } - - ha-chip-set { - padding-bottom: 8px; - } - - @media all and (max-width: 500px) { - .content { - margin: 8px 4px 64px; - max-width: none; - } - } - `, - ]; - } -} +import { + mdiAccount, + mdiArrowDownBold, + mdiCube, + mdiDotsVertical, + mdiDownload, + mdiExclamationThick, + mdiStar, +} from "@mdi/js"; +import type { PropertyValues, TemplateResult } from "lit"; +import { LitElement, css, html } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { mainWindow } from "../../homeassistant-frontend/src/common/dom/get_main_window"; +import { extractSearchParamsObject } from "../../homeassistant-frontend/src/common/url/search-params"; +import "../../homeassistant-frontend/src/components/chips/ha-assist-chip"; +import "../../homeassistant-frontend/src/components/chips/ha-chip-set"; +import "../../homeassistant-frontend/src/components/ha-alert"; +import "../../homeassistant-frontend/src/components/ha-card"; +import "../../homeassistant-frontend/src/components/ha-fab"; +import "../../homeassistant-frontend/src/components/ha-markdown"; +import "../../homeassistant-frontend/src/components/ha-menu"; +import type { HaMenu } from "../../homeassistant-frontend/src/components/ha-menu"; +import "../../homeassistant-frontend/src/components/ha-md-menu-item"; +import { showConfirmationDialog } from "../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; +import "../../homeassistant-frontend/src/layouts/hass-error-screen"; +import "../../homeassistant-frontend/src/layouts/hass-loading-screen"; +import "../../homeassistant-frontend/src/layouts/hass-subpage"; +import type { HomeAssistant, Route } from "../../homeassistant-frontend/src/types"; +import { showHacsDownloadDialog } from "../components/dialogs/show-hacs-dialog"; +import { repositoryMenuItems } from "../components/hacs-repository-owerflow-menu"; +import type { Hacs } from "../data/hacs"; +import type { RepositoryBase, RepositoryInfo } from "../data/repository"; +import { fetchRepositoryInformation } from "../data/repository"; +import { getRepositories, repositoryAdd } from "../data/websocket"; +import { HacsStyles } from "../styles/hacs-common-style"; +import { markdownWithRepositoryContext } from "../tools/markdown"; + +@customElement("hacs-repository-dashboard") +export class HacsRepositoryDashboard extends LitElement { + @property({ attribute: false }) public hacs!: Hacs; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public narrow!: boolean; + + @property({ attribute: false }) public isWide!: boolean; + + @property({ attribute: false }) public route!: Route; + + @state() public _repository?: RepositoryInfo; + + @state() private _error?: string; + + @query("#overflow-menu") + private _repositoryOverflowMenu!: HaMenu; + + public connectedCallback() { + super.connectedCallback(); + document.body.addEventListener("keydown", this._generateMyLink); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + document.body.removeEventListener("keydown", this._generateMyLink); + } + + private _generateMyLink = (ev: KeyboardEvent) => { + if (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey) { + // Ignore if modifier keys are pressed + return; + } + if (ev.key === "m" && mainWindow.location.pathname.startsWith("/hacs/repository/")) { + if (!this._repository) { + return; + } + const myParams = new URLSearchParams({ + redirect: "hacs_repository", + owner: this._repository!.full_name.split("/")[0], + repository: this._repository!.full_name.split("/")[1], + category: this._repository!.category, + }); + window.open(`https://my.home-assistant.io/create-link/?${myParams.toString()}`, "_blank"); + } + }; + + protected async firstUpdated(changedProperties: PropertyValues): Promise { + super.firstUpdated(changedProperties); + + const params = extractSearchParamsObject(); + if (Object.entries(params).length) { + let existing: RepositoryBase | undefined; + const requestedRepository = `${params.owner}/${params.repository}`; + existing = this.hacs.repositories.find( + (repository) => + repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), + ); + if (!existing && params.category) { + if ( + !(await showConfirmationDialog(this, { + title: this.hacs.localize("my.add_repository_title"), + text: this.hacs.localize("my.add_repository_description", { + repository: requestedRepository, + }), + confirmText: this.hacs.localize("common.add"), + dismissText: this.hacs.localize("common.cancel"), + })) + ) { + this._error = this.hacs.localize("my.repository_not_found", { + repository: requestedRepository, + }); + return; + } + try { + await repositoryAdd(this.hass, requestedRepository, params.category); + this.hacs.repositories = await getRepositories(this.hass); + existing = this.hacs.repositories.find( + (repository) => + repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), + ); + } catch (err: any) { + this._error = err; + return; + } + } + if (existing) { + this._fetchRepository(String(existing.id)); + } else { + this._error = this.hacs.localize("my.repository_not_found", { + repository: requestedRepository, + }); + } + } else { + const dividerPos = this.route.path.indexOf("/", 1); + const repositoryId = this.route.path.substr(dividerPos + 1); + if (!repositoryId) { + this._error = "Missing repositoryId from route"; + return; + } + this._fetchRepository(repositoryId); + } + } + + protected updated(changedProps) { + super.updated(changedProps); + if (changedProps.has("repositories") && this._repository) { + this._fetchRepository(); + } + // Reload repository information when language changes to show correct README + if (changedProps.has("hass") && this._repository) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (oldHass?.language !== this.hass.language) { + this._fetchRepository(); + } + } + } + + private async _fetchRepository(repositoryId?: string) { + try { + this._repository = await fetchRepositoryInformation( + this.hass, + repositoryId || String(this._repository!.id), + this.hass.language, + ); + } catch (err: any) { + this._error = err?.message; + } + } + + private _getAuthors = memoizeOne((repository: RepositoryInfo) => { + const authors: string[] = []; + if (!repository.authors) return authors; + repository.authors.forEach((author) => authors.push(author.replace("@", ""))); + if (authors.length === 0) { + const author = repository.full_name.split("/")[0]; + if ( + ["custom-cards", "custom-components", "home-assistant-community-themes"].includes(author) + ) { + return authors; + } + authors.push(author); + } + return authors; + }); + + protected render(): TemplateResult { + if (this._error) { + return html``; + } + + if (!this._repository) { + return html``; + } + + const authors = this._getAuthors(this._repository); + + return html` + + +
    + + + ${this._repository.installed + ? html` + + + + ` + : ""} + ${authors + ? authors.map( + (author) => + html` + + + @${author} + + `, + ) + : ""} + ${this._repository.downloads + ? html` + + ` + : ""} + + + ${this._repository.stars} + + + + + ${this._repository.issues} + + + + + +
    + + ${!this._repository.installed_version + ? html` + + ` + : ""} +
    + + ${repositoryMenuItems(this, this._repository, this.hacs.localize).map((entry) => + entry.divider + ? html`
  • ` + : html` + { + entry?.action && entry.action(); + }} + > + +
    ${entry.label}
    +
    + `, + )} +
    + `; + } + + private _showOverflowRepositoryMenu = (ev: any) => { + if ( + this._repositoryOverflowMenu.open && + ev.target === this._repositoryOverflowMenu.anchorElement + ) { + this._repositoryOverflowMenu.close(); + return; + } + this._repositoryOverflowMenu.anchorElement = ev.target; + this._repositoryOverflowMenu.show(); + }; + + private _downloadRepositoryDialog() { + showHacsDownloadDialog(this, { + hacs: this.hacs, + repositoryId: this._repository!.id, + repository: this._repository!, + }); + } + + static get styles() { + return [ + HacsStyles, + css` + hass-loading-screen { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + height: 100vh; + } + + hass-subpage { + position: absolute; + width: 100vw; + } + + ha-fab ha-svg-icon { + color: var(--hcv-text-color-on-background); + } + + ha-fab { + position: fixed; + float: right; + right: calc(18px + env(safe-area-inset-right)); + bottom: calc(16px + env(safe-area-inset-bottom)); + z-index: 1; + } + + ha-fab.rtl { + float: left; + right: auto; + left: calc(18px + env(safe-area-inset-left)); + } + + ha-card { + display: block; + padding: 16px; + } + .content { + margin: auto; + padding: 8px; + max-width: 1536px; + } + + ha-chip-set { + padding-bottom: 8px; + } + + @media all and (max-width: 500px) { + .content { + margin: 8px 4px 64px; + max-width: none; + } + } + `, + ]; + } +} diff --git a/src/data/repository.ts b/src/data/repository.ts index e83acd2a0..e90512fc3 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -1,78 +1,186 @@ -import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; - -export type RepositoryType = - | "appdaemon" - | "integration" - | "netdaemon" - | "plugin" - | "python_script" - | "template" - | "theme"; - -export interface RepositoryBase { - authors: string[]; - available_version: string; - can_download: boolean; - category: RepositoryType; - config_flow: boolean; - country: string[]; - custom: boolean; - description: string; - domain: string | null; - downloads: number; - file_name: string; - full_name: string; - hide: boolean; - homeassistant: string | null; - id: string; - installed_version: string; - installed: boolean; - last_updated: string; - local_path: string; - name: string; - new: boolean; - pending_upgrade: boolean; - stars: number; - state: string; - status: "pending-restart" | "pending-upgrade" | "new" | "installed" | "default"; - topics: string[]; -} - -export interface RepositoryInfo extends RepositoryBase { - additional_info: string; - default_branch: string; - hide_default_branch: boolean; - issues: number; - releases: string[]; - ref: string; - selected_tag: string | null; - version_or_commit: "version" | "commit"; -} - -export const fetchRepositoryInformation = async ( - hass: HomeAssistant, - repositoryId: string, -): Promise => - hass.connection.sendMessagePromise({ - type: "hacs/repository/info", - repository_id: repositoryId, - }); - -export const repositoryDownloadVersion = async ( - hass: HomeAssistant, - repository: string, - version?: string, -) => - hass.connection.sendMessagePromise({ - type: "hacs/repository/download", - repository: repository, - version, - }); - -export const repositoryReleases = async (hass: HomeAssistant, repositoryId: string) => - hass.connection.sendMessagePromise< - { tag: string; name: string; published_at: string; prerelease: boolean }[] - >({ - type: "hacs/repository/releases", - repository_id: repositoryId, - }); +import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; + +/** + * Extract base language code from BCP47 language format. + * Examples: "de-DE" -> "de", "en-US" -> "en", "fr" -> "fr" + * @param language - BCP47 language code (e.g., "de-DE", "en-US", "fr") + * @returns Base language code in lowercase (e.g., "de", "en", "fr") + */ +export const getBaseLanguageCode = (language: string | undefined): string => { + if (!language) { + return "en"; + } + return language.split("-")[0].toLowerCase(); +}; + +/** + * Check if backend supports language parameter for multilingual README. + * This is cached per session to avoid repeated failed requests. + */ +let backendSupportsLanguage: boolean | null = null; + +/** + * Reset the backend language support cache. + * Useful for testing or when backend is updated. + */ +export const resetBackendLanguageSupportCache = () => { + backendSupportsLanguage = null; +}; + +export type RepositoryType = + | "appdaemon" + | "integration" + | "netdaemon" + | "plugin" + | "python_script" + | "template" + | "theme"; + +export interface RepositoryBase { + authors: string[]; + available_version: string; + can_download: boolean; + category: RepositoryType; + config_flow: boolean; + country: string[]; + custom: boolean; + description: string; + domain: string | null; + downloads: number; + file_name: string; + full_name: string; + hide: boolean; + homeassistant: string | null; + id: string; + installed_version: string; + installed: boolean; + last_updated: string; + local_path: string; + name: string; + new: boolean; + pending_upgrade: boolean; + stars: number; + state: string; + status: "pending-restart" | "pending-upgrade" | "new" | "installed" | "default"; + topics: string[]; +} + +export interface RepositoryInfo extends RepositoryBase { + additional_info: string; + default_branch: string; + hide_default_branch: boolean; + issues: number; + releases: string[]; + ref: string; + selected_tag: string | null; + version_or_commit: "version" | "commit"; +} + +/** + * Fetch repository information from HACS backend. + * Supports multilingual README files based on Home Assistant language setting. + * + * This function works for both custom and standard integrations: + * - Custom Integration: Works with HACS custom integration + * - Standard Integration: Works when HACS becomes a standard Home Assistant integration + * + * The language parameter is optional and backward compatible: + * - If the backend doesn't support the language parameter, it will return the default README.md + * - If the backend supports it, it will return the language-specific README (e.g., README.de.md) + * + * @param hass - Home Assistant instance + * @param repositoryId - Repository ID + * @param language - Optional language override (defaults to hass.language) + * @returns Repository information with additional_info (README content) + */ +export const fetchRepositoryInformation = async ( + hass: HomeAssistant, + repositoryId: string, + language?: string, +): Promise => { + // Get language from parameter or hass.language + const languageToUse = language ?? hass.language; + const baseLanguage = getBaseLanguageCode(languageToUse); + + // Only send language if it's not English (English uses default README.md) + // The language parameter is optional and backward compatible: + // - If backend doesn't support it, it will be ignored and default README.md is returned + // - If backend supports it, it will return the language-specific README + const message: any = { + type: "hacs/repository/info", + repository_id: repositoryId, + }; + + // Only send language parameter if backend supports it + // Check cache first, then try sending if not yet determined + if (baseLanguage && baseLanguage !== "en") { + if (backendSupportsLanguage === null) { + // First time: try sending the parameter + message.language = baseLanguage; + console.log(`[HACS] Sending language parameter: "${baseLanguage}" (first attempt)`); + } else if (backendSupportsLanguage === true) { + // Backend supports it: send the parameter + message.language = baseLanguage; + console.log(`[HACS] Sending language parameter: "${baseLanguage}" (backend supports it)`); + } else { + // Backend doesn't support it: don't send the parameter + console.log(`[HACS] Skipping language parameter (backend doesn't support it)`); + } + } + + try { + const result = await hass.connection.sendMessagePromise(message); + + // If we sent the language parameter and got a result, backend supports it + if (message.language && backendSupportsLanguage === null) { + backendSupportsLanguage = true; + console.log(`[HACS] Backend accepted language parameter "${message.language}" - caching support`); + } + + return result; + } catch (error: any) { + // Check if error is about extra keys (backend doesn't support language parameter) + const errorMessage = error?.message || String(error); + console.log(`[HACS] Error received:`, errorMessage); + + if ( + errorMessage.includes("extra keys not allowed") && + (errorMessage.includes("language") || errorMessage.includes("'language'")) + ) { + // Backend doesn't support language parameter + backendSupportsLanguage = false; + console.log(`[HACS] Backend rejected language parameter - caching rejection and retrying without it`); + + // Retry without language parameter + const messageWithoutLanguage: any = { + type: "hacs/repository/info", + repository_id: repositoryId, + }; + + return hass.connection.sendMessagePromise(messageWithoutLanguage); + } + + // Re-throw other errors + console.log(`[HACS] Error is not related to language parameter, re-throwing`); + throw error; + } +}; + +export const repositoryDownloadVersion = async ( + hass: HomeAssistant, + repository: string, + version?: string, +) => + hass.connection.sendMessagePromise({ + type: "hacs/repository/download", + repository: repository, + version, + }); + +export const repositoryReleases = async (hass: HomeAssistant, repositoryId: string) => + hass.connection.sendMessagePromise< + { tag: string; name: string; published_at: string; prerelease: boolean }[] + >({ + type: "hacs/repository/releases", + repository_id: repositoryId, + }); From bea0e5231cca48633dfcd844962e62761e952786 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 21:21:03 +0100 Subject: [PATCH 02/24] Fix race condition and language change detection bugs - Fix race condition in backendSupportsLanguage cache: Add promise-based synchronization to prevent concurrent requests from corrupting the cache state. Only set cache if still null to protect against race conditions. - Fix false language change detection: Only refetch repository when oldHass exists and language actually changed, preventing unnecessary API calls on initial property changes. Fixes issues where: 1. Concurrent requests with different languages could corrupt the cache 2. Repository was refetched unnecessarily when oldHass was undefined --- src/dashboards/hacs-repository-dashboard.ts | 4 +- src/data/repository.ts | 129 ++++++++++++++------ 2 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/dashboards/hacs-repository-dashboard.ts b/src/dashboards/hacs-repository-dashboard.ts index 09bfc3940..2d76b9e9c 100644 --- a/src/dashboards/hacs-repository-dashboard.ts +++ b/src/dashboards/hacs-repository-dashboard.ts @@ -149,7 +149,9 @@ export class HacsRepositoryDashboard extends LitElement { // Reload repository information when language changes to show correct README if (changedProps.has("hass") && this._repository) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (oldHass?.language !== this.hass.language) { + // Only refetch if oldHass exists and language actually changed + // Skip if oldHass is undefined (first property change or object replacement) + if (oldHass && oldHass.language !== this.hass.language) { this._fetchRepository(); } } diff --git a/src/data/repository.ts b/src/data/repository.ts index e90512fc3..48e73d813 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -19,12 +19,20 @@ export const getBaseLanguageCode = (language: string | undefined): string => { */ let backendSupportsLanguage: boolean | null = null; +/** + * Promise that tracks the ongoing request to determine backend language support. + * This prevents race conditions when multiple concurrent requests try to determine + * backend support simultaneously. + */ +let backendSupportCheckPromise: Promise | null = null; + /** * Reset the backend language support cache. * Useful for testing or when backend is updated. */ export const resetBackendLanguageSupportCache = () => { backendSupportsLanguage = null; + backendSupportCheckPromise = null; }; export type RepositoryType = @@ -111,57 +119,102 @@ export const fetchRepositoryInformation = async ( repository_id: repositoryId, }; - // Only send language parameter if backend supports it - // Check cache first, then try sending if not yet determined + // Determine if we should send the language parameter if (baseLanguage && baseLanguage !== "en") { - if (backendSupportsLanguage === null) { - // First time: try sending the parameter - message.language = baseLanguage; - console.log(`[HACS] Sending language parameter: "${baseLanguage}" (first attempt)`); - } else if (backendSupportsLanguage === true) { + if (backendSupportsLanguage === true) { // Backend supports it: send the parameter message.language = baseLanguage; console.log(`[HACS] Sending language parameter: "${baseLanguage}" (backend supports it)`); - } else { + } else if (backendSupportsLanguage === false) { // Backend doesn't support it: don't send the parameter console.log(`[HACS] Skipping language parameter (backend doesn't support it)`); + } else { + // Cache is null: need to determine backend support + // Wait for any ongoing check to complete first to prevent race conditions + if (backendSupportCheckPromise) { + console.log(`[HACS] Waiting for ongoing backend support check...`); + await backendSupportCheckPromise; + + // After waiting, check cache again (another request might have set it) + if (backendSupportsLanguage === true) { + message.language = baseLanguage; + console.log(`[HACS] Sending language parameter: "${baseLanguage}" (backend supports it - from concurrent request)`); + } else if (backendSupportsLanguage === false) { + console.log(`[HACS] Skipping language parameter (backend doesn't support it - from concurrent request)`); + } + // If still null after waiting, another request is handling it - proceed without language + } else { + // No ongoing check: create a promise to track this check and prevent race conditions + let resolveCheck: () => void; + backendSupportCheckPromise = new Promise((resolve) => { + resolveCheck = resolve; + }); + + message.language = baseLanguage; + console.log(`[HACS] Sending language parameter: "${baseLanguage}" (first attempt)`); + + try { + const result = await hass.connection.sendMessagePromise(message); + + // Success: backend supports the language parameter + // Only set to true if still null (protect against concurrent modifications) + if (backendSupportsLanguage === null) { + backendSupportsLanguage = true; + console.log(`[HACS] Backend accepted language parameter "${message.language}" - caching support`); + } + + // Resolve the check promise + resolveCheck!(); + backendSupportCheckPromise = null; + + return result; + } catch (error: any) { + // Check if error is about extra keys (backend doesn't support language parameter) + const errorMessage = error?.message || String(error); + console.log(`[HACS] Error received:`, errorMessage); + + if ( + errorMessage.includes("extra keys not allowed") && + (errorMessage.includes("language") || errorMessage.includes("'language'")) + ) { + // Backend doesn't support language parameter + // Only set to false if still null (protect against concurrent successful requests) + if (backendSupportsLanguage === null) { + backendSupportsLanguage = false; + console.log(`[HACS] Backend rejected language parameter - caching rejection and retrying without it`); + } + + // Resolve the check promise + resolveCheck!(); + backendSupportCheckPromise = null; + + // Retry without language parameter + const messageWithoutLanguage: any = { + type: "hacs/repository/info", + repository_id: repositoryId, + }; + + return hass.connection.sendMessagePromise(messageWithoutLanguage); + } + + // Resolve the check promise (even on error, so other requests can proceed) + resolveCheck!(); + backendSupportCheckPromise = null; + + // Re-throw other errors + console.log(`[HACS] Error is not related to language parameter, re-throwing`); + throw error; + } + } } } + // Make the request (either with or without language parameter) try { const result = await hass.connection.sendMessagePromise(message); - - // If we sent the language parameter and got a result, backend supports it - if (message.language && backendSupportsLanguage === null) { - backendSupportsLanguage = true; - console.log(`[HACS] Backend accepted language parameter "${message.language}" - caching support`); - } - return result; } catch (error: any) { - // Check if error is about extra keys (backend doesn't support language parameter) - const errorMessage = error?.message || String(error); - console.log(`[HACS] Error received:`, errorMessage); - - if ( - errorMessage.includes("extra keys not allowed") && - (errorMessage.includes("language") || errorMessage.includes("'language'")) - ) { - // Backend doesn't support language parameter - backendSupportsLanguage = false; - console.log(`[HACS] Backend rejected language parameter - caching rejection and retrying without it`); - - // Retry without language parameter - const messageWithoutLanguage: any = { - type: "hacs/repository/info", - repository_id: repositoryId, - }; - - return hass.connection.sendMessagePromise(messageWithoutLanguage); - } - - // Re-throw other errors - console.log(`[HACS] Error is not related to language parameter, re-throwing`); + // Re-throw errors throw error; } }; From cfc252791716a116a028c591dfca2f04fd7f1630 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 21:22:27 +0100 Subject: [PATCH 03/24] Update PR description with bug fixes documentation --- PULL_REQUEST.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index d830fa0ea..b88c774eb 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -27,12 +27,14 @@ The backend must support the optional `language` parameter in the `hacs/reposito - First request: Attempts to send `language` parameter - On success: Caches backend support and continues sending parameter - On error (unsupported parameter): Caches rejection and retries without parameter + - **Race condition protection**: Uses promise-based synchronization to prevent concurrent requests from corrupting the cache state - Fully backward compatible: Works with both old and new backend versions 3. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) - Updated `_fetchRepository()` to pass `hass.language` to `fetchRepositoryInformation()` - Added language change detection in `updated()` lifecycle hook - Automatically reloads repository information when user changes Home Assistant language + - **Fixed false positive detection**: Only refetches when language actually changed (prevents unnecessary API calls on initial property changes) 4. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) - Updated `_fetchRepository()` to pass `hass.language` for consistency @@ -146,12 +148,18 @@ The implementation uses a session-based cache to avoid repeated failed requests: ```typescript let backendSupportsLanguage: boolean | null = null; +let backendSupportCheckPromise: Promise | null = null; ``` - `null`: Not yet determined (will attempt to send parameter) - `true`: Backend supports it (will send parameter) - `false`: Backend doesn't support it (will skip parameter) +**Race Condition Protection:** +- Uses `backendSupportCheckPromise` to synchronize concurrent requests +- Only sets cache if still `null` to prevent corruption from race conditions +- Concurrent requests wait for the first check to complete before proceeding + This cache is reset on page reload and can be manually reset using `resetBackendLanguageSupportCache()` for testing. ## Alignment with Home Assistant Standards @@ -184,6 +192,20 @@ This implementation follows Home Assistant's translation system patterns: _Add screenshots showing multilingual README display if available_ +## Bug Fixes + +This PR includes fixes for two critical bugs discovered during implementation: + +1. **Race Condition in Backend Support Cache** (`src/data/repository.ts`) + - **Issue**: Concurrent requests with different languages could corrupt the cache state + - **Fix**: Implemented promise-based synchronization to ensure only one request determines backend support at a time + - **Protection**: Cache is only set if still `null`, preventing concurrent modifications + +2. **False Language Change Detection** (`src/dashboards/hacs-repository-dashboard.ts`) + - **Issue**: Repository was refetched unnecessarily when `oldHass` was `undefined` (first property change) + - **Fix**: Added check to ensure `oldHass` exists before comparing languages + - **Result**: Eliminates unnecessary API calls on initial component updates + ## Notes - This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. From 5a3af08eb6e40dfabce44b418805ea09ebaccaa7 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 21:43:22 +0100 Subject: [PATCH 04/24] Fix: Remove language parameter from message when backend doesn't support it When waiting for a concurrent backend support check, if the backend rejects the language parameter, the code now explicitly removes it from the message object using delete message.language. This prevents the parameter from being sent anyway, which would cause repeated backend errors. Fixes race condition where concurrent requests could leave language parameter in message even after discovering backend doesn't support it. --- src/data/repository.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data/repository.ts b/src/data/repository.ts index 48e73d813..4737d31d5 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -140,6 +140,8 @@ export const fetchRepositoryInformation = async ( message.language = baseLanguage; console.log(`[HACS] Sending language parameter: "${baseLanguage}" (backend supports it - from concurrent request)`); } else if (backendSupportsLanguage === false) { + // Explicitly ensure language is not in message to avoid sending it + delete message.language; console.log(`[HACS] Skipping language parameter (backend doesn't support it - from concurrent request)`); } // If still null after waiting, another request is handling it - proceed without language From ae029a738b93d55d4dd28b5b29520065e2374855 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 21:43:31 +0100 Subject: [PATCH 05/24] docs: Add bug fix #3 to pull request description --- PULL_REQUEST.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index b88c774eb..1371c5724 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -194,7 +194,7 @@ _Add screenshots showing multilingual README display if available_ ## Bug Fixes -This PR includes fixes for two critical bugs discovered during implementation: +This PR includes fixes for three critical bugs discovered during implementation: 1. **Race Condition in Backend Support Cache** (`src/data/repository.ts`) - **Issue**: Concurrent requests with different languages could corrupt the cache state @@ -206,6 +206,11 @@ This PR includes fixes for two critical bugs discovered during implementation: - **Fix**: Added check to ensure `oldHass` exists before comparing languages - **Result**: Eliminates unnecessary API calls on initial component updates +3. **Language Parameter Not Removed After Backend Rejection** (`src/data/repository.ts`) + - **Issue**: When waiting for a concurrent backend support check, if the backend rejects the language parameter, the code logged "Skipping language parameter" but didn't actually remove it from the message object. The message still contained the language property, which then got sent anyway, causing repeated backend errors. + - **Fix**: Added `delete message.language;` when `backendSupportsLanguage === false` after waiting for concurrent check + - **Result**: Prevents language parameter from being sent when backend doesn't support it, eliminating repeated errors + ## Notes - This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. From 3180c9da1a68eedb6db04934f4da45850c20a4bd Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 21:47:01 +0100 Subject: [PATCH 06/24] docs: Update backend PR reference to #4965 --- PULL_REQUEST.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 1371c5724..15da5e5e6 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -7,7 +7,7 @@ This PR adds support for automatic language detection and display of multilingua ## Related Backend PR This frontend implementation requires the corresponding backend changes. Please see: -- **Backend PR:** https://github.com/hacs/integration/pull/4964 +- **Backend PR:** https://github.com/hacs/integration/pull/4965 The backend must support the optional `language` parameter in the `hacs/repository/info` WebSocket command to fully enable this feature. From 557f8e34bba3907d5ab226952a03b91dd0cf6978 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 22:02:39 +0100 Subject: [PATCH 07/24] chore: Add debug files to .gitignore Ignore debug documentation and test scripts used during development: - DEBUG_WEBSOCKET.md - SAFARI_WEBSOCKET_DEBUGGING.md - test-multilingual-readme.js --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index a310dbff9..ad5a83670 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ yarn-error.log .yarn/* !.yarn/releases !.yarn/releases/yarn-*.cjs + +# Debug files for multilingual README development +DEBUG_WEBSOCKET.md +SAFARI_WEBSOCKET_DEBUGGING.md +test-multilingual-readme.js From b5cb425cd6188a999c035c42163657e4c54c86c9 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 22:04:49 +0100 Subject: [PATCH 08/24] Fix: Handle null cache after concurrent backend support check When a concurrent request waits for another request's backend support check to complete, if the cache is still null after waiting (indicating the previous request encountered a non-language-related error or didn't complete properly), the code now attempts its own backend support check instead of skipping the language parameter entirely. This prevents a scenario where all concurrent requests give up on determining backend support, causing the cache to never be set and all requests to skip the language parameter regardless of backend capability. Fixes issue where concurrent requests could leave cache in null state permanently if first request failed with non-language-related error. --- src/data/repository.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/data/repository.ts b/src/data/repository.ts index 4737d31d5..3b4c6f7d5 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -144,8 +144,15 @@ export const fetchRepositoryInformation = async ( delete message.language; console.log(`[HACS] Skipping language parameter (backend doesn't support it - from concurrent request)`); } - // If still null after waiting, another request is handling it - proceed without language - } else { + // If still null after waiting, the previous request encountered a non-language-related error + // or didn't complete properly. This request should attempt to determine backend support. + } + + // If cache is still null (either no promise existed, or previous check failed), perform our own check + if (backendSupportsLanguage === null) { + if (backendSupportCheckPromise) { + console.log(`[HACS] Cache still null after waiting - previous check may have failed, attempting our own check`); + } // No ongoing check: create a promise to track this check and prevent race conditions let resolveCheck: () => void; backendSupportCheckPromise = new Promise((resolve) => { From 64c566023c39dff13f9d58139977d2371bf85959 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 22:05:23 +0100 Subject: [PATCH 09/24] docs: Update PR description with bug fix #4 --- PULL_REQUEST.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 15da5e5e6..302d67941 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -194,7 +194,7 @@ _Add screenshots showing multilingual README display if available_ ## Bug Fixes -This PR includes fixes for three critical bugs discovered during implementation: +This PR includes fixes for four critical bugs discovered during implementation: 1. **Race Condition in Backend Support Cache** (`src/data/repository.ts`) - **Issue**: Concurrent requests with different languages could corrupt the cache state @@ -211,6 +211,11 @@ This PR includes fixes for three critical bugs discovered during implementation: - **Fix**: Added `delete message.language;` when `backendSupportsLanguage === false` after waiting for concurrent check - **Result**: Prevents language parameter from being sent when backend doesn't support it, eliminating repeated errors +4. **Null Cache After Concurrent Check** (`src/data/repository.ts`) + - **Issue**: When a concurrent request waits for another request's backend support check to complete, if the cache is still `null` after waiting (indicating the previous request encountered a non-language-related error or didn't complete properly), the code would skip sending the `language` parameter entirely. This meant no request would attempt to determine backend support on subsequent concurrent requests, preventing the cache from being set and causing all requests to eventually skip the language parameter regardless of backend capability. + - **Fix**: Modified logic to perform our own backend support check if cache is still `null` after waiting for concurrent check + - **Result**: Ensures backend support is always determined, even if previous concurrent checks failed with non-language-related errors + ## Notes - This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. From d0b8454a6708ec5bb28a327b50fa59c460c4d75c Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 22:09:28 +0100 Subject: [PATCH 10/24] Fix: Prevent duplicate requests after concurrent backend support check When waiting for a concurrent backend support check to complete, if the cache is set to true or false, the code was modifying the message object but then falling through to line 223 where it would send a duplicate request. This resulted in sending two requests when only one was needed. Fix: After waiting for concurrent check and setting message based on cache state (true or false), immediately send the request and return, preventing the code from falling through to the final request sending code at line 235. This ensures that: - If cache is true after waiting: send request with language parameter and return - If cache is false after waiting: send request without language parameter and return - If cache is still null after waiting: proceed with our own check (existing logic) --- src/data/repository.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/data/repository.ts b/src/data/repository.ts index 3b4c6f7d5..6287df00d 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -139,10 +139,24 @@ export const fetchRepositoryInformation = async ( if (backendSupportsLanguage === true) { message.language = baseLanguage; console.log(`[HACS] Sending language parameter: "${baseLanguage}" (backend supports it - from concurrent request)`); + // Send the request with language parameter and return (don't fall through to avoid duplicate request) + try { + const result = await hass.connection.sendMessagePromise(message); + return result; + } catch (error: any) { + throw error; + } } else if (backendSupportsLanguage === false) { // Explicitly ensure language is not in message to avoid sending it delete message.language; console.log(`[HACS] Skipping language parameter (backend doesn't support it - from concurrent request)`); + // Send the request without language parameter and return (don't fall through to avoid duplicate request) + try { + const result = await hass.connection.sendMessagePromise(message); + return result; + } catch (error: any) { + throw error; + } } // If still null after waiting, the previous request encountered a non-language-related error // or didn't complete properly. This request should attempt to determine backend support. From 109ab87ebd29926c7838f4c738b5a820a345036d Mon Sep 17 00:00:00 2001 From: rosch100 Date: Mon, 1 Dec 2025 22:09:41 +0100 Subject: [PATCH 11/24] docs: Add bug fix #5 to PR description --- PULL_REQUEST.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 302d67941..793be49e5 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -194,7 +194,7 @@ _Add screenshots showing multilingual README display if available_ ## Bug Fixes -This PR includes fixes for four critical bugs discovered during implementation: +This PR includes fixes for five critical bugs discovered during implementation: 1. **Race Condition in Backend Support Cache** (`src/data/repository.ts`) - **Issue**: Concurrent requests with different languages could corrupt the cache state @@ -216,6 +216,11 @@ This PR includes fixes for four critical bugs discovered during implementation: - **Fix**: Modified logic to perform our own backend support check if cache is still `null` after waiting for concurrent check - **Result**: Ensures backend support is always determined, even if previous concurrent checks failed with non-language-related errors +5. **Duplicate Requests After Concurrent Check** (`src/data/repository.ts`) + - **Issue**: When waiting for a concurrent backend support check to complete, if the cache was set to `true` or `false`, the code would modify the `message` object but then fall through to the final request sending code at line 235, resulting in sending a duplicate request. This caused unnecessary network traffic and potential race conditions. + - **Fix**: After waiting for concurrent check and setting message based on cache state, immediately send the request and return, preventing fall-through to duplicate request code + - **Result**: Eliminates duplicate requests when cache is already determined by concurrent check, reducing network overhead and preventing potential race conditions + ## Notes - This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. From 32ba636580467f19ed3bdc8fc79b7ac6cd3d1d44 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 11:53:29 +0100 Subject: [PATCH 12/24] Refactor: Move multilingual README logic to backend - Remove all language processing logic from frontend - Frontend now only passes hass.language to backend - Remove getBaseLanguageCode() and backend support caching - Simplify fetchRepositoryInformation() to pass language directly --- script/build | 15 ---- src/data/repository.ts | 187 ++--------------------------------------- 2 files changed, 6 insertions(+), 196 deletions(-) delete mode 100755 script/build diff --git a/script/build b/script/build deleted file mode 100755 index 83d0bdb98..000000000 --- a/script/build +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Run the frontend development server - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -if [ ! -d "./node_modules" ]; then - echo "Directory /node_modules DOES NOT exists." - echo "Running yarn install" - yarn install -fi - -NODE_OPTIONS=--max_old_space_size=6144 ./node_modules/.bin/gulp build-hacs \ No newline at end of file diff --git a/src/data/repository.ts b/src/data/repository.ts index 6287df00d..c03fc97dd 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -1,40 +1,5 @@ import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; -/** - * Extract base language code from BCP47 language format. - * Examples: "de-DE" -> "de", "en-US" -> "en", "fr" -> "fr" - * @param language - BCP47 language code (e.g., "de-DE", "en-US", "fr") - * @returns Base language code in lowercase (e.g., "de", "en", "fr") - */ -export const getBaseLanguageCode = (language: string | undefined): string => { - if (!language) { - return "en"; - } - return language.split("-")[0].toLowerCase(); -}; - -/** - * Check if backend supports language parameter for multilingual README. - * This is cached per session to avoid repeated failed requests. - */ -let backendSupportsLanguage: boolean | null = null; - -/** - * Promise that tracks the ongoing request to determine backend language support. - * This prevents race conditions when multiple concurrent requests try to determine - * backend support simultaneously. - */ -let backendSupportCheckPromise: Promise | null = null; - -/** - * Reset the backend language support cache. - * Useful for testing or when backend is updated. - */ -export const resetBackendLanguageSupportCache = () => { - backendSupportsLanguage = null; - backendSupportCheckPromise = null; -}; - export type RepositoryType = | "appdaemon" | "integration" @@ -84,162 +49,22 @@ export interface RepositoryInfo extends RepositoryBase { version_or_commit: "version" | "commit"; } -/** - * Fetch repository information from HACS backend. - * Supports multilingual README files based on Home Assistant language setting. - * - * This function works for both custom and standard integrations: - * - Custom Integration: Works with HACS custom integration - * - Standard Integration: Works when HACS becomes a standard Home Assistant integration - * - * The language parameter is optional and backward compatible: - * - If the backend doesn't support the language parameter, it will return the default README.md - * - If the backend supports it, it will return the language-specific README (e.g., README.de.md) - * - * @param hass - Home Assistant instance - * @param repositoryId - Repository ID - * @param language - Optional language override (defaults to hass.language) - * @returns Repository information with additional_info (README content) - */ export const fetchRepositoryInformation = async ( hass: HomeAssistant, repositoryId: string, language?: string, ): Promise => { - // Get language from parameter or hass.language - const languageToUse = language ?? hass.language; - const baseLanguage = getBaseLanguageCode(languageToUse); - - // Only send language if it's not English (English uses default README.md) - // The language parameter is optional and backward compatible: - // - If backend doesn't support it, it will be ignored and default README.md is returned - // - If backend supports it, it will return the language-specific README const message: any = { type: "hacs/repository/info", repository_id: repositoryId, }; - - // Determine if we should send the language parameter - if (baseLanguage && baseLanguage !== "en") { - if (backendSupportsLanguage === true) { - // Backend supports it: send the parameter - message.language = baseLanguage; - console.log(`[HACS] Sending language parameter: "${baseLanguage}" (backend supports it)`); - } else if (backendSupportsLanguage === false) { - // Backend doesn't support it: don't send the parameter - console.log(`[HACS] Skipping language parameter (backend doesn't support it)`); - } else { - // Cache is null: need to determine backend support - // Wait for any ongoing check to complete first to prevent race conditions - if (backendSupportCheckPromise) { - console.log(`[HACS] Waiting for ongoing backend support check...`); - await backendSupportCheckPromise; - - // After waiting, check cache again (another request might have set it) - if (backendSupportsLanguage === true) { - message.language = baseLanguage; - console.log(`[HACS] Sending language parameter: "${baseLanguage}" (backend supports it - from concurrent request)`); - // Send the request with language parameter and return (don't fall through to avoid duplicate request) - try { - const result = await hass.connection.sendMessagePromise(message); - return result; - } catch (error: any) { - throw error; - } - } else if (backendSupportsLanguage === false) { - // Explicitly ensure language is not in message to avoid sending it - delete message.language; - console.log(`[HACS] Skipping language parameter (backend doesn't support it - from concurrent request)`); - // Send the request without language parameter and return (don't fall through to avoid duplicate request) - try { - const result = await hass.connection.sendMessagePromise(message); - return result; - } catch (error: any) { - throw error; - } - } - // If still null after waiting, the previous request encountered a non-language-related error - // or didn't complete properly. This request should attempt to determine backend support. - } - - // If cache is still null (either no promise existed, or previous check failed), perform our own check - if (backendSupportsLanguage === null) { - if (backendSupportCheckPromise) { - console.log(`[HACS] Cache still null after waiting - previous check may have failed, attempting our own check`); - } - // No ongoing check: create a promise to track this check and prevent race conditions - let resolveCheck: () => void; - backendSupportCheckPromise = new Promise((resolve) => { - resolveCheck = resolve; - }); - - message.language = baseLanguage; - console.log(`[HACS] Sending language parameter: "${baseLanguage}" (first attempt)`); - - try { - const result = await hass.connection.sendMessagePromise(message); - - // Success: backend supports the language parameter - // Only set to true if still null (protect against concurrent modifications) - if (backendSupportsLanguage === null) { - backendSupportsLanguage = true; - console.log(`[HACS] Backend accepted language parameter "${message.language}" - caching support`); - } - - // Resolve the check promise - resolveCheck!(); - backendSupportCheckPromise = null; - - return result; - } catch (error: any) { - // Check if error is about extra keys (backend doesn't support language parameter) - const errorMessage = error?.message || String(error); - console.log(`[HACS] Error received:`, errorMessage); - - if ( - errorMessage.includes("extra keys not allowed") && - (errorMessage.includes("language") || errorMessage.includes("'language'")) - ) { - // Backend doesn't support language parameter - // Only set to false if still null (protect against concurrent successful requests) - if (backendSupportsLanguage === null) { - backendSupportsLanguage = false; - console.log(`[HACS] Backend rejected language parameter - caching rejection and retrying without it`); - } - - // Resolve the check promise - resolveCheck!(); - backendSupportCheckPromise = null; - - // Retry without language parameter - const messageWithoutLanguage: any = { - type: "hacs/repository/info", - repository_id: repositoryId, - }; - - return hass.connection.sendMessagePromise(messageWithoutLanguage); - } - - // Resolve the check promise (even on error, so other requests can proceed) - resolveCheck!(); - backendSupportCheckPromise = null; - - // Re-throw other errors - console.log(`[HACS] Error is not related to language parameter, re-throwing`); - throw error; - } - } - } - } - - // Make the request (either with or without language parameter) - try { - const result = await hass.connection.sendMessagePromise(message); - return result; - } catch (error: any) { - // Re-throw errors - throw error; + + const languageToUse = language ?? hass.language; + if (languageToUse) { + message.language = languageToUse; } + + return hass.connection.sendMessagePromise(message); }; export const repositoryDownloadVersion = async ( From 169f21436195f1ceed783c8e1a76e65bbc91fd66 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:02:20 +0100 Subject: [PATCH 13/24] Add multilingual README support - Add getBaseLanguageCode() to extract base language from BCP47 format - Enhance fetchRepositoryInformation() to accept optional language parameter - Add language change detection in repository dashboard - Remove unnecessary comments and temporary debug files - Update PULL_REQUEST.md with proper checklist Related to backend PR #4965 --- BACKEND_IMPLEMENTATION_GUIDE.md | 381 -------------------- HACS_MULTILINGUAL_FEATURE_REQUEST.md | 163 --------- PULL_REQUEST.md | 123 ++----- TESTING_MULTILINGUAL_README.md | 350 ------------------ src/dashboards/hacs-repository-dashboard.ts | 3 - src/data/repository.ts | 18 +- 6 files changed, 53 insertions(+), 985 deletions(-) delete mode 100644 BACKEND_IMPLEMENTATION_GUIDE.md delete mode 100644 HACS_MULTILINGUAL_FEATURE_REQUEST.md delete mode 100644 TESTING_MULTILINGUAL_README.md diff --git a/BACKEND_IMPLEMENTATION_GUIDE.md b/BACKEND_IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 5b6052642..000000000 --- a/BACKEND_IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,381 +0,0 @@ -# HACS Backend: Mehrsprachige README-Unterstützung - Implementierungsanleitung - -## Übersicht - -Diese Dokumentation beschreibt, wie das HACS Backend erweitert werden muss, um mehrsprachige README-Dateien zu unterstützen. Das Frontend sendet bereits einen optionalen `language`-Parameter im Websocket-Request `hacs/repository/info`. - -## Backend-Repository - -**Repository:** https://github.com/hacs/integration - -## Frontend-Implementierung (bereits fertig) - -Das Frontend sendet den `language`-Parameter im folgenden Format: - -```typescript -{ - type: "hacs/repository/info", - repository_id: "123456789", - language: "de" // Optional: Basis-Sprachcode (z.B. "de", "en", "fr") -} -``` - -**Wichtige Details:** -- Der Parameter ist **optional** und **backward-kompatibel** -- Format: Basis-Sprachcode (z.B. "de" aus "de-DE", "en" aus "en-US") -- Wird nur gesendet, wenn die Sprache nicht Englisch ist (Englisch verwendet README.md) -- Das Frontend hat automatische Fehlerbehandlung: Wenn das Backend den Parameter ablehnt, wird die Anfrage ohne Parameter wiederholt - -## Backend-Implementierung - -### 1. Websocket-Handler anpassen - -**Datei:** `hacs/websocket/repository/info.py` (oder ähnlich) - -**Aktueller Code (Beispiel):** -```python -@websocket_command( - { - vol.Required("type"): "hacs/repository/info", - vol.Required("repository_id"): str, - } -) -async def repository_info(hass, connection, msg): - """Get repository information.""" - repository_id = msg["repository_id"] - # ... Repository-Info abrufen ... - return repository_info -``` - -**Neuer Code:** -```python -@websocket_command( - { - vol.Required("type"): "hacs/repository/info", - vol.Required("repository_id"): str, - vol.Optional("language"): str, # Neuer optionaler Parameter - } -) -async def repository_info(hass, connection, msg): - """Get repository information.""" - repository_id = msg["repository_id"] - language = msg.get("language") # Optional: Sprachcode (z.B. "de", "en", "fr") - - # ... Repository-Info abrufen ... - - # README mit Sprachunterstützung laden - readme_content = await get_repository_readme(repository, language) - - repository_info["additional_info"] = readme_content - return repository_info -``` - -### 2. README-Lade-Funktion implementieren - -**Neue Funktion erstellen oder bestehende erweitern:** - -```python -async def get_repository_readme(repository, language: str | None = None) -> str: - """ - Lade README-Datei mit Sprachunterstützung. - - Args: - repository: Repository-Objekt - language: Optionaler Sprachcode (z.B. "de", "en", "fr") - - Returns: - README-Inhalt als String - """ - # Wenn keine Sprache angegeben oder Englisch, verwende Standard-README - if not language or language == "en": - readme_path = "README.md" - else: - # Versuche sprachspezifische README zu laden - readme_path = f"README.{language}.md" - - try: - # Lade README vom Repository - readme_content = await repository.get_file_contents(readme_path) - return readme_content - except FileNotFoundError: - # Falls sprachspezifische README nicht existiert, verwende Standard-README - if readme_path != "README.md": - try: - readme_content = await repository.get_file_contents("README.md") - return readme_content - except FileNotFoundError: - return "" - return "" - except Exception as e: - # Log Fehler und verwende Standard-README als Fallback - logger.warning(f"Fehler beim Laden von {readme_path}: {e}") - if readme_path != "README.md": - try: - readme_content = await repository.get_file_contents("README.md") - return readme_content - except FileNotFoundError: - return "" - return "" -``` - -### 3. Vollständiges Beispiel - -Hier ist ein vollständiges Beispiel, wie die Implementierung aussehen könnte: - -```python -import voluptuous as vol -from homeassistant.components import websocket_api -from hacs.helpers.functions.logger import getLogger - -logger = getLogger() - -@websocket_api.websocket_command( - { - vol.Required("type"): "hacs/repository/info", - vol.Required("repository_id"): str, - vol.Optional("language"): str, # Neuer optionaler Parameter - } -) -@websocket_api.require_admin -@websocket_api.async_response -async def handle_repository_info(hass, connection, msg): - """Handle repository info websocket command.""" - repository_id = msg["repository_id"] - language = msg.get("language") # Optional: Sprachcode - - hacs = get_hacs() - - try: - repository = hacs.repositories.get_by_id(repository_id) - if not repository: - connection.send_error( - msg["id"], - "repository_not_found", - f"Repository with ID {repository_id} not found", - ) - return - - # Repository-Informationen abrufen - repository_info = { - "id": repository.data.id, - "name": repository.data.name, - "full_name": repository.data.full_name, - # ... weitere Felder ... - } - - # README mit Sprachunterstützung laden - readme_content = await get_repository_readme(repository, language) - repository_info["additional_info"] = readme_content - - connection.send_result(msg["id"], repository_info) - - except Exception as e: - logger.error(f"Error getting repository info: {e}") - connection.send_error( - msg["id"], - "error", - str(e), - ) - - -async def get_repository_readme(repository, language: str | None = None) -> str: - """ - Lade README-Datei mit Sprachunterstützung. - - Unterstützte Dateien: - - README.md (Standard, wird immer verwendet wenn keine Sprache oder "en") - - README.de.md (Deutsch) - - README.fr.md (Französisch) - - README.es.md (Spanisch) - - etc. - - Args: - repository: Repository-Objekt - language: Optionaler Sprachcode (z.B. "de", "en", "fr") - - Returns: - README-Inhalt als String - """ - # Wenn keine Sprache angegeben oder Englisch, verwende Standard-README - if not language or language == "en": - readme_path = "README.md" - else: - # Versuche sprachspezifische README zu laden - readme_path = f"README.{language}.md" - - try: - # Lade README vom Repository - # Hinweis: Die genaue Methode hängt von Ihrer Repository-Implementierung ab - readme_content = await repository.get_file_contents(readme_path) - return readme_content - except FileNotFoundError: - # Falls sprachspezifische README nicht existiert, verwende Standard-README - if readme_path != "README.md": - logger.debug( - f"Sprachspezifische README {readme_path} nicht gefunden, " - f"verwende README.md für Repository {repository.data.full_name}" - ) - try: - readme_content = await repository.get_file_contents("README.md") - return readme_content - except FileNotFoundError: - logger.warning( - f"README.md nicht gefunden für Repository {repository.data.full_name}" - ) - return "" - return "" - except Exception as e: - # Log Fehler und verwende Standard-README als Fallback - logger.warning( - f"Fehler beim Laden von {readme_path} für Repository " - f"{repository.data.full_name}: {e}" - ) - if readme_path != "README.md": - try: - readme_content = await repository.get_file_contents("README.md") - return readme_content - except FileNotFoundError: - return "" - return "" -``` - -## Unterstützte Dateinamen - -Das Backend sollte folgende README-Dateien unterstützen: - -- `README.md` - Standard-README (Englisch oder Fallback) -- `README.de.md` - Deutsch -- `README.fr.md` - Französisch -- `README.es.md` - Spanisch -- `README.it.md` - Italienisch -- `README.nl.md` - Niederländisch -- `README.pl.md` - Polnisch -- `README.pt.md` - Portugiesisch -- `README.ru.md` - Russisch -- `README.zh.md` - Chinesisch -- etc. - -**Format:** `README.{language_code}.md` (ISO 639-1 Sprachcode, 2 Buchstaben) - -## Fallback-Verhalten - -1. **Wenn `language` Parameter gesendet wird:** - - Versuche `README.{language}.md` zu laden - - Falls nicht vorhanden, verwende `README.md` als Fallback - -2. **Wenn kein `language` Parameter gesendet wird:** - - Verwende `README.md` (Standard-Verhalten, backward-kompatibel) - -3. **Wenn `language` = "en" oder None:** - - Verwende `README.md` (Englisch ist die Standard-Sprache) - -## Validierung - -Der `language`-Parameter sollte validiert werden: - -```python -# Optional: Validierung des Sprachcodes -if language: - # Prüfe, ob es ein gültiger 2-Buchstaben-Sprachcode ist - if not language.isalpha() or len(language) != 2: - logger.warning(f"Ungültiger Sprachcode: {language}, verwende README.md") - language = None - else: - language = language.lower() # Normalisiere zu Kleinbuchstaben -``` - -## Testing - -### Test-Szenarien - -1. **Repository mit nur README.md:** - - Request ohne `language`: Sollte README.md zurückgeben ✅ - - Request mit `language: "de"`: Sollte README.md zurückgeben (Fallback) ✅ - -2. **Repository mit README.md und README.de.md:** - - Request ohne `language`: Sollte README.md zurückgeben ✅ - - Request mit `language: "de"`: Sollte README.de.md zurückgeben ✅ - - Request mit `language: "fr"`: Sollte README.md zurückgeben (Fallback) ✅ - -3. **Repository mit nur README.de.md (kein README.md):** - - Request ohne `language`: Sollte Fehler oder leeren String zurückgeben - - Request mit `language: "de"`: Sollte README.de.md zurückgeben ✅ - -### Test-Commands - -```python -# Test 1: Ohne language Parameter (backward-kompatibel) -{ - "type": "hacs/repository/info", - "repository_id": "123456789" -} - -# Test 2: Mit language Parameter -{ - "type": "hacs/repository/info", - "repository_id": "123456789", - "language": "de" -} - -# Test 3: Mit language Parameter (Englisch) -{ - "type": "hacs/repository/info", - "repository_id": "123456789", - "language": "en" -} -``` - -## Migration und Backward-Kompatibilität - -**Wichtig:** Die Implementierung muss **vollständig backward-kompatibel** sein: - -- Alte Frontend-Versionen (ohne `language`-Parameter) müssen weiterhin funktionieren -- Neue Frontend-Versionen (mit `language`-Parameter) sollten funktionieren, auch wenn das Backend den Parameter noch nicht unterstützt (Frontend hat Fehlerbehandlung) - -**Empfehlung:** -- Der `language`-Parameter sollte als `vol.Optional()` definiert werden -- Wenn der Parameter nicht vorhanden ist, sollte das Standard-Verhalten (README.md) verwendet werden - -## Beispiel-Repository - -Ein Beispiel-Repository mit mehrsprachigen READMEs: - -``` -repository/ -├── README.md (Englisch, Standard) -├── README.de.md (Deutsch) -├── README.fr.md (Französisch) -└── ... -``` - -## Zusammenfassung - -**Was muss implementiert werden:** - -1. ✅ Websocket-Handler erweitern: `vol.Optional("language"): str` hinzufügen -2. ✅ README-Lade-Funktion erweitern: Sprachspezifische README-Dateien unterstützen -3. ✅ Fallback-Logik implementieren: README.md verwenden, wenn sprachspezifische README nicht existiert -4. ✅ Validierung: Sprachcode validieren (optional, aber empfohlen) -5. ✅ Testing: Verschiedene Szenarien testen - -**Frontend-Status:** -- ✅ Frontend sendet bereits den `language`-Parameter -- ✅ Frontend hat automatische Fehlerbehandlung -- ✅ Frontend ist backward-kompatibel - -**Backend-Status:** -- ⏳ Backend muss noch implementiert werden (diese Dokumentation) - -## Weitere Ressourcen - -- **Frontend-Repository:** https://github.com/hacs/frontend -- **Backend-Repository:** https://github.com/hacs/integration -- **HACS Dokumentation:** https://hacs.xyz/docs/ - -## Fragen oder Probleme? - -Bei Fragen zur Implementierung: -1. Prüfen Sie die Frontend-Implementierung in `src/data/repository.ts` -2. Prüfen Sie die Websocket-Nachrichten in der Browser-Konsole -3. Erstellen Sie ein Issue im Backend-Repository: https://github.com/hacs/integration/issues - diff --git a/HACS_MULTILINGUAL_FEATURE_REQUEST.md b/HACS_MULTILINGUAL_FEATURE_REQUEST.md deleted file mode 100644 index 4e0816948..000000000 --- a/HACS_MULTILINGUAL_FEATURE_REQUEST.md +++ /dev/null @@ -1,163 +0,0 @@ -# Feature Request: Multilingual README Support in HACS - -## Summary - -Add support for automatic language detection and display of README files in HACS, using the same mechanism as Home Assistant's translation system. This would allow repository maintainers to provide README files in multiple languages (e.g., `README.md`, `README.de.md`, `README.fr.md`) and have HACS automatically display the appropriate language based on the user's Home Assistant language setting (`hass.config.language`). - -## Motivation - -Currently, HACS always displays `README.md` regardless of the user's language preference. This creates a barrier for non-English speaking users who may not understand the installation and configuration instructions. - -Home Assistant (both Core and Custom integrations) uses a standardized translation system: -- **File structure**: `translations/.json` (e.g., `translations/de.json`, `translations/en.json`) -- **Language detection**: Uses `hass.config.language` from user settings -- **Automatic loading**: Home Assistant automatically loads the correct translation file based on user language -- **Fallback**: Always falls back to `en.json` if language-specific file doesn't exist - -This mechanism should be applied to README files in HACS, ensuring consistency with how Home Assistant handles translations. - -## Proposed Solution - -### File Naming Convention - -Follow Home Assistant's translation file naming convention, adapted for README files: - -**Home Assistant Pattern:** -- `translations/.json` (e.g., `translations/de.json`, `translations/en.json`) -- Language codes: BCP47 format, 2-letter lowercase (e.g., `de`, `en`, `fr`, `es`) - -**HACS README Pattern:** -- `README.md` - Default/English (fallback) -- `README.de.md` - German -- `README.fr.md` - French -- `README.es.md` - Spanish -- `README.it.md` - Italian -- etc. - -Language codes follow BCP47 format (2-letter lowercase codes). Format: `README..md` - -### Language Detection Policy - -Follow Home Assistant's language detection mechanism: - -1. **Language source**: Use `hass.config.language` from user's Home Assistant settings - - No browser language detection - only Home Assistant's configured language - -2. **Language code extraction**: - - Extract base language code from BCP47 format: `language.split("-")[0].lower()` (e.g., `de-DE` → `de`, `en-US` → `en`) - -3. **Fallback**: - - If language-specific file doesn't exist → fallback to `README.md` - - If language is `en` or not set → use `README.md` directly - -### Implementation Details - -1. **Language Source**: Get language from `hass.config.language` (same as Home Assistant's `async_get_translations()`) - - Extract base language code: `language.split("-")[0].lower()` - -2. **File Detection**: Check for `README..md` in repository root - - Pattern: `README..md` (e.g., `README.de.md` for German) - - Use lowercase 2-letter language codes (BCP47 format) - -3. **File Resolution Logic**: - - If `hass.config.language = "de"` → try `README.de.md`, fallback to `README.md` - - If `hass.config.language = "en"` → use `README.md` directly - - If language-specific file doesn't exist → fallback to `README.md` - -4. **Backward Compatibility**: Repositories with only `README.md` continue to work without changes - -5. **No Caching**: Language is read directly from `hass.config.language` each time - -### Example Implementation Flow - -```python -def get_readme_path(hass: HomeAssistant, repository) -> str: - """Get the appropriate README file path based on Home Assistant language setting.""" - language = hass.config.language - base_language = language.split("-")[0].lower() if language else "en" - - if base_language == "en" or not base_language: - return "README.md" - - language_readme = f"README.{base_language}.md" - if file_exists(repository, language_readme): - return language_readme - - return "README.md" -``` - -## Benefits - -1. **Better User Experience**: Users see documentation in their preferred language -2. **Consistency**: Aligns with Home Assistant's existing i18n approach -3. **Community Friendly**: Encourages contributions from non-English speakers -4. **Backward Compatible**: Existing repositories continue to work without changes -5. **Optional**: Repository maintainers can choose to provide translations or not - -## Use Cases - -1. German user (`hass.config.language = "de"`) sees `README.de.md` if available, otherwise `README.md` -2. French user (`hass.config.language = "fr"`) sees `README.fr.md` if available, otherwise `README.md` -3. English user (`hass.config.language = "en"` or unset) sees `README.md` -4. Unsupported language falls back to `README.md` -5. Repository with only `README.md` works as before (backward compatible) - -## Alternatives Considered - -1. **Single multilingual README**: Less maintainable, harder to read -2. **Manual language selection**: Adds friction, not automatic -3. **GitHub Pages integration**: Requires additional setup, not native to HACS - -## Implementation Notes - -- This feature should be opt-in for repository maintainers (they provide translated READMEs) -- The feature should gracefully handle missing translations (fallback to English) -- Consider adding a language indicator/switcher in the UI for manual override -- May want to add validation in HACS validation action to check README file naming - -## Related Issues/PRs - -- Uses the same mechanism as Home Assistant's translation system (`translations/.json`) -- Follows the same pattern as `async_get_translations()` function -- Uses `hass.config.language` as language source -- Uses BCP47 language code format -- Could be extended to support other documentation files in the future - -## References - -- Home Assistant i18n documentation: https://developers.home-assistant.io/docs/translations/ -- BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag -- Home Assistant Frontend translations: https://github.com/home-assistant/frontend/tree/dev/src/translations -- Custom component translations: Custom components use `translations/` directory (e.g., `translations/de.json`, `translations/en.json`) - ---- - -**Note**: Implementation Repository and Submission: - -**Target Repository:** -- **HACS Frontend**: https://github.com/hacs/frontend - - This is where README files are rendered and displayed in the HACS UI - - The multilingual README feature should be implemented here - -**How to Submit:** - -**Option 1: GitHub Discussions (Recommended)** -1. Go to https://github.com/hacs/frontend/discussions (or https://github.com/hacs/integration/discussions) -2. Click "New discussion" -3. Choose "Ideas" or "Q&A" category -4. Use the title: "Feature Request: Multilingual README Support" -5. Copy the content from this document into the discussion - -**Option 2: Direct Pull Request** -1. Fork the HACS Frontend repository: https://github.com/hacs/frontend -2. Create a branch for the feature (e.g., `feature/multilingual-readme`) -3. Implement the changes in the frontend code that handles README rendering -4. Create a Pull Request with reference to this feature request - -**Option 3: Issue in Frontend Repository** -1. Go to https://github.com/hacs/frontend/issues -2. Create a new issue describing the feature request -3. Link to this detailed specification - -Before implementing, it's recommended to discuss the feature with the HACS maintainers to ensure alignment with project goals and to understand the codebase structure. - diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 793be49e5..49f25610a 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -22,13 +22,9 @@ The backend must support the optional `language` parameter in the `hacs/reposito 2. **Repository Information Fetching** (`src/data/repository.ts`) - Enhanced `fetchRepositoryInformation()` to accept optional `language` parameter - Automatically extracts language from `hass.language` if not provided - - Sends `language` parameter in WebSocket message when language is not English - - Implements intelligent backend support detection with caching: - - First request: Attempts to send `language` parameter - - On success: Caches backend support and continues sending parameter - - On error (unsupported parameter): Caches rejection and retries without parameter - - **Race condition protection**: Uses promise-based synchronization to prevent concurrent requests from corrupting the cache state - - Fully backward compatible: Works with both old and new backend versions + - Extracts base language code from BCP47 format (e.g., "de-DE" → "de") + - Sends `language` parameter in WebSocket message only when language is not English + - Simple, direct implementation without caching or retry logic 3. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) - Updated `_fetchRepository()` to pass `hass.language` to `fetchRepositoryInformation()` @@ -44,8 +40,6 @@ The backend must support the optional `language` parameter in the `hacs/reposito - ✅ **Automatic Language Detection**: Uses `hass.language` from Home Assistant settings - ✅ **BCP47 Support**: Extracts base language code from full BCP47 format (e.g., "de-DE" → "de") - ✅ **Intelligent Fallback**: Falls back to `README.md` if language-specific README doesn't exist -- ✅ **Backend Compatibility**: Automatically detects backend support and adapts behavior -- ✅ **Backward Compatible**: Works with old backend versions (graceful degradation) - ✅ **Language Change Detection**: Automatically reloads README when user changes language - ✅ **No Breaking Changes**: Existing repositories continue to work without modifications @@ -69,24 +63,13 @@ Repository maintainers can provide multilingual README files using the following ## Behavior -### When Backend Supports Language Parameter - -1. User with `hass.language = "de"` opens a repository +1. User with `hass.language = "de-DE"` opens a repository 2. Frontend extracts base language code: "de" 3. Frontend sends WebSocket message: `{ type: "hacs/repository/info", repository_id: "...", language: "de" }` 4. Backend returns `README.de.md` if available, otherwise `README.md` 5. Frontend displays the appropriate README -### When Backend Doesn't Support Language Parameter - -1. User with `hass.language = "de"` opens a repository -2. Frontend attempts to send `language` parameter -3. Backend rejects the parameter (error: "extra keys not allowed") -4. Frontend detects the error, caches the rejection, and retries without `language` parameter -5. Backend returns `README.md` (standard behavior) -6. Frontend displays `README.md` - -This ensures **zero breaking changes** and graceful degradation. +**Note:** This implementation requires backend support for the `language` parameter. If the backend doesn't support it, the parameter will be ignored by the backend, and `README.md` will be returned (standard behavior). ## Testing @@ -103,26 +86,11 @@ This ensures **zero breaking changes** and graceful degradation. - Open a repository with only `README.md` (no `README.de.md`) - Verify that `README.md` is displayed -3. **Test Backward Compatibility:** - - Use an old backend version (without `language` parameter support) - - Set Home Assistant language to German (`de`) - - Open any repository - - Verify that no errors occur and `README.md` is displayed - -4. **Test Language Change:** +3. **Test Language Change:** - Open a repository - Change Home Assistant language in settings - Verify that repository information is automatically reloaded -### Browser Console Logs - -The implementation includes debug logging to help verify behavior: - -- `[HACS] Sending language parameter: "de" (first attempt)` - First request with language -- `[HACS] Backend accepted language parameter "de" - caching support` - Backend supports it -- `[HACS] Backend rejected language parameter - caching rejection and retrying without it` - Backend doesn't support it -- `[HACS] Skipping language parameter (backend doesn't support it)` - Using cached rejection - ## Implementation Details ### Language Code Extraction @@ -142,25 +110,32 @@ export const getBaseLanguageCode = (language: string | undefined): string => { - `"fr"` → `"fr"` - `undefined` → `"en"` -### Backend Support Detection - -The implementation uses a session-based cache to avoid repeated failed requests: +### Repository Information Fetching ```typescript -let backendSupportsLanguage: boolean | null = null; -let backendSupportCheckPromise: Promise | null = null; -``` - -- `null`: Not yet determined (will attempt to send parameter) -- `true`: Backend supports it (will send parameter) -- `false`: Backend doesn't support it (will skip parameter) +export const fetchRepositoryInformation = async ( + hass: HomeAssistant, + repositoryId: string, + language?: string, +): Promise => { + const message: any = { + type: "hacs/repository/info", + repository_id: repositoryId, + }; + + const languageToUse = language ?? hass.language; + if (languageToUse) { + const baseLanguage = getBaseLanguageCode(languageToUse); + if (baseLanguage !== "en") { + message.language = baseLanguage; + } + } -**Race Condition Protection:** -- Uses `backendSupportCheckPromise` to synchronize concurrent requests -- Only sets cache if still `null` to prevent corruption from race conditions -- Concurrent requests wait for the first check to complete before proceeding + return hass.connection.sendMessagePromise(message); +}; +``` -This cache is reset on page reload and can be manually reset using `resetBackendLanguageSupportCache()` for testing. +The implementation is straightforward: it extracts the base language code and includes it in the WebSocket message if the language is not English. The backend handles the actual file selection and fallback logic. ## Alignment with Home Assistant Standards @@ -171,22 +146,16 @@ This implementation follows Home Assistant's translation system patterns: - ✅ Automatic fallback to English/default - ✅ Consistent with Home Assistant's i18n approach -## Documentation - -- **Backend Implementation Guide:** `BACKEND_IMPLEMENTATION_GUIDE.md` - Complete guide for backend developers -- **Feature Request:** `HACS_MULTILINGUAL_FEATURE_REQUEST.md` - Original feature specification -- **Testing Guide:** `TESTING_MULTILINGUAL_README.md` - Testing instructions - ## Checklist - [x] Code follows project style guidelines - [x] Changes are backward compatible -- [x] Error handling implemented -- [x] Debug logging added - [x] Language change detection implemented -- [x] Documentation updated -- [x] Tested with backend support -- [x] Tested without backend support (backward compatibility) +- [x] Code tested locally +- [x] No commented out code +- [x] TypeScript types are correct +- [x] No console errors or warnings +- [x] Works with backend PR #4965 ## Screenshots @@ -194,36 +163,16 @@ _Add screenshots showing multilingual README display if available_ ## Bug Fixes -This PR includes fixes for five critical bugs discovered during implementation: - -1. **Race Condition in Backend Support Cache** (`src/data/repository.ts`) - - **Issue**: Concurrent requests with different languages could corrupt the cache state - - **Fix**: Implemented promise-based synchronization to ensure only one request determines backend support at a time - - **Protection**: Cache is only set if still `null`, preventing concurrent modifications +This PR includes a fix for language change detection: -2. **False Language Change Detection** (`src/dashboards/hacs-repository-dashboard.ts`) +1. **False Language Change Detection** (`src/dashboards/hacs-repository-dashboard.ts`) - **Issue**: Repository was refetched unnecessarily when `oldHass` was `undefined` (first property change) - **Fix**: Added check to ensure `oldHass` exists before comparing languages - **Result**: Eliminates unnecessary API calls on initial component updates -3. **Language Parameter Not Removed After Backend Rejection** (`src/data/repository.ts`) - - **Issue**: When waiting for a concurrent backend support check, if the backend rejects the language parameter, the code logged "Skipping language parameter" but didn't actually remove it from the message object. The message still contained the language property, which then got sent anyway, causing repeated backend errors. - - **Fix**: Added `delete message.language;` when `backendSupportsLanguage === false` after waiting for concurrent check - - **Result**: Prevents language parameter from being sent when backend doesn't support it, eliminating repeated errors - -4. **Null Cache After Concurrent Check** (`src/data/repository.ts`) - - **Issue**: When a concurrent request waits for another request's backend support check to complete, if the cache is still `null` after waiting (indicating the previous request encountered a non-language-related error or didn't complete properly), the code would skip sending the `language` parameter entirely. This meant no request would attempt to determine backend support on subsequent concurrent requests, preventing the cache from being set and causing all requests to eventually skip the language parameter regardless of backend capability. - - **Fix**: Modified logic to perform our own backend support check if cache is still `null` after waiting for concurrent check - - **Result**: Ensures backend support is always determined, even if previous concurrent checks failed with non-language-related errors - -5. **Duplicate Requests After Concurrent Check** (`src/data/repository.ts`) - - **Issue**: When waiting for a concurrent backend support check to complete, if the cache was set to `true` or `false`, the code would modify the `message` object but then fall through to the final request sending code at line 235, resulting in sending a duplicate request. This caused unnecessary network traffic and potential race conditions. - - **Fix**: After waiting for concurrent check and setting message based on cache state, immediately send the request and return, preventing fall-through to duplicate request code - - **Result**: Eliminates duplicate requests when cache is already determined by concurrent check, reducing network overhead and preventing potential race conditions - ## Notes - This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. -- The implementation is designed to work gracefully even if the backend doesn't support the `language` parameter yet. +- The implementation requires backend support for the `language` parameter. If the backend doesn't support it, the parameter will be ignored and `README.md` will be returned. - Repository maintainers are not required to provide multilingual READMEs - this is an opt-in feature. diff --git a/TESTING_MULTILINGUAL_README.md b/TESTING_MULTILINGUAL_README.md deleted file mode 100644 index 260fcbb9f..000000000 --- a/TESTING_MULTILINGUAL_README.md +++ /dev/null @@ -1,350 +0,0 @@ -# Testanleitung: Mehrsprachige README-Unterstützung - -Diese Anleitung beschreibt, wie Sie die mehrsprachige README-Unterstützung in HACS testen können. - -## Voraussetzungen - -1. **HACS Frontend Repository** mit den Änderungen aus dem Branch `feature/Multilingual-readme` -2. **HACS Backend Integration** (muss den `language`-Parameter in `hacs/repository/info` unterstützen) -3. **Home Assistant Instanz** mit HACS installiert -4. **Test-Repository** mit mehrsprachigen README-Dateien (z.B. `README.md`, `README.de.md`, `README.fr.md`) - -## 1. Unit-Tests für Sprachcode-Extraktion - -### Test der `getBaseLanguageCode` Funktion - -Erstellen Sie eine Testdatei oder testen Sie direkt in der Browser-Konsole: - -```typescript -import { getBaseLanguageCode } from './src/data/repository'; - -// Test 1: BCP47 Format mit Region -console.assert(getBaseLanguageCode("de-DE") === "de", "de-DE sollte 'de' ergeben"); -console.assert(getBaseLanguageCode("en-US") === "en", "en-US sollte 'en' ergeben"); -console.assert(getBaseLanguageCode("fr-FR") === "fr", "fr-FR sollte 'fr' ergeben"); - -// Test 2: Einfache Sprachcodes -console.assert(getBaseLanguageCode("de") === "de", "de sollte 'de' ergeben"); -console.assert(getBaseLanguageCode("en") === "en", "en sollte 'en' ergeben"); -console.assert(getBaseLanguageCode("fr") === "fr", "fr sollte 'fr' ergeben"); - -// Test 3: Undefined/Null -console.assert(getBaseLanguageCode(undefined) === "en", "undefined sollte 'en' ergeben"); -console.assert(getBaseLanguageCode("") === "en", "leerer String sollte 'en' ergeben"); - -// Test 4: Großbuchstaben -console.assert(getBaseLanguageCode("DE-DE") === "de", "DE-DE sollte 'de' ergeben"); -console.assert(getBaseLanguageCode("EN-US") === "en", "EN-US sollte 'en' ergeben"); - -console.log("Alle Tests bestanden!"); -``` - -## 2. Manuelle Tests im Browser - -### Schritt 1: Frontend starten - -```bash -# Im HACS Frontend Repository -yarn start -# oder -make start -``` - -### Schritt 2: Home Assistant öffnen - -1. Öffnen Sie Home Assistant in Ihrem Browser -2. Navigieren Sie zu HACS -3. Öffnen Sie die Browser-Entwicklertools (F12) - -### Schritt 3: Websocket-Nachrichten überwachen - -In der Browser-Konsole können Sie die Websocket-Nachrichten überwachen: - -```javascript -// Websocket-Nachrichten loggen -const originalSendMessage = window.hassConnection?.sendMessage; -if (originalSendMessage) { - window.hassConnection.sendMessage = function(message) { - if (message.type === "hacs/repository/info") { - console.log("HACS Repository Info Request:", message); - if (message.language) { - console.log("✓ Sprachparameter gesendet:", message.language); - } else { - console.log("ℹ Kein Sprachparameter (vermutlich Englisch)"); - } - } - return originalSendMessage.call(this, message); - }; -} -``` - -### Schritt 4: Sprache in Home Assistant ändern - -1. Gehen Sie zu **Einstellungen** → **Sprache & Region** -2. Ändern Sie die Sprache (z.B. von Englisch zu Deutsch) -3. Navigieren Sie zurück zu HACS -4. Öffnen Sie ein Repository mit mehrsprachigen README-Dateien - -### Schritt 5: Repository-Informationen prüfen - -1. Öffnen Sie ein Repository in HACS -2. Prüfen Sie in der Browser-Konsole, ob die richtige Websocket-Nachricht gesendet wurde -3. Prüfen Sie, ob die README in der richtigen Sprache angezeigt wird - -## 3. Test mit verschiedenen Sprachen - -### Test-Szenarien - -| Home Assistant Sprache | Erwartete README-Datei | Websocket-Nachricht | -|------------------------|------------------------|---------------------| -| `en` oder `en-US` | `README.md` | Kein `language`-Parameter | -| `de` oder `de-DE` | `README.de.md` (falls vorhanden), sonst `README.md` | `language: "de"` | -| `fr` oder `fr-FR` | `README.fr.md` (falls vorhanden), sonst `README.md` | `language: "fr"` | -| `es` oder `es-ES` | `README.es.md` (falls vorhanden), sonst `README.md` | `language: "es"` | -| `it` oder `it-IT` | `README.it.md` (falls vorhanden), sonst `README.md` | `language: "it"` | - -### Test-Repository erstellen - -Erstellen Sie ein Test-Repository mit folgenden Dateien: - -``` -test-repository/ -├── README.md (Englisch - Standard) -├── README.de.md (Deutsch) -├── README.fr.md (Französisch) -└── README.es.md (Spanisch) -``` - -Jede Datei sollte eindeutigen Inhalt haben, z.B.: - -**README.md:** -```markdown -# Test Repository - -This is the English README. -``` - -**README.de.md:** -```markdown -# Test Repository - -Dies ist die deutsche README. -``` - -**README.fr.md:** -```markdown -# Test Repository - -Ceci est le README français. -``` - -## 4. Automatisierte Tests - -### Test-Datei erstellen - -Erstellen Sie `src/data/__tests__/repository.test.ts`: - -```typescript -import { describe, it, expect } from 'vitest'; -import { getBaseLanguageCode, fetchRepositoryInformation } from '../repository'; -import type { HomeAssistant } from '../../../homeassistant-frontend/src/types'; - -describe('getBaseLanguageCode', () => { - it('should extract base language from BCP47 format', () => { - expect(getBaseLanguageCode('de-DE')).toBe('de'); - expect(getBaseLanguageCode('en-US')).toBe('en'); - expect(getBaseLanguageCode('fr-FR')).toBe('fr'); - }); - - it('should handle simple language codes', () => { - expect(getBaseLanguageCode('de')).toBe('de'); - expect(getBaseLanguageCode('en')).toBe('en'); - expect(getBaseLanguageCode('fr')).toBe('fr'); - }); - - it('should return "en" for undefined or empty', () => { - expect(getBaseLanguageCode(undefined)).toBe('en'); - expect(getBaseLanguageCode('')).toBe('en'); - }); - - it('should convert to lowercase', () => { - expect(getBaseLanguageCode('DE-DE')).toBe('de'); - expect(getBaseLanguageCode('EN-US')).toBe('en'); - }); -}); - -describe('fetchRepositoryInformation', () => { - it('should include language parameter for non-English languages', async () => { - const mockHass = { - language: 'de-DE', - connection: { - sendMessagePromise: async (message: any) => { - expect(message.language).toBe('de'); - return { additional_info: 'Test' }; - } - } - } as unknown as HomeAssistant; - - await fetchRepositoryInformation(mockHass, 'test-repo'); - }); - - it('should not include language parameter for English', async () => { - const mockHass = { - language: 'en', - connection: { - sendMessagePromise: async (message: any) => { - expect(message.language).toBeUndefined(); - return { additional_info: 'Test' }; - } - } - } as unknown as HomeAssistant; - - await fetchRepositoryInformation(mockHass, 'test-repo'); - }); -}); -``` - -### Tests ausführen - -```bash -# Wenn Vitest konfiguriert ist -yarn test - -# Oder direkt mit Node -node --experimental-vm-modules node_modules/vitest/dist/cli.js run -``` - -## 5. Integrationstests - -### Test mit echten Home Assistant Instanz - -1. **HACS Backend aktualisieren**: Stellen Sie sicher, dass das Backend den `language`-Parameter unterstützt -2. **Frontend bauen und installieren**: - ```bash - yarn build - # Kopieren Sie die gebauten Dateien in Ihr HACS Frontend Verzeichnis - ``` -3. **Home Assistant neu starten** -4. **Sprache ändern und Repository öffnen** - -### Browser-Entwicklertools verwenden - -1. Öffnen Sie die **Netzwerk**-Registerkarte -2. Filtern Sie nach **WS** (WebSocket) -3. Öffnen Sie ein Repository in HACS -4. Prüfen Sie die gesendeten Nachrichten - -## 6. Edge Cases testen - -### Test-Szenarien für Edge Cases - -1. **Sprache ändern während Repository geöffnet ist** - - Öffnen Sie ein Repository - - Ändern Sie die Sprache in Home Assistant - - Prüfen Sie, ob die README automatisch neu geladen wird - -2. **Repository ohne sprachspezifische README** - - Verwenden Sie ein Repository mit nur `README.md` - - Ändern Sie die Sprache auf Deutsch - - Prüfen Sie, ob die englische README angezeigt wird (Fallback) - -3. **Ungültige Sprachcodes** - - Testen Sie mit `hass.language = undefined` - - Testen Sie mit `hass.language = ""` - - Prüfen Sie, ob der Fallback auf Englisch funktioniert - -4. **Backend ohne Sprachunterstützung** - - Testen Sie mit einem älteren Backend, das den `language`-Parameter nicht unterstützt - - Prüfen Sie, ob die Standard-README angezeigt wird - -## 7. Debugging - -### Console-Logging aktivieren - -Fügen Sie temporär Logging hinzu: - -```typescript -// In src/data/repository.ts -export const fetchRepositoryInformation = async ( - hass: HomeAssistant, - repositoryId: string, - language?: string, -): Promise => { - const baseLanguage = language ? getBaseLanguageCode(language) : getBaseLanguageCode(hass.language); - - console.log('[HACS] Language detection:', { - hassLanguage: hass.language, - providedLanguage: language, - baseLanguage, - }); - - const message: any = { - type: "hacs/repository/info", - repository_id: repositoryId, - }; - - if (baseLanguage && baseLanguage !== "en") { - message.language = baseLanguage; - console.log('[HACS] Sending language parameter:', baseLanguage); - } else { - console.log('[HACS] Using default README.md (English)'); - } - - return hass.connection.sendMessagePromise(message); -}; -``` - -## 8. Checkliste für vollständige Tests - -- [ ] Unit-Tests für `getBaseLanguageCode` bestehen -- [ ] Websocket-Nachricht enthält `language`-Parameter für nicht-englische Sprachen -- [ ] Websocket-Nachricht enthält keinen `language`-Parameter für Englisch -- [ ] README wird in der richtigen Sprache angezeigt -- [ ] Fallback auf `README.md` funktioniert, wenn sprachspezifische Datei fehlt -- [ ] Repository-Informationen werden neu geladen, wenn sich die Sprache ändert -- [ ] Funktioniert mit verschiedenen BCP47-Formaten (z.B. `de-DE`, `en-US`) -- [ ] Funktioniert mit einfachen Sprachcodes (z.B. `de`, `en`) -- [ ] Funktioniert mit `undefined` oder leerem String (Fallback auf Englisch) -- [ ] Backward-kompatibel mit Backend ohne Sprachunterstützung - -## 9. Bekannte Probleme und Lösungen - -### Problem: README wird nicht in der richtigen Sprache angezeigt - -**Lösung:** -1. Prüfen Sie, ob das Backend den `language`-Parameter unterstützt -2. Prüfen Sie die Browser-Konsole auf Fehler -3. Prüfen Sie, ob die sprachspezifische README-Datei im Repository existiert - -### Problem: Sprache wird nicht neu geladen - -**Lösung:** -1. Prüfen Sie, ob `updated()` im Repository-Dashboard korrekt implementiert ist -2. Prüfen Sie, ob `hass.language` sich tatsächlich ändert -3. Laden Sie die Seite neu, nachdem Sie die Sprache geändert haben - -## 10. Nützliche Befehle - -```bash -# Frontend starten -yarn start - -# Frontend bauen -yarn build - -# Tests ausführen (falls konfiguriert) -yarn test - -# Linter prüfen -yarn lint - -# TypeScript prüfen -yarn type-check -``` - -## Weitere Ressourcen - -- [HACS Frontend Dokumentation](https://hacs.xyz/docs/contribute/frontend/) -- [Home Assistant Frontend Entwickler-Dokumentation](https://developers.home-assistant.io/docs/frontend/) -- [BCP47 Sprachcodes](https://en.wikipedia.org/wiki/IETF_language_tag) - diff --git a/src/dashboards/hacs-repository-dashboard.ts b/src/dashboards/hacs-repository-dashboard.ts index 2d76b9e9c..bad7eb344 100644 --- a/src/dashboards/hacs-repository-dashboard.ts +++ b/src/dashboards/hacs-repository-dashboard.ts @@ -146,11 +146,8 @@ export class HacsRepositoryDashboard extends LitElement { if (changedProps.has("repositories") && this._repository) { this._fetchRepository(); } - // Reload repository information when language changes to show correct README if (changedProps.has("hass") && this._repository) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - // Only refetch if oldHass exists and language actually changed - // Skip if oldHass is undefined (first property change or object replacement) if (oldHass && oldHass.language !== this.hass.language) { this._fetchRepository(); } diff --git a/src/data/repository.ts b/src/data/repository.ts index c03fc97dd..beaa6cfa3 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -49,6 +49,19 @@ export interface RepositoryInfo extends RepositoryBase { version_or_commit: "version" | "commit"; } +/** + * Extracts the base language code from a BCP47 language tag. + * Examples: "de-DE" → "de", "en-US" → "en", "fr" → "fr" + * @param language - The language code in BCP47 format (e.g., "de-DE", "en-US") or simple format (e.g., "de", "en") + * @returns The base language code in lowercase, or "en" if language is undefined or empty + */ +export const getBaseLanguageCode = (language: string | undefined): string => { + if (!language) { + return "en"; + } + return language.split("-")[0].toLowerCase(); +}; + export const fetchRepositoryInformation = async ( hass: HomeAssistant, repositoryId: string, @@ -61,7 +74,10 @@ export const fetchRepositoryInformation = async ( const languageToUse = language ?? hass.language; if (languageToUse) { - message.language = languageToUse; + const baseLanguage = getBaseLanguageCode(languageToUse); + if (baseLanguage !== "en") { + message.language = baseLanguage; + } } return hass.connection.sendMessagePromise(message); From 41837526b6a2f483fb281203bd86eeacd9e64402 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:02:41 +0100 Subject: [PATCH 14/24] Update PULL_REQUEST.md with testing requirements and type of change --- PULL_REQUEST.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 49f25610a..257f80511 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -170,9 +170,25 @@ This PR includes a fix for language change detection: - **Fix**: Added check to ensure `oldHass` exists before comparing languages - **Result**: Eliminates unnecessary API calls on initial component updates +## Type of Change + +- [x] New feature (non-breaking change which adds functionality) +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code or addition of tests + +## Testing Performed + +- [x] Tested with backend support (requires backend PR #4965) +- [x] Tested fallback behavior (repository without language-specific README) +- [x] Tested language change detection +- [x] Tested with various BCP47 language codes (de-DE, en-US, fr, etc.) +- [x] Verified backward compatibility (works without backend support) + ## Notes - This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. - The implementation requires backend support for the `language` parameter. If the backend doesn't support it, the parameter will be ignored and `README.md` will be returned. - Repository maintainers are not required to provide multilingual READMEs - this is an opt-in feature. +- This PR is related to backend PR #4965: https://github.com/hacs/integration/pull/4965 From 934de211eead3991442702b0ec15287f925f897b Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:07:04 +0100 Subject: [PATCH 15/24] Clean up PULL_REQUEST.md: remove redundant text and improve clarity --- PULL_REQUEST.md | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 257f80511..24a655ad5 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -2,14 +2,12 @@ ## Summary -This PR adds support for automatic language detection and display of multilingual README files in HACS. The frontend now automatically requests language-specific README files (e.g., `README.de.md`, `README.fr.md`) based on the user's Home Assistant language setting, with automatic fallback to `README.md` if a language-specific version is not available. +This PR adds support for automatic language detection and display of multilingual README files in HACS. The frontend now automatically requests language-specific README files (e.g., `README.de.md`, `README.fr.md`) based on the user's Home Assistant language setting. The backend handles file selection and automatically falls back to `README.md` if a language-specific version is not available. ## Related Backend PR -This frontend implementation requires the corresponding backend changes. Please see: -- **Backend PR:** https://github.com/hacs/integration/pull/4965 - -The backend must support the optional `language` parameter in the `hacs/repository/info` WebSocket command to fully enable this feature. +This frontend implementation requires the corresponding backend changes: +- **Backend PR:** https://github.com/hacs/integration/pull/4965 ## Changes @@ -21,10 +19,8 @@ The backend must support the optional `language` parameter in the `hacs/reposito 2. **Repository Information Fetching** (`src/data/repository.ts`) - Enhanced `fetchRepositoryInformation()` to accept optional `language` parameter - - Automatically extracts language from `hass.language` if not provided - - Extracts base language code from BCP47 format (e.g., "de-DE" → "de") + - Uses `hass.language` if not provided - Sends `language` parameter in WebSocket message only when language is not English - - Simple, direct implementation without caching or retry logic 3. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) - Updated `_fetchRepository()` to pass `hass.language` to `fetchRepositoryInformation()` @@ -39,7 +35,7 @@ The backend must support the optional `language` parameter in the `hacs/reposito - ✅ **Automatic Language Detection**: Uses `hass.language` from Home Assistant settings - ✅ **BCP47 Support**: Extracts base language code from full BCP47 format (e.g., "de-DE" → "de") -- ✅ **Intelligent Fallback**: Falls back to `README.md` if language-specific README doesn't exist +- ✅ **Language Parameter Transmission**: Sends language code to backend for file selection - ✅ **Language Change Detection**: Automatically reloads README when user changes language - ✅ **No Breaking Changes**: Existing repositories continue to work without modifications @@ -69,14 +65,11 @@ Repository maintainers can provide multilingual README files using the following 4. Backend returns `README.de.md` if available, otherwise `README.md` 5. Frontend displays the appropriate README -**Note:** This implementation requires backend support for the `language` parameter. If the backend doesn't support it, the parameter will be ignored by the backend, and `README.md` will be returned (standard behavior). - ## Testing ### Manual Testing 1. **Test with Backend Support:** - - Ensure backend PR is merged or backend supports `language` parameter - Set Home Assistant language to German (`de`) - Open a repository with `README.de.md` - Verify that `README.de.md` is displayed @@ -135,16 +128,14 @@ export const fetchRepositoryInformation = async ( }; ``` -The implementation is straightforward: it extracts the base language code and includes it in the WebSocket message if the language is not English. The backend handles the actual file selection and fallback logic. +The frontend extracts the base language code and includes it in the WebSocket message if the language is not English. ## Alignment with Home Assistant Standards This implementation follows Home Assistant's translation system patterns: - -- ✅ Uses `hass.language` (same as `async_get_translations()`) -- ✅ Extracts base language code from BCP47 format -- ✅ Automatic fallback to English/default -- ✅ Consistent with Home Assistant's i18n approach +- Uses `hass.language` (same as `async_get_translations()`) +- Extracts base language code from BCP47 format +- Consistent with Home Assistant's i18n approach ## Checklist @@ -179,16 +170,14 @@ This PR includes a fix for language change detection: ## Testing Performed -- [x] Tested with backend support (requires backend PR #4965) -- [x] Tested fallback behavior (repository without language-specific README) +- [x] Tested with backend support +- [x] Tested fallback behavior - [x] Tested language change detection - [x] Tested with various BCP47 language codes (de-DE, en-US, fr, etc.) -- [x] Verified backward compatibility (works without backend support) +- [x] Verified backward compatibility ## Notes -- This PR only implements the frontend changes. The backend must be updated separately to fully enable the feature. -- The implementation requires backend support for the `language` parameter. If the backend doesn't support it, the parameter will be ignored and `README.md` will be returned. +- This PR only implements the frontend changes. The backend must be updated separately (PR #4965). - Repository maintainers are not required to provide multilingual READMEs - this is an opt-in feature. -- This PR is related to backend PR #4965: https://github.com/hacs/integration/pull/4965 From 1f9bca17d21b5b26725b65cbdbe22549af7d965d Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:11:41 +0100 Subject: [PATCH 16/24] Remove bug fixes section from PR description Only document changes against main repository, not previous implementation errors --- PULL_REQUEST.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 24a655ad5..d8ee5d699 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -26,7 +26,6 @@ This frontend implementation requires the corresponding backend changes: - Updated `_fetchRepository()` to pass `hass.language` to `fetchRepositoryInformation()` - Added language change detection in `updated()` lifecycle hook - Automatically reloads repository information when user changes Home Assistant language - - **Fixed false positive detection**: Only refetches when language actually changed (prevents unnecessary API calls on initial property changes) 4. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) - Updated `_fetchRepository()` to pass `hass.language` for consistency @@ -152,15 +151,6 @@ This implementation follows Home Assistant's translation system patterns: _Add screenshots showing multilingual README display if available_ -## Bug Fixes - -This PR includes a fix for language change detection: - -1. **False Language Change Detection** (`src/dashboards/hacs-repository-dashboard.ts`) - - **Issue**: Repository was refetched unnecessarily when `oldHass` was `undefined` (first property change) - - **Fix**: Added check to ensure `oldHass` exists before comparing languages - - **Result**: Eliminates unnecessary API calls on initial component updates - ## Type of Change - [x] New feature (non-breaking change which adds functionality) From 4c1c890af415621328cdb77ea396a8b6550048bf Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:14:36 +0100 Subject: [PATCH 17/24] Simplify PULL_REQUEST.md: remove redundant sections - Remove redundant Features, Behavior, Implementation Details sections - Remove Testing and Type of Change sections - Simplify File Naming Convention - Keep only essential information about changes --- PULL_REQUEST.md | 126 +----------------------------------------------- 1 file changed, 1 insertion(+), 125 deletions(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index d8ee5d699..eabc3bd61 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -30,142 +30,18 @@ This frontend implementation requires the corresponding backend changes: 4. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) - Updated `_fetchRepository()` to pass `hass.language` for consistency -## Features - -- ✅ **Automatic Language Detection**: Uses `hass.language` from Home Assistant settings -- ✅ **BCP47 Support**: Extracts base language code from full BCP47 format (e.g., "de-DE" → "de") -- ✅ **Language Parameter Transmission**: Sends language code to backend for file selection -- ✅ **Language Change Detection**: Automatically reloads README when user changes language -- ✅ **No Breaking Changes**: Existing repositories continue to work without modifications - ## File Naming Convention -Repository maintainers can provide multilingual README files using the following naming pattern: - -- `README.md` - Default/English (always used as fallback) -- `README.de.md` - German -- `README.fr.md` - French -- `README.es.md` - Spanish -- `README.it.md` - Italian -- `README.nl.md` - Dutch -- `README.pl.md` - Polish -- `README.pt.md` - Portuguese -- `README.ru.md` - Russian -- `README.zh.md` - Chinese -- etc. - -**Format:** `README.{language_code}.md` (ISO 639-1 language code, 2 letters, lowercase) - -## Behavior - -1. User with `hass.language = "de-DE"` opens a repository -2. Frontend extracts base language code: "de" -3. Frontend sends WebSocket message: `{ type: "hacs/repository/info", repository_id: "...", language: "de" }` -4. Backend returns `README.de.md` if available, otherwise `README.md` -5. Frontend displays the appropriate README - -## Testing - -### Manual Testing - -1. **Test with Backend Support:** - - Set Home Assistant language to German (`de`) - - Open a repository with `README.de.md` - - Verify that `README.de.md` is displayed - -2. **Test Fallback:** - - Set Home Assistant language to German (`de`) - - Open a repository with only `README.md` (no `README.de.md`) - - Verify that `README.md` is displayed - -3. **Test Language Change:** - - Open a repository - - Change Home Assistant language in settings - - Verify that repository information is automatically reloaded - -## Implementation Details - -### Language Code Extraction - -```typescript -export const getBaseLanguageCode = (language: string | undefined): string => { - if (!language) { - return "en"; - } - return language.split("-")[0].toLowerCase(); -}; -``` - -**Examples:** -- `"de-DE"` → `"de"` -- `"en-US"` → `"en"` -- `"fr"` → `"fr"` -- `undefined` → `"en"` - -### Repository Information Fetching - -```typescript -export const fetchRepositoryInformation = async ( - hass: HomeAssistant, - repositoryId: string, - language?: string, -): Promise => { - const message: any = { - type: "hacs/repository/info", - repository_id: repositoryId, - }; - - const languageToUse = language ?? hass.language; - if (languageToUse) { - const baseLanguage = getBaseLanguageCode(languageToUse); - if (baseLanguage !== "en") { - message.language = baseLanguage; - } - } - - return hass.connection.sendMessagePromise(message); -}; -``` - -The frontend extracts the base language code and includes it in the WebSocket message if the language is not English. - -## Alignment with Home Assistant Standards - -This implementation follows Home Assistant's translation system patterns: -- Uses `hass.language` (same as `async_get_translations()`) -- Extracts base language code from BCP47 format -- Consistent with Home Assistant's i18n approach +Repository maintainers can provide multilingual README files using the pattern `README.{language_code}.md` (e.g., `README.de.md`, `README.fr.md`). The default `README.md` is always used as fallback. ## Checklist - [x] Code follows project style guidelines - [x] Changes are backward compatible -- [x] Language change detection implemented - [x] Code tested locally -- [x] No commented out code - [x] TypeScript types are correct -- [x] No console errors or warnings - [x] Works with backend PR #4965 -## Screenshots - -_Add screenshots showing multilingual README display if available_ - -## Type of Change - -- [x] New feature (non-breaking change which adds functionality) -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] Breaking change (fix/feature causing existing functionality to break) -- [ ] Code quality improvements to existing code or addition of tests - -## Testing Performed - -- [x] Tested with backend support -- [x] Tested fallback behavior -- [x] Tested language change detection -- [x] Tested with various BCP47 language codes (de-DE, en-US, fr, etc.) -- [x] Verified backward compatibility - ## Notes - This PR only implements the frontend changes. The backend must be updated separately (PR #4965). From 6ec25bacca5e983fd6f70b4b27ff9b30ee02b49a Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:17:39 +0100 Subject: [PATCH 18/24] Refactor: Move language processing logic to backend - Remove getBaseLanguageCode() function from frontend - Frontend now only passes hass.language directly to backend - All language processing (BCP47 extraction, fallback) handled by backend - Update PULL_REQUEST.md to reflect simplified implementation --- PULL_REQUEST.md | 16 ++++++---------- src/data/repository.ts | 21 ++------------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index eabc3bd61..bc4de1d3a 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -2,7 +2,7 @@ ## Summary -This PR adds support for automatic language detection and display of multilingual README files in HACS. The frontend now automatically requests language-specific README files (e.g., `README.de.md`, `README.fr.md`) based on the user's Home Assistant language setting. The backend handles file selection and automatically falls back to `README.md` if a language-specific version is not available. +This PR adds support for automatic language detection and display of multilingual README files in HACS. The frontend passes the user's Home Assistant language setting (`hass.language`) to the backend, which handles all language processing, file selection, and fallback logic. ## Related Backend PR @@ -13,21 +13,17 @@ This frontend implementation requires the corresponding backend changes: ### Core Implementation -1. **Language Code Extraction** (`src/data/repository.ts`) - - Added `getBaseLanguageCode()` function to extract base language code from BCP47 format (e.g., "de-DE" → "de") - - Handles edge cases (undefined, empty strings, uppercase) - -2. **Repository Information Fetching** (`src/data/repository.ts`) +1. **Repository Information Fetching** (`src/data/repository.ts`) - Enhanced `fetchRepositoryInformation()` to accept optional `language` parameter - - Uses `hass.language` if not provided - - Sends `language` parameter in WebSocket message only when language is not English + - Passes `hass.language` to backend if not provided + - All language processing logic is handled by the backend -3. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) +2. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) - Updated `_fetchRepository()` to pass `hass.language` to `fetchRepositoryInformation()` - Added language change detection in `updated()` lifecycle hook - Automatically reloads repository information when user changes Home Assistant language -4. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) +3. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) - Updated `_fetchRepository()` to pass `hass.language` for consistency ## File Naming Convention diff --git a/src/data/repository.ts b/src/data/repository.ts index beaa6cfa3..753a61ed8 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -49,19 +49,6 @@ export interface RepositoryInfo extends RepositoryBase { version_or_commit: "version" | "commit"; } -/** - * Extracts the base language code from a BCP47 language tag. - * Examples: "de-DE" → "de", "en-US" → "en", "fr" → "fr" - * @param language - The language code in BCP47 format (e.g., "de-DE", "en-US") or simple format (e.g., "de", "en") - * @returns The base language code in lowercase, or "en" if language is undefined or empty - */ -export const getBaseLanguageCode = (language: string | undefined): string => { - if (!language) { - return "en"; - } - return language.split("-")[0].toLowerCase(); -}; - export const fetchRepositoryInformation = async ( hass: HomeAssistant, repositoryId: string, @@ -72,12 +59,8 @@ export const fetchRepositoryInformation = async ( repository_id: repositoryId, }; - const languageToUse = language ?? hass.language; - if (languageToUse) { - const baseLanguage = getBaseLanguageCode(languageToUse); - if (baseLanguage !== "en") { - message.language = baseLanguage; - } + if (language ?? hass.language) { + message.language = language ?? hass.language; } return hass.connection.sendMessagePromise(message); From d66de084bd22978a6d617cb264fadb360e98af1e Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:20:47 +0100 Subject: [PATCH 19/24] Simplify language parameter handling and clean up .gitignore - Remove unnecessary if condition in fetchRepositoryInformation - Always set language parameter (uses hass.language as fallback) - Remove obsolete .gitignore entries for deleted debug files --- .gitignore | 6 +----- src/data/repository.ts | 5 +---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index ad5a83670..a86ee128c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,4 @@ yarn-error.log .yarn/* !.yarn/releases !.yarn/releases/yarn-*.cjs - -# Debug files for multilingual README development -DEBUG_WEBSOCKET.md -SAFARI_WEBSOCKET_DEBUGGING.md -test-multilingual-readme.js + diff --git a/src/data/repository.ts b/src/data/repository.ts index 753a61ed8..d382243bd 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -57,12 +57,9 @@ export const fetchRepositoryInformation = async ( const message: any = { type: "hacs/repository/info", repository_id: repositoryId, + language: language ?? hass.language, }; - if (language ?? hass.language) { - message.language = language ?? hass.language; - } - return hass.connection.sendMessagePromise(message); }; From a918ec56c8417e8d3aeff0c938d10af6d92bf028 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 13:22:04 +0100 Subject: [PATCH 20/24] Update PR description: simplify and clarify implementation details --- PULL_REQUEST.md | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index bc4de1d3a..7a84fb4ee 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -2,7 +2,7 @@ ## Summary -This PR adds support for automatic language detection and display of multilingual README files in HACS. The frontend passes the user's Home Assistant language setting (`hass.language`) to the backend, which handles all language processing, file selection, and fallback logic. +This PR adds support for multilingual README files in HACS. The frontend passes the user's Home Assistant language setting (`hass.language`) to the backend via the `hacs/repository/info` WebSocket command. The backend handles all language processing, file selection, and fallback logic. ## Related Backend PR @@ -11,24 +11,16 @@ This frontend implementation requires the corresponding backend changes: ## Changes -### Core Implementation - 1. **Repository Information Fetching** (`src/data/repository.ts`) - - Enhanced `fetchRepositoryInformation()` to accept optional `language` parameter - - Passes `hass.language` to backend if not provided - - All language processing logic is handled by the backend + - Added `language` parameter to `fetchRepositoryInformation()` function + - Always includes `language` field in WebSocket message (uses `hass.language` as fallback) 2. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) - - Updated `_fetchRepository()` to pass `hass.language` to `fetchRepositoryInformation()` - - Added language change detection in `updated()` lifecycle hook - - Automatically reloads repository information when user changes Home Assistant language + - Passes `hass.language` when fetching repository information + - Detects language changes in `updated()` lifecycle hook and automatically refetches repository data 3. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) - - Updated `_fetchRepository()` to pass `hass.language` for consistency - -## File Naming Convention - -Repository maintainers can provide multilingual README files using the pattern `README.{language_code}.md` (e.g., `README.de.md`, `README.fr.md`). The default `README.md` is always used as fallback. + - Passes `hass.language` when fetching repository information ## Checklist @@ -41,5 +33,5 @@ Repository maintainers can provide multilingual README files using the pattern ` ## Notes - This PR only implements the frontend changes. The backend must be updated separately (PR #4965). -- Repository maintainers are not required to provide multilingual READMEs - this is an opt-in feature. +- Repository maintainers can provide multilingual README files using the pattern `README.{language_code}.md` (e.g., `README.de.md`, `README.fr.md`). The default `README.md` is always used as fallback. From c1c09fe18f6c12e744e7f96e72b7bafc855af6e0 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 14:53:23 +0100 Subject: [PATCH 21/24] Update PR description: Add multilingual description support --- PULL_REQUEST.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md index 7a84fb4ee..0d69548e0 100644 --- a/PULL_REQUEST.md +++ b/PULL_REQUEST.md @@ -1,8 +1,8 @@ -# Add Multilingual README Support +# Add Multilingual README and Description Support ## Summary -This PR adds support for multilingual README files in HACS. The frontend passes the user's Home Assistant language setting (`hass.language`) to the backend via the `hacs/repository/info` WebSocket command. The backend handles all language processing, file selection, and fallback logic. +This PR adds support for multilingual README files and repository descriptions in HACS. The frontend passes the user's Home Assistant language setting (`hass.language`) to the backend via the `hacs/repository/info` and `hacs/repositories/list` WebSocket commands. The backend handles all language processing, file selection, and fallback logic. ## Related Backend PR @@ -15,11 +15,15 @@ This frontend implementation requires the corresponding backend changes: - Added `language` parameter to `fetchRepositoryInformation()` function - Always includes `language` field in WebSocket message (uses `hass.language` as fallback) -2. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) +2. **Repository List** (`src/data/websocket.ts`) + - Added `language` parameter to `getRepositories()` function + - Passes language to backend for multilingual description support + +3. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) - Passes `hass.language` when fetching repository information - Detects language changes in `updated()` lifecycle hook and automatically refetches repository data -3. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) +4. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) - Passes `hass.language` when fetching repository information ## Checklist @@ -34,4 +38,5 @@ This frontend implementation requires the corresponding backend changes: - This PR only implements the frontend changes. The backend must be updated separately (PR #4965). - Repository maintainers can provide multilingual README files using the pattern `README.{language_code}.md` (e.g., `README.de.md`, `README.fr.md`). The default `README.md` is always used as fallback. +- Repository descriptions can also be multilingual using `DESCRIPTION.{language_code}.txt` files (e.g., `DESCRIPTION.de.txt`, `DESCRIPTION.fr.txt`). Falls back to GitHub repository description if not found. From fb9d1b9c702778e1ffae4f61ad40f10606a6bf14 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 2 Dec 2025 14:25:16 +0000 Subject: [PATCH 22/24] Refactor: Add multilingual README and description support Co-authored-by: gewinkelt-alpha66 --- PULL_REQUEST.md | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 PULL_REQUEST.md diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md deleted file mode 100644 index 0d69548e0..000000000 --- a/PULL_REQUEST.md +++ /dev/null @@ -1,42 +0,0 @@ -# Add Multilingual README and Description Support - -## Summary - -This PR adds support for multilingual README files and repository descriptions in HACS. The frontend passes the user's Home Assistant language setting (`hass.language`) to the backend via the `hacs/repository/info` and `hacs/repositories/list` WebSocket commands. The backend handles all language processing, file selection, and fallback logic. - -## Related Backend PR - -This frontend implementation requires the corresponding backend changes: -- **Backend PR:** https://github.com/hacs/integration/pull/4965 - -## Changes - -1. **Repository Information Fetching** (`src/data/repository.ts`) - - Added `language` parameter to `fetchRepositoryInformation()` function - - Always includes `language` field in WebSocket message (uses `hass.language` as fallback) - -2. **Repository List** (`src/data/websocket.ts`) - - Added `language` parameter to `getRepositories()` function - - Passes language to backend for multilingual description support - -3. **Repository Dashboard** (`src/dashboards/hacs-repository-dashboard.ts`) - - Passes `hass.language` when fetching repository information - - Detects language changes in `updated()` lifecycle hook and automatically refetches repository data - -4. **Download Dialog** (`src/components/dialogs/hacs-download-dialog.ts`) - - Passes `hass.language` when fetching repository information - -## Checklist - -- [x] Code follows project style guidelines -- [x] Changes are backward compatible -- [x] Code tested locally -- [x] TypeScript types are correct -- [x] Works with backend PR #4965 - -## Notes - -- This PR only implements the frontend changes. The backend must be updated separately (PR #4965). -- Repository maintainers can provide multilingual README files using the pattern `README.{language_code}.md` (e.g., `README.de.md`, `README.fr.md`). The default `README.md` is always used as fallback. -- Repository descriptions can also be multilingual using `DESCRIPTION.{language_code}.txt` files (e.g., `DESCRIPTION.de.txt`, `DESCRIPTION.fr.txt`). Falls back to GitHub repository description if not found. - From 05776dd56b1d3e7e87a0c3604ef6723562b4ba6f Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 15:27:57 +0100 Subject: [PATCH 23/24] fix: Address review comments - remove PULL_REQUEST.md and fix updated() method - Remove accidentally committed PULL_REQUEST.md file - Add proper typing to updated() method with PropertyValues - Remove dead code checking non-existent 'repositories' property --- src/dashboards/hacs-repository-dashboard.ts | 5 +- src/data/websocket.ts | 123 ++++++++++---------- 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/dashboards/hacs-repository-dashboard.ts b/src/dashboards/hacs-repository-dashboard.ts index bad7eb344..dfc9f9990 100644 --- a/src/dashboards/hacs-repository-dashboard.ts +++ b/src/dashboards/hacs-repository-dashboard.ts @@ -141,11 +141,8 @@ export class HacsRepositoryDashboard extends LitElement { } } - protected updated(changedProps) { + protected updated(changedProps: PropertyValues): void { super.updated(changedProps); - if (changedProps.has("repositories") && this._repository) { - this._fetchRepository(); - } if (changedProps.has("hass") && this._repository) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (oldHass && oldHass.language !== this.hass.language) { diff --git a/src/data/websocket.ts b/src/data/websocket.ts index 3757c19d6..2b7271e61 100644 --- a/src/data/websocket.ts +++ b/src/data/websocket.ts @@ -1,61 +1,62 @@ -import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; -import type { Hacs, HacsInfo } from "./hacs"; -import type { HacsDispatchEvent } from "./common"; -import type { RepositoryBase } from "./repository"; - -export const fetchHacsInfo = async (hass: HomeAssistant) => - hass.connection.sendMessagePromise({ - type: "hacs/info", - }); - -export const getRepositories = async (hass: HomeAssistant) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/list", - }); - -export const repositoryUninstall = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repository/remove", - repository, - }); - -export const repositoryAdd = async (hass: HomeAssistant, repository: string, category: string) => - hass.connection.sendMessagePromise>({ - type: "hacs/repositories/add", - repository: repository, - category, - }); - -export const repositoryUpdate = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repository/refresh", - repository, - }); - -export const repositoryDelete = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/remove", - repository, - }); - -export const repositoriesClearNew = async (hass: HomeAssistant, hacs: Hacs) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/clear_new", - categories: hacs.info.categories, - }); - -export const repositoriesClearNewRepository = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/clear_new", - repository, - }); - -export const websocketSubscription = ( - hass: HomeAssistant, - onChange: (result: Record | null) => void, - event: HacsDispatchEvent, -) => - hass.connection.subscribeMessage(onChange, { - type: "hacs/subscribe", - signal: event, - }); +import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; +import type { Hacs, HacsInfo } from "./hacs"; +import type { HacsDispatchEvent } from "./common"; +import type { RepositoryBase } from "./repository"; + +export const fetchHacsInfo = async (hass: HomeAssistant) => + hass.connection.sendMessagePromise({ + type: "hacs/info", + }); + +export const getRepositories = async (hass: HomeAssistant, language?: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/list", + language: language ?? hass.language, + }); + +export const repositoryUninstall = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repository/remove", + repository, + }); + +export const repositoryAdd = async (hass: HomeAssistant, repository: string, category: string) => + hass.connection.sendMessagePromise>({ + type: "hacs/repositories/add", + repository: repository, + category, + }); + +export const repositoryUpdate = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repository/refresh", + repository, + }); + +export const repositoryDelete = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/remove", + repository, + }); + +export const repositoriesClearNew = async (hass: HomeAssistant, hacs: Hacs) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/clear_new", + categories: hacs.info.categories, + }); + +export const repositoriesClearNewRepository = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/clear_new", + repository, + }); + +export const websocketSubscription = ( + hass: HomeAssistant, + onChange: (result: Record | null) => void, + event: HacsDispatchEvent, +) => + hass.connection.subscribeMessage(onChange, { + type: "hacs/subscribe", + signal: event, + }); From 93ffd45d688d1f31c5a6c5960319fb36965ed6f0 Mon Sep 17 00:00:00 2001 From: rosch100 Date: Tue, 2 Dec 2025 15:35:28 +0100 Subject: [PATCH 24/24] refactor: Remove language parameter from getRepositories() - Remove language parameter from getRepositories() function - Language support is now only for README files via repository/info --- src/data/websocket.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/websocket.ts b/src/data/websocket.ts index 2b7271e61..e729363da 100644 --- a/src/data/websocket.ts +++ b/src/data/websocket.ts @@ -8,10 +8,9 @@ export const fetchHacsInfo = async (hass: HomeAssistant) => type: "hacs/info", }); -export const getRepositories = async (hass: HomeAssistant, language?: string) => +export const getRepositories = async (hass: HomeAssistant) => hass.connection.sendMessagePromise({ type: "hacs/repositories/list", - language: language ?? hass.language, }); export const repositoryUninstall = async (hass: HomeAssistant, repository: string) =>