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() {
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}
}
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