shopping: UI strings all English
Picker dialog, hamburger menu entry, phone page and help section
still had German labels from the original request ("Einkaufsliste",
"keine aktive Liste", etc.). The rest of the app's UI is English,
so align everything here: menu entry is "Shopping list", dialog +
phone page copy translated, help references match. Trigger tags
stay as-is — both #einkauf and #shopping keep working so users with
either convention aren't disrupted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d925070a36
commit
b9d610b612
5 files changed
+23
-22
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('APP_NAME', 'hyprcrd');
|
define('APP_NAME', 'hyprcrd');
|
||||||
define('APP_VERSION', '1.9.19');
|
define('APP_VERSION', '1.9.20');
|
||||||
|
|
||||||
// Database path — writable directory, outside webroot
|
// Database path — writable directory, outside webroot
|
||||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyprcrd",
|
"name": "hyprcrd",
|
||||||
"version": "1.9.19",
|
"version": "1.9.20",
|
||||||
"description": "hyprcrd — self-hosted card manager",
|
"description": "hyprcrd — self-hosted card manager",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+3
-3
@@ -1362,7 +1362,7 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
<i class="ph ph-list-dashes text-sm"></i>
|
<i class="ph ph-list-dashes text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
<div id="header-menu" class="hidden absolute right-0 mt-1 min-w-[180px] bg-t-surface border border-t-border shadow-2xl z-50">
|
<div id="header-menu" class="hidden absolute right-0 mt-1 min-w-[180px] bg-t-surface border border-t-border shadow-2xl z-50">
|
||||||
<button id="menu-shopping" class="w-full text-left px-3 py-2 text-xs text-t-muted hover:bg-t-raised hover:text-t-accent transition-colors flex items-center gap-2"><i class="ph ph-shopping-cart text-sm"></i><span class="font-pixel text-[10px]">Einkaufsliste</span></button>
|
<button id="menu-shopping" class="w-full text-left px-3 py-2 text-xs text-t-muted hover:bg-t-raised hover:text-t-accent transition-colors flex items-center gap-2"><i class="ph ph-shopping-cart text-sm"></i><span class="font-pixel text-[10px]">Shopping list</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="calendar-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
|
<button id="calendar-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
|
||||||
@@ -1693,13 +1693,13 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
<section>
|
<section>
|
||||||
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-shopping-cart text-sm"></i>shopping list (phone sync)</h3>
|
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-shopping-cart text-sm"></i>shopping list (phone sync)</h3>
|
||||||
<div class="text-xs leading-relaxed space-y-3 text-t-muted mb-3">
|
<div class="text-xs leading-relaxed space-y-3 text-t-muted mb-3">
|
||||||
<p>Tag a card with <span class="text-t-accent">#einkauf</span> or <span class="text-t-accent">#shopping</span> to mark it as a shopping-list candidate. Open the header's <i class="ph ph-list-dashes text-xs"></i> menu → <span class="text-t-bright font-pixel">Einkaufsliste</span> to pick which one is the currently <span class="text-t-bright">active</span> list — only one at a time is mirrored to the phone.</p>
|
<p>Tag a card with <span class="text-t-accent">#einkauf</span> or <span class="text-t-accent">#shopping</span> to mark it as a shopping-list candidate. Open the header's <i class="ph ph-list-dashes text-xs"></i> menu → <span class="text-t-bright font-pixel">Shopping list</span> to pick which one is the currently <span class="text-t-bright">active</span> list — only one at a time is mirrored to the phone.</p>
|
||||||
<p>The active card's body is pushed to the relay on every sync tick. On your phone, open <span class="text-t-accent"><your-relay>/shopping</span> and log in with your relay credentials. The page shows only the <span class="text-t-bright">unchecked</span> <code class="text-t-accent">- [ ]</code> items as tappable rows. Tapping an item flips it to <code class="text-t-accent">- [x]</code> on the relay in-place; the next client tick pulls that back and updates the local card.</p>
|
<p>The active card's body is pushed to the relay on every sync tick. On your phone, open <span class="text-t-accent"><your-relay>/shopping</span> and log in with your relay credentials. The page shows only the <span class="text-t-bright">unchecked</span> <code class="text-t-accent">- [ ]</code> items as tappable rows. Tapping an item flips it to <code class="text-t-accent">- [x]</code> on the relay in-place; the next client tick pulls that back and updates the local card.</p>
|
||||||
<p>Editing stays in hyprcrd — the phone can only check/uncheck, never change text. That keeps conflict handling trivial (last-writer wins by timestamp).</p>
|
<p>Editing stays in hyprcrd — the phone can only check/uncheck, never change text. That keeps conflict handling trivial (last-writer wins by timestamp).</p>
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-xs">
|
<table class="w-full text-xs">
|
||||||
<tr><td class="py-1 text-t-dim w-40 font-pixel"><i class="ph ph-hash text-xs"></i> trigger tags</td><td><span class="text-t-accent">#einkauf</span> or <span class="text-t-accent">#shopping</span> — either counts</td></tr>
|
<tr><td class="py-1 text-t-dim w-40 font-pixel"><i class="ph ph-hash text-xs"></i> trigger tags</td><td><span class="text-t-accent">#einkauf</span> or <span class="text-t-accent">#shopping</span> — either counts</td></tr>
|
||||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-list-dashes text-xs"></i> pick active</td><td>header menu → Einkaufsliste → radio select → Save</td></tr>
|
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-list-dashes text-xs"></i> pick active</td><td>header menu → Shopping list → radio select → Save</td></tr>
|
||||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-device-mobile text-xs"></i> phone URL</td><td><code>https://<relay>/shopping</code> — session-auth, mobile layout</td></tr>
|
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-device-mobile text-xs"></i> phone URL</td><td><code>https://<relay>/shopping</code> — session-auth, mobile layout</td></tr>
|
||||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-arrows-clockwise text-xs"></i> sync cadence</td><td>piggybacks on the shared sync tick (see Settings → Relay)</td></tr>
|
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-arrows-clockwise text-xs"></i> sync cadence</td><td>piggybacks on the shared sync tick (see Settings → Relay)</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ function openDialog() {
|
|||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<div class="flex items-center gap-3 px-5 py-3 border-b border-t-border">
|
<div class="flex items-center gap-3 px-5 py-3 border-b border-t-border">
|
||||||
<i class="ph ph-shopping-cart text-t-accent text-base"></i>
|
<i class="ph ph-shopping-cart text-t-accent text-base"></i>
|
||||||
<h2 class="font-pixel text-sm text-t-accent flex-1">Einkaufsliste</h2>
|
<h2 class="font-pixel text-sm text-t-accent flex-1">Shopping list</h2>
|
||||||
<button class="shopping-picker-close text-t-muted hover:text-t-danger"><i class="ph ph-x text-sm"></i></button>
|
<button class="shopping-picker-close text-t-muted hover:text-t-danger"><i class="ph ph-x text-sm"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5 py-3 text-[11px] text-t-muted leading-relaxed">
|
<div class="px-5 py-3 text-[11px] text-t-muted leading-relaxed">
|
||||||
Karten mit Tag <span class="text-t-accent">#einkauf</span> oder
|
Cards tagged with <span class="text-t-accent">#einkauf</span> or
|
||||||
<span class="text-t-accent">#shopping</span> können als aktive Liste fürs Telefon
|
<span class="text-t-accent">#shopping</span> can be set as the active list for the
|
||||||
gewählt werden. Nur eine ist aktiv; ihr Body wird bei jedem Sync zum Relay geschickt
|
phone. Only one is active at a time; its body is pushed to the relay on every sync
|
||||||
und unter <span class="text-t-accent">/shopping</span> angezeigt.
|
and shown at <span class="text-t-accent">/shopping</span>.
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-y-auto px-2 py-2" id="shopping-picker-list"></div>
|
<div class="flex-1 overflow-y-auto px-2 py-2" id="shopping-picker-list"></div>
|
||||||
<div class="flex items-center gap-2 px-5 py-3 border-t border-t-border">
|
<div class="flex items-center gap-2 px-5 py-3 border-t border-t-border">
|
||||||
@@ -91,8 +91,9 @@ function openDialog() {
|
|||||||
|
|
||||||
if (!candidates.length) {
|
if (!candidates.length) {
|
||||||
list.innerHTML = `<div class="px-4 py-6 text-center text-[11px] text-t-dim">
|
list.innerHTML = `<div class="px-4 py-6 text-center text-[11px] text-t-dim">
|
||||||
Keine Karten mit dem Tag <span class="text-t-accent">#einkauf</span>.
|
No cards tagged <span class="text-t-accent">#einkauf</span> or
|
||||||
Lege eine Karte mit Checkbox-Items an und gib ihr den Tag, dann erscheint sie hier.
|
<span class="text-t-accent">#shopping</span> yet. Create a card with checkbox items
|
||||||
|
and add one of those tags — it'll show up here.
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
list.innerHTML = candidates.map(c => {
|
list.innerHTML = candidates.map(c => {
|
||||||
@@ -104,14 +105,14 @@ function openDialog() {
|
|||||||
<input type="radio" name="shopping-active" value="${id}" ${checked} style="margin-top:3px;">
|
<input type="radio" name="shopping-active" value="${id}" ${checked} style="margin-top:3px;">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-xs text-t-bright font-pixel">${esc(c.title || '(untitled)')}</div>
|
<div class="text-xs text-t-bright font-pixel">${esc(c.title || '(untitled)')}</div>
|
||||||
<div class="text-[10px] text-t-dim font-pixel mt-0.5">${open} offen · ${items.length} gesamt</div>
|
<div class="text-[10px] text-t-dim font-pixel mt-0.5">${open} open · ${items.length} total</div>
|
||||||
</div>
|
</div>
|
||||||
</label>`;
|
</label>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
// "No active" option — explicit way to stop syncing.
|
// "No active" option — explicit way to stop syncing.
|
||||||
list.insertAdjacentHTML('beforeend', `<label class="flex items-center gap-3 px-3 py-2 border-t border-t-border hover:bg-t-raised cursor-pointer mt-2">
|
list.insertAdjacentHTML('beforeend', `<label class="flex items-center gap-3 px-3 py-2 border-t border-t-border hover:bg-t-raised cursor-pointer mt-2">
|
||||||
<input type="radio" name="shopping-active" value="" ${currentActive ? '' : 'checked'} style="margin-top:0;">
|
<input type="radio" name="shopping-active" value="" ${currentActive ? '' : 'checked'} style="margin-top:0;">
|
||||||
<span class="text-[11px] text-t-muted italic">(keine aktive Liste)</span>
|
<span class="text-[11px] text-t-muted italic">(no active list)</span>
|
||||||
</label>`);
|
</label>`);
|
||||||
list.querySelectorAll('input[name="shopping-active"]').forEach(r => {
|
list.querySelectorAll('input[name="shopping-active"]').forEach(r => {
|
||||||
r.addEventListener('change', () => { selectedId = r.value || null; });
|
r.addEventListener('change', () => { selectedId = r.value || null; });
|
||||||
|
|||||||
@@ -133,12 +133,12 @@ if ($route === '/shopping' && $method === 'GET') {
|
|||||||
$userLabel = htmlspecialchars($sessionTenant['username'], ENT_QUOTES);
|
$userLabel = htmlspecialchars($sessionTenant['username'], ENT_QUOTES);
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||||
<meta name="theme-color" content="#0f172a">
|
<meta name="theme-color" content="#0f172a">
|
||||||
<title>Einkaufsliste</title>
|
<title>Shopping list</title>
|
||||||
<style>
|
<style>
|
||||||
:root { color-scheme: dark; }
|
:root { color-scheme: dark; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
@@ -160,7 +160,7 @@ if ($route === '/shopping' && $method === 'GET') {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>Einkaufsliste</h1>
|
<h1>Shopping list</h1>
|
||||||
<span class="user"><?= $userLabel ?></span>
|
<span class="user"><?= $userLabel ?></span>
|
||||||
<button class="refresh" id="refresh">↻</button>
|
<button class="refresh" id="refresh">↻</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -205,7 +205,7 @@ if ($route === '/shopping' && $method === 'GET') {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.ok || !data.state) {
|
if (!data.ok || !data.state) {
|
||||||
elTitle.textContent = '';
|
elTitle.textContent = '';
|
||||||
elItems.innerHTML = '<div class="empty">keine aktive Einkaufsliste</div>';
|
elItems.innerHTML = '<div class="empty">no active shopping list</div>';
|
||||||
elStale.textContent = '';
|
elStale.textContent = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ if ($route === '/shopping' && $method === 'GET') {
|
|||||||
elTitle.textContent = title || '';
|
elTitle.textContent = title || '';
|
||||||
const items = parseItems(body);
|
const items = parseItems(body);
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
elItems.innerHTML = '<div class="empty">✓ alles erledigt</div>';
|
elItems.innerHTML = '<div class="empty">✓ all done</div>';
|
||||||
} else {
|
} else {
|
||||||
elItems.innerHTML = '';
|
elItems.innerHTML = '';
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
@@ -226,10 +226,10 @@ if ($route === '/shopping' && $method === 'GET') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ago = Math.round((Date.now() - Date.parse(updated_at_server.replace(' ','T') + 'Z')) / 1000);
|
const ago = Math.round((Date.now() - Date.parse(updated_at_server.replace(' ','T') + 'Z')) / 1000);
|
||||||
elStale.textContent = 'zuletzt synchronisiert: vor ' + (ago < 60 ? ago + 's' : Math.round(ago/60) + 'min');
|
elStale.textContent = 'last synced: ' + (ago < 60 ? ago + 's ago' : Math.round(ago/60) + ' min ago');
|
||||||
showErr('');
|
showErr('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErr('laden fehlgeschlagen: ' + e.message);
|
showErr('load failed: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ if ($route === '/shopping' && $method === 'GET') {
|
|||||||
body: JSON.stringify({ line: li.dataset.line, checked: true }),
|
body: JSON.stringify({ line: li.dataset.line, checked: true }),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.ok) throw new Error(data.error || 'fehler');
|
if (!data.ok) throw new Error(data.error || 'check failed');
|
||||||
setTimeout(() => { busy = false; load(); }, 300);
|
setTimeout(() => { busy = false; load(); }, 300);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
busy = false;
|
busy = false;
|
||||||
|
|||||||
Reference in new issue
Block a user