Web interface: dates and times in the user's Nextcloud locale

This commit is contained in:
2026-10-15 21:43:09 +02:00
parent 144097d159
commit 942e8ec003
5 changed files with 25 additions and 9 deletions
+2
View File
@@ -14,6 +14,8 @@ instead carried by the app description, the git tag, the release, and this file.
which location, setup and size you used, and how it went. which location, setup and size you used, and how it went.
### Changed ### Changed
- Dates and times in the web interface follow the locale in the user's Nextcloud settings instead
of the browser's language, so a 24-hour locale shows 16:00 rather than 4:00 PM.
- The list of backup runs (web interface and `occ backup:status`) shows the runs of the last 7 - The list of backup runs (web interface and `occ backup:status`) shows the runs of the last 7
days, those still running, and at least the 5 newest; a failed run stays visible for that week. days, those still running, and at least the 5 newest; a failed run stays visible for that week.
`occ backup:status --all` and "Show older runs" in the web interface list all of them. `occ backup:status --all` and "Show older runs" in the web interface list all of them.
+5 -5
View File
@@ -22,8 +22,8 @@
<p class="ngb-muted">{{ t('ng_backup', 'Removed restore points can be brought back until they are deleted permanently.') }}</p> <p class="ngb-muted">{{ t('ng_backup', 'Removed restore points can be brought back until they are deleted permanently.') }}</p>
<ul> <ul>
<li v-for="item in trash[target.id]" :key="item.id"> <li v-for="item in trash[target.id]" :key="item.id">
{{ item.time ? new Date(item.time).toLocaleString() : item.id }}{{ item.label ? ' – ' + item.label : '' }} {{ item.time ? formatDateTime(item.time) : item.id }}{{ item.label ? ' – ' + item.label : '' }}
<span class="ngb-muted">{{ t('ng_backup', 'removed by {by}, deleted after {date}', { by: item.by, date: new Date(item.deleteAfter * 1000).toLocaleDateString() }) }}</span> <span class="ngb-muted">{{ t('ng_backup', 'removed by {by}, deleted after {date}', { by: item.by, date: formatDate(item.deleteAfter) }) }}</span>
<NcButton variant="tertiary" @click="untrash(target, item)">{{ t('ng_backup', 'Restore') }}</NcButton> <NcButton variant="tertiary" @click="untrash(target, item)">{{ t('ng_backup', 'Restore') }}</NcButton>
</li> </li>
</ul> </ul>
@@ -46,10 +46,10 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="run in status.runs" :key="run.id" :title="run.error || ''"> <tr v-for="run in status.runs" :key="run.id" :title="run.error || ''">
<td>{{ new Date(run.startedAt * 1000).toLocaleString() }}</td> <td>{{ formatDateTime(run.startedAt) }}</td>
<td>{{ targetName(run.targetId) }}</td> <td>{{ targetName(run.targetId) }}</td>
<td>{{ run.kind }}</td> <td>{{ run.kind }}</td>
<td>{{ run.snapshot ? new Date(run.snapshot.createdAt * 1000).toLocaleString() + (run.snapshot.label ? ' – ' + run.snapshot.label : '') : '' }}</td> <td>{{ run.snapshot ? formatDateTime(run.snapshot.createdAt) + (run.snapshot.label ? ' – ' + run.snapshot.label : '') : '' }}</td>
<td :class="'ngb-' + run.status">{{ run.status }}{{ run.status === 'running' ? ' (' + run.phase + percent(run) + ')' : '' }}{{ run.error ? ': ' + run.error : '' }}</td> <td :class="'ngb-' + run.status">{{ run.status }}{{ run.status === 'running' ? ' (' + run.phase + percent(run) + ')' : '' }}{{ run.error ? ': ' + run.error : '' }}</td>
<td>{{ run.stats?.files?.files ?? run.stats?.restored ?? '' }}</td> <td>{{ run.stats?.files?.files ?? run.stats?.restored ?? '' }}</td>
<td>{{ run.stats?.files ? formatSize(run.stats.files.bytes) : '' }}</td> <td>{{ run.stats?.files ? formatSize(run.stats.files.bytes) : '' }}</td>
@@ -79,7 +79,7 @@ import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import NcTextField from '@nextcloud/vue/components/NcTextField' import NcTextField from '@nextcloud/vue/components/NcTextField'
import api from '../api.js' import api from '../api.js'
import { formatSize } from '../format.js' import { formatDateTime, formatDate, formatSize } from '../format.js'
import { confirmPassword } from '@nextcloud/password-confirmation' import { confirmPassword } from '@nextcloud/password-confirmation'
import { reactive } from 'vue' import { reactive } from 'vue'
+3 -2
View File
@@ -35,7 +35,7 @@
<tr v-for="slot in status.key.slots" :key="slot.slot"> <tr v-for="slot in status.key.slots" :key="slot.slot">
<td>{{ t('ng_backup', 'Slot {n}', { n: slot.slot }) }}</td> <td>{{ t('ng_backup', 'Slot {n}', { n: slot.slot }) }}</td>
<td><strong>{{ slot.label }}</strong></td> <td><strong>{{ slot.label }}</strong></td>
<td class="ngb-muted">{{ slot.created ? new Date(slot.created * 1000).toLocaleDateString() : '' }}</td> <td class="ngb-muted">{{ slot.created ? formatDate(slot.created) : '' }}</td>
<td class="ngb-actions"> <td class="ngb-actions">
<NcButton variant="tertiary" @click="openSlotForm(slot)">{{ t('ng_backup', 'New passphrase') }}</NcButton> <NcButton variant="tertiary" @click="openSlotForm(slot)">{{ t('ng_backup', 'New passphrase') }}</NcButton>
<NcButton v-if="status.key.slots.length > 1" variant="tertiary" @click="removeSlot(slot)">{{ t('ng_backup', 'Remove') }}</NcButton> <NcButton v-if="status.key.slots.length > 1" variant="tertiary" @click="removeSlot(slot)">{{ t('ng_backup', 'Remove') }}</NcButton>
@@ -69,7 +69,7 @@
<NcButton variant="primary" :disabled="!downloaded || !accepted || code.length < 8 || busy" @click="confirm">{{ status.confirmationPhrase }}</NcButton> <NcButton variant="primary" :disabled="!downloaded || !accepted || code.length < 8 || busy" @click="confirm">{{ status.confirmationPhrase }}</NcButton>
</div> </div>
<p v-else> <p v-else>
{{ t('ng_backup', 'Recovery kit confirmed by {user} on {date}.', { user: status.key.confirmation.uid, date: new Date(status.key.confirmation.time * 1000).toLocaleString() }) }} {{ t('ng_backup', 'Recovery kit confirmed by {user} on {date}.', { user: status.key.confirmation.uid, date: formatDateTime(status.key.confirmation.time) }) }}
<NcButton variant="tertiary" @click="downloadKit">{{ t('ng_backup', 'Download the kit again') }}</NcButton> <NcButton variant="tertiary" @click="downloadKit">{{ t('ng_backup', 'Download the kit again') }}</NcButton>
<span class="ngb-muted">{{ t('ng_backup', 'Requires your password; all administrators are notified.') }}</span> <span class="ngb-muted">{{ t('ng_backup', 'Requires your password; all administrators are notified.') }}</span>
</p> </p>
@@ -90,6 +90,7 @@ import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import NcTextField from '@nextcloud/vue/components/NcTextField' import NcTextField from '@nextcloud/vue/components/NcTextField'
import api from '../api.js' import api from '../api.js'
import { formatDateTime, formatDate } from '../format.js'
const props = defineProps({ status: { type: Object, required: true } }) const props = defineProps({ status: { type: Object, required: true } })
const emit = defineEmits(['changed']) const emit = defineEmits(['changed'])
+2 -2
View File
@@ -52,7 +52,7 @@ import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import { confirmPassword } from '@nextcloud/password-confirmation' import { confirmPassword } from '@nextcloud/password-confirmation'
import '@nextcloud/password-confirmation/style.css' import '@nextcloud/password-confirmation/style.css'
import api from '../api.js' import api from '../api.js'
import { formatSize } from '../format.js' import { formatDateTime, formatSize } from '../format.js'
const props = defineProps({ status: { type: Object, required: true } }) const props = defineProps({ status: { type: Object, required: true } })
const emit = defineEmits(['changed']) const emit = defineEmits(['changed'])
@@ -74,7 +74,7 @@ const modes = [
{ id: 'replace', label: t('ng_backup', 'Into the original place, overwrite and move files that are not in the backup to the trash') }, { id: 'replace', label: t('ng_backup', 'Into the original place, overwrite and move files that are not in the backup to the trash') },
] ]
const snapshotLabel = (s) => new Date(s.createdAt * 1000).toLocaleString() + (s.label ? ' – ' + s.label : '') const snapshotLabel = (s) => formatDateTime(s.createdAt) + (s.label ? ' – ' + s.label : '')
const join = (name) => path.value + '/' + name const join = (name) => path.value + '/' + name
const restorable = (p) => /^data\/[^/]+\/files(\/.*)?$/.test(p) const restorable = (p) => /^data\/[^/]+\/files(\/.*)?$/.test(p)
const crumbs = computed(() => { const crumbs = computed(() => {
+13
View File
@@ -1,5 +1,18 @@
// SPDX-FileCopyrightText: 2026 Dillard Blom // SPDX-FileCopyrightText: 2026 Dillard Blom
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { getCanonicalLocale } from '@nextcloud/l10n'
// The locale of the user's Nextcloud settings (Personal info > Locale), not the browser's:
// a browser set to English would otherwise show 4:00 PM to someone with a 24-hour locale.
const locale = getCanonicalLocale()
// A number is a unix timestamp in seconds, a string an ISO 8601 date (snapshot times).
const toDate = (value) => typeof value === 'number' ? new Date(value * 1000) : new Date(value)
export const formatDateTime = (value) => toDate(value).toLocaleString(locale)
export const formatDate = (value) => toDate(value).toLocaleDateString(locale)
export function formatSize(bytes) { export function formatSize(bytes) {
if (bytes === null || bytes === undefined) { if (bytes === null || bytes === undefined) {
return '' return ''