From cfcff69b95ad3c5a31ad0e9c5e34d284587a4aeb Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 10 May 2025 16:31:57 -0500 Subject: [PATCH] Finalize first i18n implementation --- src/admin/AdminPhotoMenu.tsx | 13 +++++----- src/camera/meta.ts | 11 +++++---- src/components/CopyButton.tsx | 3 ++- src/components/ImageInput.tsx | 11 ++++++--- src/film/index.tsx | 3 ++- src/focal/index.ts | 5 ++-- src/i18n/locales/pt-br.ts | 32 ++++++++++++++++++++++++- src/i18n/locales/us-en.ts | 40 ++++++++++++++++++++++++------- src/lens/meta.ts | 5 ++-- src/photo/PhotoHeader.tsx | 8 +++++-- src/photo/index.ts | 11 +++++---- src/recipe/PhotoRecipeOverlay.tsx | 3 ++- src/recipe/index.ts | 5 ++-- src/share/ShareModal.tsx | 4 ++-- src/tag/TagHeader.tsx | 2 +- src/tag/index.ts | 10 +++++--- src/utility/date.ts | 7 ++++-- 17 files changed, 126 insertions(+), 47 deletions(-) diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index 64fefc73..f9104cc6 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -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[] = [{ - label: 'Edit', + label: APP_TEXT.admin.edit, icon: - Sync + {APP_TEXT.admin.sync} {photoNeedsToBeSynced(photo) && [] = useMemo(() => [{ - label: 'Delete', + label: APP_TEXT.admin.delete, icon: [ - 'Shot on', - formatCameraText(cameraFromPhoto(photos[0], camera)), + 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[], diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index 5c6dd3ba..e9e18f6c 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -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" diff --git a/src/components/ImageInput.tsx b/src/components/ImageInput.tsx index 44c50d22..9b25ee3f 100644 --- a/src/components/ImageInput.tsx +++ b/src/components/ImageInput.tsx @@ -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} } - `Photos shot on ${labelForFilm(film).large}`; + APP_TEXT.category.filmShare(labelForFilm(film).large); export const descriptionForFilmPhotos = ( photos: Photo[], diff --git a/src/focal/index.ts b/src/focal/index.ts index 609a1836..ee5569e5 100644 --- a/src/focal/index.ts +++ b/src/focal/index.ts @@ -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[], diff --git a/src/i18n/locales/pt-br.ts b/src/i18n/locales/pt-br.ts index 82d55214..77772584 100644 --- a/src/i18n/locales/pt-br.ts +++ b/src/i18n/locales/pt-br.ts @@ -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; diff --git a/src/i18n/locales/us-en.ts b/src/i18n/locales/us-en.ts index a468c412..0182bfe5 100644 --- a/src/i18n/locales/us-en.ts +++ b/src/i18n/locales/us-en.ts @@ -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`, }, - paginate: ( - index: number, - count: number, - verb?: string, - ) => verb - ? `${verb} ${index} of ${count}` - : `${index} of ${count}`, + utility: { + paginate: ( + index: number, + count: number, + action?: string, + ) => action + ? `${action} ${index} of ${count}` + : `${index} of ${count}`, + }, + dateLocale: enUS, }; export default TEXT; diff --git a/src/lens/meta.ts b/src/lens/meta.ts index 6b7036ac..f7af2167 100644 --- a/src/lens/meta.ts +++ b/src/lens/meta.ts @@ -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(' '); diff --git a/src/photo/PhotoHeader.tsx b/src/photo/PhotoHeader.tsx index f3199671..9f639099 100644 --- a/src/photo/PhotoHeader.tsx +++ b/src/photo/PhotoHeader.tsx @@ -155,9 +155,13 @@ export default function PhotoHeader({ }} />} : - {APP_TEXT.paginate( + {APP_TEXT.utility.paginate( paginationIndex, paginationCount, entityVerb)} diff --git a/src/photo/index.ts b/src/photo/index.ts index c12b4a76..cce0340f 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -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 = ( diff --git a/src/recipe/PhotoRecipeOverlay.tsx b/src/recipe/PhotoRecipeOverlay.tsx index 4030d506..a26b44b8 100644 --- a/src/recipe/PhotoRecipeOverlay.tsx +++ b/src/recipe/PhotoRecipeOverlay.tsx @@ -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" /> diff --git a/src/recipe/index.ts b/src/recipe/index.ts index e7beba4c..84477358 100644 --- a/src/recipe/index.ts +++ b/src/recipe/index.ts @@ -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[] = [], diff --git a/src/share/ShareModal.tsx b/src/share/ShareModal.tsx index 12d5edba..3ae0fec5 100644 --- a/src/share/ShareModal.tsx +++ b/src/share/ShareModal.tsx @@ -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({ , () => { navigator.clipboard.writeText(pathShare); - toastSuccess('Link to photo copied'); + toastSuccess(APP_TEXT.photo.copied); }, true, )} diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index 635ac246..a449c4ca 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -26,7 +26,7 @@ export default function TagHeader({ entity={isTagFavs(tag) ? : } - entityVerb={APP_TEXT.category.tagged} + entityVerb={APP_TEXT.category.taggedPhotos} entityDescription={descriptionForTaggedPhotos(photos, undefined, count)} photos={photos} selectedPhoto={selectedPhoto} diff --git a/src/tag/index.ts b/src/tag/index.ts index cda86051..476e6a8f 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -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), })); diff --git a/src/utility/date.ts b/src/utility/date.ts index f7d34acd..962d6291 100644 --- a/src/utility/date.ts +++ b/src/utility/date.ts @@ -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) =>