Localize dates and numbers (#574)

This commit is contained in:
InfiniteTF
2020-05-25 07:49:13 +02:00
committed by GitHub
parent ccd75731b7
commit 197918d13c
12 changed files with 129 additions and 23 deletions

View File

@@ -47,6 +47,9 @@
"react/prop-types": "off", "react/prop-types": "off",
"react/destructuring-assignment": "off", "react/destructuring-assignment": "off",
"react/jsx-props-no-spreading": "off", "react/jsx-props-no-spreading": "off",
"react/style-prop-object": ["error", {
"allow": ["FormattedNumber"]
}],
"spaced-comment": ["error", "always", { "spaced-comment": ["error", "always", {
"markers": ["/"] "markers": ["/"]
}], }],

View File

@@ -24,6 +24,12 @@ import Movies from "./components/Movies/Movies";
// Set fontawesome/free-solid-svg as default fontawesome icons // Set fontawesome/free-solid-svg as default fontawesome icons
library.add(fas); library.add(fas);
const intlFormats = {
date: {
long: { year: "numeric", month: "long", day: "numeric" },
},
};
export const App: React.FC = () => { export const App: React.FC = () => {
const config = useConfiguration(); const config = useConfiguration();
const language = config.data?.configuration?.interface?.language ?? "en-US"; const language = config.data?.configuration?.interface?.language ?? "en-US";
@@ -33,7 +39,7 @@ export const App: React.FC = () => {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<IntlProvider locale={language} messages={messages}> <IntlProvider locale={language} messages={messages} formats={intlFormats}>
<ToastProvider> <ToastProvider>
<MainNavbar /> <MainNavbar />
<div className="main container-fluid"> <div className="main container-fluid">

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import { useIntl } from "react-intl"; import { FormattedNumber, useIntl } from "react-intl";
interface IPaginationProps { interface IPaginationProps {
itemsPerPage: number; itemsPerPage: number;
@@ -62,7 +62,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
active={currentPage === page} active={currentPage === page}
onClick={() => onChangePage(page)} onClick={() => onChangePage(page)}
> >
{page} <FormattedNumber value={page} />
</Button> </Button>
)); ));

View File

@@ -1,5 +1,6 @@
import { Card } from "react-bootstrap"; import { Card } from "react-bootstrap";
import React, { FunctionComponent } from "react"; import React, { FunctionComponent } from "react";
import { FormattedPlural } from "react-intl";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -26,7 +27,16 @@ export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
function maybeRenderSceneNumber() { function maybeRenderSceneNumber() {
if (!props.sceneIndex) { if (!props.sceneIndex) {
return <span>{props.movie.scene_count} scenes.</span>; return (
<span>
{props.movie.scene_count}&nbsp;
<FormattedPlural
value={props.movie.scene_count ?? 0}
one="scene"
other="scenes"
/>
</span>
);
} }
return <span>Scene number: {props.sceneIndex}</span>; return <span>Scene number: {props.sceneIndex}</span>;

View File

@@ -1,5 +1,6 @@
/* eslint-disable react/no-this-in-sfc */ /* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
useFindMovie, useFindMovie,
@@ -65,6 +66,8 @@ export const Movie: React.FC = () => {
getMovieInput() as GQL.MovieDestroyInput getMovieInput() as GQL.MovieDestroyInput
); );
const intl = useIntl();
function updateMovieEditState(state: Partial<GQL.MovieDataFragment>) { function updateMovieEditState(state: Partial<GQL.MovieDataFragment>) {
setName(state.name ?? undefined); setName(state.name ?? undefined);
setAliases(state.aliases ?? undefined); setAliases(state.aliases ?? undefined);
@@ -238,8 +241,10 @@ export const Movie: React.FC = () => {
setDuration(value ? Number.parseInt(value, 10) : undefined), setDuration(value ? Number.parseInt(value, 10) : undefined),
})} })}
{TableUtils.renderInputGroup({ {TableUtils.renderInputGroup({
title: "Date (YYYY-MM-DD)", title: `Date ${isEditing ? "(YYYY-MM-DD)" : ""}`,
value: date, value: isEditing
? date
: intl.formatDate(date, { format: "long" }),
isEditing, isEditing,
onChange: setDate, onChange: setDate,
})} })}

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { Card } from "react-bootstrap"; import { Card } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { FormattedNumber, FormattedPlural } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils"; import { NavUtils, TextUtils } from "src/utils";
import { CountryFlag } from "src/components/Shared"; import { CountryFlag } from "src/components/Shared";
@@ -39,8 +40,17 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
{age !== 0 ? <div className="text-muted">{ageString}</div> : ""} {age !== 0 ? <div className="text-muted">{ageString}</div> : ""}
<CountryFlag country={performer.country} /> <CountryFlag country={performer.country} />
<div className="text-muted"> <div className="text-muted">
Stars in {performer.scene_count}{" "} Stars in&nbsp;
<Link to={NavUtils.makePerformerScenesUrl(performer)}>scenes</Link>. <FormattedNumber value={performer.scene_count ?? 0} />
&nbsp;
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
<FormattedPlural
value={performer.scene_count ?? 0}
one="scene"
other="scenes"
/>
</Link>
.
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -1,6 +1,7 @@
/* eslint-disable react/no-this-in-sfc */ /* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { Button, Popover, OverlayTrigger, Table } from "react-bootstrap"; import { Button, Popover, OverlayTrigger, Table } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
@@ -83,6 +84,8 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const intl = useIntl();
const Scrapers = useListPerformerScrapers(); const Scrapers = useListPerformerScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]); const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
@@ -459,6 +462,20 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
}); });
} }
const formatHeight = () => {
if (isEditing) {
return height;
}
if (!height) {
return "";
}
return intl.formatNumber(Number.parseInt(height, 10), {
style: "unit",
unit: "centimeter",
unitDisplay: "narrow",
});
};
return ( return (
<> <>
{renderDeleteAlert()} {renderDeleteAlert()}
@@ -471,7 +488,9 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
{renderGender()} {renderGender()}
{TableUtils.renderInputGroup({ {TableUtils.renderInputGroup({
title: "Birthdate", title: "Birthdate",
value: birthdate, value: isEditing
? birthdate
: intl.formatDate(birthdate, { format: "long" }),
isEditing: !!isEditing, isEditing: !!isEditing,
onChange: setBirthdate, onChange: setBirthdate,
})} })}
@@ -489,8 +508,8 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
onChange: setCountry, onChange: setCountry,
})} })}
{TableUtils.renderInputGroup({ {TableUtils.renderInputGroup({
title: "Height (cm)", title: `Height ${isEditing ? "(cm)" : ""}`,
value: height, value: formatHeight(),
isEditing: !!isEditing, isEditing: !!isEditing,
onChange: setHeight, onChange: setHeight,
})} })}

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { FormattedDate } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { TagLink } from "src/components/Shared"; import { TagLink } from "src/components/Shared";
@@ -38,7 +39,9 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
{props.scene.title ?? TextUtils.fileNameFromPath(props.scene.path)} {props.scene.title ?? TextUtils.fileNameFromPath(props.scene.path)}
</h3> </h3>
<div className="col-6 scene-details"> <div className="col-6 scene-details">
<h4>{props.scene.date ?? ""}</h4> <h4>
<FormattedDate value={props.scene.date ?? ""} format="long" />
</h4>
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""} {props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""}
{props.scene.file.height && ( {props.scene.file.height && (
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6> <h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { FormattedNumber } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
@@ -49,11 +50,23 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
if (props.scene.file.size === undefined) { if (props.scene.file.size === undefined) {
return; return;
} }
const { size, unit } = TextUtils.fileSize(
Number.parseInt(props.scene.file.size ?? "0", 10)
);
return ( return (
<div className="row"> <div className="row">
<span className="col-4">File Size</span> <span className="col-4">File Size</span>
<span className="col-8 text-truncate"> <span className="col-8 text-truncate">
{TextUtils.fileSize(parseInt(props.scene.file.size ?? "0", 10))} <FormattedNumber
value={size}
// eslint-disable-next-line react/style-prop-object
style="unit"
unit={unit}
unitDisplay="narrow"
maximumFractionDigits={2}
/>
</span> </span>
</div> </div>
); );
@@ -95,13 +108,15 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
<div className="row"> <div className="row">
<span className="col-4">Frame Rate</span> <span className="col-4">Frame Rate</span>
<span className="col-8 text-truncate"> <span className="col-8 text-truncate">
{props.scene.file.framerate} frames per second <FormattedNumber value={props.scene.file.framerate ?? 0} /> frames per
second
</span> </span>
</div> </div>
); );
} }
function renderbitrate() { function renderbitrate() {
// TODO: An upcoming react-intl version will support compound units, megabits-per-second
if (props.scene.file.bitrate === undefined) { if (props.scene.file.bitrate === undefined) {
return; return;
} }
@@ -109,7 +124,11 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
<div className="row"> <div className="row">
<span className="col-4">Bit Rate</span> <span className="col-4">Bit Rate</span>
<span className="col-8 text-truncate"> <span className="col-8 text-truncate">
{TextUtils.bitRate(props.scene.file.bitrate ?? 0)} <FormattedNumber
value={(props.scene.file.bitrate ?? 0) / 1000000}
maximumFractionDigits={2}
/>
&nbsp;megabits per second
</span> </span>
</div> </div>
); );

View File

@@ -2,6 +2,7 @@ import { Card } from "react-bootstrap";
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { FormattedPlural } from "react-intl";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@@ -19,7 +20,15 @@ export const StudioCard: React.FC<IProps> = ({ studio }) => {
</Link> </Link>
<div className="card-section"> <div className="card-section">
<h5 className="text-truncate">{studio.name}</h5> <h5 className="text-truncate">{studio.name}</h5>
<span>{studio.scene_count} scenes.</span> <span>
{studio.scene_count}&nbsp;
<FormattedPlural
value={studio.scene_count ?? 0}
one="scene"
other="scenes"
/>
.
</span>
</div> </div>
</Card> </Card>
); );

View File

@@ -1,6 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { FormattedNumber } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
mutateMetadataAutoTag, mutateMetadataAutoTag,
@@ -115,7 +116,7 @@ export const TagList: React.FC = () => {
to={NavUtils.makeTagScenesUrl(tag)} to={NavUtils.makeTagScenesUrl(tag)}
className="tag-list-anchor" className="tag-list-anchor"
> >
Scenes: {tag.scene_count} Scenes: <FormattedNumber value={tag.scene_count ?? 0} />
</Link> </Link>
</Button> </Button>
<Button variant="secondary" className="tag-list-button"> <Button variant="secondary" className="tag-list-button">
@@ -123,11 +124,14 @@ export const TagList: React.FC = () => {
to={NavUtils.makeTagSceneMarkersUrl(tag)} to={NavUtils.makeTagSceneMarkersUrl(tag)}
className="tag-list-anchor" className="tag-list-anchor"
> >
Markers: {tag.scene_marker_count} Markers: <FormattedNumber value={tag.scene_marker_count ?? 0} />
</Link> </Link>
</Button> </Button>
<span className="tag-list-count"> <span className="tag-list-count">
Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)} Total:{" "}
<FormattedNumber
value={(tag.scene_count || 0) + (tag.scene_marker_count || 0)}
/>
</span> </span>
<Button variant="danger" onClick={() => setDeletingTag(tag)}> <Button variant="danger" onClick={() => setDeletingTag(tag)}>
<Icon icon="trash-alt" color="danger" /> <Icon icon="trash-alt" color="danger" />

View File

@@ -1,4 +1,19 @@
const Units = ["bytes", "kB", "MB", "GB", "TB", "PB"]; // Typescript currently does not implement the intl Unit interface
type Unit =
| "byte"
| "kilobyte"
| "megabyte"
| "gigabyte"
| "terabyte"
| "petabyte";
const Units: Unit[] = [
"byte",
"kilobyte",
"megabyte",
"gigabyte",
"terabyte",
"petabyte",
];
const truncate = ( const truncate = (
value?: string, value?: string,
@@ -9,9 +24,9 @@ const truncate = (
return value.length > limit ? value.substring(0, limit) + tail : value; return value.length > limit ? value.substring(0, limit) + tail : value;
}; };
const fileSize = (bytes: number = 0, precision: number = 2) => { const fileSize = (bytes: number = 0) => {
if (Number.isNaN(parseFloat(String(bytes))) || !Number.isFinite(bytes)) if (Number.isNaN(parseFloat(String(bytes))) || !Number.isFinite(bytes))
return "?"; return { size: 0, unit: Units[0] };
let unit = 0; let unit = 0;
let count = bytes; let count = bytes;
@@ -20,7 +35,10 @@ const fileSize = (bytes: number = 0, precision: number = 2) => {
unit++; unit++;
} }
return `${count.toFixed(+precision)} ${Units[unit]}`; return {
size: count,
unit: Units[unit],
};
}; };
const secondsToTimestamp = (seconds: number) => { const secondsToTimestamp = (seconds: number) => {