1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(web): Localize dates and numbers (#1056)

This commit is contained in:
Kiel Hurley 2022-12-05 04:35:20 +13:00 committed by GitHub
parent 426ce77f1c
commit 5f2b75997f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 86 additions and 48 deletions

14
web/package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 () => {

View File

@ -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>

View File

@ -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

View File

@ -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 />

View File

@ -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
}`}
/>

View File

@ -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}`;
}