Works on any Volto 18 site. On a site that also switches on Volto Light Theme (7.x, from 7.8.1), it adds the theme's hooks as well.
Consolidated Juizi block set for Volto, with a central colour dashboard and per-block toggles
- Nine blocks: Hero, Content Row, Carousel, Gallery, Callout, Audio, Pricing tiers, Redirect and Print button. Reviews with star ratings and scrolling logo strips are display styles of the Carousel (see Blocks).
- A dashboard to switch any block on or off, limit it to some user groups, and manage the colours and themes the blocks share, the editing language and the printout. Its settings can be exported to a file and imported on another site.
- Extras for blocks from other add-ons, when a site has them: the Form block
(
volto-form-block) gets a cleaner look, text beside the form, label and help text options, the site's button styles and a background; the donation and membership sign-up blocks get a background and form colours (see Other add-ons' blocks). - Available in English, French, Portuguese, Brazilian Portuguese, Spanish and Afrikaans (see Languages).
| Part | Version |
|---|---|
Plone (Products.CMFPlone) |
6.1.5 or later (this repository's site: 6.1.5; also installed on 6.2.0) |
| Volto | 18 (this repository's site: 18.32.1; also tested on 18.35.0) |
Volto Light Theme (@kitconcept/volto-light-theme / kitconcept.voltolighttheme) |
optional, the site's choice: any 7.x from 7.8.1 (the 7.x line is the one for Volto 18; 8.x needs Volto 19). This repository's site uses 7.8.6 |
embla-carousel-react |
^8.6.0 |
lucide-react |
^1.48.0 |
The backend runs on Python 3.12 or later (this repository: 3.12,
backend/.python-version; also tested on 3.13).
Volto Light Theme is optional. juizi-blocks never imports from it,
installs it or switches it on; a site switches it on itself (in its own
package.json and volto.config.js). juizi-blocks checks at runtime whether
the site has it (hasVLT() in src/config/vlt.ts, from Volto's list of the
site's add-ons) and only then adds the theme's hooks, such as the block theme
picker for blocks marked usesThemes. The backend package doesn't need it
either. juizi-blocks adds only its own blocks (and switches on
@kitconcept/volto-bm3-compat, which its block wrapper uses); other block
add-ons are the site's choice. This repository's development site switches on
Volto Light Theme and the block add-ons it works with, in
frontend/package.json and frontend/volto.config.js.
juizi-blocks has two halves, installed separately:
- the backend package
juizi.blocks, on the site's Plone backend, stores the settings (block switches, colours and themes, editing language, print settings) and serves them; - the Volto add-on
volto-juizi-blocks, in the site's frontend, has the blocks, the dashboard and the styling.
A backend can serve several Plone sites, each with its own frontend. Add the backend package once; add the frontend add-on to each site that should have the blocks.
- Plone 6.1 or 6.2 (
juizi.blocksrequiresProducts.CMFPlone6.1.5 or later), Python 3.12 or later - Volto 18, Node.js 22, pnpm
- Volto Light Theme is optional (7.x, from 7.8.1); see below
With mxdev (as in Juizi's projects), add a section to the backend's
mx.ini:
[juizi.blocks]
url = git@github.com:juizi-com/juizi-blocks.git
pushurl = git@github.com:juizi-com/juizi-blocks.git
branch = main
subdirectory = backendand juizi.blocks to the dependencies in the backend's pyproject.toml.
Without mxdev, depend on the git URL directly (uv or pip):
juizi.blocks @ git+https://github.com/juizi-com/juizi-blocks.git@main#subdirectory=backend
Run the backend's install (make install) and restart it. This only adds
the package: it installs nothing on any site, and doesn't bring Volto Light
Theme. The package loads itself (plone.autoinclude); no ZCML to add.
Check the repository out into the frontend's packages/ with
mrs-developer: add to the frontend's mrs.developer.json:
"juizi-blocks": {
"output": "./packages",
"package": "volto-juizi-blocks",
"url": "git@github.com:juizi-com/juizi-blocks.git",
"https": "https://github.com/juizi-com/juizi-blocks.git",
"path": "frontend/packages/volto-juizi-blocks",
"branch": "main"
}mrs-developer clones the whole repository into packages/juizi-blocks; the
add-on is at packages/juizi-blocks/frontend/packages/volto-juizi-blocks,
which pnpm finds through the workspace pattern packages/**/packages/* (in
pnpm-workspace.yaml; Cookieplone projects have it).
add it to the frontend's package.json dependencies:
"volto-juizi-blocks": "workspace:*"and switch it on in volto.config.js:
const addons = ['volto-juizi-blocks', 'volto-<site>'];Then make install (it runs mrs-developer and pnpm install) and rebuild or
restart the frontend. The site's own add-on comes last, so it can override.
With the Form block: list volto-juizi-blocks after volto-form-block
(and after any add-on that replaces the form's sidebar schema), or its form
options aren't added (see Other add-ons' blocks):
const addons = ['volto-subblocks', 'volto-form-block', 'volto-juizi-blocks', 'volto-<site>'];With Volto Light Theme: juizi-blocks doesn't install or switch it on. A
site that wants it adds it (and the block add-ons it works with) itself, and
lists it before volto-juizi-blocks (after the block add-ons it adjusts):
const addons = [
// ...the kitconcept / eeacms block add-ons the site uses...
'@kitconcept/volto-light-theme',
'volto-juizi-blocks',
'volto-<site>',
];
const theme = '@kitconcept/volto-light-theme';Volto Light Theme's logos block asks for its own copy of the theme (7.6.0);
add a pnpm override in the frontend's package.json so there is one:
"pnpm": { "overrides": { "@kitconcept/volto-light-theme": "<version>" } }.
Without the theme, juizi-blocks styles its blocks for Volto's own layout
(see "Two layers: width and full width").
A site with its own copies of these blocks: if the site's add-on registers
blocks with the same ids (juiziHero, contentRow, emblaCarousel,
emblaGallery, redirectBlock, juiziCallout, audioBlock), remove them, so
juizi-blocks' are used. Saved pages keep their blocks; older block types are
converted as they load (see "Older content"). A site with plone.org's copy
of the Hero (block type hero) removes it too: its blocks are converted to
juiziHero.
In the site's frontend, go to Site Setup β Add-ons and install Juizi
Blocks. It is only listed on a frontend that includes volto-juizi-blocks
(see "Installing" under "How the block set works"). Installing adds the add-on's own settings and
nothing else: the site's title, settings and content are left alone. The
Juizi blocks then appear in the block chooser, and Site Setup β Juizi
Blocks opens the dashboard.
Until it is installed, a frontend with the add-on shows none of it (see above), so the frontend can be deployed first.
A new install starts with juizi-blocks' own colours and themes, not the site's. To give it another site's settings (say, from the development site to production), use Site Setup β Juizi Blocks β Export and import: Download settings on the site that has them, then choose the file on the new site, pick the parts to take, check them and Save. See The dashboard.
Pull the new version (mxdev / mrs-developer, make install), restart the
backend and rebuild the frontend. If the backend's install profile changed,
Site Setup β Add-ons offers the upgrade (for example 1003, which adds the
Editing language setting). See "Versions" for how releases are
numbered.
backend juizi.blocks frontend volto-juizi-blocks
βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββ
registry juizi.blocks.disabled_blocks blocks/index.ts the block manifest
juizi.blocks.enabled_blocks components/Blocks/* the blocks
juizi.blocks.block_groups (JSON)
juizi.blocks.color_config (JSON) config/colors.js colour API used by blocks
GET /@juizi-blocks-settings (public) ββΊ settings/runtime.ts applies settings to the app
PATCH /@juizi-blocks-settings (Manager) ββ components/Dashboard /controlpanel/juizi-blocks
Paths below starting with src/ are in frontend/packages/volto-juizi-blocks/src/.
Installing. Install Juizi Blocks in Site Setup β Add-ons. One backend
can serve several sites, and only the ones whose frontend includes
volto-juizi-blocks can use the add-on, so it is only offered there: the
backend leaves it out of its add-ons list until it is installed on the site,
and this frontend add-on adds it back (src/config/addons.ts); Volto's usual
Install button installs juizi.blocks:default, which adds only the add-on's
own settings. Once installed, the backend lists it as usual. Other add-ons
offer themselves the same way (the collective payments and form add-ons,
juizi.dashboard, members-only), so this one wraps whichever add-ons reducer
an earlier add-on configured instead of replacing it, and they all stay in
the list.
The frontend add-on stays inactive on a site until juizi.blocks is installed
there, so it can be deployed first. The backend's settings service only exists
once the add-on is installed; until it answers, the Juizi blocks are left out
of the block chooser, the dashboard is not listed in Site Setup, no colours
are output, the site's own themes are kept and older blocks are not
converted. Blocks already on pages keep rendering. See
src/settings/runtime.ts (setInstalled).
This README is the home for everything the blocks share: the dashboard,
colours and buttons, shared styling, links, the shared block toolkit and the
conventions every block follows. Each block's own README (in its folder under
src/components/Blocks/) covers only what that block does, and links back
here. src/components/Blocks/_shared/README.md and src/config/README.md
point here too.
| Block | id | Colours | Themes | README |
|---|---|---|---|---|
| Hero | juiziHero |
yes | HeroBlock/README.md |
|
| Content Row | contentRow |
yes | ContentRow/README.md |
|
| Carousel | emblaCarousel |
yes | EmblaCarousel/README.md |
|
| Gallery | emblaGallery |
yes | EmblaGallery/README.md |
|
| Redirect | redirectBlock |
Redirect/README.md |
||
| Callout | juiziCallout |
yes | Callout/README.md |
|
| Audio | audioBlock |
yes | Audio/README.md |
|
| Pricing tiers | pricingTiers |
yes | PricingTiers/README.md |
|
| Print button | printReport |
yes | PrintButton/README.md |
The ids match the previous add-on, so existing content keeps rendering.
In the block chooser they are grouped under Juizi (after "Most used").
A block definition can set its own group to go elsewhere. The decisions
from the September 2026 block audit are summarised in each block's README.
Some things that were blocks of their own in earlier add-ons are a display style of one of these blocks, chosen as the block's first option:
| Looking for | Use | Display style |
|---|---|---|
| Reviews, testimonials, star ratings | Carousel | Reviews |
| A scrolling strip of partner or sponsor logos | Carousel | Scrolling logo strip |
| Cards with a picture above the text | Carousel | Image above content |
| Numbered steps, icon links, statistics, image cards | Content Row | Numbered, Icon, Statistics, Image card |
| A slideshow or a grid of pictures | Gallery | Slideshow, Even grid, Natural grid |
| A page header, or a coloured band with a heading and buttons | Hero | Hero (page header), Section (content band) |
A site that also loads kitconcept or eeacms block add-ons (as this repository's development site does) has blocks that do the same job as a Juizi block. An editor with no context sees both in the chooser. Decide per site which to keep, and switch the others off in Site Setup β Juizi Blocks β Blocks:
| Juizi block | Overlaps with |
|---|---|
| Carousel | kitconcept Carousel, kitconcept Slider |
| Hero (Hero style) | kitconcept Banner, kitconcept Introduction |
| Hero (Section style) | kitconcept Highlight |
Switching a block off only hides it from the chooser: pages that already
use it keep rendering. Add this check to each site's launch checklist.
The Hero's Snapshot summary mode was removed: it depended on the NQF
Snapshot content type and utilities that are not part of this project.
juizi-blocks also adds to some blocks that come from other add-ons. Each extension does nothing on a site without that block, and the block works without juizi-blocks: removing juizi-blocks leaves saved pages rendering, with the extra settings ignored.
| Block | From | What juizi-blocks adds | README |
|---|---|---|---|
Form (form) |
volto-form-block (collective.volto.formsupport) |
A cleaner look for every form; text beside the form; labels and help text placement; help text colour; Send and Cancel button styles; background and padding | extensions/formBlock/README.md |
Donation (donationBlock) |
volto-collective-donations (collective.donations) |
Background and padding, form colours; the block in the Juizi group | extensions/donationBlock/README.md |
Membership sign-up (membershipSignup) |
volto-collective-membership (collective.membership) |
Background and padding, form colours; the block in the Juizi group | extensions/membershipSignup/README.md |
Buy button (shopBuy) and shop pages |
volto-collective-shop (collective.shop) |
The site's button colours and shape on the block, cart and checkout; background and padding; button colours; the block in the Juizi group | extensions/shopBuy/README.md |
| Resource type, Resource facts, Download button, Abstract, Key findings | volto-resource-library (collective.resourcelibrary) |
Background and padding on all five; a label colour, heading position and button colours where they apply. The blocks stay in their own Resource group | extensions/resourceBlocks/README.md |
The donation, membership, Buy and resource blocks look up a blockExtension
utility that juizi-blocks registers. volto-form-block has no such hook, so juizi-blocks
wraps the form's view, edit and sidebar schema in its block config instead of
shadowing its files (the collective.formsupport add-ons already shadow some,
and only one shadow of a file wins). That is why volto-juizi-blocks must be
listed after volto-form-block.
The Form block, in more detail:
- Every form gets the Juizi look, with no setting to change: a light box
with a soft shadow, bordered fields with a focus ring, labels in the text
colour, help text in a readable grey without a line under it, errors as a
red border and message, "errors in the form" / sending error / "Sent!" as
boxes with a coloured edge, Juizi-shaped buttons, and the side gutter on
narrow screens. Adjust it with the
--juizi-form-*tokens (see Tokens). - Layout: "Form with text beside it" adds a text column (text above the heading, heading, introduction, more text) on the left or right; on phones the text comes first. Same fields and wording as the donation and membership blocks. "This is the primary page heading" makes its heading the page's h1.
- Appearance: labels beside or above the fields (always above on phones); help text below the field or under the label, in a colour from the site list (a light colour gets an edit-mode warning); Send and Cancel/back buttons solid or outlined in a site colour.
- Background & spacing: colour, image (with position and overlay) and top and bottom padding, as in the Content Row. The form keeps its white box on any background, so labels, errors and messages stay legible.
- In the editor the canvas shows the same layout, label and help text position as the published page. With the form's Data tab open, the form takes the full width and the text moves above it, so the saved answers have room.
- Saved forms don't change until an option is chosen; only the shared look applies to them.
- Not yet: the text beside the form isn't in the site search (no
searchable-text adapter for
formblocks yet).
Three tabs, and a fourth (Export and import) for people who can see Site Setup, sharing one Save: the save button in Volto's toolbar, or the bar that appears at the top as soon as something has changed (Save / Discard changes). Problems that would stop a save (a colour without a valid name, a theme missing its text colour, β¦) are listed there in the editor's language.
The footer shows the add-on's name and version, read from the package.
Look: the dashboard follows the Juizi dashboard style (first used in
collective.bulkmailproviders): a centred 800px column of plain elements,
light grey cards with white panels inside, flat buttons, small upper-case
badges for state. The rules are in src/theme/juizi-blocks.scss; new
dashboard sections should reuse its classes (juizi-dashboard__section,
__btn, __badge, __table, __input, β¦) rather than Semantic UI
components.
Editing language (top of the Blocks tab) β shows the Juizi blocks'
editing interface in one chosen language, whatever the page's language:
handy when building a multilingual site in a language you don't read. See
Editing in another language. "Same as the
page" (the default) switches it off; set it back when the site launches.
Stored as editor_language ('', en, fr, pt, pt-br, es, af).
Blocks tab β a checkbox for every block registered in the site (Volto, Volto Light Theme, add-ons and Juizi), with an Enabled / Disabled badge, grouped as in the block chooser with the Juizi group first.
- The list is read from the running configuration, so a rebuild that adds or removes blocks updates it automatically. New blocks start switched on. Saved choices are kept, including those for blocks that are missing from the current build.
- A block's own
restrictedsetting is its default.truemeans off by default; you can switch it on. A function (e.g. Title once per page, Event metadata only on Events) keeps its constraint while switched on. A site's own config may change a block'srestricted(CRA makes Lead Image available everywhere); its value is then the block's default. - Text (
slate), Description, Image and Title are locked on (LOCKED_BLOCKSinsettings/constants.ts). - Switched-off blocks disappear from the block chooser and the
/menu. Blocks already on pages keep rendering. Containers with their own list of allowed blocks (e.g. Grid) use that list. - Stored as
disabled_blocks/enabled_blocks: only the departures from each block's default. - Who can add it β every switched-on block that isn't locked has a
collapsed line under its name, "Who can add it: Everybody". Open it and tick
user groups to offer the block only to their members; the collapsed line
then lists those groups. Tick none (or Offer to everybody) to go back.
Like the switch, this only affects the block chooser and the
/menu: existing blocks keep rendering and other people can still edit them. The check uses the groups Volto loads for the current user (@users/<id>), and there's no exception for Managers, so include your own group if you still want to add the block. Stored asblock_groups({block id: [group id, ...]}); groups deleted later stay listed so they can be unticked. Until the site's groups have loaded (and in the page the server sends), the list shows only the ticked groups and says nothing about the site having none, so the server and browser draw the same page.
Colours tab:
- Colours β the master colour list. Each colour has a name (its CSS
variable), label, hex value, a dark flag (suggested from the colour, can be
overridden) and an optional text colour (automatic by default; the dashboard
shows the contrast ratio). The colours are output as
--<name>,--<name>-rgband--<name>-foregroundon:root. - Block themes β Volto Light Theme themes (
config.blocks.themes, the Styling β Background color swatches) built from the colours: background, text, cards/surfaces and muted text, each optionally faded. They're used by VLT's own blocks (Grid, Teaser, Text, Listing, β¦). The Juizi blocks use the colour list instead. Shown only on sites that switch on Volto Light Theme. - Colours per block β narrow the colours each Juizi block offers. Nothing ticked means everything.
- Callout types β the starting background and icon colour for each
Callout type (Information, Tip, Warning, β¦). Shown while the Callout is
registered; stored under
color_config.blocks.juiziCallout.calloutTypes. SeeCallout/README.md. - A "Themes per block" table appears for any Juizi block defined with
usesThemes(none are right now), on sites with Volto Light Theme.
Print tab β the Print button block's settings for the whole site,
stored as print_config (validated by juizi.blocks.settings.validate_print_config):
the button's default colour and style (each block can choose its own), the
top of the printout (logo: the site's, another picture by address, or none;
two lines of text), the bottom line, the paper size and orientation, and the
kinds of blocks left out. Text can use [title], [section],
[publication date], [today], [site name] and [page address]. Under
Advanced, a custom HTML header and footer for the web team replace the
fields when filled in. See PrintButton/README.md.
Export and import tab β copies the settings from one site to another,
e.g. from a development site to production. Shown only to people who can see
Site Setup (Managers and Site Administrators: Volto's plone_setup action),
the same people the backend lets save.
- Download settings saves a JSON file holding the saved settings (not
unsaved changes): block switches and groups, editing language, colours and
themes with the per-block choices, and print settings. It also records the
site, the date and the add-on version (
format: "juizi-blocks-settings",format_version: 1;settings/transfer.ts). - Choose a file⦠reads such a file and lists the parts it has, all ticked: Blocks switched on or off, and who can add them, Editing language, Colours and themes, and which blocks offer them, Print settings. Each part replaces the same part here as a whole.
- Importing changes only the dashboard's draft: the unsaved bar appears, so
the other tabs can be checked before Save (or Discard changes). Saving goes
through
PATCH @juizi-blocks-settings, which validates every value again. - Other JSON files and files from a newer
format_versionare refused. Colours that fail the dashboard's checks are held back while the rest can still be imported. Blocks limited to user groups the site doesn't have are flagged, since nobody would be offered them.
Blocks store a colour as var(--<name>) (the same format as before), so
changing a colour's value updates every block that uses it. Renaming a colour
does not rewrite existing block data.
The settings are fetched during server-side rendering and applied before the
page renders (and in the browser from the serialised state before hydration),
so MASTER_COLOR_LIST, getBlockColorList() and VLT's themes are correct in
the first response.
All colour logic for the blocks goes through this file; the colours
themselves are managed in the dashboard. MASTER_COLOR_LIST keeps its
[CSS variable, Label, 'light' | 'dark'] format and is updated in place when
the settings load. A block's schema asks for its own list with
getBlockColorList('<block id>'), inside the schema function (never copy
colour lists at module level), so the dashboard's "Colours per block" applies.
| Function | Purpose |
|---|---|
MASTER_COLOR_LIST |
Full colour list with lightness values (live) |
getBlockColorList(blockType) |
The colours the dashboard allows for a block |
getColorTextStyle(value) |
{ color: var(--<name>-foreground) } for a background colour |
getColorChoices(list) |
[value, label] pairs for Volto select widgets |
getButtonChoices(list, t) |
Solid + outlined button options for every colour (t: the schema's translator) |
getDefaultButton(bgColor, list) |
A contrasting button style for a background |
getContrastingColor(bgColor, list) |
The first colour of opposite lightness to the background (a transparent or unknown background counts as light), for defaults that must stand out, e.g. the Gallery's highlight around the current picture |
getButtonClasses(value, baseClass) |
className and style for a button element |
isColorDark(value, list) |
true if a colour needs light text |
colorValueToKey(value) |
var(--brand-primary-blue) β brand-primary-blue |
colorKeyToCssVar(key, list) |
Reverses colorValueToKey |
An overlay is the layer drawn between a background picture and the text on it, so the text stays readable: a gradient that darkens one side, or a flat tint. Editors choose it in the Image overlay field of a Hero or a Content Row background, and in Card image overlay for Content Row picture cards. Every one of those fields offers the same list, and every block draws the choice the same way (inline, from the list; no overlay colours in the blocks' stylesheets).
The file is called gradients.ts because most custom overlays are gradients,
but it holds every overlay, tints and "None" included.
A site that doesn't set its own list gets DEFAULT_OVERLAYS:
| Id | Editors see | What it draws |
|---|---|---|
gradient |
Gradient (default) | Dark from the bottom: black 10% at the top to 60% at the foot. Also what blocks with no overlay saved get |
none |
None | Nothing. Use for patterns and textures, which already sit well behind text |
black-30, -50, -70 |
Black β Light / Medium / Dark | Flat black at 30, 50 or 70% |
white-30, -50, -70 |
White β β¦ | Flat white (for dark text on a picture) |
primary-30, -50, -70 |
Brand colour β β¦ | Meant as a flat tint in the brand colour, but --accent-color-rgb isn't defined, so these currently draw nothing (known issue) |
A site's list replaces the built-in one, so editors see one list made for that site. Keep it short: a handful of overlays an editor can tell apart by name.
-
In the site's add-on, add
src/config/gradients.tswith the full list, and install it from the add-on'sindex.ts(the site's add-on loads after juizi-blocks, so its list wins):import type { ConfigType } from '@plone/registry'; import { overlayById, type Overlay, } from 'volto-juizi-blocks/config/gradients'; const SITE_OVERLAYS: Overlay[] = [ overlayById('none')!, overlayById('gradient')!, // the default for blocks with none saved { id: 'brand-side', label: 'Brand blue, from the left', background: 'linear-gradient(90deg, rgba(2, 62, 138, 0.94) 0%, rgba(2, 62, 138, 0.74) 48%, rgba(2, 62, 138, 0.2) 100%)', }, ]; export default function install(config: ConfigType) { config.settings.juiziBlocks = { ...config.settings.juiziBlocks, overlays: SITE_OVERLAYS, }; return config; }
-
Keep
noneandgradient. "None" is what editors need for patterns;gradientis what every block without a saved overlay draws, so leaving it out takes it off the list while those blocks still use it. -
Ids are stored in the pages. Pick a short, lower-case id per overlay and never change it once editors have used it; to retire one, remove it and the blocks that used it lose their overlay (their field shows the bare id). Labels can change at any time.
-
Labels describe the look, in the editor's words: the colour and where it's strongest ("Brand blue, from the left", "Navy, from the bottom"), not the CSS ("90deg gradient"). Site labels are shown as written, so write them in the site's language.
-
backgroundis any CSS background, drawn over the whole picture:- A gradient fades from strong where the text sits to light or clear
elsewhere, so the picture still shows. Direction:
90degstrong on the left,270degon the right,180degat the foot,0degat the top,135deg/160degdiagonal. - Write colours as
rgba()with an opacity (ortransparent); a solid colour hides the picture. - Where text sits, aim for 0.7 or more, and check the contrast of the text against the overlay on a light picture (at least 4.5:1 for body text, 3:1 for large headings). Bright photos need stronger overlays.
- To follow the dashboard's colours instead of fixing them, use the
--<name>-rgbvariable it outputs for each colour (see Colours tab):rgba(var(--liberty-blue-rgb), 0.8). Changing that colour in the dashboard then changes the overlay too. - Layered backgrounds work too:
'linear-gradient(...), linear-gradient(...)'.
- A gradient fades from strong where the text sits to light or clear
elsewhere, so the picture still shows. Direction:
-
Text colour doesn't follow the overlay. In a Hero's Hero style, text on a picture is white, so overlays there should be dark. In a Section or a Content Row the text follows the background colour, so pick that to match (a dark overlay over a light colour gives dark text on a dark overlay).
-
Check each overlay on a light and a dark picture, on a wide and a narrow screen, before handing it to editors.
Saved blocks keep their look when the list changes: a built-in overlay the site left out is still drawn, and stays in that block's list as a choice so the field isn't blank. An overlay removed altogether draws nothing.
IRR's list (volto-irr-test/src/config/gradients.ts in the IRR project) is a
worked example: None, Gradient and six brand gradients.
| Function | Purpose |
|---|---|
getOverlays() |
The current list: the site's, else DEFAULT_OVERLAYS |
getOverlayChoices(t, saved) |
[id, label] choices for a schema; saved keeps the block's saved value listed |
overlayBackground(id) |
{ background } for the overlay layer, or null |
overlayById(id) |
A built-in overlay, to reuse in a site's list |
DEFAULT_OVERLAY |
'gradient', for blocks with none saved |
Blocks never ask editors for a text colour. It follows the background:
isColorDark() gives the bg-dark / bg-light / tone-* classes and
getColorTextStyle() the inline colour (the dashboard's text colour for that
background). A block's heading and intro text use that one colour, so they
always match. The one exception is text over a background image, which
can't be judged automatically: the Carousel asks "Text over the background
image" only when one is set, and the Hero's Section style explains in the
"Background colour" help that the colour still sets the text colour over an
image.
When adding colours in the dashboard, check the contrast ratio it shows meets WCAG AA (4.5:1 for body text, 3:1 for large text).
import { getButtonClasses } from '../../../config/colors';
const { className, style } = getButtonClasses(btn.buttonStyle, 'hero-button');
// className β 'hero-button btn-unified btn-solid btn-color-white hero-button--solid'
// style β { '--btn-color': 'var(--white)', '--btn-foreground': '#111' }
return <a href={href} className={className} style={style}>{btn.label}</a>;--btn-color is the button's colour, --btn-foreground its text colour
(from the dashboard, else from the colour's lightness), so button text is
always readable without the editor choosing it.
buttons.scss is the CSS counterpart and holds every button colour rule;
block stylesheets handle layout only. Rules cover view (.public-ui) and
edit (.cms-ui) and are specific enough to beat VLT's link colour without
!important. Add btn-small for the small size (the Hero's "Use smaller
buttons" does this). When adding a block with buttons, add its button base
class to $block-btn-classes:
$block-btn-classes: 'hero-button', 'row-button', 'card-button', 'button';Shape: every button, and the Carousel's and Gallery's controls (arrows,
"More" button, enlarged-view buttons), use --juizi-button-radius (4px) and
--juizi-button-border-width (2px), so a site changes them in one place.
Like buttons.scss does for buttons, this file styles what every block has:
preheaders, block titles, Hero titles, descriptions, item preheaders and
headings, the padding scale, the two width layers and full width, card
corners and shadows, links, and visually-hidden. Each rule reads a
--juizi-* token; SCSS lists near the top map each block's own class names
onto those roles ($juizi-titles, $juizi-descriptions, $juizi-layers,
$juizi-plain-links, β¦).
To align a site, override tokens in its theme rather than restyling blocks.
Set them on :root: some tokens are worked out from others there, so an
override on a narrower selector wouldn't reach them.
:root {
--juizi-preheader-font-size: 16px;
--juizi-title-font-size: 68px;
--juizi-title-line-height: 64px;
--juizi-card-radius: 24px;
--juizi-card-shadow: 0 3px 6px -4px #000;
--juizi-content-width: 1200px; /* every block's content width */
--juizi-button-radius: 0; /* square buttons and carousel controls */
}| Group | Tokens |
|---|---|
| Typography | --juizi-heading-font-family, --juizi-preheader-*, --juizi-title-*, --juizi-hero-title-*, --juizi-description-*, --juizi-item-* |
| Spacing | --juizi-pad-compact / -default / -spacious / -extra-spacious (+ -mobile) |
| Width | --juizi-gutter, --juizi-content-width, per-block --juizi-*-width |
| Boxes | --juizi-callout-padding (1.5rem), --juizi-redirect-padding (1.25rem) |
| Cards | --juizi-card-radius, --juizi-card-shadow |
| Item borders | --juizi-item-line-color (a shade of the text colour), --juizi-item-line-width, --juizi-item-line-spacing, --juizi-item-box-padding (the Content Row's "Item borders") |
| Icon backgrounds | --juizi-icon-background-radius (the Content Row's icon and number circles: 50% round, 0 square) |
| Reviews | --juizi-review-star-color, --juizi-review-star-color-on-dark (the Carousel's Reviews style; keep each at 3:1 or more against its background) |
| Buttons | --juizi-button-radius, --juizi-button-border-width |
| Form block | --juizi-form-box-background / -foreground / -border / -radius / -shadow / -padding, --juizi-form-field-background / -border / -radius, --juizi-form-accent (focus), --juizi-form-error, --juizi-form-success |
Defaults follow Volto Light Theme's sizes, and the blocks' own stylesheets don't set these values themselves.
Blocks with top/bottom padding options share one scale: the Hero and
Content Row (always set), and the Carousel, Gallery, form, donation and
membership sign-up blocks (optional: left empty, the block keeps its usual
space, so saved blocks don't change; $juizi-optional-padded-blocks in
juizi-common.scss). The Callout, Audio and Redirect blocks are boxes in
the flow of the page and follow its usual spacing, so they don't offer it.
| Value | Desktop | Mobile |
|---|---|---|
none |
0 | 0 |
compact |
2rem | 2rem |
default |
4rem | 4rem |
spacious |
8rem | 5rem |
extra-spacious |
12rem | 8rem |
On a site with Volto Light Theme, the blocks sit in its
.blocks-group-wrapper and use the two layers below. On a site without it,
the blocks stay in Volto's page column (#page-document): a block with
"Full width" on has its outer layer (background) run to the window edges
while its content stays in line with the page column, and the other blocks
keep the side gutter inside the column (juizi-common.scss, "Full width on a
site without Volto Light Theme").
- The outer layer is always full width, carries the block's background
colour or image, and has
--juizi-gutterside padding, so the content keeps equal side padding as the window narrows. - The inner layer holds the content at one shared width,
--juizi-content-width(VLT's layout width by default), or spans the outer layer when the block's "Full width" option is on. Per-block overrides exist (--juizi-hero-content-width,--juizi-content-row-width,--juizi-carousel-content-width,--juizi-gallery-content-width,--juizi-callout-width,--juizi-redirect-width) but all follow--juizi-content-widthunless set.
Boxes (Callout, Redirect): their inner layer is a box with its own
background, not just a width container, so they keep side padding inside it
(--juizi-callout-padding, only when the Callout has a background colour;
--juizi-redirect-padding). Everything else clears the inner layer's side
padding.
Full width and Volto's .full-width: the Carousel and Gallery mark
"Full width" with a full-width class, which Volto's own theme also uses to
break out of the page column (margin-left: -50vw !important). The shared
styling undoes Volto's version for these blocks, so they don't land half off
screen in the editor; in the editor a full-width block fills the editing
area.
VLT colours every link in page content with var(--link-foreground-color)
and underlines it, with a selector that beats ordinary block CSS. The shared
styling handles this for every block; don't fight it in a block's own
stylesheet.
- Colour: each block's outer class in
$juizi-layerssets--link-foreground-color: currentColor, so links follow the text colour the block or item sets. A new block gets this by being added to$juizi-layers. Anything that sets its own value (buttons, the Callout's link colour) keeps it. - Underline: links that aren't in running text (a whole-item link, a
breadcrumb, a card heading that is the card's link) go in
$juizi-plain-linksand lose the underline; links in running text inside those go in$juizi-underlined-linksand keep it. VLT's selector is (0,4,1), so these rules repeat the class to outrank it (the same trickbuttons.scssuses).
The two Embla blocks share these decisions, each in its own stylesheet:
- Arrows have the button shape above. "Arrow style" starts with Standard
(dark arrows over the pictures on the sides, outlined in the text colour
elsewhere); the other choices are the button colours, and make the arrows
unified buttons with those colours and their hover (
config/buttons.scsscovers<button>as well as links for colours; links alone get the words-sized padding). - On phones (768px and below) the arrows always sit in a row below, whatever "Arrow position" says; the field's help text tells editors.
- Arrows only show when there's somewhere to go: not when every slide is in view (Carousel) or the row of small pictures fits (Gallery).
- A row that fits across can be aligned left, centre or right (logo strip, row of small pictures); once it scrolls, it starts at the left.
- Autoplay and moving strips stop for visitors who ask their device for less motion.
Code every Juizi block uses. _shared/ isn't a block: src/blocks/index.ts
registers blocks from an explicit list. When a pattern in one block solves a
problem another block also has, it belongs here.
makeBlockEdit(View, { getChangedData, normalizeData, getFormData, formKey })
(src/components/BlockEdit/BlockEdit.jsx) is the Edit for every Juizi block.
It renders the View with isEditMode as the canvas, and the block's
juiziSchema in the sidebar.
- Schemas are registered as
juiziSchema, not Volto'sblockSchema: Volto mergesblockSchemadefaults into every saved block when it renders, which would change how older content looks. Schema functions read their data withschemaData(args). - Dropdowns with number values: Volto's select widget can't show the
label of a number value (it looks it up among string keys), so it showed
"8000" instead of "8 seconds".
makeBlockEdithands the form numbers as strings for any field with choices (display only; nothing is saved until the editor changes a field), andwithSavedChoicereturns string values. Views read these fields withparseInt, so saved numbers and strings both work. - A View must never do anything in edit mode that it would do for visitors (the Redirect never redirects there).
BlockPlaceholder β the start screen and the "nothing to show" message,
for when the whole block has nothing to show. Only rendered in edit mode;
with no display style chosen, a block's view returns null for visitors.
| Prop | Required | Notes |
|---|---|---|
blockClass |
yes | The block's own class, so block CSS can scope it. Use a separate class if the block's root class carries box styles (the Redirect uses redirect-block-placeholder) |
prompt |
yes | One or two sentences telling the editor what to do next |
modes |
no | [{ name, description }], one per display style, in dropdown order, in the editor's words |
Styling (block-placeholder.css): dashed border, lightest background,
centred, 480px inner width, using --light, --lightest, --grey, --dark
with fallbacks.
EditHint β a short edit-only message inside the block's own frame
("Add a heading in the sidebar.", "No results tagged 'Events'β¦"). Renders
nothing unless isEditMode. Props: isEditMode, tone ('hint' or
'warning'), as (p by default; span inside a button), live (announce
it when it appears, for warnings that follow an editor's change). A hint
follows the block's text colour on a dark background; a warning is a box of
its own, always dark text on a light background, so it stays readable
whatever the block's colours (edit-hint.css).
Unfinished items (a button without a link, a slide without a picture) use
the class block__unfinished (dashed outline) with an EditHint as their
text, and are left out for visitors. Flag them only where it helps: the Hero
flags an unfinished button only while it has nothing else in it.
BlockErrorBoundary β wrap a block's view in it. On an error, editors
see "This block couldn't be displayed. Try undoing your last changeβ¦" (in
their language), visitors see nothing, and the error is logged. Pass resetKey (e.g.
JSON.stringify(data)) so the editor's next change gets a fresh try.
BlockWrapper β the block's outer container, from
@kitconcept/volto-bm3-compat. Under block model 2 it renders
<div class="block {@type} {className}">; under block model 3 it renders
nothing and VLT draws the containers. Every Juizi view renders inside it
(placeholders too), with its modifiers in className: type-{style},
align-{x}, tone-{light|dark}. Add them to Volto's own classes, never in
their place: className={cx(props.className, 'type-β¦')}. Volto passes the
block's style classes and VLT's neighbour classes (next--is--slate,
previous--has--same--backgroundColor, β¦) in props.className, and VLT's
spacing between blocks depends on them. The block's own element keeps its BEM class
but not block. Before a site switches to block model 3, set
blockModel: 3 and a category on every Juizi block and the global
config.settings.blockModel = 3 in the same release; the className
modifiers are dropped under v3, so move them then. In tests,
jest.mock('../_shared/BlockWrapper') uses the manual mock in __mocks__/.
MobileCarousel β the swipeable row of items on phones (Embla), used by
the Content Row and Pricing tiers. The block renders its normal grid too and
its own CSS shows one or the other per screen size. Props: blockClass (the
block's class; the carousel's classes are {blockClass}__embla,
__embla-viewport, __embla-container, __embla-slide, __embla-dots,
__embla-dot), label (the region's accessible name), autoplay (never with
reduced motion), showDots, startIndex (the slide shown first), arrows ('above', 'middle' β one on each side, in columns of their own so the slide narrows instead of being covered β or 'below') and arrowStyle (a button style; empty is outlined in the text colour; mobile-carousel.css). Without arrows the markup is unchanged. Slides get equal heights.
NavArrowIcon β the left / right arrow used on the Carousel's and the mobile carousel's buttons (direction, className).
RichText β shows a richtext field's saved value (HTML, Slate nodes or
an HTML string) with a className; renders nothing for an empty value
(<p></p>, nodes without text). hasRichText(value) tells you whether
there's anything to show.
Hook-free wrapper β blocks whose view uses hooks (Embla) check for a
display style in an outer, hook-free component and only mount the inner one
once a style exists (WrappedEmblaCarousel, WrappedEmblaGallery).
| Module | What |
|---|---|
editMode.js |
isEditing(props): props.isEditMode (set by makeBlockEdit), else typeof props.onChangeBlock === 'function'. Never props.mode. |
links.js |
getHref(link) for every stored shape (object browser array, object, plain string); '' when there's no link. isExternalHref(href) and externalLinkProps(href). No window reads, so safe on the server. |
images.js |
getImageUrl, getListingImageUrl, resolveScales (size-aware scale picking), getPickedImageUrl(image, scale) |
choices.js |
getAlignmentChoices(t) (Left / Centre / Right, not icons), getPaddingChoices(t), getOptionalPaddingProperties(t) (optional paddingTop / paddingBottom fields, no default), paddingClasses(base, data) ({base}--pad-top-* / --pad-bottom-* for the sides chosen), getVerticalChoices(t), withSavedChoice(choices, saved, formatLabel, t) for fields that became named choices ("Custom (β¦)" for unusual saved values; values as strings, see above), msToSeconds(ms, t) |
i18n.js, messages.js |
translator(intl) gives schemas and choice lists their t(message, values); messages.js holds the text several blocks share. See Languages |
format.js |
formatDate(value, options, language), formatNumber(value, language): in the site's language (pass intl.locale), British English for English ("13 November 2024", "1,000"), so the server and every browser agree |
anchors.js |
slugify, blockAnchorId(heading, blockId, fallback) |
items.js |
withItemTitles(items, ...fields) names list items in the sidebar after their own heading (VLT's list widget shows item.title); ensureIds(items) |
skeleton.css |
Loading skeleton tiles (block-skeleton, block-skeleton__tile, --mixed) |
- First choice: a block starts with one choice (a display style or type,
field
displayMode, nevervariation) with no schema default. Until it's made, the sidebar shows only that field, the canvas showsBlockPlaceholderand visitors see nothing.isConfigured(data)on the definition also hides VLT's Styling tab until then. - Options in the sidebar, in the order the editor works through them, each switch directly above the field it controls; fields appear once they apply.
- Structure: each block is a
<section>, named by its heading througharia-labelledbywhen there is one. Block headings are<h2>(the Hero's can be the page's<h1>), item and slide headings<h3>. Every block should have a heading, even a short one. - Decorative backgrounds: background images, videos and overlays are
aria-hidden; meaning goes in the heading and text. - Links: external links open in a new tab with
rel="noopener noreferrer"(externalLinkProps); a card or item has one real link, stretched over the card with CSS where the whole card is clickable. - Reduced motion: autoplay, count-ups, moving strips, videos and hover zooms stop or don't run for visitors who ask for less motion.
- Languages: every string an editor or visitor can read is a translatable
message, and dates and numbers follow the site's language (
format.js; British English for English). See Languages. - Anchors: blocks with a heading get an id from it (
blockAnchorId), in the same format as before, so existing same-page links keep working.
Content from earlier Juizi block add-ons (customHero, buttonRow,
multiCard, iconLinkRow, EmblaCarousel, EmblaRatings, and plone.org's hero) is converted to the current
blocks as it loads. See COMPATIBILITY.md for what maps
where and what's only partly supported. The standalone EmblaRatings
testimonials become the Carousel's Reviews style.
Before adding one, check whether the job fits an existing block as a new
display style. A second block that does nearly the same thing leaves editors
choosing between two look-alikes in the chooser, and repeats everything the
first block already handles (scrolling, colours, empty states, translations).
Reviews were added to the Carousel this way instead of porting the old
ratings block; see EmblaCarousel/README.md, "Reviews", for how one style
gives the slide fields its own labels and hides the options that don't apply.
- Put it in
src/components/Blocks/<Name>/, with a README covering only what the block does (see the existing ones), linking back here for shared parts. - For colour pickers use
getBlockColorList('<id>')inside the schema function, andisColorDark()/getButtonClasses()/getColorTextStyle()in the view. Or setusesThemes: trueto get VLT's theme picker instead (only offered on sites with Volto Light Theme; plan for the block without it). - Add a definition to
src/blocks/index.tswithusesColors/usesThemesso it shows up in the dashboard tables. ItseditismakeBlockEdit(View, { β¦ })and its schema goes injuiziSchema. - Follow the conventions above: first choice,
BlockPlaceholder,BlockWrapper,isConfigured. See thevolto-block-standardsskill. - Add the block's classes to the lists in
juizi-common.scss: its two layers to$juizi-layers(which also makes its links follow its text colour), and its preheader, title, description (β¦) classes to their lists, instead of setting their sizes in the block's own stylesheet. - Buttons:
getButtonClasses()in the view, and the button base class in$block-btn-classesinbuttons.scss. - Text: put every string in the block's
messages.js, add its title and description tosrc/blocks/messages.js, runmake i18nand translate the new entries (see Languages).
- An operating system that runs all the requirements mentioned.
- uv
- nvm
- Node.js and pnpm 22
- Make
- Git
- Docker (optional)
-
Clone this repository, then change your working directory.
git clone git@github.com:juizi-com/juizi-blocks.git cd juizi-blocks -
Install this code base.
make install
-
Create a new Plone site on your first run. It is multilingual, with a page showing one of each block in every language (see Languages).
make backend-create-site
-
Start the backend at http://localhost:8080/.
make backend-start
-
In a new shell session, start the frontend at http://localhost:3000/.
make frontend-start
Voila! Your Plone site should be live and kicking! π The home page sends
you to /en; the block showcase is at /en/juizi-blocks, and the dashboard
under Site Setup β Juizi Blocks (log in first).
Deploy a local Docker Compose environment that includes the following.
- Docker images for Backend and Frontend πΌοΈ
- A stack with a Traefik router and a PostgreSQL database ποΈ
- Accessible at http://juizi-blocks.localhost π
Run the following commands in a shell session.
make stack-create-site
make stack-startAnd... you're all set! Your Plone site is up and running locally! π
This monorepo consists of the following distinct sections:
- backend: Houses the API and Plone installation, utilizing pip instead of buildout, and includes a policy package named juizi.blocks.
- frontend: Contains the React (Volto) package.
- devops: Encompasses Docker stack, Ansible playbooks, and cache settings.
- All necessary codebases to run the site are contained within the repository (excluding existing add-ons for Plone and React).
- Specific GitHub Workflows are triggered based on changes in each codebase (refer to .github/workflows).
- Simplifies the creation of Docker images for each codebase.
- Demonstrates Plone installation/setup without buildout.
One release has one version, kept in four places. Bump them together:
| File | Format | Shown in |
|---|---|---|
version.txt |
1.0.0b1 |
the repository's release |
backend/src/juizi/blocks/__init__.py (__version__) |
1.0.0b1 |
the backend package, Site Setup β Add-ons once installed |
frontend/packages/volto-juizi-blocks/package.json |
1.0.0-beta.1 |
Site Setup β Add-ons before it's installed, the dashboard's footer |
frontend/package.json |
1.0.0-beta.1 |
the development workspace |
npm writes a pre-release as 1.0.0-beta.1, Python as 1.0.0b1. A test on
each side (backend/tests/unit/test_versions.py,
frontend/packages/volto-juizi-blocks/src/versions.test.js) fails when they
disagree, naming the file that's out of step.
The backend's install profile has its own number (metadata.xml, 1002):
raise it only with an upgrade step, not for every release.
To check your code against quality standards, run the following shell command.
make checkTo format and rewrite the code base, ensuring it adheres to quality standards, run the following shell command.
make format| Section | Tool | Description | Configuration |
|---|---|---|---|
| backend | Ruff | Python code formatting, imports sorting | backend/pyproject.toml |
| backend | zpretty |
XML and ZCML formatting | -- |
| frontend | ESLint | Fixes most common frontend issues | frontend/.eslintrc.js |
| frontend | prettier | Format JS and Typescript code | frontend/.prettierrc |
| frontend | Stylelint | Format Styles (css, less, sass) | frontend/.stylelintrc |
Formatters can also be run within the backend or frontend folders.
juizi-blocks must build and work on a site that doesn't switch on Volto Light
Theme. Code checks hasVLT() (src/config/vlt.ts) instead of importing from
the theme; ESLint rejects imports from @kitconcept/volto-light-theme in the
add-on. To build as such a site would (CI does this too):
make build-without-vltIt uses frontend/volto.config.no-vlt.js, which switches on only
volto-juizi-blocks.
or lint:
make lint| Section | Tool | Description | Configuration |
|---|---|---|---|
| backend | Ruff | Checks code formatting, imports sorting | backend/pyproject.toml |
| backend | Pyroma | Checks Python package metadata | -- |
| backend | check-python-versions | Checks Python version information | -- |
| backend | zpretty |
Checks XML and ZCML formatting | -- |
| frontend | ESLint | Checks JS / Typescript lint | frontend/.eslintrc.js |
| frontend | prettier | Check JS / Typescript formatting | frontend/.prettierrc |
| frontend | Stylelint | Check Styles (css, less, sass) formatting | frontend/.stylelintrc |
Linters can be run individually within the backend or frontend folders.
The dashboard and every Juizi block are available in:
| Language | Code | Status |
|---|---|---|
| English | en |
Source language (British English) |
| French | fr |
Translated |
| Portuguese | pt |
Translated |
| Portuguese (Brazil) | pt_BR |
Translated |
| Spanish | es |
Translated |
| Afrikaans | af |
Translated |
| German | de |
Scaffolding only: falls back to English until translated |
That covers what editors see (the sidebar, the prompts on the canvas, the dashboard) and what visitors see or hear (default button text such as "Read more", the Redirect's countdown, the names screen readers announce for arrows and dots, dates and numbers).
These translations were produced by AI (Claude, Anthropic's AI assistant) and have not yet been reviewed by native speakers. They are tested for completeness and for technical correctness, not for tone or local usage. We welcome feedback and collaboration: if a wording is wrong, awkward or not what your region would say, please open an issue or send a pull request. Corrections from native speakers and translations into further languages are very welcome.
The blocks follow the language Volto renders the site in: the site's language
in Plone, with Volto told about it (for a single-language site, start Volto
with SITE_DEFAULT_LANGUAGE=fr, or set config.settings.defaultLanguage and
supportedLanguages in the project's config). The add-on doesn't set or
override the site's language.
Afrikaans isn't in Volto 18's own list of interface languages, so Volto would
render an Afrikaans site in English. The add-on adds it to that list
(EXTRA_LANGUAGES in src/config/settings.ts); as a result "Afrikaans" is
also offered in Volto's personal preferences. Volto's own interface (toolbar,
core blocks) has no Afrikaans translation yet and stays in English.
The dashboard's Editing language shows the Juizi blocks' editing interface in one of the languages above, whatever the page's language. It covers what a Juizi block shows while it's being edited: its sidebar (including the Volto widgets inside it), the hints and warnings on the canvas, and the block's preview. The Form block's editor is covered too, through the form extension. Visitors always get the page's language, and the rest of Volto's interface (toolbar, other blocks, the page's own fields) keeps following the page.
- The messages are loaded in the browser when a Juizi block is edited:
Volto's own for that language (the site's
locales/<lang>.json, when the site builds it) with the add-on's on top (src/locales/<lang>.json, generated from the.pofiles bypnpm i18n, so they're there on every site). - Not covered: the donation and membership sign-up blocks' panels added by juizi-blocks (those blocks build their own sidebars), and the dashboard itself, which follows Volto's interface language.
- Meant for building a site; set it back to "Same as the page" when the site launches, so editors work in their own language.
Not translated: the default colour and theme names the dashboard starts with (they are site data, renamed in the dashboard), text editors have already saved in a block, and the error messages the backend returns if a save is rejected (the dashboard checks the same rules first, in the editor's language).
This repository's own site runs in all six languages, so every translation can be seen in place:
| Language | Block showcase page |
|---|---|
| English | /en/juizi-blocks |
| French | /fr/juizi-blocks |
| Portuguese | /pt/juizi-blocks |
| Portuguese (Brazil) | /pt-br/juizi-blocks |
| Spanish | /es/juizi-blocks |
| Afrikaans | /af/juizi-blocks |
Each page holds a Hero, a Content Row, a Carousel, a second Carousel in its
Reviews style (with made-up reviews), a Gallery and a Callout, written in that
language; the pages are translations of one another, so the
language switcher moves between them. The Redirect block sends visitors away
from the page it is on, so it has a page of its own, β¦/juizi-blocks/redirect,
reached from the Callout; it sends visitors back to the showcase.
- Backend:
make create-sitesets this up on a new site (the example content profile,juizi.blocks:initial). For a site that already exists, runmake create-language-demoinbackend/; it is safe to run again. The code is inbackend/src/juizi/blocks/setuphandlers/language_demo.py, the words inlanguage_demo_texts.py. - Frontend:
frontend/volto.config.jsloads the add-on asvolto-juizi-blocks:languageDemo, which switches Volto to multilingual with these languages. Only this repository's site does that.
Installing the add-on on another site does none of this: neither package changes a site's languages.
Volto renders pages that aren't content (the dashboard, other control panels) in the language of the site root when they are loaded directly, which is English here. Reached from a page in another language without reloading, they stay in that language.
- Frontend:
frontend/packages/volto-juizi-blocks/locales/<code>/LC_MESSAGES/volto.po(one entry per message, with the English text in the#. Default:comment). - Backend:
backend/src/juizi/blocks/locales/<code>/LC_MESSAGES/juizi.blocks.po(the add-on's name and the registry field descriptions).
To correct a translation, edit the msgstr in the .po file. To add a
language, copy a language folder, change the Language headers and translate
(or leave msgstr "" to fall back to English), then add the code to
TRANSLATED in src/locales.test.js, which checks that every listed language
is complete, keeps every {placeholder} and is valid for react-intl.
src/locales/<code>.json (used by the Editing language setting) is
generated from the .po files by scripts/locales-json.js, which pnpm i18n runs; a test fails when it's out of date.
After adding or changing text in the code, regenerate the translation files:
make i18n- Every string an editor or visitor can read is a message: each block has a
messages.js(defineMessages, idsjuizi-<block>-<name>), and_shared/messages.jsholds what several blocks share. - Components use
useIntl(). Schemas and choice lists usetranslator(intl)from_shared/i18n.js:const t = translator(args.intl), thentitle: t(messages.heading). Withoutintl(tests) it returns English. - Block titles and descriptions (
src/blocks/messages.js) use the English text as the message id, because Volto's block chooser looks a block'stitleup that way. - Dates and numbers:
formatDate(value, options, intl.locale)andformatNumber(value, intl.locale). - Apostrophes in translations are typographic (β): a straight
'before{starts a quoted section in react-intl's message syntax.
- Karel Calitz (Juizi) [karel@juizi.com]
- Claude (Anthropic), AI coding assistant, via Claude Code
Generated using Cookieplone (1.1.0) and cookieplone-templates (8e49881) on 2026-09-29 07:51:29.370335. A special thanks to all contributors and supporters!