Finalize first i18n implementation

This commit is contained in:
Sam Becker 2025-05-10 16:31:57 -05:00
parent 927b4b85b5
commit cfcff69b95
17 changed files with 126 additions and 47 deletions

View File

@ -26,6 +26,7 @@ import IconFavs from '@/components/icons/IconFavs';
import IconEdit from '@/components/icons/IconEdit';
import { photoNeedsToBeSynced } from '@/photo/sync';
import { KEY_COMMANDS } from '@/photo/key-commands';
import { APP_TEXT } from '@/app/config';
export default function AdminPhotoMenu({
photo,
@ -48,7 +49,7 @@ export default function AdminPhotoMenu({
const sectionMain = useMemo(() => {
const items: ComponentProps<typeof MoreMenuItem>[] = [{
label: 'Edit',
label: APP_TEXT.admin.edit,
icon: <IconEdit
size={15}
className="translate-x-[0.5px]"
@ -58,7 +59,7 @@ export default function AdminPhotoMenu({
}];
if (includeFavorite) {
items.push({
label: isFav ? 'Unfavorite' : 'Favorite',
label: isFav ? APP_TEXT.admin.unfavorite : APP_TEXT.admin.favorite,
icon: <IconFavs
size={14}
className="translate-x-[-1px] translate-y-[0.5px]"
@ -76,7 +77,7 @@ export default function AdminPhotoMenu({
});
}
items.push({
label: 'Download',
label: APP_TEXT.admin.download,
icon: <MdOutlineFileDownload
size={17}
className="translate-x-[-1px]"
@ -86,9 +87,9 @@ export default function AdminPhotoMenu({
...showKeyCommands && { keyCommand: KEY_COMMANDS.download },
});
items.push({
label: 'Sync',
label: APP_TEXT.admin.sync,
labelComplex: <span className="inline-flex items-center gap-2">
<span>Sync</span>
<span>{APP_TEXT.admin.sync}</span>
{photoNeedsToBeSynced(photo) &&
<InsightsIndicatorDot
colorOverride="blue"
@ -115,7 +116,7 @@ export default function AdminPhotoMenu({
]);
const sectionDelete: ComponentProps<typeof MoreMenuItem>[] = useMemo(() => [{
label: 'Delete',
label: APP_TEXT.admin.delete,
icon: <BiTrash
size={15}
className="translate-x-[-1px]"

View File

@ -9,6 +9,7 @@ import {
absolutePathForCamera,
absolutePathForCameraImage,
} from '@/app/paths';
import { APP_TEXT } from '@/app/config';
// Meta functions moved to separate file to avoid
// dependencies (camelcase-keys) found in photo/index.ts
@ -19,8 +20,9 @@ export const titleForCamera = (
photos: Photo[],
explicitCount?: number,
) => [
'Shot on',
APP_TEXT.category.cameraShare(
formatCameraText(cameraFromPhoto(photos[0], camera)),
),
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
@ -28,10 +30,9 @@ export const shareTextForCamera = (
camera: Camera,
photos: Photo[],
) =>
[
'Photos shot on',
APP_TEXT.category.cameraShare(
formatCameraText(cameraFromPhoto(photos[0], camera)),
].join(' ');
);
export const descriptionForCameraPhotos = (
photos: Photo[],

View File

@ -3,6 +3,7 @@ import LoaderButton from './primitives/LoaderButton';
import clsx from 'clsx/lite';
import { toastSuccess } from '@/toast';
import { ComponentProps } from 'react';
import { APP_TEXT } from '@/app/config';
export default function CopyButton({
label,
@ -29,7 +30,7 @@ export default function CopyButton({
onClick={text
? () => {
navigator.clipboard.writeText(text);
toastSuccess(`${label} copied to clipboard`);
toastSuccess(APP_TEXT.misc.copyPhrase(label));
}
: undefined}
styleAs="link"

View File

@ -9,6 +9,7 @@ import { FiUploadCloud } from 'react-icons/fi';
import { MAX_IMAGE_SIZE } from '@/platforms/next-image';
import ProgressButton from './primitives/ProgressButton';
import { useAppState } from '@/state/AppState';
import { APP_TEXT } from '@/app/config';
export default function ImageInput({
ref: inputRefExternal,
@ -84,9 +85,13 @@ export default function ImageInput({
>
{isUploading
? filesLength > 1
? `Uploading ${fileUploadIndex + 1} of ${filesLength}`
: 'Uploading'
: 'Upload Photos'}
? APP_TEXT.utility.paginate(
fileUploadIndex + 1,
filesLength,
APP_TEXT.admin.uploading,
)
: APP_TEXT.admin.uploading
: APP_TEXT.admin.uploadPhotos}
</ProgressButton>}
<input
ref={inputRef}

View File

@ -19,6 +19,7 @@ import {
} from '@/utility/string';
import { AnnotatedTag } from '@/photo/form';
import PhotoFilmIcon from './PhotoFilmIcon';
import { APP_TEXT } from '@/app/config';
export type FilmWithCount = {
film: string
@ -67,7 +68,7 @@ export const titleForFilm = (
export const shareTextForFilm = (
film: string,
) =>
`Photos shot on ${labelForFilm(film).large}`;
APP_TEXT.category.filmShare(labelForFilm(film).large);
export const descriptionForFilmPhotos = (
photos: Photo[],

View File

@ -8,6 +8,7 @@ import {
absolutePathForFocalLength,
absolutePathForFocalLengthImage,
} from '@/app/paths';
import { APP_TEXT } from '@/app/config';
export type FocalLengths = {
focal: number
@ -31,12 +32,12 @@ export const titleForFocalLength = (
photos: Photo[],
explicitCount?: number,
) => [
`${formatFocalLength(focal)} Focal Length`,
APP_TEXT.category.focalLengthTitle(formatFocalLengthSafe(focal)),
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
export const shareTextFocalLength = (focal: number) =>
`Photos shot at ${formatFocalLength(focal)}`;
APP_TEXT.category.focalLengthShare(formatFocalLengthSafe(focal));
export const descriptionForFocalLengthPhotos = (
photos: Photo[],

View File

@ -1,4 +1,5 @@
import { I18NDeepPartial } from '..';
import { ptBR } from 'date-fns/locale';
const TEXT: I18NDeepPartial = {
photo: {
@ -7,21 +8,30 @@ const TEXT: I18NDeepPartial = {
taken: 'Capturado',
created: 'Criado',
updated: 'Atualizado',
copied: 'Link para foto copiado',
},
category: {
camera: 'Câmera',
cameraPlural: 'Câmeras',
cameraTitle: (camera: string) => `Tirado com ${camera}`,
cameraShare: (camera: string) => `Fotos tiradas com ${camera}`,
lens: 'Lente',
lensPlural: 'Lentes',
tag: 'Tag',
tagPlural: 'Tags',
tagged: 'Marcado',
taggedPhotos: 'Fotos Marcadas',
taggedPhrase: (tag: string) => `Fotos marcadas com '${tag}'`,
taggedFavs: 'Fotos Favoritas',
recipe: 'Receita',
recipePlural: 'Receitas',
recipeShare: (recipe: string) => `Fotos da receita ${recipe}`,
film: 'Filme',
filmPlural: 'Filmes',
filmShare: (film: string) => `Fotos tiradas com ${film}`,
focalLength: 'Distância Focal',
focalLengthPlural: 'Distâncias Focais',
focalLengthTitle: (focal: string) => `Distância Focal ${focal}`,
focalLengthShare: (focal: string) => `Fotos tiradas em ${focal}`,
},
nav: {
home: 'Início',
@ -62,6 +72,7 @@ const TEXT: I18NDeepPartial = {
uploadPhotos: 'Enviar Fotos',
upload: 'Enviar',
uploadPlural: 'Envios',
uploading: 'Enviando',
updates: 'Atualizações',
managePhotos: 'Gerenciar Fotos',
manageCameras: 'Gerenciar Câmeras',
@ -73,10 +84,29 @@ const TEXT: I18NDeepPartial = {
batchExitEdit: 'Sair da Edição em Lote',
appInsights: 'Insights do App',
appConfig: 'Configuração do App',
edit: 'Editar',
favorite: 'Favoritar',
unfavorite: 'Remover dos Favoritos',
download: 'Baixar',
sync: 'Sincronizar',
delete: 'Excluir',
deleteConfirm: (photoTitle: string) =>
`Tem certeza que deseja excluir "${photoTitle}"?`,
},
misc: {
repo: 'Feito com',
copyPhrase: (label: string) => `${label} copiado`,
},
utility: {
paginate: (
index: number,
count: number,
action?: string,
) => action
? `${action} ${index} de ${count}`
: `${index} de ${count}`,
},
dateLocale: ptBR,
};
export default TEXT;

View File

@ -1,3 +1,5 @@
import { enUS } from 'date-fns/locale';
const TEXT = {
photo: {
photo: 'Photo',
@ -5,21 +7,30 @@ const TEXT = {
taken: 'Taken',
created: 'Created',
updated: 'Updated',
copied: 'Link to photo copied',
},
category: {
camera: 'Camera',
cameraPlural: 'Cameras',
cameraTitle: (camera: string) => `Shot on ${camera}`,
cameraShare: (camera: string) => `Photos shot on ${camera}`,
lens: 'Lens',
lensPlural: 'Lenses',
tag: 'Tag',
tagPlural: 'Tags',
tagged: 'Tagged Photos',
taggedPhotos: 'Tagged Photos',
taggedPhrase: (tag: string) => `Photos tagged '${tag}'`,
taggedFavs: 'Favorite Photos',
recipe: 'Recipe',
recipePlural: 'Recipes',
recipeShare: (recipe: string) => `${recipe} recipe photos`,
film: 'Film',
filmPlural: 'Films',
filmShare: (film: string) => `Photos shot on ${film}`,
focalLength: 'Focal Length',
focalLengthPlural: 'Focal Lengths',
focalLengthTitle: (focal: string) => `Focal Length ${focal}`,
focalLengthShare: (focal: string) => `Photos shot at ${focal}`,
},
nav: {
home: 'Home',
@ -60,6 +71,7 @@ const TEXT = {
uploadPhotos: 'Upload Photos',
upload: 'Upload',
uploadPlural: 'Uploads',
uploading: 'Uploading',
updates: 'Updates',
managePhotos: 'Manage Photos',
manageCameras: 'Manage Cameras',
@ -71,17 +83,29 @@ const TEXT = {
batchExitEdit: 'Exit Batch Edit',
appInsights: 'App Insights',
appConfig: 'App Configuration',
edit: 'Edit',
favorite: 'Favorite',
unfavorite: 'Unfavorite',
download: 'Download',
sync: 'Sync',
delete: 'Delete',
deleteConfirm: (photoTitle: string) =>
`Are you sure you want to delete "${photoTitle}?"`,
},
misc: {
repo: 'Made with',
copyPhrase: (label: string) => `${label} copied`,
},
utility: {
paginate: (
index: number,
count: number,
verb?: string,
) => verb
? `${verb} ${index} of ${count}`
action?: string,
) => action
? `${action} ${index} of ${count}`
: `${index} of ${count}`,
},
dateLocale: enUS,
};
export default TEXT;

View File

@ -9,6 +9,7 @@ import {
absolutePathForLens,
absolutePathForLensImage,
} from '@/app/paths';
import { APP_TEXT } from '@/app/config';
// Meta functions moved to separate file to avoid
// dependencies (camelcase-keys) found in photo/index.ts
@ -19,7 +20,7 @@ export const titleForLens = (
photos: Photo[],
explicitCount?: number,
) => [
'Lens:',
`${APP_TEXT.category.lens}:`,
formatLensText(lensFromPhoto(photos[0], lens)),
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
@ -29,7 +30,7 @@ export const shareTextForLens = (
photos: Photo[],
) =>
[
'Lens:',
`${APP_TEXT.category.lens}:`,
formatLensText(lensFromPhoto(photos[0], lens)),
].join(' ');

View File

@ -155,9 +155,13 @@ export default function PhotoHeader({
}} />}
</>
: <ResponsiveText
shortText={APP_TEXT.paginate(paginationIndex, paginationCount)}
shortText={APP_TEXT.utility.paginate(
paginationIndex,
paginationCount,
entityVerb,
)}
>
{APP_TEXT.paginate(
{APP_TEXT.utility.paginate(
paginationIndex,
paginationCount,
entityVerb)}

View File

@ -172,7 +172,7 @@ export const photoStatsAsString = (photo: Photo) => [
].join(' ');
export const descriptionForPhoto = (photo: Photo) =>
photo.takenAtNaiveFormatted?.toUpperCase();
formatDate({ date: photo.takenAt }).toLocaleUpperCase();
export const getPreviousPhoto = (photo: Photo, photos: Photo[]) => {
const index = photos.findIndex(p => p.id === photo.id);
@ -251,7 +251,7 @@ export const photoQuantityText = (
: `${count} ${photoLabelForCount(count, capitalize)}`;
export const deleteConfirmationTextForPhoto = (photo: Photo) =>
`Are you sure you want to delete "${titleForPhoto(photo)}?"`;
APP_TEXT.admin.deleteConfirm(titleForPhoto(photo));
export type PhotoDateRange = { start: string, end: string };
@ -265,9 +265,10 @@ export const descriptionForPhotoSet = (
dateBased
? dateRangeForPhotos(photos, explicitDateRange).description.toUpperCase()
: [
explicitCount ?? photos.length,
descriptor,
photoLabelForCount(explicitCount ?? photos.length, false),
explicitCount ?? photos.length, (
descriptor ||
photoLabelForCount(explicitCount ?? photos.length, false)
),
].join(' ');
const sortPhotosByDateNonDestructively = (

View File

@ -19,6 +19,7 @@ import { TbChecklist } from 'react-icons/tb';
import CopyButton from '@/components/CopyButton';
import { labelForFilm } from '@/film';
import PhotoRecipe from './PhotoRecipe';
import { APP_TEXT } from '@/app/config';
export default function PhotoRecipeOverlay({
ref,
@ -138,7 +139,7 @@ export default function PhotoRecipeOverlay({
'text-black/40 active:text-black/75',
'hover:text-black/40',
)}
tooltip="Copy recipe text"
tooltip={APP_TEXT.tooltip.recipeCopy}
tooltipColor="frosted"
/>
<span>

View File

@ -8,6 +8,7 @@ import {
} from '@/utility/string';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { labelForFilm } from '@/film';
import { APP_TEXT } from '@/app/config';
export type RecipeWithCount = {
recipe: string
@ -32,12 +33,12 @@ export const titleForRecipe = (
photos:Photo[] = [],
explicitCount?: number,
) => [
`Recipe: ${formatRecipe(recipe)}`,
`${APP_TEXT.category.recipe}: ${formatRecipe(recipe)}`,
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
export const shareTextForRecipe = (recipe: string) =>
`${formatRecipe(recipe)} recipe photos`;
APP_TEXT.category.recipeShare(formatRecipe(recipe));
export const descriptionForRecipePhotos = (
photos: Photo[] = [],

View File

@ -8,7 +8,7 @@ import { ReactNode, useEffect } from 'react';
import { shortenUrl } from '@/utility/url';
import { toastSuccess } from '@/toast';
import { PiXLogo } from 'react-icons/pi';
import { SHOW_SOCIAL } from '@/app/config';
import { APP_TEXT, SHOW_SOCIAL } from '@/app/config';
import { generateXPostText } from '@/utility/social';
import { useAppState } from '@/state/AppState';
import useOnPathChange from '@/utility/useOnPathChange';
@ -96,7 +96,7 @@ export default function ShareModal({
<BiCopy size={18} />,
() => {
navigator.clipboard.writeText(pathShare);
toastSuccess('Link to photo copied');
toastSuccess(APP_TEXT.photo.copied);
},
true,
)}

View File

@ -26,7 +26,7 @@ export default function TagHeader({
entity={isTagFavs(tag)
? <FavsTag contrast="high" />
: <PhotoTag tag={tag} contrast="high" />}
entityVerb={APP_TEXT.category.tagged}
entityVerb={APP_TEXT.category.taggedPhotos}
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
photos={photos}
selectedPhoto={selectedPhoto}

View File

@ -16,6 +16,7 @@ import {
formatCountDescriptive,
} from '@/utility/string';
import { sortCategoryByCount } from '@/category';
import { APP_TEXT } from '@/app/config';
// Reserved tags
export const TAG_FAVS = 'favs';
@ -48,7 +49,9 @@ export const titleForTag = (
].join(' ');
export const shareTextForTag = (tag: string) =>
isTagFavs(tag) ? 'Favorite photos' : `Photos tagged '${formatTag(tag)}'`;
isTagFavs(tag)
? APP_TEXT.category.taggedFavs
: APP_TEXT.category.taggedPhrase(formatTag(tag));
export const sortTagsArray = (
tags: string[],
@ -95,7 +98,7 @@ export const descriptionForTaggedPhotos = (
) =>
descriptionForPhotoSet(
photos,
'tagged',
APP_TEXT.category.taggedPhotos,
dateBased,
explicitCount,
explicitDateRange,
@ -139,5 +142,6 @@ export const convertTagsForForm = (tags: Tags = []) =>
.map(({ tag, count }) => ({
value: tag,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count, 'tagged'),
annotationAria:
formatCountDescriptive(count, APP_TEXT.category.taggedPhotos),
}));

View File

@ -1,6 +1,7 @@
import { parseISO, parse, format } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
import { Timezone } from './timezone';
import { APP_TEXT } from '@/app/config';
const DATE_STRING_FORMAT_TINY = 'dd MMM yy';
const DATE_STRING_FORMAT_TINY_PLACEHOLDER = '00 000 00';
@ -65,8 +66,10 @@ export const formatDate = ({
return showPlaceholder
? placeholderString
: timezone
? formatInTimeZone(date, timezone, formatString)
: format(date, formatString);
? formatInTimeZone(
date, timezone, formatString, { locale: APP_TEXT.dateLocale },
)
: format(date, formatString, { locale: APP_TEXT.dateLocale });
};
export const formatDateFromPostgresString = (date: string, length?: Length) =>