mirror of
https://github.com/immich-app/immich.git
synced 2024-12-24 10:37:28 +02:00
feat(web): Localize dates and numbers (#1056)
This commit is contained in:
parent
426ce77f1c
commit
5f2b75997f
14
web/package-lock.json
generated
14
web/package-lock.json
generated
@ -15,7 +15,6 @@
|
||||
"leaflet": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.3",
|
||||
"socket.io-client": "^4.5.1",
|
||||
"svelte-keydown": "^0.5.0",
|
||||
"svelte-material-icons": "^2.0.2"
|
||||
@ -8873,14 +8872,6 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@ -17603,11 +17594,6 @@
|
||||
"minimist": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
@ -65,7 +65,6 @@
|
||||
"leaflet": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.3",
|
||||
"socket.io-client": "^4.5.1",
|
||||
"svelte-keydown": "^0.5.0",
|
||||
"svelte-material-icons": "^2.0.2"
|
||||
|
@ -4,6 +4,7 @@
|
||||
import PlayCircle from 'svelte-material-icons/PlayCircle.svelte';
|
||||
import Memory from 'svelte-material-icons/Memory.svelte';
|
||||
import StatsCard from './stats-card.svelte';
|
||||
import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units';
|
||||
export let stats: ServerStatsResponseDto;
|
||||
export let allUsers: Array<UserResponseDto>;
|
||||
|
||||
@ -15,8 +16,9 @@
|
||||
return name;
|
||||
};
|
||||
|
||||
$: spaceUnit = stats.usage.split(' ')[1];
|
||||
$: spaceUsage = stats.usage.split(' ')[0];
|
||||
$: [spaceUsage, spaceUnit] = getBytesWithUnit(stats.usageRaw);
|
||||
|
||||
const locale = navigator.languages;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
@ -55,9 +57,9 @@
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{getFullName(user.userId)}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.usage}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString(locale)}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString(locale)}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usageRaw)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
@ -5,7 +5,6 @@
|
||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
|
||||
import moment from 'moment';
|
||||
|
||||
export let allUsers: Array<UserResponseDto>;
|
||||
|
||||
@ -15,8 +14,15 @@
|
||||
return user.deletedAt != null;
|
||||
};
|
||||
|
||||
const locale = navigator.languages;
|
||||
const deleteDateFormat: Intl.DateTimeFormatOptions = {
|
||||
month: 'long', day: 'numeric', year: 'numeric'
|
||||
};
|
||||
|
||||
const getDeleteDate = (user: UserResponseDto): string => {
|
||||
return moment(user.deletedAt).add(7, 'days').format('LL');
|
||||
let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
|
||||
deletedAt.setDate(deletedAt.getDate() + 7);
|
||||
return deletedAt.toLocaleString(locale, deleteDateFormat);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -53,6 +53,8 @@
|
||||
onMount(async () => {
|
||||
imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || NO_THUMBNAIL;
|
||||
});
|
||||
|
||||
const locale = navigator.languages;
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -87,7 +89,7 @@
|
||||
</p>
|
||||
|
||||
<span class="text-xs flex gap-2 dark:text-immich-dark-fg" data-testid="album-details">
|
||||
<p>{album.assetCount} items</p>
|
||||
<p>{album.assetCount.toLocaleString(locale)} {album.assetCount == 1 ? `item` : `items`}</p>
|
||||
|
||||
{#if album.shared}
|
||||
<p>·</p>
|
||||
|
@ -86,19 +86,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
const getDateRange = () => {
|
||||
const startDate = new Date(album.assets[0].createdAt);
|
||||
const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
|
||||
|
||||
const timeFormatOption: Intl.DateTimeFormatOptions = {
|
||||
const locale = navigator.languages;
|
||||
const albumDateFormat: Intl.DateTimeFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
};
|
||||
|
||||
const startDateString = startDate.toLocaleDateString('us-EN', timeFormatOption);
|
||||
const endDateString = endDate.toLocaleDateString('us-EN', timeFormatOption);
|
||||
return `${startDateString} - ${endDateString}`;
|
||||
const getDateRange = () => {
|
||||
const startDate = new Date(album.assets[0].createdAt);
|
||||
const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
|
||||
|
||||
const startDateString = startDate.toLocaleDateString(locale, albumDateFormat);
|
||||
const endDateString = endDate.toLocaleDateString(locale, albumDateFormat);
|
||||
|
||||
// If the start and end date are the same, only show one date
|
||||
return startDateString === endDateString ? startDateString : `${startDateString} - ${endDateString}`;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
|
@ -4,11 +4,10 @@
|
||||
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||
import CameraIris from 'svelte-material-icons/CameraIris.svelte';
|
||||
import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
|
||||
import moment from 'moment';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { AssetResponseDto, AlbumResponseDto } from '@api';
|
||||
import { getHumanReadableBytes } from '../../utils/byte-units';
|
||||
import { asByteUnitString } from '../../utils/byte-units';
|
||||
|
||||
type Leaflet = typeof import('leaflet');
|
||||
type LeafletMap = import('leaflet').Map;
|
||||
@ -70,6 +69,8 @@
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const locale = navigator.languages;
|
||||
</script>
|
||||
|
||||
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||
@ -92,18 +93,18 @@
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo?.dateTimeOriginal}
|
||||
{@const assetDateTimeOriginal = new Date(asset.exifInfo.dateTimeOriginal)}
|
||||
<div class="flex gap-4 py-4">
|
||||
<div>
|
||||
<Calendar size="24" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>{moment(asset.exifInfo.dateTimeOriginal).format('MMM DD, YYYY')}</p>
|
||||
<p>{assetDateTimeOriginal.toLocaleDateString(locale, {month:'short', day:'numeric', year: 'numeric'})}</p>
|
||||
<div class="flex gap-2 text-sm">
|
||||
<p>
|
||||
{moment(asset.exifInfo.dateTimeOriginal).format('ddd, hh:mm A')}
|
||||
{assetDateTimeOriginal.toLocaleString(locale, {weekday:'short', hour: 'numeric', minute: '2-digit', timeZoneName:'longOffset'})}
|
||||
</p>
|
||||
<p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>{/if}
|
||||
@ -124,7 +125,7 @@
|
||||
|
||||
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>
|
||||
{/if}
|
||||
<p>{getHumanReadableBytes(asset.exifInfo.fileSizeInByte)}</p>
|
||||
<p>{asByteUnitString(asset.exifInfo.fileSizeInByte)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -137,14 +138,14 @@
|
||||
<div>
|
||||
<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p>
|
||||
<div class="flex text-sm gap-2">
|
||||
<p>{`ƒ/${asset.exifInfo.fNumber}` || ''}</p>
|
||||
<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString(locale)}` || ''}</p>
|
||||
|
||||
{#if asset.exifInfo.exposureTime}
|
||||
<p>{`1/${Math.floor(1 / asset.exifInfo.exposureTime)}`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.focalLength}
|
||||
<p>{`${asset.exifInfo.focalLength} mm`}</p>
|
||||
<p>{`${asset.exifInfo.focalLength.toLocaleString(locale)} mm`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.iso}
|
||||
@ -164,7 +165,9 @@
|
||||
<div>
|
||||
<p>{asset.exifInfo.city}</p>
|
||||
<div class="flex text-sm gap-2">
|
||||
<p>{asset.exifInfo.state},</p>
|
||||
<p>{asset.exifInfo.state}</p>
|
||||
</div>
|
||||
<div class="flex text-sm gap-2">
|
||||
<p>{asset.exifInfo.country}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,6 @@
|
||||
import { fly } from 'svelte/transition';
|
||||
import { AssetResponseDto } from '@api';
|
||||
import lodash from 'lodash-es';
|
||||
import moment from 'moment';
|
||||
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
@ -19,12 +18,20 @@
|
||||
export let bucketHeight: number;
|
||||
export let isAlbumSelectionMode = false;
|
||||
|
||||
const locale = navigator.languages;
|
||||
const groupDateFormat: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
};
|
||||
|
||||
let isMouseOverGroup = false;
|
||||
let actualBucketHeight: number;
|
||||
let hoveredDateGroup = '';
|
||||
$: assetsGroupByDate = lodash
|
||||
.chain(assets)
|
||||
.groupBy((a) => moment(a.createdAt).format('ddd, MMM DD YYYY'))
|
||||
.groupBy((a) => new Date(a.createdAt).toLocaleDateString(locale, groupDateFormat))
|
||||
.sortBy((group) => assets.indexOf(group[0]))
|
||||
.value();
|
||||
|
||||
@ -107,7 +114,7 @@
|
||||
bind:clientHeight={actualBucketHeight}
|
||||
>
|
||||
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
|
||||
{@const dateGroupTitle = moment(assetsInDateGroup[0].createdAt).format('ddd, MMM DD YYYY')}
|
||||
{@const dateGroupTitle = new Date(assetsInDateGroup[0].createdAt).toLocaleDateString(locale, groupDateFormat)}
|
||||
<!-- Asset Group By Date -->
|
||||
|
||||
<div
|
||||
|
@ -4,6 +4,7 @@
|
||||
import Dns from 'svelte-material-icons/Dns.svelte';
|
||||
import LoadingSpinner from './loading-spinner.svelte';
|
||||
import { api, ServerInfoResponseDto } from '@api';
|
||||
import { asByteUnitString } from '../../utils/byte-units';
|
||||
|
||||
let isServerOk = true;
|
||||
let serverVersion = '';
|
||||
@ -61,7 +62,7 @@
|
||||
style={`width: ${getStorageUsagePercentage()}%`}
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs">{serverInfo?.diskUse} of {serverInfo?.diskSize} used</p>
|
||||
<p class="text-xs">{asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used</p>
|
||||
{:else}
|
||||
<div class="mt-2">
|
||||
<LoadingSpinner />
|
||||
|
@ -6,7 +6,7 @@
|
||||
import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte';
|
||||
import type { UploadAsset } from '$lib/models/upload-asset';
|
||||
import { notificationController, NotificationType } from './notification/notification';
|
||||
import { getHumanReadableBytes } from '../../utils/byte-units';
|
||||
import { getBytesWithUnit } from '../../utils/byte-units';
|
||||
|
||||
let showDetail = true;
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
<input
|
||||
disabled
|
||||
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
|
||||
value={`[${getHumanReadableBytes(uploadAsset.file.size)}] ${
|
||||
value={`[${getBytesWithUnit(uploadAsset.file.size)}] ${
|
||||
uploadAsset.file.name
|
||||
}`}
|
||||
/>
|
||||
|
@ -1,4 +1,14 @@
|
||||
export function getHumanReadableBytes(bytes: number): string {
|
||||
/**
|
||||
* Convert bytes to best human readable unit and number of that unit.
|
||||
*
|
||||
* * For `1024` bytes, returns `1` and `KiB`.
|
||||
* * For `1536` bytes, returns `1.5` and `KiB`.
|
||||
*
|
||||
* @param bytes number of bytes
|
||||
* @param maxPrecision maximum number of decimal places, default is `1`
|
||||
* @returns size (number) and unit (string)
|
||||
*/
|
||||
export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, string] {
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
|
||||
|
||||
let magnitude = 0;
|
||||
@ -12,5 +22,24 @@ export function getHumanReadableBytes(bytes: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
return `${remainder.toFixed(magnitude == 0 ? 0 : 1)} ${units[magnitude]}`;
|
||||
remainder = parseFloat(remainder.toFixed(maxPrecision));
|
||||
|
||||
return [remainder, units[magnitude]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Localized number of bytes with a unit.
|
||||
*
|
||||
* For `1536` bytes:
|
||||
* * en: `1.5 KiB`
|
||||
* * de: `1,5 KiB`
|
||||
*
|
||||
* @param bytes number of bytes
|
||||
* @param maxPrecision maximum number of decimal places, default is `1`
|
||||
* @returns localized bytes with unit as string
|
||||
*/
|
||||
export function asByteUnitString(bytes: number, maxPrecision = 1): string {
|
||||
const locale = Array.from(navigator.languages);
|
||||
const [size, unit] = getBytesWithUnit(bytes, maxPrecision);
|
||||
return `${size.toLocaleString(locale)} ${unit}`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user