diff --git a/README.md b/README.md index a96aa92..ee14f0c 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ Options are grouped into three sections in the integration settings. | Option | Default | Description | | --- | --- | --- | | Hide Markdown toolbar | `false` | Remove formatting controls from the note editor | +| Hide markdown placeholder text | `false` | Hide the long Markdown formatting hint shown in the empty note field | | Hide the Preview button | `false` | Remove the live preview toggle from the editor | | Use compact empty note field until clicked | `false` | Show a simple placeholder first, then reveal formatting controls and Markdown hints when editing starts | | Custom empty note placeholder | blank | Override the simple empty-note placeholder; leave blank to use the translated default | @@ -217,6 +218,10 @@ Notes are included in normal Home Assistant backups. The integration also provid | `entity_notes.backup_notes` | `/entity_notes_backup.json` | | `entity_notes.restore_notes` | `/entity_notes_backup.json` | +## Development Testing + +For testing unreleased changes on a Home Assistant system, see [Dev Testing In Home Assistant](docs/dev-testing.md). + ## Troubleshooting ### Notes Do Not Appear @@ -246,22 +251,23 @@ Thanks to [@Bjoern3D](https://github.com/Bjoern3D) for the Markdown toolbar, und ### Adding A Translation -All UI strings live in the `strings` object near the top of `custom_components/entity_notes/entity-notes.js`, organised by language code. The correct language is picked automatically at runtime based on each user's Home Assistant language setting, falling back to English for any missing keys. +All frontend UI strings live in the `frontend` section of the integration translation files under `custom_components/entity_notes/translations/`. The correct language is picked automatically at runtime based on each user's Home Assistant language setting, falling back to English for any missing keys. To add a new language: -1. Open `entity-notes.js` and find the `strings` object inside `window.entityNotes`. -2. Add a new block using the appropriate [BCP 47 language code](https://developers.home-assistant.io/docs/internationalization/core/#supported-languages) (e.g. `fr` for French, `de` for German): +1. Copy `custom_components/entity_notes/translations/en.json` to a new file using the appropriate [BCP 47 language code](https://developers.home-assistant.io/docs/internationalization/core/#supported-languages) (e.g. `fr.json` for French, `de.json` for German). +2. Translate the values in the `frontend`, `config`, and `options` sections: -```js -fr: { - save: 'ENREGISTRER', - delete: 'SUPPRIMER', - // ... all keys from the 'en' block -}, +```json +{ + "frontend": { + "save": "ENREGISTRER", + "delete": "SUPPRIMER" + } +} ``` -3. Translate every key from the `en` block. Any key you omit will automatically fall back to English. +3. Translate every key from the `frontend` section. Any key you omit will automatically fall back to English. 4. Open a pull request. ## Support diff --git a/custom_components/entity_notes/__init__.py b/custom_components/entity_notes/__init__.py index c2bd062..70c1e15 100644 --- a/custom_components/entity_notes/__init__.py +++ b/custom_components/entity_notes/__init__.py @@ -29,6 +29,7 @@ CONF_ENABLE_DEVICE_NOTES, CONF_SHOW_MARKDOWN_TOOLBAR, CONF_HIDE_MARKDOWN_TOOLBAR, + CONF_HIDE_MARKDOWN_PLACEHOLDER, CONF_CONFIRM_DELETE, CONF_HIDE_PREVIEW_BUTTON, CONF_HIDE_MARKDOWN_HINTS, @@ -46,6 +47,7 @@ DEFAULT_CONFIRM_DELETE, DEFAULT_SHOW_MARKDOWN_TOOLBAR, DEFAULT_HIDE_MARKDOWN_TOOLBAR, + DEFAULT_HIDE_MARKDOWN_PLACEHOLDER, DEFAULT_HIDE_PREVIEW_BUTTON, DEFAULT_HIDE_MARKDOWN_HINTS, DEFAULT_EMPTY_NOTE_PLACEHOLDER, @@ -116,6 +118,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: not options.get(CONF_SHOW_MARKDOWN_TOOLBAR, DEFAULT_SHOW_MARKDOWN_TOOLBAR), ) show_markdown_toolbar = not hide_markdown_toolbar + hide_markdown_placeholder = options.get(CONF_HIDE_MARKDOWN_PLACEHOLDER, DEFAULT_HIDE_MARKDOWN_PLACEHOLDER) confirm_delete = options.get(CONF_CONFIRM_DELETE, DEFAULT_CONFIRM_DELETE) hide_preview_button = options.get(CONF_HIDE_PREVIEW_BUTTON, DEFAULT_HIDE_PREVIEW_BUTTON) hide_markdown_hints = options.get(CONF_HIDE_MARKDOWN_HINTS, DEFAULT_HIDE_MARKDOWN_HINTS) @@ -234,6 +237,7 @@ def _write_migrated(): CONF_ENABLE_DEVICE_NOTES: enable_device_notes, CONF_SHOW_MARKDOWN_TOOLBAR: show_markdown_toolbar, CONF_HIDE_MARKDOWN_TOOLBAR: hide_markdown_toolbar, + CONF_HIDE_MARKDOWN_PLACEHOLDER: hide_markdown_placeholder, CONF_CONFIRM_DELETE: confirm_delete, CONF_HIDE_PREVIEW_BUTTON: hide_preview_button, CONF_HIDE_MARKDOWN_HINTS: hide_markdown_hints, @@ -800,6 +804,29 @@ class EntityNotesJSView(HomeAssistantView): name = "api:entity_notes_js" requires_auth = False + def _load_frontend_translations(self): + """Load frontend strings from Home Assistant translation files.""" + translations_path = Path(__file__).parent / "translations" + translations = {} + + for translation_file in translations_path.glob("*.json"): + try: + with open(translation_file, 'r') as f: + translation_data = json.load(f) + except Exception as e: + _LOGGER.warning( + "Could not load Entity Notes translation file %s: %s", + translation_file, + e, + ) + continue + + frontend_strings = translation_data.get("frontend") + if isinstance(frontend_strings, dict): + translations[translation_file.stem] = frontend_strings + + return translations + async def get(self, request): """Serve the JavaScript file.""" hass = request.app["hass"] @@ -811,6 +838,7 @@ async def get(self, request): enable_device_notes = hass.data[DOMAIN]["config"].get(CONF_ENABLE_DEVICE_NOTES, True) hide_markdown_toolbar = hass.data[DOMAIN]["config"].get(CONF_HIDE_MARKDOWN_TOOLBAR, DEFAULT_HIDE_MARKDOWN_TOOLBAR) show_markdown_toolbar = not hide_markdown_toolbar + hide_markdown_placeholder = hass.data[DOMAIN]["config"].get(CONF_HIDE_MARKDOWN_PLACEHOLDER, DEFAULT_HIDE_MARKDOWN_PLACEHOLDER) confirm_delete = hass.data[DOMAIN]["config"].get(CONF_CONFIRM_DELETE, True) hide_preview_button = hass.data[DOMAIN]["config"].get(CONF_HIDE_PREVIEW_BUTTON, False) hide_markdown_hints = hass.data[DOMAIN]["config"].get(CONF_HIDE_MARKDOWN_HINTS, False) @@ -827,6 +855,7 @@ def read_file(): return f.read() js_content = await hass.async_add_executor_job(read_file) + translations = await hass.async_add_executor_job(self._load_frontend_translations) # Replace configuration placeholders js_content = js_content.replace('{{DEBUG_LOGGING}}', str(debug_logging).lower()) @@ -837,10 +866,12 @@ def read_file(): js_content = js_content.replace('{{ENABLE_DEVICE_NOTES}}', str(enable_device_notes).lower()) js_content = js_content.replace('{{CONFIRM_DELETE}}', str(confirm_delete).lower()) js_content = js_content.replace('{{SHOW_MARKDOWN_TOOLBAR}}', str(show_markdown_toolbar).lower()) + js_content = js_content.replace('{{HIDE_MARKDOWN_PLACEHOLDER}}', str(hide_markdown_placeholder).lower()) js_content = js_content.replace('{{HIDE_PREVIEW_BUTTON}}', str(hide_preview_button).lower()) js_content = js_content.replace('{{HIDE_MARKDOWN_HINTS}}', str(hide_markdown_hints).lower()) js_content = js_content.replace('{{EMPTY_NOTE_PLACEHOLDER}}', json.dumps(empty_note_placeholder)) js_content = js_content.replace('{{HIDE_LAST_MODIFIED}}', str(hide_last_modified).lower()) + js_content = js_content.replace('{{FRONTEND_TRANSLATIONS}}', json.dumps(translations)) return web.Response( text=js_content, diff --git a/custom_components/entity_notes/config_flow.py b/custom_components/entity_notes/config_flow.py index cccaf63..9654605 100644 --- a/custom_components/entity_notes/config_flow.py +++ b/custom_components/entity_notes/config_flow.py @@ -21,6 +21,7 @@ CONF_DELETE_NOTES_WITH_ENTITY, CONF_HIDE_MARKDOWN_TOOLBAR, CONF_SHOW_MARKDOWN_TOOLBAR, + CONF_HIDE_MARKDOWN_PLACEHOLDER, CONF_CONFIRM_DELETE, CONF_HIDE_PREVIEW_BUTTON, CONF_HIDE_MARKDOWN_HINTS, @@ -35,6 +36,7 @@ DEFAULT_DELETE_NOTES_WITH_ENTITY, DEFAULT_HIDE_MARKDOWN_TOOLBAR, DEFAULT_SHOW_MARKDOWN_TOOLBAR, + DEFAULT_HIDE_MARKDOWN_PLACEHOLDER, DEFAULT_CONFIRM_DELETE, DEFAULT_HIDE_PREVIEW_BUTTON, DEFAULT_HIDE_MARKDOWN_HINTS, @@ -52,6 +54,7 @@ SECTION_CONFIG = [ (SECTION_DISPLAY, [ (CONF_HIDE_MARKDOWN_TOOLBAR, DEFAULT_HIDE_MARKDOWN_TOOLBAR, bool), + (CONF_HIDE_MARKDOWN_PLACEHOLDER, DEFAULT_HIDE_MARKDOWN_PLACEHOLDER, bool), (CONF_HIDE_PREVIEW_BUTTON, DEFAULT_HIDE_PREVIEW_BUTTON, bool), (CONF_HIDE_MARKDOWN_HINTS, DEFAULT_HIDE_MARKDOWN_HINTS, bool), (CONF_EMPTY_NOTE_PLACEHOLDER, DEFAULT_EMPTY_NOTE_PLACEHOLDER, vol.All(str, vol.Length(max=120))), diff --git a/custom_components/entity_notes/const.py b/custom_components/entity_notes/const.py index f77211a..71e5623 100644 --- a/custom_components/entity_notes/const.py +++ b/custom_components/entity_notes/const.py @@ -18,6 +18,7 @@ CONF_ENABLE_DEVICE_NOTES = "enable_device_notes" CONF_SHOW_MARKDOWN_TOOLBAR = "show_markdown_toolbar" CONF_HIDE_MARKDOWN_TOOLBAR = "hide_markdown_toolbar" +CONF_HIDE_MARKDOWN_PLACEHOLDER = "hide_markdown_placeholder" CONF_CONFIRM_DELETE = "confirm_delete" CONF_HIDE_PREVIEW_BUTTON = "hide_preview_button" CONF_HIDE_MARKDOWN_HINTS = "hide_markdown_hints" @@ -54,6 +55,7 @@ DEFAULT_ENABLE_DEVICE_NOTES = True DEFAULT_SHOW_MARKDOWN_TOOLBAR = True DEFAULT_HIDE_MARKDOWN_TOOLBAR = False +DEFAULT_HIDE_MARKDOWN_PLACEHOLDER = False DEFAULT_CONFIRM_DELETE = True DEFAULT_HIDE_PREVIEW_BUTTON = False DEFAULT_HIDE_MARKDOWN_HINTS = False diff --git a/custom_components/entity_notes/entity-notes.js b/custom_components/entity_notes/entity-notes.js index 7b7f01b..91d4143 100644 --- a/custom_components/entity_notes/entity-notes.js +++ b/custom_components/entity_notes/entity-notes.js @@ -11,42 +11,12 @@ window.entityNotes = { enableDeviceNotes: {{ENABLE_DEVICE_NOTES}}, confirmDelete: {{CONFIRM_DELETE}}, showMarkdownToolbar: {{SHOW_MARKDOWN_TOOLBAR}}, + hideMarkdownPlaceholder: {{HIDE_MARKDOWN_PLACEHOLDER}}, hidePreviewButton: {{HIDE_PREVIEW_BUTTON}}, hideMarkdownHints: {{HIDE_MARKDOWN_HINTS}}, emptyNotePlaceholder: {{EMPTY_NOTE_PLACEHOLDER}}, hideLastModified: {{HIDE_LAST_MODIFIED}}, - - strings: { - en: { - save: 'SAVE', - delete: 'DELETE', - preview: 'Preview', - add_note: 'Add a note...', - markdown_hints: 'Notes (# H1, ## H2, **bold**, *italic*, - bullets, 1. numbered, --- divider, `inline code`, > blockquote, ~strikethrough~)', - preview_empty: 'Preview (empty)', - confirm_delete: 'Are you sure you want to delete the note for {type} {item_id}?', - toolbar_toggle_preview: 'Toggle Live Preview', - toolbar_undo: 'Undo (Ctrl+Z)', - toolbar_redo: 'Redo (Ctrl+Y)', - toolbar_heading1: 'Heading 1', - toolbar_heading2: 'Heading 2', - toolbar_bold: 'Bold', - toolbar_italic: 'Italic', - toolbar_bullet_list: 'Bullet list', - toolbar_numbered_list: 'Numbered list', - toolbar_divider: 'Divider', - toolbar_inline_code: 'Inline Code', - toolbar_code_block: 'Code Block', - toolbar_insert_link: 'Insert Link', - toolbar_blockquote: 'Blockquote', - toolbar_strikethrough: 'Strikethrough', - error_loading_note: 'Error loading note.', - prompt_link_text: 'Enter link text:', - prompt_link_url: 'Enter URL:', - }, - // Community translations — add your language here and open a pull request. - // Keys must match the 'en' block above. Missing keys fall back to English. - }, + translations: {{FRONTEND_TRANSLATIONS}}, // Convenience methods for users enableDebug: function() { @@ -74,8 +44,10 @@ function infoLog(message) { function localize(key, replacements) { const ha = document.querySelector('home-assistant'); const lang = ha?.hass?.language || 'en'; - const strings = window.entityNotes.strings[lang] || window.entityNotes.strings['en']; - let str = strings[key] ?? window.entityNotes.strings['en'][key] ?? key; + const languageCode = lang.split('-')[0]; + const translations = window.entityNotes.translations || {}; + const strings = translations[lang] || translations[languageCode] || translations['en'] || {}; + let str = strings[key] ?? translations['en']?.[key] ?? key; if (replacements) { for (const [k, v] of Object.entries(replacements)) { str = str.replace(`{${k}}`, v); @@ -92,6 +64,13 @@ function emptyNotePlaceholder() { return localize('add_note'); } +function markdownPlaceholder() { + if (window.entityNotes.hideMarkdownPlaceholder) { + return emptyNotePlaceholder(); + } + return localize('markdown_hints'); +} + class EntityNotesCard extends HTMLElement { constructor() { super(); @@ -116,7 +95,7 @@ class EntityNotesCard extends HTMLElement { } catch (e) { debugLog('Entity Notes: Error getting user name: ' + e); } - return "User"; + return localize('default_user'); } get accessToken() { @@ -178,7 +157,7 @@ class EntityNotesCard extends HTMLElement { const maxLength = window.entityNotes.maxNoteLength; const previewButtonHtml = window.entityNotes.hidePreviewButton ? '' : ``; - const initialPlaceholder = window.entityNotes.hideMarkdownHints ? emptyNotePlaceholder() : localize('markdown_hints'); + const initialPlaceholder = window.entityNotes.hideMarkdownHints ? emptyNotePlaceholder() : markdownPlaceholder(); this.shadowRoot.innerHTML = `