From 7f88a5a0a9dcb86b7fd1dd158acba838b60c7f0a Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:38:32 +0300 Subject: [PATCH] feat(studio): collections in the sidebar, list, settings and New collection (RFC-0040 step 3) The sidebar lists the registry (declared, inferred from entries, posts) with names in Studio's language, icons (a closed set, lazy-loaded so the first load stays in budget) and counts, then Collections. New routes: /collections (overview + New collection dialog), /collections/$collection (the Articles list, generalised: taxonomy columns only where they apply) and /collections/$collection/settings (names per locale, icon, feed, sitemap, sort, taxonomies; the address links to Site settings). /articles redirects to /collections/posts; the editor's crumb follows the entry's collection. Comments, language review options and the taxonomy dialog read the registry. New lazy message namespace "collections"; drafts for 18 languages (awaiting review). Refs #71 (RFC-0040, ADR-0042) Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/collections-studio.md | 12 + apps/studio/scripts/design-lint.ts | 2 + .../src/components/CollectionActions.tsx | 34 ++ apps/studio/src/components/CollectionNav.tsx | 61 +++ .../src/components/LanguageSettings.tsx | 24 +- .../src/components/NewCollectionDialog.tsx | 216 +++++++++++ .../src/components/TaxonomySettingsDialog.tsx | 5 +- .../src/components/useCollectionView.ts | 97 +++++ apps/studio/src/i18n/ar.ts | 10 +- apps/studio/src/i18n/de.ts | 10 +- apps/studio/src/i18n/en.ts | 8 + apps/studio/src/i18n/es.ts | 10 +- apps/studio/src/i18n/fa.ts | 10 +- apps/studio/src/i18n/fr.ts | 10 +- apps/studio/src/i18n/generated/params.ts | 6 + apps/studio/src/i18n/he.ts | 10 +- apps/studio/src/i18n/hi.ts | 10 +- apps/studio/src/i18n/id.ts | 10 +- apps/studio/src/i18n/it.ts | 10 +- apps/studio/src/i18n/ja.ts | 10 +- apps/studio/src/i18n/ko.ts | 10 +- apps/studio/src/i18n/lazy/collections.ar.ts | 98 +++++ apps/studio/src/i18n/lazy/collections.de.ts | 100 +++++ apps/studio/src/i18n/lazy/collections.en.ts | 90 +++++ apps/studio/src/i18n/lazy/collections.es.ts | 100 +++++ apps/studio/src/i18n/lazy/collections.fa.ts | 99 +++++ apps/studio/src/i18n/lazy/collections.fr.ts | 102 +++++ apps/studio/src/i18n/lazy/collections.he.ts | 97 +++++ apps/studio/src/i18n/lazy/collections.hi.ts | 98 +++++ apps/studio/src/i18n/lazy/collections.id.ts | 99 +++++ apps/studio/src/i18n/lazy/collections.it.ts | 99 +++++ apps/studio/src/i18n/lazy/collections.ja.ts | 98 +++++ apps/studio/src/i18n/lazy/collections.ko.ts | 97 +++++ apps/studio/src/i18n/lazy/collections.nl.ts | 99 +++++ apps/studio/src/i18n/lazy/collections.pl.ts | 100 +++++ .../studio/src/i18n/lazy/collections.pt-BR.ts | 100 +++++ apps/studio/src/i18n/lazy/collections.ru.ts | 98 +++++ apps/studio/src/i18n/lazy/collections.tr.ts | 101 +++++ apps/studio/src/i18n/lazy/collections.uk.ts | 98 +++++ .../src/i18n/lazy/collections.zh-Hans.ts | 93 +++++ apps/studio/src/i18n/namespaces.ts | 2 + apps/studio/src/i18n/nl.ts | 10 +- apps/studio/src/i18n/pl.ts | 10 +- apps/studio/src/i18n/pt-BR.ts | 10 +- apps/studio/src/i18n/ru.ts | 10 +- apps/studio/src/i18n/tr.ts | 10 +- apps/studio/src/i18n/uk.ts | 10 +- apps/studio/src/i18n/zh-Hans.ts | 10 +- apps/studio/src/lib/api.ts | 2 + apps/studio/src/lib/api/collections.ts | 28 ++ apps/studio/src/lib/collection-icons.ts | 31 ++ apps/studio/src/lib/collections.ts | 108 ++++++ apps/studio/src/lib/taxonomy-settings.ts | 13 +- apps/studio/src/router.tsx | 47 ++- apps/studio/src/routes/ArticleEditorPage.tsx | 8 +- apps/studio/src/routes/ArticlesPage.tsx | 67 ++-- .../src/routes/CollectionSettingsPage.tsx | 357 ++++++++++++++++++ apps/studio/src/routes/CollectionsPage.tsx | 150 ++++++++ apps/studio/src/routes/CommentsPage.tsx | 4 +- apps/studio/src/routes/SiteShell.tsx | 16 +- apps/studio/test/collections.test.ts | 71 ++++ scripts/size-baseline.json | 4 +- 62 files changed, 3324 insertions(+), 95 deletions(-) create mode 100644 .changeset/collections-studio.md create mode 100644 apps/studio/src/components/CollectionActions.tsx create mode 100644 apps/studio/src/components/CollectionNav.tsx create mode 100644 apps/studio/src/components/NewCollectionDialog.tsx create mode 100644 apps/studio/src/components/useCollectionView.ts create mode 100644 apps/studio/src/i18n/lazy/collections.ar.ts create mode 100644 apps/studio/src/i18n/lazy/collections.de.ts create mode 100644 apps/studio/src/i18n/lazy/collections.en.ts create mode 100644 apps/studio/src/i18n/lazy/collections.es.ts create mode 100644 apps/studio/src/i18n/lazy/collections.fa.ts create mode 100644 apps/studio/src/i18n/lazy/collections.fr.ts create mode 100644 apps/studio/src/i18n/lazy/collections.he.ts create mode 100644 apps/studio/src/i18n/lazy/collections.hi.ts create mode 100644 apps/studio/src/i18n/lazy/collections.id.ts create mode 100644 apps/studio/src/i18n/lazy/collections.it.ts create mode 100644 apps/studio/src/i18n/lazy/collections.ja.ts create mode 100644 apps/studio/src/i18n/lazy/collections.ko.ts create mode 100644 apps/studio/src/i18n/lazy/collections.nl.ts create mode 100644 apps/studio/src/i18n/lazy/collections.pl.ts create mode 100644 apps/studio/src/i18n/lazy/collections.pt-BR.ts create mode 100644 apps/studio/src/i18n/lazy/collections.ru.ts create mode 100644 apps/studio/src/i18n/lazy/collections.tr.ts create mode 100644 apps/studio/src/i18n/lazy/collections.uk.ts create mode 100644 apps/studio/src/i18n/lazy/collections.zh-Hans.ts create mode 100644 apps/studio/src/lib/api/collections.ts create mode 100644 apps/studio/src/lib/collection-icons.ts create mode 100644 apps/studio/src/lib/collections.ts create mode 100644 apps/studio/src/routes/CollectionSettingsPage.tsx create mode 100644 apps/studio/src/routes/CollectionsPage.tsx create mode 100644 apps/studio/test/collections.test.ts diff --git a/.changeset/collections-studio.md b/.changeset/collections-studio.md new file mode 100644 index 00000000..f1aa334e --- /dev/null +++ b/.changeset/collections-studio.md @@ -0,0 +1,12 @@ +--- +"@cloudhal/cli": minor +--- + +Studio: first-class collections (RFC-0040 step 3). +- **Sidebar:** lists every collection of the site, declared ones and those that agents or imports filled. Each shows its name in Studio's language, its icon and its entry count, followed by **Collections**. +- **Collections screen:** shows each collection's address, entry count and whether it is named. It has a **New collection** dialog with plural and singular name, key, icon and address, and the API checks the address for overlaps. +- **Collection list:** each collection gets the Articles list (`/collections/`, where `/articles` now redirects). Category and tag columns and filters appear only where those taxonomies apply. +- **Collection settings:** names in every site language, icon, RSS feed, sitemap, default order, and which categories and tags apply. +- **Editor:** its breadcrumb leads back to the entry's own collection. +- **Elsewhere:** the comments queue, the language review options and the taxonomy dialog cover every collection. +- **Translations:** new strings in all 19 languages; every language except English is an AI draft awaiting review. diff --git a/apps/studio/scripts/design-lint.ts b/apps/studio/scripts/design-lint.ts index 19a9d4aa..80f6e4d3 100644 --- a/apps/studio/scripts/design-lint.ts +++ b/apps/studio/scripts/design-lint.ts @@ -49,6 +49,8 @@ export const ROUTE_TEMPLATES: Record = { "routes/ProjectPage.tsx": "dashboard", "routes/PagesPage.tsx": "list", "routes/ArticlesPage.tsx": "list", + "routes/CollectionsPage.tsx": "list", + "routes/CollectionSettingsPage.tsx": "settings", "routes/SnapshotsPage.tsx": "list", "routes/ActivityPage.tsx": "list", "routes/ApprovalsPage.tsx": "queue", diff --git a/apps/studio/src/components/CollectionActions.tsx b/apps/studio/src/components/CollectionActions.tsx new file mode 100644 index 00000000..af3b4601 --- /dev/null +++ b/apps/studio/src/components/CollectionActions.tsx @@ -0,0 +1,34 @@ +import { LinkButton } from "@cloudhal/ui"; +import { GearSix } from "@phosphor-icons/react"; +import type { ReactNode } from "react"; +import { useT } from "../i18n/index.tsx"; +import { useCan } from "./useMe.ts"; + +/** A collection list's header actions: its settings (people who may change them), then "New". */ +export function CollectionActions({ + siteId, + collection, + add, +}: { + siteId: string; + collection: string; + add: ReactNode; +}) { + const t = useT(); + const settings = useCan("settings.write"); + return ( +
+ {settings && ( + + {t("collectionList.settings")} + + )} + {add} +
+ ); +} diff --git a/apps/studio/src/components/CollectionNav.tsx b/apps/studio/src/components/CollectionNav.tsx new file mode 100644 index 00000000..9d59b1bf --- /dev/null +++ b/apps/studio/src/components/CollectionNav.tsx @@ -0,0 +1,61 @@ +import type { Site } from "@cloudhal/core-contracts"; +import { Sidebar } from "@cloudhal/ui"; +import { Stack } from "@phosphor-icons/react"; +import { useI18n } from "../i18n/index.tsx"; +import { + collectionIcon, + collectionNames, + entryTotal, + useCollectionIcons, + useCollections, +} from "../lib/collections.ts"; + +/** + * The sidebar's collections (RFC-0040 §4): one item per collection of the registry, by its plural + * name in Studio's language, with its entry count, then "Collections", the overview with "New + * collection". `posts` keeps Studio's own "Articles" until someone names it. + */ +export function CollectionNav({ + siteId, + site, + is, +}: { + siteId: string; + site: Site | undefined; + is: (path: string, exact?: boolean) => boolean; +}) { + const { t, locale } = useI18n(); + const registry = useCollections(siteId); + const icons = useCollectionIcons(); + const base = `/sites/${siteId}`; + return ( + <> + {(registry.data ?? []).map((c) => { + const href = `${base}/collections/${c.key}`; + const name = + collectionNames(site, c.key, locale)?.plural ?? + (c.key === "posts" ? t("nav.articles") : c.key); + return ( + + {name} + {entryTotal(c)} + + ); + })} + + {t("nav.collections")} + + + ); +} diff --git a/apps/studio/src/components/LanguageSettings.tsx b/apps/studio/src/components/LanguageSettings.tsx index b5723646..967977cc 100644 --- a/apps/studio/src/components/LanguageSettings.tsx +++ b/apps/studio/src/components/LanguageSettings.tsx @@ -12,6 +12,7 @@ import { useEffect, useState } from "react"; import { useI18n } from "../i18n/index.tsx"; import { ApiRequestError, api } from "../lib/api.ts"; import { apiErrorText } from "../lib/api-errors.ts"; +import { collectionNames, useCollections } from "../lib/collections.ts"; import { languageName } from "../lib/languages.ts"; import { EditionGuide } from "./EditionGuide.tsx"; import { LanguagePicker } from "./LanguagePicker.tsx"; @@ -58,8 +59,6 @@ function withForm(site: Site, f: Form): Site { }; } -const REVIEWABLE = ["pages", "posts"] as const; - export function LanguageSettingsSection({ siteId }: { siteId: string }) { const { t, locale } = useI18n(); const qc = useQueryClient(); @@ -127,7 +126,7 @@ export function LanguageSettingsSection({ siteId }: { siteId: string }) { )} - + ); } @@ -197,8 +196,15 @@ function LocalesEditor({ } /** Addresses, missing translations, review before publishing, the reader's notice. */ -function LocalizationRows({ form, set, disabled }: EditorProps) { - const { t } = useI18n(); +function LocalizationRows({ form, set, disabled, site }: EditorProps & { site: Site | undefined }) { + const { t, locale } = useI18n(); + // Pages, then every collection of the registry (RFC-0040), `posts` first. + const registry = useCollections(site?.id ?? ""); + const reviewable = [ + "pages", + "posts", + ...(registry.data ?? []).map((c) => c.key).filter((k) => k !== "posts"), + ]; return (
@@ -227,10 +233,14 @@ function LocalizationRows({ form, set, disabled }: EditorProps) { - {REVIEWABLE.map((c) => ( + {reviewable.map((c) => ( diff --git a/apps/studio/src/components/NewCollectionDialog.tsx b/apps/studio/src/components/NewCollectionDialog.tsx new file mode 100644 index 00000000..07640677 --- /dev/null +++ b/apps/studio/src/components/NewCollectionDialog.tsx @@ -0,0 +1,216 @@ +import type { CollectionSettings, Site } from "@cloudhal/core-contracts"; +import { Banner, Input, LayerDialog, Select } from "@cloudhal/ui"; +import { WarningCircle } from "@phosphor-icons/react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { useI18n } from "../i18n/index.tsx"; +import { api } from "../lib/api.ts"; +import { apiErrorText } from "../lib/api-errors.ts"; +import { validatePrefix } from "../lib/collection-permalinks.ts"; +import { COLLECTION_ICON_NAMES } from "../lib/collections.ts"; +import { slugify } from "../lib/workflow.ts"; +import { useToast } from "./Toast.ts"; + +const KEY = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +/** What the form makes: the declared settings, the permalink only when not the default. */ +export function newCollectionSettings(input: { + key: string; + plural: string; + singular: string; + icon: string; + prefix: string; + locale: string; +}): CollectionSettings { + const prefix = input.prefix.trim(); + return { + labels: { [input.locale]: { singular: input.singular.trim(), plural: input.plural.trim() } }, + icon: input.icon, + ...(prefix && prefix !== `/${input.key}` ? { permalink: `${prefix}/{slug}` } : {}), + }; +} + +/** + * "New collection" (RFC-0040 §4): plural and singular name in the site's default language, the + * key (derived from the name until edited, immutable afterwards), an icon and the address prefix. + * The API checks the prefix against other collections, archives and reserved paths (409). + */ +export function NewCollectionDialog({ + open, + onOpenChange, + site, + taken, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + site: Site | undefined; + taken: readonly string[]; +}) { + const { t } = useI18n("collections"); + const [plural, setPlural] = useState(""); + const [singular, setSingular] = useState(""); + const [key, setKey] = useState(""); + const [keyEdited, setKeyEdited] = useState(false); + const [icon, setIcon] = useState("files"); + const [prefix, setPrefix] = useState(""); + const [submitted, setSubmitted] = useState(false); + const create = useCreateCollection(site, () => onOpenChange(false), { + key, + settings: () => + newCollectionSettings({ + key, + plural, + singular, + icon, + prefix, + locale: site?.defaultLocale ?? "en", + }), + name: () => plural.trim(), + }); + // Every opening starts clean. + useEffect(() => { + if (!open) return; + setPlural(""); + setSingular(""); + setKey(""); + setKeyEdited(false); + setIcon("files"); + setPrefix(""); + setSubmitted(false); + create.reset(); + }, [open]); + + const required = (v: string) => + submitted && !v.trim() ? t("collections.create.required") : undefined; + const keyError = + submitted && (!KEY.test(key) || taken.includes(key)) + ? t("collections.create.keyInvalid") + : undefined; + const prefixProblem = prefix.trim() ? validatePrefix(prefix) : undefined; + const prefixError = prefixProblem + ? t(`siteSettings.collections.errors.${prefixProblem}`) + : undefined; + const valid = + plural.trim() && singular.trim() && KEY.test(key) && !taken.includes(key) && !prefixProblem; + const submit = () => { + setSubmitted(true); + if (create.isPending || !valid) return; + create.mutate(); + }; + const icons = Object.fromEntries( + COLLECTION_ICON_NAMES.map((name) => [name, t(`collections.icons.${name}`)]), + ); + return ( + + + {t("collections.create.title")} + {t("collections.create.description")} + +
+ {create.error && ( + } + title={t("collections.create.failed")} + description={apiErrorText(t, create.error)} + /> + )} + { + setPlural(v); + // "ı" has no decomposition: map it, or "Etkinlikları" would lose letters. + if (!keyEdited) setKey(v.trim() ? slugify(v.replace(/ı/g, "i")).slice(0, 64) : ""); + }} + error={required(plural)} + maxLength={100} + autoFocus + /> + + { + setKey(v); + setKeyEdited(true); + }} + error={keyError} + maxLength={64} + /> + +
+
+ + + {t("collections.create.submit")} + + +
+
+ ); +} + +/** Saves the declaration, then shows the new collection's list. */ +function useCreateCollection( + site: Site | undefined, + close: () => void, + input: { key: string; settings: () => CollectionSettings; name: () => string }, +) { + const { t } = useI18n("collections"); + const qc = useQueryClient(); + const toast = useToast(); + const navigate = useNavigate(); + return useMutation({ + mutationFn: async () => { + if (!site) throw new Error("site not loaded"); + return api.collections.save(site, input.key, input.settings()); + }, + onSuccess: async () => { + await Promise.all([ + qc.invalidateQueries({ queryKey: ["collections", site?.id] }), + qc.invalidateQueries({ queryKey: ["site", site?.id] }), + ]); + close(); + toast.success( + t("collections.create.created"), + t("collections.create.createdDescription", { name: input.name() }), + ); + if (site) + await navigate({ + to: "/sites/$siteId/collections/$collection", + params: { siteId: site.id, collection: input.key }, + }); + }, + }); +} diff --git a/apps/studio/src/components/TaxonomySettingsDialog.tsx b/apps/studio/src/components/TaxonomySettingsDialog.tsx index aed98994..672d688a 100644 --- a/apps/studio/src/components/TaxonomySettingsDialog.tsx +++ b/apps/studio/src/components/TaxonomySettingsDialog.tsx @@ -18,6 +18,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { type ReactNode, useEffect, useState } from "react"; import { type MessageKey, useI18n, useT } from "../i18n/index.tsx"; import { api } from "../lib/api.ts"; +import { useCollectionKeys } from "../lib/collections.ts"; import { primaryHostname } from "../lib/site-settings.ts"; import { archiveChange, @@ -63,7 +64,7 @@ const PATH_ERROR: Record = { const NEW_TERMS = ["none", "propose", "free"] as const; /** A `Switch` with its explanation underneath (related text close together). */ -function SwitchField({ +export function SwitchField({ label, hint, checked, @@ -181,7 +182,7 @@ export function TaxonomySettingsDialog({ const editing = taxonomy !== undefined; const set = (patch: Partial) => setDraft((d) => ({ ...d, ...patch })); - const collections = knownCollections(entries.data ?? [], draft); + const collections = knownCollections(entries.data ?? [], draft, useCollectionKeys(siteId)); const errors = validateTaxonomy(draft, { defaultLocale, taxonomies: data.taxonomies.data ?? [], diff --git a/apps/studio/src/components/useCollectionView.ts b/apps/studio/src/components/useCollectionView.ts new file mode 100644 index 00000000..0e40a42c --- /dev/null +++ b/apps/studio/src/components/useCollectionView.ts @@ -0,0 +1,97 @@ +/** + * RFC-0040 §4: what a collection's list screen needs beyond the Articles list it generalises: the + * collection from the route, its name in Studio's language, a "New" that creates in it, and only + * the taxonomies that apply to it (`Taxonomy.collections`, absent = every collection). + */ +import type { Taxonomy } from "@cloudhal/core-contracts"; +import { BUILT_IN_COLLECTION } from "@cloudhal/core-ts/collections"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useParams } from "@tanstack/react-router"; +import { useI18n } from "../i18n/index.tsx"; +import { api } from "../lib/api.ts"; +import { COLLECTION_ICONS } from "../lib/collection-icons.ts"; +import { collectionIcon, collectionNames } from "../lib/collections.ts"; +import type { TaxonomyData } from "./Taxonomy.tsx"; + +/** `$collection` of `/sites/$siteId/collections/$collection`; `posts` elsewhere (`/articles`). */ +export function useCollectionParam(): string { + const params = useParams({ strict: false }) as { collection?: string }; + return params.collection ?? BUILT_IN_COLLECTION; +} + +/** Whether `tax` applies to `collection`. */ +export function appliesTo(tax: Pick | undefined, collection: string) { + return tax !== undefined && (!tax.collections || tax.collections.includes(collection)); +} + +/** The taxonomy data with only the default taxonomies that apply to `collection`. */ +export function scopeTaxonomies(data: TaxonomyData, collection: string): TaxonomyData { + const { category, tag } = data.defaults; + return { + ...data, + defaults: { + ...data.defaults, + category: appliesTo(category, collection) ? category : undefined, + tag: appliesTo(tag, collection) ? tag : undefined, + }, + }; +} + +/** + * The screen's title, description and "New" label. `posts` without labels keeps Studio's own + * Articles wording; another collection without labels is shown by its key. + */ +export function useCollectionHeader(siteId: string, collection: string) { + const { t, locale } = useI18n(); + const site = useQuery({ queryKey: ["site", siteId], queryFn: () => api.sites.get(siteId) }); + const names = collectionNames(site.data, collection, locale); + const builtIn = collection === BUILT_IN_COLLECTION && !names; + const description = site.data?.collections?.[collection]?.labels?.[locale]?.description; + return { + title: builtIn ? t("pageHeaders.articles.title") : (names?.plural ?? collection), + description: builtIn + ? t("pageHeaders.articles.description") + : (description ?? t("collectionList.description", { key: collection })), + newLabel: builtIn ? t("articles.newArticle") : t("collectionList.newEntry"), + search: builtIn ? t("articles.search") : t("collectionList.search"), + empty: builtIn ? t("articles.empty") : t("collectionList.empty"), + Icon: collectionIcon( + { key: collection, settings: site.data?.collections?.[collection] ?? {} }, + COLLECTION_ICONS, + ), + }; +} + +/** Creates an untitled draft in `collection` and opens it in the editor. */ +export function useNewEntry(siteId: string, collection: string, locale: string) { + const { t } = useI18n(); + const qc = useQueryClient(); + const navigate = useNavigate(); + return useMutation({ + mutationFn: () => + api.entries.create(siteId, { + collection, + slug: `untitled-${Date.now().toString(36)}`, + locale, + status: "draft", + title: t("articles.untitled"), + body: { format: "markdown", value: "" }, + fields: {}, + tags: [], + }), + onSuccess: async (entry) => { + await qc.invalidateQueries({ queryKey: ["entries", siteId] }); + await navigate({ + to: "/sites/$siteId/articles/$entryId", + params: { siteId, entryId: entry.id }, + }); + }, + }); +} + +/** The editor's first crumb: the entry's collection list (Articles for `posts`). */ +export function useCollectionCrumb(siteId: string, collection: string | undefined) { + const key = collection ?? BUILT_IN_COLLECTION; + const { title } = useCollectionHeader(siteId, key); + return { label: title, href: `/sites/${siteId}/collections/${key}` }; +} diff --git a/apps/studio/src/i18n/ar.ts b/apps/studio/src/i18n/ar.ts index 46e6199f..45ed2a62 100644 --- a/apps/studio/src/i18n/ar.ts +++ b/apps/studio/src/i18n/ar.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ar core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 73826adcc08f4e01 +// hash: f37d12b800218c0f import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -175,6 +175,7 @@ export const messages: Messages = { home: "نظرة عامة", pages: "الصفحات", articles: "المقالات", + collections: "المجموعات", snapshots: "اللقطات", agents: "الوكلاء", allSites: "كل المواقع", @@ -298,6 +299,13 @@ export const messages: Messages = { emptyDescription: "جرّب فئة أو وسمًا آخر، أو امسح عوامل التصفية.", }, }, + collectionList: { + description: "إدخالات مجموعة «{key}».", + newEntry: "إدخال جديد", + search: "ابحث في الإدخالات", + empty: "لا توجد إدخالات بعد", + settings: "الإعدادات", + }, articleCorrections: { title: "التصحيحات", description: diff --git a/apps/studio/src/i18n/de.ts b/apps/studio/src/i18n/de.ts index 5afc9788..72c133ba 100644 --- a/apps/studio/src/i18n/de.ts +++ b/apps/studio/src/i18n/de.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): de core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 44b05f74914d641c +// hash: 27ec0bf6abd925e4 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -181,6 +181,7 @@ export const messages: Messages = { home: "Übersicht", pages: "Seiten", articles: "Artikel", + collections: "Sammlungen", snapshots: "Snapshots", agents: "Agents", allSites: "Alle Websites", @@ -304,6 +305,13 @@ export const messages: Messages = { "Versuche eine andere Kategorie oder ein anderes Schlagwort oder setze die Filter zurück.", }, }, + collectionList: { + description: "Einträge der Sammlung „{key}“.", + newEntry: "Neuer Eintrag", + search: "Einträge durchsuchen", + empty: "Noch keine Einträge", + settings: "Einstellungen", + }, articleCorrections: { title: "Korrekturen", description: diff --git a/apps/studio/src/i18n/en.ts b/apps/studio/src/i18n/en.ts index 0713dd8a..e68316f9 100644 --- a/apps/studio/src/i18n/en.ts +++ b/apps/studio/src/i18n/en.ts @@ -176,6 +176,7 @@ export const en = { home: "Overview", pages: "Pages", articles: "Articles", + collections: "Collections", snapshots: "Snapshots", agents: "Agents", allSites: "All sites", @@ -293,6 +294,13 @@ export const en = { emptyDescription: "Try another category or tag, or clear the filters.", }, }, + collectionList: { + description: "Entries of the “{key}” collection.", + newEntry: "New entry", + search: "Search entries", + empty: "No entries yet", + settings: "Settings", + }, articleCorrections: { title: "Corrections", description: diff --git a/apps/studio/src/i18n/es.ts b/apps/studio/src/i18n/es.ts index 2750877e..59131329 100644 --- a/apps/studio/src/i18n/es.ts +++ b/apps/studio/src/i18n/es.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): es core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 5a723f044877f0c7 +// hash: 3732892c59ef4c5a import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -180,6 +180,7 @@ export const messages: Messages = { home: "Resumen", pages: "Páginas", articles: "Artículos", + collections: "Colecciones", snapshots: "Instantáneas", agents: "Agentes", allSites: "Todos los sitios", @@ -303,6 +304,13 @@ export const messages: Messages = { emptyDescription: "Prueba con otra categoría o etiqueta, o borra los filtros.", }, }, + collectionList: { + description: "Entradas de la colección «{key}».", + newEntry: "Nueva entrada", + search: "Buscar entradas", + empty: "Aún no hay entradas", + settings: "Configuración", + }, articleCorrections: { title: "Correcciones", description: diff --git a/apps/studio/src/i18n/fa.ts b/apps/studio/src/i18n/fa.ts index 7a45a2a0..e8bd60c3 100644 --- a/apps/studio/src/i18n/fa.ts +++ b/apps/studio/src/i18n/fa.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): fa core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 1b955626801b1c9a +// hash: 9d54cb3c29a5cfcd import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -178,6 +178,7 @@ export const messages: Messages = { home: "نمای کلی", pages: "صفحه‌ها", articles: "مقاله‌ها", + collections: "مجموعه‌ها", snapshots: "اسنپ‌شات‌ها", agents: "عامل‌ها", allSites: "همهٔ سایت‌ها", @@ -300,6 +301,13 @@ export const messages: Messages = { emptyDescription: "دسته یا برچسب دیگری را امتحان کنید، یا فیلترها را پاک کنید.", }, }, + collectionList: { + description: "ورودی‌های مجموعهٔ «{key}».", + newEntry: "ورودی جدید", + search: "جستجوی ورودی‌ها", + empty: "هنوز ورودی‌ای نیست", + settings: "تنظیمات", + }, articleCorrections: { title: "اصلاحیه‌ها", description: diff --git a/apps/studio/src/i18n/fr.ts b/apps/studio/src/i18n/fr.ts index ee763aac..fa831ed8 100644 --- a/apps/studio/src/i18n/fr.ts +++ b/apps/studio/src/i18n/fr.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): fr core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 50175a555a425cfc +// hash: c66c036917e4445e import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -181,6 +181,7 @@ export const messages: Messages = { home: "Vue d’ensemble", pages: "Pages", articles: "Articles", + collections: "Collections", snapshots: "Instantanés", agents: "Agents", allSites: "Tous les sites", @@ -305,6 +306,13 @@ export const messages: Messages = { emptyDescription: "Essayez une autre catégorie ou étiquette, ou effacez les filtres.", }, }, + collectionList: { + description: "Entrées de la collection « {key} ».", + newEntry: "Nouvelle entrée", + search: "Rechercher des entrées", + empty: "Aucune entrée pour l’instant", + settings: "Paramètres", + }, articleCorrections: { title: "Corrections", description: diff --git a/apps/studio/src/i18n/generated/params.ts b/apps/studio/src/i18n/generated/params.ts index 146b62a2..6a5819e0 100644 --- a/apps/studio/src/i18n/generated/params.ts +++ b/apps/studio/src/i18n/generated/params.ts @@ -144,6 +144,12 @@ export interface MessageParams { "build.chat.scopeSection": { index: string | number }; "build.chat.writing": { components: string | number }; "build.title": { title: string | number }; + "collectionList.description": { key: string | number }; + "collections.create.createdDescription": { name: string | number }; + "collections.entries": { count: number }; + "collections.settings.key": { key: string | number }; + "collections.settings.taxonomyAll": { label: string | number }; + "collections.settings.title": { name: string | number }; "comments.agentRequests.title": { count: number; agent: string | number }; "comments.agentRequests.titleSeveral": { count: number }; "comments.approvals.approveConfirm": { count: number }; diff --git a/apps/studio/src/i18n/he.ts b/apps/studio/src/i18n/he.ts index ef2160a3..3ce0b18d 100644 --- a/apps/studio/src/i18n/he.ts +++ b/apps/studio/src/i18n/he.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): he core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 7886dad64d634752 +// hash: d9d3d828dbc54197 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -175,6 +175,7 @@ export const messages: Messages = { home: "סקירה", pages: "עמודים", articles: "מאמרים", + collections: "אוספים", snapshots: "תמונות מצב", agents: "סוכנים", allSites: "כל האתרים", @@ -298,6 +299,13 @@ export const messages: Messages = { emptyDescription: "אפשר לנסות קטגוריה או תגית אחרת, או לנקות את המסננים.", }, }, + collectionList: { + description: "רשומות באוסף “{key}”.", + newEntry: "רשומה חדשה", + search: "חיפוש רשומות", + empty: "אין עדיין רשומות", + settings: "הגדרות", + }, articleCorrections: { title: "תיקונים", description: diff --git a/apps/studio/src/i18n/hi.ts b/apps/studio/src/i18n/hi.ts index fdf97267..8f4afc63 100644 --- a/apps/studio/src/i18n/hi.ts +++ b/apps/studio/src/i18n/hi.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): hi core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 64d97f408260561a +// hash: 92a4c9c865173891 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -175,6 +175,7 @@ export const messages: Messages = { home: "अवलोकन", pages: "पेज", articles: "लेख", + collections: "संग्रह", snapshots: "स्नैपशॉट", agents: "एजेंट", allSites: "सभी साइटें", @@ -297,6 +298,13 @@ export const messages: Messages = { emptyDescription: "कोई दूसरी श्रेणी या टैग आज़माएँ, या फ़िल्टर साफ़ करें।", }, }, + collectionList: { + description: "“{key}” संग्रह की प्रविष्टियाँ।", + newEntry: "नई प्रविष्टि", + search: "प्रविष्टियाँ खोजें", + empty: "अभी कोई प्रविष्टि नहीं", + settings: "सेटिंग्स", + }, articleCorrections: { title: "सुधार", description: diff --git a/apps/studio/src/i18n/id.ts b/apps/studio/src/i18n/id.ts index ae83804a..6d270b82 100644 --- a/apps/studio/src/i18n/id.ts +++ b/apps/studio/src/i18n/id.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): id core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 4a4445427fe42698 +// hash: 88cb08a31fd712aa import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -179,6 +179,7 @@ export const messages: Messages = { home: "Ringkasan", pages: "Halaman", articles: "Artikel", + collections: "Koleksi", snapshots: "Snapshot", agents: "Agen", allSites: "Semua situs", @@ -300,6 +301,13 @@ export const messages: Messages = { emptyDescription: "Coba kategori atau tag lain, atau hapus filter.", }, }, + collectionList: { + description: "Entri koleksi “{key}”.", + newEntry: "Entri baru", + search: "Cari entri", + empty: "Belum ada entri", + settings: "Pengaturan", + }, articleCorrections: { title: "Koreksi", description: diff --git a/apps/studio/src/i18n/it.ts b/apps/studio/src/i18n/it.ts index d3fb4163..19f407c3 100644 --- a/apps/studio/src/i18n/it.ts +++ b/apps/studio/src/i18n/it.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): it core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: db1b5e144b3bf167 +// hash: bb548a608f22d7d8 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -179,6 +179,7 @@ export const messages: Messages = { home: "Panoramica", pages: "Pagine", articles: "Articoli", + collections: "Raccolte", snapshots: "Snapshot", agents: "Agenti", allSites: "Tutti i siti", @@ -302,6 +303,13 @@ export const messages: Messages = { emptyDescription: "Prova un’altra categoria o un altro tag, oppure cancella i filtri.", }, }, + collectionList: { + description: "Voci della raccolta “{key}”.", + newEntry: "Nuova voce", + search: "Cerca voci", + empty: "Ancora nessuna voce", + settings: "Impostazioni", + }, articleCorrections: { title: "Correzioni", description: diff --git a/apps/studio/src/i18n/ja.ts b/apps/studio/src/i18n/ja.ts index 664a5338..a00a854a 100644 --- a/apps/studio/src/i18n/ja.ts +++ b/apps/studio/src/i18n/ja.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ja core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 1fdf4708ec854f00 +// hash: ab64871c684608cf import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -178,6 +178,7 @@ export const messages: Messages = { home: "概要", pages: "ページ", articles: "記事", + collections: "コレクション", snapshots: "スナップショット", agents: "エージェント", allSites: "すべてのサイト", @@ -299,6 +300,13 @@ export const messages: Messages = { emptyDescription: "別のカテゴリーやタグを試すか、フィルターをクリアしてください。", }, }, + collectionList: { + description: "コレクション「{key}」のエントリー。", + newEntry: "新しいエントリー", + search: "エントリーを検索", + empty: "まだエントリーはありません", + settings: "設定", + }, articleCorrections: { title: "訂正", description: diff --git a/apps/studio/src/i18n/ko.ts b/apps/studio/src/i18n/ko.ts index 36f1d871..b81bfa17 100644 --- a/apps/studio/src/i18n/ko.ts +++ b/apps/studio/src/i18n/ko.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ko core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: a64ee6c70c5e957a +// hash: 9fdcdca79060394e import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -176,6 +176,7 @@ export const messages: Messages = { home: "개요", pages: "페이지", articles: "글", + collections: "컬렉션", snapshots: "스냅샷", agents: "에이전트", allSites: "모든 사이트", @@ -297,6 +298,13 @@ export const messages: Messages = { emptyDescription: "다른 카테고리나 태그를 선택하거나 필터를 지우세요.", }, }, + collectionList: { + description: "“{key}” 컬렉션의 항목입니다.", + newEntry: "새 항목", + search: "항목 검색", + empty: "아직 항목이 없습니다", + settings: "설정", + }, articleCorrections: { title: "정정", description: diff --git a/apps/studio/src/i18n/lazy/collections.ar.ts b/apps/studio/src/i18n/lazy/collections.ar.ts new file mode 100644 index 00000000..e6beb658 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.ar.ts @@ -0,0 +1,98 @@ +// Generated by `pnpm i18n:import` (ADR-0034): ar `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 433145927a900327 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "المجموعات", + description: + "أنواع المحتوى التي يحتفظ بها هذا الموقع: مقالات وفعاليات وفريق ووظائف، وكل ما له إدخالات خاصة به. لكل مجموعة قائمتها وعنوانها وإعداداتها.", + newCollection: "مجموعة جديدة", + search: "ابحث في المجموعات", + columns: { + name: "الاسم", + address: "العنوان", + entries: "الإدخالات", + state: "الحالة", + }, + entries: + "{count, plural, zero {# إدخال} one {إدخال واحد} two {إدخالان} few {# إدخالات} many {# إدخالًا} other {# إدخال}}", + declared: "مُسمّاة", + inferred: "لم تُسمَّ بعد", + inferredHint: "توجد إدخالات لكن لم يمنح أحد هذه المجموعة اسمًا. افتح إعداداتها لتمنحها اسمًا.", + empty: "لا توجد مجموعات بعد", + emptyDescription: "أنشئ مجموعة للفعاليات أو أعضاء الفريق أو الوظائف أو أي شيء آخر.", + create: { + title: "مجموعة جديدة", + description: + "نوع جديد من المحتوى بقائمته الخاصة في الشريط الجانبي. يمكن للوكلاء كتابة إدخالات فيه بعد إنشائه.", + plural: "الاسم", + pluralHint: "بصيغة الجمع كما يعرضه الشريط الجانبي، مثل: الفعاليات.", + singular: "يُسمّى الإدخال الواحد", + singularHint: "بصيغة المفرد، مثل: فعالية.", + key: "المفتاح", + keyHint: + "أحرف إنجليزية صغيرة وأرقام وواصلات. تستخدمه السمات والمرشّحات والوكلاء، ولا يمكن تغييره لاحقًا.", + keyInvalid: "استخدم أحرفًا إنجليزية صغيرة وأرقامًا وواصلات.", + required: "مطلوب", + icon: "الأيقونة", + address: "العنوان", + addressHint: "توجد الإدخالات تحت هذه البادئة متبوعةً بمعرّفها (slug).", + submit: "أنشئ المجموعة", + failed: "لم تُنشأ المجموعة", + created: "أُنشئت المجموعة", + createdDescription: "{name} في الشريط الجانبي. أضف إدخالها الأول.", + }, + icons: { + article: "مقال", + calendar: "تقويم", + briefcase: "حقيبة", + users: "أشخاص", + images: "صور", + package: "طرد", + "book-open": "كتاب", + megaphone: "مكبّر صوت", + quotes: "اقتباسات", + question: "سؤال", + "map-pin": "دبوس خريطة", + files: "ملفات", + }, + settings: { + title: "إعدادات {name}", + description: "كيف تُسمّى هذه المجموعة، وأين توجد إدخالاتها، وما الذي يعرضها.", + names: "الأسماء", + namesDescription: + "بكل لغة من لغات الموقع. يعرض Studio والوكلاء والسمات هذه الأسماء بدل المفتاح.", + descriptionField: "الوصف", + descriptionHint: "اختياري. يظهر تحت عنوان قائمة المجموعة.", + appearance: "الأيقونة", + appearanceDescription: "أيقونة المجموعة في الشريط الجانبي.", + listing: "العرض", + listingDescription: "أين تظهر إدخالات المجموعة في الموقع العام.", + feed: "تضمين في خلاصات RSS", + feedHint: "تعرض خلاصات الموقع أحدث إدخالات كل مجموعة فعّلت هذا الخيار.", + sitemap: "تضمين في خريطة الموقع", + sitemapHint: "تعثر محركات البحث على الإدخالات المنشورة عبر sitemap.xml.", + sort: "الترتيب الافتراضي", + sortHint: "تستخدمه قائمة Studio وقوائم السمات التي لا تحدد ترتيبًا.", + sorts: { + newest: "الأحدث أولًا", + oldest: "الأقدم أولًا", + title: "حسب العنوان", + }, + taxonomies: "الفئات والوسوم", + taxonomiesDescription: + "التصنيفات التي تستخدمها إدخالات هذه المجموعة. التصنيف الذي ليس له قائمة مجموعات ينطبق عليها جميعًا.", + taxonomyAll: "{label} (كل المجموعات)", + noTaxonomies: "لا توجد تصنيفات في هذا الموقع.", + address: "العنوان", + addressDescription: + "أين توجد الإدخالات. تغييره ينقل عناوين URL العامة لها: تردّ العناوين القديمة بإعادة توجيه دائمة.", + addressLink: "غيّره في إعدادات الموقع › المجموعات.", + save: "احفظ", + saved: "حُفظت المجموعة", + key: "المفتاح: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.de.ts b/apps/studio/src/i18n/lazy/collections.de.ts new file mode 100644 index 00000000..1ab84eab --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.de.ts @@ -0,0 +1,100 @@ +// Generated by `pnpm i18n:import` (ADR-0034): de `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 6120d8f4fc83e512 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Sammlungen", + description: + "Die Arten von Inhalten, die diese Website verwaltet: Artikel, Veranstaltungen, Team, Stellen, alles mit eigenen Einträgen. Jede hat ihre eigene Liste, ihre Adresse und ihre Einstellungen.", + newCollection: "Neue Sammlung", + search: "Sammlungen durchsuchen", + columns: { + name: "Name", + address: "Adresse", + entries: "Einträge", + state: "Status", + }, + entries: "{count, plural, one {# Eintrag} other {# Einträge}}", + declared: "Benannt", + inferred: "Noch nicht benannt", + inferredHint: + "Es gibt Einträge, aber niemand hat diese Sammlung benannt. Öffne ihre Einstellungen, um ihr einen Namen zu geben.", + empty: "Noch keine Sammlungen", + emptyDescription: + "Lege eine für Veranstaltungen, Teammitglieder, Stellenangebote oder anderes an.", + create: { + title: "Neue Sammlung", + description: + "Eine neue Art von Inhalt mit eigener Liste in der Seitenleiste. Agents können Einträge hineinschreiben, sobald sie existiert.", + plural: "Name", + pluralHint: "Plural, so wie ihn die Seitenleiste zeigt, z. B. Veranstaltungen.", + singular: "Ein Eintrag heißt", + singularHint: "Singular, z. B. Veranstaltung.", + key: "Schlüssel", + keyHint: + "Kleinbuchstaben, Ziffern und Bindestriche. Themes, Filter und Agents verwenden ihn; er lässt sich später nicht ändern.", + keyInvalid: "Verwende Kleinbuchstaben, Ziffern und Bindestriche.", + required: "Pflichtfeld", + icon: "Symbol", + address: "Adresse", + addressHint: "Einträge liegen unter diesem Präfix, gefolgt von ihrem Slug.", + submit: "Sammlung anlegen", + failed: "Die Sammlung wurde nicht angelegt", + created: "Sammlung angelegt", + createdDescription: "{name} steht in der Seitenleiste. Füge den ersten Eintrag hinzu.", + }, + icons: { + article: "Artikel", + calendar: "Kalender", + briefcase: "Aktentasche", + users: "Personen", + images: "Bilder", + package: "Paket", + "book-open": "Buch", + megaphone: "Megafon", + quotes: "Zitate", + question: "Frage", + "map-pin": "Kartenmarker", + files: "Dateien", + }, + settings: { + title: "Einstellungen für {name}", + description: "Wie diese Sammlung heißt, wo ihre Einträge liegen und was sie auflistet.", + names: "Namen", + namesDescription: + "In jeder Sprache der Website. Studio, Agents und Themes zeigen diese Namen statt des Schlüssels.", + descriptionField: "Beschreibung", + descriptionHint: "Optional. Wird unter dem Titel der Liste der Sammlung angezeigt.", + appearance: "Symbol", + appearanceDescription: "Das Symbol der Sammlung in der Seitenleiste.", + listing: "Auflistung", + listingDescription: "Wo die Einträge der Sammlung auf der öffentlichen Website erscheinen.", + feed: "In RSS-Feeds aufnehmen", + feedHint: + "Die Feeds der Website listen die neuesten Einträge jeder Sammlung auf, bei der dies aktiviert ist.", + sitemap: "In die Sitemap aufnehmen", + sitemapHint: "Suchmaschinen finden veröffentlichte Einträge über sitemap.xml.", + sort: "Standardreihenfolge", + sortHint: "Die Liste in Studio und Theme-Listen ohne eigene Reihenfolge verwenden sie.", + sorts: { + newest: "Neueste zuerst", + oldest: "Älteste zuerst", + title: "Nach Titel", + }, + taxonomies: "Kategorien & Schlagwörter", + taxonomiesDescription: + "Welche Taxonomien die Einträge dieser Sammlung verwenden. Eine Taxonomie ohne Liste von Sammlungen gilt für alle.", + taxonomyAll: "{label} (jede Sammlung)", + noTaxonomies: "Diese Website hat keine Taxonomien.", + address: "Adresse", + addressDescription: + "Wo Einträge liegen. Eine Änderung verschiebt ihre öffentlichen URLs: Alte Adressen antworten mit einer dauerhaften Weiterleitung.", + addressLink: "Ändere sie unter Website-Einstellungen › Sammlungen.", + save: "Speichern", + saved: "Sammlung gespeichert", + key: "Schlüssel: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.en.ts b/apps/studio/src/i18n/lazy/collections.en.ts new file mode 100644 index 00000000..907ef7f9 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.en.ts @@ -0,0 +1,90 @@ +/** RFC-0040: the Collections overview, "New collection" and a collection's settings. */ +export const messages = { + collections: { + title: "Collections", + description: + "The kinds of content this site keeps: articles, events, team, jobs, anything with entries of its own. Each has its own list, its address and its settings.", + newCollection: "New collection", + search: "Search collections", + columns: { + name: "Name", + address: "Address", + entries: "Entries", + state: "State", + }, + entries: "{count, plural, one {# entry} other {# entries}}", + declared: "Named", + inferred: "Not named yet", + inferredHint: + "Entries exist, but nobody has named this collection. Open its settings to give it a name.", + empty: "No collections yet", + emptyDescription: "Create one for events, team members, jobs or anything else.", + create: { + title: "New collection", + description: + "A new kind of content with its own list in the sidebar. Agents may write entries into it once it exists.", + plural: "Name", + pluralHint: "Plural, as the sidebar shows it, e.g. Events.", + singular: "One entry is called", + singularHint: "Singular, e.g. Event.", + key: "Key", + keyHint: + "Lower-case letters, digits and hyphens. Themes, filters and agents use it; it cannot be changed later.", + keyInvalid: "Use lower-case letters, digits and hyphens.", + required: "Required", + icon: "Icon", + address: "Address", + addressHint: "Entries live at this prefix followed by their slug.", + submit: "Create collection", + failed: "The collection was not created", + created: "Collection created", + createdDescription: "{name} is in the sidebar. Add its first entry.", + }, + icons: { + article: "Article", + calendar: "Calendar", + briefcase: "Briefcase", + users: "People", + images: "Images", + package: "Package", + "book-open": "Book", + megaphone: "Megaphone", + quotes: "Quotes", + question: "Question", + "map-pin": "Map pin", + files: "Files", + }, + settings: { + title: "{name} settings", + description: "How this collection is named, where its entries live and what lists them.", + names: "Names", + namesDescription: + "In every language of the site. Studio, agents and themes show these instead of the key.", + descriptionField: "Description", + descriptionHint: "Optional. Shown under the title of the collection's list.", + appearance: "Icon", + appearanceDescription: "The collection's icon in the sidebar.", + listing: "Listing", + listingDescription: "Where the collection's entries appear on the public site.", + feed: "Include in RSS feeds", + feedHint: "The site's feeds list the newest entries of every collection that has this on.", + sitemap: "Include in the sitemap", + sitemapHint: "Search engines find published entries through sitemap.xml.", + sort: "Default order", + sortHint: "Studio's list and theme lists that name no order use it.", + sorts: { newest: "Newest first", oldest: "Oldest first", title: "By title" }, + taxonomies: "Categories & tags", + taxonomiesDescription: + "Which taxonomies entries of this collection use. A taxonomy without a list of collections applies to all of them.", + taxonomyAll: "{label} (every collection)", + noTaxonomies: "This site has no taxonomies.", + address: "Address", + addressDescription: + "Where entries live. Changing it moves their public URLs: old addresses answer with a permanent redirect.", + addressLink: "Change it in Site settings › Collections.", + save: "Save", + saved: "Collection saved", + key: "Key: {key}", + }, + }, +} as const; diff --git a/apps/studio/src/i18n/lazy/collections.es.ts b/apps/studio/src/i18n/lazy/collections.es.ts new file mode 100644 index 00000000..c16a98be --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.es.ts @@ -0,0 +1,100 @@ +// Generated by `pnpm i18n:import` (ADR-0034): es `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 363395ac4cb751fc +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Colecciones", + description: + "Los tipos de contenido que mantiene este sitio: artículos, eventos, equipo, empleos, cualquier cosa con entradas propias. Cada una tiene su lista, su dirección y su configuración.", + newCollection: "Nueva colección", + search: "Buscar colecciones", + columns: { + name: "Nombre", + address: "Dirección", + entries: "Entradas", + state: "Estado", + }, + entries: "{count, plural, one {# entrada} many {# de entradas} other {# entradas}}", + declared: "Con nombre", + inferred: "Sin nombre todavía", + inferredHint: + "Hay entradas, pero nadie ha dado nombre a esta colección. Abre su configuración para ponerle uno.", + empty: "Aún no hay colecciones", + emptyDescription: "Crea una para eventos, miembros del equipo, empleos o lo que necesites.", + create: { + title: "Nueva colección", + description: + "Un nuevo tipo de contenido con su propia lista en la barra lateral. Los agentes podrán escribir entradas en ella cuando exista.", + plural: "Nombre", + pluralHint: "Plural, tal como lo muestra la barra lateral, p. ej. Eventos.", + singular: "Una entrada se llama", + singularHint: "Singular, p. ej. Evento.", + key: "Clave", + keyHint: + "Letras minúsculas, dígitos y guiones. Los temas, filtros y agentes la usan; no se puede cambiar después.", + keyInvalid: "Usa letras minúsculas, dígitos y guiones.", + required: "Obligatorio", + icon: "Icono", + address: "Dirección", + addressHint: "Las entradas viven en este prefijo seguido de su slug.", + submit: "Crear colección", + failed: "No se creó la colección", + created: "Colección creada", + createdDescription: "{name} está en la barra lateral. Añade su primera entrada.", + }, + icons: { + article: "Artículo", + calendar: "Calendario", + briefcase: "Maletín", + users: "Personas", + images: "Imágenes", + package: "Paquete", + "book-open": "Libro", + megaphone: "Megáfono", + quotes: "Citas", + question: "Pregunta", + "map-pin": "Marcador de mapa", + files: "Archivos", + }, + settings: { + title: "Configuración de {name}", + description: "Cómo se llama esta colección, dónde viven sus entradas y qué las lista.", + names: "Nombres", + namesDescription: + "En cada idioma del sitio. Studio, los agentes y los temas muestran estos nombres en lugar de la clave.", + descriptionField: "Descripción", + descriptionHint: "Opcional. Se muestra bajo el título de la lista de la colección.", + appearance: "Icono", + appearanceDescription: "El icono de la colección en la barra lateral.", + listing: "Listado", + listingDescription: "Dónde aparecen las entradas de la colección en el sitio público.", + feed: "Incluir en los feeds RSS", + feedHint: + "Los feeds del sitio listan las entradas más recientes de cada colección que lo tenga activado.", + sitemap: "Incluir en el sitemap", + sitemapHint: + "Los motores de búsqueda encuentran las entradas publicadas mediante sitemap.xml.", + sort: "Orden predeterminado", + sortHint: "La lista de Studio y las listas de temas que no indican orden lo usan.", + sorts: { + newest: "Más recientes primero", + oldest: "Más antiguas primero", + title: "Por título", + }, + taxonomies: "Categorías y etiquetas", + taxonomiesDescription: + "Qué taxonomías usan las entradas de esta colección. Una taxonomía sin lista de colecciones se aplica a todas.", + taxonomyAll: "{label} (todas las colecciones)", + noTaxonomies: "Este sitio no tiene taxonomías.", + address: "Dirección", + addressDescription: + "Dónde viven las entradas. Cambiarla mueve sus URL públicas: las direcciones antiguas responden con una redirección permanente.", + addressLink: "Cámbiala en Configuración del sitio › Colecciones.", + save: "Guardar", + saved: "Colección guardada", + key: "Clave: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.fa.ts b/apps/studio/src/i18n/lazy/collections.fa.ts new file mode 100644 index 00000000..37eec124 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.fa.ts @@ -0,0 +1,99 @@ +// Generated by `pnpm i18n:import` (ADR-0034): fa `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 307fbabd3c272ef6 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "مجموعه‌ها", + description: + "انواع محتوایی که این سایت نگه می‌دارد: مقاله، رویداد، تیم، فرصت شغلی و هر چیزی که ورودی‌های خودش را دارد. هر مجموعه فهرست، نشانی و تنظیمات خودش را دارد.", + newCollection: "مجموعهٔ جدید", + search: "جستجوی مجموعه‌ها", + columns: { + name: "نام", + address: "نشانی", + entries: "ورودی‌ها", + state: "وضعیت", + }, + entries: "{count, plural, one {# ورودی} other {# ورودی}}", + declared: "نام‌دار", + inferred: "هنوز نام ندارد", + inferredHint: + "ورودی‌هایی وجود دارد، اما کسی برای این مجموعه نامی نگذاشته است. تنظیماتش را باز کنید و نامی برایش بگذارید.", + empty: "هنوز مجموعه‌ای وجود ندارد", + emptyDescription: "برای رویدادها، اعضای تیم، فرصت‌های شغلی یا هر چیز دیگری یک مجموعه بسازید.", + create: { + title: "مجموعهٔ جدید", + description: + "نوع تازه‌ای از محتوا با فهرست مخصوص خودش در نوار کناری. عامل‌ها پس از ساخته شدن مجموعه می‌توانند در آن ورودی بنویسند.", + plural: "نام", + pluralHint: "به صورت جمع، همان‌طور که نوار کناری نشان می‌دهد، مثلاً رویدادها.", + singular: "هر ورودی چه نامیده می‌شود", + singularHint: "به صورت مفرد، مثلاً رویداد.", + key: "کلید", + keyHint: + "حروف کوچک انگلیسی، رقم و خط تیره. قالب‌ها، فیلترها و عامل‌ها از آن استفاده می‌کنند و بعداً قابل تغییر نیست.", + keyInvalid: "از حروف کوچک انگلیسی، رقم و خط تیره استفاده کنید.", + required: "الزامی", + icon: "نماد", + address: "نشانی", + addressHint: "ورودی‌ها زیر این پیشوند و به دنبال آن slug خودشان قرار می‌گیرند.", + submit: "ساخت مجموعه", + failed: "مجموعه ساخته نشد", + created: "مجموعه ساخته شد", + createdDescription: "{name} در نوار کناری است. نخستین ورودی‌اش را اضافه کنید.", + }, + icons: { + article: "مقاله", + calendar: "تقویم", + briefcase: "کیف", + users: "افراد", + images: "تصاویر", + package: "بسته", + "book-open": "کتاب", + megaphone: "بلندگو", + quotes: "نقل‌قول‌ها", + question: "پرسش", + "map-pin": "نشانگر نقشه", + files: "پرونده‌ها", + }, + settings: { + title: "تنظیمات {name}", + description: "نام این مجموعه چیست، ورودی‌هایش کجا قرار دارند و چه چیزی آن‌ها را فهرست می‌کند.", + names: "نام‌ها", + namesDescription: + "در هر زبان سایت. Studio، عامل‌ها و قالب‌ها به جای کلید این نام‌ها را نشان می‌دهند.", + descriptionField: "توضیح", + descriptionHint: "اختیاری. زیر عنوان فهرست مجموعه نشان داده می‌شود.", + appearance: "نماد", + appearanceDescription: "نماد مجموعه در نوار کناری.", + listing: "فهرست‌شدن", + listingDescription: "جایی که ورودی‌های مجموعه در سایت عمومی ظاهر می‌شوند.", + feed: "گنجاندن در فیدهای RSS", + feedHint: + "فیدهای سایت جدیدترین ورودی‌های هر مجموعه‌ای را که این گزینه برایش روشن است فهرست می‌کنند.", + sitemap: "گنجاندن در sitemap", + sitemapHint: "موتورهای جستجو ورودی‌های منتشرشده را از طریق sitemap.xml پیدا می‌کنند.", + sort: "ترتیب پیش‌فرض", + sortHint: "فهرست Studio و فهرست‌های قالبی که ترتیبی تعیین نکرده‌اند از آن استفاده می‌کنند.", + sorts: { + newest: "جدیدترین نخست", + oldest: "قدیمی‌ترین نخست", + title: "بر اساس عنوان", + }, + taxonomies: "دسته‌ها و برچسب‌ها", + taxonomiesDescription: + "ورودی‌های این مجموعه از کدام طبقه‌بندی‌ها استفاده می‌کنند. طبقه‌بندی‌ای که فهرست مجموعه ندارد برای همهٔ آن‌ها اعمال می‌شود.", + taxonomyAll: "{label} (همهٔ مجموعه‌ها)", + noTaxonomies: "این سایت طبقه‌بندی ندارد.", + address: "نشانی", + addressDescription: + "جایی که ورودی‌ها قرار دارند. تغییر آن URLهای عمومی‌شان را جابه‌جا می‌کند: نشانی‌های قدیمی با تغییر مسیر دائمی پاسخ می‌دهند.", + addressLink: "آن را در تنظیمات سایت › مجموعه‌ها تغییر دهید.", + save: "ذخیره", + saved: "مجموعه ذخیره شد", + key: "کلید: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.fr.ts b/apps/studio/src/i18n/lazy/collections.fr.ts new file mode 100644 index 00000000..296caa62 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.fr.ts @@ -0,0 +1,102 @@ +// Generated by `pnpm i18n:import` (ADR-0034): fr `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 23749d76cd8ef21f +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Collections", + description: + "Les types de contenu que ce site conserve : articles, événements, équipe, offres d’emploi, tout ce qui a ses propres entrées. Chacune a sa liste, son adresse et ses paramètres.", + newCollection: "Nouvelle collection", + search: "Rechercher des collections", + columns: { + name: "Nom", + address: "Adresse", + entries: "Entrées", + state: "État", + }, + entries: "{count, plural, one {# entrée} many {# d’entrées} other {# entrées}}", + declared: "Nommée", + inferred: "Pas encore nommée", + inferredHint: + "Des entrées existent, mais personne n’a nommé cette collection. Ouvrez ses paramètres pour lui donner un nom.", + empty: "Aucune collection pour l’instant", + emptyDescription: + "Créez-en une pour des événements, des membres de l’équipe, des offres d’emploi ou autre chose.", + create: { + title: "Nouvelle collection", + description: + "Un nouveau type de contenu avec sa propre liste dans la barre latérale. Les agents pourront y écrire des entrées une fois qu’elle existe.", + plural: "Nom", + pluralHint: "Pluriel, tel que l’affiche la barre latérale, p. ex. Événements.", + singular: "Une entrée s’appelle", + singularHint: "Singulier, p. ex. Événement.", + key: "Clé", + keyHint: + "Lettres minuscules, chiffres et traits d’union. Les thèmes, les filtres et les agents l’utilisent ; elle ne peut plus être modifiée ensuite.", + keyInvalid: "Utilisez des lettres minuscules, des chiffres et des traits d’union.", + required: "Obligatoire", + icon: "Icône", + address: "Adresse", + addressHint: "Les entrées se trouvent sous ce préfixe, suivi de leur slug.", + submit: "Créer la collection", + failed: "La collection n’a pas été créée", + created: "Collection créée", + createdDescription: "{name} figure dans la barre latérale. Ajoutez sa première entrée.", + }, + icons: { + article: "Article", + calendar: "Calendrier", + briefcase: "Mallette", + users: "Personnes", + images: "Images", + package: "Colis", + "book-open": "Livre", + megaphone: "Mégaphone", + quotes: "Citations", + question: "Question", + "map-pin": "Repère de carte", + files: "Fichiers", + }, + settings: { + title: "Paramètres de {name}", + description: + "Comment cette collection est nommée, où se trouvent ses entrées et ce qui les liste.", + names: "Noms", + namesDescription: + "Dans chaque langue du site. Studio, les agents et les thèmes affichent ces noms à la place de la clé.", + descriptionField: "Description", + descriptionHint: "Facultative. Affichée sous le titre de la liste de la collection.", + appearance: "Icône", + appearanceDescription: "L’icône de la collection dans la barre latérale.", + listing: "Liste publique", + listingDescription: "Où les entrées de la collection apparaissent sur le site public.", + feed: "Inclure dans les flux RSS", + feedHint: + "Les flux du site listent les entrées les plus récentes de chaque collection pour laquelle cette option est activée.", + sitemap: "Inclure dans le sitemap", + sitemapHint: "Les moteurs de recherche trouvent les entrées publiées via sitemap.xml.", + sort: "Ordre par défaut", + sortHint: + "La liste de Studio et les listes de thème qui n’indiquent aucun ordre l’utilisent.", + sorts: { + newest: "Plus récentes d’abord", + oldest: "Plus anciennes d’abord", + title: "Par titre", + }, + taxonomies: "Catégories et étiquettes", + taxonomiesDescription: + "Les taxonomies utilisées par les entrées de cette collection. Une taxonomie sans liste de collections s’applique à toutes.", + taxonomyAll: "{label} (toutes les collections)", + noTaxonomies: "Ce site n’a aucune taxonomie.", + address: "Adresse", + addressDescription: + "Où se trouvent les entrées. La modifier déplace leurs URL publiques : les anciennes adresses répondent par une redirection permanente.", + addressLink: "Modifiez-la dans Paramètres du site › Collections.", + save: "Enregistrer", + saved: "Collection enregistrée", + key: "Clé : {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.he.ts b/apps/studio/src/i18n/lazy/collections.he.ts new file mode 100644 index 00000000..7f3a1021 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.he.ts @@ -0,0 +1,97 @@ +// Generated by `pnpm i18n:import` (ADR-0034): he `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 973939f4da4defc9 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "אוספים", + description: + "סוגי התוכן שהאתר הזה מחזיק: מאמרים, אירועים, צוות, משרות, כל דבר שיש לו רשומות משלו. לכל אוסף יש רשימה, כתובת והגדרות משלו.", + newCollection: "אוסף חדש", + search: "חיפוש אוספים", + columns: { + name: "שם", + address: "כתובת", + entries: "רשומות", + state: "מצב", + }, + entries: "{count, plural, one {רשומה אחת} two {שתי רשומות} other {# רשומות}}", + declared: "בעל שם", + inferred: "עדיין ללא שם", + inferredHint: "יש רשומות, אבל אף אחד לא נתן שם לאוסף הזה. פתחו את ההגדרות שלו כדי לתת לו שם.", + empty: "עדיין אין אוספים", + emptyDescription: "צרו אוסף לאירועים, חברי צוות, משרות או כל דבר אחר.", + create: { + title: "אוסף חדש", + description: + "סוג חדש של תוכן עם רשימה משלו בסרגל הצד. סוכנים יוכלו לכתוב אליו רשומות לאחר שייווצר.", + plural: "שם", + pluralHint: "ברבים, כפי שסרגל הצד מציג אותו, למשל אירועים.", + singular: "רשומה אחת נקראת", + singularHint: "ביחיד, למשל אירוע.", + key: "מפתח", + keyHint: + "אותיות קטנות באנגלית, ספרות ומקפים. ערכות נושא, מסננים וסוכנים משתמשים בו; אי אפשר לשנות אותו אחר כך.", + keyInvalid: "השתמשו באותיות קטנות באנגלית, בספרות ובמקפים.", + required: "חובה", + icon: "סמל", + address: "כתובת", + addressHint: "הרשומות נמצאות תחת התחילית הזו ואחריה ה-slug שלהן.", + submit: "יצירת אוסף", + failed: "האוסף לא נוצר", + created: "האוסף נוצר", + createdDescription: "{name} מופיע בסרגל הצד. הוסיפו את הרשומה הראשונה שלו.", + }, + icons: { + article: "מאמר", + calendar: "לוח שנה", + briefcase: "תיק", + users: "אנשים", + images: "תמונות", + package: "חבילה", + "book-open": "ספר", + megaphone: "מגפון", + quotes: "ציטוטים", + question: "שאלה", + "map-pin": "סיכת מפה", + files: "קבצים", + }, + settings: { + title: "הגדרות {name}", + description: "איך האוסף נקרא, היכן הרשומות שלו נמצאות ומה מציג אותן.", + names: "שמות", + namesDescription: + "בכל שפה של האתר. Studio, סוכנים וערכות נושא מציגים את השמות האלה במקום המפתח.", + descriptionField: "תיאור", + descriptionHint: "אופציונלי. מוצג מתחת לכותרת הרשימה של האוסף.", + appearance: "סמל", + appearanceDescription: "הסמל של האוסף בסרגל הצד.", + listing: "רישום", + listingDescription: "היכן הרשומות של האוסף מופיעות באתר הציבורי.", + feed: "לכלול בפידי RSS", + feedHint: "פידי האתר מציגים את הרשומות החדשות ביותר של כל אוסף שהאפשרות הזו מופעלת בו.", + sitemap: "לכלול ב-sitemap", + sitemapHint: "מנועי חיפוש מוצאים רשומות שפורסמו דרך sitemap.xml.", + sort: "סדר ברירת מחדל", + sortHint: "הרשימה ב-Studio ורשימות ערכת הנושא שלא קובעות סדר משתמשות בו.", + sorts: { + newest: "החדשות ראשונות", + oldest: "הישנות ראשונות", + title: "לפי כותרת", + }, + taxonomies: "קטגוריות ותגיות", + taxonomiesDescription: + "באילו טקסונומיות משתמשות הרשומות של האוסף הזה. טקסונומיה ללא רשימת אוספים חלה על כולם.", + taxonomyAll: "{label} (כל האוספים)", + noTaxonomies: "לאתר הזה אין טקסונומיות.", + address: "כתובת", + addressDescription: + "היכן הרשומות נמצאות. שינוי שלה מזיז את כתובות ה-URL הציבוריות שלהן: כתובות ישנות מגיבות בהפניה קבועה.", + addressLink: "שנו אותה בהגדרות האתר › אוספים.", + save: "שמירה", + saved: "האוסף נשמר", + key: "מפתח: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.hi.ts b/apps/studio/src/i18n/lazy/collections.hi.ts new file mode 100644 index 00000000..bb27b98a --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.hi.ts @@ -0,0 +1,98 @@ +// Generated by `pnpm i18n:import` (ADR-0034): hi `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 1b91c35b1761ab67 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "संग्रह", + description: + "साइट में रखी जाने वाली सामग्री के प्रकार: लेख, इवेंट, टीम, जॉब, या अपनी प्रविष्टियों वाली कोई भी चीज़। हर संग्रह की अपनी सूची, अपना पता और अपनी सेटिंग्स होती हैं।", + newCollection: "नया संग्रह", + search: "संग्रह खोजें", + columns: { + name: "नाम", + address: "पता", + entries: "प्रविष्टियाँ", + state: "स्थिति", + }, + entries: "{count, plural, one {# प्रविष्टि} other {# प्रविष्टियाँ}}", + declared: "नाम दिया गया", + inferred: "अभी नाम नहीं दिया गया", + inferredHint: + "प्रविष्टियाँ मौजूद हैं, लेकिन इस संग्रह को अभी तक नाम नहीं दिया गया है। नाम देने के लिए इसकी सेटिंग्स खोलें।", + empty: "अभी कोई संग्रह नहीं", + emptyDescription: "इवेंट, टीम के सदस्यों, जॉब या किसी और चीज़ के लिए एक बनाएँ।", + create: { + title: "नया संग्रह", + description: + "सामग्री का एक नया प्रकार, जिसकी अपनी सूची साइडबार में दिखती है। संग्रह बनने के बाद एजेंट उसमें प्रविष्टियाँ लिख सकते हैं।", + plural: "नाम", + pluralHint: "बहुवचन, जैसा साइडबार में दिखता है, जैसे इवेंट।", + singular: "एक प्रविष्टि को कहा जाता है", + singularHint: "एकवचन, जैसे इवेंट।", + key: "कुंजी", + keyHint: + "छोटे अक्षर (a–z), अंक और हाइफ़न। थीम, फ़िल्टर और एजेंट इसका उपयोग करते हैं; इसे बाद में बदला नहीं जा सकता।", + keyInvalid: "छोटे अक्षर (a–z), अंक और हाइफ़न का उपयोग करें।", + required: "आवश्यक", + icon: "आइकन", + address: "पता", + addressHint: "प्रविष्टियाँ इस उपसर्ग और उसके बाद अपने स्लग पर रहती हैं।", + submit: "संग्रह बनाएँ", + failed: "संग्रह नहीं बनाया गया", + created: "संग्रह बन गया", + createdDescription: "{name} साइडबार में है। उसकी पहली प्रविष्टि जोड़ें।", + }, + icons: { + article: "लेख", + calendar: "कैलेंडर", + briefcase: "ब्रीफ़केस", + users: "लोग", + images: "चित्र", + package: "पैकेज", + "book-open": "पुस्तक", + megaphone: "लाउडस्पीकर", + quotes: "उद्धरण", + question: "प्रश्न", + "map-pin": "मैप पिन", + files: "फ़ाइलें", + }, + settings: { + title: "{name} की सेटिंग्स", + description: + "इस संग्रह का नाम कैसे रखा जाता है, उसकी प्रविष्टियाँ कहाँ रहती हैं और उन्हें कौन सूचीबद्ध करता है।", + names: "नाम", + namesDescription: "साइट की हर भाषा में। Studio, एजेंट और थीम कुंजी की जगह ये दिखाते हैं।", + descriptionField: "विवरण", + descriptionHint: "वैकल्पिक। संग्रह की सूची के शीर्षक के नीचे दिखता है।", + appearance: "आइकन", + appearanceDescription: "साइडबार में संग्रह का आइकन।", + listing: "सूची", + listingDescription: "संग्रह की प्रविष्टियाँ सार्वजनिक साइट पर कहाँ दिखती हैं।", + feed: "RSS फ़ीड में शामिल करें", + feedHint: "साइट की फ़ीड में, यह चालू रखने वाले हर संग्रह की सबसे नई प्रविष्टियाँ आती हैं।", + sitemap: "साइटमैप में शामिल करें", + sitemapHint: "खोज इंजन प्रकाशित प्रविष्टियाँ sitemap.xml से खोजते हैं।", + sort: "डिफ़ॉल्ट क्रम", + sortHint: "जिन Studio सूची और थीम सूचियों में कोई क्रम नहीं बताया गया, वे इसका उपयोग करती हैं।", + sorts: { + newest: "सबसे नई पहले", + oldest: "सबसे पुरानी पहले", + title: "शीर्षक के अनुसार", + }, + taxonomies: "श्रेणियाँ और टैग", + taxonomiesDescription: + "इस संग्रह की प्रविष्टियाँ कौन-से वर्गीकरण उपयोग करती हैं। जिस वर्गीकरण में संग्रहों की सूची नहीं है, वह सभी पर लागू होता है।", + taxonomyAll: "{label} (हर संग्रह)", + noTaxonomies: "इस साइट में कोई वर्गीकरण नहीं है।", + address: "पता", + addressDescription: + "प्रविष्टियाँ कहाँ रहती हैं। इसे बदलने से उनके सार्वजनिक URL बदल जाते हैं: पुराने पते स्थायी रीडायरेक्ट से उत्तर देते हैं।", + addressLink: "इसे साइट सेटिंग्स › संग्रह में बदलें।", + save: "सहेजें", + saved: "संग्रह सहेजा गया", + key: "कुंजी: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.id.ts b/apps/studio/src/i18n/lazy/collections.id.ts new file mode 100644 index 00000000..32a052ff --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.id.ts @@ -0,0 +1,99 @@ +// Generated by `pnpm i18n:import` (ADR-0034): id `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 7d6d775e66aacfc2 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Koleksi", + description: + "Jenis konten yang disimpan situs ini: artikel, acara, tim, lowongan, apa saja yang punya entri sendiri. Masing-masing punya daftar, alamat, dan pengaturan sendiri.", + newCollection: "Koleksi baru", + search: "Cari koleksi", + columns: { + name: "Nama", + address: "Alamat", + entries: "Entri", + state: "Status", + }, + entries: "{count, plural, other {# entri}}", + declared: "Sudah diberi nama", + inferred: "Belum diberi nama", + inferredHint: + "Entri sudah ada, tetapi belum ada yang memberi nama koleksi ini. Buka pengaturannya untuk memberinya nama.", + empty: "Belum ada koleksi", + emptyDescription: "Buat satu untuk acara, anggota tim, lowongan, atau yang lain.", + create: { + title: "Koleksi baru", + description: + "Jenis konten baru dengan daftarnya sendiri di bilah sisi. Agen dapat menulis entri ke dalamnya setelah koleksi dibuat.", + plural: "Nama", + pluralHint: "Bentuk jamak, seperti yang tampil di bilah sisi, mis. Acara.", + singular: "Satu entri disebut", + singularHint: "Bentuk tunggal, mis. Acara.", + key: "Kunci", + keyHint: + "Huruf kecil, angka, dan tanda hubung. Tema, filter, dan agen memakainya; tidak dapat diubah nanti.", + keyInvalid: "Gunakan huruf kecil, angka, dan tanda hubung.", + required: "Wajib", + icon: "Ikon", + address: "Alamat", + addressHint: "Entri berada di awalan ini diikuti slug-nya.", + submit: "Buat koleksi", + failed: "Koleksi tidak dibuat", + created: "Koleksi dibuat", + createdDescription: "{name} ada di bilah sisi. Tambahkan entri pertamanya.", + }, + icons: { + article: "Artikel", + calendar: "Kalender", + briefcase: "Tas kerja", + users: "Orang", + images: "Gambar", + package: "Paket", + "book-open": "Buku", + megaphone: "Megafon", + quotes: "Kutipan", + question: "Pertanyaan", + "map-pin": "Pin peta", + files: "File", + }, + settings: { + title: "Pengaturan {name}", + description: + "Bagaimana koleksi ini dinamai, di mana entrinya berada, dan apa yang mendaftarkannya.", + names: "Nama", + namesDescription: + "Dalam setiap bahasa situs. Studio, agen, dan tema menampilkan nama ini, bukan kuncinya.", + descriptionField: "Deskripsi", + descriptionHint: "Opsional. Tampil di bawah judul daftar koleksi.", + appearance: "Ikon", + appearanceDescription: "Ikon koleksi di bilah sisi.", + listing: "Pendaftaran", + listingDescription: "Di mana entri koleksi muncul di situs publik.", + feed: "Sertakan di feed RSS", + feedHint: "Feed situs mencantumkan entri terbaru dari setiap koleksi yang mengaktifkan ini.", + sitemap: "Sertakan di sitemap", + sitemapHint: "Mesin pencari menemukan entri yang diterbitkan melalui sitemap.xml.", + sort: "Urutan default", + sortHint: "Dipakai oleh daftar Studio dan daftar tema yang tidak menentukan urutan.", + sorts: { + newest: "Terbaru dulu", + oldest: "Terlama dulu", + title: "Menurut judul", + }, + taxonomies: "Kategori & tag", + taxonomiesDescription: + "Taksonomi mana yang dipakai entri koleksi ini. Taksonomi tanpa daftar koleksi berlaku untuk semuanya.", + taxonomyAll: "{label} (setiap koleksi)", + noTaxonomies: "Situs ini tidak punya taksonomi.", + address: "Alamat", + addressDescription: + "Di mana entri berada. Mengubahnya memindahkan URL publiknya: alamat lama dijawab dengan pengalihan permanen.", + addressLink: "Ubah di Pengaturan situs › Koleksi.", + save: "Simpan", + saved: "Koleksi disimpan", + key: "Kunci: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.it.ts b/apps/studio/src/i18n/lazy/collections.it.ts new file mode 100644 index 00000000..0e4bf0d8 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.it.ts @@ -0,0 +1,99 @@ +// Generated by `pnpm i18n:import` (ADR-0034): it `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: d6e21f372f6d5671 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Raccolte", + description: + "I tipi di contenuto che il sito conserva: articoli, eventi, team, offerte di lavoro, qualsiasi cosa con voci proprie. Ognuna ha il suo elenco, il suo indirizzo e le sue impostazioni.", + newCollection: "Nuova raccolta", + search: "Cerca raccolte", + columns: { + name: "Nome", + address: "Indirizzo", + entries: "Voci", + state: "Stato", + }, + entries: "{count, plural, one {# voce} many {# di voci} other {# voci}}", + declared: "Con nome", + inferred: "Senza nome", + inferredHint: + "Esistono delle voci, ma nessuno ha ancora dato un nome a questa raccolta. Apri le sue impostazioni per assegnargliene uno.", + empty: "Ancora nessuna raccolta", + emptyDescription: "Creane una per eventi, membri del team, offerte di lavoro o altro.", + create: { + title: "Nuova raccolta", + description: + "Un nuovo tipo di contenuto con un proprio elenco nella barra laterale. Una volta creata, gli agenti possono scrivervi delle voci.", + plural: "Nome", + pluralHint: "Plurale, come lo mostra la barra laterale, ad es. Eventi.", + singular: "Una voce si chiama", + singularHint: "Singolare, ad es. Evento.", + key: "Chiave", + keyHint: + "Lettere minuscole, cifre e trattini. Temi, filtri e agenti la usano; non può essere modificata in seguito.", + keyInvalid: "Usa lettere minuscole, cifre e trattini.", + required: "Obbligatorio", + icon: "Icona", + address: "Indirizzo", + addressHint: "Le voci si trovano a questo prefisso seguito dal loro slug.", + submit: "Crea raccolta", + failed: "La raccolta non è stata creata", + created: "Raccolta creata", + createdDescription: "{name} è nella barra laterale. Aggiungi la sua prima voce.", + }, + icons: { + article: "Articolo", + calendar: "Calendario", + briefcase: "Valigetta", + users: "Persone", + images: "Immagini", + package: "Pacco", + "book-open": "Libro", + megaphone: "Megafono", + quotes: "Citazioni", + question: "Domanda", + "map-pin": "Segnaposto", + files: "File", + }, + settings: { + title: "Impostazioni di {name}", + description: "Come si chiama questa raccolta, dove si trovano le sue voci e cosa le elenca.", + names: "Nomi", + namesDescription: + "In ogni lingua del sito. Studio, agenti e temi mostrano questi nomi al posto della chiave.", + descriptionField: "Descrizione", + descriptionHint: "Facoltativa. Mostrata sotto il titolo dell’elenco della raccolta.", + appearance: "Icona", + appearanceDescription: "L’icona della raccolta nella barra laterale.", + listing: "Elenco", + listingDescription: "Dove le voci della raccolta compaiono nel sito pubblico.", + feed: "Includi nei feed RSS", + feedHint: + "I feed del sito elencano le voci più recenti di ogni raccolta con questa opzione attiva.", + sitemap: "Includi nella sitemap", + sitemapHint: "I motori di ricerca trovano le voci pubblicate tramite sitemap.xml.", + sort: "Ordine predefinito", + sortHint: "Lo usano l’elenco di Studio e gli elenchi dei temi che non indicano un ordine.", + sorts: { + newest: "Prima le più recenti", + oldest: "Prima le più vecchie", + title: "Per titolo", + }, + taxonomies: "Categorie e tag", + taxonomiesDescription: + "Quali tassonomie usano le voci di questa raccolta. Una tassonomia senza un elenco di raccolte vale per tutte.", + taxonomyAll: "{label} (ogni raccolta)", + noTaxonomies: "Questo sito non ha tassonomie.", + address: "Indirizzo", + addressDescription: + "Dove si trovano le voci. Modificarlo sposta i loro URL pubblici: i vecchi indirizzi rispondono con un reindirizzamento permanente.", + addressLink: "Modificalo in Impostazioni del sito › Raccolte.", + save: "Salva", + saved: "Raccolta salvata", + key: "Chiave: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.ja.ts b/apps/studio/src/i18n/lazy/collections.ja.ts new file mode 100644 index 00000000..a2c1d95c --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.ja.ts @@ -0,0 +1,98 @@ +// Generated by `pnpm i18n:import` (ADR-0034): ja `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: e76cf2f3a52699ce +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "コレクション", + description: + "このサイトが保持するコンテンツの種類です。記事、イベント、チーム、求人など、独自のエントリーを持つものなら何でも置けます。それぞれに専用の一覧、アドレス、設定があります。", + newCollection: "新しいコレクション", + search: "コレクションを検索", + columns: { + name: "名前", + address: "アドレス", + entries: "エントリー", + state: "状態", + }, + entries: "{count, plural, other {#件のエントリー}}", + declared: "名前あり", + inferred: "名前未設定", + inferredHint: + "エントリーはありますが、このコレクションにはまだ名前が付けられていません。設定を開いて名前を付けてください。", + empty: "コレクションはまだありません", + emptyDescription: "イベント、チームメンバー、求人などのために作成します。", + create: { + title: "新しいコレクション", + description: + "サイドバーに専用の一覧を持つ、新しい種類のコンテンツです。作成後は、エージェントがエントリーを書き込めます。", + plural: "名前", + pluralHint: "複数形。サイドバーに表示されます(例: イベント)。", + singular: "1件のエントリーの呼び方", + singularHint: "単数形(例: イベント)。", + key: "キー", + keyHint: + "小文字の英字、数字、ハイフン。テーマ、フィルター、エージェントが使用します。後から変更できません。", + keyInvalid: "小文字の英字、数字、ハイフンを使用してください。", + required: "必須", + icon: "アイコン", + address: "アドレス", + addressHint: "エントリーはこのプレフィックスとスラッグの下に置かれます。", + submit: "コレクションを作成", + failed: "コレクションを作成できませんでした", + created: "コレクションを作成しました", + createdDescription: "{name}がサイドバーに追加されました。最初のエントリーを追加しましょう。", + }, + icons: { + article: "記事", + calendar: "カレンダー", + briefcase: "ブリーフケース", + users: "ユーザー", + images: "画像", + package: "パッケージ", + "book-open": "本", + megaphone: "メガホン", + quotes: "引用", + question: "質問", + "map-pin": "地図ピン", + files: "ファイル", + }, + settings: { + title: "{name}の設定", + description: "このコレクションの名前、エントリーの置き場所、一覧の表示方法を設定します。", + names: "名前", + namesDescription: + "サイトのすべての言語で設定します。Studio、エージェント、テーマはキーの代わりにこの名前を表示します。", + descriptionField: "説明", + descriptionHint: "任意。コレクション一覧のタイトルの下に表示されます。", + appearance: "アイコン", + appearanceDescription: "サイドバーに表示されるコレクションのアイコンです。", + listing: "公開設定", + listingDescription: "コレクションのエントリーを公開サイトのどこに表示するかを設定します。", + feed: "RSSフィードに含める", + feedHint: "オンにしたすべてのコレクションの最新エントリーが、サイトのフィードに載ります。", + sitemap: "サイトマップに含める", + sitemapHint: "検索エンジンは sitemap.xml を通じて公開済みのエントリーを見つけます。", + sort: "既定の並び順", + sortHint: "並び順を指定していないStudioの一覧とテーマの一覧で使われます。", + sorts: { + newest: "新しい順", + oldest: "古い順", + title: "タイトル順", + }, + taxonomies: "カテゴリーとタグ", + taxonomiesDescription: + "このコレクションのエントリーが使う分類です。コレクションを指定していない分類は、すべてのコレクションに適用されます。", + taxonomyAll: "{label}(すべてのコレクション)", + noTaxonomies: "このサイトには分類がありません。", + address: "アドレス", + addressDescription: + "エントリーの置き場所です。変更すると公開URLが移動し、古いアドレスは恒久的なリダイレクトで応答します。", + addressLink: "サイト設定 › コレクションで変更できます。", + save: "保存", + saved: "コレクションを保存しました", + key: "キー: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.ko.ts b/apps/studio/src/i18n/lazy/collections.ko.ts new file mode 100644 index 00000000..39707721 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.ko.ts @@ -0,0 +1,97 @@ +// Generated by `pnpm i18n:import` (ADR-0034): ko `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: a646803f6afb504f +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "컬렉션", + description: + "이 사이트가 보관하는 콘텐츠의 종류입니다. 글, 이벤트, 팀, 채용 등 자체 항목이 있는 무엇이든 둘 수 있습니다. 컬렉션마다 고유한 목록, 주소, 설정이 있습니다.", + newCollection: "새 컬렉션", + search: "컬렉션 검색", + columns: { + name: "이름", + address: "주소", + entries: "항목", + state: "상태", + }, + entries: "{count, plural, other {#개 항목}}", + declared: "이름 있음", + inferred: "이름 없음", + inferredHint: + "항목은 있지만 아직 이 컬렉션에 이름이 지정되지 않았습니다. 설정을 열어 이름을 지정하세요.", + empty: "아직 컬렉션이 없습니다", + emptyDescription: "이벤트, 팀원, 채용 등을 위한 컬렉션을 만드세요.", + create: { + title: "새 컬렉션", + description: + "사이드바에 자체 목록이 생기는 새로운 콘텐츠 종류입니다. 만들고 나면 에이전트가 항목을 작성할 수 있습니다.", + plural: "이름", + pluralHint: "복수형으로, 사이드바에 표시되는 이름입니다. 예: 이벤트.", + singular: "항목 하나의 이름", + singularHint: "단수형입니다. 예: 이벤트.", + key: "키", + keyHint: "소문자, 숫자, 하이픈. 테마, 필터, 에이전트가 사용하며 나중에 변경할 수 없습니다.", + keyInvalid: "소문자, 숫자, 하이픈을 사용하세요.", + required: "필수", + icon: "아이콘", + address: "주소", + addressHint: "항목은 이 접두사 뒤에 슬러그가 붙은 위치에 있습니다.", + submit: "컬렉션 만들기", + failed: "컬렉션을 만들지 못했습니다", + created: "컬렉션을 만들었습니다", + createdDescription: "{name}이(가) 사이드바에 추가되었습니다. 첫 항목을 추가하세요.", + }, + icons: { + article: "글", + calendar: "캘린더", + briefcase: "서류 가방", + users: "사람", + images: "이미지", + package: "패키지", + "book-open": "책", + megaphone: "확성기", + quotes: "인용문", + question: "질문", + "map-pin": "지도 핀", + files: "파일", + }, + settings: { + title: "{name} 설정", + description: "이 컬렉션의 이름, 항목이 위치할 곳, 항목을 나열하는 방식을 설정합니다.", + names: "이름", + namesDescription: + "사이트의 모든 언어로 입력합니다. Studio, 에이전트, 테마는 키 대신 이 이름을 표시합니다.", + descriptionField: "설명", + descriptionHint: "선택 사항입니다. 컬렉션 목록의 제목 아래에 표시됩니다.", + appearance: "아이콘", + appearanceDescription: "사이드바에 표시되는 컬렉션 아이콘입니다.", + listing: "목록", + listingDescription: "컬렉션의 항목이 공개 사이트의 어디에 나타날지 설정합니다.", + feed: "RSS 피드에 포함", + feedHint: "이 옵션을 켠 모든 컬렉션의 최신 항목이 사이트 피드에 나열됩니다.", + sitemap: "사이트맵에 포함", + sitemapHint: "검색 엔진은 sitemap.xml을 통해 게시된 항목을 찾습니다.", + sort: "기본 정렬", + sortHint: "정렬 기준을 지정하지 않은 Studio 목록과 테마 목록에서 사용합니다.", + sorts: { + newest: "최신순", + oldest: "오래된 순", + title: "제목순", + }, + taxonomies: "카테고리와 태그", + taxonomiesDescription: + "이 컬렉션의 항목이 사용하는 분류입니다. 컬렉션 목록이 없는 분류는 모든 컬렉션에 적용됩니다.", + taxonomyAll: "{label} (모든 컬렉션)", + noTaxonomies: "이 사이트에는 분류가 없습니다.", + address: "주소", + addressDescription: + "항목이 위치하는 곳입니다. 변경하면 공개 URL이 이동하며, 이전 주소는 영구 리디렉션으로 응답합니다.", + addressLink: "사이트 설정 › 컬렉션에서 변경하세요.", + save: "저장", + saved: "컬렉션을 저장했습니다", + key: "키: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.nl.ts b/apps/studio/src/i18n/lazy/collections.nl.ts new file mode 100644 index 00000000..913f8bb2 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.nl.ts @@ -0,0 +1,99 @@ +// Generated by `pnpm i18n:import` (ADR-0034): nl `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: f21eb83710c14f9e +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Collecties", + description: + "De soorten content die deze site bewaart: artikelen, evenementen, team, vacatures, alles met eigen items. Elke collectie heeft een eigen lijst, adres en eigen instellingen.", + newCollection: "Nieuwe collectie", + search: "Collecties zoeken", + columns: { + name: "Naam", + address: "Adres", + entries: "Items", + state: "Status", + }, + entries: "{count, plural, one {# item} other {# items}}", + declared: "Benoemd", + inferred: "Nog niet benoemd", + inferredHint: + "Er zijn items, maar niemand heeft deze collectie een naam gegeven. Open de instellingen om haar een naam te geven.", + empty: "Nog geen collecties", + emptyDescription: "Maak er een voor evenementen, teamleden, vacatures of iets anders.", + create: { + title: "Nieuwe collectie", + description: + "Een nieuw soort content met een eigen lijst in de zijbalk. Agents kunnen er items in schrijven zodra de collectie bestaat.", + plural: "Naam", + pluralHint: "Meervoud, zoals de zijbalk het toont, bijv. Evenementen.", + singular: "Eén item heet", + singularHint: "Enkelvoud, bijv. Evenement.", + key: "Sleutel", + keyHint: + "Kleine letters, cijfers en koppeltekens. Thema’s, filters en agents gebruiken hem; hij kan later niet meer worden gewijzigd.", + keyInvalid: "Gebruik kleine letters, cijfers en koppeltekens.", + required: "Verplicht", + icon: "Pictogram", + address: "Adres", + addressHint: "Items staan onder dit voorvoegsel, gevolgd door hun slug.", + submit: "Collectie maken", + failed: "De collectie is niet gemaakt", + created: "Collectie gemaakt", + createdDescription: "{name} staat in de zijbalk. Voeg het eerste item toe.", + }, + icons: { + article: "Artikel", + calendar: "Kalender", + briefcase: "Aktetas", + users: "Mensen", + images: "Afbeeldingen", + package: "Pakket", + "book-open": "Boek", + megaphone: "Megafoon", + quotes: "Citaten", + question: "Vraag", + "map-pin": "Kaartpin", + files: "Bestanden", + }, + settings: { + title: "Instellingen van {name}", + description: "Hoe deze collectie heet, waar haar items staan en wat ze weergeeft.", + names: "Namen", + namesDescription: + "In elke taal van de site. Studio, agents en thema’s tonen deze namen in plaats van de sleutel.", + descriptionField: "Beschrijving", + descriptionHint: "Optioneel. Staat onder de titel van de lijst van de collectie.", + appearance: "Pictogram", + appearanceDescription: "Het pictogram van de collectie in de zijbalk.", + listing: "Weergave", + listingDescription: "Waar de items van de collectie op de openbare site verschijnen.", + feed: "Opnemen in RSS-feeds", + feedHint: + "De feeds van de site tonen de nieuwste items van elke collectie waarvoor dit aan staat.", + sitemap: "Opnemen in de sitemap", + sitemapHint: "Zoekmachines vinden gepubliceerde items via sitemap.xml.", + sort: "Standaardvolgorde", + sortHint: "De lijst in Studio en themalijsten zonder opgegeven volgorde gebruiken deze.", + sorts: { + newest: "Nieuwste eerst", + oldest: "Oudste eerst", + title: "Op titel", + }, + taxonomies: "Categorieën en tags", + taxonomiesDescription: + "Welke taxonomieën de items van deze collectie gebruiken. Een taxonomie zonder lijst met collecties geldt voor alle collecties.", + taxonomyAll: "{label} (elke collectie)", + noTaxonomies: "Deze site heeft geen taxonomieën.", + address: "Adres", + addressDescription: + "Waar items staan. Wijzigen verplaatst hun openbare URL’s: oude adressen antwoorden met een permanente omleiding.", + addressLink: "Wijzig het in Site-instellingen › Collecties.", + save: "Opslaan", + saved: "Collectie opgeslagen", + key: "Sleutel: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.pl.ts b/apps/studio/src/i18n/lazy/collections.pl.ts new file mode 100644 index 00000000..9fe4075c --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.pl.ts @@ -0,0 +1,100 @@ +// Generated by `pnpm i18n:import` (ADR-0034): pl `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 5698f1458bd8d310 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Kolekcje", + description: + "Rodzaje treści w tej witrynie: artykuły, wydarzenia, zespół, oferty pracy, wszystko, co ma własne wpisy. Każda kolekcja ma własną listę, adres i ustawienia.", + newCollection: "Nowa kolekcja", + search: "Szukaj kolekcji", + columns: { + name: "Nazwa", + address: "Adres", + entries: "Wpisy", + state: "Stan", + }, + entries: "{count, plural, one {# wpis} few {# wpisy} many {# wpisów} other {# wpisu}}", + declared: "Nazwana", + inferred: "Jeszcze bez nazwy", + inferredHint: + "Wpisy istnieją, ale nikt nie nazwał tej kolekcji. Otwórz jej ustawienia, aby nadać jej nazwę.", + empty: "Brak kolekcji", + emptyDescription: + "Utwórz kolekcję dla wydarzeń, członków zespołu, ofert pracy lub czegokolwiek innego.", + create: { + title: "Nowa kolekcja", + description: + "Nowy rodzaj treści z własną listą na pasku bocznym. Gdy już istnieje, agenci mogą zapisywać do niej wpisy.", + plural: "Nazwa", + pluralHint: "Liczba mnoga, tak jak wyświetla ją pasek boczny, np. Wydarzenia.", + singular: "Jeden wpis nazywa się", + singularHint: "Liczba pojedyncza, np. Wydarzenie.", + key: "Klucz", + keyHint: + "Małe litery, cyfry i myślniki. Używają go motywy, filtry i agenci; nie można go później zmienić.", + keyInvalid: "Użyj małych liter, cyfr i myślników.", + required: "Wymagane", + icon: "Ikona", + address: "Adres", + addressHint: "Wpisy znajdują się pod tym prefiksem, po którym następuje ich slug.", + submit: "Utwórz kolekcję", + failed: "Nie utworzono kolekcji", + created: "Kolekcja została utworzona", + createdDescription: "Kolekcja {name} jest na pasku bocznym. Dodaj jej pierwszy wpis.", + }, + icons: { + article: "Artykuł", + calendar: "Kalendarz", + briefcase: "Teczka", + users: "Osoby", + images: "Obrazy", + package: "Paczka", + "book-open": "Książka", + megaphone: "Megafon", + quotes: "Cytaty", + question: "Pytanie", + "map-pin": "Pinezka na mapie", + files: "Pliki", + }, + settings: { + title: "Ustawienia kolekcji {name}", + description: "Jak nazywa się ta kolekcja, gdzie znajdują się jej wpisy i co je wyświetla.", + names: "Nazwy", + namesDescription: + "We wszystkich językach witryny. Studio, agenci i motywy pokazują je zamiast klucza.", + descriptionField: "Opis", + descriptionHint: "Opcjonalny. Wyświetlany pod tytułem listy kolekcji.", + appearance: "Ikona", + appearanceDescription: "Ikona kolekcji na pasku bocznym.", + listing: "Wyświetlanie", + listingDescription: "Gdzie wpisy kolekcji pojawiają się w witrynie publicznej.", + feed: "Uwzględnij w kanałach RSS", + feedHint: + "Kanały witryny zawierają najnowsze wpisy każdej kolekcji, która ma tę opcję włączoną.", + sitemap: "Uwzględnij w mapie witryny", + sitemapHint: "Wyszukiwarki znajdują opublikowane wpisy przez sitemap.xml.", + sort: "Domyślna kolejność", + sortHint: "Używa jej lista w Studio oraz listy w motywach, które nie określają kolejności.", + sorts: { + newest: "Od najnowszych", + oldest: "Od najstarszych", + title: "Według tytułu", + }, + taxonomies: "Kategorie i tagi", + taxonomiesDescription: + "Z których taksonomii korzystają wpisy tej kolekcji. Taksonomia bez listy kolekcji dotyczy wszystkich.", + taxonomyAll: "{label} (każda kolekcja)", + noTaxonomies: "Ta witryna nie ma taksonomii.", + address: "Adres", + addressDescription: + "Gdzie znajdują się wpisy. Zmiana przenosi ich publiczne adresy URL: stare adresy odpowiadają stałym przekierowaniem.", + addressLink: "Zmień w Ustawienia witryny › Kolekcje.", + save: "Zapisz", + saved: "Kolekcja została zapisana", + key: "Klucz: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.pt-BR.ts b/apps/studio/src/i18n/lazy/collections.pt-BR.ts new file mode 100644 index 00000000..7a7a6538 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.pt-BR.ts @@ -0,0 +1,100 @@ +// Generated by `pnpm i18n:import` (ADR-0034): pt-BR `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 310ec9bfd900b39f +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Coleções", + description: + "Os tipos de conteúdo que este site mantém: artigos, eventos, equipe, vagas, qualquer coisa com entradas próprias. Cada um tem sua própria lista, endereço e configurações.", + newCollection: "Nova coleção", + search: "Pesquisar coleções", + columns: { + name: "Nome", + address: "Endereço", + entries: "Entradas", + state: "Estado", + }, + entries: "{count, plural, one {# entrada} many {# de entradas} other {# entradas}}", + declared: "Com nome", + inferred: "Ainda sem nome", + inferredHint: + "Existem entradas, mas ninguém deu nome a esta coleção. Abra as configurações dela para nomeá-la.", + empty: "Nenhuma coleção ainda", + emptyDescription: "Crie uma para eventos, membros da equipe, vagas ou qualquer outra coisa.", + create: { + title: "Nova coleção", + description: + "Um novo tipo de conteúdo com sua própria lista na barra lateral. Depois de criada, os agentes podem gravar entradas nela.", + plural: "Nome", + pluralHint: "No plural, como a barra lateral exibe, por exemplo: Eventos.", + singular: "Uma entrada se chama", + singularHint: "No singular, por exemplo: Evento.", + key: "Chave", + keyHint: + "Letras minúsculas, dígitos e hifens. Temas, filtros e agentes a usam; não pode ser alterada depois.", + keyInvalid: "Use letras minúsculas, dígitos e hifens.", + required: "Obrigatório", + icon: "Ícone", + address: "Endereço", + addressHint: "As entradas ficam neste prefixo seguido do slug delas.", + submit: "Criar coleção", + failed: "A coleção não foi criada", + created: "Coleção criada", + createdDescription: "{name} está na barra lateral. Adicione a primeira entrada.", + }, + icons: { + article: "Artigo", + calendar: "Calendário", + briefcase: "Pasta", + users: "Pessoas", + images: "Imagens", + package: "Pacote", + "book-open": "Livro", + megaphone: "Megafone", + quotes: "Citações", + question: "Pergunta", + "map-pin": "Marcador de mapa", + files: "Arquivos", + }, + settings: { + title: "Configurações de {name}", + description: "Como esta coleção é nomeada, onde ficam suas entradas e o que as lista.", + names: "Nomes", + namesDescription: + "Em todos os idiomas do site. O Studio, os agentes e os temas mostram esses nomes no lugar da chave.", + descriptionField: "Descrição", + descriptionHint: "Opcional. Exibida sob o título da lista da coleção.", + appearance: "Ícone", + appearanceDescription: "O ícone da coleção na barra lateral.", + listing: "Listagem", + listingDescription: "Onde as entradas da coleção aparecem no site público.", + feed: "Incluir nos feeds RSS", + feedHint: + "Os feeds do site listam as entradas mais recentes de cada coleção que tem esta opção ativada.", + sitemap: "Incluir no sitemap", + sitemapHint: + "Os mecanismos de busca encontram as entradas publicadas por meio do sitemap.xml.", + sort: "Ordem padrão", + sortHint: "A lista do Studio e as listas de tema que não definem uma ordem a usam.", + sorts: { + newest: "Mais recentes primeiro", + oldest: "Mais antigas primeiro", + title: "Por título", + }, + taxonomies: "Categorias e tags", + taxonomiesDescription: + "Quais taxonomias as entradas desta coleção usam. Uma taxonomia sem lista de coleções vale para todas elas.", + taxonomyAll: "{label} (todas as coleções)", + noTaxonomies: "Este site não tem taxonomias.", + address: "Endereço", + addressDescription: + "Onde ficam as entradas. Alterá-lo muda as URLs públicas delas: os endereços antigos respondem com um redirecionamento permanente.", + addressLink: "Altere em Configurações do site › Coleções.", + save: "Salvar", + saved: "Coleção salva", + key: "Chave: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.ru.ts b/apps/studio/src/i18n/lazy/collections.ru.ts new file mode 100644 index 00000000..bc4a3e38 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.ru.ts @@ -0,0 +1,98 @@ +// Generated by `pnpm i18n:import` (ADR-0034): ru `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 15b238603073b48f +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Коллекции", + description: + "Виды контента, которые хранит этот сайт: статьи, события, команда, вакансии — всё, у чего есть собственные записи. У каждого вида свой список, адрес и настройки.", + newCollection: "Новая коллекция", + search: "Поиск коллекций", + columns: { + name: "Название", + address: "Адрес", + entries: "Записи", + state: "Состояние", + }, + entries: "{count, plural, one {# запись} few {# записи} many {# записей} other {# записи}}", + declared: "С названием", + inferred: "Пока без названия", + inferredHint: + "Записи есть, но никто не дал этой коллекции название. Откройте её настройки, чтобы задать название.", + empty: "Коллекций пока нет", + emptyDescription: "Создайте коллекцию для событий, сотрудников, вакансий или чего-нибудь ещё.", + create: { + title: "Новая коллекция", + description: + "Новый вид контента со своим списком на боковой панели. Когда коллекция создана, агенты могут добавлять в неё записи.", + plural: "Название", + pluralHint: "Во множественном числе, как оно показано на боковой панели, например: События.", + singular: "Одна запись называется", + singularHint: "В единственном числе, например: Событие.", + key: "Ключ", + keyHint: + "Строчные буквы, цифры и дефисы. Его используют темы, фильтры и агенты; позже изменить его нельзя.", + keyInvalid: "Используйте строчные буквы, цифры и дефисы.", + required: "Обязательно", + icon: "Значок", + address: "Адрес", + addressHint: "Записи размещаются по этому префиксу, за которым следует их слаг.", + submit: "Создать коллекцию", + failed: "Коллекция не создана", + created: "Коллекция создана", + createdDescription: "{name} уже на боковой панели. Добавьте первую запись.", + }, + icons: { + article: "Статья", + calendar: "Календарь", + briefcase: "Портфель", + users: "Люди", + images: "Изображения", + package: "Пакет", + "book-open": "Книга", + megaphone: "Рупор", + quotes: "Цитаты", + question: "Вопрос", + "map-pin": "Метка на карте", + files: "Файлы", + }, + settings: { + title: "Настройки коллекции {name}", + description: "Как называется эта коллекция, где находятся её записи и что их выводит.", + names: "Названия", + namesDescription: "На всех языках сайта. Studio, агенты и темы показывают их вместо ключа.", + descriptionField: "Описание", + descriptionHint: "Необязательно. Показывается под заголовком списка коллекции.", + appearance: "Значок", + appearanceDescription: "Значок коллекции на боковой панели.", + listing: "Вывод", + listingDescription: "Где записи коллекции появляются на публичном сайте.", + feed: "Включать в RSS-ленты", + feedHint: + "В лентах сайта выводятся новейшие записи каждой коллекции, у которой это включено.", + sitemap: "Включать в sitemap", + sitemapHint: "Поисковые системы находят опубликованные записи через sitemap.xml.", + sort: "Порядок по умолчанию", + sortHint: "Его используют список в Studio и списки тем, в которых порядок не задан.", + sorts: { + newest: "Сначала новые", + oldest: "Сначала старые", + title: "По заголовку", + }, + taxonomies: "Категории и теги", + taxonomiesDescription: + "Какие таксономии используют записи этой коллекции. Таксономия без списка коллекций относится ко всем.", + taxonomyAll: "{label} (все коллекции)", + noTaxonomies: "На этом сайте нет таксономий.", + address: "Адрес", + addressDescription: + "Где находятся записи. При изменении их публичные URL переезжают: старые адреса отвечают постоянным перенаправлением.", + addressLink: "Измените в разделе Настройки сайта › Коллекции.", + save: "Сохранить", + saved: "Коллекция сохранена", + key: "Ключ: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.tr.ts b/apps/studio/src/i18n/lazy/collections.tr.ts new file mode 100644 index 00000000..e1ec7cbb --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.tr.ts @@ -0,0 +1,101 @@ +// Generated by `pnpm i18n:import` (ADR-0034): tr `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 82ba513db50c21f9 +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Koleksiyonlar", + description: + "Bu sitenin tuttuğu içerik türleri: makaleler, etkinlikler, ekip, iş ilanları, kendi kayıtları olan her şey. Her birinin kendi listesi, adresi ve ayarları var.", + newCollection: "Yeni koleksiyon", + search: "Koleksiyonlarda ara", + columns: { + name: "Ad", + address: "Adres", + entries: "Kayıtlar", + state: "Durum", + }, + entries: "{count, plural, one {# kayıt} other {# kayıt}}", + declared: "Adlandırılmış", + inferred: "Henüz adlandırılmadı", + inferredHint: + "Kayıtlar var ama kimse bu koleksiyonu adlandırmamış. Ad vermek için ayarlarını aç.", + empty: "Henüz koleksiyon yok", + emptyDescription: + "Etkinlikler, ekip üyeleri, iş ilanları veya başka bir şey için bir tane oluştur.", + create: { + title: "Yeni koleksiyon", + description: + "Kenar çubuğunda kendi listesi olan yeni bir içerik türü. Oluşturulduktan sonra ajanlar buna kayıt yazabilir.", + plural: "Ad", + pluralHint: "Çoğul, kenar çubuğunda göründüğü gibi, ör. Etkinlikler.", + singular: "Tek bir kaydın adı", + singularHint: "Tekil, ör. Etkinlik.", + key: "Anahtar", + keyHint: + "Küçük harfler, rakamlar ve tireler. Temalar, filtreler ve ajanlar bunu kullanır; sonradan değiştirilemez.", + keyInvalid: "Küçük harf, rakam ve tire kullan.", + required: "Zorunlu", + icon: "Simge", + address: "Adres", + addressHint: "Kayıtlar bu önekin ardından kendi slug'larıyla yer alır.", + submit: "Koleksiyon oluştur", + failed: "Koleksiyon oluşturulamadı", + created: "Koleksiyon oluşturuldu", + createdDescription: "{name} kenar çubuğunda. İlk kaydını ekle.", + }, + icons: { + article: "Makale", + calendar: "Takvim", + briefcase: "Evrak çantası", + users: "Kişiler", + images: "Görseller", + package: "Paket", + "book-open": "Kitap", + megaphone: "Megafon", + quotes: "Alıntılar", + question: "Soru", + "map-pin": "Harita iğnesi", + files: "Dosyalar", + }, + settings: { + title: "{name} ayarları", + description: + "Bu koleksiyonun nasıl adlandırıldığı, kayıtlarının nerede durduğu ve onları neyin listelediği.", + names: "Adlar", + namesDescription: + "Sitenin tüm dillerinde. Studio, ajanlar ve temalar anahtar yerine bunları gösterir.", + descriptionField: "Açıklama", + descriptionHint: "İsteğe bağlı. Koleksiyon listesinin başlığının altında gösterilir.", + appearance: "Simge", + appearanceDescription: "Koleksiyonun kenar çubuğundaki simgesi.", + listing: "Listeleme", + listingDescription: "Koleksiyonun kayıtlarının herkese açık sitede göründüğü yer.", + feed: "RSS akışlarına dahil et", + feedHint: + "Sitenin akışları, bu seçeneği açık olan her koleksiyonun en yeni kayıtlarını listeler.", + sitemap: "Site haritasına dahil et", + sitemapHint: "Arama motorları yayımlanmış kayıtları sitemap.xml üzerinden bulur.", + sort: "Varsayılan sıra", + sortHint: "Studio'daki liste ve sıra belirtmeyen tema listeleri bunu kullanır.", + sorts: { + newest: "Önce en yeni", + oldest: "Önce en eski", + title: "Başlığa göre", + }, + taxonomies: "Kategoriler ve etiketler", + taxonomiesDescription: + "Bu koleksiyonun kayıtlarının hangi sınıflandırmaları kullandığı. Koleksiyon listesi olmayan bir sınıflandırma hepsine uygulanır.", + taxonomyAll: "{label} (tüm koleksiyonlar)", + noTaxonomies: "Bu sitede sınıflandırma yok.", + address: "Adres", + addressDescription: + "Kayıtların bulunduğu yer. Değiştirmek herkese açık URL'lerini taşır: eski adresler kalıcı yönlendirmeyle yanıt verir.", + addressLink: "Site ayarları › Koleksiyonlar bölümünden değiştir.", + save: "Kaydet", + saved: "Koleksiyon kaydedildi", + key: "Anahtar: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.uk.ts b/apps/studio/src/i18n/lazy/collections.uk.ts new file mode 100644 index 00000000..7caa56bc --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.uk.ts @@ -0,0 +1,98 @@ +// Generated by `pnpm i18n:import` (ADR-0034): uk `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 74b42c00e3d1fabf +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "Колекції", + description: + "Види контенту, які зберігає цей сайт: статті, події, команда, вакансії — усе, що має власні записи. Кожен вид має свій список, адресу й налаштування.", + newCollection: "Нова колекція", + search: "Пошук колекцій", + columns: { + name: "Назва", + address: "Адреса", + entries: "Записи", + state: "Стан", + }, + entries: "{count, plural, one {# запис} few {# записи} many {# записів} other {# запису}}", + declared: "З назвою", + inferred: "Ще без назви", + inferredHint: + "Записи є, але ніхто не дав цій колекції назву. Відкрийте її налаштування, щоб задати назву.", + empty: "Колекцій ще немає", + emptyDescription: "Створіть колекцію для подій, співробітників, вакансій або чогось іншого.", + create: { + title: "Нова колекція", + description: + "Новий вид контенту з власним списком на бічній панелі. Коли колекцію створено, агенти можуть додавати до неї записи.", + plural: "Назва", + pluralHint: "У множині, як її показує бічна панель, наприклад: Події.", + singular: "Один запис називається", + singularHint: "В однині, наприклад: Подія.", + key: "Ключ", + keyHint: + "Малі літери, цифри й дефіси. Його використовують теми, фільтри та агенти; пізніше змінити його не можна.", + keyInvalid: "Використовуйте малі літери, цифри й дефіси.", + required: "Обов’язково", + icon: "Значок", + address: "Адреса", + addressHint: "Записи містяться за цим префіксом, після якого йде їхній слаг.", + submit: "Створити колекцію", + failed: "Колекцію не створено", + created: "Колекцію створено", + createdDescription: "{name} уже на бічній панелі. Додайте перший запис.", + }, + icons: { + article: "Стаття", + calendar: "Календар", + briefcase: "Портфель", + users: "Люди", + images: "Зображення", + package: "Пакунок", + "book-open": "Книга", + megaphone: "Рупор", + quotes: "Цитати", + question: "Питання", + "map-pin": "Мітка на мапі", + files: "Файли", + }, + settings: { + title: "Налаштування колекції {name}", + description: "Як називається ця колекція, де розташовані її записи і що їх виводить.", + names: "Назви", + namesDescription: "Усіма мовами сайту. Studio, агенти й теми показують їх замість ключа.", + descriptionField: "Опис", + descriptionHint: "Необов’язково. Показується під заголовком списку колекції.", + appearance: "Значок", + appearanceDescription: "Значок колекції на бічній панелі.", + listing: "Виведення", + listingDescription: "Де записи колекції з’являються на публічному сайті.", + feed: "Додавати до RSS-стрічок", + feedHint: + "У стрічках сайту виводяться найновіші записи кожної колекції, в якої це ввімкнено.", + sitemap: "Додавати до sitemap", + sitemapHint: "Пошукові системи знаходять опубліковані записи через sitemap.xml.", + sort: "Порядок за замовчуванням", + sortHint: "Його використовують список у Studio та списки тем, у яких порядок не задано.", + sorts: { + newest: "Спочатку нові", + oldest: "Спочатку старі", + title: "За заголовком", + }, + taxonomies: "Категорії й теги", + taxonomiesDescription: + "Які таксономії використовують записи цієї колекції. Таксономія без списку колекцій стосується всіх.", + taxonomyAll: "{label} (усі колекції)", + noTaxonomies: "На цьому сайті немає таксономій.", + address: "Адреса", + addressDescription: + "Де розташовані записи. Зміна переносить їхні публічні URL: старі адреси відповідають постійним перенаправленням.", + addressLink: "Змініть у розділі Налаштування сайту › Колекції.", + save: "Зберегти", + saved: "Колекцію збережено", + key: "Ключ: {key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/lazy/collections.zh-Hans.ts b/apps/studio/src/i18n/lazy/collections.zh-Hans.ts new file mode 100644 index 00000000..efddeb87 --- /dev/null +++ b/apps/studio/src/i18n/lazy/collections.zh-Hans.ts @@ -0,0 +1,93 @@ +// Generated by `pnpm i18n:import` (ADR-0034): zh-Hans `collections.*` messages. +// Do not edit: change the translation file and import it; only English is edited here. +// hash: 33a983a619d6b8eb +import type { NamespaceMessages } from "../namespaces.ts"; + +export const messages: NamespaceMessages<"collections"> = { + collections: { + title: "集合", + description: + "此站点保存的内容类型:文章、活动、团队、职位,以及任何拥有自己条目的内容。每个集合都有各自的列表、地址和设置。", + newCollection: "新建集合", + search: "搜索集合", + columns: { + name: "名称", + address: "地址", + entries: "条目", + state: "状态", + }, + entries: "{count, plural, other {# 个条目}}", + declared: "已命名", + inferred: "尚未命名", + inferredHint: "已有条目,但还没有人为此集合命名。打开它的设置即可命名。", + empty: "还没有集合", + emptyDescription: "可为活动、团队成员、职位或其他任何内容创建一个集合。", + create: { + title: "新建集合", + description: "一种新的内容类型,在侧边栏中拥有自己的列表。创建后,智能体即可向其中写入条目。", + plural: "名称", + pluralHint: "复数形式,与侧边栏中的显示一致,例如:活动。", + singular: "单个条目称为", + singularHint: "单数形式,例如:活动。", + key: "键", + keyHint: "小写字母、数字和连字符。主题、筛选器和智能体会使用它,创建后无法更改。", + keyInvalid: "请使用小写字母、数字和连字符。", + required: "必填", + icon: "图标", + address: "地址", + addressHint: "条目位于此前缀之下,后接各自的 slug。", + submit: "创建集合", + failed: "集合未能创建", + created: "集合已创建", + createdDescription: "{name} 已出现在侧边栏中。请添加它的第一个条目。", + }, + icons: { + article: "文章", + calendar: "日历", + briefcase: "公文包", + users: "人员", + images: "图片", + package: "包裹", + "book-open": "书籍", + megaphone: "扩音器", + quotes: "引文", + question: "问题", + "map-pin": "地图标记", + files: "文件", + }, + settings: { + title: "{name}设置", + description: "此集合如何命名、条目位于何处,以及由什么列出它们。", + names: "名称", + namesDescription: "适用于站点的所有语言。Studio、智能体和主题会显示这些名称,而不是键。", + descriptionField: "描述", + descriptionHint: "可选。显示在集合列表标题下方。", + appearance: "图标", + appearanceDescription: "集合在侧边栏中的图标。", + listing: "列表", + listingDescription: "集合的条目在公开站点上出现的位置。", + feed: "包含在 RSS 订阅源中", + feedHint: "站点的订阅源会列出每个启用此项的集合的最新条目。", + sitemap: "包含在站点地图中", + sitemapHint: "搜索引擎通过 sitemap.xml 找到已发布的条目。", + sort: "默认排序", + sortHint: "Studio 的列表以及未指定排序的主题列表会使用它。", + sorts: { + newest: "最新优先", + oldest: "最早优先", + title: "按标题", + }, + taxonomies: "分类和标签", + taxonomiesDescription: "此集合的条目使用哪些分类法。未指定集合列表的分类法适用于所有集合。", + taxonomyAll: "{label}(所有集合)", + noTaxonomies: "此站点没有分类法。", + address: "地址", + addressDescription: + "条目所在的位置。更改后它们的公开 URL 会随之迁移:旧地址会以永久重定向响应。", + addressLink: "请在“站点设置 › 集合”中更改。", + save: "保存", + saved: "集合已保存", + key: "键:{key}", + }, + }, +}; diff --git a/apps/studio/src/i18n/namespaces.ts b/apps/studio/src/i18n/namespaces.ts index 02eba753..abf8ce0f 100644 --- a/apps/studio/src/i18n/namespaces.ts +++ b/apps/studio/src/i18n/namespaces.ts @@ -18,6 +18,7 @@ import type { messages as analyticsSettings } from "./lazy/analyticsSettings.en. import type { messages as appearance } from "./lazy/appearance.en.ts"; import type { messages as bodyEditor } from "./lazy/bodyEditor.en.ts"; import type { messages as build } from "./lazy/build.en.ts"; +import type { messages as collections } from "./lazy/collections.en.ts"; import type { messages as comments } from "./lazy/comments.en.ts"; import type { messages as commentsSettings } from "./lazy/commentsSettings.en.ts"; import type { messages as media } from "./lazy/media.en.ts"; @@ -37,6 +38,7 @@ export interface NamespaceCatalogs { appearance: DeepString<(typeof appearance)["appearance"]>; bodyEditor: DeepString<(typeof bodyEditor)["bodyEditor"]>; build: DeepString<(typeof build)["build"]>; + collections: DeepString<(typeof collections)["collections"]>; comments: DeepString<(typeof comments)["comments"]>; commentsSettings: DeepString<(typeof commentsSettings)["commentsSettings"]>; media: DeepString<(typeof media)["media"]>; diff --git a/apps/studio/src/i18n/nl.ts b/apps/studio/src/i18n/nl.ts index b79efd1b..cdea09c8 100644 --- a/apps/studio/src/i18n/nl.ts +++ b/apps/studio/src/i18n/nl.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): nl core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: bc1136817ffe7e5a +// hash: b54ff75917b7b546 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -181,6 +181,7 @@ export const messages: Messages = { home: "Overzicht", pages: "Pagina’s", articles: "Artikelen", + collections: "Collecties", snapshots: "Snapshots", agents: "Agents", allSites: "Alle sites", @@ -303,6 +304,13 @@ export const messages: Messages = { emptyDescription: "Probeer een andere categorie of tag, of wis de filters.", }, }, + collectionList: { + description: "Items van de collectie “{key}”.", + newEntry: "Nieuw item", + search: "Items zoeken", + empty: "Nog geen items", + settings: "Instellingen", + }, articleCorrections: { title: "Correcties", description: diff --git a/apps/studio/src/i18n/pl.ts b/apps/studio/src/i18n/pl.ts index 204fb3b5..70994741 100644 --- a/apps/studio/src/i18n/pl.ts +++ b/apps/studio/src/i18n/pl.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): pl core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: bd04b4845696f5aa +// hash: b3c7f93fa928a59b import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -177,6 +177,7 @@ export const messages: Messages = { home: "Przegląd", pages: "Strony", articles: "Artykuły", + collections: "Kolekcje", snapshots: "Snapshoty", agents: "Agenci", allSites: "Wszystkie witryny", @@ -300,6 +301,13 @@ export const messages: Messages = { emptyDescription: "Spróbuj innej kategorii lub tagu albo wyczyść filtry.", }, }, + collectionList: { + description: "Wpisy kolekcji „{key}”.", + newEntry: "Nowy wpis", + search: "Szukaj wpisów", + empty: "Brak wpisów", + settings: "Ustawienia", + }, articleCorrections: { title: "Sprostowania", description: diff --git a/apps/studio/src/i18n/pt-BR.ts b/apps/studio/src/i18n/pt-BR.ts index 26d2f904..9dc7734b 100644 --- a/apps/studio/src/i18n/pt-BR.ts +++ b/apps/studio/src/i18n/pt-BR.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): pt-BR core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 09480fb1af0bb483 +// hash: 9b17509d4e212b2b import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -180,6 +180,7 @@ export const messages: Messages = { home: "Visão geral", pages: "Páginas", articles: "Artigos", + collections: "Coleções", snapshots: "Snapshots", agents: "Agentes", allSites: "Todos os sites", @@ -303,6 +304,13 @@ export const messages: Messages = { emptyDescription: "Tente outra categoria ou tag, ou limpe os filtros.", }, }, + collectionList: { + description: "Entradas da coleção “{key}”.", + newEntry: "Nova entrada", + search: "Buscar entradas", + empty: "Ainda não há entradas", + settings: "Configurações", + }, articleCorrections: { title: "Correções", description: diff --git a/apps/studio/src/i18n/ru.ts b/apps/studio/src/i18n/ru.ts index a1eddc8f..8f50483c 100644 --- a/apps/studio/src/i18n/ru.ts +++ b/apps/studio/src/i18n/ru.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): ru core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 1070cbca97e0272f +// hash: 4f584f05d058b9b2 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -180,6 +180,7 @@ export const messages: Messages = { home: "Обзор", pages: "Страницы", articles: "Статьи", + collections: "Коллекции", snapshots: "Снимки", agents: "Агенты", allSites: "Все сайты", @@ -303,6 +304,13 @@ export const messages: Messages = { emptyDescription: "Попробуйте другую категорию или тег либо сбросьте фильтры.", }, }, + collectionList: { + description: "Записи коллекции «{key}».", + newEntry: "Новая запись", + search: "Поиск записей", + empty: "Записей пока нет", + settings: "Настройки", + }, articleCorrections: { title: "Исправления", description: diff --git a/apps/studio/src/i18n/tr.ts b/apps/studio/src/i18n/tr.ts index 3d3a81b3..7426f8f9 100644 --- a/apps/studio/src/i18n/tr.ts +++ b/apps/studio/src/i18n/tr.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): tr core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: e45d7ad82a90d2e9 +// hash: 61b87d873490da95 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -178,6 +178,7 @@ export const messages: Messages = { home: "Genel bakış", pages: "Sayfalar", articles: "Makaleler", + collections: "Koleksiyonlar", snapshots: "Snapshot'lar", agents: "Agent'lar", allSites: "Tüm siteler", @@ -299,6 +300,13 @@ export const messages: Messages = { emptyDescription: "Başka bir kategori ya da etiket dene veya süzgeçleri temizle.", }, }, + collectionList: { + description: "“{key}” koleksiyonunun kayıtları.", + newEntry: "Yeni kayıt", + search: "Kayıtlarda ara", + empty: "Henüz kayıt yok", + settings: "Ayarlar", + }, articleCorrections: { title: "Düzeltmeler", description: diff --git a/apps/studio/src/i18n/uk.ts b/apps/studio/src/i18n/uk.ts index 638e49a3..59093bd0 100644 --- a/apps/studio/src/i18n/uk.ts +++ b/apps/studio/src/i18n/uk.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): uk core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 88ef27c7276aa307 +// hash: 1a47fa2bff52ca89 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -179,6 +179,7 @@ export const messages: Messages = { home: "Огляд", pages: "Сторінки", articles: "Статті", + collections: "Колекції", snapshots: "Знімки", agents: "Агенти", allSites: "Усі сайти", @@ -302,6 +303,13 @@ export const messages: Messages = { emptyDescription: "Спробуйте іншу категорію чи тег або скиньте фільтри.", }, }, + collectionList: { + description: "Записи колекції «{key}».", + newEntry: "Новий запис", + search: "Пошук записів", + empty: "Записів поки немає", + settings: "Налаштування", + }, articleCorrections: { title: "Виправлення", description: diff --git a/apps/studio/src/i18n/zh-Hans.ts b/apps/studio/src/i18n/zh-Hans.ts index 91a1726b..40da405c 100644 --- a/apps/studio/src/i18n/zh-Hans.ts +++ b/apps/studio/src/i18n/zh-Hans.ts @@ -1,6 +1,6 @@ // Generated by `pnpm i18n:import` (ADR-0034): zh-Hans core messages. // Do not edit: change the translation file and import it; only English is edited here. -// hash: 77a7e7a8b606f310 +// hash: 073db4e790be94e2 import type { Messages } from "./en.ts"; export const messages: Messages = { @@ -172,6 +172,7 @@ export const messages: Messages = { home: "概览", pages: "页面", articles: "文章", + collections: "集合", snapshots: "快照", agents: "智能体", allSites: "所有站点", @@ -292,6 +293,13 @@ export const messages: Messages = { emptyDescription: "试试其他分类或标签,或清除筛选。", }, }, + collectionList: { + description: "“{key}”集合的条目。", + newEntry: "新建条目", + search: "搜索条目", + empty: "还没有条目", + settings: "设置", + }, articleCorrections: { title: "更正", description: diff --git a/apps/studio/src/lib/api.ts b/apps/studio/src/lib/api.ts index 0e5d2ecc..477c3376 100644 --- a/apps/studio/src/lib/api.ts +++ b/apps/studio/src/lib/api.ts @@ -210,6 +210,7 @@ import { agentsApi } from "./api/agents.ts"; import { aiApi } from "./api/ai.ts"; import { analyticsApi } from "./api/analytics.ts"; import { approvalsApi } from "./api/approvals.ts"; +import { collectionsApi } from "./api/collections.ts"; import { commentsApi } from "./api/comments.ts"; import { contentApi } from "./api/content.ts"; import { historyApi } from "./api/history.ts"; @@ -239,6 +240,7 @@ export const api = { projects: projectsApi, pages: contentApi.pages, entries: contentApi.entries, + collections: collectionsApi, design: contentApi.design, themeSettings: contentApi.themeSettings, blockTypes: contentApi.blockTypes, diff --git a/apps/studio/src/lib/api/collections.ts b/apps/studio/src/lib/api/collections.ts new file mode 100644 index 00000000..dbbcf57e --- /dev/null +++ b/apps/studio/src/lib/api/collections.ts @@ -0,0 +1,28 @@ +/** RFC-0040: the site's collection registry (declared, inferred from entries, and `posts`). */ +import type { CollectionSettings, Site } from "@cloudhal/core-contracts"; +import type { CollectionInfo } from "@cloudhal/core-ts/collections"; +import { json, request } from "./http.ts"; + +export type { CollectionInfo }; + +const base = (siteId: string) => `/api/sites/${encodeURIComponent(siteId)}/collections`; + +export const collectionsApi = { + list: (siteId: string) => request(base(siteId)), + /** Declares `key` or replaces its settings (`site:settings`); `rev` is the site's. */ + save: ( + site: Pick, + key: string, + settings: CollectionSettings, + dryRun = false, + ) => + request<{ site: Site; dryRun: boolean }>( + `${base(site.id)}/${encodeURIComponent(key)}${dryRun ? "?dryRun=1" : ""}`, + { method: "PUT", ...json({ settings, rev: site.rev }) }, + ), + /** Undeclares a collection without entries (409 `collection_in_use` otherwise). */ + remove: (site: Pick, key: string) => + request<{ site: Site }>(`${base(site.id)}/${encodeURIComponent(key)}?rev=${site.rev}`, { + method: "DELETE", + }), +}; diff --git a/apps/studio/src/lib/collection-icons.ts b/apps/studio/src/lib/collection-icons.ts new file mode 100644 index 00000000..705ab350 --- /dev/null +++ b/apps/studio/src/lib/collection-icons.ts @@ -0,0 +1,31 @@ +/** RFC-0040: the icons a collection may name (`CollectionSettings.icon`), loaded lazily. */ +import { + Article, + BookOpen, + Briefcase, + CalendarBlank, + Files, + Images, + MapPin, + Megaphone, + Package, + Question, + Quotes, + Users, +} from "@phosphor-icons/react"; +import type { CollectionIconSet } from "./collections.ts"; + +export const COLLECTION_ICONS: CollectionIconSet = { + article: Article, + calendar: CalendarBlank, + briefcase: Briefcase, + users: Users, + images: Images, + package: Package, + "book-open": BookOpen, + megaphone: Megaphone, + quotes: Quotes, + question: Question, + "map-pin": MapPin, + files: Files, +}; diff --git a/apps/studio/src/lib/collections.ts b/apps/studio/src/lib/collections.ts new file mode 100644 index 00000000..28c12419 --- /dev/null +++ b/apps/studio/src/lib/collections.ts @@ -0,0 +1,108 @@ +/** + * RFC-0040 in Studio: the site's collection registry, each collection's name in Studio's language + * and its icon from a closed set (`CollectionSettings.icon` is a name, never a URL or markup). + */ +import type { CollectionLabel, Site } from "@cloudhal/core-contracts"; +import { BUILT_IN_COLLECTION, collectionLabel } from "@cloudhal/core-ts/collections"; +import { Article, Files, type Icon } from "@phosphor-icons/react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import type { CollectionInfo } from "./api/collections.ts"; +import { api, onApiWrite } from "./api.ts"; + +/** The icon names a collection may use, in the order the picker shows them. */ +export const COLLECTION_ICON_NAMES = [ + "article", + "calendar", + "briefcase", + "users", + "images", + "package", + "book-open", + "megaphone", + "quotes", + "question", + "map-pin", + "files", +] as const; + +export type CollectionIconName = (typeof COLLECTION_ICON_NAMES)[number]; +export type CollectionIconSet = Readonly>; + +/** + * The collection's icon: its own (once `icons`, the lazily loaded set, is there), `article` for + * `posts`, else `files`. + */ +export function collectionIcon( + info: Pick, + icons?: CollectionIconSet, +): Icon { + const name = info.settings.icon; + const own = + name && icons && Object.hasOwn(icons, name) ? icons[name as CollectionIconName] : undefined; + return own ?? (info.key === BUILT_IN_COLLECTION ? Article : Files); +} + +let iconSet: Promise | undefined; + +/** The icon set, loaded with its first use so the shell's first load does not carry it. */ +export function useCollectionIcons(): CollectionIconSet | undefined { + const [icons, setIcons] = useState(); + useEffect(() => { + iconSet ??= import("./collection-icons.ts").then((m) => m.COLLECTION_ICONS); + let live = true; + void iconSet.then((set) => live && setIcons(set)); + return () => { + live = false; + }; + }, []); + return icons; +} + +/** + * The collection's labels in `locale`, then the site's default locale; `undefined` when it has + * none (the caller shows the key, or Studio's own "Articles" for `posts`). + */ +export function collectionNames( + site: Pick | undefined, + key: string, + locale: string, +): CollectionLabel | undefined { + if (!site?.collections?.[key]?.labels) return undefined; + return collectionLabel(site, key, locale); +} + +/** Entries of every status and locale. */ +export function entryTotal(info: Pick): number { + return Object.values(info.counts).reduce((a, b) => a + (b ?? 0), 0); +} + +export function collectionsQuery(siteId: string) { + return { queryKey: ["collections", siteId], queryFn: () => api.collections.list(siteId) }; +} + +/** Writes that can change the registry or its counts: the site, its collections and entries. */ +const REGISTRY_WRITE = /^\/api\/sites\/[^/]+(\/collections(\/[^/]+)?|\/entries(\/[^/]+)?)?$/; + +/** + * The registry, shared by the sidebar, the collection screens and the palette. Every write to the + * site, its collections or its entries through the API client refreshes it (`onApiWrite`). + */ +export function useCollections(siteId: string) { + const qc = useQueryClient(); + useEffect( + () => + onApiWrite((path) => { + const bare = path.split("?")[0] ?? path; + if (bare.startsWith(`/api/sites/${siteId}`) && REGISTRY_WRITE.test(bare)) + void qc.invalidateQueries({ queryKey: ["collections", siteId] }); + }), + [qc, siteId], + ); + return useQuery(collectionsQuery(siteId)); +} + +/** The registry's keys (empty while loading). */ +export function useCollectionKeys(siteId: string): string[] { + return (useCollections(siteId).data ?? []).map((c) => c.key); +} diff --git a/apps/studio/src/lib/taxonomy-settings.ts b/apps/studio/src/lib/taxonomy-settings.ts index 55b2e32d..ab43efd0 100644 --- a/apps/studio/src/lib/taxonomy-settings.ts +++ b/apps/studio/src/lib/taxonomy-settings.ts @@ -267,12 +267,21 @@ export function archiveChange( return { follow: 0 }; } -/** Collections a taxonomy can apply to: the ones entries use, the ones it names, and `posts`. */ +/** + * Collections a taxonomy can apply to: the registry's (RFC-0040: declared, in use, `posts`), the + * ones entries use, and the ones it names. + */ export function knownCollections( entries: readonly { collection: string }[], draft?: Pick, + registry: readonly string[] = [], ): string[] { return [ - ...new Set(["posts", ...entries.map((e) => e.collection), ...(draft?.collections ?? [])]), + ...new Set([ + "posts", + ...registry, + ...entries.map((e) => e.collection), + ...(draft?.collections ?? []), + ]), ].sort(); } diff --git a/apps/studio/src/router.tsx b/apps/studio/src/router.tsx index 39e0e42c..ad32ed2d 100644 --- a/apps/studio/src/router.tsx +++ b/apps/studio/src/router.tsx @@ -50,6 +50,14 @@ const ArticlesPage = lazyRouteComponent( () => withMessages(import("./routes/ArticlesPage.tsx"), ["readers", "menus"]), "ArticlesPage", ); +const CollectionsPage = lazyRouteComponent( + () => withMessages(import("./routes/CollectionsPage.tsx"), ["collections"]), + "CollectionsPage", +); +const CollectionSettingsPage = lazyRouteComponent( + () => withMessages(import("./routes/CollectionSettingsPage.tsx"), ["collections"]), + "CollectionSettingsPage", +); const CommentsPage = lazyRouteComponent( () => withMessages(import("./routes/CommentsPage.tsx"), ["comments"]), "CommentsPage", @@ -278,17 +286,43 @@ const pagesRoute = createRoute({ path: "/pages", component: PagesPage, }); +/** `?category=&tag=`: the facet filters (linked from Categories & tags). */ +function facetSearch(search: Record): { category?: string; tag?: string } { + const category = slugParam(search.category); + const tag = slugParam(search.tag); + return { ...(category ? { category } : {}), ...(tag ? { tag } : {}) }; +} +/** RFC-0040: Articles is the `posts` collection; old links keep working. */ const articlesRoute = createRoute({ getParentRoute: () => siteRoute, path: "/articles", - /** `?category=&tag=`: the facet filters (linked from Categories & tags). */ - validateSearch: (search: Record): { category?: string; tag?: string } => { - const category = slugParam(search.category); - const tag = slugParam(search.tag); - return { ...(category ? { category } : {}), ...(tag ? { tag } : {}) }; + validateSearch: facetSearch, + beforeLoad: ({ params, search }) => { + throw redirect({ + to: "/sites/$siteId/collections/$collection", + params: { siteId: params.siteId, collection: "posts" }, + search, + replace: true, + }); }, +}); +/** RFC-0040 §4: every collection of the registry, with "New collection". */ +const collectionsRoute = createRoute({ + getParentRoute: () => siteRoute, + path: "/collections", + component: CollectionsPage, +}); +const collectionRoute = createRoute({ + getParentRoute: () => siteRoute, + path: "/collections/$collection", + validateSearch: facetSearch, component: ArticlesPage, }); +const collectionSettingsRoute = createRoute({ + getParentRoute: () => siteRoute, + path: "/collections/$collection/settings", + component: CollectionSettingsPage, +}); /** Site › Categories & tags (RFC-0002 §8): `?taxonomy=&filter=`. */ const taxonomyRoute = createRoute({ getParentRoute: () => siteRoute, @@ -477,6 +511,9 @@ const routeTree = rootRoute.addChildren([ analyticsRoute, pagesRoute, articlesRoute, + collectionsRoute, + collectionRoute, + collectionSettingsRoute, snapshotsRoute, activityRoute, approvalsRoute, diff --git a/apps/studio/src/routes/ArticleEditorPage.tsx b/apps/studio/src/routes/ArticleEditorPage.tsx index 055e6df9..1f6ba076 100644 --- a/apps/studio/src/routes/ArticleEditorPage.tsx +++ b/apps/studio/src/routes/ArticleEditorPage.tsx @@ -56,6 +56,7 @@ import { StatusActions } from "../components/StatusActions.tsx"; import { TagsMigrationBanner } from "../components/TagsMigration.tsx"; import { useTaxonomyData } from "../components/Taxonomy.tsx"; import { useToast } from "../components/Toast.ts"; +import { useCollectionCrumb } from "../components/useCollectionView.ts"; import { useSavedToast } from "../components/useLiveSite.ts"; import { useCan, useGate, useSelf } from "../components/useMe.ts"; import { useT } from "../i18n/index.tsx"; @@ -99,9 +100,6 @@ const READ_ONLY_REASON = { published: "gating.entryPublished", } as const; -/** The Articles list, the editor's first crumb. */ -const articlesHref = (siteId: string) => `/sites/${siteId}/articles`; - export function ArticleEditorPage() { const t = useT(); const { siteId, entryId } = useParams({ from: "/sites/$siteId/articles/$entryId" }); @@ -169,10 +167,10 @@ export function ArticleEditorPage() { mutationFn: (entry: ContentEntry) => api.entries.remove(entry), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ["entries", siteId] }); - await navigate({ to: "/sites/$siteId/articles", params: { siteId } }); + await navigate({ href: articlesCrumb.href }); }, }); - const articlesCrumb = { label: t("pageHeaders.articles.title"), href: articlesHref(siteId) }; + const articlesCrumb = useCollectionCrumb(siteId, query.data?.collection); if (!draft) { return ( diff --git a/apps/studio/src/routes/ArticlesPage.tsx b/apps/studio/src/routes/ArticlesPage.tsx index 6cfc169f..77eec3e9 100644 --- a/apps/studio/src/routes/ArticlesPage.tsx +++ b/apps/studio/src/routes/ArticlesPage.tsx @@ -29,6 +29,7 @@ import { import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { useState } from "react"; +import { CollectionActions } from "../components/CollectionActions.tsx"; import { SiteCrumbs } from "../components/Crumbs.tsx"; import { deleteResourceProps } from "../components/deleteLabels.ts"; import { EntryPath } from "../components/EntryPath.tsx"; @@ -55,6 +56,12 @@ import { type TranslationList, useTranslationList, } from "../components/TranslationList.tsx"; +import { + scopeTaxonomies, + useCollectionHeader, + useCollectionParam, + useNewEntry, +} from "../components/useCollectionView.ts"; import { useCan, useSelf } from "../components/useMe.ts"; import { usePaging } from "../components/usePaging.ts"; import { listOf, useI18n, useT } from "../i18n/index.tsx"; @@ -85,17 +92,20 @@ function namesOf(entry: ContentEntry, tax: Taxonomy | undefined, data: TaxonomyD } /** - * Site › Articles (DESIGN.md §2a, §2p): the "posts" collection, newest first, with its primary - * category and first tags, and category / tag facets that query the API's filter language. + * Site › a collection (DESIGN.md §2a, §2p; RFC-0040 §4): its entries, newest first, with their + * primary category and first tags where those taxonomies apply, and category / tag facets that + * query the API's filter language. `posts` is Articles; `/articles` redirects here. */ export function ArticlesPage() { const t = useT(); const { siteId } = useParams({ from: "/sites/$siteId" }); + const collection = useCollectionParam(); + const header = useCollectionHeader(siteId, collection); const facets = useSearch({ strict: false }) as { category?: string; tag?: string }; const qc = useQueryClient(); const navigate = useNavigate(); const site = useQuery({ queryKey: ["site", siteId], queryFn: () => api.sites.get(siteId) }); - const data = useTaxonomyData(siteId); + const data = scopeTaxonomies(useTaxonomyData(siteId), collection); const host = primaryHostname(site.data?.domains ?? []); const { category: catTax, tag: tagTax } = data.defaults; const filter = facetFilter({ @@ -106,13 +116,13 @@ export function ArticlesPage() { tag: tagTax && facets.tag ? { key: tagTax.key, slug: facets.tag } : undefined, }); const entries = useQuery({ - queryKey: filter ? ["entries", siteId, "posts", filter] : ["entries", siteId, "posts"], - queryFn: () => api.entries.list(siteId, { collection: "posts", filter }), + queryKey: filter ? ["entries", siteId, collection, filter] : ["entries", siteId, collection], + queryFn: () => api.entries.list(siteId, { collection, filter }), enabled: !filter || data.taxonomies.isSuccess, }); const all = useQuery({ - queryKey: ["entries", siteId, "posts"], - queryFn: () => api.entries.list(siteId, "posts"), + queryKey: ["entries", siteId, collection], + queryFn: () => api.entries.list(siteId, collection), }); const viewsColumn = useListViews(siteId); const [toDelete, setToDelete] = useState(); @@ -132,8 +142,8 @@ export function ArticlesPage() { ); const setFacets = (next: { category?: string | undefined; tag?: string | undefined }) => navigate({ - to: "/sites/$siteId/articles", - params: { siteId }, + to: "/sites/$siteId/collections/$collection", + params: { siteId, collection }, search: { ...(next.category ? { category: next.category } : {}), ...(next.tag ? { tag: next.tag } : {}), @@ -141,26 +151,7 @@ export function ArticlesPage() { replace: true, }); const filtered = Boolean(facets.category || facets.tag); - const create = useMutation({ - mutationFn: () => - api.entries.create(siteId, { - collection: "posts", - slug: `untitled-${Date.now().toString(36)}`, - locale: list.newLocale, - status: "draft", - title: t("articles.untitled"), - body: { format: "markdown", value: "" }, - fields: {}, - tags: [], - }), - onSuccess: async (entry) => { - await qc.invalidateQueries({ queryKey: ["entries", siteId] }); - await navigate({ - to: "/sites/$siteId/articles/$entryId", - params: { siteId, entryId: entry.id }, - }); - }, - }); + const create = useNewEntry(siteId, collection, list.newLocale); const remove = useMutation({ mutationFn: (entry: ContentEntry) => api.entries.remove(entry), onSuccess: async () => { @@ -176,7 +167,7 @@ export function ArticlesPage() { loading={create.isPending} data-testid="new-article" > - {t("articles.newArticle")} + {header.newLabel} ); const migrated = (data.taxonomies.data?.length ?? 0) > 0; @@ -184,11 +175,11 @@ export function ArticlesPage() { const columns = 6 + (catTax ? 1 : 0) + (viewsColumn ? 1 : 0) + (list.groupOf ? 1 : 0); return ( } - icon={
} - title={t("pageHeaders.articles.title")} - description={t("pageHeaders.articles.description")} - actions={newButton} + crumbs={} + icon={} + title={header.title} + description={header.description} + actions={} > {needsTagsMigration(data.taxonomies.data, all.data) && ( @@ -197,7 +188,7 @@ export function ArticlesPage() { @@ -230,8 +221,8 @@ export function ArticlesPage() { /> ) : ( } - title={t("articles.empty")} + icon={} + title={header.empty} description={t("articles.emptyDescription")} contents={newButton} /> diff --git a/apps/studio/src/routes/CollectionSettingsPage.tsx b/apps/studio/src/routes/CollectionSettingsPage.tsx new file mode 100644 index 00000000..3320b21d --- /dev/null +++ b/apps/studio/src/routes/CollectionSettingsPage.tsx @@ -0,0 +1,357 @@ +import type { CollectionSettings, Site, Taxonomy } from "@cloudhal/core-contracts"; +import { collectionSettings } from "@cloudhal/core-ts/collections"; +import { collectionPermalink, permalinkPrefix } from "@cloudhal/core-ts/permalinks"; +import { Button, Checkbox, Input, Link, PageLayout, Select, Text } from "@cloudhal/ui"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useParams } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { SiteCrumbs } from "../components/Crumbs.tsx"; +import { ErrorBanner } from "../components/ErrorBanner.tsx"; +import { ReadOnly } from "../components/Gating.tsx"; +import { SettingsLayout, SettingsSection } from "../components/Settings.tsx"; +import { useTaxonomyData } from "../components/Taxonomy.tsx"; +import { SwitchField } from "../components/TaxonomySettingsDialog.tsx"; +import { useToast } from "../components/Toast.ts"; +import { useGate } from "../components/useMe.ts"; +import { useI18n } from "../i18n/index.tsx"; +import { api } from "../lib/api.ts"; +import { COLLECTION_ICON_NAMES, collectionNames, useCollections } from "../lib/collections.ts"; +import { languageOption } from "../lib/languages.ts"; + +type Sort = NonNullable; +type LabelForm = Record; + +/** The form's labels: one row per site locale, empty where the collection has none. */ +export function labelForm(site: Site, settings: CollectionSettings): LabelForm { + const locales = [site.defaultLocale, ...site.locales.filter((l) => l !== site.defaultLocale)]; + return Object.fromEntries( + locales.map((l) => { + const label = settings.labels?.[l]; + return [ + l, + { + singular: label?.singular ?? "", + plural: label?.plural ?? "", + description: label?.description ?? "", + }, + ]; + }), + ); +} + +/** The settings with the form applied: complete label rows only, defaults left out. */ +export function withForm( + settings: CollectionSettings, + form: { labels: LabelForm; icon: string; feed: boolean; sitemap: boolean; sort: Sort }, +): CollectionSettings { + const { labels: _l, icon: _i, feed: _f, sitemap: _s, sort: _o, ...rest } = settings; + const labels = Object.fromEntries( + Object.entries(form.labels) + .filter(([, v]) => v.singular.trim() && v.plural.trim()) + .map(([l, v]) => [ + l, + { + singular: v.singular.trim(), + plural: v.plural.trim(), + ...(v.description.trim() ? { description: v.description.trim() } : {}), + }, + ]), + ); + return { + ...rest, + ...(Object.keys(labels).length ? { labels } : {}), + ...(form.icon ? { icon: form.icon } : {}), + ...(form.feed ? {} : { feed: false }), + ...(form.sitemap ? {} : { sitemap: false }), + ...(form.sort === "newest" ? {} : { sort: form.sort }), + }; +} + +/** + * Site › a collection › Settings (RFC-0040 §4, DESIGN.md §2c): its names in every site language, + * icon, feed, sitemap and default order (one save, a site settings change), and which categories + * and tags apply (saved on the taxonomies). Its address stays in Site settings › Collections. + */ +export function CollectionSettingsPage() { + const { t, locale } = useI18n("collections"); + const { siteId, collection } = useParams({ + from: "/sites/$siteId/collections/$collection/settings", + }); + const site = useQuery({ queryKey: ["site", siteId], queryFn: () => api.sites.get(siteId) }); + const gate = useGate("settings.write"); + const registry = useCollections(siteId); + const info = registry.data?.find((c) => c.key === collection); + const name = + collectionNames(site.data, collection, locale)?.plural ?? + (collection === "posts" ? t("nav.articles") : collection); + return ( + + } + title={t("collections.settings.title", { name })} + description={t("collections.settings.description")} + > + + + + {site.data && ( + + )} + + + + + + ); +} + +function SettingsForm({ + site, + collection, + settings, +}: { + site: Site; + collection: string; + settings: CollectionSettings; +}) { + const { t, locale } = useI18n("collections"); + const qc = useQueryClient(); + const toast = useToast(); + const defaults = collectionSettings(site, collection); + const [labels, setLabels] = useState(() => labelForm(site, settings)); + const [icon, setIcon] = useState(settings.icon ?? ""); + const [feed, setFeed] = useState(defaults.feed); + const [sitemap, setSitemap] = useState(defaults.sitemap); + const [sort, setSort] = useState(defaults.sort); + useEffect(() => { + setLabels(labelForm(site, settings)); + setIcon(settings.icon ?? ""); + }, [site.rev]); + const save = useMutation({ + mutationFn: () => + api.collections.save( + site, + collection, + withForm(settings, { labels, icon, feed, sitemap, sort }), + ), + onSuccess: async ({ site: saved }) => { + qc.setQueryData(["site", site.id], saved); + await qc.invalidateQueries({ queryKey: ["collections", site.id] }); + toast.success(t("collections.settings.saved"), t("common.rev", { rev: saved.rev })); + }, + }); + const set = (l: string, field: "singular" | "plural" | "description", value: string) => + setLabels((prev) => ({ + ...prev, + [l]: { ...(prev[l] ?? { singular: "", plural: "", description: "" }), [field]: value }, + })); + const footer = ( +
+ +
+ ); + const iconItems = { + "": t("collections.icons.files"), + ...Object.fromEntries(COLLECTION_ICON_NAMES.map((n) => [n, t(`collections.icons.${n}`)])), + }; + return ( + <> + +
+ + + {t("collections.settings.key", { key: collection })} + + {Object.entries(labels).map(([l, v]) => ( +
+ + {languageOption(locale, l)} + +
+ set(l, "plural", x)} + maxLength={100} + /> + set(l, "singular", x)} + maxLength={100} + /> +
+ set(l, "description", x)} + maxLength={1000} + /> +
+ ))} + setSort(v as Sort)} + items={{ + newest: t("collections.settings.sorts.newest"), + oldest: t("collections.settings.sorts.oldest"), + title: t("collections.settings.sorts.title"), + }} + /> +
+
+ + ); +} + +/** Which taxonomies apply: saved on each changed taxonomy's `collections`. */ +function TaxonomiesSection({ siteId, collection }: { siteId: string; collection: string }) { + const { t } = useI18n("collections"); + const qc = useQueryClient(); + const toast = useToast(); + const data = useTaxonomyData(siteId); + const taxonomies = data.taxonomies.data ?? []; + const toggle = useMutation({ + mutationFn: ({ tax, on }: { tax: Taxonomy; on: boolean }) => { + const current = tax.collections ?? []; + const next = on ? [...current, collection] : current.filter((c) => c !== collection); + const { collections: _c, ...rest } = tax; + const [first, ...more] = next; + return api.taxonomies.update( + first ? { ...rest, collections: [first, ...more] } : (rest as Taxonomy), + ); + }, + onSuccess: async () => { + await qc.invalidateQueries({ queryKey: ["taxonomies", siteId] }); + toast.success(t("collections.settings.saved")); + }, + }); + return ( + +
+ + {taxonomies.length === 0 && ( + {t("collections.settings.noTaxonomies")} + )} + {taxonomies.map((tax) => + tax.collections ? ( + toggle.mutate({ tax, on })} + /> + ) : ( + + {t("collections.settings.taxonomyAll", { label: data.label(tax) })} + + ), + )} +
+
+ ); +} + +/** Where entries live: shown here, changed in Site settings › Collections (redirect preview). */ +function AddressSection({ + siteId, + site, + collection, +}: { + siteId: string; + site: Site | undefined; + collection: string; +}) { + const { t } = useI18n("collections"); + return ( + +
+ + {`${permalinkPrefix(collectionPermalink(site, collection))}/{slug}`} + + + {t("collections.settings.addressLink")} + +
+
+ ); +} diff --git a/apps/studio/src/routes/CollectionsPage.tsx b/apps/studio/src/routes/CollectionsPage.tsx new file mode 100644 index 00000000..23ea773b --- /dev/null +++ b/apps/studio/src/routes/CollectionsPage.tsx @@ -0,0 +1,150 @@ +import { collectionPermalink, permalinkPrefix } from "@cloudhal/core-ts/permalinks"; +import { + Badge, + Button, + Empty, + LayerCard, + Link, + PageLayout, + Pagination, + Table, + Text, + Tooltip, +} from "@cloudhal/ui"; +import { Plus, Stack } from "@phosphor-icons/react"; +import { useQuery } from "@tanstack/react-query"; +import { useParams } from "@tanstack/react-router"; +import { useState } from "react"; +import { SiteCrumbs } from "../components/Crumbs.tsx"; +import { ErrorBanner } from "../components/ErrorBanner.tsx"; +import { ListToolbar } from "../components/ListToolbar.tsx"; +import { NewCollectionDialog } from "../components/NewCollectionDialog.tsx"; +import { TableSkeleton } from "../components/TableSkeleton.tsx"; +import { useCan } from "../components/useMe.ts"; +import { usePaging } from "../components/usePaging.ts"; +import { useI18n } from "../i18n/index.tsx"; +import { api } from "../lib/api.ts"; +import { COLLECTION_ICONS } from "../lib/collection-icons.ts"; +import { collectionIcon, collectionNames, entryTotal, useCollections } from "../lib/collections.ts"; + +/** + * Site › Collections (RFC-0040 §4): every collection of the registry with its address and entry + * count, and "New collection" for people who may change site settings. + */ +export function CollectionsPage() { + const { t, locale } = useI18n("collections"); + const { siteId } = useParams({ from: "/sites/$siteId" }); + const site = useQuery({ queryKey: ["site", siteId], queryFn: () => api.sites.get(siteId) }); + const registry = useCollections(siteId); + const canCreate = useCan("settings.write"); + const [creating, setCreating] = useState(false); + const rows = (registry.data ?? []).map((c) => ({ + ...c, + name: + collectionNames(site.data, c.key, locale)?.plural ?? + (c.key === "posts" ? t("nav.articles") : c.key), + prefix: permalinkPrefix(collectionPermalink(site.data, c.key)), + })); + const paging = usePaging(rows, (c, q, fold) => fold(c.name).includes(q) || c.key.includes(q)); + const newButton = canCreate && ( + + ); + return ( + } + icon={} + title={t("collections.title")} + description={t("collections.description")} + actions={newButton} + > + + + {registry.data?.length === 0 ? ( + } + title={t("collections.empty")} + description={t("collections.emptyDescription")} + contents={newButton} + /> + ) : ( + + + + + {t("collections.columns.name")} + {t("collections.columns.address")} + {t("collections.columns.entries")} + {t("collections.columns.state")} + + + {registry.isPending ? ( + + ) : ( + + {paging.pageItems.map((c) => { + const Icon = collectionIcon(c, COLLECTION_ICONS); + return ( + + + + + + + {c.name} + + + + + + + {`${c.prefix}/…`} + + + {t("collections.entries", { count: entryTotal(c) })} + + {c.declared ? ( + {t("collections.declared")} + ) : ( + + + {t("collections.inferred")} + + + )} + + + ); + })} + + )} +
+
+ )} + {paging.total > paging.perPage && ( + + )} + c.key)} + /> +
+ ); +} diff --git a/apps/studio/src/routes/CommentsPage.tsx b/apps/studio/src/routes/CommentsPage.tsx index 5d099b60..ecd9dede 100644 --- a/apps/studio/src/routes/CommentsPage.tsx +++ b/apps/studio/src/routes/CommentsPage.tsx @@ -362,8 +362,8 @@ export function CommentsPage() { retry: (count, error) => !isNotEnabled(error) && count < 1, }); const entries = useQuery({ - queryKey: ["entries", siteId, "posts"], - queryFn: () => api.entries.list(siteId, "posts"), + queryKey: ["entries", siteId, "all"], + queryFn: () => api.entries.list(siteId), enabled: known && read.allowed, }); const approvals = usePendingApprovals(siteId); diff --git a/apps/studio/src/routes/SiteShell.tsx b/apps/studio/src/routes/SiteShell.tsx index 36031cc3..ac69f05f 100644 --- a/apps/studio/src/routes/SiteShell.tsx +++ b/apps/studio/src/routes/SiteShell.tsx @@ -1,6 +1,5 @@ import { Sidebar, SidebarSwitcher } from "@cloudhal/ui"; import { - Article, CalendarDots, Camera, ChartLine, @@ -22,6 +21,7 @@ import { useQuery } from "@tanstack/react-query"; import { Outlet, useLocation, useNavigate, useParams } from "@tanstack/react-router"; import { useCallback } from "react"; import { AppFrame } from "../components/AppFrame.tsx"; +import { CollectionNav } from "../components/CollectionNav.tsx"; import { ViewSiteButton } from "../components/LiveLinks.tsx"; import { SitePublish } from "../components/SitePublish.tsx"; import { SiteSwitcherExtra } from "../components/SiteSwitcher.tsx"; @@ -45,10 +45,6 @@ export function SiteShell() { const site = useQuery({ queryKey: ["site", siteId], queryFn: () => api.sites.get(siteId) }); const sites = useQuery({ queryKey: ["sites"], queryFn: api.sites.list }); const pages = useQuery({ queryKey: ["pages", siteId], queryFn: () => api.pages.list(siteId) }); - const entries = useQuery({ - queryKey: ["entries", siteId, "posts"], - queryFn: () => api.entries.list(siteId, "posts"), - }); const snapshots = useQuery({ queryKey: ["snapshots", siteId], queryFn: () => api.snapshots.list(siteId), @@ -143,15 +139,7 @@ export function SiteShell() { {t("nav.pages")} {pages.data?.length ?? 0} - - {t("nav.articles")} - {entries.data?.length ?? 0} - + { + it("names, icons and totals", () => { + expect(collectionNames(SITE, "events", "tr")?.plural).toBe("Events"); + expect(collectionNames(SITE, "posts", "en")).toBeUndefined(); + expect( + collectionIcon({ key: "events", settings: { icon: "calendar" } }, COLLECTION_ICONS), + ).toBe(CalendarBlank); + expect(collectionIcon({ key: "posts", settings: {} })).toBe(Article); + expect(collectionIcon({ key: "jobs", settings: { icon: "https://x" } }, COLLECTION_ICONS)).toBe( + Files, + ); + expect(entryTotal({ counts: { draft: 2, published: 3 } })).toBe(5); + }); + + it("a taxonomy without collections applies to all", () => { + expect(appliesTo({}, "events")).toBe(true); + expect(appliesTo({ collections: ["posts"] }, "events")).toBe(false); + expect(appliesTo(undefined, "events")).toBe(false); + }); + + it("a new collection: labels in the default locale, permalink only when not the default", () => { + const base = { key: "events", plural: " Events ", singular: "Event", icon: "calendar" }; + expect(newCollectionSettings({ ...base, prefix: "", locale: "en" })).toEqual({ + labels: { en: { singular: "Event", plural: "Events" } }, + icon: "calendar", + }); + expect(newCollectionSettings({ ...base, prefix: "/events", locale: "en" }).permalink).toBe( + undefined, + ); + expect(newCollectionSettings({ ...base, prefix: "/etkinlik", locale: "tr" }).permalink).toBe( + "/etkinlik/{slug}", + ); + }); + + it("the settings form keeps the permalink, drops defaults and incomplete label rows", () => { + const settings = { ...SITE.collections?.events, permalink: "/e/{slug}" }; + const labels = labelForm(SITE, settings); + expect(Object.keys(labels)).toEqual(["en", "tr"]); + labels.tr = { singular: "Etkinlik", plural: "", description: "" }; + const next = withForm(settings, { + labels, + icon: "calendar", + feed: false, + sitemap: true, + sort: "newest", + }); + expect(next).toEqual({ + permalink: "/e/{slug}", + labels: { en: { singular: "Event", plural: "Events" } }, + icon: "calendar", + feed: false, + }); + }); +}); diff --git a/scripts/size-baseline.json b/scripts/size-baseline.json index a798f99e..0bc61dd8 100644 --- a/scripts/size-baseline.json +++ b/scripts/size-baseline.json @@ -184,7 +184,7 @@ "lines": 470 }, "apps/studio/src/routes/ArticlesPage.tsx::ArticlesPage": { - "lines": 214 + "lines": 197 }, "apps/studio/src/routes/AssetPage.tsx::AssetPage": { "lines": 739 @@ -268,7 +268,7 @@ "lines": 164 }, "apps/studio/src/routes/SiteShell.tsx::SiteShell": { - "lines": 172 + "lines": 160 }, "apps/studio/src/routes/SkillsPage.tsx::SkillsPage": { "lines": 170