*/ public static function roles(): array { return [ 'headline' => __('Headline', 'wconvert'), 'offer_amount' => __('Offer amount', 'wconvert'), 'body' => __('Body text', 'wconvert'), 'fine_print' => __('Fine print', 'wconvert'), /* translators: the small line above a heading, e.g. “LIMITED TIME”. */ 'eyebrow' => __('Line above the heading', 'wconvert'), /* translators: the short word on a coloured chip, e.g. “50% OFF”. */ 'badge' => __('Badge wording', 'wconvert'), /* translators: the words beside a star rating, e.g. “from 2,000 reviews”. */ 'rating_text' => __('Words beside the stars', 'wconvert'), 'next_label' => __('Next button label', 'wconvert'), 'back_label' => __('Back button label', 'wconvert'), 'skip_label' => __('Skip button label', 'wconvert'), 'close_label' => __('Close button label', 'wconvert'), 'cta_label' => __('Button label', 'wconvert'), 'consent_text' => __('Consent wording', 'wconvert'), 'success_headline' => __('Headline after they submit', 'wconvert'), 'success_body' => __('Body text after they submit', 'wconvert'), 'success_action' => __('Action after they submit', 'wconvert'), /* translators: the four words under a boxed countdown — days, hours, minutes and seconds. */ 'countdown_units' => __('Countdown unit words', 'wconvert'), /* translators: the discount code shown after a visitor submits, e.g. “WELCOME10”. */ 'code_value' => _x('Discount code', 'the code a visitor is given', 'wconvert'), /* * ================================================================ * THE SECOND ROLE A PLAYBOOK MAY NEVER FILL, AND FOR THE SAME * REASON AS THE FIRST. * ================================================================ * Thirteen of the sixteen designs in the reference set carry a * masthead — the shop's name, above the offer. A design had nowhere * to put one: unbound it is a role-less leaf, so the merchant's own * shop name is thrown away at the next design switch, and bound as * `headline` a Playbook writes the campaign headline into the logo. * * So it is a Role of its own and it is `authored`, exactly as * `code_value` is: a shop's name names ONE site and nowhere else, * so a Playbook filling it would ship somebody else's brand to * every install that used it. The design ships a placeholder and * the merchant types theirs in. */ /* translators: the shop or publication's own name, shown above the offer. */ 'wordmark' => __('Your name', 'wconvert'), 'email_label' => __('Email label', 'wconvert'), 'email_placeholder' => __('Email placeholder', 'wconvert'), 'name_label' => __('Name label', 'wconvert'), 'name_placeholder' => __('Name placeholder', 'wconvert'), 'phone_label' => __('Phone label', 'wconvert'), 'phone_placeholder' => __('Phone placeholder', 'wconvert'), 'interest_label' => __('Interest question', 'wconvert'), 'interest_placeholder' => __('Interest prompt', 'wconvert'), 'interest_options' => __('Interest choices', 'wconvert'), ]; } /** * What each leaf is called, for the slots that fill no Role — a `field`, * whose Roles are derived from what it captures rather than declared, and * an `image`, which holds no words at all. * * @return array */ public static function nodes(): array { return [ 'heading' => __('Heading', 'wconvert'), /* translators: a block — the small line above a heading, e.g. “LIMITED TIME”. Named for where it sits, because “eyebrow” is a typographer's word. */ 'eyebrow' => __('Overline', 'wconvert'), 'text' => __('Text', 'wconvert'), 'badge' => __('Badge', 'wconvert'), /* translators: a block — five stars, some of them filled. */ 'rating' => __('Star rating', 'wconvert'), 'image' => __('Image', 'wconvert'), 'icon' => __('Icon', 'wconvert'), /* translators: a block — a horizontal line separating two parts of a design. */ 'divider' => __('Divider', 'wconvert'), /* translators: a block — the time left, ticking down to the date the Optin stops running. */ 'countdown' => __('Countdown', 'wconvert'), /* translators: a block — one discount code, boxed, the same for every visitor. */ 'code' => _x('Discount code', 'a block that shows a code', 'wconvert'), 'field' => __('Field', 'wconvert'), 'question' => __('Question', 'wconvert'), 'products' => __('Product recommendations', 'wconvert'), 'button' => __('Button', 'wconvert'), 'followup' => __('Resource link', 'wconvert'), 'consent' => __('Consent checkbox', 'wconvert'), ]; } /** * What each layout is called, for the structure editor's rows and its Add * menu. * * **Needed only because arrangement became visible.** Until the structure * editor existed a layout was a shape the renderer read and nobody named: * the settings panel walks leaves and flattens them, so `stack` never * reached a merchant's eyes. A tree that lists what a design is MADE of * cannot flatten them — a merchant moving the email field is moving it * within the `row` — so each one acquires a name the way a Slot Role did. * * Named for what the merchant SEES rather than for what the renderer does. * `split` is *"Side by side"* and not "Split", because the word a merchant * needs is the arrangement they are looking at. * * @return array */ public static function layouts(): array { return [ 'stack' => __('Column', 'wconvert'), 'row' => __('Row', 'wconvert'), 'split' => __('Side by side', 'wconvert'), /* translators: a layout — as many equal columns as fit, wrapping onto the next line. */ 'grid' => __('Equal columns', 'wconvert'), /* * **Named for what it IS and not for what it does**, which is the * opposite of the choice `stack` made one line up. *Column* says * what a stack is FOR because every step is already one; a panel is * a THING on the screen — a box with its own colour — and the * merchant is going to point at it. */ /* translators: a layout — a box with its own background colour, holding other blocks. */ 'panel' => __('Colored box', 'wconvert'), /* * **Named for the picture and not for the spread**, which is the * same call `panel` made one line up: a merchant is going to point * at the photograph, and *"Spread box"* names the mechanism they * would have to be taught before the word meant anything. * * **Two words, parallel with *Coloured box*.** It was *"Picture * with text on it"*, which is what it DOES and belongs in the note * below — measured in a browser, twenty-three characters ran under * the ⋯ menu on every row of an 11rem tree, on a design that has * two of them. */ /* translators: a layout — a picture with blocks on it, the first at its top edge and the last at its bottom. */ 'media' => __('Image box', 'wconvert'), ]; } /** * What each layout is, in five words or fewer, under its name in the Add * menus (ADR 0139). * * **A note under a menu item is glanced at, not studied.** The sentence * with an example is {@see self::layoutHelp()}, in ⓘ; this is the line a * merchant reads on the way past. * * Its own map rather than an entry in {@see self::layouts()}, which is * pinned to exactly the manifest's layout NAMES and printed on every row. * * @return array */ public static function layoutNotes(): array { return [ /* translators: what the Column layout is for, in a few words. It groups blocks so a Row treats them as one item. */ 'stack' => __('Groups blocks as one item', 'wconvert'), /* translators: what the Row layout does, in a few words. */ 'row' => __('Blocks on one line', 'wconvert'), /* translators: what the Side by side layout does, in a few words. */ 'split' => __('Two panes: picture and form', 'wconvert'), /* translators: what the Equal columns layout does, in a few words. */ 'grid' => __('Three across; stacks on phones', 'wconvert'), /* translators: what the Coloured box layout does, in a few words. */ 'panel' => __('A box with its own colors', 'wconvert'), /* translators: what the Image box layout does, in a few words. */ 'media' => __('Blocks over a picture', 'wconvert'), ]; } /** * What each layout DOES, in one line with an example — the ⓘ beside its * menu item since ADR 0139, under the few words of {@see self::layoutNotes()}. * * ============================================================================ * A NAME IS NOT AN EXPLANATION, AND FOUR OF THESE NEEDED ONE. * ============================================================================ * *Column*, *Row*, *Side by side* and *Grid* are the four arrangements the * vocabulary offers, and the Add menu offered them as four bare words. Two * of them are genuinely hard to tell apart from their names — a Row lays * blocks along one line, a Side by side gives each pane its own stack — and * the merchant finds out which is which by adding one, looking at the * preview, and deleting it again. * * **The example is the half that teaches.** *"Blocks left to right on one * line"* is a definition; *"a field, then its button"* is a picture, and a * merchant recognises the thing they were trying to build. * * **One sentence each, in ⓘ rather than under the item.** At a menu's width * even these wrapped to two or three lines, so ten items were a wall of * prose; the menu shows {@see self::layoutNotes()} and this is the * explanation behind it. * * Its own map rather than an entry in {@see self::layouts()}, which is * pinned to exactly the manifest's layout NAMES — a sentence in there would * be a name, and the block tree prints those on every row. * * @return array */ public static function layoutHelp(): array { return [ /* * **Column says what it is FOR, not what it looks like.** Every step * is already a column, so *"one block under another"* described * something the merchant already had and made the menu item read as * a no-op — which it is, at the top level. Its real job is grouping, * and it is only offered where grouping does something. */ /* translators: what the Column layout is for. It groups blocks so a Row treats them as one item. */ 'stack' => __('Groups blocks into one item.', 'wconvert'), /* translators: what the Row layout does. */ 'row' => __('On one line — a field, then its button.', 'wconvert'), /* translators: what the Side by side layout does. */ 'split' => __('Two panes — a picture, then the form.', 'wconvert'), /* * **Named and explained for the WRAPPING**, which is the whole * difference from the `grid` that was deleted for not doing it. A * merchant reading "Equal columns" has to know it will not hand a * phone three 90px columns of prose. */ /* translators: what the Equal columns layout does. */ 'grid' => __('Three across, one per line on a phone.', 'wconvert'), /* * **The note has to say what makes it different from a Column**, * because the two hold their children identically and a merchant * reading two menu items called *Column* and *Coloured box* has one * question. The answer is the whole feature: this one has its own * colour, and everything inside it inherits. */ /* translators: what the Coloured box layout does. */ 'panel' => __('A box with its own colors, holding other blocks.', 'wconvert'), /* * **The note has to say what makes it different from a Coloured * box**, for the reason the line above has to say what makes that * one different from a Column: both draw a background picture, and * a merchant reading two menu items has one question. The answer is * where the blocks go — pushed to the far edges rather than * stacked at the top, which is what a masthead above a headline on * one photograph is. */ /* translators: what the Picture with text on it layout does. */ 'media' => __('A picture, with the first block at its top and the last at its bottom.', 'wconvert'), ]; } /** * What a layout's own setting is called, keyed `"{layout}.{param}"`. * * **A layout has settings and the editor never offered them.** `split` * declares `ratio` and the renderer reads it, and no control in the admin * reached it — so a Side by side was a fixed 50/50 forever and the manifest * described a capability nobody had. (`grid`'s `columns` was the same, and * it is one of the reasons that layout is gone rather than fixed.) * * Keyed by layout AND param because a param name is only meaningful under * its layout: `ratio` means nothing on its own. * * @return array */ public static function layoutParams(): array { return [ 'split.ratio' => __('How the space is divided', 'wconvert'), 'split.basis' => __('Minimum column width', 'wconvert'), 'split.narrow_layout' => __('Compact layout', 'wconvert'), 'grid.narrow_basis' => __('Phone minimum column width', 'wconvert'), 'grid.basis' => __('Minimum column width', 'wconvert'), /* * ================================================================ * FOUR ENTRIES FOR ONE CONTROL, BECAUSE THE KEY IS PER LAYOUT. * ================================================================ * `tokens` is the same bag on every layout and the words are the * same words, and it is still spelled four times: this map is keyed * `"{layout}.{param}"` because a param name means nothing on its * own, and {@see \WConvert\Tests\Unit\Template\TemplateLabelParityTest::testEveryLayoutParamIsNamed()} * walks every layout's `params` with no opt-out. A shared entry * would be a fifth spelling of "which layouts exist". * * **It has no `choices`, and that is what makes it a bag rather * than an enum.** The block inspector draws a radio group per param * that enumerates and skips one that does not, so these words head * the Style panel rather than an empty legend. */ /* translators: the colours, spacing and type this box sets for itself and everything inside it. */ 'stack.tokens' => __('Style for this box', 'wconvert'), /* translators: the colours, spacing and type this box sets for itself and everything inside it. */ 'row.tokens' => __('Style for this box', 'wconvert'), /* translators: the colours, spacing and type this box sets for itself and everything inside it. */ 'split.tokens' => __('Style for this box', 'wconvert'), /* translators: the colours, spacing and type this box sets for itself and everything inside it. */ 'grid.tokens' => __('Style for this box', 'wconvert'), /* translators: the colours, spacing and type this box sets for itself and everything inside it. */ 'panel.tokens' => __('Style for this box', 'wconvert'), /* translators: whether the coloured box has a rule above it, a border all round, or neither. */ 'panel.edges' => __('Border', 'wconvert'), /* translators: the optional ornaments a design draws on a coloured box, which a merchant can switch off. */ 'panel.decorations' => __('Decorations', 'wconvert'), 'panel.min' => __('Minimum height', 'wconvert'), /* translators: whether two circles are punched out of the box's top corners, like a torn ticket, so the page shows through. */ 'panel.notch_position' => __('Cutout position', 'wconvert'), 'panel.notch' => __('Punched corners', 'wconvert'), /* translators: the colours, spacing and type this box sets for itself and everything inside it. */ 'media.tokens' => __('Style for this box', 'wconvert'), /* translators: the optional ornaments a design draws on a coloured box, which a merchant can switch off. */ 'media.decorations' => __('Decorations', 'wconvert'), 'media.min' => __('Minimum height', 'wconvert'), /* * ================================================================ * THE SAME WORDS AT EVERY LAYOUT, AND THE SIXTH SPELLING OF THE * SAME LIST. * ================================================================ * Exactly the bargain `*.tokens` one section up makes and for the * same reason: a param name means nothing on its own, the parity * test walks every layout's `params` with no opt-out, and a shared * entry would be another spelling of *which layouts exist*. * * **The width is not in the words**, and that is deliberate. It is * `manifest.narrow` — one number, read by the renderer's container * query — and putting it here would be a second place it is * written, in a string a translator can edit. The panel that draws * this control says the number, from the manifest. */ /* translators: a second set of colours, spacing and type for when the design is narrow — on a phone, or in a sidebar. */ 'stack.narrow' => __('Style when it is narrow', 'wconvert'), /* translators: a second set of colours, spacing and type for when the design is narrow — on a phone, or in a sidebar. */ 'row.narrow' => __('Style when it is narrow', 'wconvert'), /* translators: a second set of colours, spacing and type for when the design is narrow — on a phone, or in a sidebar. */ 'split.narrow' => __('Style when it is narrow', 'wconvert'), /* translators: a second set of colours, spacing and type for when the design is narrow — on a phone, or in a sidebar. */ 'grid.narrow' => __('Style when it is narrow', 'wconvert'), /* translators: a second set of colours, spacing and type for when the design is narrow — on a phone, or in a sidebar. */ 'panel.narrow' => __('Style when it is narrow', 'wconvert'), /* translators: a second set of colours, spacing and type for when the design is narrow — on a phone, or in a sidebar. */ 'media.narrow' => __('Style when it is narrow', 'wconvert'), ]; } /** * What each OFFERED value of a layout's setting is called, keyed * `"{layout}.{param}.{value}"`. * * **Named for what a merchant SEES, not for the number.** `ratio` is the * first pane's share of the line, so `0.35` is a narrow first pane — and * *"0.35"* is not a thing to put in front of anybody. The words are * directional the way `align`'s are, and the translator resolves them for * their own reading direction; the panes themselves are `start` and `end` * and never `left` and `right` (ADR 0009). * * **These are what the panel OFFERS and never what is allowed.** The * renderer takes any fraction, so a design shipping `0.4` keeps it — the * same bargain token `choices` make. * * @return array */ public static function layoutParamValues(): array { return [ /* translators: a side-by-side split. The LOGICAL first pane is narrower — it reads “right” in a right-to-left locale. */ 'split.ratio.0.35' => __('Narrow left', 'wconvert'), /* translators: a side-by-side split where both panes are the same width. */ 'split.ratio.0.5' => __('Even', 'wconvert'), /* translators: a side-by-side split. The LOGICAL second pane is narrower — it reads “left” in a right-to-left locale. */ 'split.ratio.0.65' => __('Narrow right', 'wconvert'), 'split.narrow_layout.wrap' => __('Wrap naturally', 'wconvert'), 'split.narrow_layout.stack' => __('Stack first column on top', 'wconvert'), 'split.narrow_layout.reverse' => __('Stack second column on top', 'wconvert'), 'grid.narrow_basis.4rem' => __('Small (4rem)', 'wconvert'), 'grid.narrow_basis.6rem' => __('Compact (6rem)', 'wconvert'), 'grid.narrow_basis.8rem' => __('Roomy (8rem)', 'wconvert'), 'grid.narrow_basis.12rem' => __('Wide (12rem)', 'wconvert'), 'grid.basis.8rem' => __('Small (8rem)', 'wconvert'), 'grid.basis.12rem' => __('Compact (12rem)', 'wconvert'), 'grid.basis.16rem' => __('Roomy (16rem)', 'wconvert'), 'grid.basis.20rem' => __('Wide (20rem)', 'wconvert'), 'split.basis.12rem' => __('Compact (12rem)', 'wconvert'), 'split.basis.16rem' => __('Roomy (16rem)', 'wconvert'), 'split.basis.20rem' => __('Wide (20rem)', 'wconvert'), /* * ================================================================ * `block-start` IS LOGICAL, AND THE ENGLISH WORD IS DIRECTIONAL. * ================================================================ * The same bargain `align`'s three values make one section down. * The renderer draws `border-block-start`, which is the top of the * box in every writing mode this plugin has ever been read in and * is the INLINE start of it in a vertical one — so the word here is * the one an English reader needs, and the translator resolves it * for their own direction. The vocabulary itself never names a * physical side (ADR 0009), which is why the stored value is * `block-start` and not `top`. */ /* translators: a coloured box with no border at all. */ 'panel.edges.none' => _x('None', 'border of a box', 'wconvert'), /* translators: a border treatment — a thick coloured rule along the block-start edge. In a vertical writing mode this is not the top of the screen. */ 'panel.edges.block-start' => __('A rule above', 'wconvert'), /* translators: a border treatment — a thin line all the way round the box. */ 'panel.edges.all' => __('All round', 'wconvert'), /* * **Named for the JOB and not for the measurement.** `16rem` is not * a thing to put in front of anybody, and the three that are not * zero are the three heights a picture pane is ever asked for. The * renderer takes any length, so a design shipping `18rem` keeps it * and the control shows nothing checked — the same bargain every * other `choices` list makes. */ /* translators: a minimum height — none, so the box is as tall as what is in it. */ 'panel.min.0' => __('As tall as its contents', 'wconvert'), /* translators: a minimum height for a box holding a picture. */ 'panel.min.10rem' => _x('Short', 'minimum height', 'wconvert'), /* translators: a minimum height for a box holding a picture. */ 'panel.min.16rem' => _x('Medium', 'minimum height', 'wconvert'), /* translators: a minimum height for a box holding a picture. */ 'panel.min.24rem' => _x('Tall', 'minimum height', 'wconvert'), /* * **Two words rather than a tick box**, because the param is on * the same panel as `edges` and `min` and a lone checkbox among * three radio groups reads as a different KIND of setting. The * words say what is drawn, not what the key is called: a merchant * choosing between *Square* and *Punched* is choosing a shape they * can see in the preview. */ /* translators: the coloured box has ordinary square corners. */ 'panel.notch_position.top' => __('Top corners', 'wconvert'), 'panel.notch_position.sides' => __('Side centres', 'wconvert'), 'panel.notch.false' => _x('Square', 'corners of a box', 'wconvert'), /* translators: two circles are punched out of the box's top corners, like a torn ticket, so the page shows through. */ 'panel.notch.true' => __('Punched, like a ticket', 'wconvert'), /* translators: a minimum height — none, so the box is as tall as what is in it. */ 'media.min.0' => __('As tall as its contents', 'wconvert'), /* translators: a minimum height for a picture. */ 'media.min.10rem' => _x('Short', 'minimum height', 'wconvert'), /* translators: a minimum height for a picture. */ 'media.min.16rem' => _x('Medium', 'minimum height', 'wconvert'), /* translators: a minimum height for a picture. */ 'media.min.24rem' => _x('Tall', 'minimum height', 'wconvert'), ]; } /** * What a LEAF's own setting is called, keyed `"{node}.{param}"`. * * ============================================================================ * THREE PARAMS THE RENDERER READS AND NO CONTROL EVER REACHED. * ============================================================================ * `heading.level` decides whether a headline is the Optin's `h2` or an `h3` * under it, `image.fit` decides whether a picture is cropped or letterboxed, * and `field.required` is read by {@see \WConvert\Lead\CaptureForm}, which * refuses a submission that left one empty. All three are declared in the * manifest, all three are honoured at both ends, and until now the only way * to set any of them was to author a [[Template]] by hand — which is exactly * the state `split.ratio` was in one level up. * * ============================================================================ * KEYED ON `choices`, WHICH IS WHAT SAYS "THIS PARAM HAS A CONTROL". * ============================================================================ * A leaf's `params` list is not the surface: `hidden` is drawn by the * inspector's *Show this* switch and `name` and `action` are drawn by the ⇄ * menu, each of which already has words of its own. A name for those here * would be a second word for one control, which is the failure * {@see self::keys()} is pinned against one section over. * * So the manifest's per-node `choices` section is the declaration, exactly as * a layout's is, and {@see \WConvert\Tests\Unit\Template\TemplateLabelParityTest} * holds these two maps to it in both directions. * * @return array */ public static function nodeParams(): array { return [ 'code.appearance' => __('Code style', 'wconvert'), 'divider.pattern' => __('Line pattern', 'wconvert'), /* * **`level` and `size` are two questions and the label for `level` * used to answer both.** It read *"Not its size, which is a token"*, * and that was true while a design had one heading size for every * heading in it. A design with a display number and a subtitle needs * a small `h2` and a large `h3`, which is the pair no token reaches. */ /* translators: a heading's rank inside the Optin — whether it is the main heading or one under it. This is the document outline, not how big it is drawn. */ 'button.icon' => __('Button icon', 'wconvert'), 'image.motion' => __('Picture motion', 'wconvert'), 'button.action' => __('Button action', 'wconvert'), 'heading.level' => __('Heading level', 'wconvert'), /* translators: how big this heading is drawn, as a step up or down from the design's own heading size. */ 'heading.size' => _x('Size', 'setting: how big text is drawn', 'wconvert'), /* translators: how big this paragraph is drawn, as a step up or down from the design's own text size. */ 'text.size' => _x('Size', 'setting: how big text is drawn', 'wconvert'), /* translators: how a picture fills the space it is given. */ 'image.fit' => __('Picture fit', 'wconvert'), 'code.layout' => __('Code layout', 'wconvert'), 'code.copy' => __('Copy button', 'wconvert'), /* translators: how a countdown is drawn — as one line of digits, or as a box per unit. */ 'countdown.layout' => __('Countdown style', 'wconvert'), /* translators: whether a visitor must fill a form field in before they can submit. */ 'field.required' => _x('Required', 'a visitor must answer it', 'wconvert'), 'field.phone_country' => __('Starting country', 'wconvert'), 'field.phone_dropdown' => __('Show a country list', 'wconvert'), 'products.action' => __('Product action', 'wconvert'), 'products.source' => __('Recommendation source', 'wconvert'), 'question.answer_type' => __('Answer type', 'wconvert'), 'question.required' => __('Required answer', 'wconvert'), /* translators: how many of the five stars are filled in. */ 'rating.value' => __('How many stars', 'wconvert'), /* translators: which of the six pictures an Icon block draws. */ 'icon.name' => __('Which picture', 'wconvert'), /* translators: whether a picture is a rectangle or a circle. */ 'image.shape' => __('Picture shape', 'wconvert'), /* translators: whether a badge sits in the flow of the design or is pinned to its corner. */ 'badge.place' => __('Badge position', 'wconvert'), ]; } /** * What each OFFERED value of a leaf's setting is called, keyed * `"{node}.{param}.{value}"`. * * **Named for what the merchant sees, never for the stored value.** `1` and * `2` are heading ranks, `cover` and `contain` are CSS keywords, and `true` * and `false` are not words anybody writes on a form — the same reason * `0.35` needed *"Narrow left"*. * * The values are the manifest's spellings and are compared as VALUES rather * than as strings on the way in, so `level` stores the number `1` while the * manifest offers `"1"` — see `builder/panel.ts`'s `valueOfChoice`. * * @return array */ public static function nodeParamValues(): array { return [ 'code.appearance.boxed' => __('Boxed', 'wconvert'), 'code.appearance.plain' => __('Plain text', 'wconvert'), 'divider.pattern.solid' => __('Solid', 'wconvert'), 'divider.pattern.dashed' => __('Dashed', 'wconvert'), /* translators: a heading rank. This heading is the Optin's own main heading. */ 'button.icon.none' => __('None', 'wconvert'), 'button.icon.arrow' => __('Arrow', 'wconvert'), 'image.motion.none' => __('None', 'wconvert'), 'image.motion.arrive' => __('Appear once', 'wconvert'), 'image.motion.breathe' => __('Gentle scale', 'wconvert'), 'image.motion.drift' => __('Gentle drift', 'wconvert'), 'button.action.submit' => __('Submit', 'wconvert'), 'button.action.next' => __('Next', 'wconvert'), 'button.action.back' => __('Back', 'wconvert'), 'button.action.skip' => __('Skip optional signup', 'wconvert'), 'button.action.close' => __('Close', 'wconvert'), 'button.action.link' => __('Open link', 'wconvert'), 'products.action.link' => __('Open product page', 'wconvert'), 'products.action.add_to_cart' => __('Add to cart', 'wconvert'), 'products.source.selected' => __('Choose products', 'wconvert'), 'products.source.cross_sells' => __('Use WooCommerce cross-sells', 'wconvert'), 'question.answer_type.single' => __('Choose one', 'wconvert'), 'question.answer_type.multi' => __('Choose several', 'wconvert'), 'question.answer_type.text' => __('Short answer', 'wconvert'), 'question.required.false' => __('Optional', 'wconvert'), 'question.required.true' => _x('Required', 'a visitor must answer it', 'wconvert'), 'code.layout.stack' => __('Stacked', 'wconvert'), 'code.layout.inline' => __('Code and button together', 'wconvert'), 'code.copy.false' => __('Code only', 'wconvert'), 'code.copy.true' => __('Code and copy button', 'wconvert'), /* translators: a countdown drawn as one line of digits, such as 2d 14:05:09. */ 'countdown.layout.line' => __('One line', 'wconvert'), /* translators: a countdown drawn as a box per unit — days, hours, minutes and seconds — each with its word under it. */ 'countdown.layout.boxes' => __('Boxes', 'wconvert'), 'heading.level.1' => __('Main heading', 'wconvert'), /* translators: a heading rank. This heading sits under the main one. */ 'heading.level.2' => __('Sub-heading', 'wconvert'), /* * ================================================================ * ONE SET OF WORDS, SPELLED TWICE, FOR THE SAME REASON `tokens` IS. * ================================================================ * The key is `"{node}.{param}.{value}"` because a param name means * nothing on its own, and the parity test walks every node's * `choices` with no opt-out. What the duplication buys is the day a * heading's steps and a paragraph's steps stop being the same list. * * **Named for the SIZE and not for the step.** `2xl` is a token in a * type scale and not a thing to put in front of anybody; a merchant * picking how big a number is drawn is choosing between *Huge* and * *Large*. `Medium` is the design's own size and is what a block * gets when nothing is chosen, which is why it is named at all * rather than left as an empty chip (ADR 0135: the same words as * the Look's type sizes). */ /* translators: a type size step — the largest step — a display number, not a sentence. */ 'heading.size.3xl' => __('Huge', 'wconvert'), /* translators: a type size step — a step for a short display line. */ 'heading.size.2xl' => __('Extra large', 'wconvert'), /* translators: a type size step — one step up from the design’s own size. */ 'heading.size.xl' => __('Large', 'wconvert'), /* translators: a type size step — the design’s own size, which is what a block gets when nothing is chosen. */ 'heading.size.m' => _x('Medium', 'text size step', 'wconvert'), /* translators: a type size step — one step down from the design’s own size. */ 'heading.size.s' => __('Small', 'wconvert'), /* translators: a type size step — the smallest step — fine print. */ 'heading.size.xs' => __('Very small', 'wconvert'), /* translators: a type size step — the largest step — a display number, not a sentence. */ 'text.size.3xl' => __('Huge', 'wconvert'), /* translators: a type size step — a step for a short display line. */ 'text.size.2xl' => __('Extra large', 'wconvert'), /* translators: a type size step — one step up from the design’s own size. */ 'text.size.xl' => __('Large', 'wconvert'), /* translators: a type size step — the design’s own size, which is what a block gets when nothing is chosen. */ 'text.size.m' => _x('Medium', 'text size step', 'wconvert'), /* translators: a type size step — one step down from the design’s own size. */ 'text.size.s' => __('Small', 'wconvert'), /* translators: a type size step — the smallest step — fine print. */ 'text.size.xs' => __('Very small', 'wconvert'), /* translators: a picture is scaled up until it fills the space, and the overflow is cropped away. */ 'image.fit.cover' => __('Fill the space, cropping', 'wconvert'), /* translators: a picture is scaled down until all of it fits, leaving space around it. */ 'image.fit.contain' => __('Fit the whole picture in', 'wconvert'), /* translators: a picture keeps the design's own corner rounding. */ 'image.shape.rect' => __('Rectangle', 'wconvert'), /* translators: a picture is cropped to a circle — the shape a portrait beside a quote wants. */ 'image.shape.circle' => __('Circle', 'wconvert'), /* translators: a badge sits in the flow of the design, above or beside the other blocks. */ 'badge.place.inline' => __('In the design', 'wconvert'), /* translators: a badge is pinned over the top corner of the panel, like a price flash. */ 'badge.place.corner' => __('Pinned to the corner', 'wconvert'), /* translators: a form field a visitor cannot leave empty. */ 'field.required.true' => _x('Required', 'a visitor must answer it', 'wconvert'), 'field.phone_country.site' => __('Use site setting', 'wconvert'), 'field.phone_dropdown.true' => __('Show country list', 'wconvert'), 'field.phone_dropdown.false' => __('Hide country list', 'wconvert'), /* translators: a form field a visitor may leave empty. */ 'field.required.false' => __('Optional', 'wconvert'), /* * **Out of five, always, which is why the words say so.** Four * stars on their own read as a four-star scale rather than as four * out of five, and the second is the claim the design is making. * Half stars are not offered: they need a clip path and buy a * design nothing. */ /* translators: a star rating. %s is not used; three of five stars are filled. */ 'rating.value.3' => __('Three of five', 'wconvert'), /* translators: a star rating — four of five stars are filled. */ 'rating.value.4' => __('Four of five', 'wconvert'), /* translators: a star rating — all five stars are filled. */ 'rating.value.5' => __('Five of five', 'wconvert'), /* * **Named for what a merchant would USE each one for**, not for the * shape. "Tick" is a description of a picture; "Tick — a benefit" * is the sentence that gets the right icon picked for a benefit * list. Six is the closed set, because an open icon slot is a * markup slot wearing a hat (`renderer/src/types.ts`). */ /* translators: an icon — a tick, for an item in a list of benefits. */ 'icon.name.check' => __('Tick', 'wconvert'), /* translators: an icon — a star, for a review or a rating. */ 'icon.name.star' => __('Star', 'wconvert'), /* translators: an icon — a lightning bolt, for speed or an instant delivery. */ 'icon.name.bolt' => __('Lightning', 'wconvert'), /* translators: an icon — a wrapped gift, for an offer or a free item. */ 'icon.name.gift' => __('Gift', 'wconvert'), /* translators: an icon — a clock, for a deadline. */ 'icon.name.clock' => __('Clock', 'wconvert'), /* translators: an icon — a delivery van, for shipping. */ 'icon.name.truck' => __('Delivery van', 'wconvert'), ]; } /** * What each field kind captures, in the merchant's words. * * @return array */ public static function fields(): array { return [ 'email' => __('Email address', 'wconvert'), 'name' => __('Name', 'wconvert'), 'phone' => __('Phone number', 'wconvert'), 'interest' => __('Interest (choice)', 'wconvert'), ]; } /** * The example wording a field of each kind ships with. * * ============================================================================ * IT IS A DEFAULT TO COMPARE AGAINST, NOT ONLY ONE TO WRITE. * ============================================================================ * The ⇄ control changes what a field captures, and an email field that * becomes a phone field must not keep `you@example.com` in front of the * visitor. So the new kind's example is written in — but only where the old * kind's was still there, which is the same comparison * {@see MerchantsOwn} makes across a Template switch: different from what * was shipped means the merchant's, and it stays. * * A design shipping its own wording — `stacked-signup` says * *"+44 7700 900000"* — therefore reads as the merchant's and is kept. That * is the safe direction, chosen rather than tolerated: the two are * indistinguishable from here, and silently overwriting words somebody * wrote is the failure that costs more. * * Separate from {@see self::keys()}, whose `placeholder` entry names the * CONTROL rather than saying what goes in it, and separate from * {@see self::fields()}, which is the label rather than the example. * * @return array */ public static function placeholders(): array { return [ 'email' => __('you@example.com', 'wconvert'), 'name' => __('Your name', 'wconvert'), 'phone' => __('+44 7700 900000', 'wconvert'), 'interest' => __('Choose an option', 'wconvert'), ]; } /** * What each editable key on a leaf is called. * * Keyed by the key alone rather than by `type.key`, because these mean the * same thing wherever they appear: a `label` is what is written beside the * control, on a field and on a button alike. * * @return array */ public static function keys(): array { return [ 'copy_label' => __('Copy button label', 'wconvert'), 'copied_label' => __('Message when copied', 'wconvert'), 'copy_failed_label' => __('Message when copying is unavailable', 'wconvert'), 'apply_label' => __('Code link text', 'wconvert'), 'apply_href' => __('Where the code link goes', 'wconvert'), /* translators: the word under the days box of a countdown. */ 'days_label' => __('Word for days', 'wconvert'), /* translators: the word under the hours box of a countdown. */ 'hours_label' => __('Word for hours', 'wconvert'), /* translators: the word under the minutes box of a countdown. */ 'minutes_label' => __('Word for minutes', 'wconvert'), /* translators: the word under the seconds box of a countdown. */ 'seconds_label' => __('Word for seconds', 'wconvert'), 'text' => __('Text', 'wconvert'), 'link' => __('Link', 'wconvert'), /* translators: the words inside a sentence that are set in bold, where the sentence carries %b. */ 'emphasis' => __('Words in bold', 'wconvert'), /* translators: plain words filling %i in a sentence. */ 'italic' => __('Words in italic', 'wconvert'), 'label' => __('Label', 'wconvert'), 'help' => __('Help text', 'wconvert'), 'placeholder' => __('Placeholder', 'wconvert'), 'src' => __('Image address', 'wconvert'), 'alt' => __('Alt text', 'wconvert'), 'href' => __('Where the button goes', 'wconvert'), 'options' => __('Choices', 'wconvert'), ]; } /** * What each PARAM VALUE a merchant may choose is called. * * ============================================================================ * ITS OWN MAP, BECAUSE {@see self::keys()} MUST STAY EXACTLY THE CONTENT KEYS. * ============================================================================ * `keys()` is asserted to be the union of every leaf's `content` list and * nothing beyond it — a label for `action` in there would be a control the * editor must not offer as words. But a `button`'s `action` is now * CHOOSABLE: the ⇄ control changes what a block is, and *"submit"* is not a * sentence to put in front of a merchant. * * So the two vocabularies stay apart. A field's kinds are already named by * {@see self::fields()}; what is left is the button's two, and they are * anchored to {@see ConvertingAct::action()} so a third act could not * arrive unnamed. * * Named for what the button DOES rather than for the value: `link` is the * node's word for a CTA that navigates, and a merchant choosing between * "link" and "submit" is being asked to know the vocabulary. * * @return array */ public static function params(): array { return [ 'submit' => __('Sends the form', 'wconvert'), 'link' => __('Goes somewhere else', 'wconvert'), 'next' => __('Goes to the next screen', 'wconvert'), 'back' => __('Returns to the previous screen', 'wconvert'), 'skip' => __('Skips optional signup', 'wconvert'), 'close' => __('Closes the popup', 'wconvert'), ]; } /** * What each token is called. * * @return array */ public static function tokens(): array { return [ 'bg' => __('Background', 'wconvert'), 'fg' => __('Text', 'wconvert'), 'muted' => __('Lighter text', 'wconvert'), 'accent' => __('Button color', 'wconvert'), 'accent-fg' => __('Button text', 'wconvert'), 'border' => __('Borders', 'wconvert'), /* translators: the background colour of a text box a visitor types into. Separate from the design's own background, so a form on a dark panel stays readable. */ 'placeholder' => __('Field example text', 'wconvert'), 'input-bg' => __('Field background', 'wconvert'), 'font' => __('Font', 'wconvert'), /* translators: a second typeface used only for headings, where the design wants one different from its body text. */ 'heading-font' => __('Heading font', 'wconvert'), 'heading-size' => __('Heading size', 'wconvert'), 'heading-weight' => __('Heading weight', 'wconvert'), 'heading-leading' => __('Heading line spacing', 'wconvert'), 'heading-style' => __('Heading style', 'wconvert'), 'image-height' => __('Image height', 'wconvert'), 'bg-size' => __('Background image size', 'wconvert'), 'countdown-size' => __('Countdown number size', 'wconvert'), /* translators: the space between letters in a heading. Typographers call it tracking; "letter spacing" is the phrase a merchant knows. */ 'tracking' => __('Heading letter spacing', 'wconvert'), 'text-weight' => __('Text weight', 'wconvert'), 'text-size' => __('Text size', 'wconvert'), /* translators: the space between lines of text. Typographers call it leading. */ 'leading' => __('Line spacing', 'wconvert'), 'radius' => __('Corner rounding', 'wconvert'), 'pad' => __('Inner spacing', 'wconvert'), 'gap' => __('Gap', 'wconvert'), 'width' => __('Width', 'wconvert'), 'align' => __('Alignment', 'wconvert'), /* translators: a picture behind the whole design. The merchant gives its web address. */ 'bg-image' => __('Background picture', 'wconvert'), 'image-position' => __('Picture focus', 'wconvert'), /* translators: a translucent colour laid over the background picture, so text on top of it stays readable. */ 'overlay' => __('Wash over the picture', 'wconvert'), /* translators: the soft shadow under the whole design, which lifts it off the page behind it. */ 'shadow' => __('Shadow', 'wconvert'), /* translators: how long the design takes to animate — its entry, and its button on hover. */ 'motion' => __('Animation speed', 'wconvert'), 'backdrop' => __('Backdrop', 'wconvert'), /* translators: the colour of the × that closes a popup. */ 'close-fg' => __('Close button icon', 'wconvert'), /* translators: the colour of the circle or square behind the × that closes a popup. */ 'close-bg' => __('Close button background', 'wconvert'), /* translators: the colour of the outline around the button that closes a popup. */ 'close-border' => __('Close button outline', 'wconvert'), /* translators: the shape of the button that closes a popup — round, rounded square or square. */ 'close-inset' => __('Close button inset', 'wconvert'), 'close-radius' => __('Close button shape', 'wconvert'), /* translators: the colour of the message shown when a visitor's entry is refused, such as an invalid email. */ 'error-fg' => __('Error text', 'wconvert'), /* translators: the colour of the box behind the message shown when a visitor's entry is refused. */ 'error-bg' => __('Error background', 'wconvert'), ]; } /** * What each OFFERED TOKEN VALUE is called, keyed `"{token}.{value}"`. * * ============================================================================ * `choices` IS WHAT THE PANEL OFFERS. IT IS NEVER WHAT IS ALLOWED. * ============================================================================ * Token *names* are checked and their *values are not* — that is what keeps * `clamp(20rem, 50vw, 30rem)` expressible for `width` and an asymmetric * corner expressible for `radius`, and it is deliberate (ADR 0010). So * {@see TemplateVocabulary} does not read `choices` and never will: these * words exist because a merchant cannot be asked to type `start` into a text * box, not because `start` is the only thing `align` may hold. * * **Keyed by the value rather than by an id**, because the value IS the * identity: `align` holds the CSS keyword the renderer's stylesheet reads, * and a font token holds the stack itself. An id would be a second spelling * of a thing the manifest already spells once, and the parity test below * would have nothing to compare. * * **The font stacks are byte-identical to the manifest's**, apostrophes and * all, and that is the one genuinely fragile join in this file. It is a red * build rather than an untranslated chip: * {@see \WConvert\Tests\Unit\Template\TemplateLabelParityTest::testEveryTokenChoiceIsNamed()} * fails in both directions. * * @return array */ public static function tokenValues(): array { return [ 'image-position.left top' => __('Top left', 'wconvert'), 'image-position.center top' => __('Top center', 'wconvert'), 'image-position.right top' => __('Top right', 'wconvert'), 'image-position.left center' => __('Center left', 'wconvert'), 'image-position.center' => __('Center', 'wconvert'), 'image-position.right center' => __('Center right', 'wconvert'), 'image-position.left bottom' => __('Bottom left', 'wconvert'), 'image-position.center bottom' => __('Bottom center', 'wconvert'), 'image-position.right bottom' => __('Bottom right', 'wconvert'), /* * ==================================================================== * THESE THREE ARE LOGICAL, AND THE ENGLISH WORDS ARE DIRECTIONAL. * ==================================================================== * The renderer sets `text-align: var(--wc-align, start)`, and `start` * and `end` are LOGICAL — under `fa_IR` a design set to `start` reads * from the right. So the English word is the one an English reader * needs and the translator resolves it for their own direction; that * is what the comments below are for. The editor’s logical alignment * icons mirror automatically; these words label their tooltips. */ /* translators: a text alignment. This is the LOGICAL start of the line, so it reads “Right” in a right-to-left locale. */ 'align.start' => __('Start', 'wconvert'), /* translators: a text alignment. */ 'align.center' => __('Center', 'wconvert'), /* translators: a text alignment. This is the LOGICAL end of the line, so it reads “Left” in a right-to-left locale. */ 'align.end' => __('End', 'wconvert'), /* * **Every stack here is system-available**, which is a constraint * rather than a preference: ADR 0010 hands the renderer the whole * stylesheet and there is no web font to load, so a stack naming a * face nobody has renders as a fallback the merchant did not pick — * and they would have chosen it by looking at it, because the panel * sets each chip in its own face. * * Named for the REGISTER rather than for a typeface: "Helvetica" on * a Windows machine is Arial, and a label that names one face while * showing another is a label that lies on most installs. */ /* translators: a font choice — the operating system's own interface typeface. */ "font.system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" => __('System', 'wconvert'), /* translators: a font choice — a typeface with no serifs. */ "font.'Helvetica Neue', Helvetica, Arial, sans-serif" => __('Sans serif', 'wconvert'), /* translators: a font choice — a typeface with serifs. */ 'font.Georgia, \'Times New Roman\', Times, serif' => __('Serif', 'wconvert'), /* translators: a font choice — a typeface whose letters are all one width. */ 'font.ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' => __('Monospace', 'wconvert'), /* * **The same four stacks, spelled a second time, and the duplication * is forced rather than lazy.** The key is `"{token}.{value}"` * because the value IS the identity, so a shared entry would need a * key that names no token — and the parity test walks `choices` * per token with no opt-out. What that buys is the thing it costs: * the day the heading list and the body list stop being the same * list, nothing here has to be untangled. */ /* translators: a font choice — the operating system's own interface typeface. */ "heading-font.system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" => __('System', 'wconvert'), /* translators: a font choice — a typeface with no serifs. */ "heading-font.'Helvetica Neue', Helvetica, Arial, sans-serif" => __('Sans serif', 'wconvert'), /* translators: a font choice — a typeface with serifs. */ 'heading-font.Georgia, \'Times New Roman\', Times, serif' => __('Serif', 'wconvert'), /* translators: a font choice — a typeface whose letters are all one width. */ 'heading-font.ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' => __('Monospace', 'wconvert'), /* * **Named for the weight a reader sees, not for the number.** `600` * is not a thing to put in front of anybody, and the CSS numbers * are only four of the nine because a system stack has nothing * between them to draw — offering `500` on a face that has no * medium is offering a chip that changes nothing. */ /* translators: a heading weight — the same thickness as body text. */ 'heading-style.normal' => __('Upright', 'wconvert'), 'heading-style.italic' => __('Italic', 'wconvert'), 'bg-size.cover' => __('Fill the panel', 'wconvert'), 'bg-size.contain' => __('Show the whole image', 'wconvert'), 'bg-size.100% auto' => __('Fit the width', 'wconvert'), 'text-weight.400' => __('Regular', 'wconvert'), 'text-weight.600' => __('Semibold', 'wconvert'), 'text-weight.700' => __('Bold', 'wconvert'), 'text-weight.900' => __('Black', 'wconvert'), 'text-weight.950' => __('Extra black', 'wconvert'), 'heading-weight.400' => __('Regular', 'wconvert'), /* translators: a heading weight, between regular and bold. */ 'heading-weight.600' => __('Semibold', 'wconvert'), /* translators: a heading weight — the usual one for a heading. */ 'heading-weight.700' => __('Bold', 'wconvert'), /* translators: the heaviest heading weight offered. */ 'heading-weight.800' => __('Extra bold', 'wconvert'), /* translators: heading letters set closer together than normal. */ 'tracking.-0.02em' => _x('Tight', 'letter spacing', 'wconvert'), /* translators: heading letters at the typeface's own spacing. */ 'tracking.normal' => _x('Normal', 'letter spacing', 'wconvert'), /* translators: heading letters set further apart than normal. */ 'tracking.0.04em' => __('Loose', 'wconvert'), /* translators: heading letters set much further apart — the spaced-out look of a label or an eyebrow. */ 'tracking.0.1em' => __('Wide', 'wconvert'), /* translators: lines of text set close together. */ 'leading.1.3' => _x('Tight', 'line spacing', 'wconvert'), /* translators: the usual space between lines of text. */ 'leading.1.5' => _x('Normal', 'line spacing', 'wconvert'), /* translators: lines of text set further apart, which reads as airier. */ 'leading.1.7' => __('Airy', 'wconvert'), /* * **`0ms` is offered, and it is the honest way to turn animation * off.** A visitor who asked their system for reduced motion gets * none regardless — that is not the merchant's to decide — but a * merchant who wants a bar that simply appears should not have to * type a unit into a box to get one. */ /* translators: an animation speed — the design appears with no animation at all. */ 'motion.0ms' => _x('None', 'animation speed', 'wconvert'), /* translators: an animation speed. */ 'motion.120ms' => __('Fast', 'wconvert'), /* translators: an animation speed — the default. */ 'motion.200ms' => _x('Normal', 'animation speed', 'wconvert'), /* translators: an animation speed. */ 'motion.400ms' => __('Slow', 'wconvert'), /* * ==================================================================== * FOUR STRINGS, AND WHAT THEY BUY IS THAT NOBODY TYPES `box-shadow`. * ==================================================================== * `shadow` had no entry here and its value — four components and a * colour — is nothing {@see \WConvert\Template\ThemeTokens} or the * admin's shape tests can read, so the panel's last branch handed * every design a raw text box, permanently * ({@see docs/adr/0054-every-control-has-the-shape-of-its-value.md}). * * Named in the register `motion` and `leading` already use: a word * for the AMOUNT of the thing, never for the CSS. The third value is * the manifest's own fallback, so a design that declares no shadow * opens with *Normal* checked and the common case lands right. * * **Seven of the nine designs that declare a shadow declare one that * is not on this list** — three of them cast upward, because a bar * sits at the bottom of the viewport. They open on *Custom* with * their own value intact, which is what `choices` means. */ /* * **Every one of these carries a NEGATIVE spread except the * flattest, and that is the whole difference.** The four offered * before were plain blurs — `0 10px 40px rgba(0,0,0,.18)` — which * spread the blur outward from the full footprint and read as a * grey haze under the panel rather than as a panel lifted off the * page. Pulling the blur back inside the footprint is what makes a * shadow look like light, and it is the single cheapest thing that * separates this library from the specimens it was measured * against. * * They are also tinted toward the ink rather than pure black: * `rgba(15, 23, 42, …)` is the slate the rest of the library is * drawn in, and a neutral-black shadow under a coloured panel * reads as dirt. */ /* translators: a shadow depth — the design casts no shadow at all. */ 'shadow.none' => _x('None', 'shadow depth', 'wconvert'), /* translators: a shadow depth — barely there, the design sits almost flat on the page. */ 'shadow.0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.1)' => __('Flat', 'wconvert'), /* translators: a shadow depth — the design sits just above the page. */ 'shadow.0 4px 12px -2px rgba(15, 23, 42, 0.12), 0 12px 32px -8px rgba(15, 23, 42, 0.18)' => __('Raised', 'wconvert'), /* translators: a shadow depth — the default. The design lifts clearly off the page. */ 'shadow.0 18px 50px -12px rgba(15, 23, 42, 0.35)' => __('Lifted', 'wconvert'), /* translators: a shadow depth — the design floats well above the page behind it. */ 'shadow.0 32px 80px -16px rgba(15, 23, 42, 0.5)' => __('Floating', 'wconvert'), /* translators: the shape of the button that closes a popup — a circle. */ 'close-radius.50%' => __('Round', 'wconvert'), /* translators: the shape of the button that closes a popup — a square with softened corners. */ 'close-radius.0.375rem' => __('Rounded square', 'wconvert'), /* translators: the shape of the button that closes a popup — a square with sharp corners. */ 'close-radius.0' => _x('Square', 'shape of a close button', 'wconvert'), ]; } /** * What each FACET the picker filters by is called. * * ============================================================================ * THE THREE A MERCHANT COMPARING DESIGNS ACTUALLY USES. * ============================================================================ * Six facets are derived from a tree and three of them are offered as * controls ({@see TemplateFacets}). These are the names over those three * chip strips, and each one has to answer ADR 0042 rule 2 — *does knowing * this change what they do next?* — before it earns a line of the toolbar. * * Named for the QUESTION rather than for the key. `has_image` is *"Picture"* * because the strip under it holds one chip and a merchant reads the pair, * not the key. * * @return array */ public static function facets(): array { return [ /* translators: a filter over the design library — how a design is arranged. */ 'shape' => __('Shape', 'wconvert'), /* translators: a filter over the design library — what the design asks a visitor for. */ 'captures' => __('Asks for', 'wconvert'), /* translators: a filter over the design library — whether the design has an image in it. */ 'has_image' => __('Picture', 'wconvert'), ]; } /** * What each OFFERED FACET VALUE is called, keyed `"{facet}.{value}"`. * * ============================================================================ * TWO OF THE THREE BORROW WORDS THE ADMIN ALREADY SAYS, DELIBERATELY. * ============================================================================ * A `shape` chip and a row in the structure editor name the same layout, and * a `captures` chip and the ⇄ menu name the same field kind. Composing them * from {@see self::layouts()} and {@see self::fields()} is what makes that * true by construction: a merchant who filtered by *Side by side* and then * opened the design finds a block called *Side by side*, and a translator * has one string to get right rather than two that must agree. * * It also removes the failure the other spelling would have: two lists * whose keys are checked against the manifest separately can both pass * while saying different words for `split`. * * `has_image` is the exception because it has no vocabulary behind it. It is * a boolean, so it offers exactly one chip — the manifest declares `"true"` * and this names it — and *"With a picture"* is the phrase, because a chip * reading *"True"* is a chip nobody presses. * * @return array */ public static function facetValues(): array { $values = []; foreach (self::layouts() as $layout => $label) { $values['shape.' . $layout] = $label; } foreach (self::fields() as $field => $label) { $values['captures.' . $field] = $label; } /* translators: the one chip under the Picture filter — designs with an image in them. */ $values['has_image.true'] = __('With a picture', 'wconvert'); return $values; } /** * Every map at once, which is what the gallery route ships. * * One object rather than five routes: they are read together, once, by one * screen, and none of them is a fact about the install. * * @return array> */ public static function all(): array { return [ 'roles' => self::roles(), 'nodes' => self::nodes(), 'layouts' => self::layouts(), 'layoutNotes' => self::layoutNotes(), 'layoutHelp' => self::layoutHelp(), 'layoutParams' => self::layoutParams(), 'layoutParamValues' => self::layoutParamValues(), 'nodeParams' => self::nodeParams(), 'nodeParamValues' => self::nodeParamValues(), 'fields' => self::fields(), 'placeholders' => self::placeholders(), 'keys' => self::keys(), 'params' => self::params(), 'tokens' => self::tokens(), 'tokenValues' => self::tokenValues(), 'facets' => self::facets(), 'facetValues' => self::facetValues(), ]; } }