Skip to content

About

Juizi Blocks: a consolidated block set for Plone 6 and Volto, with a dashboard for switching blocks on or off and managing shared colours and themes.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Juizi Blocks πŸš€

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.

Built with Cookieplone Ruff CI

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).

Stack

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.

Installing on a site

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.

Requirements

  • Plone 6.1 or 6.2 (juizi.blocks requires Products.CMFPlone 6.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

1. Backend

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 = backend

and 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.

2. Frontend

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.

3. Install on the site

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.

Updating

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.

How the block set works

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.

Blocks

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)

Overlapping blocks

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.

Other add-ons' blocks

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 form blocks yet).

The dashboard (Site Setup β†’ Juizi Blocks)

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 restricted setting is its default. true means 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's restricted (CRA makes Lead Image available everywhere); its value is then the block's default.
  • Text (slate), Description, Image and Title are locked on (LOCKED_BLOCKS in settings/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 as block_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>-rgb and --<name>-foreground on :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. See Callout/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_version are 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.

Colours and buttons

src/config/colors.js

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

Image overlays: src/config/gradients.ts

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.

The built-in list

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)

Giving a site its own overlays

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.

  1. In the site's add-on, add src/config/gradients.ts with the full list, and install it from the add-on's index.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;
    }
  2. Keep none and gradient. "None" is what editors need for patterns; gradient is what every block without a saved overlay draws, so leaving it out takes it off the list while those blocks still use it.

  3. 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.

  4. 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.

  5. background is 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: 90deg strong on the left, 270deg on the right, 180deg at the foot, 0deg at the top, 135deg / 160deg diagonal.
    • Write colours as rgba() with an opacity (or transparent); 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>-rgb variable 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(...)'.
  6. 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).

  7. 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

Text colour follows the background

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).

Buttons: getButtonClasses and src/config/buttons.scss

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.

Shared styling (src/theme/juizi-common.scss)

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, …).

Tokens

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.

Padding scale

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

Two layers: width and full width

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-gutter side 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-width unless 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.

Links

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-layers sets --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-links and lose the underline; links in running text inside those go in $juizi-underlined-links and keep it. VLT's selector is (0,4,1), so these rules repeat the class to outrank it (the same trick buttons.scss uses).

Carousel-style controls (Carousel and Gallery)

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.scss covers <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.

Shared block toolkit (src/components/Blocks/_shared/, src/components/BlockEdit/)

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.

Edit: makeBlockEdit and juiziSchema

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's blockSchema: Volto merges blockSchema defaults into every saved block when it renders, which would change how older content looks. Schema functions read their data with schemaData(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". makeBlockEdit hands the form numbers as strings for any field with choices (display only; nothing is saved until the editor changes a field), and withSavedChoice returns string values. Views read these fields with parseInt, 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).

Components

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).

Helpers

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)

Conventions every block follows

  • First choice: a block starts with one choice (a display style or type, field displayMode, never variation) with no schema default. Until it's made, the sidebar shows only that field, the canvas shows BlockPlaceholder and 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 through aria-labelledby when 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.

Older content

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.

Adding a block

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.

  1. 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.
  2. For colour pickers use getBlockColorList('<id>') inside the schema function, and isColorDark() / getButtonClasses() / getColorTextStyle() in the view. Or set usesThemes: true to get VLT's theme picker instead (only offered on sites with Volto Light Theme; plan for the block without it).
  3. Add a definition to src/blocks/index.ts with usesColors / usesThemes so it shows up in the dashboard tables. Its edit is makeBlockEdit(View, { … }) and its schema goes in juiziSchema.
  4. Follow the conventions above: first choice, BlockPlaceholder, BlockWrapper, isConfigured. See the volto-block-standards skill.
  5. 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.
  6. Buttons: getButtonClasses() in the view, and the button base class in $block-btn-classes in buttons.scss.
  7. Text: put every string in the block's messages.js, add its title and description to src/blocks/messages.js, run make i18n and translate the new entries (see Languages).

Quick Start 🏁

Prerequisites βœ…

Installation πŸ”§

  1. Clone this repository, then change your working directory.

    git clone git@github.com:juizi-com/juizi-blocks.git
    cd juizi-blocks
  2. Install this code base.

    make install

Fire Up the Servers πŸ”₯

  1. 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
  2. Start the backend at http://localhost:8080/.

    make backend-start
  3. 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).

Local Stack Deployment πŸ“¦

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-start

And... you're all set! Your Plone site is up and running locally! πŸš€

Project structure πŸ—οΈ

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.

Why this structure? πŸ€”

  • 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.

Versions

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.

Code quality assurance 🧐

To check your code against quality standards, run the following shell command.

make check

Format the codebase

To 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.

Without Volto Light Theme

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-vlt

It uses frontend/volto.config.no-vlt.js, which switches on only volto-juizi-blocks.

Linting the codebase

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.

Languages 🌐

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.

Which language a site gets

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.

Editing in another language

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 .po files by pnpm 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).

The development site is multilingual

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-site sets this up on a new site (the example content profile, juizi.blocks:initial). For a site that already exists, run make create-language-demo in backend/; it is safe to run again. The code is in backend/src/juizi/blocks/setuphandlers/language_demo.py, the words in language_demo_texts.py.
  • Frontend: frontend/volto.config.js loads the add-on as volto-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.

Where the translations live

  • 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

Writing translatable blocks

  • Every string an editor or visitor can read is a message: each block has a messages.js (defineMessages, ids juizi-<block>-<name>), and _shared/messages.js holds what several blocks share.
  • Components use useIntl(). Schemas and choice lists use translator(intl) from _shared/i18n.js: const t = translator(args.intl), then title: t(messages.heading). Without intl (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's title up that way.
  • Dates and numbers: formatDate(value, options, intl.locale) and formatNumber(value, intl.locale).
  • Apostrophes in translations are typographic (’): a straight ' before { starts a quoted section in react-intl's message syntax.

Contributors

  • Karel Calitz (Juizi) [karel@juizi.com]
  • Claude (Anthropic), AI coding assistant, via Claude Code

Credits and acknowledgements πŸ™

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!

About

Juizi Blocks: a consolidated block set for Plone 6 and Volto, with a dashboard for switching blocks on or off and managing shared colours and themes.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages