Further refine i18n structure

- Remove "nav" section, use "entities" references instead
- Remove composite strings like "Total Nodes", "Recent Advertisements"
  - Use composed patterns: t('common.total_entity', { entity: t('entities.nodes') })
  - Use common.recent_entity, common.edit_entity, common.add_entity patterns
- Hardcode MeshCore tagline (official trademark, not configurable)
- Update all page components and templates to use entity-based translations
- Update tests to reflect new structure
- Remove redundant page-specific composite keys

This maximizes reusability and reduces duplication across translations.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Louis King
2026-02-13 21:32:36 +00:00
parent 85129e528e
commit a15e91c754
13 changed files with 70 additions and 100 deletions
-14
View File
@@ -3,21 +3,9 @@ name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -39,5 +27,3 @@ jobs:
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
+5 -5
View File
@@ -154,7 +154,7 @@ function createActivityChart(canvasId, advertData, messageData) {
if (advertData && advertData.data && advertData.data.length > 0) {
if (!labels) labels = formatDateLabels(advertData.data);
datasets.push({
label: (window.t && window.t('nav.advertisements')) || 'Advertisements',
label: (window.t && window.t('entities.advertisements')) || 'Advertisements',
data: advertData.data.map(function(d) { return d.count; }),
borderColor: ChartColors.adverts,
backgroundColor: ChartColors.advertsFill,
@@ -168,7 +168,7 @@ function createActivityChart(canvasId, advertData, messageData) {
if (messageData && messageData.data && messageData.data.length > 0) {
if (!labels) labels = formatDateLabels(messageData.data);
datasets.push({
label: (window.t && window.t('nav.messages')) || 'Messages',
label: (window.t && window.t('entities.messages')) || 'Messages',
data: messageData.data.map(function(d) { return d.count; }),
borderColor: ChartColors.messages,
backgroundColor: ChartColors.messagesFill,
@@ -200,7 +200,7 @@ function initDashboardCharts(nodeData, advertData, messageData) {
createLineChart(
'nodeChart',
nodeData,
(window.t && window.t('dashboard.total_nodes')) || 'Total Nodes',
(window.t && window.t('common.total_entity', { entity: t('entities.nodes') })) || 'Total Nodes',
ChartColors.nodes,
ChartColors.nodesFill,
true
@@ -211,7 +211,7 @@ function initDashboardCharts(nodeData, advertData, messageData) {
createLineChart(
'advertChart',
advertData,
(window.t && window.t('nav.advertisements')) || 'Advertisements',
(window.t && window.t('entities.advertisements')) || 'Advertisements',
ChartColors.adverts,
ChartColors.advertsFill,
true
@@ -222,7 +222,7 @@ function initDashboardCharts(nodeData, advertData, messageData) {
createLineChart(
'messageChart',
messageData,
(window.t && window.t('nav.messages')) || 'Messages',
(window.t && window.t('entities.messages')) || 'Messages',
ChartColors.messages,
ChartColors.messagesFill,
true
+1 -1
View File
@@ -7,7 +7,7 @@
* Usage:
* import { t, loadLocale } from './i18n.js';
* await loadLocale('en');
* t('nav.home'); // "Home"
* t('entities.home'); // "Home"
* t('common.total', { count: 42 }); // "42 total"
*/
@@ -34,8 +34,8 @@ export async function render(container, params, router) {
<h1 class="text-3xl font-bold">${t('entities.admin')}</h1>
<div class="text-sm breadcrumbs">
<ul>
<li><a href="/">${t('nav.home')}</a></li>
<li>${t('nav.admin')}</li>
<li><a href="/">${t('entities.home')}</a></li>
<li>${t('entities.admin')}</li>
</ul>
</div>
</div>
@@ -88,8 +88,8 @@ export async function render(container, params, router) {
<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><a href="/">${t('entities.home')}</a></li>
<li><a href="/a/">${t('entities.admin')}</a></li>
<li>${t('entities.members')}</li>
</ul>
</div>
@@ -305,8 +305,8 @@ export async function render(container, params, router) {
<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><a href="/">${t('entities.home')}</a></li>
<li><a href="/a/">${t('entities.admin')}</a></li>
<li>${t('entities.tags')}</li>
</ul>
</div>
@@ -152,7 +152,7 @@ ${topCount > 0 ? html`
<div class="stat-figure" style="color: ${pageColors.nodes}">
${iconNodes('h-8 w-8')}
</div>
<div class="stat-title">${t('dashboard.total_nodes')}</div>
<div class="stat-title">${t('common.total_entity', { entity: t('entities.nodes') })}</div>
<div class="stat-value" style="color: ${pageColors.nodes}">${stats.total_nodes}</div>
<div class="stat-desc">${t('dashboard.all_discovered_nodes')}</div>
</div>` : nothing}
@@ -184,7 +184,7 @@ ${topCount > 0 ? html`
<div class="card-body">
<h2 class="card-title text-base">
${iconNodes('h-5 w-5')}
${t('dashboard.total_nodes')}
${t('common.total_entity', { entity: t('entities.nodes') })}
</h2>
<p class="text-xs opacity-70">${t('time.over_time_last_7_days')}</p>
<div class="h-32">
@@ -229,7 +229,7 @@ ${bottomCount > 0 ? html`
<div class="card-body">
<h2 class="card-title">
${iconAdvertisements('h-6 w-6')}
${t('dashboard.recent_advertisements')}
${t('common.recent_entity', { entity: t('entities.advertisements') })}
</h2>
${renderRecentAds(stats.recent_advertisements)}
</div>
@@ -81,27 +81,27 @@ export async function render(container, params, router) {
${features.dashboard !== false ? html`
<a href="/dashboard" class="btn btn-outline btn-info">
${iconDashboard('h-5 w-5 mr-2')}
${t('nav.dashboard')}
${t('entities.dashboard')}
</a>` : nothing}
${features.nodes !== false ? html`
<a href="/nodes" class="btn btn-outline btn-primary">
${iconNodes('h-5 w-5 mr-2')}
${t('nav.nodes')}
${t('entities.nodes')}
</a>` : nothing}
${features.advertisements !== false ? html`
<a href="/advertisements" class="btn btn-outline btn-secondary">
${iconAdvertisements('h-5 w-5 mr-2')}
${t('nav.advertisements')}
${t('entities.advertisements')}
</a>` : nothing}
${features.messages !== false ? html`
<a href="/messages" class="btn btn-outline btn-accent">
${iconMessages('h-5 w-5 mr-2')}
${t('nav.messages')}
${t('entities.messages')}
</a>` : nothing}
${features.map !== false ? html`
<a href="/map" class="btn btn-outline btn-warning">
${iconMap('h-5 w-5 mr-2')}
${t('nav.map')}
${t('entities.map')}
</a>` : nothing}
${customPageButtons}
</div>
@@ -114,7 +114,7 @@ export async function render(container, params, router) {
<div class="stat-figure" style="color: ${pageColors.nodes}">
${iconNodes('h-8 w-8')}
</div>
<div class="stat-title">${t('home.total_nodes')}</div>
<div class="stat-title">${t('common.total_entity', { entity: t('entities.nodes') })}</div>
<div class="stat-value" style="color: ${pageColors.nodes}">${stats.total_nodes}</div>
<div class="stat-desc">${t('home.all_discovered_nodes')}</div>
</div>` : nothing}
@@ -160,7 +160,7 @@ export async function render(container, params, router) {
<a href="https://meshcore.co.uk/" target="_blank" rel="noopener noreferrer" class="hover:opacity-80 transition-opacity">
<img src="/static/img/meshcore.svg" alt="MeshCore" class="theme-logo h-8" />
</a>
<p class="text-xs opacity-50 mt-4 text-center">${t('home.meshcore_tagline')}</p>
<p class="text-xs opacity-50 mt-4 text-center">Connecting people and things, without using the internet</p>
<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')}
@@ -127,15 +127,15 @@ export async function render(container, params, router) {
const adminTagsHtml = (config.admin_enabled && config.is_authenticated)
? html`<div class="mt-3">
<a href="/a/node-tags?public_key=${node.public_key}" class="btn btn-sm btn-outline">${tags.length > 0 ? t('nodes.edit_tags') : t('nodes.add_tags')}</a>
<a href="/a/node-tags?public_key=${node.public_key}" class="btn btn-sm btn-outline">${tags.length > 0 ? t('common.edit_entity', { entity: t('entities.tags') }) : t('common.add_entity', { entity: t('entities.tags') })}</a>
</div>`
: nothing;
litRender(html`
<div class="breadcrumbs text-sm mb-4">
<ul>
<li><a href="/">${t('nav.home')}</a></li>
<li><a href="/nodes">${t('nav.nodes')}</a></li>
<li><a href="/">${t('entities.home')}</a></li>
<li><a href="/nodes">${t('entities.nodes')}</a></li>
<li>${tagName || node.name || node.public_key.slice(0, 12) + '...'}</li>
</ul>
</div>
@@ -164,7 +164,7 @@ ${heroHtml}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">${t('nodes.recent_advertisements')}</h2>
<h2 class="card-title">${t('common.recent_entity', { entity: t('entities.advertisements') })}</h2>
${adsTableHtml}
</div>
</div>
@@ -237,8 +237,8 @@ function renderNotFound(publicKey) {
return html`
<div class="breadcrumbs text-sm mb-4">
<ul>
<li><a href="/">${t('nav.home')}</a></li>
<li><a href="/nodes">${t('nav.nodes')}</a></li>
<li><a href="/">${t('entities.home')}</a></li>
<li><a href="/nodes">${t('entities.nodes')}</a></li>
<li>${t('not_found.title')}</li>
</ul>
</div>
@@ -246,5 +246,5 @@ function renderNotFound(publicKey) {
${iconError('stroke-current shrink-0 h-6 w-6')}
<span>${t('nodes.node_not_found', { public_key: publicKey })}</span>
</div>
<a href="/nodes" class="btn btn-primary mt-4">${t('nodes.back_to_nodes')}</a>`;
<a href="/nodes" class="btn btn-primary mt-4">${t('common.view_entity', { entity: t('entities.nodes') })}</a>`;
}
@@ -18,7 +18,7 @@ export async function render(container, params, router) {
</a>
<a href="/nodes" class="btn btn-outline">
${iconNodes('h-5 w-5 mr-2')}
${t('not_found.browse_nodes')}
${t('common.view_entity', { entity: t('entities.nodes') })}
</a>
</div>
</div>
+5 -23
View File
@@ -16,16 +16,6 @@
"tags": "Tags",
"tag": "Tag"
},
"nav": {
"home": "Home",
"dashboard": "Dashboard",
"nodes": "Nodes",
"advertisements": "Advertisements",
"messages": "Messages",
"map": "Map",
"members": "Members",
"admin": "Admin"
},
"common": {
"filter": "Filter",
"clear": "Clear",
@@ -46,6 +36,9 @@
"move_entity": "Move {{entity}}",
"copy_entity": "Copy {{entity}}",
"view_entity": "View {{entity}}",
"recent_entity": "Recent {{entity}}",
"total_entity": "Total {{entity}}",
"all_entity": "All {{entity}}",
"previous": "Previous",
"next": "Next",
"go_home": "Go Home",
@@ -66,8 +59,6 @@
"sign_out": "Sign Out",
"view_details": "View Details",
"all_types": "All Types",
"all_nodes": "All Nodes",
"all_members": "All Members",
"node_type": "Node Type",
"show": "Show",
"search_placeholder": "Search by name, ID, or public key...",
@@ -113,12 +104,10 @@
},
"home": {
"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",
"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",
"frequency": "Frequency",
"bandwidth": "Bandwidth",
"spreading_factor": "Spreading Factor",
@@ -126,9 +115,7 @@
"tx_power": "TX Power"
},
"dashboard": {
"total_nodes": "Total Nodes",
"all_discovered_nodes": "All discovered nodes",
"recent_advertisements": "Recent Advertisements",
"recent_channel_messages": "Recent Channel Messages",
"channel": "Channel {{number}}",
"no_advertisements_yet": "No advertisements recorded yet."
@@ -136,13 +123,9 @@
"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.",
"no_tags": "No tags defined.",
"edit_tags": "Edit Tags",
"add_tags": "Add Tags"
"no_tags": "No tags defined."
},
"advertisements": {
"no_advertisements_found": "No advertisements found."
@@ -177,8 +160,7 @@
},
"not_found": {
"title": "Page Not Found",
"description": "The page you're looking for doesn't exist or has been moved.",
"browse_nodes": "Browse Nodes"
"description": "The page you're looking for doesn't exist or has been moved."
},
"custom_page": {
"page_not_found": "Page not found",
+15 -15
View File
@@ -60,24 +60,24 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /></svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="/" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg> {{ t('nav.home') }}</a></li>
<li><a href="/" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg> {{ t('entities.home') }}</a></li>
{% if features.dashboard %}
<li><a href="/dashboard" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-dashboard" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg> {{ t('nav.dashboard') }}</a></li>
<li><a href="/dashboard" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-dashboard" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg> {{ t('entities.dashboard') }}</a></li>
{% endif %}
{% if features.nodes %}
<li><a href="/nodes" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-nodes" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg> {{ t('nav.nodes') }}</a></li>
<li><a href="/nodes" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-nodes" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg> {{ t('entities.nodes') }}</a></li>
{% endif %}
{% if features.advertisements %}
<li><a href="/advertisements" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-adverts" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" /></svg> {{ t('nav.advertisements') }}</a></li>
<li><a href="/advertisements" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-adverts" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" /></svg> {{ t('entities.advertisements') }}</a></li>
{% endif %}
{% if features.messages %}
<li><a href="/messages" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-messages" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg> {{ t('nav.messages') }}</a></li>
<li><a href="/messages" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-messages" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg> {{ t('entities.messages') }}</a></li>
{% endif %}
{% if features.map %}
<li><a href="/map" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-map" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /></svg> {{ t('nav.map') }}</a></li>
<li><a href="/map" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-map" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /></svg> {{ t('entities.map') }}</a></li>
{% endif %}
{% if features.members %}
<li><a href="/members" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-members" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg> {{ t('nav.members') }}</a></li>
<li><a href="/members" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-members" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg> {{ t('entities.members') }}</a></li>
{% endif %}
{% if features.pages %}
{% for page in custom_pages %}
@@ -93,24 +93,24 @@
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><a href="/" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg> {{ t('nav.home') }}</a></li>
<li><a href="/" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg> {{ t('entities.home') }}</a></li>
{% if features.dashboard %}
<li><a href="/dashboard" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-dashboard" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg> {{ t('nav.dashboard') }}</a></li>
<li><a href="/dashboard" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-dashboard" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg> {{ t('entities.dashboard') }}</a></li>
{% endif %}
{% if features.nodes %}
<li><a href="/nodes" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-nodes" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg> {{ t('nav.nodes') }}</a></li>
<li><a href="/nodes" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-nodes" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg> {{ t('entities.nodes') }}</a></li>
{% endif %}
{% if features.advertisements %}
<li><a href="/advertisements" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-adverts" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" /></svg> {{ t('nav.advertisements') }}</a></li>
<li><a href="/advertisements" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-adverts" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" /></svg> {{ t('entities.advertisements') }}</a></li>
{% endif %}
{% if features.messages %}
<li><a href="/messages" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-messages" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg> {{ t('nav.messages') }}</a></li>
<li><a href="/messages" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-messages" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg> {{ t('entities.messages') }}</a></li>
{% endif %}
{% if features.map %}
<li><a href="/map" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-map" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /></svg> {{ t('nav.map') }}</a></li>
<li><a href="/map" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-map" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /></svg> {{ t('entities.map') }}</a></li>
{% endif %}
{% if features.members %}
<li><a href="/members" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-members" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg> {{ t('nav.members') }}</a></li>
<li><a href="/members" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-members" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg> {{ t('entities.members') }}</a></li>
{% endif %}
{% if features.pages %}
{% for page in custom_pages %}
@@ -161,7 +161,7 @@
<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>
<p class="text-xs opacity-50 mt-2">{% if admin_enabled %}<a href="/a/" class="link link-hover">{{ t('entities.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>
</aside>
</footer>
+20 -18
View File
@@ -26,10 +26,10 @@ class TestLoadLocale:
def test_fallback_to_english(self, tmp_path: Path):
"""Unknown locale falls back to 'en' if the directory has en.json."""
# Copy en.json into a temp directory
en_data = {"nav": {"home": "Home"}}
en_data = {"entities": {"home": "Home"}}
(tmp_path / "en.json").write_text(json.dumps(en_data))
load_locale("xx", locales_dir=tmp_path)
assert t("nav.home") == "Home"
assert t("entities.home") == "Home"
def test_missing_locale_dir(self, tmp_path: Path):
"""Missing locale file doesn't crash."""
@@ -43,12 +43,12 @@ class TestTranslation:
def test_simple_key(self):
"""Simple dot-separated key resolves correctly."""
assert t("nav.home") == "Home"
assert t("nav.nodes") == "Nodes"
assert t("entities.home") == "Home"
assert t("entities.nodes") == "Nodes"
def test_nested_key(self):
"""Deeply nested keys resolve correctly."""
assert t("nav.advertisements") == "Advertisements"
assert t("entities.advertisements") == "Advertisements"
def test_missing_key_returns_key(self):
"""Missing key returns the key itself as fallback."""
@@ -61,7 +61,7 @@ class TestTranslation:
def test_interpolation_multiple(self):
"""Multiple placeholders are all replaced."""
result = t(
"admin_node_tags.copied_tags",
"admin_node_tags.copied_entities",
copied=5,
skipped=2,
)
@@ -96,10 +96,11 @@ class TestEnJsonCompleteness:
en_path = LOCALES_DIR / "en.json"
data = json.loads(en_path.read_text(encoding="utf-8"))
required = [
"nav",
"page_title",
"entities",
"common",
"links",
"time",
"node_types",
"home",
"dashboard",
"nodes",
@@ -108,6 +109,7 @@ class TestEnJsonCompleteness:
"map",
"members",
"not_found",
"custom_page",
"admin",
"admin_members",
"admin_node_tags",
@@ -116,13 +118,13 @@ class TestEnJsonCompleteness:
for section in required:
assert section in data, f"Missing section: {section}"
def test_nav_keys(self):
"""Navigation keys are all present."""
assert t("nav.home") != "nav.home"
assert t("nav.dashboard") != "nav.dashboard"
assert t("nav.nodes") != "nav.nodes"
assert t("nav.advertisements") != "nav.advertisements"
assert t("nav.messages") != "nav.messages"
assert t("nav.map") != "nav.map"
assert t("nav.members") != "nav.members"
assert t("nav.admin") != "nav.admin"
def test_entity_keys(self):
"""Entity keys are all present."""
assert t("entities.home") != "entities.home"
assert t("entities.dashboard") != "entities.dashboard"
assert t("entities.nodes") != "entities.nodes"
assert t("entities.advertisements") != "entities.advertisements"
assert t("entities.messages") != "entities.messages"
assert t("entities.map") != "entities.map"
assert t("entities.members") != "entities.members"
assert t("entities.admin") != "entities.admin"