diff --git a/.changeset/visible-taxonomy-fallbacks.md b/.changeset/visible-taxonomy-fallbacks.md
new file mode 100644
index 0000000000..7d2eb7ddf6
--- /dev/null
+++ b/.changeset/visible-taxonomy-fallbacks.md
@@ -0,0 +1,6 @@
+---
+"emdash": patch
+"@emdash-cms/admin": patch
+---
+
+Fixes localized taxonomy assignments so the editor shows the configured default-locale variant or an unresolved translation prompt instead of hiding the assignment, and identifies the stored content locale and implicit English default.
diff --git a/packages/admin/src/components/ContentSettingsPanel.tsx b/packages/admin/src/components/ContentSettingsPanel.tsx
index 0d4e086a21..21bb026d43 100644
--- a/packages/admin/src/components/ContentSettingsPanel.tsx
+++ b/packages/admin/src/components/ContentSettingsPanel.tsx
@@ -1,5 +1,6 @@
import {
Badge,
+ Banner,
Button,
Dialog,
Input,
@@ -416,6 +417,8 @@ export const ContentSettingsPanel = React.memo(function ContentSettingsPanel({
const hasApplicableTaxonomies = useHasApplicableTaxonomies(collection);
const canUpdatePublishedDate =
item?.publishedAt != null && (currentUser?.role ?? 0) >= ROLE_EDITOR && !!onPublishedAtChange;
+ const contentLocale = item?.locale ?? entryLocale ?? manifest?.contentLocale?.defaultLocale;
+ const usesImplicitEnglish = manifest?.contentLocale?.implicit === true && contentLocale === "en";
React.useEffect(() => {
setPublishedDate(storedPublishedDate);
@@ -485,6 +488,25 @@ export const ContentSettingsPanel = React.memo(function ContentSettingsPanel({
onChange={(e) => onSlugChange(e.target.value)}
placeholder="my-post-slug"
/>
+ {contentLocale ? (
+
@@ -682,6 +704,7 @@ export const ContentSettingsPanel = React.memo(function ContentSettingsPanel({
collection={collection}
entryId={item.id}
entryLocale={item.locale ?? entryLocale}
+ canManageTaxonomies={(currentUser?.role ?? 0) >= ROLE_EDITOR}
/>
)}
diff --git a/packages/admin/src/components/TaxonomySidebar.tsx b/packages/admin/src/components/TaxonomySidebar.tsx
index c316aee23b..7f322c0e4a 100644
--- a/packages/admin/src/components/TaxonomySidebar.tsx
+++ b/packages/admin/src/components/TaxonomySidebar.tsx
@@ -6,7 +6,7 @@
* - Tag input for flat taxonomies (tags)
*/
-import { Autocomplete, Button, Checkbox, Input, Label, Text, Toast } from "@cloudflare/kumo";
+import { Autocomplete, Badge, Button, Checkbox, Input, Label, Text, Toast } from "@cloudflare/kumo";
import { i18n } from "@lingui/core";
import { msg } from "@lingui/core/macro";
import { useLingui } from "@lingui/react/macro";
@@ -15,7 +15,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import * as React from "react";
import { apiFetch, parseApiResponse, throwResponseError } from "../lib/api/client.js";
-import { createTerm, withLocale } from "../lib/api/taxonomies.js";
+import { createTerm, createTermTranslation, withLocale } from "../lib/api/taxonomies.js";
import { rankTermMatches, termExactMatches } from "../lib/taxonomy-match.js";
import { cn } from "../lib/utils.js";
@@ -24,8 +24,24 @@ interface TaxonomyTerm {
name: string;
slug: string;
label: string;
- parentId?: string;
+ parentId?: string | null;
children: TaxonomyTerm[];
+ locale: string;
+ translationGroup: string | null;
+}
+
+interface UnresolvedAssignment {
+ translationGroup: string;
+ availableLocales: string[];
+ translations: Array<{ id: string; slug: string; locale: string }>;
+}
+
+interface EntryTermsResponse {
+ terms: TaxonomyTerm[];
+ unresolved: UnresolvedAssignment[];
+ entryLocale: string;
+ defaultLocale: string;
+ implicitDefaultLocale: boolean;
}
interface TaxonomyDef {
@@ -40,6 +56,7 @@ interface TaxonomyDef {
interface TaxonomySidebarProps {
collection: string;
entryId?: string;
+ canManageTaxonomies: boolean;
/** Locale of the entry being edited. Scopes term reads/writes so only the
* matching translation variants are shown — see issue #1218. */
entryLocale?: string;
@@ -50,6 +67,7 @@ interface TaxonomySidebarProps {
}
const EMPTY_TERMS: TaxonomyTerm[] = [];
+const EMPTY_UNRESOLVED_ASSIGNMENTS: UnresolvedAssignment[] = [];
type TagInputOption = { type: "term"; term: TaxonomyTerm } | { type: "create"; label: string };
@@ -84,9 +102,8 @@ export function useHasApplicableTaxonomies(collection: string): boolean {
* opts out of the per-collection count aggregate the endpoint runs by default.
*/
async function fetchTerms(taxonomyName: string, locale?: string): Promise {
- const res = await apiFetch(
- withLocale(`/_emdash/api/taxonomies/${taxonomyName}/terms?includeCounts=false`, locale),
- );
+ const path = `/_emdash/api/taxonomies/${taxonomyName}/terms?includeCounts=false${locale ? "&resolveFallback=true" : ""}`;
+ const res = await apiFetch(withLocale(path, locale));
const data = await parseApiResponse<{ terms: TaxonomyTerm[] }>(
res,
i18n._(msg`Failed to fetch terms`),
@@ -101,16 +118,13 @@ async function fetchEntryTerms(
collection: string,
entryId: string,
taxonomy: string,
- locale?: string,
-): Promise {
- const res = await apiFetch(
- withLocale(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`, locale),
- );
- const data = await parseApiResponse<{ terms: TaxonomyTerm[] }>(
+): Promise {
+ const res = await apiFetch(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`);
+ const data = await parseApiResponse(
res,
i18n._(msg`Failed to fetch entry terms`),
);
- return data.terms;
+ return data;
}
/**
@@ -121,16 +135,12 @@ async function setEntryTerms(
entryId: string,
taxonomy: string,
termIds: string[],
- locale?: string,
): Promise {
- const res = await apiFetch(
- withLocale(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`, locale),
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ termIds }),
- },
- );
+ const res = await apiFetch(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ termIds }),
+ });
if (!res.ok) await throwResponseError(res, i18n._(msg`Failed to set entry terms`));
}
@@ -142,11 +152,13 @@ function CategoryCheckboxTree({
level = 0,
selectedIds,
onToggle,
+ entryLocale,
}: {
term: TaxonomyTerm;
level?: number;
selectedIds: Set;
onToggle: (termId: string) => void;
+ entryLocale?: string;
}) {
const isChecked = selectedIds.has(term.id);
@@ -159,7 +171,12 @@ function CategoryCheckboxTree({
onToggle(term.id)}
- label={{term.label}}
+ label={
+
+ {term.label}
+
+
+ }
/>
{term.children.map((child) => (
@@ -169,6 +186,7 @@ function CategoryCheckboxTree({
level={level + 1}
selectedIds={selectedIds}
onToggle={onToggle}
+ entryLocale={entryLocale}
/>
))}
@@ -185,7 +203,10 @@ function TagInput({
onRemove,
onCreate,
isCreating,
+ createError,
label,
+ entryLocale,
+ canCreate,
}: {
terms: TaxonomyTerm[];
selectedIds: Set