Refactor i18n translations for better maintainability

- Remove page_title section, compose titles dynamically as "{{entity}} - {{network_name}}"
- Add entities section for centralized entity names (nodes, members, tags, etc.)
- Replace specific action translations with composed patterns (add_entity, edit_entity, etc.)
- Create links section for common platform names (github, discord, youtube)
- Remove redundant page-specific title fields, use entity names instead
- Update all page components to use new translation structure
- Keep user-defined strings (network_name) separate from translatable content

This follows i18n best practices by using composition over duplication,
centralizing reusable terms, and making it easier to add new languages.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Louis King
2026-02-13 21:19:02 +00:00
parent 127cd7adf6
commit 85129e528e
13 changed files with 112 additions and 118 deletions
+22 -11
View File
@@ -126,6 +126,17 @@ function updateNavActiveState(pathname) {
}
}
/**
* Compose a page title from entity name and network name.
* @param {string} entityKey - Translation key for entity (e.g., 'entities.dashboard')
* @returns {string}
*/
function composePageTitle(entityKey) {
const networkName = config.network_name || 'MeshCore Network';
const entity = t(entityKey);
return `${entity} - ${networkName}`;
}
/**
* Update the page title based on the current route.
* @param {string} pathname
@@ -134,24 +145,24 @@ function updatePageTitle(pathname) {
const networkName = config.network_name || 'MeshCore Network';
const titles = {
'/': networkName,
'/a': t('page_title.admin', { network_name: networkName }),
'/a/': t('page_title.admin', { network_name: networkName }),
'/a/node-tags': t('page_title.admin_node_tags', { network_name: networkName }),
'/a/members': t('page_title.admin_members', { network_name: networkName }),
'/a': composePageTitle('entities.admin'),
'/a/': composePageTitle('entities.admin'),
'/a/node-tags': `${t('entities.tags')} - ${t('entities.admin')} - ${networkName}`,
'/a/members': `${t('entities.members')} - ${t('entities.admin')} - ${networkName}`,
};
// Add feature-dependent titles
if (features.dashboard !== false) titles['/dashboard'] = t('page_title.dashboard', { network_name: networkName });
if (features.nodes !== false) titles['/nodes'] = t('page_title.nodes', { network_name: networkName });
if (features.messages !== false) titles['/messages'] = t('page_title.messages', { network_name: networkName });
if (features.advertisements !== false) titles['/advertisements'] = t('page_title.advertisements', { network_name: networkName });
if (features.map !== false) titles['/map'] = t('page_title.map', { network_name: networkName });
if (features.members !== false) titles['/members'] = t('page_title.members', { network_name: networkName });
if (features.dashboard !== false) titles['/dashboard'] = composePageTitle('entities.dashboard');
if (features.nodes !== false) titles['/nodes'] = composePageTitle('entities.nodes');
if (features.messages !== false) titles['/messages'] = composePageTitle('entities.messages');
if (features.advertisements !== false) titles['/advertisements'] = composePageTitle('entities.advertisements');
if (features.map !== false) titles['/map'] = composePageTitle('entities.map');
if (features.members !== false) titles['/members'] = composePageTitle('entities.members');
if (titles[pathname]) {
document.title = titles[pathname];
} else if (pathname.startsWith('/nodes/')) {
document.title = t('page_title.node_detail', { network_name: networkName });
document.title = composePageTitle('entities.node_detail');
} else if (pathname.startsWith('/pages/')) {
// Custom pages set their own title in the page module
document.title = networkName;
@@ -31,7 +31,7 @@ export async function render(container, params, router) {
litRender(html`
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-3xl font-bold">${t('admin.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.admin')}</h1>
<div class="text-sm breadcrumbs">
<ul>
<li><a href="/">${t('nav.home')}</a></li>
@@ -85,12 +85,12 @@ export async function render(container, params, router) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold">${t('admin_members.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.members')}</h1>
<div class="text-sm breadcrumbs">
<ul>
<li><a href="/">${t('nav.home')}</a></li>
<li><a href="/a/">${t('nav.admin')}</a></li>
<li>${t('admin_members.title')}</li>
<li>${t('entities.members')}</li>
</ul>
</div>
</div>
@@ -103,7 +103,7 @@ ${flashHtml}
<div class="card-body">
<div class="flex justify-between items-center">
<h2 class="card-title">${t('admin_members.network_members', { count: members.length })}</h2>
<button id="btn-add-member" class="btn btn-primary btn-sm">${t('admin_members.add_member')}</button>
<button id="btn-add-member" class="btn btn-primary btn-sm">${t('common.add_entity', { entity: t('entities.member') })}</button>
</div>
${tableHtml}
</div>
@@ -111,7 +111,7 @@ ${flashHtml}
<dialog id="addModal" class="modal">
<div class="modal-box w-11/12 max-w-2xl">
<h3 class="font-bold text-lg">${t('admin_members.add_new_member')}</h3>
<h3 class="font-bold text-lg">${t('common.add_new_entity', { entity: t('entities.member') })}</h3>
<form id="add-member-form" class="py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
@@ -151,7 +151,7 @@ ${flashHtml}
</div>
<div class="modal-action">
<button type="button" class="btn" id="addCancel">${t('common.cancel')}</button>
<button type="submit" class="btn btn-primary">${t('admin_members.add_member')}</button>
<button type="submit" class="btn btn-primary">${t('common.add_entity', { entity: t('entities.member') })}</button>
</div>
</form>
</div>
@@ -160,7 +160,7 @@ ${flashHtml}
<dialog id="editModal" class="modal">
<div class="modal-box w-11/12 max-w-2xl">
<h3 class="font-bold text-lg">${t('admin_members.edit_member')}</h3>
<h3 class="font-bold text-lg">${t('common.edit_entity', { entity: t('entities.member') })}</h3>
<form id="edit-member-form" class="py-4">
<input type="hidden" name="id" id="edit_id">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -206,7 +206,7 @@ ${flashHtml}
<dialog id="deleteModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">${t('admin_members.delete_member')}</h3>
<h3 class="font-bold text-lg">${t('common.delete_entity', { entity: t('entities.member') })}</h3>
<div class="py-4">
<p class="py-4">Are you sure you want to delete member <strong id="delete_member_name"></strong>?</p>
<div class="alert alert-error mb-4">
@@ -249,7 +249,7 @@ ${flashHtml}
try {
await apiPost('/api/v1/members', body);
container.querySelector('#addModal').close();
router.navigate('/a/members?message=' + encodeURIComponent(t('admin_members.member_added')));
router.navigate('/a/members?message=' + encodeURIComponent(t('admin_members.entity_added')));
} catch (err) {
container.querySelector('#addModal').close();
router.navigate('/a/members?error=' + encodeURIComponent(err.message));
@@ -289,7 +289,7 @@ ${flashHtml}
try {
await apiPut('/api/v1/members/' + encodeURIComponent(id), body);
container.querySelector('#editModal').close();
router.navigate('/a/members?message=' + encodeURIComponent(t('admin_members.member_updated')));
router.navigate('/a/members?message=' + encodeURIComponent(t('admin_members.entity_updated')));
} catch (err) {
container.querySelector('#editModal').close();
router.navigate('/a/members?error=' + encodeURIComponent(err.message));
@@ -314,7 +314,7 @@ ${flashHtml}
try {
await apiDelete('/api/v1/members/' + encodeURIComponent(activeDeleteId));
container.querySelector('#deleteModal').close();
router.navigate('/a/members?message=' + encodeURIComponent(t('admin_members.member_deleted')));
router.navigate('/a/members?message=' + encodeURIComponent(t('admin_members.entity_deleted')));
} catch (err) {
container.querySelector('#deleteModal').close();
router.navigate('/a/members?error=' + encodeURIComponent(err.message));
@@ -131,7 +131,7 @@ export async function render(container, params, router) {
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">${t('admin_node_tags.add_new_tag')}</h2>
<h2 class="card-title">${t('common.add_new_entity', { entity: t('entities.tag') })}</h2>
<form id="add-tag-form" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">${t('common.key')}</span></label>
@@ -151,7 +151,7 @@ export async function render(container, params, router) {
</div>
<div class="form-control">
<label class="label"><span class="label-text">&nbsp;</span></label>
<button type="submit" class="btn btn-primary">${t('admin_node_tags.add_tag')}</button>
<button type="submit" class="btn btn-primary">${t('common.add_entity', { entity: t('entities.tag') })}</button>
</div>
</form>
</div>
@@ -159,7 +159,7 @@ export async function render(container, params, router) {
<dialog id="editModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">${t('admin_node_tags.edit_tag')}</h3>
<h3 class="font-bold text-lg">${t('common.edit_entity', { entity: t('entities.tag') })}</h3>
<form id="edit-tag-form" class="py-4">
<div class="form-control mb-4">
<label class="label"><span class="label-text">${t('common.key')}</span></label>
@@ -188,7 +188,7 @@ export async function render(container, params, router) {
<dialog id="moveModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">${t('admin_node_tags.move_tag_title')}</h3>
<h3 class="font-bold text-lg">${t('admin_node_tags.move_to_another_node')}</h3>
<form id="move-tag-form" class="py-4">
<div class="form-control mb-4">
<label class="label"><span class="label-text">${t('admin_node_tags.tag_key')}</span></label>
@@ -207,11 +207,11 @@ export async function render(container, params, router) {
</div>
<div class="alert alert-warning mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span>${t('admin_node_tags.move_tag_warning')}</span>
<span>${t('admin_node_tags.move_warning')}</span>
</div>
<div class="modal-action">
<button type="button" class="btn" id="moveCancel">${t('common.cancel')}</button>
<button type="submit" class="btn btn-warning">${t('admin_node_tags.move_tag')}</button>
<button type="submit" class="btn btn-warning">${t('common.move_entity', { entity: t('entities.tag') })}</button>
</div>
</form>
</div>
@@ -220,7 +220,7 @@ export async function render(container, params, router) {
<dialog id="deleteModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">${t('admin_node_tags.delete_tag')}</h3>
<h3 class="font-bold text-lg">${t('common.delete_entity', { entity: t('entities.tag') })}</h3>
<div class="py-4">
<p class="py-4">Are you sure you want to delete the tag "<span id="deleteKeyDisplay" class="font-mono font-semibold"></span>"?</p>
<div class="alert alert-error mb-4">
@@ -267,7 +267,7 @@ export async function render(container, params, router) {
<dialog id="deleteAllModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">${t('admin_node_tags.delete_all_title')}</h3>
<h3 class="font-bold text-lg">${t('common.delete_all_entity', { entity: t('entities.tags') })}</h3>
<div class="py-4">
<p class="mb-4">${unsafeHTML(t('admin_node_tags.delete_all_confirm', { count: tags.length, name: nodeName }))}</p>
<div class="alert alert-error mb-4">
@@ -276,7 +276,7 @@ export async function render(container, params, router) {
</div>
<div class="modal-action">
<button type="button" class="btn" id="deleteAllCancel">${t('common.cancel')}</button>
<button type="button" class="btn btn-error" id="deleteAllConfirm">${t('admin_node_tags.delete_all_tags')}</button>
<button type="button" class="btn btn-error" id="deleteAllConfirm">${t('common.delete_all_entity', { entity: t('entities.tags') })}</button>
</div>
</div>
</div>
@@ -302,12 +302,12 @@ export async function render(container, params, router) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold">${t('admin_node_tags.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.tags')}</h1>
<div class="text-sm breadcrumbs">
<ul>
<li><a href="/">${t('nav.home')}</a></li>
<li><a href="/a/">${t('nav.admin')}</a></li>
<li>${t('admin_node_tags.title')}</li>
<li>${t('entities.tags')}</li>
</ul>
</div>
</div>
@@ -371,7 +371,7 @@ ${contentHtml}`, container);
await apiPost('/api/v1/nodes/' + encodeURIComponent(selectedPublicKey) + '/tags', {
key, value, value_type,
});
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.tag_added')));
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.entity_added')));
} catch (err) {
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&error=' + encodeURIComponent(err.message));
}
@@ -403,7 +403,7 @@ ${contentHtml}`, container);
value, value_type,
});
container.querySelector('#editModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.tag_updated')));
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.entity_updated')));
} catch (err) {
container.querySelector('#editModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&error=' + encodeURIComponent(err.message));
@@ -435,7 +435,7 @@ ${contentHtml}`, container);
new_public_key: newPublicKey,
});
container.querySelector('#moveModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.tag_moved')));
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.entity_moved')));
} catch (err) {
container.querySelector('#moveModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&error=' + encodeURIComponent(err.message));
@@ -460,7 +460,7 @@ ${contentHtml}`, container);
try {
await apiDelete('/api/v1/nodes/' + encodeURIComponent(selectedPublicKey) + '/tags/' + encodeURIComponent(activeTagKey));
container.querySelector('#deleteModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.tag_deleted')));
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.entity_deleted')));
} catch (err) {
container.querySelector('#deleteModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&error=' + encodeURIComponent(err.message));
@@ -487,7 +487,7 @@ ${contentHtml}`, container);
try {
const result = await apiPost('/api/v1/nodes/' + encodeURIComponent(selectedPublicKey) + '/tags/copy-to/' + encodeURIComponent(destKey));
container.querySelector('#copyAllModal').close();
const msg = t('admin_node_tags.copied_tags', { copied: result.copied, skipped: result.skipped });
const msg = t('admin_node_tags.copied_entities', { copied: result.copied, skipped: result.skipped });
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(msg));
} catch (err) {
container.querySelector('#copyAllModal').close();
@@ -511,7 +511,7 @@ ${contentHtml}`, container);
try {
await apiDelete('/api/v1/nodes/' + encodeURIComponent(selectedPublicKey) + '/tags');
container.querySelector('#deleteAllModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.all_tags_deleted')));
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&message=' + encodeURIComponent(t('admin_node_tags.all_entities_deleted')));
} catch (err) {
container.querySelector('#deleteAllModal').close();
router.navigate('/a/node-tags?public_key=' + encodeURIComponent(selectedPublicKey) + '&error=' + encodeURIComponent(err.message));
@@ -23,7 +23,7 @@ export async function render(container, params, router) {
function renderPage(content, { total = null } = {}) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">${t('advertisements.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.advertisements')}</h1>
<div class="flex items-center gap-2">
${tzBadge}
${total !== null ? html`<span class="badge badge-lg">${t('common.total', { count: total })}</span>` : nothing}
@@ -142,7 +142,7 @@ export async function render(container, params, router) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">${t('dashboard.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.dashboard')}</h1>
</div>
${topCount > 0 ? html`
@@ -11,7 +11,7 @@ import {
function renderRadioConfig(rc) {
if (!rc) return nothing;
const fields = [
[t('home.profile'), rc.profile],
[t('links.profile'), rc.profile],
[t('home.frequency'), rc.frequency],
[t('home.bandwidth'), rc.bandwidth],
[t('home.spreading_factor'), rc.spreading_factor],
@@ -164,11 +164,11 @@ export async function render(container, params, router) {
<div class="flex gap-2 mt-4">
<a href="https://meshcore.co.uk/" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm">
${iconGlobe('h-4 w-4 mr-1')}
${t('home.website')}
${t('links.website')}
</a>
<a href="https://github.com/meshcore-dev/MeshCore" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm">
${iconGithub('h-4 w-4 mr-1')}
${t('home.github')}
${t('links.github')}
</a>
</div>
</div>
@@ -173,7 +173,7 @@ export async function render(container, params, router) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">${t('map.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.map')}</h1>
<div class="flex items-center gap-2">
${timezoneIndicator()}
<span id="node-count" class="badge badge-lg">${t('common.loading')}</span>
@@ -85,8 +85,8 @@ export async function render(container, params, router) {
if (members.length === 0) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">${t('members.title')}</h1>
<span class="badge badge-lg">${t('members.count', { count: 0 })}</span>
<h1 class="text-3xl font-bold">${t('entities.members')}</h1>
<span class="badge badge-lg">${t('common.count_entity', { count: 0, entity: t('entities.members').toLowerCase() })}</span>
</div>
<div class="alert alert-info">
@@ -138,8 +138,8 @@ export async function render(container, params, router) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">${t('members.title')}</h1>
<span class="badge badge-lg">${t('members.count', { count: members.length })}</span>
<h1 class="text-3xl font-bold">${t('entities.members')}</h1>
<span class="badge badge-lg">${t('common.count_entity', { count: members.length, entity: t('entities.members').toLowerCase() })}</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-start">
@@ -22,7 +22,7 @@ export async function render(container, params, router) {
function renderPage(content, { total = null } = {}) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">${t('messages.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.messages')}</h1>
<div class="flex items-center gap-2">
${tzBadge}
${total !== null ? html`<span class="badge badge-lg">${t('common.total', { count: total })}</span>` : nothing}
@@ -24,7 +24,7 @@ export async function render(container, params, router) {
function renderPage(content, { total = null } = {}) {
litRender(html`
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">${t('nodes.title')}</h1>
<h1 class="text-3xl font-bold">${t('entities.nodes')}</h1>
<div class="flex items-center gap-2">
${tzBadge}
${total !== null ? html`<span class="badge badge-lg">${t('common.total', { count: total })}</span>` : nothing}
+47 -64
View File
@@ -1,4 +1,21 @@
{
"entities": {
"home": "Home",
"dashboard": "Dashboard",
"nodes": "Nodes",
"node": "Node",
"node_detail": "Node Detail",
"advertisements": "Advertisements",
"advertisement": "Advertisement",
"messages": "Messages",
"message": "Message",
"map": "Map",
"members": "Members",
"member": "Member",
"admin": "Admin",
"tags": "Tags",
"tag": "Tag"
},
"nav": {
"home": "Home",
"dashboard": "Dashboard",
@@ -9,18 +26,6 @@
"members": "Members",
"admin": "Admin"
},
"page_title": {
"dashboard": "Dashboard - {{network_name}}",
"nodes": "Nodes - {{network_name}}",
"node_detail": "Node Detail - {{network_name}}",
"advertisements": "Advertisements - {{network_name}}",
"messages": "Messages - {{network_name}}",
"map": "Map - {{network_name}}",
"members": "Members - {{network_name}}",
"admin": "Admin - {{network_name}}",
"admin_node_tags": "Node Tags - Admin - {{network_name}}",
"admin_members": "Members - Admin - {{network_name}}"
},
"common": {
"filter": "Filter",
"clear": "Clear",
@@ -30,7 +35,17 @@
"delete": "Delete",
"edit": "Edit",
"move": "Move",
"save": "Save",
"save_changes": "Save Changes",
"add": "Add",
"add_entity": "Add {{entity}}",
"add_new_entity": "Add New {{entity}}",
"edit_entity": "Edit {{entity}}",
"delete_entity": "Delete {{entity}}",
"delete_all_entity": "Delete All {{entity}}",
"move_entity": "Move {{entity}}",
"copy_entity": "Copy {{entity}}",
"view_entity": "View {{entity}}",
"previous": "Previous",
"next": "Next",
"go_home": "Go Home",
@@ -39,12 +54,12 @@
"failed_to_load_page": "Failed to load page",
"total": "{{count}} total",
"shown": "{{count}} shown",
"count_entity": "{{count}} {{entity}}",
"type": "Type",
"name": "Name",
"key": "Key",
"value": "Value",
"time": "Time",
"node": "Node",
"actions": "Actions",
"updated": "Updated",
"sign_in": "Sign In",
@@ -53,7 +68,6 @@
"all_types": "All Types",
"all_nodes": "All Nodes",
"all_members": "All Members",
"member": "Member",
"node_type": "Node Type",
"show": "Show",
"search_placeholder": "Search by name, ID, or public key...",
@@ -70,11 +84,17 @@
"received_by": "Received By",
"receivers": "Receivers",
"from": "From",
"message": "Message",
"close": "close",
"unnamed": "Unnamed",
"unnamed_node": "Unnamed Node"
},
"links": {
"website": "Website",
"github": "GitHub",
"discord": "Discord",
"youtube": "YouTube",
"profile": "Profile"
},
"time": {
"days_ago": "{{count}}d ago",
"hours_ago": "{{count}}h ago",
@@ -95,15 +115,10 @@
"welcome_default": "Welcome to the {{network_name}} mesh network dashboard. Monitor network activity, view connected nodes, and explore message history.",
"total_nodes": "Total Nodes",
"all_discovered_nodes": "All discovered nodes",
"advertisements": "Advertisements",
"messages": "Messages",
"network_info": "Network Info",
"network_activity": "Network Activity",
"meshcore_attribution": "Our local off-grid mesh network is made possible by",
"meshcore_tagline": "Connecting people and things, without using the internet",
"website": "Website",
"github": "GitHub",
"profile": "Profile",
"frequency": "Frequency",
"bandwidth": "Bandwidth",
"spreading_factor": "Spreading Factor",
@@ -111,35 +126,28 @@
"tx_power": "TX Power"
},
"dashboard": {
"title": "Dashboard",
"total_nodes": "Total Nodes",
"all_discovered_nodes": "All discovered nodes",
"advertisements": "Advertisements",
"messages": "Messages",
"recent_advertisements": "Recent Advertisements",
"recent_channel_messages": "Recent Channel Messages",
"channel": "Channel {{number}}",
"no_advertisements_yet": "No advertisements recorded yet."
},
"nodes": {
"title": "Nodes",
"no_nodes_found": "No nodes found.",
"node_not_found": "Node not found: {{public_key}}",
"back_to_nodes": "Back to Nodes",
"scan_to_add": "Scan to add as contact",
"recent_advertisements": "Recent Advertisements",
"no_advertisements": "No advertisements recorded.",
"tags": "Tags",
"no_tags": "No tags defined.",
"edit_tags": "Edit Tags",
"add_tags": "Add Tags"
},
"advertisements": {
"title": "Advertisements",
"no_advertisements_found": "No advertisements found."
},
"messages": {
"title": "Messages",
"no_messages_found": "No messages found.",
"type_direct": "Direct",
"type_channel": "Channel",
@@ -147,7 +155,6 @@
"type_public": "Public"
},
"map": {
"title": "Map",
"show_labels": "Show Labels",
"infrastructure_only": "Infrastructure Only",
"legend": "Legend:",
@@ -162,8 +169,6 @@
"select_destination_node": "-- Select destination node --"
},
"members": {
"title": "Members",
"count": "{{count}} members",
"no_members_configured": "No members configured",
"no_members_description": "To display network members, create a members.yaml file in your seed directory.",
"members_file_format": "Members File Format",
@@ -180,78 +185,56 @@
"failed_to_load": "Failed to load page"
},
"admin": {
"title": "Admin",
"access_denied": "Access Denied",
"admin_not_enabled": "The admin interface is not enabled.",
"admin_enable_hint": "Set <code>WEB_ADMIN_ENABLED=true</code> to enable admin features.",
"auth_required": "Authentication Required",
"auth_required_description": "You must sign in to access the admin interface.",
"welcome": "Welcome to the admin panel.",
"members_title": "Members",
"members_description": "Manage network members and operators.",
"node_tags_title": "Node Tags",
"node_tags_description": "Manage custom tags and metadata for network nodes."
},
"admin_members": {
"title": "Members",
"network_members": "Network Members ({{count}})",
"add_member": "Add Member",
"add_new_member": "Add New Member",
"edit_member": "Edit Member",
"delete_member": "Delete Member",
"member_id": "Member ID",
"member_id_hint": "Unique identifier (letters, numbers, underscore)",
"no_members_yet": "No members configured yet.",
"no_members_hint": "Click \"Add Member\" to create the first member.",
"delete_confirm": "Are you sure you want to delete member <strong>{{name}}</strong>?",
"cannot_be_undone": "This action cannot be undone.",
"member_added": "Member added successfully",
"member_updated": "Member updated successfully",
"member_deleted": "Member deleted successfully"
"entity_added": "Member added successfully",
"entity_updated": "Member updated successfully",
"entity_deleted": "Member deleted successfully"
},
"admin_node_tags": {
"title": "Node Tags",
"select_node": "Select Node",
"select_node_placeholder": "-- Select a node --",
"load_tags": "Load Tags",
"tags_count": "Tags ({{count}})",
"add_new_tag": "Add New Tag",
"add_tag": "Add Tag",
"edit_tag": "Edit Tag",
"move_tag": "Move Tag",
"move_tag_title": "Move Tag to Another Node",
"move_tag_warning": "This will move the tag from the current node to the destination node.",
"delete_tag": "Delete Tag",
"move_to_another_node": "Move Tag to Another Node",
"move_warning": "This will move the tag from the current node to the destination node.",
"delete_tag_confirm": "Are you sure you want to delete the tag \"{{key}}\"?",
"cannot_be_undone": "This action cannot be undone.",
"copy_all": "Copy All",
"copy_all_title": "Copy All Tags to Another Node",
"copy_all_description": "Copy all {{count}} tag(s) from <strong>{{name}}</strong> to another node.",
"copy_all_info": "Tags that already exist on the destination node will be skipped. Original tags remain on this node.",
"copy_tags": "Copy Tags",
"delete_all": "Delete All",
"delete_all_title": "Delete All Tags",
"delete_all_confirm": "Are you sure you want to delete all {{count}} tag(s) from <strong>{{name}}</strong>?",
"delete_all_warning": "This action cannot be undone. All tags will be permanently deleted.",
"delete_all_tags": "Delete All Tags",
"view_node": "View Node",
"destination_node": "Destination Node",
"tag_key": "Tag Key",
"no_tags_found": "No tags found for this node.",
"no_tags_hint": "Add a new tag below.",
"select_a_node": "Select a Node",
"select_a_node_description": "Choose a node from the dropdown above to view and manage its tags.",
"tag_added": "Tag added successfully",
"tag_updated": "Tag updated successfully",
"tag_moved": "Tag moved successfully",
"tag_deleted": "Tag deleted successfully",
"all_tags_deleted": "All tags deleted successfully",
"copied_tags": "Copied {{copied}} tag(s), skipped {{skipped}}"
"entity_added": "Tag added successfully",
"entity_updated": "Tag updated successfully",
"entity_moved": "Tag moved successfully",
"entity_deleted": "Tag deleted successfully",
"all_entities_deleted": "All tags deleted successfully",
"copied_entities": "Copied {{copied}} tag(s), skipped {{skipped}}"
},
"footer": {
"powered_by": "Powered by",
"discord": "Discord",
"github": "GitHub",
"youtube": "YouTube"
"powered_by": "Powered by"
}
}
+3 -3
View File
@@ -150,15 +150,15 @@
{% endif %}
{% if network_contact_email and network_contact_discord %} | {% endif %}
{% if network_contact_discord %}
<a href="{{ network_contact_discord }}" target="_blank" rel="noopener noreferrer" class="link link-hover">{{ t('footer.discord') }}</a>
<a href="{{ network_contact_discord }}" target="_blank" rel="noopener noreferrer" class="link link-hover">{{ t('links.discord') }}</a>
{% endif %}
{% if (network_contact_email or network_contact_discord) and network_contact_github %} | {% endif %}
{% if network_contact_github %}
<a href="{{ network_contact_github }}" target="_blank" rel="noopener noreferrer" class="link link-hover">{{ t('footer.github') }}</a>
<a href="{{ network_contact_github }}" target="_blank" rel="noopener noreferrer" class="link link-hover">{{ t('links.github') }}</a>
{% endif %}
{% if (network_contact_email or network_contact_discord or network_contact_github) and network_contact_youtube %} | {% endif %}
{% if network_contact_youtube %}
<a href="{{ network_contact_youtube }}" target="_blank" rel="noopener noreferrer" class="link link-hover">{{ t('footer.youtube') }}</a>
<a href="{{ network_contact_youtube }}" target="_blank" rel="noopener noreferrer" class="link link-hover">{{ t('links.youtube') }}</a>
{% endif %}
</p>
<p class="text-xs opacity-50 mt-2">{% if admin_enabled %}<a href="/a/" class="link link-hover">{{ t('nav.admin') }}</a> | {% endif %}{{ t('footer.powered_by') }} <a href="https://github.com/ipnet-mesh/meshcore-hub" target="_blank" rel="noopener noreferrer" class="link link-hover">MeshCore Hub</a> {{ version }}</p>