Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/api/model-labels/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,18 @@ export async function PUT(request: NextRequest) {
const config: ModelLabelsConfig = {
labels: normalizeModelLabels((body as { labels?: unknown }).labels as Record<string, unknown>),
};
await writeModelLabels(config);
try {
await writeModelLabels(config, token);
} catch (err) {
const status =
err &&
typeof err === 'object' &&
'status' in err &&
typeof (err as { status: unknown }).status === 'number'
? (err as { status: number }).status
: 502;
return NextResponse.json({ error: 'Failed to update site labels' }, { status });
}
Comment thread
Copilot marked this conversation as resolved.
revalidatePath('/', 'layout');
return NextResponse.json(config);
}
17 changes: 11 additions & 6 deletions app/backoffice/translations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ const generalConfigFieldMeta: Array<{ key: ModelLabelKey; title: string; descrip
description: 'The short strapline shown next to the site title in the header.',
},
{
key: 'footerFunded',
title: 'Footer: Funding Statement',
description: 'The funding acknowledgement shown in the footer.',
key: 'footerLine1',
title: 'Footer: Line 1',
description: 'The first line of footer text.',
},
{
key: 'footerCopyright',
title: 'Footer: Copyright Notice',
description: 'The copyright notice shown at the bottom of the footer.',
key: 'footerLine2',
title: 'Footer: Line 2',
description: 'The second line of footer text.',
},
{
key: 'footerBottomLine',
title: 'Footer: Bottom Line',
description: 'The copyright/attribution notice shown at the bottom of the footer.',
},
];

Expand Down
8 changes: 4 additions & 4 deletions components/layout/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default async function Footer() {
getPublishedPages(),
]);
const locale = rawLocale as ModelLabelLocale;
const getLabel = (key: 'siteTitle' | 'footerFunded' | 'footerCopyright') =>
const getLabel = (key: 'siteTitle' | 'footerLine1' | 'footerLine2' | 'footerBottomLine') =>
resolveModelLabel(modelLabels.labels[key], locale);
const quickLinkPages = pages
.filter((page) => page.include_in_quick_link)
Expand All @@ -76,8 +76,8 @@ export default async function Footer() {
<h2 className="font-serif text-lg font-semibold tracking-tight">
{getLabel('siteTitle')}
</h2>
<p className="text-sm text-primary-foreground/85 leading-relaxed">{t('about')}</p>
<p className="text-sm text-primary-foreground/85">{getLabel('footerFunded')}</p>
<p className="text-sm text-primary-foreground/85">{getLabel('footerLine1')}</p>
<p className="text-sm text-primary-foreground/85">{getLabel('footerLine2')}</p>
</div>

{/* Links column */}
Expand Down Expand Up @@ -131,7 +131,7 @@ export default async function Footer() {
{/* Bottom bar */}
<div className="border-t border-primary-foreground/20 pt-6 flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-xs text-primary-foreground/85 text-center md:text-left max-w-2xl">
{getLabel('footerCopyright')}
{getLabel('footerBottomLine')}
</p>
<div className="flex items-center gap-3">
<Link
Expand Down
47 changes: 32 additions & 15 deletions lib/model-labels-server.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
import { readJsonConfig, writeJsonConfig } from './json-config-file';
import { apiFetch, authFetch } from './api-fetch';
import {
getDefaultModelLabelsConfig,
normalizeModelLabels,
type ModelLabelsConfig,
} from './model-labels';

const CONFIG_FILE = 'model-labels.json';
const SITE_LABELS_PATH = '/api/v1/site-labels/';

/**
* Model labels are backend-owned (`SiteLabel` rows, superuser-editable via
* the backoffice). Any failure - network error, non-200, or an unexpected
* response shape - falls back to defaults so SSR never 500s over a backend
* hiccup (matches `getPublishedPages`'s fallback behavior).
*/
export async function readModelLabels(): Promise<ModelLabelsConfig> {
const defaults = getDefaultModelLabelsConfig();
return readJsonConfig(
CONFIG_FILE,
(raw) => {
// If the file was hand-edited to `null`, an array, or a primitive,
// reading `parsed.labels` below would crash; bail to defaults.
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return defaults;
const parsed = raw as Partial<ModelLabelsConfig>;
return { labels: normalizeModelLabels(parsed.labels) };
},
defaults
);
try {
const res = await apiFetch(SITE_LABELS_PATH);
if (!res.ok) return defaults;
const raw = await res.json();
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return defaults;
const parsed = raw as Partial<ModelLabelsConfig>;
return { labels: normalizeModelLabels(parsed.labels) };
} catch {
return defaults;
}
}

export async function writeModelLabels(config: ModelLabelsConfig): Promise<void> {
await writeJsonConfig(CONFIG_FILE, { labels: normalizeModelLabels(config.labels) });
/** Upserts the given keys via the backend's superuser-only PUT; throws on failure. */
export async function writeModelLabels(config: ModelLabelsConfig, token: string): Promise<void> {
const res = await authFetch(SITE_LABELS_PATH, token, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ labels: normalizeModelLabels(config.labels) }),
});
if (!res.ok) {
const details = await res.text().catch(() => '');
throw Object.assign(
new Error(`Failed to write site labels: ${res.status}${details ? ` - ${details}` : ''}`),
{ status: res.status }
);
}
}
Comment thread
Copilot marked this conversation as resolved.
27 changes: 19 additions & 8 deletions lib/model-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ export type ModelLabelKey =
// General site branding, shown in the header and footer.
| 'siteTitle'
| 'siteTagline'
| 'footerFunded'
| 'footerCopyright';
| 'footerLine1'
| 'footerLine2'
| 'footerBottomLine';

export type ModelLabelLocale = 'en' | 'fr';

Expand Down Expand Up @@ -59,13 +60,23 @@ export const DEFAULT_MODEL_LABELS: Record<ModelLabelKey, LocalizedLabel> = {
en: 'Archetype tagline',
fr: 'Archetype tagline',
},
footerFunded: {
en: 'Archetype funding text.',
fr: "Texte de financement d'Archetype.",
footerLine1: {
en: 'Footer first section',
fr: 'Pied de page, première section',
},
footerCopyright: {
en: 'Archetype copyright',
fr: "Droits d'auteur d'Archetype",
footerLine2: {
en: 'Footer second section',
fr: 'Pied de page, deuxième section',
},
footerBottomLine: {
en:
'Archetype is freely-available software for structured annotations of images which allows users to search for, ' +
'view, and organise detailed characteristics of handwriting or other material in both verbal and visual form. ' +
'Archetype is designed primarily for the palaeographical analysis of handwriting.',
fr:
'Archetype est un logiciel libre pour les annotations structurées d’images qui permet aux utilisateurs de rechercher' +
' , de visualiser et d’organiser les caractéristiques détaillées de l’écriture ou d’autres matériaux sous forme verbale et visuelle. ' +
'Archetype est conçu principalement pour l’analyse paléographique de l’écriture.',
},
};

Expand Down