Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -217,6 +218,10 @@ Notes are included in normal Home Assistant backups. The integration also provid
| `entity_notes.backup_notes` | `<config_directory>/entity_notes_backup.json` |
| `entity_notes.restore_notes` | `<config_directory>/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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions custom_components/entity_notes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"]
Expand All @@ -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)
Expand All @@ -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())
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions custom_components/entity_notes/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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))),
Expand Down
2 changes: 2 additions & 0 deletions custom_components/entity_notes/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
61 changes: 20 additions & 41 deletions custom_components/entity_notes/entity-notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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() {
Expand Down Expand Up @@ -178,7 +157,7 @@ class EntityNotesCard extends HTMLElement {
const maxLength = window.entityNotes.maxNoteLength;
const previewButtonHtml = window.entityNotes.hidePreviewButton ? '' :
`<button class="entity-notes-md-button" data-action="toggle-preview" title="${localize('toolbar_toggle_preview')}" style="width: auto; padding: 0 8px;" disabled>${localize('preview')}</button>`;
const initialPlaceholder = window.entityNotes.hideMarkdownHints ? emptyNotePlaceholder() : localize('markdown_hints');
const initialPlaceholder = window.entityNotes.hideMarkdownHints ? emptyNotePlaceholder() : markdownPlaceholder();
this.shadowRoot.innerHTML = `
<style>
.entity-notes-container {
Expand Down Expand Up @@ -893,7 +872,7 @@ class EntityNotesCard extends HTMLElement {

textarea.addEventListener('focus', () => {
if (window.entityNotes.hideMarkdownHints) {
textarea.placeholder = localize('markdown_hints');
textarea.placeholder = markdownPlaceholder();
}
this.autoResize();
this.updateCharCountVisibility();
Expand Down Expand Up @@ -1276,11 +1255,11 @@ class EntityNotesCard extends HTMLElement {
debugLog(`Entity Notes: Note saved successfully for ${type}, hasExistingNote: ${this.hasExistingNote}`);
} else {
console.error(`Entity Notes: Save failed for ${type} - HTTP ${response.status}`);
alert(`Entity Notes: Save failed (HTTP ${response.status}). Your session might have expired. Please reload the page.`);
alert(localize('save_failed', { status: response.status }));
}
} catch (error) {
console.error(`Entity Notes: Error saving note for ${type}:`, error);
alert(`Entity Notes: Connection error during save. Please check your network connection.`);
alert(localize('save_connection_error'));
}
}

Expand Down Expand Up @@ -1331,11 +1310,11 @@ class EntityNotesCard extends HTMLElement {
debugLog(`Entity Notes: Note deleted successfully for ${type}`);
} else {
console.error(`Entity Notes: Delete failed for ${type} - HTTP ${response.status}`);
alert(`Entity Notes: Delete failed (HTTP ${response.status}). Your session might have expired. Please reload the page.`);
alert(localize('delete_failed', { status: response.status }));
}
} catch (error) {
console.error(`Entity Notes: Error deleting note for ${type}:`, error);
alert(`Entity Notes: Connection error during delete. Please check your network connection.`);
alert(localize('delete_connection_error'));
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions custom_components/entity_notes/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"name": "Display",
"data": {
"hide_markdown_toolbar": "Hide markdown formatting toolbar",
"hide_markdown_placeholder": "Hide markdown placeholder text",
"hide_preview_button": "Hide the Preview button",
"hide_markdown_hints": "Use compact empty note field until clicked",
"empty_note_placeholder": "Custom empty note placeholder",
Expand Down Expand Up @@ -43,6 +44,7 @@
"name": "Display",
"data": {
"hide_markdown_toolbar": "Hide markdown formatting toolbar",
"hide_markdown_placeholder": "Hide markdown placeholder text",
"hide_preview_button": "Hide the Preview button",
"hide_markdown_hints": "Use compact empty note field until clicked",
"empty_note_placeholder": "Custom empty note placeholder",
Expand Down Expand Up @@ -89,6 +91,7 @@
"name": "Display",
"data": {
"hide_markdown_toolbar": "Hide markdown formatting toolbar",
"hide_markdown_placeholder": "Hide markdown placeholder text",
"hide_preview_button": "Hide the Preview button",
"hide_markdown_hints": "Use compact empty note field until clicked",
"empty_note_placeholder": "Custom empty note placeholder",
Expand Down Expand Up @@ -119,5 +122,37 @@
"error": {
"invalid_max_length": "Maximum note length must be between 50 and 2000 characters"
}
},
"frontend": {
"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:",
"default_user": "User",
"save_failed": "Entity Notes: Save failed (HTTP {status}). Your session might have expired. Please reload the page.",
"save_connection_error": "Entity Notes: Connection error during save. Please check your network connection.",
"delete_failed": "Entity Notes: Delete failed (HTTP {status}). Your session might have expired. Please reload the page.",
"delete_connection_error": "Entity Notes: Connection error during delete. Please check your network connection."
}
}
Loading
Loading