diff --git a/README.md b/README.md index 27557abd..8a84fb2c 100644 --- a/README.md +++ b/README.md @@ -257,12 +257,13 @@ Vercel Postgres can be switched to another Postgres-compatible, pooling provider 💬   I18N - -Partial internationalization (non-admin, user-facing text) provided for a handful of languages. If you'd like to add support for a new language, [open a PR](https://github.com/sambecker/exif-photo-blog/compare) using [`ES_EN`](https://github.com/sambecker/exif-photo-blog) for reference. +Partial internationalization (non-admin, user-facing text) provided for a handful of languages. If you'd like to add support for a new language, open a PR [using `US_EN`](https://github.com/sambecker/exif-photo-blog/main/src/i18n/languages/us-en.ts) for reference. ### Supported Languages -- `ES_ES` -- `PT_BR` -- `PT_PT` +- `US_EN` +- `ES_ES` (coming soon) +- `PT_BR` (coming soon) +- `PT_PT` (coming soon) 📖  FAQ - diff --git a/src/app/Footer.tsx b/src/app/Footer.tsx index baebe14e..c2cc5ede 100644 --- a/src/app/Footer.tsx +++ b/src/app/Footer.tsx @@ -4,7 +4,7 @@ import { clsx } from 'clsx/lite'; import AppGrid from '../components/AppGrid'; import ThemeSwitcher from '@/app/ThemeSwitcher'; import Link from 'next/link'; -import { SHOW_REPO_LINK } from '@/app/config'; +import { APP_TEXT, SHOW_REPO_LINK } from '@/app/config'; import RepoLink from '../components/RepoLink'; import { usePathname } from 'next/navigation'; import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths'; @@ -51,7 +51,7 @@ export default function Footer() {
signOutAction() .then(clearAuthStateAndRedirectIfNecessary)}> - Sign out + {APP_TEXT.auth.signOut}
@@ -60,7 +60,7 @@ export default function Footer() { : SHOW_REPO_LINK ? : - Admin + {APP_TEXT.footer.admin} }
diff --git a/src/app/ThemeSwitcher.tsx b/src/app/ThemeSwitcher.tsx index 6ef0d021..d0975cc3 100644 --- a/src/app/ThemeSwitcher.tsx +++ b/src/app/ThemeSwitcher.tsx @@ -5,6 +5,7 @@ import { useTheme } from 'next-themes'; import Switcher from '@/components/Switcher'; import SwitcherItem from '@/components/SwitcherItem'; import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi'; +import { APP_TEXT } from './config'; export default function ThemeSwitcher () { const [mounted, setMounted] = useState(false); @@ -25,19 +26,19 @@ export default function ThemeSwitcher () { icon={} onClick={() => setTheme('system')} active={theme === 'system'} - tooltip={{ content: 'System' }} + tooltip={{ content: APP_TEXT.footer.system }} /> } onClick={() => setTheme('light')} active={theme === 'light'} - tooltip={{ content: 'Light Mode' }} + tooltip={{ content: APP_TEXT.footer.light }} /> } onClick={() => setTheme('dark')} active={theme === 'dark'} - tooltip={{ content: 'Dark Mode' }} + tooltip={{ content: APP_TEXT.footer.dark }} /> ); diff --git a/src/app/config.ts b/src/app/config.ts index eef94bf4..856a0d41 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -5,7 +5,7 @@ import { import { getOrderedCategoriesFromString } from '@/category'; import type { StorageType } from '@/platforms/storage'; import { makeUrlAbsolute, shortenUrl } from '@/utility/url'; -import { getContentForLanguage } from '@/i18n'; +import { getTextForLanguage } from '@/i18n'; // HARD-CODED GLOBAL CONFIGURATION @@ -99,7 +99,7 @@ const SITE_DOMAIN_SHORT = shortenUrl(SITE_DOMAIN); // SITE META -export const APP_TEXT = await getContentForLanguage( +export const APP_TEXT = await getTextForLanguage( process.env.NEXT_PUBLIC_LANGUAGE, ); diff --git a/src/auth/SignInForm.tsx b/src/auth/SignInForm.tsx index 4fda5f64..70ee6d1b 100644 --- a/src/auth/SignInForm.tsx +++ b/src/auth/SignInForm.tsx @@ -21,6 +21,7 @@ import { useAppState } from '@/state/AppState'; import { clsx } from 'clsx/lite'; import { PATH_ADMIN_PHOTOS } from '@/app/paths'; import IconLock from '@/components/icons/IconLock'; +import { APP_TEXT } from '@/app/config'; export default function SignInForm({ includeTitle = true, @@ -79,27 +80,27 @@ export default function SignInForm({ )}> - Sign in + {APP_TEXT.auth.signIn} }
{response === KEY_CREDENTIALS_SIGN_IN_ERROR && - Invalid email/password + {APP_TEXT.auth.invalidEmailPassword} }
}
- Sign in + {APP_TEXT.auth.signIn}
diff --git a/src/cmdk/CommandKClient.tsx b/src/cmdk/CommandKClient.tsx index 5b81f1ca..4c261ecf 100644 --- a/src/cmdk/CommandKClient.tsx +++ b/src/cmdk/CommandKClient.tsx @@ -52,7 +52,11 @@ import { FaCheck } from 'react-icons/fa6'; import { addHiddenToTags, formatTag, isTagFavs, isTagHidden } from '@/tag'; import { formatCount, formatCountDescriptive } from '@/utility/string'; import CommandKItem from './CommandKItem'; -import { CATEGORY_VISIBILITY, GRID_HOMEPAGE_ENABLED } from '@/app/config'; +import { + APP_TEXT, + CATEGORY_VISIBILITY, + GRID_HOMEPAGE_ENABLED, +} from '@/app/config'; import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog'; import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot'; @@ -285,7 +289,7 @@ export default function CommandKClient({ .map(category => { switch (category) { case 'cameras': return { - heading: 'Cameras', + heading: APP_TEXT.category.cameraPlural, accessory: , items: cameras.map(({ camera, count }) => ({ label: formatCameraText(camera), @@ -295,7 +299,7 @@ export default function CommandKClient({ })), }; case 'lenses': return { - heading: 'Lenses', + heading: APP_TEXT.category.lensPlural, accessory: , items: lenses.map(({ lens, count }) => ({ label: formatLensText(lens, 'medium'), @@ -306,7 +310,7 @@ export default function CommandKClient({ })), }; case 'tags': return { - heading: 'Tags', + heading: APP_TEXT.category.tagPlural, accessory: , items: films.map(({ film, count }) => ({ label: labelForFilm(film).medium, @@ -356,7 +360,7 @@ export default function CommandKClient({ })), }; case 'focal-lengths': return { - heading: 'Focal Lengths', + heading: APP_TEXT.category.focalLengthPlural, accessory: , items: focalLengths.map(({ focal, count }) => ({ label: formatFocalLength(focal)!, @@ -588,7 +592,7 @@ export default function CommandKClient({ 'focus:outline-hidden', isPending && 'opacity-20', )} - placeholder="Search photos, views, settings ..." + placeholder={APP_TEXT.cmdk.placeholder} disabled={isPending} /> {isLoading && !isPending && diff --git a/src/components/DownloadButton.tsx b/src/components/DownloadButton.tsx index ed45dea6..677e695c 100644 --- a/src/components/DownloadButton.tsx +++ b/src/components/DownloadButton.tsx @@ -4,6 +4,7 @@ import { downloadFileNameForPhoto, Photo } from '@/photo'; import LoaderButton from './primitives/LoaderButton'; import { useState } from 'react'; import { downloadFileFromBrowser } from '@/utility/url'; +import { APP_TEXT } from '@/app/config'; export default function DownloadButton({ photo, @@ -16,7 +17,7 @@ export default function DownloadButton({ return ( - Made with + {APP_TEXT.footer.repo} ; +} + export const LANGUAGES: Record< string, - (() => Promise>) | undefined + (() => Promise) | undefined > = { 'pt-br': () => import('./languages/pt-br').then(module => module.default), }; -export const getContentForLanguage = async ( +export const getTextForLanguage = async ( language = '', -): Promise => ({ - ...US_EN, - ...await LANGUAGES[language.toLocaleLowerCase()]?.(), -}); +): Promise => { + const text = US_EN; + + Object.entries(await LANGUAGES[language.toLocaleLowerCase()]?.() ?? {}) + .forEach(([key, value]) => { + text[key as keyof I18N] = { + ...text[key as keyof I18N], + ...value, + } as any; + }); + + return text; +}; diff --git a/src/i18n/languages/pt-br.ts b/src/i18n/languages/pt-br.ts index b0473f29..2785cac8 100644 --- a/src/i18n/languages/pt-br.ts +++ b/src/i18n/languages/pt-br.ts @@ -1,6 +1,6 @@ -import { I18N } from '..'; +import { I18NDeepPartial } from '..'; -const language: Partial = { +const TEXT: I18NDeepPartial = { core: { photo: 'Foto', photoPlural: 'Fotos', @@ -16,6 +16,45 @@ const language: Partial = { next: 'Próximo', nextShort: 'Prox', }, + footer: { + admin: 'Admin', + repo: 'Feito com', + system: 'Sistema', + light: 'Modo Claro', + dark: 'Modo Escuro', + }, + cmdk: { + placeholder: 'Pesquisar fotos, visualizações, configurações ...', + }, + category: { + camera: 'Câmera', + cameraPlural: 'Câmeras', + lens: 'Lente', + lensPlural: 'Lentes', + tag: 'Tag', + tagPlural: 'Tags', + recipe: 'Receita', + recipePlural: 'Receitas', + film: 'Filme', + filmPlural: 'Filmes', + focalLength: 'Distância Focal', + focalLengthPlural: 'Distâncias Focais', + }, + auth: { + signIn: 'Entrar', + signOut: 'Sair', + email: 'Email do Admin', + password: 'Senha do Admin', + invalidEmailPassword: 'Email/senha inválidos', + }, + tooltip: { + '35mm': 'Equivalente 35mm', + zoom: 'Aumentar Zoom', + sharePhoto: 'Compartilhar Foto', + recipeInfo: 'Informações da Receita', + recipeCopy: 'Copiar Texto da Receita', + download: 'Baixar Arquivo Original', + }, }; -export default language; +export default TEXT; diff --git a/src/i18n/languages/us-en.ts b/src/i18n/languages/us-en.ts index 86c99252..05e165c4 100644 --- a/src/i18n/languages/us-en.ts +++ b/src/i18n/languages/us-en.ts @@ -1,4 +1,4 @@ -const language = { +const TEXT = { core: { photo: 'Photo', photoPlural: 'Photos', @@ -14,7 +14,17 @@ const language = { next: 'Next', nextShort: 'Next', }, - categories: { + footer: { + admin: 'Admin', + repo: 'Made with', + system: 'System', + light: 'Light Mode', + dark: 'Dark Mode', + }, + cmdk: { + placeholder: 'Search photos, views, settings ...', + }, + category: { camera: 'Camera', cameraPlural: 'Cameras', lens: 'Lens', @@ -28,25 +38,21 @@ const language = { focalLength: 'Focal Length', focalLengthPlural: 'Focal Lengths', }, - footer: { - repo: 'Made with', - system: 'System', - light: 'Light', - dark: 'Dark', - }, auth: { signIn: 'Sign in', signOut: 'Sign out', email: 'Admin Email', password: 'Admin Password', + invalidEmailPassword: 'Invalid email/password', }, - tooltips: { + tooltip: { '35mm': '35mm Equivalent', - imageViewer: 'Open Image Viewer', + zoom: 'Zoom In', sharePhoto: 'Share Photo', recipeInfo: 'Recipe Info', recipeCopy: 'Copy Recipe Text', + download: 'Download Original File', }, }; -export default language; +export default TEXT; diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 4fe03c94..3273b0d2 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -10,7 +10,7 @@ import FavsTag from '../tag/FavsTag'; import { useAppState } from '@/state/AppState'; import { useMemo, useRef } from 'react'; import HiddenTag from '@/tag/HiddenTag'; -import { CATEGORY_VISIBILITY } from '@/app/config'; +import { APP_TEXT, CATEGORY_VISIBILITY } from '@/app/config'; import { clsx } from 'clsx/lite'; import PhotoRecipe from '@/recipe/PhotoRecipe'; import IconCamera from '@/components/icons/IconCamera'; @@ -83,7 +83,7 @@ export default function PhotoGridSidebar({ const camerasContent = cameras.length > 0 ? 0 ? } maxItems={maxItemsPerCategory} items={lenses @@ -127,7 +127,7 @@ export default function PhotoGridSidebar({ const tagsContent = tags.length > 0 ? 0 ? 0 ? } maxItems={maxItemsPerCategory} items={films @@ -213,7 +213,7 @@ export default function PhotoGridSidebar({ const focalLengthsContent = focalLengths.length > 0 ? } maxItems={maxItemsPerCategory} items={focalLengths.map(({ focal, count }) => diff --git a/src/photo/PhotoHeader.tsx b/src/photo/PhotoHeader.tsx index 68f386f6..b2215fca 100644 --- a/src/photo/PhotoHeader.tsx +++ b/src/photo/PhotoHeader.tsx @@ -17,12 +17,13 @@ import PhotoLink from './PhotoLink'; import ResponsiveText from '@/components/primitives/ResponsiveText'; import { useAppState } from '@/state/AppState'; import { GRID_GAP_CLASSNAME } from '@/components'; +import { APP_TEXT } from '@/app/config'; export default function PhotoHeader({ photos, selectedPhoto, entity, - entityVerb = 'PHOTO', + entityVerb = APP_TEXT.core.photo.toLocaleUpperCase(), entityDescription, indexNumber, count, diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 8225c799..adea75da 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -31,6 +31,7 @@ import { SHOW_TAKEN_AT_TIME, MATTE_COLOR, MATTE_COLOR_DARK, + APP_TEXT, } from '@/app/config'; import AdminPhotoMenu from '@/admin/AdminPhotoMenu'; import { RevalidatePhoto } from './InfinitePhotoScroll'; @@ -378,7 +379,7 @@ export default function PhotoLarge({ <> {' '} @@ -434,7 +435,7 @@ export default function PhotoLarge({ )}> {showZoomControls && } onClick={() => refZoomControls.current?.open()} styleAs="link" @@ -443,7 +444,7 @@ export default function PhotoLarge({ />} {shouldShare && - PREV + + {APP_TEXT.nav.prevShort} + / - NEXT + + {APP_TEXT.nav.nextShort} +
diff --git a/src/recipe/PhotoRecipeOverlayButton.tsx b/src/recipe/PhotoRecipeOverlayButton.tsx index b6a39df0..f1d213f2 100644 --- a/src/recipe/PhotoRecipeOverlayButton.tsx +++ b/src/recipe/PhotoRecipeOverlayButton.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx/lite'; import { FaPlus } from 'react-icons/fa6'; import Tooltip from '@/components/Tooltip'; import { useRef } from 'react'; +import { APP_TEXT } from '@/app/config'; export default function PhotoRecipeOverlayButton({ className, @@ -17,7 +18,7 @@ export default function PhotoRecipeOverlayButton({ const ref = useRef(null); return ( - +