Skip to content

Commit 4356085

Browse files
committed
add i18n support
1 parent 990d13d commit 4356085

8 files changed

Lines changed: 203 additions & 20 deletions

File tree

TODO.md

Lines changed: 0 additions & 2 deletions
This file was deleted.

demo/localized/index.html

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Cap demo</title>
7+
8+
<style>
9+
body {
10+
padding: 0px;
11+
display: flex;
12+
align-items: center;
13+
justify-content: center;
14+
min-height: 100vh;
15+
margin: 0px;
16+
flex-direction: column;
17+
}
18+
19+
button {
20+
margin-top: 10px;
21+
font-family: system, -apple-system, "BlinkMacSystemFont",
22+
".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI",
23+
"Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
24+
background-color: rgba(20, 20, 25);
25+
border: 1px solid rgba(60, 60, 70);
26+
border-radius: 12px;
27+
color: #ffffff;
28+
cursor: pointer;
29+
font-size: 14px;
30+
padding: 12px 16px;
31+
text-align: center;
32+
transition: transform 0.2s, background-color 0.2s, border-color 0.2s;
33+
width: fit-content;
34+
}
35+
36+
button:focus {
37+
outline: 0;
38+
}
39+
40+
button:hover {
41+
background-color: rgba(40, 40, 40);
42+
border: 1px solid rgba(100, 100, 120);
43+
}
44+
45+
button:active {
46+
transform: scale(0.98);
47+
}
48+
.source {
49+
color: black;
50+
font-family: system, -apple-system, "BlinkMacSystemFont",
51+
".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI",
52+
"Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
53+
margin-top: 10px;
54+
font-size: 13px;
55+
}
56+
</style>
57+
</head>
58+
<body>
59+
<cap-widget
60+
id="cap"
61+
onsolve="console.log(`Token: ${event.detail.token}`)"
62+
data-cap-api-endpoint="/api/"
63+
data-cap-hidden-field-name="test-endpoint"
64+
data-cap-i18n-verifying-label="Verificando..."
65+
data-cap-i18n-initial-state="Eu sou um humano"
66+
data-cap-i18n-solved-label="É um humano"
67+
data-cap-i18n-error-label="Erro!"
68+
></cap-widget>
69+
70+
<cap-widget
71+
id="floating"
72+
onsolve="console.log(`Token: ${event.detail.token}`)"
73+
data-cap-api-endpoint="/api/"
74+
></cap-widget>
75+
76+
<button data-cap-floating="#floating" data-cap-floating-position="bottom">
77+
Trigger floating mode
78+
</button>
79+
80+
<a
81+
href="https://github.com/tiagorangel1/cap/tree/main/demo"
82+
target="_blank"
83+
class="source"
84+
>Source code</a
85+
>
86+
</body>
87+
88+
<!--
89+
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget"></script>
90+
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget/cap-floating.min.js"></script>
91+
-->
92+
93+
<script src="/cap.js"></script>
94+
<script src="/cap-floating.js"></script>
95+
</html>

demo/localized/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "cap-demo",
3+
"description": "Cap demo",
4+
"license": "Apache-2.0",
5+
"author": "Tiago Rangel",
6+
"type": "commonjs",
7+
"main": "server.js",
8+
"scripts": {
9+
"start": "bun run server.js",
10+
"dev": "bun run --watch server.js",
11+
"test": "echo \"Error: no test specified\" && exit 1"
12+
},
13+
"dependencies": {
14+
"@cap.js/server": "^1.0.10",
15+
"fastify": "^5.3.2"
16+
}
17+
}

demo/localized/server.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import Cap from "@cap.js/server";
4+
import Fastify from "fastify";
5+
6+
const fastify = Fastify();
7+
const cap = new Cap({
8+
tokens_store_path: ".data/tokensList.json",
9+
});
10+
11+
fastify.get("/", (req, res) => {
12+
res.header("Content-Type", "text/html");
13+
res.send(fs.createReadStream(path.join(__dirname, "index.html")));
14+
});
15+
16+
fastify.get("/cap.js", (req, res) => {
17+
res.header("Content-Type", "application/javascript");
18+
res.send(
19+
fs.createReadStream(path.join(__dirname, "../../widget/src/src/cap.js"))
20+
);
21+
});
22+
23+
fastify.get("/cap-floating.js", (req, res) => {
24+
res.header("Content-Type", "application/javascript");
25+
res.send(
26+
fs.createReadStream(
27+
path.join(__dirname, "../../widget/src/src/cap-floating.js")
28+
)
29+
);
30+
});
31+
32+
fastify.post("/api/challenge", (req, res) => {
33+
res.send(cap.createChallenge());
34+
});
35+
36+
fastify.post("/api/redeem", async (req, res) => {
37+
const { token, solutions } = req.body;
38+
if (!token || !solutions) {
39+
return res.code(400).send({ success: false });
40+
}
41+
42+
const answer = await cap.redeemChallenge({ token, solutions });
43+
44+
res.send(answer);
45+
46+
console.log("new challenge redeemed", {
47+
...answer,
48+
isValid: (await cap.validateToken(answer.token)).success,
49+
});
50+
});
51+
52+
fastify.listen({ port: 3000, host: "0.0.0.0" }).then(() => {
53+
console.log("Server is running on http://localhost:3000");
54+
});

docs/guide/widget.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,18 @@
1414
```html [unpkg]
1515
<script src="https://unpkg.com/@cap.js/widget"></script>
1616
```
17+
1718
:::
1819

1920
You can now use the `<cap-widget>` component in your HTML.
2021

2122
```html
22-
<cap-widget
23-
id="cap"
24-
data-cap-api-endpoint="<your cap api endpoint>"
25-
></cap-widget>
23+
<cap-widget id="cap" data-cap-api-endpoint="<your cap api endpoint>"></cap-widget>
2624
```
2725

28-
> [!NOTE]
29-
> You'll need to start a server with the Cap API running at the same URL as specified in the `data-cap-api-endpoint` attribute.
30-
> In the server-side example we provided, it's set to `/api`, but you can change this by replacing every `app.post('/api/...', ...)` to `app.post('/<endpoint>/...', ...)`.
26+
> [!NOTE] You'll need to start a server with the Cap API running at the same URL as specified in the `data-cap-api-endpoint` attribute. In the server-side example we provided, it's set to `/api`, but you can change this by replacing every `app.post('/api/...', ...)` to `app.post('/<endpoint>/...', ...)`.
3127
32-
> [!TIP]
33-
> The following attributes are supported:
28+
> [!TIP] The following attributes are supported:
3429
>
3530
> - `data-cap-api-endpoint`: API endpoint (required)
3631
> - `data-cap-worker-count`: Number of workers to use (defaults to `navigator.hardwareConcurrency || 8`)
@@ -67,12 +62,29 @@ The following custom events are supported:
6762
- `reset`: Triggered when the widget is reset.
6863
- `progress`: Triggered when the there's a progress update while in verification.
6964

65+
## i18n
66+
67+
You can change the text on each label of the widget by setting the `data-cap-i18n-*` attribute, like this:
68+
69+
```html
70+
<cap-widget
71+
id="cap"
72+
data-cap-api-endpoint="<your cap api endpoint>"
73+
74+
data-cap-i18n-verifying-label="Verifying..."
75+
data-cap-i18n-initial-state="I'm a human"
76+
data-cap-i18n-solved-label="I'm a human"
77+
data-cap-i18n-error-label="Error"
78+
></cap-widget>
79+
```
80+
7081
## Custom fetch
82+
7183
You can override the default fetch implementation by setting `window.CAP_CUSTOM_FETCH` to a custom function. This function will receive the URL and options as arguments and should return a promise that resolves to the response.
7284

7385
```js
7486
window.CAP_CUSTOM_FETCH = function (url, options) {
7587
// Custom fetch implementation
7688
return fetch(url, options);
7789
};
78-
```
90+
```

widget/src/cap.compat.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

widget/src/cap.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

widget/src/src/cap.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
#solving = false;
2121
#eventHandlers;
2222

23+
getI18nText(key, defaultValue) {
24+
return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue;
25+
}
26+
2327
static get observedAttributes() {
2428
return [
2529
"onsolve",
@@ -102,7 +106,7 @@
102106

103107
try {
104108
this.#solving = true;
105-
this.updateUI("verifying", "Verifying...", true);
109+
this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true);
106110

107111
this.dispatchEvent("progress", { progress: 0 });
108112

@@ -226,7 +230,7 @@
226230
this.#div.setAttribute("role", "button");
227231
this.#div.setAttribute("tabindex", "0");
228232
this.#div.setAttribute("disabled", "true");
229-
this.#div.innerHTML = `<div class="checkbox"></div><p>I'm a human</p><a href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener"><span>Secured by&nbsp;</span>Cap</a>`;
233+
this.#div.innerHTML = `<div class="checkbox"></div><p>${this.getI18nText("initial-state", "I'm a human")}</p><a href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener"><span>Secured by&nbsp;</span>Cap</a>`;
230234
this.#shadow.innerHTML = `<style>.captcha{background-color:var(--cap-background);border:1px solid var(--cap-border-color);border-radius:var(--cap-border-radius);width:var(--cap-widget-width);display:flex;align-items:center;padding:var(--cap-widget-padding);gap:var(--cap-gap);cursor:pointer;transition:filter var(--cap-transition-duration),transform var(--cap-transition-duration);position:relative;-webkit-tap-highlight-color:rgba(255,255,255,0);overflow:hidden;color:var(--cap-color)}.captcha:hover{filter:var(--cap-hover-filter)}.captcha:not([disabled]):active{transform:scale(var(--cap-active-scale))}.checkbox{width:var(--cap-checkbox-size);height:var(--cap-checkbox-size);border:var(--cap-checkbox-border);border-radius:var(--cap-checkbox-border-radius);background-color:var(--cap-checkbox-background);transition:opacity var(--cap-transition-duration);margin-top:var(--cap-checkbox-margin);margin-bottom:var(--cap-checkbox-margin)}.captcha *{font-family:var(--cap-font)}.captcha p{margin:0;font-weight:500;font-size:15px;user-select:none;transition:opacity var(--cap-transition-duration)}.captcha[data-state=verifying] .checkbox{background: none;display:flex;align-items:center;justify-content:center;transform: scale(1.1);border: none;border-radius: 50%;background: conic-gradient(var(--cap-spinner-color) 0%, var(--cap-spinner-color) var(--progress, 0%), var(--cap-spinner-background-color) var(--progress, 0%), var(--cap-spinner-background-color) 100%);position: relative;}.captcha[data-state=verifying] .checkbox::after {content: "";background-color: var(--cap-background);width: calc(100% - var(--cap-spinner-thickness));height: calc(100% - var(--cap-spinner-thickness));border-radius: 50%;margin:calc(var(--cap-spinner-thickness) / 2)}.captcha[data-state=done] .checkbox{border:1px solid transparent;background-image:var(--cap-checkmark);background-size:cover}.captcha[data-state=error] .checkbox{border:1px solid transparent;background-image:var(--cap-error-cross);background-size:cover}.captcha[disabled]{
231235
cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{position:absolute;bottom:10px;right:10px;font-size:var(--cap-credits-font-size);color:var(--cap-color);opacity:var(--cap-opacity-hover)}.captcha .credits span{display:none;text-decoration:underline}.captcha .credits:hover span{display:inline-block}</style>`;
232236

@@ -276,23 +280,26 @@
276280
this.#div
277281
.querySelector(".checkbox")
278282
.style.setProperty("--progress", `${event.detail.progress}%`);
279-
progressElement.innerText = `Verifying... ${event.detail.progress}%`;
283+
progressElement.innerText = `${this.getI18nText(
284+
"verifying-label",
285+
"Verifying..."
286+
)} ${event.detail.progress}%`;
280287
}
281288
this.executeAttributeCode("onprogress", event);
282289
}
283290

284291
handleSolve(event) {
285-
this.updateUI("done", "You're a human", true);
292+
this.updateUI("done", this.getI18nText("solved-label", "You're a human"), true);
286293
this.executeAttributeCode("onsolve", event);
287294
}
288295

289296
handleError(event) {
290-
this.updateUI("error", "Error. Try again.");
297+
this.updateUI("error", this.getI18nText("error-label", "Error. Try again."));
291298
this.executeAttributeCode("onerror", event);
292299
}
293300

294301
handleReset(event) {
295-
this.updateUI("", "I'm a human");
302+
this.updateUI("", this.getI18nText("initial-state", "I'm a human"));
296303
this.executeAttributeCode("onreset", event);
297304
}
298305

0 commit comments

Comments
 (0)