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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions classes/Guards/CapGuard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace tobimori\DreamForm\Guards;

use Kirby\Http\Remote;
use tobimori\DreamForm\DreamForm;
use tobimori\DreamForm\Models\SubmissionPage;

class CapGuard extends Guard
{
public const TYPE = 'cap';

public static function endpoint(): string|null
{
return DreamForm::option('guards.cap.endpoint');
}

protected static function secretKey(): string|null
{
return DreamForm::option('guards.cap.secretKey');
}

public function run(): void
{
$data = [
'secret' => static::secretKey(),
'response' => SubmissionPage::valueFromBody('cap-token')
];

$remote = Remote::post(DreamForm::option('guards.cap.endpoint') . 'siteverify', [
'data' => $data
]);
Comment on lines +30 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Build verification URL robustly and add timeout

Be resilient to missing/trailing slashes and set a reasonable timeout to avoid hanging requests.

-		$remote = Remote::post(DreamForm::option('guards.cap.endpoint') . 'siteverify', [
-			'data' => $data
-		]);
+		$endpoint = static::endpoint();
+		$verifyUrl = rtrim((string)$endpoint, '/') . '/siteverify';
+		$remote = Remote::post($verifyUrl, [
+			'data' => $data,
+			'timeout' => 5,
+		]);

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 PHPMD (2.15.0)

30-32: Avoid using static access to class '\Kirby\Http\Remote' in method 'run'. (Clean Code Rules)

(StaticAccess)


30-30: Avoid using static access to class '\tobimori\DreamForm\DreamForm' in method 'run'. (Clean Code Rules)

(StaticAccess)

🤖 Prompt for AI Agents
In classes/Guards/CapGuard.php around lines 30 to 32, the construction of the
verification URL does not handle missing or trailing slashes robustly and lacks
a timeout setting for the HTTP request. Fix this by ensuring the base URL and
endpoint are concatenated properly with exactly one slash between them,
regardless of trailing slashes. Also, add a timeout parameter to the
Remote::post call to prevent the request from hanging indefinitely.


$result = $remote->json();

if (
$result['success'] !== true
) {
$this->cancel(t('dreamform.submission.error.captcha'));
}
}
Comment on lines +34 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against network/JSON failures and undefined indexes

Remote::post/json() can fail or return non-array; accessing $result['success'] directly risks warnings. Handle HTTP errors and parse safely.

-		$result = $remote->json();
-
-		if (
-			$result['success'] !== true
-		) {
-			$this->cancel(t('dreamform.submission.error.captcha'));
-		}
+		$result = $remote->json();
+
+		$ok = method_exists($remote, 'code') ? ($remote->code() >= 200 && $remote->code() < 400) : true;
+		$success = is_array($result) ? ($result['success'] ?? false) === true : false;
+
+		if (!$ok || !$success) {
+			$this->cancel(t('dreamform.submission.error.captcha'));
+			return;
+		}
🤖 Prompt for AI Agents
In classes/Guards/CapGuard.php around lines 34 to 41, the code directly accesses
$result['success'] without checking if $result is a valid array or if the HTTP
request succeeded, which can cause warnings or errors. Modify the code to first
verify that the HTTP response was successful and that $result is a valid array
before accessing 'success'. Add error handling for network failures and JSON
parsing issues, and only call $this->cancel if the 'success' key exists and is
not true.


public static function hasSnippet(): bool
{
return true;
}

public static function isAvailable(): bool
{
return static::endpoint() !== null
&& static::secretKey() !== null;
}
}
6 changes: 6 additions & 0 deletions config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
'injectScript' => true,
'customTheme' => null // Custom theme configuration (Pro/Enterprise only)
],
'cap' => [
'endpoint' => null,
'secretKey' => null,
'injectScript' => true,
'useAssetServer' => false
],
'ratelimit' => [
'limit' => 10,
'interval' => 3
Expand Down
53 changes: 53 additions & 0 deletions docs/4_guards/2_cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Cap
description: Cap proof-of-work captcha integration
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Capitalize “CAPTCHA” for consistency with the rest of the doc

Use consistent capitalization across title/description/body.

-description: Cap proof-of-work captcha integration
+description: Cap proof-of-work CAPTCHA integration
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: Cap proof-of-work captcha integration
description: Cap proof-of-work CAPTCHA integration
🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md at line 3, the word "captcha" in the description
should be capitalized as "CAPTCHA" to maintain consistency with the rest of the
document. Update the description line to use "CAPTCHA" instead of "captcha".

---

DreamForm has built-in support for [Cap](https://capjs.js.org/), a lightweight, privacy-friendly CAPTCHA that uses SHA-256 proof-of-work instead of tracking or fingerprinting. It’s fast, self-hostable, and easy to integrate.

## Adding Cap

Follow the official Cap docs to deploy a Cap server (Node/Bun/Deno or standalone Docker) and create your keys. Then add the guard and required options to your `config.php`.

```php
// site/config/config.php

return [
'tobimori.dreamform' => [
'guards' => [
'available' => ['cap', /* other guards here */],
'cap' => [
// Base API endpoint of your Cap server (https://<instance_url>/<site_key>/)
'endpoint' => fn () => env('CAP_ENDPOINT'),
// Server secret used to verify the token server-to-server
'secretKey' => fn () => env('CAP_SECRET_KEY'),
// Automatically inject the widget script
'injectScript' => true,
// Load widget from your Cap server's asset endpoint instead of the CDN
'useAssetServer' => false,
],
Comment on lines +21 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Clarify endpoint format and script loading behavior

  • Endpoint: explicitly state it must include the site key and a trailing slash, e.g. https://example.com/abcd1234/. That avoids common misconfig issues when deriving other URLs.
  • Script loading: clarify precedence and behavior:
    • If injectScript = true and useAssetServer = true, the widget JS is loaded from the asset server derived from endpoint; otherwise from CDN.
    • If injectScript = false, users must include the widget script manually and ensure the inline snippet behavior (e.g., HTMX reset) is also included, if required.

Please confirm the actual runtime precedence used by CapGuard/snippet so the docs can match it precisely.


🏁 Script executed:

#!/bin/bash
# Search for injectScript configuration references and implementation
rg -n "injectScript" -A5 -B5

# Search for useAssetServer configuration references and implementation
rg -n "useAssetServer" -A5 -B5

Length of output: 10107


🏁 Script executed:

#!/bin/bash
# Display the full injection logic in the Cap guard snippet for precise behavior
rg -n "if (DreamForm::option('guards.cap.injectScript')" -A20 -B5 snippets/guards/cap.php

Length of output: 200


🏁 Script executed:

#!/bin/bash
# Show the full Cap guard snippet to confirm script injection logic
sed -n '1,200p' snippets/guards/cap.php

Length of output: 2034


Clarify CAP endpoint format and widget‐script loading
Please update docs/4_guards/2_cap.md to match the actual runtime logic you’ve verified:

  • Endpoint

    • Make it clear that this value must include your site key (and it’s safest to include a trailing slash), e.g.
      // Base API endpoint of your Cap server (including site key and trailing slash):
      //   https://example.com/abcd1234/
      'endpoint' => fn() => env('CAP_ENDPOINT'),
    • This full URL is passed into the <cap-widget> via data-cap-api-endpoint and used by the client library to build its own URLs.
  • injectScript (lines 24–25)

    • When true, the widget’s <script src="…"> is injected automatically; when false, nothing is injected and you must include both the <script> tag and the inline HTMX reset snippet yourself.
    • Default: true
    • Fallback (useAssetServer = false): loads from CDN:
      https://cdn.jsdelivr.net/npm/@cap.js/widget
  • useAssetServer (line 26)

    • Only takes effect when injectScript = true.
    • When true, the script is loaded from your Cap server’s asset endpoint at
      https://<host>[:port]/assets/widget.js
      
      (the code parses only the scheme/host/port from your endpoint and appends /assets/widget.js)
    • Default: false

Locations to update:

  • docs/4_guards/2_cap.md around lines 20–28 (sample config block and prose)
  • Table rows at lines 50–53 to reflect this exact precedence and the correct /assets/widget.js path
🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md around lines 20 to 28 and 50 to 53, update the
documentation to clarify that the 'endpoint' value must include the full URL
with the site key and a trailing slash, e.g., https://example.com/abcd1234/, as
this URL is passed to the <cap-widget> and used by the client library. For
'injectScript' (lines 24-25), specify that when true, the widget script is
injected automatically, and when false, the user must include both the script
tag and the HTMX reset snippet manually; note the default is true and fallback
loads from the CDN URL https://cdn.jsdelivr.net/npm/@cap.js/widget. For
'useAssetServer' (line 26), clarify it only applies when injectScript is true,
and when true, the script loads from the Cap server's asset endpoint at
https://<host>[:port]/assets/widget.js, derived from the scheme/host/port of the
endpoint; default is false. Also update the table rows at lines 50-53 to reflect
this precedence and the exact /assets/widget.js path.

],
],
];
```

Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Minor grammar polish: “e.g., …”

Add a comma after “e.g.” per style guides.

-Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
+Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g., using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g., using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
🧰 Tools
🪛 LanguageTool

[grammar] ~34-~34: Use commas correctly
Context: ...ou should not commit these keys to your repository, but instead load them from environment v...

(QB_NEW_EN_OTHER_ERROR_IDS_33)


[grammar] ~34-~34: Use a comma after introductory words or phrases
Context: ...d load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno...

(QB_NEW_EN_OTHER_ERROR_IDS_19)


[grammar] ~34-~34: Use correct spacing
Context: ...kirby3-dotenv), as shown in the example above. ## How it works - When a form includes the...

(QB_NEW_EN_OTHER_ERROR_IDS_5)

🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md at line 34, add a comma after "e.g." to correct the
grammar and follow style guides. Change "e.g using" to "e.g., using" to improve
readability and correctness.


## How it works

- When a form includes the `cap` guard, DreamForm renders a `<cap-widget>` element.
- The widget obtains a challenge from your Cap server and, after solving the PoW, writes a token into the form as `cap-token`.
- On submit, DreamForm verifies the token against your Cap server using the configured `endpoint` and `secretKey`.

Cap supports customization via CSS variables, among other features. See the official docs for details: [Cap documentation](https://capjs.js.org/guide/).

Note: If you use HTMX, the integration automatically resets the widget between swaps for a clean state.

## Options

| Option | Default | Accepts | Description |
| --- | --- | --- | --- |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Nit: remove the stray space inside parentheses

Small formatting tidy-up in the description cell.

-| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
+| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

50-50: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md at line 50, remove the extra space inside the
parentheses in the description cell so that the URL example reads
`https://<instance_url>/<site_key>/` without any leading or trailing spaces
inside the parentheses.

| tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. |
Comment on lines +48 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix table rendering (MD056) and a broken URL

  • The “Accepts” cells include a pipe character, which many linters/renderers interpret as a column separator. Escape the pipe.
  • Fix the triple slash in the asset URL.

These currently break table parsing and show an invalid URL.

-| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
-| tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
+| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
+| tobimori.dreamform.guards.cap.secretKey | `null` | `string\|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
-| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
+| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
-| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. |
+| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https://<instance_url>/widget.js` based on the configured endpoint. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Option | Default | Accepts | Description |
| --- | --- | --- | --- |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
| tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. |
| Option | Default | Accepts | Description |
| --- | --- | --- | --- |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
| tobimori.dreamform.guards.cap.secretKey | `null` | `string\|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https://<instance_url>/widget.js` based on the configured endpoint. |
🧰 Tools
🪛 LanguageTool

[grammar] ~51-~51: There might be a mistake here.
Context: ... to verify tokens with your Cap server. | | tobimori.dreamform.guards.cap.injectScr...

(QB_NEW_EN_OTHER)


[grammar] ~53-~53: There might be a mistake here.
Context: ...t.js` based on the configured endpoint. |

(QB_NEW_EN_OTHER)

🪛 markdownlint-cli2 (0.17.2)

50-50: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


51-51: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md around lines 48 to 53, the table's "Accepts" column
contains unescaped pipe characters that break markdown table rendering, and the
URL in the "useAssetServer" description has an incorrect triple slash. Fix this
by escaping the pipe characters in the "Accepts" cells (e.g., replace | with \|)
and correct the URL by changing the triple slash to a double slash to form a
valid URL.

4 changes: 3 additions & 1 deletion index.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
\tobimori\DreamForm\Guards\HCaptchaGuard::class,
\tobimori\DreamForm\Guards\TurnstileGuard::class,
\tobimori\DreamForm\Guards\RatelimitGuard::class,
\tobimori\DreamForm\Guards\AkismetGuard::class
\tobimori\DreamForm\Guards\AkismetGuard::class,
\tobimori\DreamForm\Guards\CapGuard::class
);

// register plugin
Expand Down Expand Up @@ -127,6 +128,7 @@
'dreamform/guards/honeypot' => __DIR__ . '/snippets/guards/honeypot.php',
'dreamform/guards/turnstile' => __DIR__ . '/snippets/guards/turnstile.php',
'dreamform/guards/hcaptcha' => __DIR__ . '/snippets/guards/hcaptcha.php',
'dreamform/guards/cap' => __DIR__ . '/snippets/guards/cap.php'
],
// get all files from /translations and register them as language files
'translations' => A::keyBy(
Expand Down
49 changes: 49 additions & 0 deletions snippets/guards/cap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/**
* @var tobimori\DreamForm\Models\FormPage $form
* @var tobimori\DreamForm\Guards\CapGuard $guard
*/

use tobimori\DreamForm\DreamForm;
use tobimori\DreamForm\Support\Htmx;

$attrs = [
'id' => 'cap',
'data-cap-api-endpoint' => $guard::endpoint(),
'data-cap-i18n-verifying-label' => t('dreamform.guards.cap.i18n.verifying'),
'data-cap-i18n-initial-state' => t('dreamform.guards.cap.i18n.initial'),
'data-cap-i18n-solved-label' => t('dreamform.guards.cap.i18n.solved'),
'data-cap-i18n-error-label' => t('dreamform.guards.cap.i18n.error')
];

echo '<cap-widget ' . attr($attrs) . '></cap-widget>';

if (DreamForm::option('guards.cap.injectScript')) :
$scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/widget';
if (DreamForm::option('guards.cap.useAssetServer')) {
$endpoint = $guard::endpoint();
if (is_string($endpoint) && $endpoint !== '') {
$parts = parse_url($endpoint);
if ($parts && isset($parts['scheme'], $parts['host'])) {
$serverUrl = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '');
$scriptSrc = rtrim($serverUrl, '/') . '/assets/widget.js';
}
}
}
?>
<script src="<?= $scriptSrc ?>"></script>
<?php if (Htmx::isActive()) : ?>
<script>
if (typeof htmx !== 'undefined') {
htmx.on("htmx:beforeSwap", () => {
const el = document.querySelector("cap-widget");
if (el) {
const newEl = el.cloneNode(true);
el.parentNode.replaceChild(newEl, el);
}
});
}
</script>
<?php endif; ?>
<?php endif; ?>
4 changes: 4 additions & 0 deletions translations/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"forms": "Formuláře",
"forms.empty": "Žádné formuláře",
"fromField": "Z uživatelského vstupu",
"guards.cap.i18n.verifying": "Ověřování...",
"guards.cap.i18n.initial": "Jsem člověk",
"guards.cap.i18n.solved": "Jsem člověk",
"guards.cap.i18n.error": "Chyba",
Comment on lines +185 to +188
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Czech CAP i18n keys added — minor UX nit

Keys are consistent. Consider using a distinct “solved” string (e.g., “Ověřeno”) to make state changes clearer for screen readers, instead of repeating “Jsem člověk”.

🤖 Prompt for AI Agents
In translations/cs.json around lines 185 to 188, the "guards.cap.i18n.solved"
key currently uses the same string as "guards.cap.i18n.initial" ("Jsem člověk"),
which can confuse screen readers. Update the "guards.cap.i18n.solved" value to a
distinct string like "Ověřeno" to clearly indicate the solved state and improve
accessibility.

"justNow": "právě teď",
"license.activate": "Aktivovat licenci",
"license.activate.domain": "Vaše licence bude aktivována pro doménu <strong>{{ domain }}</strong>.",
Expand Down
4 changes: 4 additions & 0 deletions translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"forms": "Formulare",
"forms.empty": "Keine Formulare",
"fromField": "Aus Eingabefeld",
"guards.cap.i18n.verifying": "Verifizieren...",
"guards.cap.i18n.initial": "Ich bin ein Mensch",
"guards.cap.i18n.solved": "Ich bin ein Mensch",
"guards.cap.i18n.error": "Fehler",
Comment on lines +185 to +188
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

German CAP i18n keys added — minor UX nit

All good. You may want a distinct solved label (e.g., “Verifiziert”) rather than repeating “Ich bin ein Mensch” to improve a11y clarity.

🤖 Prompt for AI Agents
In translations/de.json around lines 185 to 188, the "guards.cap.i18n.solved"
key currently duplicates the "Ich bin ein Mensch" label used for
"guards.cap.i18n.initial". To improve accessibility clarity, change the
"guards.cap.i18n.solved" value to a distinct label such as "Verifiziert" to
clearly differentiate the solved state from the initial state.

"justNow": "gerade eben",
"license.activate": "Lizenz aktivieren",
"license.activate.domain": "Deine Lizenz wird für die Domain <strong>{{ domain }}</strong> aktiviert.",
Expand Down
4 changes: 4 additions & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"forms": "Forms",
"forms.empty": "No Forms",
"fromField": "From User Input",
"guards.cap.i18n.verifying": "Verifying...",
"guards.cap.i18n.initial": "I'm a human",
"guards.cap.i18n.solved": "I'm a human",
"guards.cap.i18n.error": "Error",
Comment on lines +185 to +188
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

English CAP i18n keys added — minor UX nit

LGTM. Consider changing the solved label to “Verified” (or similar) so the state change is obvious to screen readers and visual users.

🤖 Prompt for AI Agents
In translations/en.json around lines 185 to 188, the "guards.cap.i18n.solved"
key currently has the value "I'm a human," which is not clear for indicating the
solved state. Change the value of "guards.cap.i18n.solved" to "Verified" or a
similar term that clearly communicates the state change to both screen readers
and visual users.

"justNow": "just now",
"license.activate": "Activate license",
"license.activate.domain": "Your license will be activated for the domain <strong>{{ domain }}</strong>.",
Expand Down
4 changes: 4 additions & 0 deletions translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"forms": "Formularios",
"forms.empty": "No hay formularios",
"fromField": "De entrada de usuario",
"guards.cap.i18n.verifying": "Verificando...",
"guards.cap.i18n.initial": "Soy un humano",
"guards.cap.i18n.solved": "Soy un humano",
"guards.cap.i18n.error": "Error",
Comment on lines +185 to +188
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Spanish CAP i18n keys added — minor UX nit

Looks good. Optionally differentiate the solved label (e.g., “Verificado”) instead of repeating “Soy un humano” to better convey state change for assistive tech.

🤖 Prompt for AI Agents
In translations/es.json around lines 185 to 188, the "guards.cap.i18n.solved"
key repeats the label "Soy un humano" which is the same as
"guards.cap.i18n.initial". To improve UX and assistive technology clarity,
change the "solved" label to a distinct term like "Verificado" to clearly
indicate the state change.

"justNow": "justo ahora",
"license.activate": "Activar licencia",
"license.activate.domain": "Tu licencia será activada para el dominio <strong>{{ domain }}</strong>.",
Expand Down
4 changes: 4 additions & 0 deletions translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"forms": "Formulaires",
"forms.empty": "Aucun formulaire",
"fromField": "De l'entrée utilisateur",
"guards.cap.i18n.verifying": "Vérification...",
"guards.cap.i18n.initial": "Je suis un humain",
"guards.cap.i18n.solved": "Je suis un humain",
"guards.cap.i18n.error": "Erreur",
"justNow": "à l'instant",
"license.activate": "Activer la licence",
"license.activate.domain": "Votre licence sera activée pour le domaine <strong>{{ domain }}</strong>.",
Expand Down
4 changes: 4 additions & 0 deletions translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"forms": "Moduli",
"forms.empty": "Nessun Modulo",
"fromField": "Da input utente",
"guards.cap.i18n.verifying": "Verifica...",
"guards.cap.i18n.initial": "Sono un umano",
"guards.cap.i18n.solved": "Sono un umano",
"guards.cap.i18n.error": "Errore",
"justNow": "proprio ora",
"license.activate": "Attiva licenza",
"license.activate.domain": "La tua licenza sarà attivata per il dominio <strong>{{ domain }}</strong>.",
Expand Down
4 changes: 4 additions & 0 deletions translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"forms": "Formulieren",
"forms.empty": "Geen formulieren",
"fromField": "Veld uit formulier",
"guards.cap.i18n.verifying": "Verifiëren...",
"guards.cap.i18n.initial": "Ik ben een mens",
"guards.cap.i18n.solved": "Ik ben een mens",
"guards.cap.i18n.error": "Fout",
"justNow": "zojuist",
"license.activate": "Activeer licentie",
"license.activate.domain": "Je licentie zal worden geactiveerd voor domein <strong>{{ domain }}</strong>.",
Expand Down
Loading