Integrate basic I18N text
This commit is contained in:
parent
f0a90172f5
commit
24a2877d82
@ -257,12 +257,13 @@ Vercel Postgres can be switched to another Postgres-compatible, pooling provider
|
|||||||
💬 I18N
|
💬 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
|
### Supported Languages
|
||||||
- `ES_ES`
|
- `US_EN`
|
||||||
- `PT_BR`
|
- `ES_ES` (coming soon)
|
||||||
- `PT_PT`
|
- `PT_BR` (coming soon)
|
||||||
|
- `PT_PT` (coming soon)
|
||||||
|
|
||||||
📖 FAQ
|
📖 FAQ
|
||||||
-
|
-
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { clsx } from 'clsx/lite';
|
|||||||
import AppGrid from '../components/AppGrid';
|
import AppGrid from '../components/AppGrid';
|
||||||
import ThemeSwitcher from '@/app/ThemeSwitcher';
|
import ThemeSwitcher from '@/app/ThemeSwitcher';
|
||||||
import Link from 'next/link';
|
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 RepoLink from '../components/RepoLink';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths';
|
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths';
|
||||||
@ -51,7 +51,7 @@ export default function Footer() {
|
|||||||
<form action={() => signOutAction()
|
<form action={() => signOutAction()
|
||||||
.then(clearAuthStateAndRedirectIfNecessary)}>
|
.then(clearAuthStateAndRedirectIfNecessary)}>
|
||||||
<SubmitButtonWithStatus styleAs="link">
|
<SubmitButtonWithStatus styleAs="link">
|
||||||
Sign out
|
{APP_TEXT.auth.signOut}
|
||||||
</SubmitButtonWithStatus>
|
</SubmitButtonWithStatus>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
@ -60,7 +60,7 @@ export default function Footer() {
|
|||||||
: SHOW_REPO_LINK
|
: SHOW_REPO_LINK
|
||||||
? <RepoLink />
|
? <RepoLink />
|
||||||
: <Link href={PATH_ADMIN_PHOTOS}>
|
: <Link href={PATH_ADMIN_PHOTOS}>
|
||||||
Admin
|
{APP_TEXT.footer.admin}
|
||||||
</Link>}
|
</Link>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center h-10">
|
<div className="flex items-center h-10">
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useTheme } from 'next-themes';
|
|||||||
import Switcher from '@/components/Switcher';
|
import Switcher from '@/components/Switcher';
|
||||||
import SwitcherItem from '@/components/SwitcherItem';
|
import SwitcherItem from '@/components/SwitcherItem';
|
||||||
import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
|
import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
|
||||||
|
import { APP_TEXT } from './config';
|
||||||
|
|
||||||
export default function ThemeSwitcher () {
|
export default function ThemeSwitcher () {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
@ -25,19 +26,19 @@ export default function ThemeSwitcher () {
|
|||||||
icon={<BiDesktop size={16} />}
|
icon={<BiDesktop size={16} />}
|
||||||
onClick={() => setTheme('system')}
|
onClick={() => setTheme('system')}
|
||||||
active={theme === 'system'}
|
active={theme === 'system'}
|
||||||
tooltip={{ content: 'System' }}
|
tooltip={{ content: APP_TEXT.footer.system }}
|
||||||
/>
|
/>
|
||||||
<SwitcherItem
|
<SwitcherItem
|
||||||
icon={<BiSun size={18} />}
|
icon={<BiSun size={18} />}
|
||||||
onClick={() => setTheme('light')}
|
onClick={() => setTheme('light')}
|
||||||
active={theme === 'light'}
|
active={theme === 'light'}
|
||||||
tooltip={{ content: 'Light Mode' }}
|
tooltip={{ content: APP_TEXT.footer.light }}
|
||||||
/>
|
/>
|
||||||
<SwitcherItem
|
<SwitcherItem
|
||||||
icon={<BiMoon size={16} />}
|
icon={<BiMoon size={16} />}
|
||||||
onClick={() => setTheme('dark')}
|
onClick={() => setTheme('dark')}
|
||||||
active={theme === 'dark'}
|
active={theme === 'dark'}
|
||||||
tooltip={{ content: 'Dark Mode' }}
|
tooltip={{ content: APP_TEXT.footer.dark }}
|
||||||
/>
|
/>
|
||||||
</Switcher>
|
</Switcher>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
import { getOrderedCategoriesFromString } from '@/category';
|
import { getOrderedCategoriesFromString } from '@/category';
|
||||||
import type { StorageType } from '@/platforms/storage';
|
import type { StorageType } from '@/platforms/storage';
|
||||||
import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
|
import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
|
||||||
import { getContentForLanguage } from '@/i18n';
|
import { getTextForLanguage } from '@/i18n';
|
||||||
|
|
||||||
// HARD-CODED GLOBAL CONFIGURATION
|
// HARD-CODED GLOBAL CONFIGURATION
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ const SITE_DOMAIN_SHORT = shortenUrl(SITE_DOMAIN);
|
|||||||
|
|
||||||
// SITE META
|
// SITE META
|
||||||
|
|
||||||
export const APP_TEXT = await getContentForLanguage(
|
export const APP_TEXT = await getTextForLanguage(
|
||||||
process.env.NEXT_PUBLIC_LANGUAGE,
|
process.env.NEXT_PUBLIC_LANGUAGE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { useAppState } from '@/state/AppState';
|
|||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
||||||
import IconLock from '@/components/icons/IconLock';
|
import IconLock from '@/components/icons/IconLock';
|
||||||
|
import { APP_TEXT } from '@/app/config';
|
||||||
|
|
||||||
export default function SignInForm({
|
export default function SignInForm({
|
||||||
includeTitle = true,
|
includeTitle = true,
|
||||||
@ -79,27 +80,27 @@ export default function SignInForm({
|
|||||||
)}>
|
)}>
|
||||||
<IconLock className="text-main translate-y-[0.5px]" />
|
<IconLock className="text-main translate-y-[0.5px]" />
|
||||||
<span className="text-main">
|
<span className="text-main">
|
||||||
Sign in
|
{APP_TEXT.auth.signIn}
|
||||||
</span>
|
</span>
|
||||||
</h1>}
|
</h1>}
|
||||||
<form action={action} className="w-full">
|
<form action={action} className="w-full">
|
||||||
<div className="space-y-5 w-full -translate-y-0.5">
|
<div className="space-y-5 w-full -translate-y-0.5">
|
||||||
{response === KEY_CREDENTIALS_SIGN_IN_ERROR &&
|
{response === KEY_CREDENTIALS_SIGN_IN_ERROR &&
|
||||||
<ErrorNote>
|
<ErrorNote>
|
||||||
Invalid email/password
|
{APP_TEXT.auth.invalidEmailPassword}
|
||||||
</ErrorNote>}
|
</ErrorNote>}
|
||||||
<div className="space-y-4 w-full">
|
<div className="space-y-4 w-full">
|
||||||
<FieldSetWithStatus
|
<FieldSetWithStatus
|
||||||
id="email"
|
id="email"
|
||||||
inputRef={emailRef}
|
inputRef={emailRef}
|
||||||
label="Admin Email"
|
label={APP_TEXT.auth.email}
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={setEmail}
|
onChange={setEmail}
|
||||||
/>
|
/>
|
||||||
<FieldSetWithStatus
|
<FieldSetWithStatus
|
||||||
id="password"
|
id="password"
|
||||||
label="Admin Password"
|
label={APP_TEXT.auth.password}
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={setPassword}
|
onChange={setPassword}
|
||||||
@ -112,7 +113,7 @@ export default function SignInForm({
|
|||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
<SubmitButtonWithStatus disabled={!isFormValid}>
|
<SubmitButtonWithStatus disabled={!isFormValid}>
|
||||||
Sign in
|
{APP_TEXT.auth.signIn}
|
||||||
</SubmitButtonWithStatus>
|
</SubmitButtonWithStatus>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -52,7 +52,11 @@ import { FaCheck } from 'react-icons/fa6';
|
|||||||
import { addHiddenToTags, formatTag, isTagFavs, isTagHidden } from '@/tag';
|
import { addHiddenToTags, formatTag, isTagFavs, isTagHidden } from '@/tag';
|
||||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||||
import CommandKItem from './CommandKItem';
|
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 { DialogDescription, DialogTitle } from '@radix-ui/react-dialog';
|
||||||
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
|
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
|
||||||
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
|
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
|
||||||
@ -285,7 +289,7 @@ export default function CommandKClient({
|
|||||||
.map(category => {
|
.map(category => {
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case 'cameras': return {
|
case 'cameras': return {
|
||||||
heading: 'Cameras',
|
heading: APP_TEXT.category.cameraPlural,
|
||||||
accessory: <IconCamera size={14} />,
|
accessory: <IconCamera size={14} />,
|
||||||
items: cameras.map(({ camera, count }) => ({
|
items: cameras.map(({ camera, count }) => ({
|
||||||
label: formatCameraText(camera),
|
label: formatCameraText(camera),
|
||||||
@ -295,7 +299,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'lenses': return {
|
case 'lenses': return {
|
||||||
heading: 'Lenses',
|
heading: APP_TEXT.category.lensPlural,
|
||||||
accessory: <IconLens size={14} className="translate-y-[0.5px]" />,
|
accessory: <IconLens size={14} className="translate-y-[0.5px]" />,
|
||||||
items: lenses.map(({ lens, count }) => ({
|
items: lenses.map(({ lens, count }) => ({
|
||||||
label: formatLensText(lens, 'medium'),
|
label: formatLensText(lens, 'medium'),
|
||||||
@ -306,7 +310,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'tags': return {
|
case 'tags': return {
|
||||||
heading: 'Tags',
|
heading: APP_TEXT.category.tagPlural,
|
||||||
accessory: <IconTag
|
accessory: <IconTag
|
||||||
size={13}
|
size={13}
|
||||||
className="translate-x-[1px] translate-y-[0.75px]"
|
className="translate-x-[1px] translate-y-[0.75px]"
|
||||||
@ -333,7 +337,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'recipes': return {
|
case 'recipes': return {
|
||||||
heading: 'Recipes',
|
heading: APP_TEXT.category.recipePlural,
|
||||||
accessory: <IconRecipe
|
accessory: <IconRecipe
|
||||||
size={15}
|
size={15}
|
||||||
className="translate-x-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
@ -346,7 +350,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'films': return {
|
case 'films': return {
|
||||||
heading: 'Films',
|
heading: APP_TEXT.category.filmPlural,
|
||||||
accessory: <IconFilm size={14} />,
|
accessory: <IconFilm size={14} />,
|
||||||
items: films.map(({ film, count }) => ({
|
items: films.map(({ film, count }) => ({
|
||||||
label: labelForFilm(film).medium,
|
label: labelForFilm(film).medium,
|
||||||
@ -356,7 +360,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'focal-lengths': return {
|
case 'focal-lengths': return {
|
||||||
heading: 'Focal Lengths',
|
heading: APP_TEXT.category.focalLengthPlural,
|
||||||
accessory: <IconFocalLength className="text-[14px]" />,
|
accessory: <IconFocalLength className="text-[14px]" />,
|
||||||
items: focalLengths.map(({ focal, count }) => ({
|
items: focalLengths.map(({ focal, count }) => ({
|
||||||
label: formatFocalLength(focal)!,
|
label: formatFocalLength(focal)!,
|
||||||
@ -588,7 +592,7 @@ export default function CommandKClient({
|
|||||||
'focus:outline-hidden',
|
'focus:outline-hidden',
|
||||||
isPending && 'opacity-20',
|
isPending && 'opacity-20',
|
||||||
)}
|
)}
|
||||||
placeholder="Search photos, views, settings ..."
|
placeholder={APP_TEXT.cmdk.placeholder}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
{isLoading && !isPending &&
|
{isLoading && !isPending &&
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { downloadFileNameForPhoto, Photo } from '@/photo';
|
|||||||
import LoaderButton from './primitives/LoaderButton';
|
import LoaderButton from './primitives/LoaderButton';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { downloadFileFromBrowser } from '@/utility/url';
|
import { downloadFileFromBrowser } from '@/utility/url';
|
||||||
|
import { APP_TEXT } from '@/app/config';
|
||||||
|
|
||||||
export default function DownloadButton({
|
export default function DownloadButton({
|
||||||
photo,
|
photo,
|
||||||
@ -16,7 +17,7 @@ export default function DownloadButton({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
tooltip="Download Original File"
|
tooltip={APP_TEXT.tooltip.download}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
'text-medium',
|
'text-medium',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TEMPLATE_REPO_NAME, TEMPLATE_REPO_URL } from '@/app/config';
|
import { APP_TEXT, TEMPLATE_REPO_NAME, TEMPLATE_REPO_URL } from '@/app/config';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { BiLogoGithub } from 'react-icons/bi';
|
import { BiLogoGithub } from 'react-icons/bi';
|
||||||
@ -7,7 +7,7 @@ export default function RepoLink() {
|
|||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-2 whitespace-nowrap">
|
<span className="inline-flex items-center gap-2 whitespace-nowrap">
|
||||||
<span className="hidden sm:inline-block">
|
<span className="hidden sm:inline-block">
|
||||||
Made with
|
{APP_TEXT.footer.repo}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href={TEMPLATE_REPO_URL}
|
href={TEMPLATE_REPO_URL}
|
||||||
|
|||||||
@ -2,16 +2,29 @@ import US_EN from './languages/us-en';
|
|||||||
|
|
||||||
export type I18N = typeof US_EN;
|
export type I18N = typeof US_EN;
|
||||||
|
|
||||||
|
export type I18NDeepPartial = {
|
||||||
|
[key in keyof I18N]?: Partial<I18N[key]>;
|
||||||
|
}
|
||||||
|
|
||||||
export const LANGUAGES: Record<
|
export const LANGUAGES: Record<
|
||||||
string,
|
string,
|
||||||
(() => Promise<Partial<I18N>>) | undefined
|
(() => Promise<I18NDeepPartial>) | undefined
|
||||||
> = {
|
> = {
|
||||||
'pt-br': () => import('./languages/pt-br').then(module => module.default),
|
'pt-br': () => import('./languages/pt-br').then(module => module.default),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContentForLanguage = async (
|
export const getTextForLanguage = async (
|
||||||
language = '',
|
language = '',
|
||||||
): Promise<I18N> => ({
|
): Promise<I18N> => {
|
||||||
...US_EN,
|
const text = US_EN;
|
||||||
...await LANGUAGES[language.toLocaleLowerCase()]?.(),
|
|
||||||
});
|
Object.entries(await LANGUAGES[language.toLocaleLowerCase()]?.() ?? {})
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
text[key as keyof I18N] = {
|
||||||
|
...text[key as keyof I18N],
|
||||||
|
...value,
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { I18N } from '..';
|
import { I18NDeepPartial } from '..';
|
||||||
|
|
||||||
const language: Partial<I18N> = {
|
const TEXT: I18NDeepPartial = {
|
||||||
core: {
|
core: {
|
||||||
photo: 'Foto',
|
photo: 'Foto',
|
||||||
photoPlural: 'Fotos',
|
photoPlural: 'Fotos',
|
||||||
@ -16,6 +16,45 @@ const language: Partial<I18N> = {
|
|||||||
next: 'Próximo',
|
next: 'Próximo',
|
||||||
nextShort: 'Prox',
|
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;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const language = {
|
const TEXT = {
|
||||||
core: {
|
core: {
|
||||||
photo: 'Photo',
|
photo: 'Photo',
|
||||||
photoPlural: 'Photos',
|
photoPlural: 'Photos',
|
||||||
@ -14,7 +14,17 @@ const language = {
|
|||||||
next: 'Next',
|
next: 'Next',
|
||||||
nextShort: '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',
|
camera: 'Camera',
|
||||||
cameraPlural: 'Cameras',
|
cameraPlural: 'Cameras',
|
||||||
lens: 'Lens',
|
lens: 'Lens',
|
||||||
@ -28,25 +38,21 @@ const language = {
|
|||||||
focalLength: 'Focal Length',
|
focalLength: 'Focal Length',
|
||||||
focalLengthPlural: 'Focal Lengths',
|
focalLengthPlural: 'Focal Lengths',
|
||||||
},
|
},
|
||||||
footer: {
|
|
||||||
repo: 'Made with',
|
|
||||||
system: 'System',
|
|
||||||
light: 'Light',
|
|
||||||
dark: 'Dark',
|
|
||||||
},
|
|
||||||
auth: {
|
auth: {
|
||||||
signIn: 'Sign in',
|
signIn: 'Sign in',
|
||||||
signOut: 'Sign out',
|
signOut: 'Sign out',
|
||||||
email: 'Admin Email',
|
email: 'Admin Email',
|
||||||
password: 'Admin Password',
|
password: 'Admin Password',
|
||||||
|
invalidEmailPassword: 'Invalid email/password',
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltip: {
|
||||||
'35mm': '35mm Equivalent',
|
'35mm': '35mm Equivalent',
|
||||||
imageViewer: 'Open Image Viewer',
|
zoom: 'Zoom In',
|
||||||
sharePhoto: 'Share Photo',
|
sharePhoto: 'Share Photo',
|
||||||
recipeInfo: 'Recipe Info',
|
recipeInfo: 'Recipe Info',
|
||||||
recipeCopy: 'Copy Recipe Text',
|
recipeCopy: 'Copy Recipe Text',
|
||||||
|
download: 'Download Original File',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default language;
|
export default TEXT;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import FavsTag from '../tag/FavsTag';
|
|||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import HiddenTag from '@/tag/HiddenTag';
|
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 { clsx } from 'clsx/lite';
|
||||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||||
import IconCamera from '@/components/icons/IconCamera';
|
import IconCamera from '@/components/icons/IconCamera';
|
||||||
@ -83,7 +83,7 @@ export default function PhotoGridSidebar({
|
|||||||
const camerasContent = cameras.length > 0
|
const camerasContent = cameras.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="cameras"
|
key="cameras"
|
||||||
title="Cameras"
|
title={APP_TEXT.category.cameraPlural}
|
||||||
icon={<IconCamera
|
icon={<IconCamera
|
||||||
size={15}
|
size={15}
|
||||||
className="translate-x-[0.5px]"
|
className="translate-x-[0.5px]"
|
||||||
@ -107,7 +107,7 @@ export default function PhotoGridSidebar({
|
|||||||
const lensesContent = lenses.length > 0
|
const lensesContent = lenses.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="lenses"
|
key="lenses"
|
||||||
title="Lenses"
|
title={APP_TEXT.category.lensPlural}
|
||||||
icon={<IconLens size={15} />}
|
icon={<IconLens size={15} />}
|
||||||
maxItems={maxItemsPerCategory}
|
maxItems={maxItemsPerCategory}
|
||||||
items={lenses
|
items={lenses
|
||||||
@ -127,7 +127,7 @@ export default function PhotoGridSidebar({
|
|||||||
const tagsContent = tags.length > 0
|
const tagsContent = tags.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="tags"
|
key="tags"
|
||||||
title='Tags'
|
title={APP_TEXT.category.tagPlural}
|
||||||
icon={<IconTag
|
icon={<IconTag
|
||||||
size={14}
|
size={14}
|
||||||
className="translate-x-[1px] translate-y-[1px]"
|
className="translate-x-[1px] translate-y-[1px]"
|
||||||
@ -172,7 +172,7 @@ export default function PhotoGridSidebar({
|
|||||||
const recipesContent = recipes.length > 0
|
const recipesContent = recipes.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="recipes"
|
key="recipes"
|
||||||
title="Recipes"
|
title={APP_TEXT.category.recipePlural}
|
||||||
icon={<IconRecipe
|
icon={<IconRecipe
|
||||||
size={16}
|
size={16}
|
||||||
className="translate-x-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
@ -195,7 +195,7 @@ export default function PhotoGridSidebar({
|
|||||||
const filmsContent = films.length > 0
|
const filmsContent = films.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="films"
|
key="films"
|
||||||
title="Films"
|
title={APP_TEXT.category.filmPlural}
|
||||||
icon={<IconFilm size={15} />}
|
icon={<IconFilm size={15} />}
|
||||||
maxItems={maxItemsPerCategory}
|
maxItems={maxItemsPerCategory}
|
||||||
items={films
|
items={films
|
||||||
@ -213,7 +213,7 @@ export default function PhotoGridSidebar({
|
|||||||
const focalLengthsContent = focalLengths.length > 0
|
const focalLengthsContent = focalLengths.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="focal-lengths"
|
key="focal-lengths"
|
||||||
title="Focal Lengths"
|
title={APP_TEXT.category.focalLengthPlural}
|
||||||
icon={<IconFocalLength size={13} />}
|
icon={<IconFocalLength size={13} />}
|
||||||
maxItems={maxItemsPerCategory}
|
maxItems={maxItemsPerCategory}
|
||||||
items={focalLengths.map(({ focal, count }) =>
|
items={focalLengths.map(({ focal, count }) =>
|
||||||
|
|||||||
@ -17,12 +17,13 @@ import PhotoLink from './PhotoLink';
|
|||||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { GRID_GAP_CLASSNAME } from '@/components';
|
import { GRID_GAP_CLASSNAME } from '@/components';
|
||||||
|
import { APP_TEXT } from '@/app/config';
|
||||||
|
|
||||||
export default function PhotoHeader({
|
export default function PhotoHeader({
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
entity,
|
entity,
|
||||||
entityVerb = 'PHOTO',
|
entityVerb = APP_TEXT.core.photo.toLocaleUpperCase(),
|
||||||
entityDescription,
|
entityDescription,
|
||||||
indexNumber,
|
indexNumber,
|
||||||
count,
|
count,
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
SHOW_TAKEN_AT_TIME,
|
SHOW_TAKEN_AT_TIME,
|
||||||
MATTE_COLOR,
|
MATTE_COLOR,
|
||||||
MATTE_COLOR_DARK,
|
MATTE_COLOR_DARK,
|
||||||
|
APP_TEXT,
|
||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
|
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
|
||||||
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
||||||
@ -378,7 +379,7 @@ export default function PhotoLarge({
|
|||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="35mm equivalent"
|
content={APP_TEXT.tooltip['35mm']}
|
||||||
sideOffset={3}
|
sideOffset={3}
|
||||||
supportMobile
|
supportMobile
|
||||||
>
|
>
|
||||||
@ -434,7 +435,7 @@ export default function PhotoLarge({
|
|||||||
)}>
|
)}>
|
||||||
{showZoomControls &&
|
{showZoomControls &&
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
tooltip="Zoom In"
|
tooltip={APP_TEXT.tooltip.zoom}
|
||||||
icon={<LuExpand size={15} />}
|
icon={<LuExpand size={15} />}
|
||||||
onClick={() => refZoomControls.current?.open()}
|
onClick={() => refZoomControls.current?.open()}
|
||||||
styleAs="link"
|
styleAs="link"
|
||||||
@ -443,7 +444,7 @@ export default function PhotoLarge({
|
|||||||
/>}
|
/>}
|
||||||
{shouldShare &&
|
{shouldShare &&
|
||||||
<ShareButton
|
<ShareButton
|
||||||
tooltip="Share Photo"
|
tooltip={APP_TEXT.tooltip.sharePhoto}
|
||||||
photo={photo}
|
photo={photo}
|
||||||
tag={shouldShareTag
|
tag={shouldShareTag
|
||||||
? primaryTag
|
? primaryTag
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { isPhotoFav } from '@/tag';
|
|||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import {
|
import {
|
||||||
ALLOW_PUBLIC_DOWNLOADS,
|
ALLOW_PUBLIC_DOWNLOADS,
|
||||||
|
APP_TEXT,
|
||||||
SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
|
SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
|
||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
import { downloadFileFromBrowser } from '@/utility/url';
|
import { downloadFileFromBrowser } from '@/utility/url';
|
||||||
@ -200,7 +201,7 @@ export default function PhotoPrevNextActions({
|
|||||||
'*:select-none',
|
'*:select-none',
|
||||||
)}>
|
)}>
|
||||||
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: 'Previous',
|
content: APP_TEXT.nav.prev,
|
||||||
keyCommand: KEY_COMMANDS.prev[0],
|
keyCommand: KEY_COMMANDS.prev[0],
|
||||||
}}>
|
}}>
|
||||||
<PhotoLink
|
<PhotoLink
|
||||||
@ -213,14 +214,16 @@ export default function PhotoPrevNextActions({
|
|||||||
prefetch
|
prefetch
|
||||||
>
|
>
|
||||||
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
|
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
|
||||||
<span className="hidden sm:inline-block">PREV</span>
|
<span className="hidden sm:inline-block uppercase">
|
||||||
|
{APP_TEXT.nav.prevShort}
|
||||||
|
</span>
|
||||||
</PhotoLink>
|
</PhotoLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-extra-extra-dim">
|
<span className="text-extra-extra-dim">
|
||||||
/
|
/
|
||||||
</span>
|
</span>
|
||||||
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: 'Next',
|
content: APP_TEXT.nav.next,
|
||||||
keyCommand: KEY_COMMANDS.next[0],
|
keyCommand: KEY_COMMANDS.next[0],
|
||||||
}}>
|
}}>
|
||||||
<PhotoLink
|
<PhotoLink
|
||||||
@ -233,7 +236,9 @@ export default function PhotoPrevNextActions({
|
|||||||
prefetch
|
prefetch
|
||||||
>
|
>
|
||||||
<FiChevronRight className="sm:hidden text-[1.1rem]" />
|
<FiChevronRight className="sm:hidden text-[1.1rem]" />
|
||||||
<span className="hidden sm:inline-block">NEXT</span>
|
<span className="hidden sm:inline-block uppercase">
|
||||||
|
{APP_TEXT.nav.nextShort}
|
||||||
|
</span>
|
||||||
</PhotoLink>
|
</PhotoLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import clsx from 'clsx/lite';
|
|||||||
import { FaPlus } from 'react-icons/fa6';
|
import { FaPlus } from 'react-icons/fa6';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { APP_TEXT } from '@/app/config';
|
||||||
|
|
||||||
export default function PhotoRecipeOverlayButton({
|
export default function PhotoRecipeOverlayButton({
|
||||||
className,
|
className,
|
||||||
@ -17,7 +18,7 @@ export default function PhotoRecipeOverlayButton({
|
|||||||
const ref = useRef<HTMLButtonElement>(null);
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="Recipe Info">
|
<Tooltip content={APP_TEXT.tooltip.recipeInfo}>
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user