Simplify I18N api

This commit is contained in:
Sam Becker 2025-05-12 17:46:22 -05:00
parent 7c6f4bfdeb
commit cd69e6e3ab
15 changed files with 63 additions and 66 deletions

View File

@ -9,7 +9,7 @@ import {
absolutePathForCamera, absolutePathForCamera,
absolutePathForCameraImage, absolutePathForCameraImage,
} from '@/app/paths'; } from '@/app/paths';
import { I18NState } from '@/i18n/state'; import { AppTextState } from '@/i18n/state';
// Meta functions moved to separate file to avoid // Meta functions moved to separate file to avoid
// dependencies (camelcase-keys) found in photo/index.ts // dependencies (camelcase-keys) found in photo/index.ts
@ -18,7 +18,7 @@ import { I18NState } from '@/i18n/state';
export const titleForCamera = ( export const titleForCamera = (
camera: Camera, camera: Camera,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
) => [ ) => [
appText.category.cameraTitle( appText.category.cameraTitle(
@ -30,7 +30,7 @@ export const titleForCamera = (
export const shareTextForCamera = ( export const shareTextForCamera = (
camera: Camera, camera: Camera,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
) => ) =>
appText.category.cameraShare( appText.category.cameraShare(
formatCameraText(cameraFromPhoto(photos[0], camera)), formatCameraText(cameraFromPhoto(photos[0], camera)),
@ -38,7 +38,7 @@ export const shareTextForCamera = (
export const descriptionForCameraPhotos = ( export const descriptionForCameraPhotos = (
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
dateBased?: boolean, dateBased?: boolean,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
@ -55,7 +55,7 @@ export const descriptionForCameraPhotos = (
export const generateMetaForCamera = ( export const generateMetaForCamera = (
camera: Camera, camera: Camera,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
) => ({ ) => ({

View File

@ -19,7 +19,7 @@ import {
} from '@/utility/string'; } from '@/utility/string';
import { AnnotatedTag } from '@/photo/form'; import { AnnotatedTag } from '@/photo/form';
import PhotoFilmIcon from './PhotoFilmIcon'; import PhotoFilmIcon from './PhotoFilmIcon';
import { I18NState } from '@/i18n/state'; import { AppTextState } from '@/i18n/state';
export type FilmWithCount = { export type FilmWithCount = {
film: string film: string
@ -59,7 +59,7 @@ export const sortFilmsWithCount = (
export const titleForFilm = ( export const titleForFilm = (
film: string, film: string,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
) => [ ) => [
labelForFilm(film).large, labelForFilm(film).large,
@ -68,13 +68,13 @@ export const titleForFilm = (
export const shareTextForFilm = ( export const shareTextForFilm = (
film: string, film: string,
appText: I18NState, appText: AppTextState,
) => ) =>
appText.category.filmShare(labelForFilm(film).large); appText.category.filmShare(labelForFilm(film).large);
export const descriptionForFilmPhotos = ( export const descriptionForFilmPhotos = (
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
dateBased?: boolean, dateBased?: boolean,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
@ -91,7 +91,7 @@ export const descriptionForFilmPhotos = (
export const generateMetaForFilm = ( export const generateMetaForFilm = (
film: string, film: string,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
) => ({ ) => ({

View File

@ -8,7 +8,7 @@ import {
absolutePathForFocalLength, absolutePathForFocalLength,
absolutePathForFocalLengthImage, absolutePathForFocalLengthImage,
} from '@/app/paths'; } from '@/app/paths';
import { I18NState } from '@/i18n/state'; import { AppTextState } from '@/i18n/state';
export type FocalLengths = { export type FocalLengths = {
focal: number focal: number
@ -30,7 +30,7 @@ export const formatFocalLengthSafe = (focal = 0) =>
export const titleForFocalLength = ( export const titleForFocalLength = (
focal: number, focal: number,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
) => [ ) => [
appText.category.focalLengthTitle(formatFocalLengthSafe(focal)), appText.category.focalLengthTitle(formatFocalLengthSafe(focal)),
@ -39,13 +39,13 @@ export const titleForFocalLength = (
export const shareTextFocalLength = ( export const shareTextFocalLength = (
focal: number, focal: number,
appText: I18NState, appText: AppTextState,
) => ) =>
appText.category.focalLengthShare(formatFocalLengthSafe(focal)); appText.category.focalLengthShare(formatFocalLengthSafe(focal));
export const descriptionForFocalLengthPhotos = ( export const descriptionForFocalLengthPhotos = (
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
dateBased?: boolean, dateBased?: boolean,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
@ -62,7 +62,7 @@ export const descriptionForFocalLengthPhotos = (
export const generateMetaForFocalLength = ( export const generateMetaForFocalLength = (
focal: number, focal: number,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
) => ({ ) => ({

View File

@ -10,8 +10,8 @@ export const LOCALE_TEXT: Record<
string, string,
() => Promise<I18NDeepPartial | undefined> () => Promise<I18NDeepPartial | undefined>
> = { > = {
'pt-br': () => import('./locales/pt-br').then((m) => m.default), 'pt-br': () => import('./locales/pt-br').then(m => m.default),
'pt-pt': () => import('./locales/pt-pt').then((m) => m.default), 'pt-pt': () => import('./locales/pt-pt').then(m => m.default),
}; };
export const getTextForLocale = async ( export const getTextForLocale = async (

View File

@ -1,7 +1,6 @@
import { I18NDeepPartial } from '..'; import { I18NDeepPartial } from '..';
const TEXT: I18NDeepPartial = { const TEXT: I18NDeepPartial = {
locale: 'pt-br',
photo: { photo: {
photo: 'Foto', photo: 'Foto',
photoPlural: 'Fotos', photoPlural: 'Fotos',

View File

@ -1,7 +1,6 @@
import { I18NDeepPartial } from '..'; import { I18NDeepPartial } from '..';
const TEXT: I18NDeepPartial = { const TEXT: I18NDeepPartial = {
locale: 'pt-pt',
photo: { photo: {
photo: 'Fotografia', photo: 'Fotografia',
photoPlural: 'Fotografias', photoPlural: 'Fotografias',

View File

@ -1,5 +1,4 @@
const TEXT = { const TEXT = {
locale: 'en-us',
photo: { photo: {
photo: 'Photo', photo: 'Photo',
photoPlural: 'Photos', photoPlural: 'Photos',

View File

@ -3,7 +3,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { AppTextContext } from './client'; import { AppTextContext } from './client';
import { I18N } from '..'; import { I18N } from '..';
import { generateI18NState } from '.'; import { generateAppTextState } from '.';
export default function AppTextProviderClient({ export default function AppTextProviderClient({
children, children,
@ -13,7 +13,7 @@ export default function AppTextProviderClient({
value: I18N value: I18N
}) { }) {
return ( return (
<AppTextContext.Provider value={generateI18NState(value)}> <AppTextContext.Provider value={generateAppTextState(value)}>
{children} {children}
</AppTextContext.Provider> </AppTextContext.Provider>
); );

View File

@ -1,9 +1,9 @@
'use client'; 'use client';
import { createContext, use } from 'react'; import { createContext, use } from 'react';
import { generateI18NState } from '.'; import { generateAppTextState } from '.';
import US_EN from '../locales/us-en'; import US_EN from '../locales/us-en';
export const AppTextContext = createContext(generateI18NState(US_EN)); export const AppTextContext = createContext(generateAppTextState(US_EN));
export const useAppText = () => use(AppTextContext); export const useAppText = () => use(AppTextContext);

View File

@ -1,45 +1,45 @@
import { I18N } from '..'; import { I18N } from '..';
export type I18NState = ReturnType<typeof generateI18NState>; export type AppTextState = ReturnType<typeof generateAppTextState>;
export const generateI18NState = (i18nText: I18N) => { export const generateAppTextState = (i18n: I18N) => {
return { return {
...i18nText, ...i18n,
category: { category: {
...i18nText.category, ...i18n.category,
cameraTitle: (camera: string) => cameraTitle: (camera: string) =>
i18nText.category.cameraTitle.replace('{{camera}}', camera), i18n.category.cameraTitle.replace('{{camera}}', camera),
cameraShare: (camera: string) => cameraShare: (camera: string) =>
i18nText.category.cameraShare.replace('{{camera}}', camera), i18n.category.cameraShare.replace('{{camera}}', camera),
taggedPhrase: (tag: string) => taggedPhrase: (tag: string) =>
i18nText.category.taggedPhrase.replace('{{tag}}', tag), i18n.category.taggedPhrase.replace('{{tag}}', tag),
recipeShare: (recipe: string) => recipeShare: (recipe: string) =>
i18nText.category.recipeShare.replace('{{recipe}}', recipe), i18n.category.recipeShare.replace('{{recipe}}', recipe),
filmShare: (film: string) => filmShare: (film: string) =>
i18nText.category.filmShare.replace('{{film}}', film), i18n.category.filmShare.replace('{{film}}', film),
focalLengthTitle: (focal: string) => focalLengthTitle: (focal: string) =>
i18nText.category.focalLengthTitle.replace('{{focal}}', focal), i18n.category.focalLengthTitle.replace('{{focal}}', focal),
focalLengthShare: (focal: string) => focalLengthShare: (focal: string) =>
i18nText.category.focalLengthShare.replace('{{focal}}', focal), i18n.category.focalLengthShare.replace('{{focal}}', focal),
}, },
admin: { admin: {
...i18nText.admin, ...i18n.admin,
deleteConfirm: (photoTitle: string) => deleteConfirm: (photoTitle: string) =>
i18nText.admin.deleteConfirm.replace('{{photoTitle}}', photoTitle), i18n.admin.deleteConfirm.replace('{{photoTitle}}', photoTitle),
}, },
misc: { misc: {
...i18nText.misc, ...i18n.misc,
copyPhrase: (label: string) => copyPhrase: (label: string) =>
i18nText.misc.copyPhrase.replace('{{label}}', label), i18n.misc.copyPhrase.replace('{{label}}', label),
}, },
utility: { utility: {
...i18nText.utility, ...i18n.utility,
paginate: (index: number, count: number) => paginate: (index: number, count: number) =>
i18nText.utility.paginate i18n.utility.paginate
.replace('{{index}}', index.toString()) .replace('{{index}}', index.toString())
.replace('{{count}}', count.toString()), .replace('{{count}}', count.toString()),
paginateAction: (index: number, count: number, action: string) => paginateAction: (index: number, count: number, action: string) =>
i18nText.utility.paginateAction i18n.utility.paginateAction
.replace('{{index}}', index.toString()) .replace('{{index}}', index.toString())
.replace('{{count}}', count.toString()) .replace('{{count}}', count.toString())
.replace('{{action}}', action), .replace('{{action}}', action),

View File

@ -1,6 +1,6 @@
import { APP_LOCALE } from '@/app/config'; import { APP_LOCALE } from '@/app/config';
import { getTextForLocale } from '..'; import { getTextForLocale } from '..';
import { generateI18NState } from '.'; import { generateAppTextState } from '.';
export const getAppText = async () => export const getAppText = () =>
getTextForLocale(APP_LOCALE).then(generateI18NState); getTextForLocale(APP_LOCALE).then(generateAppTextState);

View File

@ -9,7 +9,7 @@ import {
absolutePathForLens, absolutePathForLens,
absolutePathForLensImage, absolutePathForLensImage,
} from '@/app/paths'; } from '@/app/paths';
import { I18NState } from '@/i18n/state'; import { AppTextState } from '@/i18n/state';
// Meta functions moved to separate file to avoid // Meta functions moved to separate file to avoid
// dependencies (camelcase-keys) found in photo/index.ts // dependencies (camelcase-keys) found in photo/index.ts
@ -18,7 +18,7 @@ import { I18NState } from '@/i18n/state';
export const titleForLens = ( export const titleForLens = (
lens: Lens, lens: Lens,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
) => [ ) => [
`${appText.category.lens}:`, `${appText.category.lens}:`,
@ -29,7 +29,7 @@ export const titleForLens = (
export const shareTextForLens = ( export const shareTextForLens = (
lens: Lens, lens: Lens,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
) => ) =>
[ [
`${appText.category.lens}:`, `${appText.category.lens}:`,
@ -38,7 +38,7 @@ export const shareTextForLens = (
export const descriptionForLensPhotos = ( export const descriptionForLensPhotos = (
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
dateBased?: boolean, dateBased?: boolean,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
@ -55,7 +55,7 @@ export const descriptionForLensPhotos = (
export const generateMetaForLens = ( export const generateMetaForLens = (
lens: Lens, lens: Lens,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
) => ({ ) => ({

View File

@ -24,7 +24,7 @@ import type { Metadata } from 'next';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation'; import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync'; import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync';
import { I18NState } from '@/i18n/state'; import { AppTextState } from '@/i18n/state';
// INFINITE SCROLL: FEED // INFINITE SCROLL: FEED
export const INFINITE_SCROLL_FEED_INITIAL = export const INFINITE_SCROLL_FEED_INITIAL =
@ -234,7 +234,7 @@ export const altTextForPhoto = (photo: Photo) =>
export const photoLabelForCount = ( export const photoLabelForCount = (
count: number, count: number,
appText: I18NState, appText: AppTextState,
_capitalize = true, _capitalize = true,
) => { ) => {
const label = count === 1 const label = count === 1
@ -247,7 +247,7 @@ export const photoLabelForCount = (
export const photoQuantityText = ( export const photoQuantityText = (
count: number, count: number,
appText: I18NState, appText: AppTextState,
includeParentheses = true, includeParentheses = true,
capitalize?: boolean, capitalize?: boolean,
) => ) =>
@ -257,7 +257,7 @@ export const photoQuantityText = (
export const deleteConfirmationTextForPhoto = ( export const deleteConfirmationTextForPhoto = (
photo: Photo, photo: Photo,
appText: I18NState, appText: AppTextState,
) => ) =>
appText.admin.deleteConfirm(titleForPhoto(photo)); appText.admin.deleteConfirm(titleForPhoto(photo));
@ -265,7 +265,7 @@ export type PhotoDateRange = { start: string, end: string };
export const descriptionForPhotoSet = ( export const descriptionForPhotoSet = (
photos:Photo[] = [], photos:Photo[] = [],
appText: I18NState, appText: AppTextState,
descriptor?: string, descriptor?: string,
dateBased?: boolean, dateBased?: boolean,
explicitCount?: number, explicitCount?: number,

View File

@ -8,7 +8,7 @@ import {
} from '@/utility/string'; } from '@/utility/string';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { labelForFilm } from '@/film'; import { labelForFilm } from '@/film';
import { I18NState } from '@/i18n/state'; import { AppTextState } from '@/i18n/state';
export type RecipeWithCount = { export type RecipeWithCount = {
recipe: string recipe: string
@ -31,7 +31,7 @@ export const formatRecipe = (recipe?: string) =>
export const titleForRecipe = ( export const titleForRecipe = (
recipe: string, recipe: string,
photos:Photo[] = [], photos:Photo[] = [],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
) => [ ) => [
`${appText.category.recipe}: ${formatRecipe(recipe)}`, `${appText.category.recipe}: ${formatRecipe(recipe)}`,
@ -40,13 +40,13 @@ export const titleForRecipe = (
export const shareTextForRecipe = ( export const shareTextForRecipe = (
recipe: string, recipe: string,
appText: I18NState, appText: AppTextState,
) => ) =>
appText.category.recipeShare(formatRecipe(recipe)); appText.category.recipeShare(formatRecipe(recipe));
export const descriptionForRecipePhotos = ( export const descriptionForRecipePhotos = (
photos: Photo[] = [], photos: Photo[] = [],
appText: I18NState, appText: AppTextState,
dateBased?: boolean, dateBased?: boolean,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
@ -145,7 +145,7 @@ export const generateRecipeText = (
export const generateMetaForRecipe = ( export const generateMetaForRecipe = (
recipe: string, recipe: string,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
) => ({ ) => ({

View File

@ -16,7 +16,7 @@ import {
formatCountDescriptive, formatCountDescriptive,
} from '@/utility/string'; } from '@/utility/string';
import { sortCategoryByCount } from '@/category'; import { sortCategoryByCount } from '@/category';
import { I18NState } from '@/i18n/state'; import { AppTextState } from '@/i18n/state';
// Reserved tags // Reserved tags
export const TAG_FAVS = 'favs'; export const TAG_FAVS = 'favs';
@ -42,7 +42,7 @@ export const getValidationMessageForTags = (tags?: string) => {
export const titleForTag = ( export const titleForTag = (
tag: string, tag: string,
photos:Photo[] = [], photos:Photo[] = [],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
) => [ ) => [
formatTag(tag), formatTag(tag),
@ -51,7 +51,7 @@ export const titleForTag = (
export const shareTextForTag = ( export const shareTextForTag = (
tag: string, tag: string,
appText: I18NState, appText: AppTextState,
) => ) =>
isTagFavs(tag) isTagFavs(tag)
? appText.category.taggedFavs ? appText.category.taggedFavs
@ -96,7 +96,7 @@ export const sortTagsObjectWithoutFavs = (tags: Tags) =>
export const descriptionForTaggedPhotos = ( export const descriptionForTaggedPhotos = (
photos: Photo[] = [], photos: Photo[] = [],
appText: I18NState, appText: AppTextState,
dateBased?: boolean, dateBased?: boolean,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
@ -113,7 +113,7 @@ export const descriptionForTaggedPhotos = (
export const generateMetaForTag = ( export const generateMetaForTag = (
tag: string, tag: string,
photos: Photo[], photos: Photo[],
appText: I18NState, appText: AppTextState,
explicitCount?: number, explicitCount?: number,
explicitDateRange?: PhotoDateRange, explicitDateRange?: PhotoDateRange,
) => ({ ) => ({
@ -151,7 +151,7 @@ export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) =>
export const convertTagsForForm = ( export const convertTagsForForm = (
tags: Tags = [], tags: Tags = [],
appText: I18NState, appText: AppTextState,
) => ) =>
sortTagsObjectWithoutFavs(tags) sortTagsObjectWithoutFavs(tags)
.map(({ tag, count }) => ({ .map(({ tag, count }) => ({