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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ Class names, test ids, route paths, API field names, query keys, and any
status or type value sent to the server. Those are identifiers, not
words, and translating one breaks the software.

Also leave alone the product name Gophenberg, the size units B, KB, MB,
GB and TB, and anything that arrives from the server or from an author.
Also leave alone the product name Gophenberg, and anything that arrives
from the server or from an author. A size unit is no identifier. Wrap
it under the context WordPress gives it,
`_x('KB', 'unit symbol', 'gophenberg')`, so French can write Ko.
A content type's plural label and its description read as their author
typed them, in whatever language they typed them, and no catalogue
reaches them.
Expand Down
5 changes: 4 additions & 1 deletion docs/src/content/docs/contributing/translate-gophenberg.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ that note describes. Your language may well need two different words where
English used one, and that is the reason the context exists.

**Some words stay in English.** The name Gophenberg is never translated.
Neither are the size units B, KB, MB, GB and TB.

**Size units follow your language.** The units B, KB, MB and the larger
ones carry the context "unit symbol". Write the symbol your language
uses, as WordPress does, such as Ko and Mo in French.

**Write the way the software speaks.** Gophenberg addresses the reader
directly and plainly. Keep sentences short, and prefer the everyday word
Expand Down
16 changes: 10 additions & 6 deletions docs/src/content/docs/extending/the-plugin-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,15 +548,19 @@ for the account at `/api/auth/session`, so serve that address with

The frontend SDK, `@gophenberg/frontend-sdk`, passes on a few icons
from `@wordpress/icons`, each named after the job it does in the
admin: `authorIcon`, `backIcon`, `backupIcon`, `brushIcon`,
`categoryIcon`, `closeIcon`, `customPostTypeIcon`, `downIcon`,
`languageIcon`, `listIcon`, `listViewIcon`, `mediaIcon`, `pageIcon`,
`postIcon`, `redoIcon`, `saveIcon`, `settingsIcon`, `sidebarIcon`,
`sidebarRtlIcon`, `trashIcon`, `undoIcon`, `upIcon` and
`uploadIcon`. `authorIcon` is the one an
admin: `audioIcon`, `authorIcon`, `backIcon`, `backupIcon`,
`brushIcon`, `categoryIcon`, `closeIcon`, `customPostTypeIcon`,
`downIcon`, `fileIcon`, `imageIcon`, `languageIcon`, `listIcon`,
`listViewIcon`, `mediaIcon`, `pageIcon`, `postIcon`, `redoIcon`,
`saveIcon`, `settingsIcon`, `sidebarIcon`, `sidebarRtlIcon`,
`trashIcon`, `undoIcon`, `upIcon`, `uploadIcon` and `videoIcon`.
`authorIcon` is the one an
author cell shows when the site shows no avatars, `postIcon` the one
an empty content list shows, `mediaIcon` the one an empty media
library shows, and `uploadIcon` the one an upload button carries.
`imageIcon`, `videoIcon`, `audioIcon` and `fileIcon` are the ones a
media card draws for a file that shows no picture, picked by its
file type as WordPress picks them.
`sidebarIcon` is the one the editor's Settings button carries, and
`sidebarRtlIcon` its mirror for a language written right to left.
`saveIcon` is the one Save draft shows on a phone, and `closeIcon`
Expand Down
150 changes: 128 additions & 22 deletions docs/src/content/docs/guides/media.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: The media library
description: Uploading pictures and files, describing them, and placing them in a post.
description: Uploading pictures and files, finding and editing them, and placing them in a post.
---

The Media screen holds every file you have uploaded. Pictures,
Expand Down Expand Up @@ -54,27 +54,118 @@ An animated GIF is the exception. It is stored exactly as
uploaded and gains no smaller copies, because making them would
throw the animation away.

## Finding and describing

The library opens as a grid of thumbnails. The **Layout** control
switches it to a table, which adds the file name, type and date.
Search matches titles and file names, and the filter narrows the
list to images or to plain files. When nothing matches, the library
says `No media items found.` An empty library says `No media items
yet.` and how to add one.

Every item carries four descriptions, all editable through
**Describe**:

- **Title** names the item in the library.
## Reading the library

The library opens as a grid of cards, the newest file first. On a
screen narrower than 640 pixels, such as a phone, it shows a list
with one file per row instead. There is no layout switch.

Each card shows the picture, the title in bold and the author. An
untitled file shows `(no title)`. A small round avatar sits before
the author's name, drawn the way the site's
[avatar settings](/self-hosting/configuration/#avatars) say, as in
the [posts list](/guides/posts/).

A file that is no picture shows the icon of its kind, video, audio
or a plain file, above its file name. A picture that fails to load
shows the picture icon the same way. A long file name is cut short
with `…` on the card.

Date added, Date modified, Alt text, Caption, Description, File
name, File size, Dimensions and File type are hidden at first. Turn
them on under **View options**. Both dates show the whole date and
time the way WordPress writes them in the language you read the
admin in, such as `October 8, 2026 5:54 pm` or
`8 de octubre de 2026 17:54`. A file name longer than 15 characters
is cut short with `…` and shows whole when you rest the pointer on
it. Alt text shows on pictures alone. An empty alt text, caption,
description or file type shows `-`.

## Finding a file

The library shows 20 files per page by default. **View options**
offers the other page sizes the site allows, and the arrows under
the library move between pages. It also sorts by Title, Date added,
Date modified or Author.

The search box reads `Search media…`. It waits for you to stop
typing, then matches titles, file names, captions and descriptions.

**Add filter** narrows the library:

- **Author** keeps the files of the people you pick, or with **Is
none of** leaves them out.
- **Date added** and **Date modified** keep the files added or
changed before or after a moment you pick.
- **Type** keeps one type of file: Images, Audio, Video, Documents
for PDF files or Archives for ZIP files. Every type is listed, even
one the library holds none of.

Filters add up, so a file has to match every one of them. The page,
the page size, the sort, the search and the filters all live in the
address of the screen. Reload it or share the link and the library
opens the same way.

When nothing matches, the library says `No media items found.` An
empty library says `No media items yet.` and how to add one.

## The actions on a file

The actions menu of a file offers:

- **View** opens the file itself in a new tab.
- **Edit…** opens the file's details, as the next part explains.
- **Rename…** changes the title alone, in a dialog sent with
**Save**.
- **Permanently delete…** removes the file, after a question.

View is the main action and shows on every file. On a phone it also
shows as a button beside the menu of each row. The other three show
only on the files you may change: an author's own uploads, and every
file for an editor or an administrator. Another person's file shows
its title as plain text to an author, and on a phone a tap on its
row opens the file in a new tab, as View does.

## Editing a file

Click the title or the picture of a file you may change to open
**Edit media**, or pick **Edit…** from its actions menu. On a phone,
tap the row. The dialog holds four fields you write in:

- **Title** names the file in the library.
- **Alt text** describes a picture to someone who cannot see it.
Leave it empty when the picture is decorative.
Leave it empty when the picture is decorative. Only pictures have
it, and its link explains how to write one.
- **Caption** is the line shown under a picture in a post.
- **Description** is a longer note for your own use.

If someone else changes an item while you are describing it, your
save is refused rather than quietly overwriting theirs. Reload and
make the change again.
Under them sit the details you cannot change: Date added, Author,
File name, File type, File size and Dimensions. The file name shows
whole here.

Cancel and **Save** stay at the bottom of the dialog while the fields
above them scroll, as in WordPress's own media editor. While the
fields do not fit, the part they scroll in is a stop of its own for
the Tab key, named `Scrollable section` for a screen reader, so the
arrow keys, Page Down and End scroll it. Once it has scrolled, a line
shows under the dialog's header, as in WordPress's dialogs. The dialog
closes the moment you press **Save**, and a short message at the
bottom of the screen says `Media file updated.` once the server
stored the change. The focus then goes back to what opened the
dialog, the title, the picture, the actions menu or the phone row,
wherever the file now sits in the library. It goes to the file's
actions menu when that part no longer shows, and to the library
itself when the file no longer shows there. Only the fields
you changed are sent, so a change someone else made to another field
stays. When the save fails, the message says why, starting with
`Could not save image.` for a picture and `Could not save media.`
for any other file. The dialog then opens on what you typed the next
time you open it for that file.

**Rename…** closes at once too, says `Name updated.`, and keeps a
refused name the same way. Saves to one file go one after the
other, so when you rename a file and then edit it, the last one you
sent wins.

## Placing media in a post

Expand All @@ -96,7 +187,22 @@ library from the Document panel, and the same warning applies.

## Deleting

**Delete Permanently** removes an item and every copy of it from
disk. There is no media trash, so the confirmation is the only
step between you and a deleted file. You can select several items
and delete them together.
**Permanently delete…** removes a file and every copy of it from
disk. There is no media trash, so the question is the only step
between you and a deleted file. It asks in a dialog with no header
that names the file, such as
`Delete "Sunset" for good? This cannot be undone.` Cancel has the
focus when the dialog opens. Press **Delete permanently** and the
button shows the work running until the server answers, then the
dialog closes.

Tick several cards and the **Permanently delete…** button under the
library deletes them all at once, behind one question that counts
them. A phone list has no tick boxes.

A short message at the bottom of the screen names the file, such as
`"Sunset" permanently deleted.`, or counts the files when you deleted
several. A long title is cut short with `…`, in the message and in
the question. When the server refuses, the message gives the reason
instead, and when some of several files fail it counts the ones that
failed.
14 changes: 8 additions & 6 deletions docs/src/content/docs/self-hosting/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,18 +358,20 @@ change them. They shape every list the same way:
- **The format.** `GOPHENBERG_FORMAT_LOCALE` sets the locale for
dates, times and numbers, whatever language a person reads the
admin in. The tag `es-ES` writes 04/10/2026, 14:05 and 1.234,56.
The date column of a content list is the one exception. Like
WordPress, it writes the date the way WordPress does in the
reader's language, with a short month in English and Spanish and
a whole one in French.
The date column of a content list and the two dates of the media
library are the exceptions. Like WordPress, they write the date the
way WordPress does in the reader's language. The content list
writes a short month in English and Spanish and a whole one in
French. The media library writes the whole date and time, such as
`8 de octubre de 2026 17:54` in Spanish.

## Avatars

An avatar is the small round picture beside a person's name. The
three avatar settings copy the Avatars part of WordPress's Discussion
settings. The server reads them when it starts. The author column of
the admin's content lists follows them, and so does every plugin that
shows who wrote something.
the admin's content and media lists follows them, and so does every
plugin that shows who wrote something.

- **`initials`**, the default, draws the first letter of the name in
a coloured circle. The site draws it, so nothing is sent to another
Expand Down
75 changes: 52 additions & 23 deletions frontend/src/content/actions.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// SPDX-License-Identifier: Apache-2.0

import { backupIcon, sessionMayChange, trashIcon } from '@gophenberg/frontend-sdk'
import type { Action } from '@gophenberg/frontend-sdk/dataviews'
import type { Action, RenderModalProps } from '@gophenberg/frontend-sdk/dataviews'
import { RunConfirm, useBulkRun, useToaster } from '@gopherium/godmin'
import type { BulkRun, ListRun } from '@gopherium/godmin'
import { useSession } from '@gopherium/react-auth'
import { __, _x } from '@wordpress/i18n'
import { useQueryClient } from '@tanstack/react-query'
import { useCallback, useMemo } from 'react'

import { useCloseWhileOpen } from '../lists/closeWhileOpen'
import { deleteQuestion, deleteWords } from '../lists/itemWords'
import type { NameCut } from '../lists/itemWords'
import { useUnsavedText } from '../lists/unsavedText'
Expand Down Expand Up @@ -46,6 +47,54 @@ function useRunOver(list: ListRun): BulkRun<Post> {
return useBulkRun(list, refreshPosts)
}

/** What a confirm of the posts list takes beside the props DataViews hands a modal. */
interface PostConfirmProps extends RenderModalProps<Post> {
/** Runs one call over the picked posts. */
run: BulkRun<Post>
/** Cuts a title to the length a toast shows. */
name: NameCut
}

/**
* Renders the confirm moving the picked posts to the trash, closing only itself once the run ends.
* @param props - The posts, the handler closing the modal, the run and the name cut.
* @returns The confirm.
*/
function TrashConfirm({ items, closeModal, run, name }: PostConfirmProps) {
const close = useCloseWhileOpen(closeModal)
return (
<RunConfirm
items={items}
closeModal={close}
confirmLabel={__('Move to trash', 'gophenberg')}
cancelLabel={__('Cancel', 'gophenberg')}
run={(picked) => run(picked, (post) => trashPost(post.id), trashWords(name))}
>
{trashQuestion(items, name)}
</RunConfirm>
)
}

/**
* Renders the confirm deleting the picked posts for good, closing only itself once the run ends.
* @param props - The posts, the handler closing the modal, the run and the name cut.
* @returns The confirm.
*/
function DeleteConfirm({ items, closeModal, run, name }: PostConfirmProps) {
const close = useCloseWhileOpen(closeModal)
return (
<RunConfirm
items={items}
closeModal={close}
confirmLabel={__('Delete permanently', 'gophenberg')}
cancelLabel={__('Cancel', 'gophenberg')}
run={(picked) => run(picked, (post) => deletePost(post.id), deleteWords(name, postName))}
>
{deleteQuestion(items, name, postName)}
</RunConfirm>
)
}

/**
* Returns the actions offered on the rows of a trash view.
* @param mine - Whether the session may change a post.
Expand All @@ -60,7 +109,6 @@ function trashViewActions(
name: NameCut,
type: string,
): Action<Post>[] {
const confirmLabel = __('Permanently delete', 'gophenberg')
return [
{
id: 'restore',
Expand All @@ -79,16 +127,7 @@ function trashViewActions(
isEligible: mine,
hideModalHeader: true,
modalFocusOnMount: 'firstContentElement',
RenderModal: (props) => (
<RunConfirm
{...props}
confirmLabel={confirmLabel}
cancelLabel={__('Cancel', 'gophenberg')}
run={(items) => run(items, (post) => deletePost(post.id), deleteWords(name, postName))}
>
{deleteQuestion(props.items, name, postName)}
</RunConfirm>
),
RenderModal: (props) => <DeleteConfirm {...props} run={run} name={name} />,
},
]
}
Expand Down Expand Up @@ -119,7 +158,6 @@ function listActions(
served: boolean,
unsaved: UnsavedForms,
): Action<Post>[] {
const confirmLabel = __('Move to trash', 'gophenberg')
const changeable = (post: Post) => post.status !== 'trash' && mine(post)
return [
viewAction(served),
Expand Down Expand Up @@ -147,16 +185,7 @@ function listActions(
isEligible: changeable,
hideModalHeader: true,
modalFocusOnMount: 'firstContentElement',
RenderModal: (props) => (
<RunConfirm
{...props}
confirmLabel={confirmLabel}
cancelLabel={__('Cancel', 'gophenberg')}
run={(items) => run(items, (post) => trashPost(post.id), trashWords(name))}
>
{trashQuestion(props.items, name)}
</RunConfirm>
),
RenderModal: (props) => <TrashConfirm {...props} run={run} name={name} />,
},
]
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/languages/es-ES.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/src/languages/fr-FR.json

Large diffs are not rendered by default.

Loading
Loading